如下面試題,基於網絡整理,和本身編輯。具體參考的文章,會在文末給出全部的連接。html
若是胖友有本身的疑問,歡迎在星球提問,咱們一塊兒整理吊吊的 Java【併發】面試題的大保健。java
而題目的難度,艿艿儘可能按照從容易到困難的順序,逐步下去。git
由於 Java 併發涉及到的內容會很是多,本面試題可能很難覆蓋到全部的知識點,因此推薦 《Java併發編程的藝術》 。而且,本文會將面試題和該書的章節,大致保持一致。嘻嘻~程序員
另外,本文涉及的面試題會超級超級超級多,因此艿艿已經分了小節,胖友要注意喲。github
🦅 程序web
程序,是含有指令和數據的文件,被存儲在磁盤或其餘的數據存儲設備中,也就是說程序是靜態的代碼。面試
🦅 進程算法
進程,是程序的一次執行過程,是系統運行程序的基本單位,所以進程是動態的。系統運行一個程序便是一個進程從建立,運行到消亡的過程。簡單來講,一個進程就是一個執行中的程序,它在計算機中一個指令接着一個指令地執行着,同時,每一個進程還佔有某些系統資源如CPU時間,內存空間,文件,文件,輸入輸出設備的使用權等等。換句話說,當程序在執行時,將會被操做系統載入內存中。shell
🦅 線程數據庫
線程,與進程類似,但線程是一個比進程更小的執行單位。一個進程在其執行的過程當中能夠產生多個線程。與進程不一樣的是同類的多個線程共享同一塊內存空間和一組系統資源,因此係統在產生一個線程,或是在各個線程之間做切換工做時,負擔要比進程小得多,也正由於如此,線程也被稱爲輕量級進程。
艿艿:以下是可選內容。
另外,Java 線程是重量級的,每一個線程默認使用 1024KB 的內存,因此一個 Java 進程是沒法開啓大量線程的。感興趣的胖友,能夠看 《Java 中的輕量級線程?》 的討論,沒準將來 Java 也有內置的協程(Coroutine)。
🦅 三者之間的關係
🦅 線程有什麼優缺點?
1)好處
2)壞處
🦅 你瞭解守護線程嗎?它和非守護線程有什麼區別?
Java 中的線程分爲兩種:守護線程(Daemon)和用戶線程(User)。
Thread#setDaemon(boolean on)
設置。true
則把該線程設置爲守護線程,反之則爲用戶線程。Thread#setDaemon(boolean on)
方法,必須在Thread#start()
方法以前調用,不然運行時會拋出異常。惟一的區別是:
程序運行完畢,JVM 會等待非守護線程完成後關閉,可是 JVM 不會等待守護線程。
擴展:Thread Dump 打印出來的線程信息,含有 daemon 字樣的線程即爲守護進程。可能會有:服務守護進程、編譯守護進程、Windows 下的監聽 Ctrl + break 的守護進程、Finalizer 守護進程、引用處理守護進程、GC 守護進程。
關於守護線程的各類騷操做,能夠看看 《Java 守護線程概述》 。
🦅 什麼是線程組,爲何在 Java 中不推薦使用?
艿艿:這是個小衆知識,瞭解便可。貌似,艿艿也重來沒使用過這個類。
ThreadGroup 類,能夠把線程歸屬到某一個線程組中,線程組中能夠有線程對象,也能夠有線程組,組中還能夠有線程,這樣的組織結構有點相似於樹的形式。
簡單的說,ThreadGroup 爲了方便線程的管理。
爲何不推薦使用?ThreadGroup API 比較薄弱,它並無比 Thread 提供了更多的功能。它有兩個主要的功能:一是獲取線程組中處於活躍狀態線程的列表;二是設置爲線程設置未捕獲異常處理器(uncaught exception handler)。但在 Java5 中 Thread 類也添加了 #setUncaughtExceptionHandler(UncaughtExceptionHandler eh)
方法,因此 ThreadGroup 是已通過時的,不建議繼續使用。
多線程會共同使用一組計算機上的 CPU ,而線程數大於給程序分配的 CPU 數量時,爲了讓各個線程都有執行的機會,就須要輪轉使用 CPU 。
不一樣的線程切換使用 CPU 發生的切換數據等,就是上下文切換。
🦅 Java 中用到的線程調度算法是什麼?
假設計算機只有一個 CPU ,則在任意時刻只能執行一條機器指令,每一個線程只有得到 CPU 的使用權才能執行指令。
有兩種調度模型:分時調度模型和搶佔式調度模型。
分時調度模型是指讓全部的線程輪流得到 CPU 的使用權,而且平均分配每一個線程佔用的 CPU 的時間片這個也比較好理解。
Java 虛擬機採用搶佔式調度模型,是指優先讓可運行池中優先級高的線程佔用 CPU ,若是可運行池中的線程優先級相同,那麼就隨機選擇一個線程,使其佔用 CPU 。處於運行狀態的線程會一直運行,直至它不得不放棄 CPU 。
如非特別須要,儘可能不要用,防止線程飢餓。
🦅 什麼是線程飢餓?
飢餓,一個或者多個線程由於種種緣由沒法得到所須要的資源,致使一直沒法執行的狀態。
Java 中致使飢餓的緣由:
🦅 你對線程優先級的理解是什麼?
每個線程都是有優先級的,通常來講,高優先級的線程在運行時會具備優先權,但這依賴於線程調度的實現,這個實現是和操做系統相關的(OS dependent)。
線程一共有五個狀態,分別以下:
新建(new):當建立Thread類的一個實例(對象)時,此線程進入新建狀態(未被啓動)。例如:Thread t1 = new Thread()
。
可運行(runnable):線程對象建立後,其餘線程(好比 main 線程)調用了該對象的 start 方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取 cpu 的使用權。例如:t1.start()
。
有些文章,會稱可運行(runnable)爲就緒,意思是同樣的。
運行(running):線程得到 CPU 資源正在執行任務(#run()
方法),此時除非此線程自動放棄 CPU 資源或者有優先級更高的線程進入,線程將一直運行到結束。
死亡(dead):當線程執行完畢或被其它線程殺死,線程就進入死亡狀態,這時線程不可能再進入就緒狀態等待執行。
#run()
方法,終止。#stop()
方法,讓一個線程終止運行。堵塞(blocked):因爲某種緣由致使正在運行的線程讓出 CPU 並暫停本身的執行,即進入堵塞狀態。直到線程進入可運行(runnable)狀態,纔有機會再次得到 CPU 資源,轉到運行(running)狀態。阻塞的狀況有三種:
正在睡眠:調用 #sleep(long t)
方法,可以使線程進入睡眠方式。
一個睡眠着的線程在指定的時間過去可進入可運行(runnable)狀態。
正在等待:調用 #wait()
方法。
調用
notify()
方法,回到就緒狀態。
被另外一個線程所阻塞:調用 #suspend()
方法。
調用
#resume()
方法,就能夠恢復。
總體以下圖所示:
以下是另一個圖,把阻塞的狀況,放在了一塊兒,也能夠做爲參考:
無心中,又看到一張畫的更牛逼的,以下圖:
🦅 如何結束一個一直運行的線程?
通常來講,有兩種方式:
方式一,使用退出標誌,這個 flag 變量要多線程可見。
在這種方式中,之因此引入共享變量,是由於該變量能夠被多個執行相同任務的線程用來做爲是否中斷的信號,通知中斷線程的執行。
方式二,使用 interrupt 方法,結合 isInterrupted 方法一塊兒使用。
若是一個線程因爲等待某些事件的發生而被阻塞,又該怎樣中止該線程呢?這種狀況常常會發生,好比當一個線程因爲須要等候鍵盤輸入而被阻塞,或者調用
Thread#join()
方法,或者Thread#sleep(...)
方法,在網絡中調用ServerSocket#accept()
方法,或者調用了DatagramSocket#receive()
方法時,都有可能致使線程阻塞,使線程處於處於不可運行狀態時。即便主程序中將該線程的共享變量設置爲true
,但該線程此時根本沒法檢查循環標誌,固然也就沒法當即中斷。這裏咱們給出的建議是,不要使用
Thread#stop()· 方法,而是使用 Thread 提供的
#interrupt()` 方法,由於該方法雖然不會中斷一個正在運行的線程,可是它能夠使一個被阻塞的線程拋出一箇中斷異常,從而使線程提早結束阻塞狀態,退出堵塞代碼。
因此,方式一和方式二,並非衝突的兩種方式,而是可能根據實際場景下,進行結合。
🦅 一個線程若是出現了運行時異常會怎麼樣?
若是這個異常沒有被捕獲的話,這個線程就中止執行了。
另外重要的一點是:若是這個線程持有某個某個對象的監視器,那麼這個對象監視器會被當即釋放。
Java 中建立線程主要有三種方式:
具體的每種方式的代碼實現,能夠看看 《Java建立線程的四種方式》 。
關於文章中的方式四,實際是基於線程池的方式,使用下面的三種方式,也是生產實踐中,最爲推薦和經常使用的方式。
建立線程的三種方式的對比:
Thread#currentThread()
方法,直接使用 this
便可得到當前線程。線程類只是實現了 Runnable 接口或 Callable 接口,還能夠繼承其餘類。
在這種方式下,多個線程能夠共享同一個 target
對象,因此很是適合多個相同線程來處理同一份資源的狀況,從而能夠將 CPU、代碼和數據分開,造成清晰的模型,較好地體現了面向對象的思想。
Runnable runner = new Runnable(){ ... }; |
【最重要】能夠使用線程池。
Thread#currentThread()
方法。🦅 start 和 run 方法有什麼區別?
一個線程運行時發生異常會怎樣?
若是異常沒有被捕獲該線程將會中止執行。Thread.UncaughtExceptionHandler
是用於處理未捕獲異常形成線程忽然中斷狀況的一個內嵌接口。當一個未捕獲異常將形成線程中斷的時候 JVM 會使用 Thread#getUncaughtExceptionHandler()
方法來查詢線程的 UncaughtExceptionHandler 並將線程和異常做爲參數傳遞給 handler 的 #uncaughtException(exception)
方法進行處理。
具體的使用,能夠看看 《JAVA 多線程之 UncaughtExceptionHandler —— 處理非正常的線程停止》 。
wait + notify 對於大多數胖友,一開始理解可能會比較困難,多看多理解吧。
在 Java 發展史上,曾經使用 suspend、resume 方法對於線程進行阻塞喚醒,但隨之出現不少問題,比較典型的仍是死鎖問題。
解決方案能夠使用以對象爲目標的阻塞,即利用 Object 類的 wait 和 notify方法實現線程阻塞。
synchronized
塊或方法中被調用,而且要保證同步塊或方法的鎖對象與調用 wait、notify 方法的對象是同一個,如此一來在調用 wait 以前當前線程就已經成功獲取某對象的鎖,執行 wait 阻塞後當前線程就將以前獲取的對象鎖釋放。具體的實現,看看 《Wait / Notify通知機制解析》 文章。
經過 wait + notify 的組合,能夠通知機制,不過咱們也能夠使用其它工具,胖友能夠思考下。例如以下的每個方式:
艿艿:這個問題能夠衍生下,Java 如何實現多線程之間的通信和協做?具體的能夠看看 《Java多線程——線程間協做方式總結及使用示例》 文章,固然不只限於該文章所提供的方式。😈 胖友能夠認真思索下。
🦅 Thread類的 sleep 方法和對象的 wait 方法均可以讓線程暫停執行,它們有什麼區別?
關於這個問題,能夠結合 「線程的生命週期?」 問題的圖,一塊兒瞅瞅。
#wait()
方法,會致使當前線程放棄對象的鎖(線程暫停執行),進入對象的等待池(wait pool),只有調用對象的 #notify()
方法(或#notifyAll()
方法)時,才能喚醒等待池中的線程進入等鎖池(lock pool),若是線程從新得到對象的鎖就能夠進入就緒狀態。🦅 請說出與線程同步以及線程調度相關的方法?
🦅 notify 和 notifyAll 有什麼區別?
當一個線程進入 wait 以後,就必須等其餘線程 notify/notifyAll 。
關於 notify 的信息丟失,能夠看看 《wait 和 notify 的坑》 文章。
🦅 爲何 wait, notify 和 notifyAll 這三方法不在 Thread 類裏面?
一個很明顯的緣由是 Java 提供的鎖是對象級的而不是線程級的,每一個對象都有鎖,經過線程得到。
因爲 wait,notify 和 notifyAll 方法都是鎖級別的操做,因此把它們定義在 Object 類中,由於鎖屬於對象。
🦅 爲何 wait 和 notify 方法要在同步塊中調用?
🦅 爲何你應該在循環中檢查等待條件?
處於等待狀態的線程可能會收到錯誤警報和僞喚醒,若是不在循環中檢查等待條件,程序就會在沒有知足結束條件的狀況下退出。
因此,咱們不能寫 if (condition)
而應該是 while (condition)
,特別是 CAS 競爭的時候。示例代碼以下:
// The standard idiom for using the wait method |
1)sleep 方法
在指定的毫秒數內,讓當前正在執行的線程休眠(暫停執行),此操做受到系統計時器和調度程序精度和準確性的影響。讓其餘線程有機會繼續執行,但它並不釋放對象鎖。也就是若是有synchronized
同步塊,其餘線程仍然不能訪問共享數據。注意該方法要捕獲異常。
好比有兩個線程同時執行(沒有 synchronized
),一個線程優先級爲MAX_PRIORITY
,另外一個爲 MIN_PRIORITY
。
#sleep(5000)
後,低優先級就有機會執行了。2)yield 方法
yield 方法和 sleep 方法相似,也不會釋放「鎖標誌」,區別在於:
3)join 方法
Thread 的非靜態方法 join ,讓一個線程 B 「加入」到另一個線程 A 的尾部。在線程 A 執行完畢以前,線程 B 不能工做。示例代碼以下:
Thread t = new MyThread(); |
t
完成爲止。然而,若是它加入的線程 t
沒有存活,則當前線程不須要中止。🦅 線程的 sleep 方法和 yield 方法有什麼區別?
艿艿:實際場景下,咱們不多使用 yield 方法噢。
🦅 爲何 Thread 類的 sleep 和 yield 方法是靜態的?
Thread 類的 sleep 和 yield 方法,將在當前正在執行的線程上運行。因此在其餘處於等待狀態的線程上調用這些方法是沒有意義的。這就是爲何這些方法是靜態的。它們能夠在當前正在執行的線程中工做,並避免程序員錯誤的認爲能夠在其餘非運行線程調用這些方法。
🦅 sleep(0) 有什麼用途?
Thread#sleep(0)
方法,並不是是真的要線程掛起 0 毫秒,意義在於此次調用 Thread#sleep(0)
方法,把當前線程確實的被凍結了一下,讓其餘線程有機會優先執行。Thread#sleep(0)
方法,是你的線程暫時放棄 CPU ,也就是釋放一些未用的時間片給其餘線程或進程使用,就至關於一個讓位動做。
感興趣的胖友,能夠看看 《Sleep(0) 的妙用》 的示例。
🦅 你如何確保 main 方法所在的線程是 Java 程序最後結束的線程?
考點,就是 join 方法。
咱們能夠使用 Thread 類的 #join()
方法,來確保全部程序建立的線程在 main 方法退出前結束。
1)interrupt 方法
Thread#interrupt()
方法,用於中斷線程。調用該方法的線程的狀態爲將被置爲」中斷」狀態。
注意:線程中斷僅僅是置線程的中斷狀態位,不會中止線程。須要用戶本身去監視線程的狀態爲並作處理。支持線程中斷的方法(也就是線程中斷後會拋出 InterruptedException 的方法)就是在監視線程的中斷狀態,一旦線程的中斷狀態被置爲「中斷狀態」,就會拋出中斷異常。
2)interrupted
Thread#interrupted()
靜態方法,查詢當前線程的中斷狀態,而且清除原狀態。若是一個線程被中斷了,第一次調用 #interrupted()
方法則返回 true
,第二次和後面的就返回 false
了。
// Thread.java |
3)interrupted
Thread#isInterrupted()
方法,查詢指定線程的中斷狀態,不會清除原狀態。代碼以下:
// Thread.java |
線程安全,是編程中的術語,指某個函數、函數庫在多線程環境中被調用時,可以正確地處理多個線程之間的共享變量,使程序功能正確完成。
🦅 Servlet 是線程安全嗎?
Servlet 不是線程安全的,Servlet 是單實例多線程的,當多個線程同時訪問同一個方法,是不能保證共享變量的線程安全性的。
🦅 Struts2 是線程安全嗎?
Struts2 的 Action 是多實例多線程的,是線程安全的,每一個請求過來都會 new
一個新的 Action 分配給這個請求,請求完成後銷燬。
🦅 SpringMVC 是線程安全嗎?
不是的,和 Servlet 相似的處理流程。
🦅 單例模式的線程安全性?
老生常談的問題了,首先要說的是單例模式的線程安全意味着:某個類的實例在多線程環境下只會被建立一次出來。單例模式有不少種的寫法,我總結一下:
1)線程同步
線程同步,是指線程之間所具備的一種制約關係,一個線程的執行依賴另外一個線程的消息,當它沒有獲得另外一個線程的消息時應等待,直到消息到達時才被喚醒。
線程間的同步方法,大致可分爲兩類:用戶模式和內核模式。顧名思義:
2)線程互斥
線程互斥,是指對於共享的進程系統資源,在各單個線程訪問時的排它性。
🦅 如何在兩個線程間共享數據?
在兩個線程間共享變量,便可實現共享。
通常來講,共享變量要求變量自己是線程安全的,而後在線程內使用的時候,若是有對共享變量的複合操做,那麼也得保證複合操做的線程安全性。
🦅 怎麼檢測一個線程是否擁有鎖?
調用 Thread#holdsLock(Object obj)
靜態方法,它返回 true
若是當且僅當當前線程擁有某個具體對象的鎖。代碼以下:
// Thread.java |
🦅 10 個線程和 2 個線程的同步代碼,哪一個更容易寫?
從寫代碼的角度來講,二者的複雜度是相同的,由於同步代碼與線程數量是相互獨立的。
可是同步策略的選擇依賴於線程的數量,由於越多的線程意味着更大的競爭,因此你須要利用同步技術,如鎖分離,這要求更復雜的代碼和專業知識。
ThreadLocal ,是 Java 裏一種特殊的變量。每一個線程都有一個 ThreadLocal 就是每一個線程都擁有了本身獨立的一個變量,競爭條件被完全消除了。
它是爲建立代價高昂的對象獲取線程安全的好方法,好比你能夠用 ThreadLocal 讓 SimpleDateFormat 變成線程安全的,由於那個類建立代價高昂且每次調用都須要建立不一樣的實例因此不值得在局部範圍使用它,若是爲每一個線程提供一個本身獨有的變量拷貝,將大大提升效率。
😈 因此,ThreadLocal 很適合實現線程級的單例。
詳細的,能夠看看 《Java併發編程:深刻剖析ThreadLocal》 文章。
關於源碼,能夠看看 《【死磕 Java 併發】—– 深刻分析 ThreadLocal》 。
🦅 什麼是 InheritableThreadLocal ?
InheritableThreadLocal 類,是 ThreadLocal 類的子類。ThreadLocal 中每一個線程擁有它本身的值,與 ThreadLocal 不一樣的是,InheritableThreadLocal 容許一個線程以及該線程建立的全部子線程均可以訪問它保存的值。
🦅 在多線程環境下,SimpleDateFormat 是線程安全的嗎?
不是,很是不幸,DateFormat 的全部實現,包括 SimpleDateFormat 都不是線程安全的,所以你不該該在多線程序中使用,除非是在對外線程安全的環境中使用,如將 SimpleDateFormat 限制在 ThreadLocal 中。
若是你不這麼作,在解析或者格式化日期的時候,可能會獲取到一個不正確的結果。所以,從日期、時間處理的全部實踐來講,我強力推薦 joda-time 庫。
kill -3 [java pid]
不會在當前終端輸出,它會輸出到代碼執行的或指定的地方去。好比, kill -3 tomcat pid
, 輸出堆棧到 log 目錄下。
Jstack [java pid]
這個比較簡單,在當前終端顯示,也能夠重定向到指定文件中。
不作說明,打開 JVisualVM 後,都是界面操做,過程仍是很簡單的。
java.util.Timer
,是一個工具類,能夠用於安排一個線程在將來的某個特定時間執行。Timer 類能夠用安排一次性任務或者週期任務。
java.util.TimerTask
,是一個實現了 Runnable 接口的抽象類,咱們須要去繼承這個類來建立咱們本身的定時任務並使用 Timer 去安排它的執行。
目前有開源的 Qurtz 能夠用來建立定時任務。
一、給線程命名。
這樣能夠方便找 bug 或追蹤。OrderProcessor、QuoteProcessor、TradeProcessor 這種名字比 Thread-一、Thread-二、Thread-3 好多了,給線程起一個和它要完成的任務相關的名字,全部的主要框架甚至JDK都遵循這個最佳實踐。
二、最小化同步範圍。
鎖花費的代價高昂且上下文切換更耗費時間空間,試試最低限度的使用同步和鎖,縮小臨界區。所以相對於同步方法我更喜歡同步塊,它給我擁有對鎖的絕對控制權。
三、優先使用 volatile
,而不是 synchronized
。
四、儘量使用更高層次的併發工具而非 wait 和 notify 方法來實現線程通訊。
首先,CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 這些同步類簡化了編碼操做,而用 wait 和 notify 很難實現對複雜控制流的控制。
其次,這些類是由最好的企業編寫和維護在後續的 JDK 中它們還會不斷優化和完善,使用這些更高等級的同步工具你的程序能夠不費吹灰之力得到優化。
五、優先使用併發容器,而非同步容器。
這是另一個容易遵循且受益巨大的最佳實踐,併發容器比同步容器的可擴展性更好,因此在併發編程時使用併發集合效果更好。若是下一次你須要用到 Map ,咱們應該首先想到用 ConcurrentHashMap 類。
六、考慮使用線程池。
併發(Concurrency)和並行(Parallellism)是:
因此併發編程的目標是,充分的利用處理器的每個核,以達到最高的處理性能。
若是數據將在線程間共享。例如正在寫的數據之後可能被另外一個線程讀到,或者正在讀的數據可能已經被另外一個線程寫過了,那麼這些數據就是共享數據,必須進行同步存取。
當應用程序在對象上調用了一個須要花費很長時間來執行的方法,而且不但願讓程序等待方法的返回時,就應該使用異步編程,在不少狀況下采用異步途徑每每更有效率。
固然,若是咱們對效率沒有特別大的要求,也不必定須要使用異步編程,由於它會帶來編碼的複雜性。總之,合適纔是正確的。
synchronized
volatile
synchronized
的原理是什麼?synchronized
是 Java 內置的關鍵字,它提供了一種獨佔的加鎖方式。
synchronized
的獲取和釋放鎖由JVM實現,用戶不須要顯示的釋放鎖,很是方便。synchronized
也有必定的侷限性。
關於原理,直接閱讀 《【死磕 Java 併發】—– 深刻分析 synchronized 的實現原理》 文章,有幾個重點要注意看。
🦅 當一個線程進入某個對象的一個 synchronized
的實例方法後,其它線程是否可進入此對象的其它方法?
synchronized
的話,其餘線程是能夠進入的。🦅 同步方法和同步塊,哪一個是更好的選擇?
同步塊是更好的選擇,由於它不會鎖住整個對象(固然你也可讓它鎖住整個對象)。同步方法會鎖住整個對象,哪怕這個類中有多個不相關聯的同步塊,這一般會致使他們中止執行並須要等待得到這個對象上的鎖。
同步塊更要符合開放調用的原則,只在須要鎖住的代碼塊鎖住相應的對象,這樣從側面來講也能夠避免死鎖。
🦅 在監視器(Monitor)內部,是如何作線程同步的?
監視器和鎖在 Java 虛擬機中是一塊使用的。監視器監視一塊同步代碼塊,確保一次只有一個線程執行同步代碼塊。每個監視器都和一個對象引用相關聯。線程在獲取鎖以前不容許執行同步代碼。
🦅 Java 如何實現「自旋」(spin)
代碼以下:
public class SpinLock { |
<1>
處,#lock()
方法,若是得到不到鎖,就會「死循環」,直到或獲得鎖爲止。考慮到「死循環」會持續佔用 CPU ,可能致使其它線程沒法得到到 CPU 執行,能夠在 <1.1>
處增長 Thread.yiead()
代碼段,出讓下 CPU 。<2>
處,#unlock()
方法,釋放鎖。volatile
涉及的內容,其實蠻多的,因此胖友直接看:
🦅 volatile
有什麼用?
volatile
保證內存可見性和禁止指令重排。
同時,
volatile
能夠提供部分原子性。
簡單來講,volatile
用於多線程環境下的單次操做(單次讀或者單次寫)。
🦅 volatile
變量和 atomic 變量有什麼不一樣?
volatile
變量,能夠確保先行關係,即寫操做會發生在後續的讀操做以前,但它並不能保證原子性。例如用 volatile
修飾 count
變量,那麼 count++
操做就不是原子性的。#getAndIncrement()
方法,會原子性的進行增量操做把當前值加一,其它數據類型和引用變量也能夠進行類似操做。🦅 能夠建立 volatile
數組嗎?
Java 中能夠建立 volatile
類型數組,不過只是一個指向數組的引用,而不是整個數組。若是改變引用指向的數組,將會受到 volatile
的保護,可是若是多個線程同時改變數組的元素,volatile
標示符就不能起到以前的保護做用了。
同理,對於 Java POJO 類,使用 volatile
修飾,只能保證這個引用的可見性,不能保證其內部的屬性。
🦅 volatile
能使得一個非原子操做變成原子操做嗎?
一個典型的例子是在類中有一個 long
類型的成員變量。若是你知道該成員變量會被多個線程訪問,如計數器、價格等,你最好是將其設置爲 volatile
。爲何?由於 Java 中讀取 long
類型變量不是原子的,須要分紅兩步,若是一個線程正在修改該 long
變量的值,另外一個線程可能只能看到該值的一半(前 32 位)。可是對一個 volatile
型的 long
或 double
變量的讀寫是原子。
以下的內容,能夠做爲上面的內容的補充。
一種實踐是用
volatile
修飾long
和double
變量,使其能按原子類型來讀寫。double
和long
都是64位寬,所以對這兩種類型的讀是分爲兩部分的,第一次讀取第一個 32 位,而後再讀剩下的 32 位,這個過程不是原子的,但 Java 中volatile
型的long
或double
變量的讀寫是原子的。
🦅 volatile
類型變量提供什麼保證?
volatile
主要有兩方面的做用:
例如,JVM 或者 JIT 爲了得到更好的性能會對語句重排序,可是 volatile
類型變量即便在沒有同步塊的狀況下賦值也不會與其餘語句重排序。
volatile
提供 happens-before 的保證,確保一個線程的修改能對其餘線程是可見的。volatile
還能提供原子性,如讀 64 位數據類型,像 long
和 double
都不是原子的(低 32 位和高 32 位),但 volatile
類型的 double
和 long
就是原子的。不過須要在 64 位的 JVM 虛擬機上。詳細的分析,能夠看看 《Java中 long 和 double 的原子性》 。🦅 volatile
和 synchronized
的區別?
volatile
本質是在告訴 JVM 當前變量在寄存器(工做內存)中的值是不肯定的,須要從主存中讀取。synchronized
則是鎖定當前變量,只有當前線程能夠訪問該變量,其餘線程被阻塞住。volatile
僅能使用在變量級別。synchronized
則能夠使用在變量、方法、和類級別的。volatile
僅能實現變量的修改可見性,不能保證原子性。而synchronized
則能夠保證變量的修改可見性和原子性。volatile
不會形成線程的阻塞。synchronized
可能會形成線程的阻塞。volatile
標記的變量不會被編譯器優化。synchronized
標記的變量能夠被編譯器優化。另外,會有面試官會問
volatile
可否取代synchronized
呢?答案確定是不能,雖說volatile
被稱之爲輕量級鎖,可是和synchronized
是有本質上的區別,緣由就是上面的幾點落。
🦅 什麼場景下能夠使用 volatile
替換 synchronized
?
volatile
替代,synchronized
保證可操做的原子性一致性和可見性。volatile
適用於新值不依賴於舊值的情形。volatile
。死鎖,是指兩個或兩個以上的進程(或線程)在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。
產生死鎖的必要條件:
死鎖的解決方法:
🦅 什麼是活鎖?
活鎖,任務或者執行者沒有被阻塞,因爲某些條件沒有知足,致使一直重複嘗試,失敗,嘗試,失敗。
🦅 死鎖與活鎖的區別?
活鎖和死鎖的區別在於,處於活鎖的實體是在不斷的改變狀態,所謂的「活」,而處於死鎖的實體表現爲等待;活鎖有可能自行解開,死鎖則不能。
實際上,聰慧的胖友是否是已經發現,死鎖就是悲觀鎖可能產生的結果,而活鎖是樂觀鎖可能產生的結果。
1)悲觀鎖
悲觀鎖,老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它拿到鎖。
synchronized
關鍵字的實現也是悲觀鎖。2)樂觀鎖
樂觀鎖,顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,能夠使用版本號等機制。樂觀鎖適用於多讀的應用類型,這樣能夠提升吞吐量。
像數據庫提供的相似於 write_condition 機制,其實都是提供的樂觀鎖。
例如,version 字段(比較跟上一次的版本號,若是同樣則更新,若是失敗則要重複讀-比較-寫的操做)
在 Java 中 java.util.concurrent.atomic
包下面的原子變量類就是使用了樂觀鎖的一種實現方式 CAS 實現的。
樂觀鎖的實現方式:
艿艿:雖然 Lock 也翻譯成鎖,可是和上面的 「Java 鎖」 分開,它更多強調的是
synchronized
和volatile
關鍵字帶來的重量級和輕量級鎖。而 Lock 是 Java 鎖接口,提供了更多靈活的功能。
java.util.concurrent.locks.AbstractQueuedSynchronizer
抽象類,簡稱 AQS ,是一個用於構建鎖和同步容器的同步器。事實上concurrent
包內許多類都是基於 AQS 構建。例如 ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,等。AQS 解決了在實現同步容器時設計的大量細節問題。
AQS 使用一個 FIFO 的隊列表示排隊等待鎖的線程,隊列頭節點稱做「哨兵節點」或者「啞節點」,它不與任何線程關聯。其餘的節點與等待線程關聯,每一個節點維護一個等待狀態 waitStatus
。
可能這麼說,胖友會一臉懵逼,最好的方式,仍是直接去擼源碼,可見以下的四篇文章。
可能胖友在閱讀時,會有必定的挫敗感,不要緊,你們都是如此,包括艿艿,還有我認識的各類大佬。
java.util.concurrent.locks.Lock
接口,比 synchronized
提供更具拓展行的鎖操做。它容許更靈活的結構,能夠具備徹底不一樣的性質,而且能夠支持多個相關類的條件對象。它的優點有:
舉例來講明鎖的可重入性。代碼以下:
public class UnReentrant{ |
#outer()
方法中調用了 #inner()
方法,#outer()
方法先鎖住了 lock
,這樣 #inner()
就不能再獲取 lock
。#outer()
方法的線程已經獲取了 lock
鎖,可是不能在 #inner()
方法中重複利用已經獲取的鎖資源,這種鎖即稱之爲不可重入。synchronized
、ReentrantLock 都是可重入的鎖,可重入鎖相對來講簡化了併發編程的開發。
關於 ReentrantLock 類,詳細的源碼解析,能夠看看 《【死磕 Java 併發】—– J.U.C 之重入鎖:ReentrantLock》 。
簡單來講,ReenTrantLock 的實現是一種自旋鎖,經過循環調用 CAS 操做來實現加鎖。它的性能比較好也是由於避免了使線程進入內核態的阻塞狀態。想盡辦法避免線程進入內核的阻塞狀態是咱們去分析和理解鎖設計的關鍵鑰匙。
🦅 synchronized 和 ReentrantLock 異同?
synchronized
經過 Java 對象頭鎖標記和 Monitor 對象實現同步。synchronized
依賴 JVM 內存模型保證包含共享變量的多線程內存可見性。volatile state
保證包含共享變量的多線程內存可見性。synchronized
能夠修飾實例方法(鎖住實例對象)、靜態方法(鎖住類對象)、代碼塊(顯示指定鎖對象)。finally
塊中釋放鎖。synchronized
不可設置等待時間、不可被中斷(interrupted)。synchronized
只支持非公平鎖。在
synchronized
優化之前,它的性能是比 ReenTrantLock 差不少的,可是自從synchronized
引入了偏向鎖,輕量級鎖(自旋鎖)後,二者的性能就差很少了,在兩種方法均可用的狀況下,官方甚至建議使用synchronized
。而且,實際代碼實戰中,可能的優化場景是,經過讀寫分離,進一步性能的提高,因此使用 ReentrantReadWriteLock 。😝
ReadWriteLock ,讀寫鎖是,用來提高併發程序性能的鎖分離技術的 Lock 實現類。能夠用於 「多讀少寫」 的場景,讀寫鎖支持多個讀操做併發執行,寫操做只能由一個線程來操做。
ReadWriteLock 對向數據結構相對不頻繁地寫入,可是有多個任務要常常讀取這個數據結構的這類狀況進行了優化。ReadWriteLock 使得你能夠同時有多個讀取者,只要它們都不試圖寫入便可。若是寫鎖已經被其餘任務持有,那麼任何讀取者都不能訪問,直至這個寫鎖被釋放爲止。
ReadWriteLock 對程序性能的提升主要受制於以下幾個因素:
ReadWriteLock 的源碼解析,能夠看看 《【死磕 Java 併發】—– J.U.C 之讀寫鎖:ReentrantReadWriteLock》 。
在沒有 Lock 以前,咱們使用 synchronized
來控制同步,配合 Object 的 #wait()
、#notify()
等一系列方法能夠實現等待 / 通知模式。在 Java SE 5 後,Java 提供了 Lock 接口,相對於 synchronized
而言,Lock 提供了條件 Condition ,對線程的等待、喚醒操做更加詳細和靈活。下圖是 Condition 與 Object 的監視器方法的對比(摘自《Java併發編程的藝術》):
🦅 用三個線程按順序循環打印 abc 三個字母,好比 abcabcabc ?
synchronized
+ await/notifyAll 來實現,參看 《Java用三個線程按順序循環打印 abc 三個字母,好比 abcabcabc》 。LockSupport 是 JDK 中比較底層的類,用來建立鎖和其餘同步工具類的基本線程阻塞。
LockSupport#park()
和 LockSupport#unpark()
方法,來實現線程的阻塞和喚醒的。對於 LockSupport 瞭解便可,面試通常問的很少。感興趣的胖友,能夠看看以下文章:
關於 Java 內存模型,涉及的內容會不少,因此建議胖友看以下的 《深刻Java內存模型.pdf》 這本小書。
而後,看完以後你確定會忘記,就能夠靠 《《深刻理解 Java 內存模型》讀書筆記》 來補刀。
再另外,《深刻拆解 Java 虛擬機》 的 「第五部分 高效併發」 也推薦閱讀。
Java 虛擬機規範中試圖定義一種 Java 內存模型(Java Memory Model,JMM)來屏蔽掉各層硬件和操做系統的內存訪問差別,以實現讓 Java 程序在各類平臺下都能達到一致的內存訪問效果。
Java 內存模型規定了全部的變量都存儲在主內存(Main Memory)中。每條線程還有本身的工做內存(Working Memory),線程的工做內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對變量的全部操做(讀取、賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間的變量值的傳遞均須要經過主內存來完成,線程、主內存、工做內存三者的關係以下圖:
艿艿:固然,有個面試官會把 Java 內存模型,和 JVM 內存結構搞混淆。因此,在回答以前,能夠先和麪試官確認下說的是哪一個。
關於 JVM 內存結構的面試題,咱們在 《精盡 Java【虛擬機】面試題》 中在詳細分享。
線程之間的通訊方式,目前有共享內存和消息傳遞兩種。
1)共享內存
在共享內存的併發模型裏,線程之間共享程序的公共狀態,線程之間經過寫-讀內存中的公共狀態來隱式進行通訊。典型的共享內存通訊方式,就是經過共享對象進行通訊。
例如上圖線程 A 與 線程 B 之間若是要通訊的話,那麼就必須經歷下面兩個步驟:
2)消息傳遞
在消息傳遞的併發模型裏,線程之間沒有公共狀態,線程之間必須經過明確的發送消息來顯式進行通訊。在 Java 中典型的消息傳遞方式,就是 #wait()
和 #notify()
,或者 BlockingQueue 。
在執行程序時,爲了提供性能,處理器和編譯器經常會對指令進行重排序,可是不能隨意重排序,不是你想怎麼排序就怎麼排序,它須要知足如下兩個條件:
須要注意的是:重排序不會影響單線程環境的執行結果,可是會破壞多線程的執行語義。
詳細看 《【死磕 Java 併發】—– Java 內存模型之 happens-before》 文章。
內存屏障,又稱內存柵欄,是一組處理器指令,用於實現對內存操做的順序限制。
🦅 內存屏障爲什麼重要?
對主存的一次訪問通常花費硬件的數百次時鐘週期。處理器經過緩存(caching)可以從數量級上下降內存延遲的成本這些緩存爲了性能從新排列待定內存操做的順序。也就是說,程序的讀寫操做不必定會按照它要求處理器的順序執行。當數據是不可變的,同時/或者數據限制在線程範圍內,這些優化是無害的。若是把這些優化與對稱多處理(symmetric multi-processing)和共享可變狀態(shared mutable state)結合,那麼就是一場噩夢。
當基於共享可變狀態的內存操做被從新排序時,程序可能行爲不定。一個線程寫入的數據可能被其餘線程可見,緣由是數據寫入的順序不一致。適當的放置內存屏障,經過強制處理器順序執行待定的內存操做來避免這個問題。
何爲同步容器?能夠簡單地理解爲經過 synchronized
來實現同步的容器,若是有多個線程調用同步容器的方法,它們將會串行執行。
Collections#synchronizedSet()
,Collections#synchronizedList()
等方法返回的容器。synchronized
。併發容器,使用了與同步容器徹底不一樣的加鎖策略來提供更高的併發性和伸縮性。
new
新的數據從而不影響原有的數據,iterator 完成後再將頭指針替換爲新的數據 ,這樣 iterator 線程能夠使用原來老的數據,而寫線程也能夠併發的完成改變。關於 ConcurrentHashMap 的源碼解析,推薦胖友看看以下兩篇文章:
🦅 Java 中 ConcurrentHashMap 的併發度是什麼?
在 JDK8 前,ConcurrentHashMap 把實際 map 劃分紅若干部分來實現它的可擴展性和線程安全。這種劃分是使用併發度得到的,它是 ConcurrentHashMap 類構造函數的一個可選參數,默認值爲 16 ,這樣在多線程狀況下就能避免爭用。
在 JDK8 後,它摒棄了 Segment(鎖段)的概念,而是啓用了一種全新的方式實現,利用 CAS 算法。同時加入了更多的輔助變量來提升併發度,具體內容仍是查看源碼吧。
🦅 ConcurrentHashMap 爲什麼讀不用加鎖?
在 JDK7 以及之前
key
、hash
、next
均爲 final
型,只能表頭插入、刪除結點。
value
域被聲明爲 volatile
型。null
做爲鍵和值,當讀線程讀到某個 HashEntry 的 value
域的值爲 null
時,便知道產生了衝突——發生了重排序現象(put 方法設置新 value
對象的字節碼指令重排序),須要加鎖後從新讀入這個 value
值。volatile
變量 count
協調讀寫線程之間的內存可見性,寫操做後修改 count
,讀操做先讀 count
,根據 happen-before 傳遞性原則寫操做的修改讀操做可以看到。在 JDK8 開始
val
和 next
均爲 volatile
型。#tabAt(..,)
和 #casTabAt(...)
對應的 Unsafe 操做實現了 volatile
語義。CopyOnWriteArrayList(免鎖容器)的好處之一是當多個迭代器同時遍歷和修改這個列表時,不會拋出ConcurrentModificationException 異常。在 CopyOnWriteArrayList 中,寫入將致使建立整個底層數組的副本,而源數組將保留在原地,使得複製的數組在被修改時,讀取操做能夠安全地執行。
CopyOnWriteArrayList 透露的思想:
CopyOnWriteArrayList 適用於讀操做遠遠多於寫操做的場景。例如,緩存。
關於 CopyOnWriteArrayList 的源碼,能夠看看 《CopyOnWriteArrayList 實現原理及源碼分析》 文章。
阻塞隊列(BlockingQueue)是一個支持兩個附加操做的隊列。這兩個附加的操做是:
阻塞隊列經常使用於生產者和消費者的場景:
艿艿:以下的內容,和上面是相對重複的,或者是換一個說法,從新描述。
BlockingQueue 接口,是 Queue 的子接口,它的主要用途並非做爲容器,而是做爲線程同步的的工具,所以他具備一個很明顯的特性:
阻塞隊列使用最經典的場景,就是 Socket 客戶端數據的讀取和解析:
JDK7 提供了 7 個阻塞隊列。分別是:
Java5 以前實現同步存取時,能夠使用普通的一個集合,而後在使用線程的協做和線程同步能夠實現生產者,消費者模式,主要的技術就是用好 wait、notify、notifyAll、
sychronized
這些關鍵字。而在 Java5 以後,能夠使用阻塞隊列來實現,此方式大大簡少了代碼量,使得多線程編程更加容易,安全方面也有保障。
【最經常使用】ArrayBlockingQueue :一個由數組結構組成的有界阻塞隊列。
此隊列按照先進先出(FIFO)的原則對元素進行排序,可是默認狀況下不保證線程公平的訪問隊列,即若是隊列滿了,那麼被阻塞在外面的線程對隊列訪問的順序是不能保證線程公平(即先阻塞,先插入)的。
LinkedBlockingQueue :一個由鏈表結構組成的有界阻塞隊列。
此隊列按照先出先進的原則對元素進行排序
PriorityBlockingQueue :一個支持優先級排序的無界阻塞隊列。
DelayQueue:支持延時獲取元素的無界阻塞隊列,便可以指定多久才能從隊列中獲取當前元素。
SynchronousQueue:一個不存儲元素的阻塞隊列。
每個 put 必須等待一個 take 操做,不然不能繼續添加元素。而且他支持公平訪問隊列。
LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列。
相對於其餘阻塞隊列,多了 tryTransfer 和 transfer 方法。
- transfer 方法:若是當前有消費者正在等待接收元素(take 或者待時間限制的 poll 方法),transfer 能夠把生產者傳入的元素馬上傳給消費者。若是沒有消費者等待接收元素,則將元素放在隊列的 tail 節點,並等到該元素被消費者消費了才返回。
- tryTransfer 方法:用來試探生產者傳入的元素可否直接傳給消費者。若是沒有消費者在等待,則返回 false 。和上述方法的區別是該方法不管消費者是否接收,方法當即返回。而 transfer 方法是必須等到消費者消費了才返回。
LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列。
優點在於多線程入隊時,減小一半的競爭。
具體的源碼解析,能夠看看以下文章:
🦅 阻塞隊列提供哪些重要方法?
方法處理方式 | 拋出異常 | 返回特殊值 | 一直阻塞 | 超時退出 |
---|---|---|---|---|
插入方法 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除方法 | remove() | poll() | take() | poll(time, unit) |
檢查方法 | element() | peek() | 不可用 | 不可用 |
🦅 ArrayBlockingQueue 與 LinkedBlockingQueue 的區別?
Queue | 阻塞與否 | 是否有界 | 線程安全保障 | 適用場景 | 注意事項 |
---|---|---|---|---|---|
ArrayBlockingQueue | 阻塞 | 有界 | 一把全局鎖 | 生產消費模型,平衡兩邊處理速度 | 用於存儲隊列元素的存儲空間是預先分配的,使用過程當中內存開銷較小(無須動態申請存儲空間) |
LinkedBlockingQueue | 阻塞 | 可配置 | 存取採用 2 把鎖 | 生產消費模型,平衡兩邊處理速度 | 無界的時候注意內存溢出問題,用於存儲隊列元素的存儲空間是在其使用過程當中動態分配的,所以它可能會增長 JVM 垃圾回收的負擔。 |
感興趣的胖友,能夠看看以下兩篇文章:
在上面,咱們看到的 LinkedBlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue、SynchronousQueue 等,都是阻塞隊列。
而 ArrayDeque、LinkedBlockingDeque 就是雙端隊列,類名以 Deque 結尾。
😈 實際場景下,雙端隊列,咱們使用比較少。艿艿根本沒用過。
JDK 的 Timer 和 DelayQueue 插入和刪除操做的平均時間複雜度爲 O(nlog(n))
,而基於時間輪能夠將插入和刪除操做的時間複雜度都降爲 O(1)
。
參考 《LinkedBlockingQueue 和 ConcurrentLinkedQueue的用法及區別》 。
在 Java 多線程應用中,隊列的使用率很高,多數生產消費模型的首選數據結構就是隊列(先進先出)。
Java 提供的線程安全的 Queue 能夠分爲
阻塞隊列,典型例子是 LinkedBlockingQueue 。
適用阻塞隊列的好處:多線程操做共同的隊列時不須要額外的同步,另外就是隊列會自動平衡負載,即那邊(生產與消費兩邊)處理快了就會被阻塞掉,從而減小兩邊的處理速度差距。
非阻塞隊列,典型例子是 ConcurrentLinkedQueue 。
當許多線程共享訪問一個公共集合時,
ConcurrentLinkedQueue
是一個恰當的選擇。
具體的選擇,以下:
原子操做(Atomic Operation),意爲」不可被中斷的一個或一系列操做」。
原子操做是指一個不受其餘操做影響的操做任務單元。原子操做是在多線程環境下避免數據不一致必須的手段。
int++
並非一個原子操做,因此當一個線程讀取它的值並加 1 時,另一個線程有可能會讀到以前的值,這就會引起錯誤。java.util.concurrent.atomic
包提供了 int
和 long
類型的原子包裝類,它們能夠自動的保證對於他們的操做是原子的而且不須要使用同步。java.util.concurrent
這個包裏面提供了一組原子類。其基本的特性就是在多線程環境下,當有多個線程同時執行這些類的實例包含的方法時,具備排他性,即當某個線程進入方法,執行其中的指令時,不會被其餘線程打斷,而別的線程就像自旋鎖同樣,一直等到該方法執行完成,才由 JVM 從等待隊列中選擇一個另外一個線程進入,這只是一種邏輯上的理解。
boolean
來反映中間有沒有變過),AtomicStampedReference(經過引入一個 int
來累加來反映中間有沒有變過)。關於 CAS 的內容,建議胖友在看看 《【死磕 Java 併發】—- 深刻分析 CAS》 。
1)ABA 問題
好比說一個線程 one 從內存位置 V 中取出 A ,這時候另外一個線程 two 也從內存中取出 A ,而且 two 進行了一些操做變成了 B ,而後 two 又將 V 位置的數據變成 A ,這時候線程 one 進行 CAS 操做發現內存中仍然是 A ,而後 one 操做成功。儘管線程 one 的 CAS 操做成功,但可能存在潛藏的問題。
從 Java5 開始 JDK 的 atomic
包裏提供了一個類 AtomicStampedReference 來解決 ABA 問題。
2)循環時間長開銷大
對於資源競爭嚴重(線程衝突嚴重)的狀況,CAS 自旋的機率會比較大,從而浪費更多的 CPU 資源,效率低於 synchronized
。
3)只能保證一個共享變量的原子操做
當對一個共享變量執行操做時,咱們能夠使用循環 CAS 的方式來保證原子操做,可是對多個共享變量操做時,循環 CAS 就沒法保證操做的原子性,這個時候就能夠用鎖。
Semaphore ,是一種新的同步類,它是一個計數信號。從概念上講,從概念上講,信號量維護了一個許可集合。
#acquire()
方法,而後再獲取該許可。#release()
方法,添加一個許可,從而可能釋放一個正在阻塞的獲取者。信號量經常用於多線程的代碼中,好比數據庫鏈接池。
CountDownLatch ,字面意思是減少計數(CountDown)的門閂(Latch)。它要作的事情是,等待指定數量的計數被減小,意味着門閂被打開,而後進行執行。
CountDownLatch 默認的構造方法是 CountDownLatch(int count)
,其參數表示須要減小的計數,主線程調用 #await()
方法告訴 CountDownLatch 阻塞等待指定數量的計數被減小,而後其它線程調用 CountDownLatch 的 #countDown()
方法,減少計數(不會阻塞)。等待計數被減小到零,主線程結束阻塞等待,繼續往下執行。
CyclicBarrier ,字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要作的事情是,讓一組線程到達一個屏障(也能夠叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,全部被屏障攔截的線程纔會繼續幹活。
CyclicBarrier 默認的構造方法是 CyclicBarrier(int parties)
,其參數表示屏障攔截的線程數量,每一個線程調用 #await()
方法告訴 CyclicBarrier 我已經到達了屏障,而後當前線程被阻塞,直到 parties
個線程到達,結束阻塞。
實際場景下,問了一圈朋友,Exchanger 基本沒在業務中使用過。
CyclicBarrier 能夠重複使用,而 CountdownLatch 不能重複使用。
#await()
方法都會阻塞,直到這個計數器的計數值被其餘的線程減爲 0 爲止。因此在當前計數到達零以前,await 方法會一直受阻塞。以後,會釋放全部等待的線程,await 的全部後續調用都將當即返回。這種現象只出現一次——計數沒法被重置。若是須要重置計數,請考慮使用 CyclicBarrier 。#await()
方法,其餘的任務執行完本身的任務後調用同一個 CountDownLatch 對象上的 #countDown()
方法,這個調用 #await()
方法的任務將一直阻塞等待,直到這個 CountDownLatch 對象的計數值減到 0 爲止。整理表格以下:
CountDownLatch | CyclicBarrier |
---|---|
減計數方式 | 加計數方式 |
計算爲 0 時釋放全部等待的線程 | 計數達到指定值時釋放全部等待線程 |
計數爲 0 時,沒法重置 | 計數達到指定值時,計數置爲 0 從新開始 |
調用 #countDown() 方法計數減一,調用 #await() 方法只進行阻塞,對計數沒任何影響 |
調用 #await() 方法計數加 1 ,若加 1 後的值不等於構造方法的值,則線程阻塞 |
不可重複利用 | 可重複利用 |
Executor 框架,是一個根據一組執行策略調用,調度,執行和控制的異步任務的框架。
無限制的建立線程,會引發應用程序內存溢出。因此建立一個線程池是個更好的的解決方案,由於能夠限制線程的數量而且能夠回收再利用這些線程。利用 Executor 框架,能夠很是方便的建立一個線程池。
🦅 爲何使用 Executor 框架?
new Thread()
比較消耗性能,建立一個線程是比較耗時、耗資源的。new Thread()
建立的線程缺少管理,被稱爲野線程,並且能夠無限制的建立,線程之間的相互競爭會致使過多佔用系統資源而致使系統癱瘓,還有線程之間的頻繁交替也會消耗不少系統資源。new Thread()
啓動的線程不利於擴展,好比定時執行、按期執行、定時按期執行、線程中斷等都不便實現。🦅 在 Java 中 Executor 和 Executors 的區別?
#get()
方法,獲取計算的結果。Java 類庫提供一個靈活的線程池以及一些有用的默認配置,咱們能夠經過Executors 的靜態方法來建立線程池。
Executors 建立的線程池,分紅普通任務線程池,和定時任務線程池。
#newFixedThreadPool(int nThreads)
方法,建立一個固定長度的線程池。
#newCachedThreadPool()
方法,建立一個可緩存的線程池。
#newSingleThreadExecutor()
方法,建立一個單線程的線程池。
#newScheduledThreadPool(int corePoolSize)
方法,建立了一個固定長度的線程池,並且以延遲或定時的方式來執行任務,相似 Timer 。#newSingleThreadExecutor()
方法,建立了一個固定長度爲 1 的線程池,並且以延遲或定時的方式來執行任務,相似 Timer 。🦅 如何使用 ThreadPoolExecutor 建立線程池?
Executors 提供了建立線程池的經常使用模板,實際場景下,咱們可能須要自動以更靈活的線程池,此時就須要使用 ThreadPoolExecutor 類。
// ThreadPoolExecutor.java |
corePoolSize
參數,核心線程數大小,當線程數 < corePoolSize ,會建立線程執行任務。maximumPoolSize
參數,最大線程數, 當線程數 >= corePoolSize 的時候,會把任務放入 workQueue
隊列中。
keepAliveTime
參數,保持存活時間,當線程數大於 corePoolSize
的空閒線程能保持的最大時間。unit
參數,時間單位。workQueue
參數,保存任務的阻塞隊列。
handler
參數,超過阻塞隊列的大小時,使用的拒絕策略。threadFactory
參數,建立線程的工廠。🦅 ThreadPoolExecutor 有哪些拒絕策略?
ThreadPoolExecutor 默認有四個拒絕策略:
ThreadPoolExecutor.AbortPolicy()
,直接拋出異常 RejectedExecutionException 。ThreadPoolExecutor.CallerRunsPolicy()
,直接調用 run 方法而且阻塞執行。ThreadPoolExecutor.DiscardPolicy()
,直接丟棄後來的任務。ThreadPoolExecutor.DiscardOldestPolicy()
,丟棄在隊列中隊首的任務。若是咱們有須要,能夠本身實現 RejectedExecutionHandler 接口,實現自定義的拒絕邏輯。固然,絕大多數是不須要的。
🦅 ****
ThreadPoolExecutor 提供了兩個方法,用於線程池的關閉,分別是:
#shutdown()
方法,不會當即終止線程池,而是要等全部任務緩存隊列中的任務都執行完後才終止,但不再會接受新的任務。#shutdownNow()
方法,當即終止線程池,並嘗試打斷正在執行的任務,而且清空任務緩存隊列,返回還沒有執行的任務。實際場景下,通常會結合這兩個方法,一塊兒實現線程池的優雅關閉。示例代碼以下:
void shutdownAndAwaitTermination(ExecutorService pool) { |
詳細的能夠看看 《如何合理地估算線程池大小?》 。以下是簡單的總結和整理:
通常說來,你們認爲線程池的大小經驗值應該這樣設置:(其中 N 爲CPU的個數)
若是是 CPU 密集型應用,則線程池大小設置爲 N+1
由於 CPU 密集型任務使得 CPU 使用率很高,若開過多的線程數,只能增長上下文切換的次數,所以會帶來額外的開銷。
若是是 IO 密集型應用,則線程池大小設置爲 2N+1
IO密 集型任務 CPU 使用率並不高,所以可讓 CPU 在等待 IO 的時候去處理別的任務,充分利用 CPU 時間。
若是是混合型應用,那麼分別建立線程池
能夠將任務分紅 IO 密集型和 CPU 密集型任務,而後分別用不一樣的線程池去處理。 只要分完以後兩個任務的執行時間相差不大,那麼就會比串行執行來的高效。
由於若是劃分以後兩個任務執行時間相差甚遠,那麼先執行完的任務就要等後執行完的任務,最終的時間仍然取決於後執行完的任務,並且還要加上任務拆分與合併的開銷,得不償失。
若是一臺服務器上只部署這一個應用而且只有這一個線程池,那麼這種估算或許合理,具體還需自行測試驗證。
可是,IO 優化中,這樣的估算公式可能更適合:最佳線程數目 = ((線程等待時間 + 線程 CPU 時間)/ 線程 CPU 時間 )* CPU 數目 由於很顯然,線程等待時間所佔比例越高,須要越多線程。線程CPU時間所佔比例越高,須要越少線程。
下面舉個例子:好比平均每一個線程 CPU 運行時間爲 0.5s ,而線程等待時間(非 CPU 運行時間,好比 IO)爲 1.5s ,CPU 核心數爲 8 。 那麼根據上面這個公式估算獲得:((0.5 + 1.5) / 0.5) * 8 = 32
。這個公式進一步轉化爲:最佳線程數目 = (線程等待時間與線程 CPU 時間之比 + 1)* CPU數目。
🦅 線程池容量的動態調整?
ThreadPoolExecutor 提供了動態調整線程池容量大小的方法:
當上述參數從小變大時,ThreadPoolExecutor 進行線程賦值,還可能當即建立新的線程來執行任務。
1)Callable
Callable 接口,相似於 Runnable ,從名字就能夠看出來了,可是Runnable 不會返回結果,而且沒法拋出返回結果的異常,而 Callable 功能更強大一些,被線程執行後,能夠返回值,這個返回值能夠被 Future 拿到,也就是說,Future 能夠拿到異步執行任務的返回值。
簡單來講,能夠認爲是帶有回調的 Runnable 。
2)Future
Future 接口,表示異步任務,是尚未完成的任務給出的將來結果。因此說 Callable 用於產生結果,Future 用於獲取結果。
3)FutureTask
在 Java 併發程序中,FutureTask 表示一個能夠取消的異步運算。
剛建立時,裏面沒有線程調用 execute() 方法,添加任務時:
corePoolSize
,繼續建立線程運行這個任務
corePoolSize
,將任務加入到阻塞隊列中。maximumPoolSize
,繼續建立線程運行這個任務。maximumPoolSize
,根據設置的拒絕策略處理。corePoolSize
。🦅 線程池中 submit 和 execute 方法有什麼區別?
兩個方法均可以向線程池提交任務。
#execute(...)
方法,返回類型是 void
,它定義在 Executor 接口中。#submit(...)
方法,能夠返回持有計算結果的 Future 對象,它定義在 ExecutorService 接口中,它擴展了 Executor 接口,其它線程池類像 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有這些方法。🦅 若是你提交任務時,線程池隊列已滿,這時會發生什麼?
艿艿:重點在於線程池的隊列是有界仍是無界的。
艿艿:這是,可能瞭解的人很少,我也是。大致知道就好。
Oracle 的官方給出的定義是:Fork/Join 框架是一個實現了 ExecutorService接口 的多線程處理器。它能夠把一個大的任務劃分爲若干個小的任務併發執行,充分利用可用的資源,進而提升應用的執行效率。
咱們再經過 Fork 和 Join 這兩個單詞來理解下 Fork/Join 框架。
1+2+...+10000
,能夠分割成 10 個子任務,每一個子任務分別對 1000 個數進行求和,最終彙總這 10 個子任務的結果。感興趣的胖友,能夠看看以下文章:
一、CountDownLatch:容許一個或者多個線程等待前面的一個或多個線程完成,構造一個 CountDownLatch 時指定須要 countDown 的點的數量,每完成一點就 countDown 一下。當全部點都完成,CountDownLatch 的 #await()
就解除阻塞。
二、CyclicBarrier:可循環使用的 Barrier ,它的做用是讓一組線程到達一個 Barrier 後阻塞,直到全部線程都到達 Barrier 後才能繼續執行。
CountDownLatch 的計數值只能使用一次,CyclicBarrier 能夠經過使用 reset 重置,還能夠指定到達柵欄後優先執行的任務。
三、Fork/Join 框架,fork 把大任務分解成多個小任務,而後彙總多個小任務的結果獲得最終結果。使用一個雙端隊列,當線程空閒時從雙端隊列的另外一端領取任務。
真多,真多,好他喵的多!耐心看。
哈哈哈,實際面試吧,也不會問這麼多。嘻嘻~
參考與推薦以下文章: