2:java
hotspot中對象在內存的佈局是分3部分 算法
這裏主要講對象頭:通常而言synchronized使用的鎖對象是存儲在對象頭裏的,對象頭是由Mark Word和Class Metadata Address組成編程
mark word存儲自身運行時數據,是實現輕量級鎖和偏向鎖的關鍵,默認存儲對象的hasCode、分代年齡、鎖類型、鎖標誌位等信息。緩存
因爲對象頭的信息是與對象定義的數據沒有關係的額外存儲成本,因此考慮到jvm的空間效率,mark word 被設計出一個非固定的存儲結構,以便存儲更多有效的數據,它會根據對象自己的狀態複用本身的存儲空間(輕量級鎖和偏向鎖是java6後對synchronized優化後新增長的)安全
Monitor:每一個Java對象天生就自帶了一把看不見的鎖,它叫內部鎖或者Monitor鎖(監視器鎖)。上圖的重量級鎖的指針指向的就是Monitor的起始地址。多線程
每一個對象都存在一個Monitor與之關聯,對象與其Monitor之間的關係存在多種實現方式,如Monitor能夠和對象一塊兒建立銷燬、或當線程獲取對象鎖時自動生成,當線程獲取鎖時Monitor處於鎖定狀態。併發
Monitor是虛擬機源碼裏面用C++實現的框架
源碼解讀:_WaitSet 和_EntryList就是以前學的等待池和鎖池,_owner是指向持有Monitor對象的線程。當多個線程訪問同一個對象的同步代碼的時候,首先會進入到_EntryList集合裏面,當線程獲取到對象Monitor後就會進入到_object區域並把_owner設置成當前線程,同時Monitor裏面的_count會加一。當調用wait方法會釋放當前對象的Monitor,_owner恢復成null,_count減一,同時該線程實例進入_WaitSet集合中等待喚醒。若是當前線程執行完畢也會釋放Monitor鎖並復位對應變量的值。jvm
接下來是字節碼的分析:ide
package interview.thread; /** * 字節碼分析synchronized * @Author: cctv * @Date: 2019/5/20 13:50 */ public class SyncBlockAndMethod { public void syncsTask() { synchronized (this) { System.out.println("Hello"); } } public synchronized void syncTask() { System.out.println("Hello Again"); } }
而後控制檯輸入 javac thread/SyncBlockAndMethod.java
而後反編譯 javap -verbose thread/SyncBlockAndMethod.class
先看看syncsTask方法裏的同步代碼塊
從字節碼中能夠看出 同步代碼塊 使用的是 monitorenter 和 monitorexit ,當執行monitorenter指令時當前線程講試圖獲取對象的鎖,當Monitor的count 爲0時將獲的monitor,並將count設置爲1表示取鎖成功。若是當前線程以前有這個monitor的持有權它能夠重入這個Monnitor。monitorexit指令會釋放monitor鎖並將計數器設爲0。爲了保證正常執行monitorenter 和 monitorexit 編譯器會自動生成一個異常處理器,該處理器能夠處理全部異常。主要保證異常結束時monitorexit(字節碼中多了個monitorexit指令的目的)釋放monitor鎖
ps:重入是從互斥鎖的設計上來講的,當一個線程試圖操做一個由其餘線程持有的對象鎖的臨界資源時,將會處於阻塞狀態,當一個線程再次請求本身持有對象鎖的臨界資源時,這種狀況屬於重入。就像以下狀況:hello2也是會輸出的,並不會鎖住。
再看看syncTask同步方法
解讀:這個字節碼中沒有monitorenter和monitorexit指令而且字節碼也比較短,其實方法級的同步是隱式實現的(無需字節碼來控制)ACC_SYNCHRONIZED是用來區分一個方法是否同步方法,若是設置了ACC_SYNCHRONIZED執行線程將持有monitor,而後執行方法,不管方法是否正常完成都會釋放調monitor,在方法執行期間,其餘線程都沒法在得到這個monitor。若是同步方法在執行期間拋出異常並且在方法內部沒法處理此異常,那麼這個monitor將會在異常拋到方法以外時自動釋放。
java6以前Synchronized效率低下的緣由:
在早期版本Synchronized屬於重量級鎖,性能低下,由於監視器鎖(monitor)是依賴於底層操做系統的的MutexLock實現的。
而操做系統切換線程時須要從用戶態轉換到核心態,時間較長,開銷較大
java6之後Synchronized性能獲得了很大提高(hotspot從jvm層面作了較大優化,減小重量級鎖的使用):
自旋鎖:
自適應自旋鎖:(java6引入,jvm對鎖的預測會愈來愈精準,jvm也會愈來愈聰明)
鎖消除:jvm的另外一種鎖優化,更完全的優化
鎖粗化:另外一種極端,鎖消除的做用在儘可能小的範圍使用鎖,而鎖粗化則相反,擴大加鎖範圍。好比加鎖出如今循環體中,每次循環都要執行加鎖解鎖的,如此頻繁操做比較消耗性能
synchronized的四種狀態
鎖膨脹方向:無鎖->偏向鎖->輕量級鎖->重量級鎖,synchronized會隨着競爭狀況逐漸升級,如出現了閒置的monitor也會出現鎖降級
偏向鎖:減小同一個線程獲取鎖的代價
ps:核心思想就是若是一個線程得到了鎖,那麼鎖就進入偏向模式,此時MarkWord的結構也變成偏向鎖結構,當該線程再次請求鎖時,無需再作任何同步操做,即獲取鎖的過程只須要檢查MarkWord的鎖標記位爲偏向鎖以及當前線程ID等於MarkWord的ThreadID便可,這樣就省去了大量有關鎖申請的操做
不適合用於鎖競爭比較激烈的多線程場合
輕量級鎖:
輕量級鎖是由偏向鎖升級而來的,偏向鎖運行再一個線程進入同步塊的狀況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級爲輕量級鎖
適用場景:線程交替執行的同步塊
若存在同一時間訪問同一鎖的狀況,就會致使輕量級鎖膨脹爲重量級鎖
輕量級鎖的加鎖過程:
此圖來自https://blog.csdn.net/zqz_zqz
鎖的內存語義
總結:
問:synchronized和ReentrantLock的區別?
ReentrantLock(可重入鎖)
ReentrantLock公平性設置
ReentrantLock fairLock = new ReentrantLock(true);
參數爲ture時,傾向於將鎖賦予等待時間最久的線程
公平鎖:獲取鎖的順序按前後調用lock方法的順序(慎用,一般公平性沒有想象的那麼重要,java默認的調用策略不多會有飢餓狀況的發生,與此同時若要保證公平性,會增長額外的開銷,致使必定的吞吐量降低)
非公平鎖:獲取鎖的順序是無序的,synchronized是非公平鎖
例子:
package interview.thread; import java.util.concurrent.locks.ReentrantLock; /** * @Author: cctv * @Date: 2019/5/21 11:46 */ public class ReentrantLockDemo implements Runnable { private static ReentrantLock lock = new ReentrantLock(false); @Override public void run() { while (true) { lock.lock(); System.out.println(Thread.currentThread().getName() + " get lock"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public static void main(String[] args) { ReentrantLockDemo rtld = new ReentrantLockDemo(); Thread t1 = new Thread(rtld); Thread t2 = new Thread(rtld); t1.start(); t2.start(); } }
公平鎖 new ReentrantLock(true);
非公平鎖 new ReentrantLock(false);
ReentrantLock將鎖對象化
是否能將wait\notify\notifyAll對象化
總結synchronized和ReentrantLock的區別:
volatile和synchronized的區別
3:CAS(Co'mpare and Swap)
一種高效實現線程安全性的方法
一、支持原子更新操做、適用於計數器、序列發生器等場景。
二、屬於樂觀鎖機制,號稱 lock - free
三、CAS操做失敗時由開發者決定是繼續嘗試,仍是執行別的操做。
悲觀鎖:
CAS 多數狀況下對開發者來講是透明的。
在使用CAS 前要考慮ABA 問題 是否影響程序併發的正確性,若是須要解決ABA 問題,改用傳統的互斥同步,可能會比原子性更高效。
java線程池,利用Exceutors建立不一樣的線程池知足不一樣場景需求:
Fork/Join框架
由於分割成若干個小任務由多個線程去執行,就會出現有的線程已經完成任務而有的還未完成任務,已經完成的線程就閒置了,爲了提高效率,讓已經完成任務的線程去其餘線程竊取隊列裏的任務來執行。爲了減小竊取線程對其餘線程的競爭,一般會使用雙端隊列,執行任務的線程從頭部拿任務執行,竊取線程是從隊列尾部拿任務執行
問:爲何要使用線程池?
Executor的框架圖
JUC的三個Executor接口
ThreadPoolExecutor的構造函數
ps:newCachedThreadPool傳入的隊列是容量爲0的SynchronousQueue,(Java 6的併發編程包中的SynchronousQueue是一個沒有數據緩衝的BlockingQueue,生產者線程對其的插入操做put必須等待消費者的移除操做take,反過來也同樣)
handler:線程池的飽和策略
execute方法執行流程以下:
線程池的狀態:
狀態轉換圖:
工做線程的生命週期:
問:如何選擇線程池大小?(沒有絕對的算法或規定,是靠經驗累計總結出來的)
ps:
阿里編碼規範指出:線程池不容許使用Executors去建立,而是經過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同窗更加明確線程池的運行規則,規避資源耗盡的風險。 說明:Executors各個方法的弊端:
1)newFixedThreadPool和newSingleThreadExecutor:
主要問題是堆積的請求處理隊列可能會耗費很是大的內存,甚至OOM。
2)newCachedThreadPool和newScheduledThreadPool:
主要問題是線程數最大數是Integer.MAX_VALUE,可能會建立數量很是多的線程,甚至OOM。
例子:使用Guava的ThreadFactoryBuilder
輸出:
不加劇試的輸出是:
從例子中看出 maxPoolSize + QueueSize < taskNum 就會拋出拒絕異常 若是不catch這個異常程序沒法結束(這裏重試機制只是個demo,正確的作法是實現RejectedExecutionHandler接口自定義handler處理)