問:線程安全問題的主要誘因?java
解決方法:同一時刻有且只有一個線程在操做共享數據,其餘線程必須等到該線程處理完數據後再對共享數據進行操做緩存
互斥鎖的特徵:安全
ps:synchronized 鎖的不是代碼,鎖的是對象多線程
獲取鎖的分類:獲取對象鎖、獲取類鎖併發
獲取對象鎖的兩種用法:框架
獲取類鎖的兩種用法:jvm
類鎖和對象鎖在鎖同一個對象的時候表現行爲是同樣的,由於class也是對象鎖,只是比較特殊,全部的實例共享同一個類(同一個class對象)佈局
若是鎖的是不一樣對象(同一個class的不一樣實例)表現就不同了,類鎖是全同步的,對象鎖是按對象區分同步的性能
類鎖和對象鎖互不干擾的,由於對象實例和類是兩個不一樣的對象優化
對象鎖和類鎖的終結:
樂觀鎖
樂觀鎖是一種樂觀思想,即認爲讀多寫少,遇到併發寫的可能性低,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,採起在寫時先讀出當前版本號,而後加鎖操做(比較跟上一次的版本號,若是同樣則更新),若是失敗則要重複讀-比較-寫的操做。
java中的樂觀鎖基本都是經過CAS操做實現的,CAS是一種更新的原子操做,比較當前值跟傳入值是否同樣,同樣則更新,不然失敗。
悲觀鎖
悲觀鎖是就是悲觀思想,即認爲寫多,遇到併發寫的可能性高,每次去拿數據的時候都認爲別人會修改,因此每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會block直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嚐試cas樂觀鎖去獲取鎖,獲取不到,纔會轉換爲悲觀鎖,如RetreenLock。
阻塞代價
java的線程是映射到操做系統原生線程之上的,若是要阻塞或喚醒一個線程就須要操做系統介入,須要在戶態與核心態之間切換,這種切換會消耗大量的系統資源,由於用戶態與內核態都有各自專用的內存空間,專用的寄存器等,用戶態切換至內核態須要傳遞給許多變量、參數給內核,內核也須要保護好用戶態在切換時的一些寄存器值、變量等,以便內核態調用結束後切換回用戶態繼續工做。
synchronized會致使爭用不到鎖的線程進入阻塞狀態,因此說它是java語言中一個重量級的同步操縱,被稱爲重量級鎖,爲了緩解上述性能問題,JVM從1.5開始,引入了輕量鎖與偏向鎖,默認啓用了自旋鎖,他們都屬於樂觀鎖。
深刻理解synchronized底層實現原理:
Java對象頭和Monitor是實現synchronized的基礎
hotspot中對象在內存的佈局是分3部分
這裏主要講對象頭:通常而言synchronized使用的鎖對象是存儲在對象頭裏的,對象頭是由Mark Word和Class Metadata Address組成
要詳細瞭解java對象的結構點擊:http://www.javashuo.com/article/p-gpknmlyb-hd.html
mark word存儲自身運行時數據,是實現輕量級鎖和偏向鎖的關鍵,默認存儲對象的hasCode、分代年齡、鎖類型、鎖標誌位等信息。
mark word數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32bit和64bit,它的最後2bit是鎖狀態標誌位,用來標記當前對象的狀態,對象的所處的狀態,決定了markword存儲的內容,以下表所示:
因爲對象頭的信息是與對象定義的數據沒有關係的額外存儲成本,因此考慮到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鎖並復位對應變量的值。
接下來是字節碼的分析:
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層面作了較大優化,減小重量級鎖的使用):
自旋鎖: