線程安全是并發(fā)編程中的重要關注點待讳,應該注意到的是,造成線程安全問題的主要誘因有兩點仰剿,一是存在共享數(shù)據(jù)(也稱臨界資源)创淡,二是存在多條線程共同操作共享數(shù)據(jù)。因此為了解決這個問題酥馍,我們可能需要這樣一個方案辩昆,當存在多個線程操作共享數(shù)據(jù)時阅酪,需要保證同一時刻有且只有一個線程在操作共享數(shù)據(jù)旨袒,其他線程必須等到該線程處理完數(shù)據(jù)后再進行,這種方式有個高尚的名稱叫互斥鎖术辐,即能達到互斥訪問目的的鎖砚尽,也就是說當一個共享數(shù)據(jù)被當前正在訪問的線程加上互斥鎖后,在同一個時刻辉词,其他線程只能處于等待的狀態(tài)必孤,直到當前線程處理完畢釋放該鎖。在 Java 中瑞躺,關鍵字 synchronized可以保證在同一個時刻敷搪,只有一個線程可以執(zhí)行某個方法或者某個代碼塊(主要是對方法或者代碼塊中存在共享數(shù)據(jù)的操作),同時我們還應該注意到synchronized另外一個重要的作用幢哨,synchronized可保證一個線程的變化(主要是共享數(shù)據(jù)的變化)被其他線程所看到(保證可見性赡勘,完全可以替代Volatile功能),這點確實也是很重要的捞镰。
一闸与、synchronized的三種應用方式
synchronized關鍵字最主要有以下3種應用方式,下面分別介紹
修飾實例方法岸售,作用于當前實例加鎖践樱,進入同步代碼前要獲得當前實例的鎖
修飾靜態(tài)方法,作用于當前類對象加鎖凸丸,進入同步代碼前要獲得當前類對象的鎖
修飾代碼塊拷邢,指定加鎖對象,對給定對象加鎖屎慢,進入同步代碼庫前要獲得給定對象的鎖解孙。
1.1 synchronized作用于實例方法
所謂的實例對象鎖就是用synchronized修飾實例對象中的實例方法坑填,注意是實例方法不包括靜態(tài)方法,如下
public class AccountingSync implements Runnable{
//共享資源(臨界資源)
static int i=0;
/**
* synchronized 修飾實例方法
*/
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
AccountingSync instance=new AccountingSync();
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
/**
* 輸出結果:
* 2000000
*/
}
上述代碼中弛姜,我們開啟兩個線程操作同一個共享資源即變量i脐瑰,由于i++;操作并不具備原子性,該操作是先讀取值廷臼,然后寫回一個新值苍在,相當于原來的值加上1,分兩步完成荠商,如果第二個線程在第一個線程讀取舊值和寫回新值期間讀取i的域值寂恬,那么第二個線程就會與第一個線程一起看到同一個值,并執(zhí)行相同值的加1操作莱没,這也就造成了線程安全失敗初肉,因此對于increase方法必須使用synchronized修飾,以便保證線程安全饰躲。此時我們應該注意到synchronized修飾的是實例方法increase牙咏,在這樣的情況下,當前線程的鎖便是實例對象instance嘹裂,注意Java中的線程同步鎖可以是任意對象妄壶。從代碼執(zhí)行結果來看確實是正確的,倘若我們沒有使用synchronized關鍵字寄狼,其最終輸出結果就很可能小于2000000丁寄,這便是synchronized關鍵字的作用。這里我們還需要意識到泊愧,當一個線程正在訪問一個對象的 synchronized 實例方法伊磺,那么其他線程不能訪問該對象的其他 synchronized 方法,畢竟一個對象只有一把鎖删咱,當一個線程獲取了該對象的鎖之后屑埋,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized實例方法腋腮,但是其他線程還是可以訪問該實例對象的其他非synchronized方法雀彼,當然如果是一個線程 A 需要訪問實例對象 obj1 的 synchronized 方法 f1(當前對象鎖是obj1),另一個線程 B 需要訪問實例對象 obj2 的 synchronized 方法 f2(當前對象鎖是obj2)即寡,這樣是允許的徊哑,因為兩個實例對象鎖并不同相同,此時如果兩個線程操作數(shù)據(jù)并非共享的聪富,線程安全是有保障的莺丑,遺憾的是如果兩個線程操作的是共享數(shù)據(jù),那么線程安全就有可能無法保證了,如下代碼將演示出該現(xiàn)象
public class AccountingSyncBad implements Runnable{
static int i=0;
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新實例
Thread t1=new Thread(new AccountingSyncBad());
//new新實例
Thread t2=new Thread(new AccountingSyncBad());
t1.start();
t2.start();
//join含義:當前線程A等待thread線程終止之后才能從thread.join()返回
t1.join();
t2.join();
System.out.println(i);
}
}
上述代碼與前面不同的是我們同時創(chuàng)建了兩個新實例AccountingSyncBad梢莽,然后啟動兩個不同的線程對共享變量i進行操作萧豆,但很遺憾操作結果是1452317而不是期望結果2000000,因為上述代碼犯了嚴重的錯誤昏名,雖然我們使用synchronized修飾了increase方法涮雷,但卻new了兩個不同的實例對象,這也就意味著存在著兩個不同的實例對象鎖轻局,因此t1和t2都會進入各自的對象鎖洪鸭,也就是說t1和t2線程使用的是不同的鎖,因此線程安全是無法保證的仑扑。解決這種困境的的方式是將synchronized作用于靜態(tài)的increase方法览爵,這樣的話,對象鎖就當前類對象镇饮,由于無論創(chuàng)建多少個實例對象蜓竹,但對于的類對象擁有只有一個,所有在這樣的情況下對象鎖就是唯一的储藐。下面我們看看如何使用將synchronized作用于靜態(tài)的increase方法俱济。
1.2 synchronized作用于靜態(tài)方法
當synchronized作用于靜態(tài)方法時,其鎖就是當前類的class對象鎖邑茄。由于靜態(tài)成員不專屬于任何一個實例對象姨蝴,是類成員俊啼,因此通過class對象鎖可以控制靜態(tài) 成員的并發(fā)操作肺缕。需要注意的是如果一個線程A調用一個實例對象的非static synchronized方法,而線程B需要調用這個實例對象所屬類的靜態(tài) synchronized方法授帕,是允許的同木,不會發(fā)生互斥現(xiàn)象,因為訪問靜態(tài) synchronized 方法占用的鎖是當前類的class對象跛十,而訪問非靜態(tài) synchronized 方法占用的鎖是當前實例對象鎖彤路,看如下代碼
public class AccountingSyncClass implements Runnable{
static int i=0;
/**
* 作用于靜態(tài)方法,鎖是當前class對象,也就是
* AccountingSyncClass類對應的class對象
*/
public static synchronized void increase(){
i++;
}
/**
* 非靜態(tài),訪問時鎖不一樣不會發(fā)生互斥
*/
public synchronized void increase4Obj(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新實例
Thread t1=new Thread(new AccountingSyncClass());
//new心事了
Thread t2=new Thread(new AccountingSyncClass());
//啟動線程
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
由于synchronized關鍵字修飾的是靜態(tài)increase方法,與修飾實例方法不同的是芥映,其鎖對象是當前類的class對象洲尊。注意代碼中的increase4Obj方法是實例方法,其對象鎖是當前實例對象奈偏,如果別的線程調用該方法坞嘀,將不會產生互斥現(xiàn)象,畢竟鎖對象不同惊来,但我們應該意識到這種情況下可能會發(fā)現(xiàn)線程安全問題(操作了共享靜態(tài)變量i)丽涩。
1.3 synchronized同步代碼塊
除了使用關鍵字修飾實例方法和靜態(tài)方法外,還可以使用同步代碼塊,在某些情況下矢渊,我們編寫的方法體可能比較大继准,同時存在一些比較耗時的操作,而需要同步的代碼又只有一小部分矮男,如果直接對整個方法進行同步操作移必,可能會得不償失,此時我們可以使用同步代碼塊的方式對需要同步的代碼進行包裹毡鉴,這樣就無需對整個方法進行同步操作了避凝,同步代碼塊的使用示例如下:
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
@Override
public void run() {
//省略其他耗時操作....
//使用同步代碼塊對變量i進行同步操作,鎖對象為instance
synchronized(instance){
for(int j=0;j<1000000;j++){
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
從代碼看出,將synchronized作用于一個給定的實例對象instance眨补,即當前實例對象就是鎖對象管削,每次當線程進入synchronized包裹的代碼塊時就會要求當前線程持有instance實例對象鎖,如果當前有其他線程正持有該對象鎖撑螺,那么新到的線程就必須等待含思,這樣也就保證了每次只有一個線程執(zhí)行i++;操作。當然除了instance作為對象外甘晤,我們還可以使用this對象(代表當前實例)或者當前類的class對象作為鎖含潘,如下代碼:
//this,當前實例對象鎖
synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}
//class對象鎖
synchronized(AccountingSync.class){
for(int j=0;j<1000000;j++){
i++;
}
}
了解完synchronized的基本含義及其使用方式后,下面我們將進一步深入理解synchronized的底層實現(xiàn)原理线婚。
二遏弱、synchronized底層語義原理
Java 虛擬機中的同步(Synchronization)基于進入和退出管程(Monitor)對象實現(xiàn), 無論是顯式同步(有明確的 monitorenter 和 monitorexit 指令,即同步代碼塊)還是隱式同步都是如此塞弊。在 Java 語言中漱逸,同步用的最多的地方可能是被 synchronized 修飾的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令來實現(xiàn)同步的游沿,而是由方法調用指令讀取運行時常量池中方法的 ACC_SYNCHRONIZED 標志來隱式實現(xiàn)的饰抒,關于這點,稍后詳細分析诀黍。下面先來了解一個概念Java對象頭袋坑,這對深入理解synchronized實現(xiàn)原理非常關鍵。
2.1 理解Java對象頭與Monitor
在JVM中眯勾,對象在內存中的布局分為三塊區(qū)域:對象頭枣宫、實例數(shù)據(jù)和對齊填充。如下:
實例變量:存放類的屬性數(shù)據(jù)信息吃环,包括父類的屬性信息也颤,如果是數(shù)組的實例部分還包括數(shù)組的長度,這部分內存按4字節(jié)對齊模叙。
填充數(shù)據(jù):由于虛擬機要求對象起始地址必須是8字節(jié)的整數(shù)倍歇拆。填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對齊,這點了解即可故觅。
而對于頂部有额,則是Java頭對象瞎饲,它實現(xiàn)synchronized的鎖對象的基礎迹鹅,這點我們重點分析它色洞,一般而言,synchronized使用的鎖對象是存儲在Java對象頭里的贯溅,jvm中采用2個字來存儲對象頭(如果對象是數(shù)組則會分配3個字拄氯,多出來的1個字記錄的是數(shù)組長度),其主要結構是由Mark Word 和 Class Metadata Address 組成它浅,其結構說明如下表:
虛擬機位數(shù) | 頭對象結構 | 說明 |
---|---|---|
32/64bit | Mark Word | 存儲對象的hashCode译柏、鎖信息或分代年齡或GC標志等信息 |
32/64bit | Class Metadata Address | 類型指針指向對象的類元數(shù)據(jù),JVM通過這個指針確定該對象是哪個類的實例姐霍。 |
其中Mark Word在默認情況下存儲著對象的HashCode鄙麦、分代年齡、鎖標記位等以下是32位JVM的Mark Word默認存儲結構
鎖狀態(tài) | 25bit | 4bit | 1bit是否是偏向鎖 | 2bit 鎖標志位 |
---|---|---|---|---|
無鎖狀態(tài) | 對象HashCode | 對象分代年齡 | 0 | 01 |
由于對象頭的信息是與對象自身定義的數(shù)據(jù)沒有關系的額外存儲成本镊折,因此考慮到JVM的空間效率胯府,Mark Word 被設計成為一個非固定的數(shù)據(jù)結構,以便存儲更多有效的數(shù)據(jù)恨胚,它會根據(jù)對象本身的狀態(tài)復用自己的存儲空間骂因,如32位JVM下,除了上述列出的Mark Word默認存儲結構外赃泡,還有如下可能變化的結構:
其中輕量級鎖和偏向鎖是Java 6 對 synchronized 鎖進行優(yōu)化后新增加的寒波,稍后我們會簡要分析。這里我們主要分析一下重量級鎖也就是通常說synchronized的對象鎖急迂,鎖標識位為10影所,其中指針指向的是monitor對象(也稱為管程或監(jiān)視器鎖)的起始地址蹦肴。每個對象都存在著一個 monitor 與之關聯(lián)僚碎,對象與其 monitor 之間的關系有存在多種實現(xiàn)方式,如monitor可以與對象一起創(chuàng)建銷毀或當線程試圖獲取對象鎖時自動生成阴幌,但當一個 monitor 被某個線程持有后勺阐,它便處于鎖定狀態(tài)。在Java虛擬機(HotSpot)中矛双,monitor是由ObjectMonitor實現(xiàn)的渊抽,其主要數(shù)據(jù)結構如下(位于HotSpot虛擬機源碼ObjectMonitor.hpp文件,C++實現(xiàn)的)
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 ;
}
ObjectMonitor中有兩個隊列,_WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象)愤估,_owner指向持有ObjectMonitor對象的線程帮辟,當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合玩焰,當線程獲取到對象的monitor 后進入 _Owner 區(qū)域并把monitor中的owner變量設置為當前線程同時monitor中的計數(shù)器count加1由驹,若線程調用 wait() 方法,將釋放當前持有的monitor昔园,owner變量恢復為null蔓榄,count自減1,同時該線程進入 WaitSe t集合中等待被喚醒默刚。若當前線程執(zhí)行完畢也將釋放monitor(鎖)并復位變量的值甥郑,以便其他線程進入獲取monitor(鎖)。如下圖所示
由此看來荤西,monitor對象存在于每個Java對象的對象頭中(存儲的指針的指向)壹若,synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因皂冰,同時也是notify/notifyAll/wait等方法存在于頂級對象Object中的原因(關于這點稍后還會進行分析)店展,ok~,有了上述知識基礎后秃流,下面我們將進一步分析synchronized在字節(jié)碼層面的具體語義實現(xiàn)赂蕴。
2.2 synchronized代碼塊底層原理
現(xiàn)在我們重新定義一個synchronized修飾的同步代碼塊,在代碼塊中操作共享變量i舶胀,如下
public class SyncCodeBlock {
public int i;
public void syncTask(){
//同步代碼庫
synchronized (this){
i++;
}
}
}
編譯上述代碼并使用javap反編譯后得到字節(jié)碼如下(這里我們省略一部分沒有必要的信息):
Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
Last modified 2017-6-2; size 426 bytes
MD5 checksum c80bc322c87b312de760942820b4fed5
Compiled from "SyncCodeBlock.java"
public class com.zejian.concurrencys.SyncCodeBlock
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
//........省略常量池中數(shù)據(jù)
//構造函數(shù)
public com.zejian.concurrencys.SyncCodeBlock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
//===========主要看看syncTask方法實現(xiàn)================
public void syncTask();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter //注意此處概说,進入同步方法
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit //注意此處,退出同步方法
16: goto 24
19: astore_2
20: aload_1
21: monitorexit //注意此處嚣伐,退出同步方法
22: aload_2
23: athrow
24: return
Exception table:
//省略其他字節(jié)碼.......
}
SourceFile: "SyncCodeBlock.java"
我們主要關注字節(jié)碼中的如下代碼
3: monitorenter //進入同步方法
//..........省略其他
15: monitorexit //退出同步方法
16: goto 24
//省略其他.......
21: monitorexit //退出同步方法
從字節(jié)碼中可知同步語句塊的實現(xiàn)使用的是monitorenter 和 monitorexit 指令糖赔,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置轩端,當執(zhí)行monitorenter指令時放典,當前線程將試圖獲取 objectref(即對象鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數(shù)器為 0基茵,那線程可以成功取得 monitor奋构,并將計數(shù)器值設置為 1,取鎖成功拱层。如果當前線程已經擁有 objectref 的 monitor 的持有權弥臼,那它可以重入這個 monitor (關于重入性稍后會分析),重入時計數(shù)器的值也會加 1根灯。倘若其他線程已經擁有 objectref 的 monitor 的所有權径缅,那當前線程將被阻塞掺栅,直到正在執(zhí)行線程執(zhí)行完畢,即monitorexit指令被執(zhí)行纳猪,執(zhí)行線程將釋放 monitor(鎖)并設置計數(shù)器值為0 柿冲,其他線程將有機會持有 monitor 。值得注意的是編譯器將會確保無論方法通過何種方式完成兆旬,方法中調用過的每條 monitorenter 指令都有執(zhí)行其對應 monitorexit 指令假抄,而無論這個方法是正常結束還是異常結束。為了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執(zhí)行丽猬,編譯器會自動產生一個異常處理器宿饱,這個異常處理器聲明可處理所有的異常,它的目的就是用來執(zhí)行 monitorexit 指令脚祟。從字節(jié)碼中也可以看出多了一個monitorexit指令谬以,它就是異常結束時被執(zhí)行的釋放monitor 的指令。
2.3 synchronized方法底層原理
方法級的同步是隱式由桌,即無需通過字節(jié)碼指令來控制的为黎,它實現(xiàn)在方法調用和返回操作之中。JVM可以從方法常量池中的方法表結構(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標志區(qū)分一個方法是否同步方法行您。當方法調用時铭乾,調用指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設置,如果設置了娃循,執(zhí)行線程將先持有monitor(虛擬機規(guī)范中用的是管程一詞)炕檩, 然后再執(zhí)行方法,最后再方法完成(無論是正常完成還是非正常完成)時釋放monitor捌斧。在方法執(zhí)行期間笛质,執(zhí)行線程持有了monitor,其他任何線程都無法再獲得同一個monitor捞蚂。如果一個同步方法執(zhí)行期間拋 出了異常妇押,并且在方法內部無法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放姓迅。下面我們看看字節(jié)碼層面如何實現(xiàn):
public class SyncMethod {
public int i;
public synchronized void syncTask(){
i++;
}
}
使用javap反編譯后的字節(jié)碼如下:
Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.class
Last modified 2017-6-2; size 308 bytes
MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
Compiled from "SyncMethod.java"
public class com.zejian.concurrencys.SyncMethod
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool;
//省略沒必要的字節(jié)碼
//==================syncTask方法======================
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 12: 0
line 13: 10
}
SourceFile: "SyncMethod.java"
從字節(jié)碼中可以看出,synchronized修飾的方法并沒有monitorenter指令和monitorexit指令队贱,取得代之的確實是ACC_SYNCHRONIZED標識色冀,該標識指明了該方法是一個同步方法,JVM通過該ACC_SYNCHRONIZED訪問標志來辨別一個方法是否聲明為同步方法柱嫌,從而執(zhí)行相應的同步調用。這便是synchronized鎖在同步代碼塊和同步方法上實現(xiàn)的基本原理屯换。同時我們還必須注意到的是在Java早期版本中编丘,synchronized屬于重量級鎖与学,效率低下,因為監(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的Mutex Lock來實現(xiàn)的嘉抓,而操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉換到核心態(tài)索守,這個狀態(tài)之間的轉換需要相對比較長的時間,時間成本相對較高抑片,這也是為什么早期的synchronized效率低的原因卵佛。慶幸的是在Java 6之后Java官方對從JVM層面對synchronized較大優(yōu)化,所以現(xiàn)在的synchronized鎖效率也優(yōu)化得很不錯了敞斋,Java 6之后截汪,為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了輕量級鎖和偏向鎖植捎,接下來我們將簡單了解一下Java官方在JVM層面對synchronized鎖的優(yōu)化衙解。
三、Java虛擬機對synchronized的優(yōu)化
鎖的狀態(tài)總共有四種焰枢,無鎖狀態(tài)蚓峦、偏向鎖、輕量級鎖和重量級鎖济锄。隨著鎖的競爭暑椰,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖荐绝,但是鎖的升級是單向的干茉,也就是說只能從低到高升級,不會出現(xiàn)鎖的降級很泊,關于重量級鎖角虫,前面我們已詳細分析過,下面我們將介紹偏向鎖和輕量級鎖以及JVM的其他優(yōu)化手段委造,這里并不打算深入到每個鎖的實現(xiàn)和轉換過程更多地是闡述Java虛擬機所提供的每個鎖的核心優(yōu)化思想戳鹅,畢竟涉及到具體過程比較繁瑣,如需了解詳細過程可以查閱《深入理解Java虛擬機原理》昏兆。
3.1 偏向鎖
偏向鎖是Java 6之后加入的新鎖枫虏,它是一種針對加鎖操作的優(yōu)化手段,經過研究發(fā)現(xiàn)爬虱,在大多數(shù)情況下隶债,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得跑筝,因此為了減少同一線程獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖死讹。偏向鎖的核心思想是,如果一個線程獲得了鎖曲梗,那么鎖就進入偏向模式赞警,此時Mark Word 的結構也變?yōu)槠蜴i結構妓忍,當這個線程再次請求鎖時,無需再做任何同步操作愧旦,即獲取鎖的過程世剖,這樣就省去了大量有關鎖申請的操作,從而也就提供程序的性能笤虫。所以旁瘫,對于沒有鎖競爭的場合,偏向鎖有很好的優(yōu)化效果琼蚯,畢竟極有可能連續(xù)多次是同一個線程申請相同的鎖酬凳。但是對于鎖競爭比較激烈的場合,偏向鎖就失效了凌停,因為這樣場合極有可能每次申請鎖的線程都是不相同的粱年,因此這種場合下不應該使用偏向鎖,否則會得不償失罚拟,需要注意的是台诗,偏向鎖失敗后,并不會立即膨脹為重量級鎖赐俗,而是先升級為輕量級鎖拉队。下面我們接著了解輕量級鎖。
3.2 輕量級鎖
倘若偏向鎖失敗阻逮,虛擬機并不會立即升級為重量級鎖粱快,它還會嘗試使用一種稱為輕量級鎖的優(yōu)化手段(1.6之后加入的),此時Mark Word 的結構也變?yōu)檩p量級鎖的結構叔扼。輕量級鎖能夠提升程序性能的依據(jù)是“對絕大部分的鎖事哭,在整個同步周期內都不存在競爭”,注意這是經驗數(shù)據(jù)瓜富。需要了解的是鳍咱,輕量級鎖所適應的場景是線程交替執(zhí)行同步塊的場合,如果存在同一時間訪問同一鎖的場合与柑,就會導致輕量級鎖膨脹為重量級鎖谤辜。
3.3 自旋鎖
輕量級鎖失敗后,虛擬機為了避免線程真實地在操作系統(tǒng)層面掛起价捧,還會進行一項稱為自旋鎖的優(yōu)化手段丑念。這是基于在大多數(shù)情況下,線程持有鎖的時間都不會太長结蟋,如果直接掛起操作系統(tǒng)層面的線程可能會得不償失脯倚,畢竟操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉換到核心態(tài),這個狀態(tài)之間的轉換需要相對比較長的時間椎眯,時間成本相對較高挠将,因此自旋鎖會假設在不久將來胳岂,當前的線程可以獲得鎖编整,因此虛擬機會讓當前想要獲取鎖的線程做幾個空循環(huán)(這也是稱為自旋的原因)舔稀,一般不會太久,可能是50個循環(huán)或100循環(huán)掌测,在經過若干次循環(huán)后内贮,如果得到鎖,就順利進入臨界區(qū)汞斧。如果還不能獲得鎖夜郁,那就會將線程在操作系統(tǒng)層面掛起,這就是自旋鎖的優(yōu)化方式粘勒,這種方式確實也是可以提升效率的竞端。最后沒辦法也就只能升級為重量級鎖了。
3.4 鎖消除
消除鎖是虛擬機另外一種鎖的優(yōu)化庙睡,這種優(yōu)化更徹底事富,Java虛擬機在JIT編譯時(可以簡單理解為當某段代碼即將第一次被執(zhí)行時進行編譯,又稱即時編譯)乘陪,通過對運行上下文的掃描统台,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖啡邑,可以節(jié)省毫無意義的請求鎖時間贱勃,如下StringBuffer的append是一個同步方法,但是在add方法中的StringBuffer屬于一個局部變量谤逼,并且不會被其他線程所使用贵扰,因此StringBuffer不可能存在共享資源競爭的情景,JVM會自動將其鎖消除流部。
/**
* 消除StringBuffer同步鎖
*/
public class StringBufferRemoveSync {
public void add(String str1, String str2) {
//StringBuffer是線程安全,由于sb只會在append方法中使用,不可能被其他線程引用
//因此sb屬于不可能共享的資源,JVM會自動消除內部的鎖
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
public static void main(String[] args) {
StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
for (int i = 0; i < 10000000; i++) {
rmsync.add("abc", "123");
}
}
}
四戚绕、關于synchronized 可能需要了解的關鍵點
4.1 synchronized的可重入性
從互斥鎖的設計上來說,當一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時贵涵,將會處于阻塞狀態(tài)列肢,但當一個線程再次請求自己持有對象鎖的臨界資源時,這種情況屬于重入鎖宾茂,請求將會成功瓷马,在java中synchronized是基于原子性的內部鎖機制,是可重入的跨晴,因此在一個線程調用synchronized方法的同時在其方法體內部調用該對象另一個synchronized方法欧聘,也就是說一個線程得到一個對象鎖后再次請求該對象鎖,是允許的端盆,這就是synchronized的可重入性怀骤。如下:
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
static int j=0;
@Override
public void run() {
for(int j=0;j<1000000;j++){
//this,當前實例對象鎖
synchronized(this){
i++;
increase();//synchronized的可重入性
}
}
}
public synchronized void increase(){
j++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
正如代碼所演示的费封,在獲取當前實例對象鎖后進入synchronized代碼塊執(zhí)行同步代碼,并在代碼塊中調用了當前實例對象的另外一個synchronized方法蒋伦,再次請求當前實例鎖時弓摘,將被允許,進而執(zhí)行方法體代碼痕届,這就是重入鎖最直接的體現(xiàn)韧献,需要特別注意另外一種情況,當子類繼承父類時研叫,子類也是可以通過可重入鎖調用父類的同步方法锤窑。注意由于synchronized是基于monitor實現(xiàn)的,因此每次重入嚷炉,monitor中的計數(shù)器仍會加1渊啰。
4.2 線程中斷與synchronized
4.2.1 線程中斷
正如中斷二字所表達的意義,在線程運行(run方法)中間打斷它申屹,在Java中绘证,提供了以下3個有關線程中斷的方法
//中斷線程(實例方法)
public void Thread.interrupt();
//判斷線程是否被中斷(實例方法)
public boolean Thread.isInterrupted();
//判斷是否被中斷并清除當前中斷狀態(tài)(靜態(tài)方法)
public static boolean Thread.interrupted();
當一個線程處于被阻塞狀態(tài)或者試圖執(zhí)行一個阻塞操作時,使用Thread.interrupt()方式中斷該線程独柑,注意此時將會拋出一個InterruptedException的異常迈窟,同時中斷狀態(tài)將會被復位(由中斷狀態(tài)改為非中斷狀態(tài)),如下代碼將演示該過程:
public class InterruputSleepThread3 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
//while在try中忌栅,通過異常中斷就可以退出run循環(huán)
try {
while (true) {
//當前線程處于阻塞狀態(tài)车酣,異常必須捕捉處理,無法往外拋出
TimeUnit.SECONDS.sleep(2);
}
} catch (InterruptedException e) {
System.out.println("Interruted When Sleep");
boolean interrupt = this.isInterrupted();
//中斷狀態(tài)被復位
System.out.println("interrupt:"+interrupt);
}
}
};
t1.start();
TimeUnit.SECONDS.sleep(2);
//中斷處于阻塞狀態(tài)的線程
t1.interrupt();
/**
* 輸出結果:
Interruted When Sleep
interrupt:false
*/
}
}
如上述代碼所示索绪,我們創(chuàng)建一個線程湖员,并在線程中調用了sleep方法從而使用線程進入阻塞狀態(tài),啟動線程后瑞驱,調用線程實例對象的interrupt方法中斷阻塞異常娘摔,并拋出InterruptedException異常,此時中斷狀態(tài)也將被復位唤反。這里有些人可能會詫異凳寺,為什么不用Thread.sleep(2000);而是用TimeUnit.SECONDS.sleep(2);其實原因很簡單,前者使用時并沒有明確的單位說明彤侍,而后者非常明確表達秒的單位肠缨,事實上后者的內部實現(xiàn)最終還是調用了Thread.sleep(2000);,但為了編寫的代碼語義更清晰盏阶,建議使用TimeUnit.SECONDS.sleep(2);的方式晒奕,注意TimeUnit是個枚舉類型。ok~,除了阻塞中斷的情景脑慧,我們還可能會遇到處于運行期且非阻塞的狀態(tài)的線程魄眉,這種情況下,直接調用Thread.interrupt()中斷線程是不會得到任響應的闷袒,如下代碼坑律,將無法中斷非阻塞狀態(tài)下的線程:
public class InterruputThread {
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(){
@Override
public void run(){
while(true){
System.out.println("未被中斷");
}
}
};
t1.start();
TimeUnit.SECONDS.sleep(2);
t1.interrupt();
/**
* 輸出結果(無限執(zhí)行):
未被中斷
未被中斷
未被中斷
......
*/
}
}
雖然我們調用了interrupt方法,但線程t1并未被中斷霜运,因為處于非阻塞狀態(tài)的線程需要我們手動進行中斷檢測并結束程序脾歇,改進后代碼如下:
public class InterruputThread {
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(){
@Override
public void run(){
while(true){
//判斷當前線程是否被中斷
if (this.isInterrupted()){
System.out.println("線程中斷");
break;
}
}
System.out.println("已跳出循環(huán),線程中斷!");
}
};
t1.start();
TimeUnit.SECONDS.sleep(2);
t1.interrupt();
/**
* 輸出結果:
線程中斷
已跳出循環(huán),線程中斷!
*/
}
}
是的蒋腮,我們在代碼中使用了實例方法isInterrupted判斷線程是否已被中斷淘捡,如果被中斷將跳出循環(huán)以此結束線程,注意非阻塞狀態(tài)調用interrupt()并不會導致中斷狀態(tài)重置。綜合所述池摧,可以簡單總結一下中斷兩種情況焦除,一種是當線程處于阻塞狀態(tài)或者試圖執(zhí)行一個阻塞操作時,我們可以使用實例方法interrupt()進行線程中斷作彤,執(zhí)行中斷操作后將會拋出interruptException異常(該異常必須捕捉無法向外拋出)并將中斷狀態(tài)復位膘魄,另外一種是當線程處于運行狀態(tài)時,我們也可調用實例方法interrupt()進行線程中斷竭讳,但同時必須手動判斷中斷狀態(tài)创葡,并編寫中斷線程的代碼(其實就是結束run方法體的代碼)。有時我們在編碼時可能需要兼顧以上兩種情況绢慢,那么就可以如下編寫:
public void run(){
try {
//判斷當前線程是否已中斷,注意interrupted方法是靜態(tài)的,執(zhí)行后會對中斷狀態(tài)進行復位
while (!Thread.interrupted()) {
TimeUnit.SECONDS.sleep(2);
}
} catch (InterruptedException e) {
}
}
4.2.2 中斷與synchronized
事實上線程的中斷操作對于正在等待獲取的鎖對象的synchronized方法或者代碼塊并不起作用灿渴,也就是對于synchronized來說,如果一個線程在等待鎖胰舆,那么結果只有兩種骚露,要么它獲得這把鎖繼續(xù)執(zhí)行,要么它就保存等待缚窿,即使調用中斷線程的方法棘幸,也不會生效。演示代碼如下
public class SynchronizedBlocked implements Runnable{
public synchronized void f() {
System.out.println("Trying to call f()");
while(true) // Never releases lock
Thread.yield();
}
/**
* 在構造器中創(chuàng)建新線程并啟動獲取對象鎖
*/
public SynchronizedBlocked() {
//該線程已持有當前實例鎖
new Thread() {
public void run() {
f(); // Lock acquired by this thread
}
}.start();
}
public void run() {
//中斷判斷
while (true) {
if (Thread.interrupted()) {
System.out.println("中斷線程!!");
break;
} else {
f();
}
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedBlocked sync = new SynchronizedBlocked();
Thread t = new Thread(sync);
//啟動后調用f()方法,無法獲取當前實例鎖處于等待狀態(tài)
t.start();
TimeUnit.SECONDS.sleep(1);
//中斷線程,無法生效
t.interrupt();
}
}
我們在SynchronizedBlocked構造函數(shù)中創(chuàng)建一個新線程并啟動獲取調用f()獲取到當前實例鎖倦零,由于SynchronizedBlocked自身也是線程误续,啟動后在其run方法中也調用了f(),但由于對象鎖被其他線程占用扫茅,導致t線程只能等到鎖蹋嵌,此時我們調用了t.interrupt();但并不能中斷線程。
4.3 等待喚醒機制與synchronized
所謂等待喚醒機制本篇主要指的是notify/notifyAll和wait方法诞帐,在使用這3個方法時欣尼,必須處于synchronized代碼塊或者synchronized方法中,否則就會拋出IllegalMonitorStateException異常,這是因為調用這幾個方法前必須拿到當前對象的監(jiān)視器monitor對象愕鼓,也就是說notify/notifyAll和wait方法依賴于monitor對象钙态,在前面的分析中,我們知道m(xù)onitor 存在于對象頭的Mark Word 中(存儲monitor引用指針)菇晃,而synchronized關鍵字可以獲取 monitor 册倒,這也就是為什么notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調用的原因。
synchronized (obj) {
obj.wait();
obj.notify();
obj.notifyAll();
}
需要特別理解的一點是磺送,與sleep方法不同的是wait方法調用完成后驻子,線程將被暫停,但wait方法將會釋放當前持有的監(jiān)視器鎖(monitor)估灿,直到有線程調用notify/notifyAll方法后方能繼續(xù)執(zhí)行崇呵,而sleep方法只讓線程休眠并不釋放鎖。同時notify/notifyAll方法調用后馅袁,并不會馬上釋放監(jiān)視器鎖域慷,而是在相應的synchronized(){}/synchronized方法執(zhí)行結束后才自動釋放鎖。