線程安全?
當存在多個線程操作共享數(shù)據(jù)時,需要保證同一時刻有且只有一個線程在操作共享數(shù)據(jù),其他線程必須等到該線程處理完數(shù)據(jù)后再進行,這種方式叫互斥鎖,即能達到互斥訪問目的的鎖,也就是說當一個共享數(shù)據(jù)被當前正在訪問的線程加上互斥鎖后,在同一個時刻,其他線程只能處于等待的狀態(tài),直到當前線程處理完畢釋放該鎖。在 Java 中,關(guān)鍵字syn可以保證在同一個時刻,只有一個線程可以執(zhí)行某個方法或某個代碼塊。
定義
三大特性:可重入性、重量級、保證原子性,可見性和有序性。

可見性:完全可以替代Volatile。
互斥性:即在同一時間只允許一個線程持有某個對象鎖,通過這種特性來實現(xiàn)多線程中的協(xié)調(diào)機制,這樣在同一時間只有一個線程對需同步的代碼塊(復(fù)合操作)進行訪問?;コ庑晕覀円餐Q為操作的原子性。
鎖的狀態(tài)總共有四種,無鎖狀態(tài)、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖,但是鎖升級是單向的,只能從低到高升級,不會出現(xiàn)鎖的降級。
synchronized 的用法
1、修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼塊前要獲得給定對象的鎖。
2、修飾實例方法(對象鎖),是給調(diào)用這個方法的對象加鎖,進入同步代碼前要獲得當前對象實例的鎖。
3、修飾靜態(tài)方法(類鎖),是給當前類class對象加鎖,作用于該類的所有對象,進入同步代碼前要獲得當前類class對象的鎖。
注意:同類中syn修飾的多個靜態(tài)方法加的鎖會互斥
對象鎖和類鎖
對象鎖:修飾非靜態(tài)方法、synchronized(this|object) {}
在Java中,每個對象都會有一個 monitor 對象,這個對象其實就是 Java 對象的鎖,通常會被稱為“內(nèi)置鎖”或“對象鎖”。類的對象可以有多個,所以每個對象有其獨立的對象鎖,互不干擾。
類鎖:修飾靜態(tài)方法、synchronized(類.class) {}
在Java中,針對每個類也有一個鎖,可以稱為“類鎖”,類鎖實際上是通過對象鎖實現(xiàn)的,即類的 Class 對象鎖。每個類只有一個 Class 對象,所以每個類只有一個類鎖。
小結(jié):
1、只要采用類鎖,就會攔截所有線程,只能讓一個線程訪問。
2、如果對象鎖跟訪問的對象沒有關(guān)系,那么就會都同時訪問。
3、對于對象鎖(this),如果是同一個實例,就會按順序訪問,但是如果是不同實例,就可以同時訪問。
底層實現(xiàn):

在JVM中,對象在內(nèi)存中的布局分為三塊區(qū)域:對象頭、實例數(shù)據(jù)和對齊填充。
實例變量:
存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息,如果是數(shù)組的實例部分還包括數(shù)組的長度,這部分內(nèi)存按4字節(jié)對齊。
填充數(shù)據(jù):
由于虛擬機要求對象起始地址必須是8字節(jié)的整數(shù)倍。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對齊,這點了解即可。
Java頭對象
它是實現(xiàn)syn鎖對象的基礎(chǔ),syn使用的鎖對象是存儲在Java對象頭里的,jvm中采用2個字來存儲對象頭(如果對象是數(shù)組則會分配3個字,多出來的1個字記錄的是數(shù)組長度),其結(jié)構(gòu)是由Mark Word 和 Class Metadata Address 組成。
Mark Word(標記字段):
存儲對象自身運行時數(shù)據(jù)信息;如:對象hashCode、GC分代年齡、GC標志、鎖狀態(tài)標志、線程持有的鎖、偏向線程 ID、偏向時間戳等。它是實現(xiàn)輕量級鎖和偏向鎖的關(guān)鍵.
Class Metadata Address(類型指針、Klass Pointer):
指向?qū)ο蟮念愒獢?shù)據(jù),JVM通過這個指針確定該對象是哪個類的實例。
由于對象頭的信息是與對象自身定義的數(shù)據(jù)沒有關(guān)系的額外存儲成本,因此考慮到JVM的空間效率,Mark Word 被設(shè)計成為一個非固定的數(shù)據(jù)結(jié)構(gòu),以便存儲更多有效的數(shù)據(jù),它會根據(jù)對象本身的狀態(tài)復(fù)用自己的存儲空間,如32位JVM下,除了上述列出的Mark Word默認存儲結(jié)構(gòu)外,還有如下可能變化的結(jié)構(gòu):

什么是Monitor?
管程對象、對象監(jiān)視器、可以理解為一個同步工具或一種同步機制;每個對象的對象頭中都存在一個對應(yīng)的monitor(對象頭Address的指針指向monitor對象),對象與其monitor之間的關(guān)系有存在多種實現(xiàn)方式,如monitor可以與對象一起創(chuàng)建銷毀或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有后,它便處于鎖定狀態(tài)。在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現(xiàn)的)
正因為每個對象都存在一個對應(yīng)的monitor,所以Java中任意對象都可以作為鎖,也解釋了notify/notifyAll/wait等方法存在于頂級對象Object中的原因。
ObjectMonitor() {
_header = NULL;
_count = 0; 記錄個數(shù)
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; 處于wait狀態(tài)的線程,會被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; 處于等待鎖block狀態(tài)的線程,會被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
當多個線程同時請求某個對象監(jiān)視器時,對象監(jiān)視器會設(shè)置幾種狀態(tài)用來區(qū)分請求的線程:
Contention List:所有請求鎖的線程將被首先放置到該競爭隊列
Entry List:Contention List中那些有資格成為候選人的線程被移到Entry List
Wait Set:那些調(diào)用wait方法被阻塞的線程被放置到Wait Set
OnDeck:任何時刻最多只能有一個線程正在競爭鎖,該線程稱為OnDeck
Owner:獲得鎖的線程稱為Owner
!Owner:釋放鎖的線程
當線程獲取到對象的monitor 后進入 _Owner 區(qū)域并把monitor中的owner變量設(shè)置為當前線程同時monitor中的計數(shù)器count加1,若線程調(diào)用 wait() 方法,將釋放當前持有的monitor,owner變量恢復(fù)為null,count自減1,同時該線程進入WaitSet集合中等待被喚醒。若當前線程執(zhí)行完畢也將釋放monitor鎖并復(fù)位變量的值,以便其他線程進入獲取monitor鎖。

新請求鎖的線程將首先被加入到ConetentionList中,當某個擁有鎖的線程(Owner狀態(tài))調(diào)用unlock之后,如果發(fā)現(xiàn)EntryList為空則從ContentionList中移動線程到EntryList中。
ContentionList虛擬隊列
ContentionList并不是一個真正的Queue,而只是一個虛擬隊列,原因在于ContentionList是由Node及其next指針邏輯構(gòu)成,并不存在一個Queue的數(shù)據(jù)結(jié)構(gòu)。ContentionList是一個先進先出(FIFO)的隊列,每次新加入Node時都會在隊頭進行,通過CAS改變第一個節(jié)點的的指針為新增節(jié)點,同時設(shè)置新增節(jié)點的next指向后續(xù)節(jié)點,而取得操作則發(fā)生在隊尾。顯然,該結(jié)構(gòu)其實是個Lock-Free的隊列。
因為只有Owner線程才能從隊尾取元素,也即線程出列操作無爭用,當然也就避免了CAS的ABA問題。
EntryList
EntryList與ContentionList邏輯上同屬等待隊列,ContentionList會被線程并發(fā)訪問,為了降低對ContentionList隊尾的爭用,而建立EntryList。Owner線程在unlock時會從ContentionList中遷移線程到EntryList,并會指定EntryList中的某個線程(一般為Head)為Ready(OnDeck)線程。Owner線程并不是把鎖傳遞給OnDeck線程,只是把競爭鎖的權(quán)利交給OnDeck,OnDeck線程需要重新競爭鎖。這樣做雖然犧牲了一定的公平性,但極大的提高了整體吞吐量,在Hotspot中把OnDeck的選擇行為稱之為“競爭切換”。
OnDeck線程獲得鎖后即變?yōu)閛wner線程,無法獲得鎖則會依然留在EntryList中,考慮到公平性,在EntryList中的位置不發(fā)生變化(依然在隊頭)。如果Owner線程被wait方法阻塞,則轉(zhuǎn)移到WaitSet隊列;如果在某個時刻被notify/notifyAll喚醒,則再次轉(zhuǎn)移到EntryList。
重量級鎖
java 6之前syn都是重量級鎖,鎖標識位為10,
JVM 是通過進入、退出對象監(jiān)視器(Monitor) 來實現(xiàn)對方法、同步塊的同步的,而對象監(jiān)視器的本質(zhì)依賴于底層操作系統(tǒng)的互斥鎖(Mutex Lock) 實現(xiàn)。
對于沒有獲取到鎖的線程將會阻塞到方法入口處,直到獲取鎖的線程monitor.exit之后才能嘗試繼續(xù)獲取鎖。
syn代碼塊底層實現(xiàn)
具體實現(xiàn)是在編譯之后在同步方法調(diào)用前加入一個monitor.enter指令,在退出方法和異常處插入monitor.exit的指令。上代碼:
public static void main(String[] args) {
synchronized (Demo02.class){
System.out.println("Synchronize");
}
}
編譯后字節(jié)碼:
public static void main(java.lang.String[]);
Code:
0: ldc #2
2: dup
3: astore_1
4: monitorenter // 進入同步方法
5: getstatic #3
8: ldc #4
10: invokevirtual #5
13: aload_1
14: monitorexit // 退出同步方法
15: goto 23
18: astore_2
19: aload_1
20: monitorexit // 退出同步方法
21: aload_2
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
從字節(jié)碼中可知同步語句塊的實現(xiàn)使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結(jié)束位置,當執(zhí)行monitorenter指令時,當前線程將試圖獲取 objectref(即對象鎖) 所對應(yīng)的 monitor 的持有權(quán),當 objectref 的 monitor 的計數(shù)器為 0,那線程可以成功取得 monitor,并將計數(shù)器值設(shè)為 1也就是加1,取鎖成功。如果當前線程已經(jīng)擁有 objectref 的 monitor 的持有權(quán),那它可以重入這個 monitor (鎖的重入性),重入時計數(shù)器的值也會加 1。倘若其他線程已經(jīng)擁有 objectref 的 monitor 的所有權(quán),那當前線程將被阻塞,直到正在執(zhí)行的線程monitorexit指令被執(zhí)行,才會釋放monitor鎖(計數(shù)器值設(shè)為0) ,其他線程才能嘗試獲取 monitor鎖 。
注意:字節(jié)碼中為什么多了一個monitorexit指令?
編譯器需要確保方法中調(diào)用過的每條monitorenter指令都要執(zhí)行對應(yīng)的monitorexit 指令。為了保證在方法異常時,monitorenter和monitorexit指令也能正常配對執(zhí)行,編譯器會自動產(chǎn)生一個異常處理器,處理所有異常,目的就是用來執(zhí)行異常的monitorexit指令。所以多出的monitorexit就是異常結(jié)束時,用來釋放monitor的。
syn方法底層實現(xiàn)
方法級的同步是隱式,即無需通過字節(jié)碼指令來控制的,它實現(xiàn)在方法調(diào)用和返回操作之中。JVM可以從方法常量池中的方法表結(jié)構(gòu)(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標志區(qū)分一個方法是否同步方法。當方法調(diào)用時,調(diào)用指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先持有monitor(虛擬機規(guī)范中用的是管程一詞), 然后再執(zhí)行方法,最后再方法完成(無論是正常完成還是非正常完成)時釋放monitor。在方法執(zhí)行期間,執(zhí)行線程持有了monitor,其他任何線程都無法再獲得同一個monitor。如果一個同步方法執(zhí)行期間拋 出了異常,并且在方法內(nèi)部無法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放。下面我們看看字節(jié)碼層面如何實現(xiàn):
public class Demo03 {
public int i;
public synchronized void syncTask(){
i++;
}
public static void main(String[] args) {
new Demo03().syncTask();
}
}
編譯后的字節(jié)碼:
public synchronized void syncTask();
descriptor: ()V
// ACC_PUBLIC表示public修飾,ACC_SYNCHRONIZED表示該方法為同步方法
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 8: 0
line 9: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/test/demo/test/Demo03;
從字節(jié)碼中可以看出,syn修飾的方法并沒有monitorenter指令和monitorexit指令,取得代之的確是ACC_SYNCHRONIZED標識,JVM通過ACC_SYNCHRONIZED標識來辨別方法是否為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用。
缺點:
重量級鎖效率低下,因為監(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的Mutex Lock來實現(xiàn)的,而操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個狀態(tài)之間的轉(zhuǎn)換需要相對較長的時間(代價相對較高)。
JDK1.6優(yōu)化
為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了輕量級鎖和偏向鎖。
輕量級鎖
這時Mark Word的結(jié)構(gòu)也變?yōu)檩p量級鎖的結(jié)構(gòu),輕量級鎖并不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少重量鎖產(chǎn)生的性能消耗。
輕量鎖認為競爭存在,但是競爭的程度很輕,一般兩個線程對于同一個鎖的操作都會錯開,或者說稍微等待一下(自旋),另一個線程就會釋放鎖。 但是當自旋超過一定的次數(shù),或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量鎖就會膨脹為重量鎖。
輕量鎖加鎖過程
1、在代碼進入同步塊時,如果同步對象鎖狀態(tài)為無鎖狀態(tài)(鎖標志位為“01”狀態(tài),是否為偏向鎖為“0”),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝。
2、拷貝對象頭中的Mark Word復(fù)制到鎖記錄中。
3、拷貝成功后,虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,并將Lock Record里的owner指針指向object mark word。如果更新成功則執(zhí)行步驟(4),否則執(zhí)行步驟(5)。
4、如果這個更新動作成功,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標志位設(shè)置為“00”,即表示此對象處于輕量級鎖定狀態(tài)。
5、如果這個更新操作失敗,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經(jīng)擁有了這個對象的鎖,那就可以直接進入同步塊繼續(xù)執(zhí)行。否則說明多個線程競爭鎖。若當前只有一個等待線程,則可通過自旋稍微等待一下,可能另一個線程很快就會釋放鎖。 但是當自旋超過一定的次數(shù),或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖就要膨脹為重量級鎖,重量級鎖使除了擁有鎖的線程以外的線程都阻塞,防止CPU空轉(zhuǎn),鎖標志的狀態(tài)值變?yōu)椤?0”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態(tài)。
輕量鎖解鎖過程
1、通過CAS操作嘗試把線程中復(fù)制的Displaced Mark Word對象替換當前的Mark Word。
2、如果替換成功,整個同步過程完成。
3、如果替換失敗,說明有其他線程嘗試過獲取該鎖(此時鎖已膨脹),那就在釋放鎖的同時,喚醒被掛起的線程。
可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false。
缺點:
在個別場景,輕量鎖的獲取及釋放產(chǎn)生的多次CAS原子指令是多余的。
偏向鎖
偏向鎖可以避免輕量鎖多次CAS操作產(chǎn)生的性能消耗,偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令。
如果一個線程獲得了鎖,那么鎖就進入偏向模式,此時Mark Word 的結(jié)構(gòu)也變?yōu)槠蜴i結(jié)構(gòu),當這個線程再次請求鎖時,無需再做任何同步操作就能獲取鎖。即獲取鎖的過程,省去了鎖申請操作產(chǎn)生的性能消耗,但是對于多條線程使用同一把鎖的場景,偏向鎖就失效了。但不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。
獲取過程
1、一個對象剛開始實例化,沒有任何線程來訪問它的時候,它是可偏向的。當?shù)谝粋€線程來訪問它的時候,它會偏向這個線程。這個線程會通過CAS將對象頭的Mark Word偏向鎖標識修改為“1”,鎖標志位修改為“01”,并將對象頭中的ThreadID改成自己的ID。之后再次訪問這個對象時,只需要對比ID,不需要再使用CAS在進行操作。
2、當?shù)诙€線程競爭該鎖時,會檢查對象頭中偏向鎖的標識是否為“1”,鎖標志位是否為“01”。如果是偏向鎖,就判斷線程ID是否指向當前線程。
-- 如果是則執(zhí)行同步代碼;
-- 如果不是則表明這個對象上已經(jīng)存在競爭了。就檢查持有偏向鎖的線程是否存活(因為可能持有偏向鎖的線程已經(jīng)執(zhí)行完畢,但是該線程并不會主動去釋放偏向鎖)。
---- 如果掛了,則可以將對象變?yōu)闊o鎖狀態(tài),然后重新偏向新的線程。
---- 如果原來的線程依然存活,則執(zhí)行那個線程的操作棧,檢查該對象的使用情況。
------ 如果仍然需要持有偏向鎖,則偏向鎖升級為輕量鎖(標志位為“00”)。此時輕量鎖由原持有偏向鎖的線程繼續(xù)持有,執(zhí)行其同步代碼。而正在競爭的線程會進入自旋等待獲得該輕量級鎖;
------ 如果不存在使用了,則可以將對象設(shè)置成無鎖狀態(tài),然后重新偏向(通過CAS競爭鎖,競爭成功則將Mark Word中線程ID設(shè)置為當前線程ID)。
偏向鎖釋放
偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處于被鎖定狀態(tài),撤銷偏向鎖后恢復(fù)到未鎖定(標志位為“01”)或輕量級鎖(標志位為“00”)的狀態(tài)。
偏向鎖、輕量鎖和重量鎖區(qū)別?
偏向所鎖,輕量級鎖都是樂觀鎖,重量級鎖是悲觀鎖。
輕量鎖適應(yīng)的場景是線程近乎交替執(zhí)行同步塊的情況,如果存在同一時間訪問同一鎖的情況,輕量鎖就會膨脹為重量級鎖。
1、一個對象剛開始實例化的時候,沒有任何線程來訪問它的時候。它是可偏向的,意味著,它現(xiàn)在認為只可能有一個線程來訪問它,所以當?shù)谝粋€線程來訪問它的時候,它會偏向這個線程,此時,對象持有偏向鎖。偏向第一個線程,這個線程在修改對象頭成為偏向鎖的時候使用CAS操作,并將對象頭中的ThreadID改成自己的ID,之后再次訪問這個對象時,只需要對比ID,不需要再使用CAS在進行操作。
2、一旦有第二個線程訪問這個對象,因為偏向鎖不會主動釋放,所以第二個線程可以看到對象時偏向狀態(tài),這時表明在這個對象上已經(jīng)存在競爭了。檢查原來持有該對象鎖的線程是否依然存活,如果掛了,則可以將對象變?yōu)闊o鎖狀態(tài),然后重新偏向新的線程。如果原來的線程依然存活,則馬上執(zhí)行那個線程的操作棧,檢查該對象的使用情況,如果仍然需要持有偏向鎖,則偏向鎖升級為輕量級鎖,(偏向鎖就是這個時候升級為輕量級鎖的),此時輕量級鎖由原持有偏向鎖的線程持有,繼續(xù)執(zhí)行其同步代碼,而正在競爭的線程會進入自旋等待獲得該輕量級鎖;如果不存在使用了,則可以將對象回復(fù)成無鎖狀態(tài),然后重新偏向。
3、輕量級鎖認為競爭存在,但是競爭的程度很輕,一般兩個線程對于同一個鎖的操作都會錯開,或者說稍微等待一下(自旋),另一個線程就會釋放鎖。 但是當自旋超過一定的次數(shù),或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹為重量級鎖,重量鎖會將擁有鎖線程以外的線程都阻塞,防止CPU空轉(zhuǎn)。
其他優(yōu)化
自旋鎖:
輕量鎖升級為重量鎖后,直接掛起線程會產(chǎn)生線程之間的切換(從用戶態(tài)轉(zhuǎn)換到核心態(tài)),性能消耗高。所以,JVM為了避免線程掛起,會讓線程自旋嘗試獲取鎖。當獲取鎖的線程執(zhí)行完釋放鎖后,當前線程便可以獲得執(zhí)行權(quán)。因為在大多數(shù)情況下,線程持有鎖的時間都不會太長。
缺點:自旋是需要消耗CPU資源的,長時間自旋會白白浪費CPU資源。
適應(yīng)性自旋鎖:
針對自旋鎖的缺點,JDK1.6 加入了適應(yīng)性自旋。
自適應(yīng)就意味著自旋的次數(shù)不再是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者狀態(tài)來決定。如果自旋成功了,那么下次自旋次數(shù)會更加多,因為JVM認為既然上次成功了,那么此次自旋也很有可能會成功,那么它就會允許自旋等待持續(xù)的次數(shù)更多。如果某個鎖自旋很少成功獲得,那么下一次就會減少自旋甚至省略掉自旋過程,以免浪費處理器資源。
通過--XX:+UseSpinning參數(shù)來開啟自旋(JDK1.6之前默認關(guān)閉自旋)。
通過--XX:PreBlockSpin修改自旋次數(shù),默認值是10次。
鎖消除
消除鎖是虛擬機另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java虛擬機在JIT編譯時(可以簡單理解為當某段代碼第一次被執(zhí)行時進行編譯,又稱即時編譯),通過對運行上下文的掃描,將不可能存在共享資源競爭的鎖,將這種鎖消除掉??梢怨?jié)省毫無意義的請求鎖時間。鎖消除的依據(jù)是逃逸分析的數(shù)據(jù)支持。比如:
public void apply(String str1){
StringBuffer sb = new StringBuffer();
for(int i = 0 ; i < 10 ; i++){
sb.append(str1);
}
System.out.println(sb);
}
因為StringBuffer是線程安全的,sb還只在方法內(nèi)使用,不會被其他線程引用
sb又是不可能共享的資源,所以JVM會自動消除加的鎖
鎖粗化:
在使用同步鎖的時候,需要讓同步塊的作用范圍盡可能小,僅在共享數(shù)據(jù)的作用域中才進行同步,這樣做目的是為了使需要同步的操作數(shù)盡量少,如果存在鎖競爭,那么等待鎖的線程也能盡快拿到鎖。
在大多數(shù)的情況下,上述觀點是正確的。但是如果一系列的連續(xù)加鎖解鎖操作,可能會導(dǎo)致不必要的性能損耗。
鎖粗話就是將多個連續(xù)的加鎖、解鎖操作連接在一起,擴展成一個范圍更大的鎖。JVM檢測到對同一個對象連續(xù)加鎖、解鎖操作,會合并一個更大范圍的加鎖、解鎖操作。即加鎖解鎖操作會移到for循環(huán)之外,類似mysql行鎖轉(zhuǎn)表鎖。
擴展
Synchronized 和 ReenTrantLock 的對比
1、兩者都是可重入鎖。
“可重入鎖”概念是:自己可以再次獲取自己的內(nèi)部鎖。比如一個線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要獲取這個對象的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。同一個線程每次獲取鎖,鎖的計數(shù)器都自增1,所以要等到鎖的計數(shù)器下降為0時才能釋放鎖。
2、synchronized依賴于JVM實現(xiàn),而ReenTrantLock依賴于API。
前面我們也講到了虛擬機團隊在JDK1.6為syn關(guān)鍵字進行了很多優(yōu)化,但是這些優(yōu)化都是在虛擬機層面實現(xiàn)的,并沒有直接暴露給我們。ReenTrantLock是JDK層面實現(xiàn)的(也就是API層面,需要lock()和unlock()方法配合try/finally語句塊來完成),所以我們可以通過查看它的源代碼,來看它是如何實現(xiàn)的。
3、ReenTrantLock比synchronized增加了一些高級功能
1、等待可中斷;
ReenTrantLock提供了一種能夠中斷等待鎖的線程的機制,通過lock.lockInterruptibly()來實現(xiàn)這個機制。也就是說正在等待的線程可以選擇放棄等待,改為處理其他事情。
2、可實現(xiàn)公平鎖;
ReenTrantLock可以指定是公平鎖還是非公平鎖。而syn只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖。ReenTrantLock默認情況是非公平的,可以通過ReenTrantLoc類的ReentrantLock(boolean fair)構(gòu)造方法來制定是否是公平的。
3、可實現(xiàn)選擇性通知(鎖可以綁定多個條件);
syn關(guān)鍵字與wait()和notify()/notifyAll()方法相結(jié)合可以實現(xiàn)等待/通知機制,ReentrantLock類當然也可以實現(xiàn),但是需要借助于Condition接口與newCondition()方法。Condition是JDK1.5之后才有的,它具有很好的靈活性,比如可以實現(xiàn)多路通知功能也就是在一個Lock對象中可以創(chuàng)建多個Condition實例(即對象監(jiān)視器),線程對象可以注冊在指定的Condition中,從而可以有選擇性的進行線程通知,在調(diào)度線程上更加靈活。 在使用notify/notifyAll()方法進行通知時,被通知的線程是由JVM選擇的,用ReentrantLock類結(jié)合Condition實例可以實現(xiàn)“選擇性通知” ,這個功能非常重要,而且是Condition接口默認提供的。而syn關(guān)鍵字就相當于整個Lock對象中只有一個Condition實例,所有的線程都注冊在它一個身上。如果執(zhí)行notifyAll()方法的話就會通知所有處于等待狀態(tài)的線程這樣會造成很大的效率問題,而Condition實例的signalAll()方法只會喚醒注冊在該Condition實例中的所有等待線程。
性能已不是選擇標準
在JDK1.6之前,syn的性能是比ReenTrantLock差很多。具體表示為:syn關(guān)鍵字吞吐量隨線程數(shù)的增加,下降得非常嚴重。而ReenTrantLock基本保持一個比較穩(wěn)定的水平。在JDK1.6之后JVM團隊對syn關(guān)鍵字做了很多優(yōu)化,性能基本能與ReenTrantLock持平。所以JDK1.6之后,性能已經(jīng)不是選擇syn和ReenTrantLock的影響因素,而且虛擬機在未來的性能改進中會更偏向于原生的syn,所以還是提倡在syn能滿足你的需求的情況下,優(yōu)先考慮使用syn關(guān)鍵字來進行同步!優(yōu)化后的syn和ReenTrantLock一樣,在很多地方都是用到了CAS操作。
CAS的原理是通過不斷的比較內(nèi)存中的值與舊值是否相同,如果相同則將內(nèi)存中的值修改為新值,相比于syn省去了掛起線程、恢復(fù)線程的開銷。
CAS的操作參數(shù):內(nèi)存位置(A)、預(yù)期原值(B)、預(yù)期新值(C)。
使用CAS解決并發(fā)的原理:
1. 首先比較A、B,若相等,則更新A中的值為C、返回True;若不相等,則返回false;
2. 通過死循環(huán),以不斷嘗試嘗試更新的方式實現(xiàn)并發(fā)
// 偽代碼如下
public boolean compareAndSwap(long memoryA, int oldB, int newC){
if(memoryA.get() == oldB){
memoryA.set(newC);
return true;
}
return false;
}
具體使用當中CAS有個先檢查后執(zhí)行的操作,而這種操作在Java中是典型的不安全的操作,所以CAS在實際中是由C++通過調(diào)用CPU指令實現(xiàn)的。
具體過程:
1、CAS在Java中的體現(xiàn)為Unsafe類。
2、Unsafe類會通過C++直接獲取到屬性的內(nèi)存地址。
3、接下來CAS由C++的Atomic::cmpxchg系列方法實現(xiàn)。
AtomicInteger的 i++ 與 i-- 是典型的CAS應(yīng)用,通過compareAndSet & 一個死循環(huán)實現(xiàn)。
private volatile int value;
/**
* Gets the current value.
* @return the current value
*/
public final int get() {
return value;
}
/**
* Atomically increments by one the current value.
* @return the previous value
*/
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
/**
* Atomically decrements by one the current value.
* @return the previous value
*/
public final int getAndDecrement() {
for (;;) {
int current = get();
int next = current - 1;
if (compareAndSet(current, next))
return current;
}
}
Synchronized 與 ThreadLocal 的對比
Synchronized關(guān)鍵字主要解決多線程共享數(shù)據(jù)同步問題;ThreadLocal主要解決多線程中數(shù)據(jù)因并發(fā)產(chǎn)生不一致問題。
Synchronized是利用鎖的機制,使變量或代碼塊只能被一個線程訪問。而ThreadLocal為每一個線程都提供變量的副本,使得每個線程訪問到的并不是同一個對象,這樣就隔離了多個線程對數(shù)據(jù)的數(shù)據(jù)共享。
線程中斷
中斷線程(實例方法)
public void interrupt();
判斷線程是否被中斷(實例方法)
public boolean isInterrupted();
判斷是否被中斷并清除當前中斷狀態(tài)(靜態(tài)方法)
public static boolean interrupted();
用法:
處于運行期且非阻塞的狀態(tài)的線程,直接調(diào)用interrupt()中斷線程方法是不會得到任何響應(yīng)的,如下代碼:
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(true)
System.out.println("中斷----");
});
t.start();
TimeUnit.SECONDS.sleep(2);
t.interrupt();
}
輸出結(jié)果:
中斷----
中斷----
中斷----
......
這時你肯定會覺得interrupt()方法不管用??
其實是因為interrupt()方法需要配合isInterrupted()方法一起使用;interrupt()方法只會將線程中斷狀態(tài)重置,處于非阻塞狀態(tài)的線程需要使用isInterrupted()方法進行中斷檢測:
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(){
@Override
public void run(){
while(true)
// 當前線程是否被中斷
if (this.isInterrupted()){
System.out.println("線程中斷狀態(tài):" + this.isInterrupted());
System.out.println("清除中斷狀態(tài)是否成功:" + Thread.interrupted());
System.out.println("線程中斷狀態(tài):" + this.isInterrupted());
break;
}
System.out.println("已跳出循環(huán),線程中斷!");
}
};
t.start();
TimeUnit.SECONDS.sleep(2);
t.interrupt();
}
輸出結(jié)果:
線程中斷狀態(tài):true
清除中斷狀態(tài)是否成功:true
線程中斷狀態(tài):false
已跳出循環(huán),線程中斷!
當一個線程處于被阻塞狀態(tài)或者試圖執(zhí)行一個阻塞操作時,使用interrupt()方法中斷該線程,此時將會拋出一個InterruptedException的異常,同時中斷狀態(tài)不會被改變(狀態(tài)仍是非中斷狀態(tài)):
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread() {
@Override
public void run() {
try {
while(true)
// 當前線程處于阻塞狀態(tài),異常捕捉處理
TimeUnit.SECONDS.sleep(100);
} catch (InterruptedException e) {
// 寫業(yè)務(wù)代碼
// 中斷狀態(tài)復(fù)位
System.out.println("線程是否被中斷:" + this.isInterrupted());
}
}
};
t.start();
TimeUnit.SECONDS.sleep(2);
// 中斷阻塞狀態(tài)的線程
t.interrupt();
}
輸出結(jié)果:
線程是否被中斷:false
小結(jié):
一種是當線程處于阻塞狀態(tài)或者試圖執(zhí)行一個阻塞操作時,我們可以使用interrupt()進行線程中斷,執(zhí)行中斷操作后將會拋出interruptException異常(該異常必須捕捉無法向外拋出)并將中斷狀態(tài)復(fù)位;
另外一種是當線程處于運行狀態(tài)時,用interrupt()進行線程中斷,但必須手動判斷中斷狀態(tài),并編寫中斷線程的代碼(結(jié)束run方法體的代碼)。
工作中我們肯定得兼顧兩種情況:
public static void main(String[] args) {
new Thread(() -> {
try {
// 判斷當前線程是否已中斷,interrupted方法執(zhí)行后會對中斷狀態(tài)進行復(fù)位
while (!Thread.interrupted()) {
// 業(yè)務(wù)代碼。。。
TimeUnit.SECONDS.sleep(2);
}
} catch (InterruptedException e) {
// 有人在中斷該線程
}
});
}
線程中斷以為這樣就完了么??
正在等待獲取鎖對象的syn方法或者代碼塊;調(diào)用interrupt()線程中斷方法是不起作用的,因為對于syn來說,如果一個線程在等待鎖,那么結(jié)果只有兩種,要么它獲得這把鎖繼續(xù)執(zhí)行,要么它就一直等待,這個時候調(diào)用中斷線程的方法,也不會生效。
public class Demo08 implements Runnable{
public synchronized void apply() {
System.out.println("apply方法執(zhí)行了");
while(true)
// 線程讓步
Thread.yield();
}
// 在構(gòu)造器中創(chuàng)建新線程并啟動獲取對象鎖
public Demo08() {
new Thread(() -> {
apply();
}).start();
}
public void run() {
try{
while (true)
// 中斷判斷
if (Thread.interrupted()) {
System.out.println("中斷線程。。。。。");
break;
} else {
apply();
}
} catch (Exception e) {
System.out.println(e);
}
}
public static void main(String[] args) throws InterruptedException {
Demo08 sync = new Demo08();
Thread t = new Thread(sync);
// 啟動后會調(diào)apply()方法,但是會搶不到鎖處于等待狀態(tài)
t.start();
TimeUnit.SECONDS.sleep(2);
//中斷線程,無法生效
t.interrupt();
}
}
輸出結(jié)果:
apply方法執(zhí)行了
上述代碼中,我們在構(gòu)造函數(shù)中創(chuàng)建一個線程調(diào)用apply()獲取到當前實例鎖,由于Demo08自身也是線程,啟動后在其run方法中也調(diào)用了apply(),但由于對象鎖被其他線程占用,導(dǎo)致t線程只能等待獲取鎖,這時我們調(diào)interrupt()方法是不能中斷線程的。
等待喚醒機制
notify/notifyAll和wait方法,在使用這3個方法時,必須處于syn代碼塊或者syn方法中,否則會拋出IllegalMonitorStateException異常,這是因為調(diào)用這幾個方法前必須拿到當前對象的監(jiān)視器monitor對象,也就是說notify/notifyAll和wait方法依賴于monitor對象,在前面的分析中,我們知道m(xù)onitor 存在于對象頭的Mark Word 中(存儲monitor引用指針),而syn關(guān)鍵字可以獲取 monitor ,這也就是為什么notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調(diào)用的原因。
synchronized (obj) {
obj.wait();
obj.notify();
obj.notifyAll();
}
sleep與wait方法的區(qū)別
sleep方法會讓線程休眠但不釋放鎖;
wait方法會讓線程暫停,釋放當前持有的監(jiān)視器鎖(monitor),直到有線程調(diào)用notify/notifyAll方法后方能繼續(xù)執(zhí)行,
注意:notify/notifyAll方法調(diào)用后,并不會馬上釋放監(jiān)視器鎖,而是在相應(yīng)的同步塊或同步方法執(zhí)行結(jié)束后才釋放鎖。
Thread.sleep(2000)和TimeUnit.SECONDS.sleep(2)區(qū)別?
前者使用時并沒有明確的單位說明,后者明確表達秒的單位,其實后者的內(nèi)部實現(xiàn)最終還是調(diào)用了Thread.sleep(2000);,但是建議使用TimeUnit.SECONDS.sleep(2)的方式。(TimeUnit是個枚舉類型)