面試必備——Java多執行緒與並發(二)
1.synchroized相關(鎖的是對象,不是程式碼)
(1)執行緒安全問題的主要原因
- 存在共享數據(也稱臨界資源)
- 存在多執行緒共同操作這些共享數據
解決:同一時刻有且只有一個執行緒在操作共享數據,其他執行緒必須等到該執行緒處理完數據後再對共享數據進行操作
(2)互斥鎖的特性
互斥性(操作的原子性):即在同一時間只允許一個執行緒持有某個對象鎖,通過這種特性來實現多執行緒的協調機制,這樣在同一時間只有一個執行緒對需要同步的程式碼塊(複合操作)進行訪問。
可見性:必須確保在鎖被釋放之前,對共享變數所做的修改,對於隨後獲得該鎖的另外一個執行緒是可見的(即在獲得鎖時應獲得最新共享變數的值),否則另外一個執行緒可能是在本地快取的某個副本上繼續操作,從而引起不一致。
(3)獲取的鎖的分類
1)獲取對象鎖
兩種用法
- 同步程式碼塊(synchronized(this),synchronized(類實例對象))),鎖是小括弧中的實例對象
- 同步非靜態方法(synchronized method),鎖是當前對象的實例對象
2)獲取類鎖
兩種用法
- 同步程式碼塊(synchronized(類.class)),鎖是小括弧中的類對象
- 同步靜態方法(synchronized static method),鎖是當前對象的類對象
3)總結
- 有執行緒訪問對象的同步程式碼塊時,另外的執行緒可以訪問該對象的非同步程式碼塊
- 若鎖住的是同個對象,一個執行緒在訪問對象的同步程式碼塊時,另一個訪問對象的同步程式碼塊的執行緒會被阻塞
- 若鎖住的是同個對象,一個執行緒在訪問對象的同步方法時,另一個訪問對象同步方法的執行緒會被阻塞
- 若鎖住的是同個對象,一個執行緒在訪問對象的同步程式碼塊時,另一個訪問對象的同步方法的執行緒會被阻塞,反之亦然
- 同一個類的不同對象的對象鎖互不干擾
- 類鎖由於也是一種特殊的對象鎖,因此表現和上述1,2,3,4一致,而由於一個類只有一把對象鎖,所以用一個類的不同對象使用類鎖將會是同步的
- 類鎖和對象鎖互不干擾
(4)底層實現原理
1)對象在記憶體中的布局
- 對象頭
- 實例數據
- 對齊填充
2)實現基礎
- Java對象頭
包含Mark Word(默認存儲對象的hashCode,分代年齡,鎖類型,鎖標誌位等資訊,用來判斷能否獲得monitor鎖)和類型指針(指向對象的類元數據,JVM通過這個指針確定該對象的哪個類的鎖)
Mark Word:由於對象頭資訊與對象自身定義的數據無關的額外存儲成本,因此考慮到JVM的空間效率,它被設計成非固定的數據結構,以便存儲更多有效的數據,它會根據自身的狀態服用自己的存儲空間,如下結構
- Monitor:每個Java對象天生自帶了一把看不見的鎖,Monitor鎖(內部鎖),也稱為管程或監視器鎖,可以理解為一種同步工具(機制),通常它被描述為一個對象
Jvm會在Java每個對象創建時,給每個Java對象一把鎖,也就是monitor鎖,monitor裡面定義了EntryList和WaitSet屬性,也就是傳說中的鎖池和等待池,
在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor,c++程式碼實現的。
ObjectMonitor中有兩個隊列,_WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表,每個等待鎖的執行緒都會被封裝成ObjectWaiter對象,_owner指向持有ObjectMonitor對象的執行緒。
當多個執行緒同時訪問一段同步程式碼時,首先會進入 _EntryList 集合,當執行緒獲取到對象的monitor 後進入 _Owner 區域,並把monitor中的_owner變數設置為當前執行緒,同時monitor中的計數器count加1,若執行緒調用 wait() 方法,將釋放當前持有的monitor,_owner被恢復為null,count減1,同時該執行緒進入 WaitSe t集合中等待被喚醒。若當前執行緒執行完畢後,將釋放monitor鎖並複位變數的值,以便其他執行緒進入獲取monitor鎖。
3)什麼是重入
從互斥鎖的設計上來說,當一個執行緒試圖操作一個由其他執行緒持有的對象鎖的臨界資源時,將會處於阻塞狀態,但當一個執行緒再次請求自己持有對象鎖的臨界資源時,這種情況屬於重入
4)為什麼會對synchronize的嗤之以鼻
- 早期版本中,synchronized屬於重量級鎖,依賴於Mutex Lock實現
- 執行緒之間的切換需要從用戶狀態轉換到核心態,開銷較大
在Java6以後,對synchronize進行了很多優化,如自旋鎖、自適應自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等等,性能有了較大的提升。
5)自旋鎖和自適應自旋鎖
①自旋鎖(java4已經引入,默認關閉,Java6默認開啟)
- 許多情況下,共享數據的鎖定狀態持續時間較短,切換執行緒不值得
- 通過讓執行緒執行忙循環(類似While(true))等待鎖的釋放,不讓出CPU
- 缺點:若鎖被其他執行緒長時間佔用,會帶來許多性能上的開銷
②自適應自旋鎖
- 自旋的次數不再固定
- 由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定
③鎖消除
更徹底的優化:JIT編譯時,對運行上下文進行掃描,去除不可能存在競爭的鎖

1 public void add(String str1, String str2){ 2 //StringBuffer是執行緒安全的,sb只會在append方法中使用,其他執行緒不可能引用 3 //不屬於共享資源,JVM會自動消除內部的鎖 4 StringBuffer sb = new StringBuffer(); 5 sb.append(str1).append(str2); 6 }
④鎖粗化
通過擴大對鎖的範圍,避免反覆加鎖和解鎖

1 public static void run(){ 2 int i =0; 3 StringBuffer buffer = new StringBuffer(); 4 while(i<100){ 5 buffer.append(i); 6 } 7 }
如上程式碼,在循環體里,每次StringBuffer調用append方法都會進行加鎖解鎖操作,而鎖粗化就是在循環體外面加一個synchronize鎖,避免循環體里頻繁加鎖解鎖。
6)synchronized四種狀態
- 無鎖、偏向鎖、輕量級鎖、重量級鎖
- 鎖膨脹方向:無鎖->偏向鎖->輕量級鎖->重量級鎖
①偏向鎖:減少同一執行緒獲取鎖的代價 CAS(Compare And Swap)
- 大多數情況下,鎖不存在多執行緒競爭,總是由同一執行緒多次獲得
核心思想:
如果一個執行緒獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word的結構也變為偏向鎖結構,當該執行緒再次請求鎖時,無需再做任務同步操作,即獲得鎖的過程只需要檢查Mark Word的鎖標記位為偏向鎖以及當前執行緒Id等於Mark Word的 ThreadID即可,這樣就省去了大量有關鎖申請的操作。
- 不適用於鎖競爭比較激烈的多執行緒場合
②輕量級鎖
- 輕量級鎖是由偏向鎖升級來的,偏向鎖運行在一個執行緒進入同步塊的情況下,當第二個執行緒加入鎖爭用的時候,偏向鎖就會升級為輕量級鎖
- 適應的場景:執行緒交替執行同步塊
- 若存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖
③synchronize加鎖的解鎖的過程
加鎖
- 在程式碼進入同步塊時,如果同步對象鎖狀態為無鎖狀態,虛擬機會在當前執行緒的棧幀中創建一個鎖記錄(Lock Record)的空間,用於存儲當前對象的Mark Word的拷貝。
- 拷貝對象頭中的Mark Word到鎖記錄中。
- 拷貝成功後,使用CAS操作嘗試將對象的Mark Word更新為指向鎖記錄的指針,並將鎖記錄里的owner指向Mark Word。
- 如果更新成功,那麼當前執行緒則獲取了該對象的鎖,並將Mark Word里的鎖標誌位設置為鎖狀態。
- 如果更新失敗,則判斷Mark Word是否指向當前棧幀,如果是,則說明當前執行緒之前已經獲取該鎖,然後進入同步塊繼續執行,如果不是,則說明別的執行緒正在請求該鎖,進行鎖膨脹,根據鎖狀態,判斷是進行自旋等待或者進入鎖池中。
解鎖
- 利用CAS操作嘗試把當前執行緒中複製的Mark Word對象替換當前對象中的Mark Word。
- 如果替換成功,則同步完成
- 如果替換失敗,則說明在該執行緒執行過程中,有其他執行緒嘗試過獲取該鎖(此時鎖已膨脹),那就要釋放鎖的同時,喚醒被掛起的執行緒。
④鎖的記憶體語義
- 當執行緒釋放鎖時,Java記憶體模型會把該執行緒對應的本地記憶體中的共享變數刷新到主記憶體中;
- 當執行緒獲取鎖時,Java記憶體模型會把該執行緒對應的本地記憶體置為無效,從而使得被監視器保護的臨界區程式碼必須從主記憶體中讀取共享變數
⑤匯總
2.ReentrantLock
(1)基本
- 位於java.util.concurrent.locks包
- 和CountDownLatch、FutureTask、Samaphore一樣基於AQS實現
- 能夠實現比synchronized更細粒度的控制,如控制fairness
- 調用lock()之後,必須調用unlock()釋放鎖
- 性能未必比synchronized高,並且也是可重入的
(2)公平性的設置
- ReentrantLock fariLock = new ReentrantLock(true);
- 參數為true時,傾向於將鎖賦予等待時間最久的執行緒;
- 公平鎖:獲取鎖的順序按先後調用lock方法的順序(慎用)
- 非公平鎖:搶佔的順序不一定,看運氣
- synchronized是非公平鎖
(3)ReentrantLock將鎖對象化
- 判斷是否有執行緒,或者某個特定執行緒,在排隊等待獲取鎖
- 帶超時的獲取鎖的嘗試
- 感知有沒有成功獲取鎖
3.Synchronized和ReentrantLock的區別
- synchronized是關鍵字,ReentrantLock是類
- ReentrantLock可以對獲取鎖的等待時間進行設置,避免死鎖
- ReentrantLock可以獲取各種鎖的資訊
- ReentrantLock可以靈活地實現多路通知
- 機制:sync操作Mark Word,lock調用Unsafe類的park()方法
4.Java記憶體模型
即Java Memory Model,簡稱JMM,是一種抽象的概念,並不真實存在,它描述的是一組規則或規範,通過這組規範定義了程式中各個變數(包括實例欄位,靜態欄位和構成數組對象的元素)的訪問方式
簡單說明:
由於JVM運行的實體是執行緒 ,而每個執行緒創建時JVM都會為其創建工作記憶體 (棧空間),工作記憶體是每個執行緒的私有數據區域 ,而JAVA記憶體模型規定所有變數都存儲在主記憶體 ,主記憶體是共享記憶體區域 ,所有執行緒都可以訪問 ,但執行緒對變數的操作必須在工作記憶體中進行 ,首先要將變數從主記憶體拷貝到自己的工作記憶體空間,然後對變數操作,操作完成後再將變數寫入主記憶體 ,不能直接操作主記憶體中的變數
各個執行緒的工作記憶體中存儲主記憶體中的變數副本拷貝 ,因此不同的執行緒之間無法訪問對方的工作記憶體 ,執行緒間的通訊必須通過主記憶體來完成
5.JMM中的主記憶體和工作記憶體
(1)JMM中的主記憶體
- 存儲Java實例對象
- 包含成員變數、類資訊、常量、靜態變數等
- 屬於數據共享的區域,多執行緒並發操作時會引發執行緒安全問題
(2)JMM中的工作記憶體
- 存儲當前方法的所有本地變數資訊,本地變數對其他執行緒不可見
- 位元組碼行號指示器、Native方法資訊
- 屬於執行緒私有數據區域,不存在執行緒安全問題
(3)JMM與Java記憶體區域劃分是不同的概念層次
- JMM描述的是一組規則(通過規則控制各個變數在共享區域和私有區域的訪問方式),圍繞原子性、有序性、可見性展開
- 相似點:存在共享區域和私有區域(在JVM中,主記憶體屬於共享區域,包含堆和方法區;而工作記憶體私有區域,包含程式計數器,虛擬機棧和本地方法棧)
(4)主記憶體與工作記憶體的數據存儲類型以及操作方式歸納
- 方法里的基本數據類型本地變數將直接存儲在工作記憶體的棧幀結構中
- 引用類型的本地變數:引用存儲在工作記憶體中,實例存儲在主記憶體中
- 成員變數、static變數、類資訊均會被存儲在主記憶體中
- 主記憶體共享的方式是執行緒各拷貝一份數據到工作記憶體,操作完成後刷新回主記憶體
6.JMM如何解決可見性問題
(1)問題描述
簡單理解為,數據從主記憶體載入到快取暫存器,然後運算結束寫回主記憶體。
但當執行緒共享變數時,情況就變得很複雜,如果處理器對某個變數進行了修改,可能體現在該對象的內核快取里,這是個本地狀態,而在運行在其他內核的執行緒可能載入的是舊狀態,很可能導致一致性的問題,從理論上說,多執行緒引入了複雜的數據依賴性,不管編譯器處理器怎麼做重排序,都必須尊重數據依賴性的要求,就打破了數據的正確性,這是JMM所要解決的問題
在執行程式的時候,為了提高性能,處理器和編譯器常常會對指令進行重排序,但有一定的規則
(2)指令重排序需要滿足的條件
- 在單執行緒環境下不能改變程式運行的結果
- 存在數據依賴關係的不允許重排序
無法通過happens-before原則推導出來的,才能進行指令的重排序
JMM內部的實現通過是依賴於記憶體屏障,通過禁止某些重排序的方式,提供記憶體可見性保證,也就是實現了各種happens-before的規則,與此同時,更多的複雜度在於,需要盡量確保各種體系結構處理器,能夠提供一致的行為
(3)happens-before的八大原則
1.程式次序規則:一個執行緒內,按照程式碼順序,書寫在前面的操作先行於書寫在後面的操作;
2.鎖定規則:一個unLock操作先行發生於後面對同一個鎖的lock操作;
3.volatile變數規則:對一個變數的寫操作先行發生於後面對這個變數的讀操作;
4.傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C;
5.執行緒啟動規則:Thread對象的start()方法先行發生於此執行緒的每一個動作;
6.執行緒中斷規則:對執行緒interrupt()方法的調用先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生;
7.執行緒終結規則:執行緒中所有的操作都先行發生於執行緒的終止檢測,我們可以通過Thread.join()方法結束,Thread.isAlive()的返回值手段檢測到執行緒已經終止執行;
8.對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始;
(4)happens-before的概念
如果兩個操作不滿足上述任意一個happens-before規則,那麼這兩個操作就沒有順序的保障,JVM可以對這兩個操作進行重排序;
如果操作A happens-before操作B,那麼操作A在記憶體上所做的操作對操作B都是可見的。
(5)volatile:JVM提供的輕量級同步機制
- 保證被volatile修飾的共享變數對所有執行緒總是可見的
- 禁止指令重排序優化
(6)volatile可見性
被修飾的變數,總是對其他執行緒立即可見的
當寫一個volatile變數時,JMM會把該執行緒對應的工作記憶體中的共享變數值刷新到主記憶體中
當讀取一個volatile變數時,JMM會把該執行緒對應的工作記憶體置為無效
(7)記憶體屏障
- 保證特定操作的執行順序
- 保證某些變數的記憶體可見性
(8)volatile如何禁止重排序化
- 通過插入記憶體屏障指令禁止在記憶體屏障前後的指令執行重排序優化
- 強制刷出各種CPU的快取數據,因此任何CPU上的執行緒都能讀取到這些數據的最新版本
(9)volatile和synchronized的區別
- volatile本質是在告訴JVM當前變數在暫存器(工作記憶體)中的值是不確定的,需要從主記憶體中讀取;synchronized則是鎖定當前變數,只有當前執行緒可以訪問變數,其他執行緒被阻塞住直到該執行緒完成變數操作為止
- volatile僅能使用在變數級別;synchronized則可以使用在變數、方法和類級別
- volatile僅能實現變數的修改可見性,不能保證原子性;而synchronized則可以保證變數修改的可見性和原子性
- volatile不會造成執行緒的阻塞;synchronized可能會造成執行緒的阻塞
- volatile標記的變數不會被編譯器優化;synchronized標記的變數可以被編譯器優化
7.CAS(Compare and Swap)
(1)概念
一種高效實現執行緒安全性的方法
- 支援原子更新操作,適用於計數器,序列發生器等場景
- 屬於樂觀鎖機制,號稱lock-free
- CAS操作失敗時由開發者決定是否繼續嘗試,還是執行別的操作
(2)思想
包含三個操作數:記憶體位置(V)、預期原值(A)和新值(B)
執行CAS操作時,將記憶體位置的值與預期原值比較,如果匹配,會將該位置的值更新為新值,否則處理器不做任何操作,這裡的記憶體位置就是指主記憶體。
(3)CAS多數情況下對開發者來說是透明的
- J.U.C的atomic包提供了常用的原子性數據類型以及引用、數組等相等原子類型和更新操作工具,是很多執行緒安全程式的首選
- Unsafe類雖提供CAS服用,但因能夠操縱任意記憶體地址讀寫而有隱患
- Java9以後,可以使用Variable Handle API來替代Unsafe
(4)缺點
- 若循壞時間長,開銷很大
- 只能保證一個共享變數的原子操作
- ABA問題(解決:AutomicStampedReference,通過控制版本來解決)
8.Java執行緒池
假設並發的執行緒比較多,但每個執行緒執行的時間很短,這樣就會頻繁創建執行緒,大大降低系統的效率,可能創建和銷毀執行緒,消耗系統資源比執行的時間更多,這就需要執行緒池了。
(1)利用Executors創建不同的執行緒池滿足不同場景的需求
1. newFixedThreadPool(int nThreads)
指定工作執行緒數量的執行緒池
2. newCachedThreadPool()
處理大量短時間工作任務的執行緒池
(1)試圖快取執行緒並重用,當無快取執行緒可用時,就會創建新的工作執行緒;
(2)如果執行緒閑置的時間超過閾值,則會被終止並移出快取;
(3)系統長時間閑置的時候,不會消耗什麼資源
3. newSingleThreadExecutor()
創建唯一的工作者執行緒來執行任務,如果執行緒異常結束,會有另一個執行緒取代它
最大特點:可保證順序去執行各個任務,並且在任意給定時間不會有多個執行緒是活動的
4. newSingleThreadScheduledExecutor0與newScheduledThreaPool(int corePoolSize)
定時或者周期性的工作調度,兩者的區別在於單一工作執行緒還是多個執行緒,前者如果執行緒異常結束,會有另一個執行緒取代它
5. newWorkStealingPool()
內部會構建ForkJoinPool,利用working-stealing演算法,並行地處理任務,不保證處理順序
(2)Fork/Join框架
1)分而治之
Java7提供的一個用於並行執行任務的框架,把大任務分割成若干個小任務並行執行,最總匯總每個小任務結果後得到大任務結果的框架。
可以更好利用多處理器帶來的好處,它是為那些能夠被遞歸的拆解成子任務的工作類型量身設計的。
2)工作竊取演算法
Fork/Join把不同子任務放到不同的隊列里,並為每個隊列創建一個單獨的執行緒來執行,會出現一種情況,有些執行緒任務執行快,有些執行緒任務執行慢,這樣就會造成已完成任務的執行緒被閑置,太浪費資源了,這就有了工作竊取演算法。
某個執行緒從其他隊列中竊取任務來執行,一般就是指執行快的執行緒竊取執行慢的執行緒等待執行的任務,同時為了減少鎖競爭,通常使用雙端隊列,被竊取執行緒永遠從雙端隊列的頭部拿任務執行,竊取執行緒從雙端隊列的尾部拿任務執行,如下圖
(3)為什麼要使用執行緒池
- 降低資源消耗
- 提高執行緒的可管理性
(4)Executor框架
是一個根據一組執行策略調用,調度,執行和控制的非同步任務框架
目的是提供一種將任務提交,如何運行分離開來的機制
(5)J.U.C的三個Executor介面
- Executor:運行新任務的簡單介面,將任務提交和任務執行細節解耦
- ExecutorService:具備管理執行器和任務生命周期的方法,提交任務機制更完善
- ScheduledExecutorService:支援Future和定期執行任務
(6)ThreadPoolExecutor
(7)ThreadPoolExecutor的構造函數
- corePoolSize:核心執行緒數據
- maximumPoolSize:執行緒不夠用時能夠創建的最大執行緒數
- workQueue:任務等待隊列
- keepAliveTime:搶佔的順序不一定,看運氣
- threadFactory:創建新執行緒,Executors.defaultThreadFactory()
(8)handler:執行緒池的飽和策略
- AbortPolicy:直接拋出異常,這是默認策略
- CallerRunsPolicy:用調用者所在的執行緒來執行任務
- DiscardOldestPolicy:丟棄隊列中靠最前的任務,並執行當前任務
- Doscar的Policy:直接丟棄任務
- 實現RejectedExecutionHandler介面的自定義handler
(9)新任務提交execute執行後的判斷
- 如果運行的執行緒少於corePoolSize,則創建新執行緒來處理任務,即使執行緒池中的其他執行緒是空閑的;
- 如果執行緒池中的執行緒數量大於等於corePoolSize且小於maximumPoolSize,則只有當workQueue滿時才創建新的執行緒去處理任務;
- 如果設置的corePoolSize和maximumPoolSize相同,則創建的執行緒池的大小是固定的,這時如果有新任務提交,若workQueue未滿,則將請求放入workQueue中,等待有空閑的執行緒去從workQueue中取任務並處理;
- 如果運行的執行緒數據大於等於maximumPoolSize,這時如果workQueue已經滿了,則通過handler所指定的策略來處理任務
(10)執行緒池的狀態
- RUNNING:能接受新提交的任務,並且也能處理阻塞隊列中的任務
- SHUTDOWN:不能接受新提交的任務,但可以處理存量任務
- STOP:不再接受新提交的任務,也不能處理存量任務
- TIDYTING:所有任務都已停止
- TERMINATED:terminated()方法執行完後進入該狀態
(11)狀態轉換圖
(12)工作執行緒的生命周期
(13)執行緒池的大小如何選定
- CPU密集型(針對計算的場景):執行緒數=按照核數或者核數+1設定
- I/O密集型(處理較多等待任務):執行緒數=CPU核數*(1+平均等待時間/平均工作時間)

