Java并發(fā)多線程高頻面試題

并發(fā)知識(shí)不管在學(xué)習(xí)、面試還是工作過程中都非常非常重要冒萄,看完本文臊岸,相信絕對(duì)能助你一臂之力。

1尊流、線程和進(jìn)程有什么區(qū)別帅戒?

線程是進(jìn)程的子集,一個(gè)進(jìn)程可以有很多線程崖技。每個(gè)進(jìn)程都有自己的內(nèi)存空間逻住,可執(zhí)行代碼和唯一進(jìn)程標(biāo)識(shí)符(PID)钟哥。

每條線程并行執(zhí)行不同的任務(wù)。不同的進(jìn)程使用不同的內(nèi)存空間(線程自己的堆棧)瞎访,而所有的線程共享一片相同的內(nèi)存空間(進(jìn)程主內(nèi)存)腻贰。別把它和棧內(nèi)存搞混,每個(gè)線程都擁有單獨(dú)的棧內(nèi)存用來存儲(chǔ)本地?cái)?shù)據(jù)装诡。

2银受、實(shí)現(xiàn)多線程的方式有哪些践盼?

  • 繼承Thread類:Java單繼承鸦采,不推薦;
  • 實(shí)現(xiàn)Runnable接口:Thread類也是繼承Runnable接口咕幻,推薦渔伯;
  • 實(shí)現(xiàn)Callable接口:實(shí)現(xiàn)Callable接口,配合FutureTask使用肄程,有返回值锣吼;
  • 使用線程池:復(fù)用,節(jié)約資源蓝厌;
  • 更多方式可以參考我的文章使用Java Executor框架實(shí)現(xiàn)多線程

3玄叠、用Runnable還是Thread?

這個(gè)問題是上題的后續(xù)拓提,大家都知道我們可以通過繼承Thread類或者調(diào)用Runnable接口來實(shí)現(xiàn)線程读恃,問題是,那個(gè)方法更好呢代态?什么情況下使用它寺惫?這個(gè)問題很容易回答,如果你知道Java不支持類的多重繼承蹦疑,但允許你調(diào)用多個(gè)接口。所以如果你要繼承其他類,當(dāng)然是調(diào)用Runnable接口好了谋减。

  • Runnable和Thread兩者最大的區(qū)別是Thread是類而Runnable是接口激蹲,至于用類還是用接口,取決于繼承上的實(shí)際需要叁温。Java類是單繼承的再悼,實(shí)現(xiàn)多個(gè)接口可以實(shí)現(xiàn)類似多繼承的操作。
  • 其次券盅, Runnable就相當(dāng)于一個(gè)作業(yè)帮哈,而Thread才是真正的處理線程,我們需要的只是定義這個(gè)作業(yè)锰镀,然后將作業(yè)交給線程去處理娘侍,這樣就達(dá)到了松耦合咖刃,也符合面向?qū)ο罄锩娼M合的使用,另外也節(jié)省了函數(shù)開銷憾筏,繼承Thread的同時(shí)嚎杨,不僅擁有了作業(yè)的方法run(),還繼承了其他所有的方法氧腰。
  • 當(dāng)需要?jiǎng)?chuàng)建大量線程的時(shí)候枫浙,有以下不足:①線程生命周期的開銷非常高;②資源消耗古拴;③穩(wěn)定性箩帚。
  • 如果二者都可以選擇不用,那就不用黄痪。因?yàn)镴ava這門語言發(fā)展到今天紧帕,在語言層面提供的多線程機(jī)制已經(jīng)比較豐富且高級(jí),完全不用在線程層面操作桅打。直接使用Thread和Runnable這樣的“裸線程”元素比較容易出錯(cuò)是嗜,還需要額外關(guān)注線程數(shù)等問題。建議:簡(jiǎn)單的多線程程序挺尾,使用Executor鹅搪。復(fù)雜的多線程程序,使用一個(gè)Actor庫遭铺,首推Akka丽柿。
  • 如果一定要在Runnable和Thread中選擇一個(gè)使用,選擇Runnable掂僵。

4航厚、Thread 類中的start() 和 run() 方法有什么區(qū)別?

這個(gè)問題經(jīng)常被問到锰蓬,但還是能從此區(qū)分出面試者對(duì)Java線程模型的理解程度幔睬。start()方法被用來啟動(dòng)新創(chuàng)建的線程,而且start()內(nèi)部調(diào)用了run()方法芹扭,JDK 1.8源碼中start方法的注釋這樣寫到:Causes this thread to begin execution; the Java Virtual Machine calls the <code>run</code> method of this thread.這和直接調(diào)用run()方法的效果不一樣麻顶。當(dāng)你調(diào)用run()方法的時(shí)候,只會(huì)是在原來的線程中調(diào)用舱卡,沒有新的線程啟動(dòng)辅肾,start()方法才會(huì)啟動(dòng)新線程,JDK 1.8源碼中注釋這樣寫:The result is that two threads are running concurrently: the current thread (which returns from the call to the <code>start</code> method) and the other thread (which executes its <code>run</code> method).轮锥。

new 一個(gè) Thread矫钓,線程進(jìn)入了新建狀態(tài);調(diào)用 start() 方法,會(huì)啟動(dòng)一個(gè)線程并使線程進(jìn)入了就緒狀態(tài),當(dāng)分配到時(shí)間片后就可以開始運(yùn)行了新娜。start() 會(huì)執(zhí)行線程的相應(yīng)準(zhǔn)備工作赵辕,然后自動(dòng)執(zhí)行 run() 方法的內(nèi)容,這是真正的多線程工作概龄。而直接執(zhí)行 run() 方法还惠,會(huì)把 run 方法當(dāng)成一個(gè) main 線程下的普通方法去執(zhí)行,并不會(huì)在某個(gè)線程中執(zhí)行它私杜,所以這并不是多線程工作蚕键。

總結(jié):調(diào)用 start 方法方可啟動(dòng)線程并使線程進(jìn)入就緒狀態(tài),而 run 方法只是 thread 的一個(gè)普通方法調(diào)用衰粹,還是在主線程里執(zhí)行锣光。

5、說說 sleep() 方法和 wait() 方法區(qū)別和共同點(diǎn)?

  • 兩者最主要的區(qū)別在于:sleep 方法沒有釋放鎖寄猩,而 wait 方法釋放了鎖 嫉晶。
  • 兩者都可以暫停線程的執(zhí)行。
  • Wait 通常被用于線程間交互/通信田篇,sleep 通常被用于暫停執(zhí)行。
  • wait() 方法被調(diào)用后箍铭,線程不會(huì)自動(dòng)蘇醒泊柬,需要?jiǎng)e的線程調(diào)用同一個(gè)對(duì)象上的 notify() 或者 notifyAll() 方法。sleep() 方法執(zhí)行完成后诈火,線程會(huì)自動(dòng)蘇醒兽赁。

6、說說并發(fā)與并行的區(qū)別?

  • 并發(fā): 同一時(shí)間段冷守,多個(gè)任務(wù)都在執(zhí)行 (單位時(shí)間內(nèi)不一定同時(shí)執(zhí)行)刀崖;
  • 并行: 單位時(shí)間內(nèi),多個(gè)任務(wù)同時(shí)執(zhí)行拍摇。

7亮钦、說說線程的生命周期和狀態(tài)?

Java 線程在運(yùn)行的生命周期中的指定時(shí)刻只可能處于下面 6 種不同狀態(tài)的其中一個(gè)狀態(tài)(圖源《Java 并發(fā)編程藝術(shù)》4.1.4 節(jié))。

線程在生命周期中并不是固定處于某一個(gè)狀態(tài)而是隨著代碼的執(zhí)行在不同狀態(tài)之間切換充活。Java 線程狀態(tài)變遷如下圖所示(圖源《Java 并發(fā)編程藝術(shù)》4.1.4 節(jié)):

由上圖可以看出:線程創(chuàng)建之后它將處于 NEW(新建) 狀態(tài)蜂莉,調(diào)用 start() 方法后開始運(yùn)行,線程這時(shí)候處于 READY(可運(yùn)行) 狀態(tài)混卵∮乘耄可運(yùn)行狀態(tài)的線程獲得了 CPU 時(shí)間片(timeslice)后就處于 RUNNING(運(yùn)行) 狀態(tài)。

操作系統(tǒng)隱藏 Java 虛擬機(jī)(JVM)中的 RUNNABLE 和 RUNNING 狀態(tài)幕随,它只能看到 RUNNABLE 狀態(tài)(圖源:HowToDoInJava:Java Thread Life Cycle and Thread States)蚁滋,所以 Java 系統(tǒng)一般將這兩個(gè)狀態(tài)統(tǒng)稱為 RUNNABLE(運(yùn)行中) 狀態(tài) 。

當(dāng)線程執(zhí)行 wait()方法之后,線程進(jìn)入 WAITING(等待)狀態(tài)辕录。進(jìn)入等待狀態(tài)的線程需要依靠其他線程的通知才能夠返回到運(yùn)行狀態(tài)澄阳,而 TIME_WAITING(超時(shí)等待) 狀態(tài)相當(dāng)于在等待狀態(tài)的基礎(chǔ)上增加了超時(shí)限制,比如通過 sleep(long millis)方法或 wait(long millis)方法可以將 Java 線程置于 TIMED WAITING 狀態(tài)踏拜。當(dāng)超時(shí)時(shí)間到達(dá)后 Java 線程將會(huì)返回到 RUNNABLE 狀態(tài)碎赢。當(dāng)線程調(diào)用同步方法時(shí),在沒有獲取到鎖的情況下速梗,線程將會(huì)進(jìn)入到 BLOCKED(阻塞) 狀態(tài)肮塞。線程在執(zhí)行 Runnable 的run()方法之后將會(huì)進(jìn)入到 TERMINATED(終止) 狀態(tài)。

8姻锁、什么是線程死鎖?

多個(gè)線程同時(shí)被阻塞枕赵,它們中的一個(gè)或者全部都在等待某個(gè)資源被釋放。由于線程被無限期地阻塞位隶,因此程序不可能正常終止拷窜。

如下圖所示,線程 A 持有資源 2涧黄,線程 B 持有資源 1篮昧,他們同時(shí)都想申請(qǐng)對(duì)方的資源,所以這兩個(gè)線程就會(huì)互相等待而進(jìn)入死鎖狀態(tài)笋妥。

下面通過一個(gè)例子來說明線程死鎖,代碼模擬了上圖的死鎖的情況 (代碼來源于《并發(fā)編程之美》):

public class DeadLockDemo {
    private static Object resource1 = new Object();//資源 1
    private static Object resource2 = new Object();//資源 2
?
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "線程 1").start();
?
        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "線程 2").start();
    }
}

輸出:

Thread[線程 1,5,main]get resource1
Thread[線程 2,5,main]get resource2
Thread[線程 1,5,main]waiting get resource2
Thread[線程 2,5,main]waiting get resource1

線程 A 通過 synchronized (resource1) 獲得 resource1 的監(jiān)視器鎖懊昨,然后通過 Thread.sleep(1000);讓線程 A 休眠 1s 為的是讓線程 B 得到執(zhí)行然后獲取到 resource2 的監(jiān)視器鎖。線程 A 和線程 B 休眠結(jié)束了都開始企圖請(qǐng)求獲取對(duì)方的資源春宣,然后這兩個(gè)線程就會(huì)陷入互相等待的狀態(tài)酵颁,這也就產(chǎn)生了死鎖。上面的例子符合產(chǎn)生死鎖的四個(gè)必要條件月帝。

學(xué)過操作系統(tǒng)的朋友都知道產(chǎn)生死鎖必須具備以下四個(gè)條件:

  1. 互斥條件:該資源任意一個(gè)時(shí)刻只由一個(gè)線程占用躏惋。
  2. 請(qǐng)求與保持條件:一個(gè)進(jìn)程因請(qǐng)求資源而阻塞時(shí),對(duì)已獲得的資源保持不放嚷辅。
  3. 不剝奪條件:線程已獲得的資源在末使用完之前不能被其他線程強(qiáng)行剝奪簿姨,只有自己使用完畢后才釋放資源。
  4. 循環(huán)等待條件:若干進(jìn)程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系潦蝇。

9款熬、如何避免線程死鎖?

我們只要破壞產(chǎn)生死鎖的四個(gè)條件中的其中一個(gè)就可以了。

  • 破壞互斥條件:這個(gè)條件我們沒有辦法破壞攘乒,因?yàn)槲覀冇面i本來就是想讓他們互斥的(臨界資源需要互斥訪問)贤牛。
  • 破壞請(qǐng)求與保持條件:一次性申請(qǐng)所有的資源。
  • 破壞不剝奪條件:占用部分資源的線程進(jìn)一步申請(qǐng)其他資源時(shí)则酝,如果申請(qǐng)不到殉簸,可以主動(dòng)釋放它占有的資源闰集。
  • 破壞循環(huán)等待條件:靠按序申請(qǐng)資源來預(yù)防。按某一順序申請(qǐng)資源般卑,釋放資源則反序釋放武鲁。破壞循環(huán)等待條件。

我們對(duì)線程 2 的代碼修改成下面這樣就不會(huì)產(chǎn)生死鎖了蝠检。

new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "線程 2").start();

輸出:

Thread[線程 1,5,main]get resource1
Thread[線程 1,5,main]waiting get resource2
Thread[線程 1,5,main]get resource2
Thread[線程 2,5,main]get resource1
Thread[線程 2,5,main]waiting get resource2
Thread[線程 2,5,main]get resource2
?
Process finished with exit code 0

我們分析一下上面的代碼為什么避免了死鎖的發(fā)生?

線程 1 首先獲得到 resource1 的監(jiān)視器鎖,這時(shí)候線程 2 就獲取不到了沐鼠。然后線程 1 再去獲取 resource2 的監(jiān)視器鎖,可以獲取到叹谁。然后線程 1 釋放了對(duì) resource1饲梭、resource2 的監(jiān)視器鎖的占用,線程 2 獲取到就可以執(zhí)行了焰檩。這樣就破壞了破壞循環(huán)等待條件憔涉,因此避免了死鎖。

10析苫、什么是死鎖兜叨,活鎖?

  • 死鎖:多個(gè)線程都無法獲得資源繼續(xù)執(zhí)行衩侥」酰可以通過避免一個(gè)線程獲取多個(gè)鎖;一個(gè)鎖占用一個(gè)資源顿乒;使用定時(shí)鎖议街;數(shù)據(jù)庫加解鎖在一個(gè)連接中。
  • 死鎖的必要條件:環(huán)路等待璧榄,不可剝奪,請(qǐng)求保持吧雹,互斥條件
  • 活鎖:線程之間相互謙讓資源骨杂,都無法獲取所有資源繼續(xù)執(zhí)行。

11雄卷、Java中CyclicBarrier 和 CountDownLatch有什么不同搓蚪?

CyclicBarrier 和 CountDownLatch 都可以用來讓一組線程等待其它線程。與 CyclicBarrier 不同的是丁鹉,CountdownLatch 不能重新使用妒潭。

  • CountDownLatch是一種靈活的閉鎖實(shí)現(xiàn),可以使一個(gè)或者多個(gè)線程等待一組事件發(fā)生揣钦。閉鎖狀態(tài)包括一個(gè)計(jì)數(shù)器雳灾,改計(jì)數(shù)器初始化為一個(gè)正數(shù),表示需要等待的事件數(shù)量冯凹。countDown方法遞減計(jì)數(shù)器谎亩,表示有一個(gè)事件發(fā)生了,而await方法等待計(jì)數(shù)器到達(dá)0,表示所有需要等待的事情都已經(jīng)發(fā)生匈庭。如果計(jì)數(shù)器的值非零夫凸,那么await就會(huì)一直阻塞知道計(jì)數(shù)器的值為0,或者等待的線程中斷阱持,或者等待超時(shí)夭拌。
  • CyclicBarrier適用于這樣的情況:你希望創(chuàng)建一組任務(wù),他們并行地執(zhí)行工作衷咽,然后在進(jìn)行下一個(gè)步驟之前等待鸽扁,直至所有任務(wù)都完成。它使得所有的并行任務(wù)都將在柵欄出列隊(duì)兵罢,因此可以一致的向前移動(dòng)献烦。這非常像CountDownLatch,只是CountDownLatch是只觸發(fā)一次的事件卖词,而CyclicBarrier可以多次重用巩那。

12、Java中的同步集合與并發(fā)集合有什么區(qū)別此蜈?

  • 同步集合與并發(fā)集合都為多線程和并發(fā)提供了合適的線程安全的集合即横,不過并發(fā)集合的可擴(kuò)展性更高。在Java1.5之前程序員們只有同步集合來用且在多線程并發(fā)的時(shí)候會(huì)導(dǎo)致爭(zhēng)用裆赵,阻礙了系統(tǒng)的擴(kuò)展性东囚。Java5介紹了并發(fā)集合像ConcurrentHashMap,不僅提供線程安全還用鎖分離和內(nèi)部分區(qū)等現(xiàn)代技術(shù)提高了可擴(kuò)展性战授。
  • 同步容器是線程安全的页藻。同步容器將所有對(duì)容器狀態(tài)的訪問都串行化,以實(shí)現(xiàn)他們的線程安全性植兰。這種方法的代價(jià)是嚴(yán)重降低并發(fā)性份帐,當(dāng)多個(gè)線程競(jìng)爭(zhēng)容器的鎖時(shí),吞吐量將嚴(yán)重降低楣导。并發(fā)容器是針對(duì)多個(gè)線程并發(fā)訪問設(shè)計(jì)的废境,改進(jìn)了同步容器的性能。通過并發(fā)容器來代替同步容器筒繁,可以極大地提高伸縮性并降低風(fēng)險(xiǎn)噩凹。

13、你如何在Java中獲取線程堆棧毡咏?

對(duì)于不同的操作系統(tǒng)驮宴,有多種方法來獲得Java進(jìn)程的線程堆棧。當(dāng)你獲取線程堆棧時(shí)血当,JVM會(huì)把所有線程的狀態(tài)存到日志文件或者輸出到控制臺(tái)幻赚。在Windows你可以使用Ctrl + Break組合鍵來獲取線程堆棧禀忆,Linux下用kill -3命令。你也可以用jstack這個(gè)工具來獲取落恼,它對(duì)線程id進(jìn)行操作箩退,你可以用jps這個(gè)工具找到id。

14佳谦、Java中ConcurrentHashMap的并發(fā)度是什么戴涝?

  • ConcurrentHashMap把實(shí)際map劃分成若干部分來實(shí)現(xiàn)它的可擴(kuò)展性和線程安全。這種劃分是使用并發(fā)度獲得的钻蔑,它是ConcurrentHashMap類構(gòu)造函數(shù)的一個(gè)可選參數(shù)啥刻,默認(rèn)值為16,這樣在多線程情況下就能避免爭(zhēng)用咪笑。
  • 并發(fā)度可以理解為程序運(yùn)行時(shí)能夠同時(shí)更新ConccurentHashMap且不產(chǎn)生鎖競(jìng)爭(zhēng)的最大線程數(shù)可帽,實(shí)際上就是ConcurrentHashMap中的分段鎖個(gè)數(shù),即Segment[]的數(shù)組長(zhǎng)度窗怒。ConcurrentHashMap默認(rèn)的并發(fā)度為16映跟,但用戶也可以在構(gòu)造函數(shù)中設(shè)置并發(fā)度。當(dāng)用戶設(shè)置并發(fā)度時(shí)扬虚,ConcurrentHashMap會(huì)使用大于等于該值的最小2冪指數(shù)作為實(shí)際并發(fā)度(假如用戶設(shè)置并發(fā)度為17努隙,實(shí)際并發(fā)度則為32)。運(yùn)行時(shí)通過將key的高n位(n = 32 – segmentShift)和并發(fā)度減1(segmentMask)做位與運(yùn)算定位到所在的Segment辜昵。segmentShift與segmentMask都是在構(gòu)造過程中根據(jù)concurrency level被相應(yīng)的計(jì)算出來荸镊。
  • 如果并發(fā)度設(shè)置的過小,會(huì)帶來嚴(yán)重的鎖競(jìng)爭(zhēng)問題堪置;如果并發(fā)度設(shè)置的過大躬存,原本位于同一個(gè)Segment內(nèi)的訪問會(huì)擴(kuò)散到不同的Segment中,CPU cache命中率會(huì)下降舀锨,從而引起程序性能下降优构。

15、Java中的同步集合與并發(fā)集合有什么區(qū)別雁竞?

  • 同步集合與并發(fā)集合都為多線程和并發(fā)提供了合適的線程安全的集合,不過并發(fā)集合的可擴(kuò)展性更高拧额。在Java1.5之前程序員們只有同步集合來用且在多線程并發(fā)的時(shí)候會(huì)導(dǎo)致爭(zhēng)用碑诉,阻礙了系統(tǒng)的擴(kuò)展性。Java5介紹了并發(fā)集合像ConcurrentHashMap侥锦,不僅提供線程安全還用鎖分離和內(nèi)部分區(qū)等現(xiàn)代技術(shù)提高了可擴(kuò)展性进栽。
  • 同步容器是線程安全的。同步容器將所有對(duì)容器狀態(tài)的訪問都串行化恭垦,以實(shí)現(xiàn)他們的線程安全性快毛。這種方法的代價(jià)是嚴(yán)重降低并發(fā)性格嗅,當(dāng)多個(gè)線程競(jìng)爭(zhēng)容器的鎖時(shí),吞吐量將嚴(yán)重降低唠帝。并發(fā)容器是針對(duì)多個(gè)線程并發(fā)訪問設(shè)計(jì)的屯掖,改進(jìn)了同步容器的性能。通過并發(fā)容器來代替同步容器襟衰,可以極大地提高伸縮性并降低風(fēng)險(xiǎn)贴铜。

16、Thread類中的yield方法有什么作用瀑晒?

  • Yield方法可以暫停當(dāng)前正在執(zhí)行的線程對(duì)象绍坝,讓其它有相同優(yōu)先級(jí)的線程執(zhí)行。它是一個(gè)靜態(tài)方法而且只保證當(dāng)前線程放棄CPU占用而不能保證使其它線程一定能占用CPU苔悦,執(zhí)行yield()的線程有可能在進(jìn)入到暫停狀態(tài)后馬上又被執(zhí)行轩褐。
  • 線程讓步:如果知道已經(jīng)完成了在run()方法的循環(huán)的一次迭代過程中所需的工作,就可以給線程調(diào)度機(jī)制一個(gè)暗示:你的工作已經(jīng)做得差不多了玖详,可以讓別的線程使用CPU了把介。這個(gè)暗示將通過調(diào)用yield()方法來做出(不過這只是一個(gè)暗示,沒有任何機(jī)制保證它將會(huì)被采納)竹宋。當(dāng)調(diào)用yield()時(shí)劳澄,也是在建議具有相同優(yōu)先級(jí)的其他線程可以運(yùn)行。
  • yield()的作用是讓步蜈七。它能讓當(dāng)前線程由“運(yùn)行狀態(tài)”進(jìn)入到“就緒狀態(tài)”秒拔,從而讓其它具有相同優(yōu)先級(jí)的等待線程獲取執(zhí)行權(quán);但是飒硅,并不能保證在當(dāng)前線程調(diào)用yield()之后砂缩,其它具有相同優(yōu)先級(jí)的線程就一定能獲得執(zhí)行權(quán);也有可能是當(dāng)前線程又進(jìn)入到“運(yùn)行狀態(tài)”繼續(xù)運(yùn)行三娩!

17庵芭、什么是ThreadLocal變量?

ThreadLocal是Java里一種特殊的變量雀监。每個(gè)線程都有一個(gè)ThreadLocal就是每個(gè)線程都擁有了自己獨(dú)立的一個(gè)變量双吆,競(jìng)爭(zhēng)條件被徹底消除了。它是為創(chuàng)建代價(jià)高昂的對(duì)象獲取線程安全的好方法会前,比如你可以用ThreadLocal讓SimpleDateFormat變成線程安全的好乐,因?yàn)槟莻€(gè)類創(chuàng)建代價(jià)高昂且每次調(diào)用都需要?jiǎng)?chuàng)建不同的實(shí)例所以不值得在局部范圍使用它,如果為每個(gè)線程提供一個(gè)自己獨(dú)有的變量拷貝瓦宜,將大大提高效率蔚万。首先,通過復(fù)用減少了代價(jià)高昂的對(duì)象的創(chuàng)建個(gè)數(shù)临庇。其次反璃,你在沒有使用高代價(jià)的同步或者不變性的情況下獲得了線程安全昵慌。線程局部變量的另一個(gè)不錯(cuò)的例子是ThreadLocalRandom類,它在多線程環(huán)境中減少了創(chuàng)建代價(jià)高昂的Random對(duì)象的個(gè)數(shù)淮蜈。

ThreadLocal是一種線程封閉技術(shù)斋攀。ThreadLocal提供了get和set等訪問接口或方法,這些方法為每個(gè)使用該變量的線程都存有一份獨(dú)立的副本礁芦,因此get總是返回由當(dāng)前執(zhí)行線程在調(diào)用set時(shí)設(shè)置的最新值蜻韭。

** 18、Java內(nèi)存模型是什么柿扣?**

Java內(nèi)存模型規(guī)定和指引Java程序在不同的內(nèi)存架構(gòu)肖方、CPU和操作系統(tǒng)間有確定性地行為。它在多線程的情況下尤其重要未状。Java內(nèi)存模型對(duì)一個(gè)線程所做的變動(dòng)能被其它線程可見提供了保證俯画,它們之間是先行發(fā)生關(guān)系。這個(gè)關(guān)系定義了一些規(guī)則讓程序員在并發(fā)編程時(shí)思路更清晰司草。比如艰垂,先行發(fā)生關(guān)系確保了:

  • 線程內(nèi)的代碼能夠按先后順序執(zhí)行,這被稱為程序次序規(guī)則埋虹。
  • 對(duì)于同一個(gè)鎖猜憎,一個(gè)解鎖操作一定要發(fā)生在時(shí)間上后發(fā)生的另一個(gè)鎖定操作之前,也叫做管程鎖定規(guī)則搔课。
  • 前一個(gè)對(duì)volatile的寫操作在后一個(gè)volatile的讀操作之前胰柑,也叫volatile變量規(guī)則。
  • 一個(gè)線程內(nèi)的任何操作必需在這個(gè)線程的start()調(diào)用之后爬泥,也叫作線程啟動(dòng)規(guī)則柬讨。
  • 一個(gè)線程的所有操作都會(huì)在線程終止之前,線程終止規(guī)則袍啡。
  • 一個(gè)對(duì)象的終結(jié)操作必需在這個(gè)對(duì)象構(gòu)造完成之后踩官,也叫對(duì)象終結(jié)規(guī)則。
  • 可傳遞性

我強(qiáng)烈建議大家閱讀《Java并發(fā)編程實(shí)踐》第十六章來加深對(duì)Java內(nèi)存模型的理解境输。

19蔗牡、Java中的volatile 變量是什么?

volatile是一個(gè)特殊的修飾符嗅剖,只有成員變量才能使用它蛋逾。在Java并發(fā)程序缺少同步類的情況下,多線程對(duì)成員變量的操作對(duì)其它線程是透明的窗悯。volatile變量可以保證下一個(gè)讀取操作會(huì)在前一個(gè)寫操作之后發(fā)生,就是上一題的volatile變量規(guī)則偷拔。

Java語言提供了一種稍弱的同步機(jī)制蒋院,即volatile變量亏钩,用來確保將變量的更新操作通知到其他線程。當(dāng)把變量聲明為volatile類型后欺旧,編譯器和運(yùn)行時(shí)都會(huì)注意到這個(gè)變量是共享的姑丑,因此不會(huì)將變量上的操作和其他內(nèi)存操作一起重排序。volatile變量不會(huì)被緩存在寄存器或者對(duì)其他處理器不可見的地方辞友,因此在讀取volatile類型的時(shí)候總會(huì)返回最新寫入的值栅哀。

在訪問volatile變量時(shí)不會(huì)執(zhí)行加鎖操作,因此也不會(huì)使執(zhí)行線程阻塞称龙,因此volatile變量是一種比synchronized關(guān)鍵字更輕量級(jí)的同步機(jī)制留拾。

加鎖機(jī)制既可以確卑樗ǎ可見性又可以確保原子性骑脱,而volatile變量只能確保可見性咐吼。

20疫向、volatile 變量和 atomic 變量有什么不同咳蔚?

這是個(gè)有趣的問題。首先搔驼,volatile 變量和 atomic 變量看起來很像谈火,但功能卻不一樣。Volatile變量可以確保先行關(guān)系舌涨,即寫操作會(huì)發(fā)生在后續(xù)的讀操作之前, 但它并不能保證原子性糯耍。例如用volatile修飾count變量那么 count++ 操作就不是原子性的。而AtomicInteger類提供的atomic方法可以讓這種操作具有原子性如getAndIncrement()方法會(huì)原子性的進(jìn)行增量操作把當(dāng)前值加一泼菌,其它數(shù)據(jù)類型和引用變量也可以進(jìn)行相似操作谍肤。

21、Java中Runnable和Callable有什么不同哗伯?

  • Runnable和Callable都代表那些要在不同的線程中執(zhí)行的任務(wù)荒揣。Runnable從JDK1.0開始就有了,Callable是在JDK1.5增加的焊刹。它們的主要區(qū)別是Callable的 call() 方法可以返回值和拋出異常系任,而Runnable的run()方法沒有這些功能。Callable可以返回裝載有計(jì)算結(jié)果的Future對(duì)象虐块。
  • Runnable是執(zhí)行工作的獨(dú)立任務(wù)俩滥,但是它不返回任何值。如果希望任務(wù)在完成的時(shí)候能夠返回一個(gè)值贺奠,那么可以實(shí)現(xiàn)Callable接口而不是Runnable接口霜旧。在Java SE5中引入的Callable是一種具有類型參數(shù)的泛型,它的類型參數(shù)表示的是從方法call()(而不是run())中返回的值儡率,并且必須使用ExecutorService.submit()方法調(diào)用它挂据。submit()方法會(huì)產(chǎn)生Future對(duì)象以清,它用Callable返回結(jié)果的特定類型進(jìn)行了參數(shù)化。

22崎逃、哪些操作釋放鎖掷倔,哪些不釋放鎖?

  • sleep(): 釋放資源个绍,不釋放鎖勒葱,進(jìn)入阻塞狀態(tài),喚醒隨機(jī)線程巴柿,Thread類方法凛虽。
  • wait(): 釋放資源,釋放鎖篮洁,Object類方法涩维。
  • yield(): 不釋放鎖,進(jìn)入可執(zhí)行狀態(tài)袁波,選擇優(yōu)先級(jí)高的線程執(zhí)行瓦阐,Thread類方法。
  • 如果線程產(chǎn)生的異常沒有被捕獲篷牌,會(huì)釋放鎖睡蟋。

23、如何正確的終止線程枷颊?

  • 使用共享變量戳杀,要用volatile關(guān)鍵字,保證可見性夭苗,能夠及時(shí)終止信卡。
  • 使用interrupt()和isInterrupted()配合使用。

24题造、interrupt(), interrupted(), isInterrupted()的區(qū)別傍菇?

  • interrupt():設(shè)置中斷標(biāo)志;
  • interrupted():響應(yīng)中斷標(biāo)志并復(fù)位中斷標(biāo)志界赔;
  • isInterrupted():響應(yīng)中斷標(biāo)志丢习;

25、synchronized的鎖對(duì)象是哪些淮悼?

  • 普通方法是當(dāng)前實(shí)例對(duì)象咐低;
  • 同步方法快是括號(hào)中配置內(nèi)容,可以是類Class對(duì)象袜腥,可以是實(shí)例對(duì)象见擦;
  • 靜態(tài)方法是當(dāng)前類Class對(duì)象。
  • 只要不是同一個(gè)鎖,就可以并行執(zhí)行锡宋,同一個(gè)鎖儡湾,只能串行執(zhí)行。
  • 更多參考我的文章Java中Synchronized關(guān)鍵字簡(jiǎn)介(譯)

26执俩、volatile和synchronized的區(qū)別是什么?

  1. volatile只能使用在變量上癌刽;而synchronized可以在類役首,變量,方法和代碼塊上显拜。
  2. volatile至保證可見性衡奥;synchronized保證原子性與可見性。
  3. volatile禁用指令重排序远荠;synchronized不會(huì)矮固。
  4. volatile不會(huì)造成阻塞;synchronized會(huì)譬淳。

27档址、什么是緩存一致性協(xié)議?

因?yàn)镃PU是運(yùn)算很快,而主存的讀寫很忙,所以在程序運(yùn)行中,會(huì)復(fù)制一份數(shù)據(jù)到高速緩存,處理完成在將結(jié)果保存主存.

這樣存在一些問題,在多核CPU中多個(gè)線程,多個(gè)線程拷貝多份的高速緩存數(shù)據(jù),最后在計(jì)算完成,刷到主存的數(shù)據(jù)就會(huì)出現(xiàn)覆蓋

所以就出現(xiàn)了緩存一致性協(xié)議。最出名的就是Intel 的MESI協(xié)議邻梆,MESI協(xié)議保證了每個(gè)緩存中使用的共享變量的副本是一致的守伸。它核心的思想是:當(dāng)CPU寫數(shù)據(jù)時(shí),如果發(fā)現(xiàn)操作的變量是共享變量浦妄,即在其他CPU中也存在該變量的副本尼摹,會(huì)發(fā)出信號(hào)通知其他CPU將該變量的緩存行置為無效狀態(tài),因此當(dāng)其他CPU需要讀取這個(gè)變量時(shí)剂娄,發(fā)現(xiàn)自己緩存中緩存該變量的緩存行是無效的蠢涝,那么它就會(huì)從內(nèi)存重新讀取。

28阅懦、Synchronized關(guān)鍵字和二、Lock,并解釋它們之間的區(qū)別故黑?

   Synchronized 與Lock都是可重入鎖儿咱,同一個(gè)線程再次進(jìn)入同步代碼的時(shí)候.可以使用自己已經(jīng)獲取到的鎖

   Synchronized是悲觀鎖機(jī)制,獨(dú)占鎖场晶。而Locks.ReentrantLock是混埠,每次不加鎖而是假設(shè)沒有沖突而去完成某項(xiàng)操作,如果因?yàn)闆_突失敗就重試诗轻,直到成功為止钳宪。ReentrantLock適用場(chǎng)景

   某個(gè)線程在等待一個(gè)鎖的控制權(quán)的這段時(shí)間需要中斷

   需要分開處理一些wait-notify,ReentrantLock里面的Condition應(yīng)用,能夠控制notify哪個(gè)線程吏颖,鎖可以綁定多個(gè)條件搔体。

   具有公平鎖功能,每個(gè)到來的線程都將排隊(duì)等候半醉。

29疚俱、Volatile如何保證內(nèi)存可見性?

  • 當(dāng)寫一個(gè)volatile變量時(shí)缩多,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存呆奕。
  • 當(dāng)讀一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效衬吆。線程接下來將從主內(nèi)存中讀取共享變量梁钾。

30、 Java中什么是競(jìng)態(tài)條件逊抡?

競(jìng)態(tài)條件會(huì)導(dǎo)致程序在并發(fā)情況下出現(xiàn)一些bugs姆泻。多線程對(duì)一些資源的競(jìng)爭(zhēng)的時(shí)候就會(huì)產(chǎn)生競(jìng)態(tài)條件,如果首先要執(zhí)行的程序競(jìng)爭(zhēng)失敗排到后面執(zhí)行了冒嫡,那么整個(gè)程序就會(huì)出現(xiàn)一些不確定的bugs拇勃。這種bugs很難發(fā)現(xiàn)而且會(huì)重復(fù)出現(xiàn),因?yàn)榫€程間的隨機(jī)競(jìng)爭(zhēng)灯谣。

31潜秋、為什么wait, notify 和 notifyAll這些方法不在thread類里面?

明顯的原因是JAVA提供的鎖是對(duì)象級(jí)的而不是線程級(jí)的胎许,每個(gè)對(duì)象都有鎖峻呛,通過線程獲得。如果線程需要等待某些鎖那么調(diào)用對(duì)象中的wait()方法就有意義了辜窑。如果wait()方法定義在Thread類中钩述,線程正在等待的是哪個(gè)鎖就不明顯了。簡(jiǎn)單的說穆碎,由于wait牙勘,notify和notifyAll都是鎖級(jí)別的操作,所以把他們定義在Object類中因?yàn)殒i屬于對(duì)象所禀。

32方面、Java中synchronized 和 ReentrantLock 有什么不同?

相似點(diǎn):

這兩種同步方式有很多相似之處色徘,它們都是加鎖方式同步恭金,而且都是阻塞式的同步,也就是說當(dāng)如果一個(gè)線程獲得了對(duì)象鎖褂策,進(jìn)入了同步塊横腿,其他訪問該同步塊的線程都必須阻塞在同步塊外面等待颓屑,而進(jìn)行線程阻塞和喚醒的代價(jià)是比較高的.

區(qū)別:

這兩種方式最大區(qū)別就是對(duì)于Synchronized來說,它是java語言的關(guān)鍵字耿焊,是原生語法層面的互斥揪惦,需要jvm實(shí)現(xiàn)。而ReentrantLock它是JDK 1.5之后提供的API層面的互斥鎖罗侯,需要lock()和unlock()方法配合try/finally語句塊來完成器腋。

Synchronized進(jìn)過編譯,會(huì)在同步塊的前后分別形成monitorenter和monitorexit這個(gè)兩個(gè)字節(jié)碼指令钩杰。在執(zhí)行monitorenter指令時(shí)蒂培,首先要嘗試獲取對(duì)象鎖。如果這個(gè)對(duì)象沒被鎖定榜苫,或者當(dāng)前線程已經(jīng)擁有了那個(gè)對(duì)象鎖,把鎖的計(jì)算器加1翎冲,相應(yīng)的垂睬,在執(zhí)行monitorexit指令時(shí)會(huì)將鎖計(jì)算器就減1,當(dāng)計(jì)算器為0時(shí)抗悍,鎖就被釋放了驹饺。如果獲取對(duì)象鎖失敗,那當(dāng)前線程就要阻塞缴渊,直到對(duì)象鎖被另一個(gè)線程釋放為止赏壹。

由于ReentrantLock是java.util.concurrent包下提供的一套互斥鎖,相比Synchronized衔沼,ReentrantLock類提供了一些高級(jí)功能蝌借,主要有以下3項(xiàng):

  • 等待可中斷,持有鎖的線程長(zhǎng)期不釋放的時(shí)候指蚁,正在等待的線程可以選擇放棄等待菩佑,這相當(dāng)于Synchronized來說可以避免出現(xiàn)死鎖的情況。
  • 公平鎖凝化,多個(gè)線程等待同一個(gè)鎖時(shí)稍坯,必須按照申請(qǐng)鎖的時(shí)間順序獲得鎖,Synchronized鎖非公平鎖搓劫,ReentrantLock默認(rèn)的構(gòu)造函數(shù)是創(chuàng)建的非公平鎖瞧哟,可以通過參數(shù)true設(shè)為公平鎖,但公平鎖表現(xiàn)的性能不是很好枪向。
  • 鎖綁定多個(gè)條件勤揩,一個(gè)ReentrantLock對(duì)象可以同時(shí)綁定對(duì)個(gè)對(duì)象。

33遣疯、Synchronized 用過嗎雄可,其原理是什么?

這是一道 Java 面試中幾乎百分百會(huì)問到的問題凿傅,因?yàn)橹灰浅绦騿T就一定會(huì)通過或者接觸過Synchronized。

答:Synchronized 是由 JVM 實(shí)現(xiàn)的一種實(shí)現(xiàn)互斥同步的一種方式数苫,如果 你查看被 Synchronized 修飾過的程序塊編譯后的字節(jié)碼聪舒,會(huì)發(fā)現(xiàn), 被 Synchronized 修飾過的程序塊虐急,在編譯前后被編譯器生成了monitorenter 和 monitorexit 兩 個(gè) 字 節(jié) 碼 指 令 箱残。

這兩個(gè)指令是什么意思呢?

在虛擬機(jī)執(zhí)行到 monitorenter 指令時(shí),首先要嘗試獲取對(duì)象的鎖: 如果這個(gè)對(duì)象沒有鎖定止吁,或者當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖被辑,把鎖 的計(jì)數(shù)器 +1;當(dāng)執(zhí)行 monitorexit 指令時(shí)將鎖計(jì)數(shù)器 -1;當(dāng)計(jì)數(shù)器 為 0 時(shí),鎖就被釋放了敬惦。如果獲取對(duì)象失敗了盼理,那當(dāng)前線程就要阻塞等待,直到對(duì)象鎖被另外一 個(gè)線程釋放為止俄删。

Java 中 Synchronize 通過在對(duì)象頭設(shè)置標(biāo)記宏怔,達(dá)到了獲取鎖和釋放 鎖的目的。

34畴椰、上面提到獲取對(duì)象的鎖臊诊,這個(gè)“鎖”到底是什么?如何確定對(duì)象的鎖?

答:“鎖”的本質(zhì)其實(shí)是 monitorenter 和 monitorexit 字節(jié)碼指令的一 個(gè) Reference 類型的參數(shù),即要鎖定和解鎖的對(duì)象斜脂。我們知道抓艳,使用Synchronized 可以修飾不同的對(duì)象,因此帚戳,對(duì)應(yīng)的對(duì)象鎖可以這么確 定:

  1. 如果 Synchronized 明確指定了鎖對(duì)象玷或,比如 Synchronized(變量 名)、Synchronized(this) 等销斟,說明加解鎖對(duì)象為該對(duì)象庐椒。

  2. 如果沒有明確指定:

  • 若 Synchronized 修飾的方法為非靜態(tài)方法,表示此方法對(duì)應(yīng)的對(duì)象為 鎖對(duì)象;
  • 若 Synchronized 修飾的方法為靜態(tài)方法蚂踊,則表示此方法對(duì)應(yīng)的類對(duì)象 為鎖對(duì)象约谈。

注意,當(dāng)一個(gè)對(duì)象被鎖住時(shí)犁钟,對(duì)象里面所有用 Synchronized 修飾的 方法都將產(chǎn)生堵塞棱诱,而對(duì)象里非 Synchronized 修飾的方法可正常被 調(diào)用,不受鎖影響涝动。

35迈勋、什么是可重入性,為什么說 Synchronized 是可重入鎖?

先來看一下維基百科關(guān)于可重入鎖的定義:

若一個(gè)程序或子程序可以“在任意時(shí)刻被中斷然后操作系統(tǒng)調(diào)度執(zhí)行另外一段代碼醋粟,這段代碼又調(diào)用了該子程序不會(huì)出錯(cuò)”靡菇,則稱其為可重入(reentrant或re-entrant)的重归。即當(dāng)該子程序正在運(yùn)行時(shí),執(zhí)行線程可以再次進(jìn)入并執(zhí)行它厦凤,仍然獲得符合設(shè)計(jì)時(shí)預(yù)期的結(jié)果鼻吮。與多線程并發(fā)執(zhí)行的線程安全不同,可重入強(qiáng)調(diào)對(duì)單個(gè)線程執(zhí)行時(shí)重新進(jìn)入同一個(gè)子程序仍然是安全的较鼓。

通俗來說:當(dāng)線程請(qǐng)求一個(gè)由其它線程持有的對(duì)象鎖時(shí)椎木,該線程會(huì)阻塞,而當(dāng)線程請(qǐng)求由自己持有的對(duì)象鎖時(shí)博烂,如果該鎖是重入鎖香椎,請(qǐng)求就會(huì)成功,否則阻塞禽篱。

要證明synchronized是不是可重入鎖畜伐,我們先來看一段代碼:

package com.mzc.common.concurrent.synchronize;
?
/**
 * <p class="detail">
 * 功能: 證明synchronized為什么是可重入鎖
 * </p>
 *
 * @author Moore
 * @ClassName Super class.
 * @Version V1.0.
 * @date 2020.02.07 15:34:12
 */
public class SuperClass {
?
    public synchronized void doSomething(){
        System.out.println("father is doing something,the thread name is:"+Thread.currentThread().getName());
    }
}

package com.mzc.common.concurrent.synchronize;
?
/**
 * <p class="detail">
 * 功能: 證明synchronized為什么是可重入鎖
 * </p>
 *
 * @author Moore
 * @ClassName Sub class.
 * @Version V1.0.
 * @date 2020.02.07 15:34:41
 */
public class SubClass extends SuperClass {
?
    public synchronized void doSomething() {
        System.out.println("child is doing doSomething,the thread name is:" + Thread.currentThread().getName());
        // 調(diào)用自己類中其他的synchronized方法
        doAnotherThing();
    }
?
    private synchronized void doAnotherThing() {
        // 調(diào)用父類的synchronized方法
        super.doSomething();
        System.out.println("child is doing anotherThing,the thread name is:" + Thread.currentThread().getName());
    }
?
    public static void main(String[] args) {
        SubClass child = new SubClass();
        child.doSomething();
    }
}

通過運(yùn)行main方法,先一下結(jié)果:

child is doing doSomething,the thread name is:main
father is doing something,the thread name is:main
child is doing anotherThing,the thread name is:main

因?yàn)檫@些方法輸出了相同的線程名稱躺率,表明即使遞歸使用synchronized也沒有發(fā)生死鎖烤礁,證明其是可重入的。

還看不懂肥照?那我就再解釋下!

這里的對(duì)象鎖只有一個(gè)勤众,就是 child 對(duì)象的鎖舆绎,當(dāng)執(zhí)行 child.doSomething 時(shí),該線程獲得 child 對(duì)象的鎖们颜,在 doSomething 方法內(nèi)執(zhí)行 doAnotherThing 時(shí)再次請(qǐng)求child對(duì)象的鎖吕朵,因?yàn)閟ynchronized 是重入鎖,所以可以得到該鎖窥突,繼續(xù)在 doAnotherThing 里執(zhí)行父類的 doSomething 方法時(shí)第三次請(qǐng)求 child 對(duì)象的鎖努溃,同樣可得到。如果不是重入鎖的話阻问,那這后面這兩次請(qǐng)求鎖將會(huì)被一直阻塞梧税,從而導(dǎo)致死鎖。

所以在 java 內(nèi)部称近,同一線程在調(diào)用自己類中其他 synchronized 方法/塊或調(diào)用父類的 synchronized 方法/塊都不會(huì)阻礙該線程的執(zhí)行第队。就是說同一線程對(duì)同一個(gè)對(duì)象鎖是可重入的,而且同一個(gè)線程可以獲取同一把鎖多次刨秆,也就是可以多次重入凳谦。因?yàn)閖ava線程是基于“每線程(per-thread)”,而不是基于“每調(diào)用(per-invocation)”的(java中線程獲得對(duì)象鎖的操作是以線程為粒度的衡未,per-invocation 互斥體獲得對(duì)象鎖的操作是以每調(diào)用作為粒度的)尸执。

重入鎖實(shí)現(xiàn)可重入性原理或機(jī)制是:每一個(gè)鎖關(guān)聯(lián)一個(gè)線程持有者和計(jì)數(shù)器家凯,當(dāng)計(jì)數(shù)器為 0 時(shí)表示該鎖沒有被任何線程持有,那么任何線程都可能獲得該鎖而調(diào)用相應(yīng)的方法如失;當(dāng)某一線程請(qǐng)求成功后绊诲,JVM會(huì)記下鎖的持有線程,并且將計(jì)數(shù)器置為 1岖常;此時(shí)其它線程請(qǐng)求該鎖驯镊,則必須等待;而該持有鎖的線程如果再次請(qǐng)求這個(gè)鎖竭鞍,就可以再次拿到這個(gè)鎖板惑,同時(shí)計(jì)數(shù)器會(huì)遞增;當(dāng)線程退出同步代碼塊時(shí)偎快,計(jì)數(shù)器會(huì)遞減冯乘,如果計(jì)數(shù)器為 0,則釋放該鎖晒夹。

36裆馒、JVM 對(duì) Java 的原生鎖做了哪些優(yōu)化?

在 Java 6 之前,Monitor 的實(shí)現(xiàn)完全依賴底層操作系統(tǒng)的互斥鎖來 實(shí)現(xiàn)丐怯,也就是我們剛才在問題二中所闡述的獲取/釋放鎖的邏輯喷好。

由于 Java 層面的線程與操作系統(tǒng)的原生線程有映射關(guān)系,如果要將一 個(gè)線程進(jìn)行阻塞或喚起都需要操作系統(tǒng)的協(xié)助读跷,這就需要從用戶態(tài)切換 到內(nèi)核態(tài)來執(zhí)行梗搅,這種切換代價(jià)十分昂貴,很耗處理器時(shí)間效览,現(xiàn)代 JDK中做了大量的優(yōu)化无切。一種優(yōu)化是使用自旋鎖,即在把線程進(jìn)行阻塞操作之前先讓線程自旋等待一段時(shí)間丐枉,可能在等待期間其他線程已經(jīng)解鎖哆键,這時(shí)就無需再讓線程 執(zhí)行阻塞操作,避免了用戶態(tài)到內(nèi)核態(tài)的切換瘦锹。

現(xiàn)代 JDK 中還提供了三種不同的 Monitor 實(shí)現(xiàn)籍嘹,也就是三種不同的鎖:

  • 偏向鎖(Biased Locking)
  • 輕量級(jí)鎖
  • 重量級(jí)鎖

這三種鎖使得 JDK 得以優(yōu)化 Synchronized 的運(yùn)行,當(dāng) JVM 檢測(cè) 到不同的競(jìng)爭(zhēng)狀況時(shí)弯院,會(huì)自動(dòng)切換到適合的鎖實(shí)現(xiàn)噩峦,這就是鎖的升級(jí)、 降級(jí)抽兆。

  • 當(dāng)沒有競(jìng)爭(zhēng)出現(xiàn)時(shí)识补,默認(rèn)會(huì)使用偏向鎖。JVM 會(huì)利用 CAS 操作辫红,在對(duì)象頭上的 Mark Word 部分設(shè)置線程ID凭涂,以表示這個(gè)對(duì)象偏向于當(dāng)前線程祝辣,所以并不涉及真正的互斥鎖,因 為在很多應(yīng)用場(chǎng)景中切油,大部分對(duì)象生命周期中最多會(huì)被一個(gè)線程鎖定蝙斜, 使用偏斜鎖可以降低無競(jìng)爭(zhēng)開銷。
  • 如果有另一線程試圖鎖定某個(gè)被偏斜過的對(duì)象澎胡,JVM 就撤銷偏斜鎖孕荠, 切換到輕量級(jí)鎖實(shí)現(xiàn)。
  • 輕量級(jí)鎖依賴 CAS 操作 Mark Word 來試圖獲取鎖攻谁,如果重試成功稚伍, 就使用普通的輕量級(jí)鎖;否則,進(jìn)一步升級(jí)為重量級(jí)鎖戚宦。

37个曙、為什么說 Synchronized 是非公平鎖?

答:非公平主要表現(xiàn)在獲取鎖的行為上,并非是按照申請(qǐng)鎖的時(shí)間前后給等 待線程分配鎖的受楼,每當(dāng)鎖被釋放后垦搬,任何一個(gè)線程都有機(jī)會(huì)競(jìng)爭(zhēng)到鎖, 這樣做的目的是為了提高執(zhí)行性能艳汽,缺點(diǎn)是可能會(huì)產(chǎn)生線程饑餓現(xiàn)象猴贰。

38、為什么說 Synchronized 是一個(gè)悲觀鎖?樂觀鎖的實(shí)現(xiàn)原理 又是什么?什么是 CAS河狐,它有什么特性?

答:Synchronized 顯然是一個(gè)悲觀鎖糟趾,因?yàn)樗牟l(fā)策略是悲觀的:不管是否會(huì)產(chǎn)生競(jìng)爭(zhēng),任何的數(shù)據(jù)操作都必須要加鎖甚牲、用戶態(tài)核心態(tài)轉(zhuǎn) 換、維護(hù)鎖計(jì)數(shù)器和檢查是否有被阻塞的線程需要被喚醒等操作蝶柿。

隨著硬件指令集的發(fā)展丈钙,我們可以使用基于沖突檢測(cè)的樂觀并發(fā)策略。先進(jìn)行操作交汤,如果沒有其他線程征用數(shù)據(jù)雏赦,那操作就成功了; 如果共享數(shù)據(jù)有征用,產(chǎn)生了沖突芙扎,那就再進(jìn)行其他的補(bǔ)償措施星岗。這種 樂觀的并發(fā)策略的許多實(shí)現(xiàn)不需要線程掛起,所以被稱為非阻塞同步戒洼。

樂觀鎖的核心算法是 CAS(Compareand Swap俏橘,比較并交換),它涉 及到三個(gè)操作數(shù):內(nèi)存值圈浇、預(yù)期值寥掐、新值靴寂。當(dāng)且僅當(dāng)預(yù)期值和內(nèi)存值相 等時(shí)才將內(nèi)存值修改為新值。這樣處理的邏輯是召耘,首先檢查某塊內(nèi)存的值是否跟之前我讀取時(shí)的一 樣百炬,如不一樣則表示期間此內(nèi)存值已經(jīng)被別的線程更改過,舍棄本次操 作污它,否則說明期間沒有其他線程對(duì)此內(nèi)存值操作剖踊,可以把新值設(shè)置給此 塊內(nèi)存。

CAS 具有原子性衫贬,它的原子性由CPU 硬件指令實(shí)現(xiàn)保證德澈,即使用JNI 調(diào)用 Native 方法調(diào)用由 C++ 編寫的硬件級(jí)別指令,JDK 中提 供了 Unsafe 類執(zhí)行這些操作祥山。

39圃验、樂觀鎖一定就是好的嗎?

答:樂觀鎖避免了悲觀鎖獨(dú)占對(duì)象的現(xiàn)象,同時(shí)也提高了并發(fā)性能缝呕,但它也 有缺點(diǎn):

  • 樂觀鎖只能保證一個(gè)共享變量的原子操作澳窑。如果多一個(gè)或幾個(gè)變量,樂 觀鎖將變得力不從心供常,但互斥鎖能輕易解決摊聋,不管對(duì)象數(shù)量多少及對(duì)象 顆粒度大小。
  • 長(zhǎng)時(shí)間自旋可能導(dǎo)致開銷大栈暇。假如 CAS 長(zhǎng)時(shí)間不成功而一直自旋麻裁,會(huì) 給 CPU 帶來很大的開銷。
  • ABA 問題源祈。CAS 的核心思想是通過比對(duì)內(nèi)存值與預(yù)期值是否一樣而判 斷內(nèi)存值是否被改過煎源,但這個(gè)判斷邏輯不嚴(yán)謹(jǐn),假如內(nèi)存值原來是 A香缺, 后來被一條線程改為 B手销,最后又被改成了 A,則 CAS 認(rèn)為此內(nèi)存值并 沒有發(fā)生改變图张,但實(shí)際上是有被其他線程改過的扼倘,這種情況對(duì)依賴過程 值的情景的運(yùn)算結(jié)果影響很大蒋荚。解決的思路是引入版本號(hào),每次變量更新都把版本號(hào)加一。

40侦鹏、談一談AQS框架旺入。

AQS(AbstractQueuedSynchronizer 類)是一個(gè)用來構(gòu)建鎖和同步器 的框架趁尼,各種Lock 包中的鎖(常用的有 ReentrantLock剩蟀、 ReadWriteLock) , 以 及 其 他 如 Semaphore、 CountDownLatch鄙陡, 甚 至是早期的 FutureTask 等冕房,都是基于 AQS 來構(gòu)建。

  1. AQS 在內(nèi)部定義了一個(gè) volatile int state 變量趁矾,表示同步狀態(tài):當(dāng)線 程調(diào)用 lock 方法時(shí) 耙册,如果 state=0,說明沒有任何線程占有共享資源 的鎖毫捣,可以獲得鎖并將 state=1;如果 state=1详拙,則說明有線程目前正在 使用共享變量,其他線程必須加入同步隊(duì)列進(jìn)行等待蔓同。
  2. AQS 通過 Node 內(nèi)部類構(gòu)成的一個(gè)雙向鏈表結(jié)構(gòu)的同步隊(duì)列饶辙,來完成線 程獲取鎖的排隊(duì)工作,當(dāng)有線程獲取鎖失敗后斑粱,就被添加到隊(duì)列末尾弃揽。Node 類是對(duì)要訪問同步代碼的線程的封裝,包含了線程本身及其狀態(tài)叫waitStatus(有五種不同 取值则北,分別表示是否被阻塞矿微,是否等待喚醒, 是否已經(jīng)被取消等)尚揣,每個(gè) Node 結(jié)點(diǎn)關(guān)聯(lián)其 prev 結(jié)點(diǎn)和 next 結(jié) 點(diǎn)涌矢,方便線程釋放鎖后快速喚醒下一個(gè)在等待的線程,是一個(gè) FIFO 的過 程快骗。Node 類有兩個(gè)常量娜庇,SHARED 和 EXCLUSIVE,分別代表共享模式和獨(dú) 占模式方篮。所謂共享模式是一個(gè)鎖允許多條線程同時(shí)操作(信號(hào)量Semaphore 就是基于 AQS 的共享模式實(shí)現(xiàn)的)名秀,獨(dú)占模式是同一個(gè)時(shí) 間段只能有一個(gè)線程對(duì)共享資源進(jìn)行操作,多余的請(qǐng)求線程需要排隊(duì)等待 ( 如 ReentranLock) 藕溅。
  3. AQS 通過內(nèi)部類 ConditionObject 構(gòu)建等待隊(duì)列(可有多個(gè))匕得,當(dāng)Condition 調(diào)用 wait() 方法后,線程將會(huì)加入等待隊(duì)列中蜈垮,而當(dāng)Condition 調(diào)用 signal() 方法后,線程將從等待隊(duì)列轉(zhuǎn)移動(dòng)同步隊(duì)列中進(jìn)行鎖競(jìng)爭(zhēng)裕照。
  4. AQS 和 Condition 各自維護(hù)了不同的隊(duì)列攒发,在使用 Lock 和Condition 的時(shí)候,其實(shí)就是兩個(gè)隊(duì)列的互相移動(dòng)晋南。

41惠猿、ReentrantLock 是如何實(shí)現(xiàn)可重入性的?

答:ReentrantLock 內(nèi)部自定義了同步器 Sync(Sync 既實(shí)現(xiàn)了 AQS, 又實(shí)現(xiàn)了 AOS负间,而 AOS 提供了一種互斥鎖持有的方式)偶妖,其實(shí)就是 加鎖的時(shí)候通過 CAS 算法姜凄,將線程對(duì)象放到一個(gè)雙向鏈表中,每次獲 取鎖的時(shí)候趾访,看下當(dāng)前維護(hù)的那個(gè)線程 ID 和當(dāng)前請(qǐng)求的線程 ID 是否 一樣态秧,一樣就可重入了。

42扼鞋、Java中Semaphore是什么申鱼?

Java中的Semaphore是一種新的同步類,它是一個(gè)計(jì)數(shù)信號(hào)云头。從概念上講捐友,從概念上講,信號(hào)量維護(hù)了一個(gè)許可集合溃槐。如有必要匣砖,在許可可用前會(huì)阻塞每一個(gè) acquire(),然后再獲取該許可昏滴。每個(gè) release()添加一個(gè)許可猴鲫,從而可能釋放一個(gè)正在阻塞的獲取者。但是影涉,不使用實(shí)際的許可對(duì)象变隔,Semaphore只對(duì)可用許可的號(hào)碼進(jìn)行計(jì)數(shù),并采取相應(yīng)的行動(dòng)蟹倾。信號(hào)量常常用于多線程的代碼中匣缘,比如數(shù)據(jù)庫連接池。

package com.mzc.common.concurrent;
?
import java.util.concurrent.Semaphore;
?
/**
 * <p class="detail">
 * 功能: Semaphore Test
 * </p>
 *
 * @author Moore
 * @ClassName Test semaphore.
 * @Version V1.0.
 * @date 2020.02.07 20:11:00
 */
public class TestSemaphore {
?
    static class Worker extends Thread{
        private int num;
        private Semaphore semaphore;
        public Worker(int num,Semaphore semaphore){
            this.num = num;
            this.semaphore = semaphore;
        }
        @Override
        public void run() {
            try {
                // 搶許可
                semaphore.acquire();
                Thread.sleep(2000);
                // 釋放許可
                semaphore.release();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
?
    public static void main(String[] args) {
        // 機(jī)器數(shù)目鲜棠,即5個(gè)許可
        Semaphore semaphore = new Semaphore(5);
        // 8個(gè)線程去搶許可
        for (int i = 0; i < 8; i++){
            new Worker(i,semaphore).start();
        }
    }
}

43肌厨、Java 中的線程池是如何實(shí)現(xiàn)的?

  • 在 Java 中,所謂的線程池中的“線程”豁陆,其實(shí)是被抽象為了一個(gè)靜態(tài) 內(nèi)部類 Worker柑爸,它基于 AQS 實(shí)現(xiàn),存放在線程池的HashSet<Worker> workers 成員變量中;
  • 而需要執(zhí)行的任務(wù)則存放在成員變量 workQueue(BlockingQueue<Runnable> workQueue)中盒音。這樣表鳍,整個(gè)線程池實(shí)現(xiàn)的基本思想就是:從 workQueue 中不斷取出 需要執(zhí)行的任務(wù),放在 Workers 中進(jìn)行處理祥诽。

44譬圣、線程池中的線程是怎么創(chuàng)建的?是一開始就隨著線程池的啟動(dòng)創(chuàng)建好的嗎?

答:顯然不是的。線程池默認(rèn)初始化后不啟動(dòng) Worker雄坪,等待有請(qǐng)求時(shí)才啟動(dòng)厘熟。每當(dāng)我們調(diào)用 execute() 方法添加一個(gè)任務(wù)時(shí),線程池會(huì)做如下判 斷:

  • 如果正在運(yùn)行的線程數(shù)量小于 corePoolSize,那么馬上創(chuàng)建線程運(yùn)行這個(gè)任務(wù);
  • 如果正在運(yùn)行的線程數(shù)量大于或等于 corePoolSize绳姨,那么將這個(gè)任務(wù)放入隊(duì)列;
  • 如果這時(shí)候隊(duì)列滿了登澜,而且正在運(yùn)行的線程數(shù)量小于maximumPoolSize,那么還是要?jiǎng)?chuàng)建非核心線程立刻運(yùn)行這個(gè)任務(wù);
  • 如果隊(duì)列滿了飘庄,而且正在運(yùn)行的線程數(shù)量大于或等于maximumPoolSize脑蠕,那么線程池會(huì)拋出異常RejectExecutionException。

當(dāng)一個(gè)線程完成任務(wù)時(shí)竭宰,它會(huì)從隊(duì)列中取下一個(gè)任務(wù)來執(zhí)行空郊。當(dāng)一個(gè)線程無事可做,超過一定的時(shí)間(keepAliveTime)時(shí)切揭,線程池會(huì)判斷狞甚。

如果當(dāng)前運(yùn)行的線程數(shù)大于 corePoolSize,那么這個(gè)線程就被停掉廓旬。所以線程池的所有任務(wù)完成后哼审,它最終會(huì)收縮到 corePoolSize 的大小。

45孕豹、什么是競(jìng)爭(zhēng)條件涩盾?如何發(fā)現(xiàn)和解決競(jìng)爭(zhēng)?

兩個(gè)線程同步操作同一個(gè)對(duì)象励背,使這個(gè)對(duì)象的最終狀態(tài)不明——叫做競(jìng)爭(zhēng)條件春霍。競(jìng)爭(zhēng)條件可以在任何應(yīng)該由程序員保證原子操作的,而又忘記使用synchronized的地方叶眉。

唯一的解決方案就是加鎖址儒。

Java有兩種鎖可供選擇:

  • 對(duì)象或者類(class)的鎖。每一個(gè)對(duì)象或者類都有一個(gè)鎖衅疙。使用synchronized關(guān)鍵字獲取莲趣。 synchronized加到static方法上面就使用類鎖,加到普通方法上面就用對(duì)象鎖饱溢。除此之外synchronized還可以用于鎖定關(guān)鍵區(qū)域塊(Critical Section)喧伞。 synchronized之后要制定一個(gè)對(duì)象(鎖的攜帶者),并把關(guān)鍵區(qū)域用大括號(hào)包裹起來绩郎。synchronized(this){// critical code}潘鲫。
  • 顯示構(gòu)建的鎖(java.util.concurrent.locks.Lock),調(diào)用lock的lock方法鎖定關(guān)鍵代碼肋杖。

46溉仑、很多人都說要慎用 ThreadLocal,談?wù)勀愕睦斫馐薹撸褂肨hreadLocal 需要注意些什么?

答:使 用 ThreadLocal 要 注 意 remove!

ThreadLocal 的實(shí)現(xiàn)是基于一個(gè)所謂的 ThreadLocalMap彼念,在ThreadLocalMap 中,它的 key 是一個(gè)弱引用浅萧。通常弱引用都會(huì)和引用隊(duì)列配合清理機(jī)制使用逐沙,但是 ThreadLocal 是 個(gè)例外,它并沒有這么做洼畅。這意味著吩案,廢棄項(xiàng)目的回收依賴于顯式地觸發(fā),否則就要等待線程結(jié) 束帝簇,進(jìn)而回收相應(yīng) ThreadLocalMap!這就是很多 OOM 的來源徘郭,所 以通常都會(huì)建議,應(yīng)用一定要自己負(fù)責(zé) remove丧肴,并且不要和線程池配 合残揉,因?yàn)?worker 線程往往是不會(huì)退出的。

線程與鎖

哲學(xué)家問題
問題描述:五位哲學(xué)家圍繞一個(gè)圓桌就做芋浮,桌上在每?jī)晌徽軐W(xué)家之間擺著一支筷子抱环。哲學(xué)家的狀態(tài)可能是“思考”或者“饑餓”。如果饑餓纸巷,哲學(xué)家將拿起他兩邊的筷子就餐一段時(shí)間镇草。進(jìn)餐結(jié)束后,哲學(xué)家就會(huì)放回筷子瘤旨。

代碼實(shí)現(xiàn):

public class Philosopher extends Thread {
    private Chopstick left;
    private Chopstick right;
    private Random random;
    
    public Philosopher(Chopstick left, Chopstick right) {
        this.left = left;
        this.right = right;
        random = new Random();
    }

    @Override
    public void run() {
        try {
            while (true) {
                Thread.sleep(random.nextInt(1000));  // 思考一會(huì)兒
                synchronized (left) {                       // 拿起左手的筷子
                    synchronized (right) {                  // 拿起右手的筷子
                        Thread.sleep(random.nextInt(1000)); // 進(jìn)餐
                    }
                }
            }
        } catch (InterruptedException e) {
            // handle exception
        }
    }
}

規(guī)避方法:
一個(gè)線程使用多把鎖時(shí)梯啤,就需要考慮死鎖的可能。幸運(yùn)的是存哲,如果總是按照一個(gè)全局的固定的順序獲得多把鎖因宇,就可以避開死鎖。

public class Philosopher2 extends Thread {
    private Chopstick first;
    private Chopstick second;
    private Random random;

    public Philosopher2(Chopstick left, Chopstick right) {
        if (left.getId() < right.getId()) {
            first = left;
            second = right;
        } else {
            first = right;
            second = left;
        }
        random = new Random();
    }

    @Override
    public void run() {
        try {
            while (true) {
                Thread.sleep(random.nextInt(1000));  // 思考一會(huì)兒
                synchronized (first) {                       // 拿起左手的筷子
                    synchronized (second) {                  // 拿起右手的筷子
                        Thread.sleep(random.nextInt(1000)); // 進(jìn)餐
                    }
                }
            }
        } catch (InterruptedException e) {
            // handle exception
        }
    }
}

外星方法
定義:調(diào)用這類方法時(shí)宏胯,調(diào)用者對(duì)方法的實(shí)現(xiàn)細(xì)節(jié)并不了解羽嫡。

public class Downloader extends Thread {
    private InputStream in;
    private OutputStream out;
    private ArrayList<ProgressListener> listeners;

    public Downloader(URL url, String outputFilename) throws IOException {
        in = url.openConnection().getInputStream();
        out = new FileOutputStream(outputFilename);
        listeners = new ArrayList<>();
    }

    public synchronized void addListener(ProgressListener listener) {
        listeners.add(listener);
    }

    public synchronized void removeListener(ProgressListener listener) {
        listeners.remove(listener);
    }

    private synchronized void updateProgress(int n) {
        for (ProgressListener listener : listeners) {
            listener.onProgress(n);
        }
    }

    @Override
    public void run() {
        // ...
    }
}

這里 updateProgress(n) 方法調(diào)用了一個(gè)外星方法,這個(gè)外星方法可能做任何事肩袍,比如持有另外一把鎖杭棵。

可以這樣來修改:

private  void updateProgress(int n) {
ArrayList<ProgressListener> listenersCopy;
    synchronized (this) {
        listenersCopy = (ArrayList<ProgressListener>) listeners.clone();
    }
        
    for (ProgressListener listener : listenersCopy) {
        listener.onProgress(n);
    }
}

線程與鎖模型帶來的三個(gè)主要危害:

競(jìng)態(tài)條件
死鎖
內(nèi)存可見性
規(guī)避原則:

對(duì)共享變量的所有訪問都需要同步化
讀線程和寫線程都需要同步化
按照約定的全局順序來獲取多把鎖
當(dāng)持有鎖時(shí)避免調(diào)用外星方法
持有鎖的時(shí)間應(yīng)盡可能短
內(nèi)置鎖
內(nèi)置鎖限制:

無法中斷 一個(gè)線程因?yàn)榈却齼?nèi)置鎖而進(jìn)入阻塞之后,就無法中斷該線程了氛赐;
無法超時(shí) 嘗試獲取內(nèi)置鎖時(shí)魂爪,無法設(shè)置超時(shí);
不靈活 獲得內(nèi)置鎖艰管,必須使用 synchronized 塊滓侍。

synchronized( object ) {
    <<使用共享資源>>
}

ReentrantLock
其提供了顯式的lock和unlock, 可以突破以上內(nèi)置鎖的幾個(gè)限制牲芋。

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    <<使用共享資源>>
} finally {
    lock.unlock()
}

可中斷
使用內(nèi)置鎖時(shí)撩笆,由于阻塞的線程無法被中斷捺球,程序不可能從死鎖中恢復(fù)。

內(nèi)置鎖:制造一個(gè)死鎖:

public class Uninterruptible {

    public static void main(String[] args) throws InterruptedException {
        final Object o1 = new Object();
        final Object o2 = new Object();

        Thread t1 = new Thread(){
            @Override
            public void run() {
                try {
                    synchronized (o1) {
                        Thread.sleep(1000);
                        synchronized (o2) {}
                    }
                } catch (InterruptedException e) {
                    System.out.println("Thread-1 interrupted");
                }
            }
        };

        Thread t2 = new Thread(){
            @Override
            public void run() {
                try {
                    synchronized (o2) {
                        Thread.sleep(1000);
                        synchronized (o1) {}
                    }
                } catch (InterruptedException e) {
                    System.out.println("Thread-2 interrupted");
                }
            }
        };

        t1.start();
        t2.start();
        Thread.sleep(2000);
        t1.interrupt();
        t2.interrupt();
        t1.join();
        t2.join();
    }

}

ReentrantLock 替代內(nèi)置鎖:

public class Interruptible {
    
    public static void main(String[] args) {
        final ReentrantLock lock1 = new ReentrantLock();
        final ReentrantLock lock2 = new ReentrantLock();
        
        Thread t1 = new Thread(){
            @Override
            public void run() {
                try {
                    lock1.lockInterruptibly();
                    Thread.sleep(1000);
                    lock2.lockInterruptibly();
                } catch (InterruptedException e) {
                    System.out.println("Thread-1 interrupted");
                }
            }
        };
        
        // ...
    }
}

可超時(shí)
利用 ReentrantLock 超時(shí)設(shè)置解決哲學(xué)家問題:

public class Philosopher3 extends Thread {
    private ReentrantLock leftChopstick;
    private ReentrantLock rightChopstick;
    private Random random;
    
    public Philosopher3(ReentrantLock leftChopstick, ReentrantLock rightChopstick) {
        this.leftChopstick = leftChopstick;
        this.rightChopstick = rightChopstick;
        random = new Random();
    }

    @Override
    public void run() {
        try {
            while (true) {
                Thread.sleep(random.nextInt(1000));  // 思考一會(huì)兒
                leftChopstick.lock();
                try {
                    // 獲取右手邊的筷子
                    if (rightChopstick.tryLock(1000, TimeUnit.MILLISECONDS)) {
                        try {
                            Thread.sleep(random.nextInt(1000));
                        } finally {
                            rightChopstick.unlock();
                        }
                    } else {
                        // 沒有獲取到右手邊的筷子夕冲,放棄并繼續(xù)思考
                    }
                } finally {
                    leftChopstick.unlock();
                }
            }
        } catch (InterruptedException e) {
            // ...
        }
    }
}

交替鎖

場(chǎng)景:在鏈表中插入一個(gè)節(jié)點(diǎn)時(shí)氮兵,使用交替鎖只鎖住鏈表的一部分,而不是用鎖保護(hù)整個(gè)鏈表歹鱼。

線程安全鏈表:

public class ConcurrentSortedList {  // 降序有序鏈表
    
    private class Node {
        int value;
        Node pre;
        Node next;
        
        ReentrantLock lock = new ReentrantLock();
        
        Node() {}
        
        Node(int value, Node pre, Node next) {
            this.value = value;
            this.pre = pre;
            this.next = next;
        }
    }
    
    private final Node head;
    private final Node tail;
    
    public ConcurrentSortedList() {
        this.head = new Node();
        this.tail = new Node();
        this.head.next = tail;
        this.tail.pre = head;
    }
    
    public void insert(int value) {
        Node current = this.head;
        current.lock.lock();
        Node next = current.next;
        try {
            while (true) {
                next.lock.lock();
                try {
                    if (next == tail || next.value < value) {
                        Node newNode = new Node(value, current, next);
                        next.pre = newNode;
                        current.next = newNode;
                        return;
                    }
                } finally {
                    current.lock.unlock();
                }
                current = next;
                next = current.next;
                
            }
        } finally {
            next.lock.unlock();
        }
    }
    
    public int size() { 
        Node current = tail; // 這里為什么要是從尾部開始遍歷呢泣栈? 因?yàn)椴迦胧菑念^部開始遍歷的
        int count = 0;
        while (current != head) {
            ReentrantLock lock = current.lock;
            lock.lock();
            try {
                ++count;
                current = current.pre;
            } finally {
                lock.unlock();
            }
        }
        return count;
    }
}

條件變量

并發(fā)編程經(jīng)常要等待某個(gè)條件滿足。比如從隊(duì)列刪除元素必須等待隊(duì)列不為空弥姻、向緩存添加數(shù)據(jù)前需要等待緩存有足夠的空間南片。

條件變量模式:

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newConditiion();

lock.lock();
try {
    while(!<<條件為真>>) {  // 條件不為真時(shí)
        condition.await();
    }
    <<使用共享資源>>
} finnally {
    lock.unlock();
}

一個(gè)條件變量需要與一把鎖關(guān)聯(lián),線程在開始等待條件之前必須獲得鎖庭敦。獲取鎖后疼进,線程檢查等待的條件是否為真。

如果為真秧廉,線程將繼續(xù)執(zhí)行并解鎖颠悬;
如果不為真,線程會(huì)調(diào)用 await()定血,它將原子的解鎖并阻塞等待條件赔癌。
當(dāng)另一個(gè)線程調(diào)用 signal() 或 signalAll(),意味著對(duì)應(yīng)的條件可能變?yōu)檎妫?await() 將原子的恢復(fù)運(yùn)行并重新加鎖澜沟。

條件變量解決哲學(xué)家就餐問題:

public class Philosopher4 extends Thread {

    private boolean eating;
    private Philosopher4 left;
    private Philosopher4 right;
    private ReentrantLock table;
    private Condition condition;
    private Random random;

    public Philosopher4(ReentrantLock table) {
        this.eating = false;
        this.table = table;
        this.condition = table.newCondition();
        this.random = new Random();
    }

    public void setLeft(Philosopher4 left) {
        this.left = left;
    }

    public void setRight(Philosopher4 right) {
        this.right = right;
    }

    @Override
    public void run() {
        try {
            while (true) {
                think();
                eat();
            }
        } catch (InterruptedException e) {
            // ...
        }
    }

    private void think() throws InterruptedException {
        this.table.lock();
        try {
            this.eating = false;
            this.left.condition.signal();
            this.right.condition.signal();
        } finally {
            table.unlock();
        }
        Thread.sleep(1000);
    }

    private void eat() throws InterruptedException {
        this.table.lock();
        try {
            while (left.eating || right.eating) {
                this.condition.await();
            }
            this.eating = true;
        } finally {
            this.table.unlock();
        }
        Thread.sleep(1000);
    }
}

原子變量

原子變量是無鎖(lock-free) 非阻塞(non-blocking)算法的基礎(chǔ)灾票,這種算法可以不用鎖和阻塞來達(dá)到同步的目的。

參考資料

http://www.reibang.com/p/e65f7b6c2a94
https://www.cnblogs.com/jxldjsn/p/10872154.html
https://www.cnblogs.com/sgh1023/p/10297322.html
https://blog.csdn.net/u011780616/article/details/95339236
http://www.reibang.com/p/ca98ca34b47e


Kotlin 開發(fā)者社區(qū)

國內(nèi)第一Kotlin 開發(fā)者社區(qū)公眾號(hào)茫虽,主要分享刊苍、交流 Kotlin 編程語言、Spring Boot濒析、Android正什、React.js/Node.js、函數(shù)式編程号杏、編程思想等相關(guān)主題婴氮。

越是喧囂的世界,越需要寧靜的思考盾致。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末主经,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子庭惜,更是在濱河造成了極大的恐慌罩驻,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件护赊,死亡現(xiàn)場(chǎng)離奇詭異惠遏,居然都是意外死亡砾跃,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門节吮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蜓席,“玉大人,你說我怎么就攤上這事课锌。” “怎么了祈秕?”我有些...
    開封第一講書人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵渺贤,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我请毛,道長(zhǎng)志鞍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任方仿,我火速辦了婚禮固棚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仙蚜。我一直安慰自己此洲,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開白布委粉。 她就那樣靜靜地躺著呜师,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贾节。 梳的紋絲不亂的頭發(fā)上汁汗,一...
    開封第一講書人閱讀 49,730評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音栗涂,去河邊找鬼知牌。 笑死,一個(gè)胖子當(dāng)著我的面吹牛斤程,可吹牛的內(nèi)容都是我干的角寸。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼忿墅,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼袭厂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起球匕,我...
    開封第一講書人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤纹磺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后亮曹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體橄杨,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡秘症,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了式矫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乡摹。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖采转,靈堂內(nèi)的尸體忽然破棺而出聪廉,到底是詐尸還是另有隱情,我是刑警寧澤故慈,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布板熊,位于F島的核電站,受9級(jí)特大地震影響察绷,放射性物質(zhì)發(fā)生泄漏干签。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一拆撼、第九天 我趴在偏房一處隱蔽的房頂上張望容劳。 院中可真熱鬧,春花似錦闸度、人聲如沸竭贩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽娶视。三九已至,卻和暖如春睁宰,著一層夾襖步出監(jiān)牢的瞬間肪获,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來泰國打工柒傻, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留孝赫,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓红符,卻偏偏與公主長(zhǎng)得像青柄,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子预侯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • Java SE 基礎(chǔ): 封裝致开、繼承、多態(tài) 封裝: 概念:就是把對(duì)象的屬性和操作(或服務(wù))結(jié)合為一個(gè)獨(dú)立的整體萎馅,并盡...
    Jayden_Cao閱讀 2,103評(píng)論 0 8
  • 本文出自 Eddy Wiki 双戳,轉(zhuǎn)載請(qǐng)注明出處:http://eddy.wiki/interview-java.h...
    eddy_wiki閱讀 2,066評(píng)論 0 14
  • 下面最近發(fā)的一些并發(fā)編程的文章匯總,通過閱讀這些文章大家再看大廠面試中的并發(fā)編程問題就沒有那么頭疼了糜芳。今天給大家總...
    架構(gòu)師springboot閱讀 678評(píng)論 0 3
  • 1飒货、Synchronized關(guān)鍵字 1魄衅、方法中的變量不存在非線程安全問題,都是線程安全的塘辅。 2晃虫、兩個(gè)線程訪問同一個(gè)...
    AI喬治閱讀 1,191評(píng)論 1 7
  • 寫這個(gè)字很簡(jiǎn)單,做到卻很難扣墩,原因各式各樣哲银,好像都挺有道理的。 喜歡一件事呻惕,堅(jiān)持做一件事荆责,認(rèn)真的對(duì)待,無論人前人后蟆融,...
    希雅的花園閱讀 31評(píng)論 0 0