進入時:monitorenter
每個對象有一個監視器鎖(monitor)。當monitor被占用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:
1、如果monitor的進入數為0,則該線程進入monitor,然后將進入數設置為1,該線程即為monitor的所有者。
2、如果該線程已經占有該monitor,又重新進入,則進入monitor的進入數加1。
3、如果其他線程已經占用了monitor,則該線程進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權。
退出時:monitorexit
執行monitorexit的線程必須是objectref所對應的monitor的所有者。
指令執行時,monitor的進入數減1,如果減1后進入數為0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個
monitor 的所有權。
通過這兩段描述,我們應該能很清楚的看出synchronized的實現原理,synchronized的語義底層是通過一個monitor的對象來完成。
其實wait/notify等方法也依賴於monitor對象,這就是為什么只有在同步的塊或者方法中才能調用wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException的異常的原因。
當synchronized加在方法前時:
從反編譯的結果來看,方法的同步並沒有通過指令monitorenter和monitorexit來完成(其實也可以通過這兩條指令來實現)。
相對於普通方法,其常量池中多了ACC_SYNCHRONIZED標示符。
JVM就是根據該標示符來實現方法的同步的:當方法被調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設置,如果設置了,執行線程將先獲取monitor,獲取成功之后才能執行方法體,方法執行完后再釋放monitor。在方法執行期間,其他任何線程都無法再獲得同一個monitor對象。 其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過字節碼來完成。
---------------------
以上轉自:https://blog.csdn.net/hbtj_1216/article/details/77773292
Monitor 是線程私有的數據結構,每一個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。每一個被鎖住的對象都會和一個monitor關聯(對象頭的MarkWord中的LockWord指向monitor的起始地址),同時monitor中有一個Owner字段存放擁有該鎖的線程的唯一標識,表示該鎖被這個線程占用。其結構如下:

JVM中synchronized的優化實現
一、基礎知識
1.對象頭中的Mark Word數據結構

上圖是java對象在堆中的結構,其中對象頭是我們本次關注的重點。
synchronized用的鎖是存在Java對象頭里的?Hotspot虛擬機的對象頭主要包括兩部分數據:Mark Word(標記字段)、Klass Pointer(類型指針)。其中Klass Point是是對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例,Mark Word用於存儲對象自身的運行時數據,它是實現輕量級鎖和偏向鎖的關鍵,所以下面將重點闡述。
Mark Word用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程 ID、偏向時間戳等等。Java對象頭一般占有兩個機器碼(在32位虛擬機中,1個機器碼等於4字節,也就是32bit),但是如果對象是數組類型,則需要三個機器碼,因為JVM虛擬機可以通過Java對象的元數據信息確定Java對象的大小,但是無法從數組的元數據來確認數組的大小,所以用一塊來記錄數組長度。下圖是Java對象頭的存儲結構(32位虛擬機):

對象頭信息是與對象自身定義的數據無關的額外存儲成本,但是考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存存儲盡量多的數據,它會根據對象的狀態復用自己的存儲空間,也就是說,Mark Word會隨着程序的運行發生變化(依據鎖標志位和是否偏向鎖進行判斷),變化狀態如下(32位虛擬機):

2.CAS操作(非阻塞同步)
2.1概念
使用鎖時,線程獲取鎖是一種悲觀鎖策略,即假設每一次執行臨界區代碼都會產生沖突,所以當前線程獲取到鎖的時候同時也會阻塞其他線程獲取該鎖.而CAS操作(又稱為無鎖操作)是一種樂觀鎖策略.它假設所有線程訪問共享資源的時候不會出現沖突,既然不會出現沖突自然而然就不會阻塞其他線程的操作.因此,線程就不會出現阻塞停頓的狀態.那么,如果出現沖突怎么辦?無鎖操作是使用CAS(compare and swap)又叫做比較交換來鑒別線程是否出現沖突,出現沖突重試當前操作直到沒有沖突為止.
CAS的實現需要硬件指令集的支撐,在JDK1.6及之后虛擬機才可以使用處理器提供的CMPXCHG指令實現.
2.2操作過程
CAS比較交換的過程可以通俗的理解為CAS(V,O,N),包含三個值分別為:V 內存地址存放的實際值;O 預期的值(舊值);N 更新的新值。當V和O相同時,也就是說舊值和內存中實際的值相同表明該值沒有被其他線程更改過,即該舊值O就是目前來說最新的值了,自然而然可以將新值N賦值給V。反之,V和O不相同,表明該值已經被其他線程改過了則該舊值O不是最新版本的值了,所以不能將新值N賦給V,返回V即可。當多個線程使用CAS操作一個變量是,只有一個線程會成功,並成功更新,其余會失敗。失敗的線程會重新嘗試,當然也可以選擇掛起線程
2.3 CAS的應用場景
在J.U.C包中利用CAS實現類有很多,可以說是支撐起整個concurrency包的實現,在Lock實現中會有CAS改變state變量,在atomic包中的實現類也幾乎都是用CAS實現.
2.4. ABA問題
因為CAS會檢查舊值有沒有變化,這里存在這樣一個有意思的問題.這里存在這樣一個有意思的問題.比如一個舊值A變為成B,然后再變成A,剛好在做CAS時檢查發現舊值並沒有變化依然為A,但是實際上的確發生了變化.解決方案可以沿襲數據庫常用的樂觀鎖方式,添加一個版本號或時間戳可以解決.原來的變化路徑 A->B->A 就變成了 1A->2B->3C. java這么優秀的語言,當然在java1.5后的atomic包中提供了AtomicStampedReference來解決ABA問題,解決思路就是這樣的.
2.5.只能保證一個共享變量的原子操作
當對一個共享變量執行操作是CAS能保證其原子性,如果對多個共享變量進行操作,CAS就不性能保證其原子性.有一個解決方案就是利用對象整合多個共享變量,即一個類中的成員變量就是這幾個共享變量.然后將這個對象做CAS操作就可以保證其原子.atomic中提供了AtomicReference來保證引用對象之間的原子性.
二、synchronized的優化
對於synchronized這個關鍵字,在jdk1.5及之前,他是一個重量級鎖,開銷很大,建議大家少用點。但到了jdk1.6之后,該關鍵字被進行了很多的優化,已經不像以前那樣不給力了,建議大家多使用。
(1)在jdk1.6中對於synchronized的實現,JVM中引入了偏向鎖、輕量級鎖,重量級鎖(自旋鎖--JDK1.4.2引入,自適應自旋鎖,鎖消除,鎖粗化)等方法和概念,對synchronized鎖的實現進行了優化。
(2)synchronized中的鎖一般分為重量鎖(對象鎖),自旋鎖,自適應自旋鎖,輕量鎖,偏向鎖
自旋鎖的應用場景:
線程的阻塞和喚醒需要CPU從用戶態轉為核心態,頻繁的阻塞和喚醒對CPU來說是一件負擔很重的工作,勢必會給系統的並發性能帶來很大的壓力。同時我們發現在許多應用上面,對象鎖的鎖狀態只會持續很短一段時間,為了這一段很短的時間頻繁地阻塞和喚醒線程是非常不值得的,所以引入自旋鎖。
若一個線程等待獲取鎖對象所持續的時間非常短,這時適合使用自旋鎖。所謂自旋鎖,就是等待鎖的線程並不進入阻塞狀態,而是執行一個無意義的循環。在循環結束后查看鎖是否已經被釋放,若已經釋放則直接進入執行狀態。因為長時間無意義循環也會大量浪費系統資源,因此自旋鎖適用於間隔時間短的加鎖場景。
自適應自旋鎖對自旋次數的調整:
JDK 1.6引入了更加聰明的自旋鎖,即自適應自旋鎖。所謂自適應就意味着自旋的次數不再是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。線程如果自旋成功了,那么下次自旋的次數會更加多,因為虛擬機認為既然上次成功了,那么此次自旋也很有可能會再次成功,那么它就會允許自旋等待持續的次數更多。反之,如果對於某個鎖,很少有自旋能夠成功的,那么在以后要或者這個鎖的時候自旋的次數會減少甚至省略掉自旋過程,以免浪費處理器資源。
鎖消除
有些情況下,JVM檢測到不可能存在共享數據競爭,這時JVM會對這些同步鎖進行鎖消除。鎖消除的依據是逃逸分析的數據支持。
有時我們雖然沒有顯示使用鎖,但是我們在使用一些JDK的內置API時,如StringBuffer、Vector、HashTable等,它們的內部實現存在隱形的加鎖操作。比如StringBuffer的append()方法,Vector的add()方法。
public void vectorTest(){ Vector<String> vector = new Vector<String>(); for(int i = 0 ; i < 10 ; i++){ vector.add(i + ""); } System.out.println(vector); }
在運行這段代碼時,JVM可以明顯檢測到變量vector沒有逃逸出方法vectorTest()之外,所以JVM可以大膽地將vector內部的加鎖操作消除。
鎖粗化
我們知道在使用同步鎖的時候,需要讓同步塊的作用范圍盡可能小—僅在共享數據的實際作用域中才進行同步,這樣做的目的是為了使需要同步的操作數量盡可能縮小,如果存在鎖競爭,那么等待鎖的線程也能盡快拿到鎖。
在大多數的情況下,上述觀點是正確的。但是如果一系列的連續加鎖解鎖操作,可能會導致不必要的性能損耗,所以引入鎖粗話的概念。
鎖粗化就是將多個連續的加鎖、解鎖操作連接在一起,擴展成一個范圍更大的鎖。如上面實例:vector每次add的時候都需要加鎖操作,JVM檢測到對同一個對象(vector)連續加鎖、解鎖操作,會合並一個更大范圍的加鎖、解鎖操作,即加鎖解鎖操作會移到for循環之外。
輕量鎖和偏向鎖:
適用於沒有線程競爭的情況。無法代替重量鎖
重量級鎖:
重量級鎖通過對象內部的監視器(monitor)實現,其中monitor的本質是依賴於底層操作系統的Mutex Lock實現,操作系統實現線程之間的切換需要從用戶態到內核態的切換,切換成本非常高。
(3)上面幾種鎖都是JVM自己內部實現,當我們執行synchronized同步塊的時候jvm會根據啟用的鎖和當前線程的爭用情況,決定如何執行同步操作;
在所有的鎖都啟用的情況下線程進入臨界區時會先去獲取偏向鎖,如果已經存在偏向鎖了,則會嘗試獲取輕量級鎖,如果以上兩種都失敗,則啟用自旋鎖,如果自旋也沒有獲取到鎖,則使用重量級鎖,沒有獲取到鎖的線程阻塞掛起,直到持有鎖的線程執行完同步塊喚醒他們;
偏向鎖--》輕量級鎖--》自旋鎖--》重量級鎖
輕量鎖與偏向鎖的不同:
- 輕量鎖每次退出同步塊都需要釋放鎖,而偏向鎖是在競爭發生時才釋放鎖
- 輕量鎖每次進入/退出同步塊都需要CAS更新對象頭
- 爭奪輕量級鎖失敗時,自旋嘗試搶占鎖
可以看到輕量鎖適合在競爭情況下使用,其自旋鎖可以保證響應速度快,但自旋操作會占用CPU,所以一些計算時間長的操作不適合使用輕量級鎖。
==>可以認為 自旋鎖 是輕量鎖執行中的一部分

(4)偏向鎖是在無鎖爭用的情況下使用的,也就是同步塊在當前線程沒有執行完之前,沒有其它線程會執行該同步快,一旦有了第二個線程的爭用,偏向鎖就會升級為輕量級鎖,一點有兩個以上線程爭用,就會升級為重量級鎖;
(5)如果線程爭用激烈,那么應該禁用偏向鎖。
偏向鎖的獲取和釋放流程 及 輕量級鎖的獲取和釋放/膨脹過程 請參考 https://blog.csdn.net/shandian000/article/details/54927876
不同鎖的比較

Synchronized是非公平鎖。 Synchronized在線程進入ContentionList時,等待的線程會先嘗試自旋獲取鎖,如果獲取不到就進入ContentionList,這明顯對於已經進入隊列的線程是不公平的,還有一個不公平的事情就是自旋獲取鎖的線程還可能直接搶占OnDeck線程的鎖資源。
三、程序中可以進行的鎖優化
以上介紹的鎖優化是JVM自動控制的,不是我們代碼中能夠控制的,但是借鑒上面的思想,我們可以優化我們自己線程的加鎖操作;
1.減少鎖的時間
不需要同步執行的代碼,能不放在同步快里面執行,就不要放在同步塊內,可以讓鎖盡快釋放;
2.減少鎖的粒度
它的思想是將物理上的一個鎖,拆成邏輯上的多個鎖,增加並行度,從而降低鎖競爭。它的思想也是用空間來換時間;
java中很多數據結構都是采用這種方法提高並發操作的效率,例如:ConcurrentHashMap、LongAdder、LinkedBlockingQueue
ConcurrentHashMap
java中的ConcurrentHashMap在jdk1.8之前的版本,使用一個Segment 數組: Segment< K,V >[] segments
Segment繼承自ReenTrantLock,所以每個Segment就是個可重入鎖,每個Segment 有一個HashEntry< K,V >數組用來存放數據,put操作時,先確定往哪個Segment放數據,只需要鎖定這個Segment,執行put,其它的Segment不會被鎖定;所以數組中有多少個Segment就允許同一時刻多少個線程存放數據,這樣增加了並發能力。
LongAdder
LongAdder 實現思路也類似ConcurrentHashMap,LongAdder有一個根據當前並發狀況動態改變的Cell數組,Cell對象里面有一個long類型的value用來存儲值;
開始沒有並發爭用的時候或者是cells數組正在初始化的時候,會使用cas來將值累加到成員變量的base上,在並發爭用的情況下,LongAdder會初始化cells數組,在Cell數組中選定一個Cell加鎖,數組有多少個cell,就允許同時有多少線程進行修改,最后將數組中每個Cell中的value相加,在加上base的值,就是最終的值;cell數組還能根據當前線程爭用情況進行擴容,初始長度為2,每次擴容會增長一倍,直到擴容到大於等於cpu數量就不再擴容;
LinkedBlockingQueue
LinkedBlockingQueue也體現了這樣的思想,在隊列頭入隊,在隊列尾出隊,入隊和出隊使用不同的鎖,相對於LinkedBlockingArray只有一個鎖效率要高;
注意:拆鎖的粒度不能無限拆,最多可以將一個鎖拆為當前cup數量個鎖即可;
3.鎖粗化
大部分情況下我們是要讓鎖的粒度最小化,鎖的粗化則是要增大鎖的粒度;
在以下場景下需要粗化鎖的粒度: 假如有一個循環,循環內的操作需要加鎖,我們應該把鎖放到循環外面,否則每次進出循環,都進出一次臨界區,效率是非常差的;
3.使用讀寫鎖
ReentrantReadWriteLock 是一個讀寫鎖,讀操作加讀鎖,可以並發讀,寫操作使用寫鎖,只能單線程寫;
4.消除緩存行的偽共享(基本由JVM實現)
除了我們在代碼中使用的同步鎖和jvm自己內置的同步鎖外,還有一種隱藏的鎖就是緩存行,它也被稱為性能殺手。
在多核cup的處理器中,每個cup都有自己獨占的一級緩存、二級緩存,甚至還有一個共享的三級緩存,為了提高性能,cpu讀取數據是以緩存行為最小單元讀取的;32位的cpu緩存行為32字節,64位cup的緩存行為64字節,這就導致了一些問題。
例如,多個不需要同步的變量因為存儲在連續的32字節或64字節里面,當需要其中的一個變量時,就將它們作為一個緩存行一起加載到某個cup-1私有的緩存中(雖然只需要一個變量,但是cpu讀取會以緩存行為最小單位,將其相鄰的變量一起讀入),被讀入cpu緩存的變量相當於是對主內存變量的一個拷貝,也相當於變相的將在同一個緩存行中的幾個變量加了一把鎖,這個緩存行中任何一個變量發生了變化,當cup-2需要讀取這個緩存行時,就需要先將cup-1中被改變了的整個緩存行更新回主存(即使其它變量沒有更改),然后cup-2才能夠讀取,而cup-2可能需要更改這個緩存行的變量與cpu-1已經更改的緩存行中的變量是不一樣的,所以這相當於給幾個毫不相關的變量加了一把同步鎖;
為了防止偽共享,不同jdk版本實現方式是不一樣的:
1. 在jdk1.7之前會 將需要獨占緩存行的變量前后添加一組long類型的變量,依靠這些無意義的數組的填充做到一個變量自己獨占一個緩存行;
2. 在jdk1.7因為jvm會將這些沒有用到的變量優化掉,所以采用繼承一個聲明了好多long變量的類的方式來實現;
3. 在jdk1.8中通過添加sun.misc.Contended注解來解決這個問題,若要使該注解有效必須在jvm中添加以下參數:
-XX:-RestrictContended
sun.misc.Contended注解會在變量前面添加128字節的padding將當前變量與其他變量進行隔離;
關於什么是緩存行,jdk是如何避免緩存行的,網上有非常多的解釋,在這里就不再深入講解了;
---------------------
以上參考:https://blog.csdn.net/kirito_j/article/details/79201213
