一、synchronized簡介
在并發(fā)編程中多個線程同時操作同一個資源,極易導(dǎo)致錯誤數(shù)據(jù)的產(chǎn)生。因此為了解決這個問題,當(dāng)存在多個線程操作共享數(shù)據(jù)時,需要保證同一時刻有且只有一個線程在操作共享數(shù)據(jù),其他線程必須等到該線程處理完數(shù)據(jù)后再進(jìn)行。
在Java中,關(guān)鍵字synchronized可以保證在同一個時刻,只有一個線程可以執(zhí)行某個方法或者某個代碼塊(主要是對方法或者代碼塊中存在共享數(shù)據(jù)的操作),同時我們還應(yīng)該注意到synchronized另外一個重要的作用,synchronized可保證一個線程的變化(主要是共享數(shù)據(jù)的變化)被其他線程所看到(保證可見性,完全可以替代Volatile功能),這點確實也是很重要的。
二、synchronized應(yīng)用方式
synchronized主要有以下三種使用方式
作用于實例方法,當(dāng)前實例加鎖,進(jìn)入同步代碼前要獲得當(dāng)前實例的鎖;
作用于靜態(tài)方法,當(dāng)前類加鎖,進(jìn)去同步代碼前要獲得當(dāng)前類對象的鎖;
作用于代碼塊,這需要指定加鎖的對象,對所給的指定對象加鎖,進(jìn)入同步代碼前要獲得指定對象的鎖。
1、作用于實例方法
public class SynchronizedMethodTest implements Runnable {
private int i = 0;
private static int TOTAL = 1000;
public synchronized void add() {
i++;
}
@Override
public void run() {
for (int j = 0; j < TOTAL; j++) {
add();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedMethodTest s = new SynchronizedMethodTest();
Thread a = new Thread(s, "線程A");
Thread b = new Thread(s, "線程B");
a.start();
b.start();
a.join();
b.join();
System.out.printf("i=%s", s.i);
}
}
/**
* 輸出結(jié)果: i=2000
*/
2、作用于靜態(tài)方法
package com.dragon.thread.sync;
public class SynchronizedStaticMethodTest implements Runnable {
private static int i = 0;
private static int TOTAL = 1000;
public synchronized static void add() {
i++;
}
@Override
public void run() {
for (int j = 0; j < TOTAL; j++) {
add();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedStaticMethodTest s1 = new SynchronizedStaticMethodTest();
SynchronizedStaticMethodTest s2 = new SynchronizedStaticMethodTest();
Thread a = new Thread(s1, "線程A");
Thread b = new Thread(s2, "線程B");
a.start();
b.start();
a.join();
b.join();
System.out.printf("i=%s", i);
}
}
/**
* 輸出結(jié)果: i=2000
*/
3、作用于代碼塊
public class SynchronizedBlockTest implements Runnable {
private int i = 0;
private static int TOTAL = 1000;
public void add() {
synchronized (this) {
i++;
}
}
@Override
public void run() {
for (int j = 0; j < TOTAL; j++) {
add();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedBlockTest s = new SynchronizedBlockTest();
Thread a = new Thread(s, "線程A");
Thread b = new Thread(s, "線程B");
a.start();
b.start();
a.join();
b.join();
System.out.printf("i=%s", s.i);
}
}
/**
* 輸出結(jié)果: i=2000
*/
三、synchronized底層原理
Java 虛擬機(jī)中的同步Synchronization基于進(jìn)入和退出管程Monitor對象實現(xiàn), 無論是顯式同步(有明確的monitorenter和monitorexit指令,即同步代碼塊)還是隱式同步都是如此。在 Java 語言中,同步用的最多的地方可能是被synchronized修飾的同步方法。同步方法 并不是由monitorenter和monitorexit指令來實現(xiàn)同步的,而是由方法調(diào)用指令讀取運行時常量池中方法的ACC_SYNCHRONIZED標(biāo)志來隱式實現(xiàn)的,關(guān)于這點,稍后詳細(xì)分析。下面先來了解一個概念Java對象頭,這對深入理解synchronized實現(xiàn)原理非常關(guān)鍵。
1、理解Java對象頭與Monitor
在JVM中,對象在內(nèi)存中的布局分為三塊區(qū)域:對象頭、實例數(shù)據(jù)和對齊填充。

對象頭
HotSpot虛擬機(jī)的對象頭包括兩部分信息:
- markword
第一部分markword,用于存儲對象自身的運行時數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分?jǐn)?shù)據(jù)的長度在32位和64位的虛擬機(jī)(未開啟壓縮指針)中分別為32bit和64bit,官方稱它為MarkWord。 - klass
對象頭的另外一部分是klass類型指針,即對象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個指針來確定這個對象是哪個類的實例. - 數(shù)組長度(只有數(shù)組對象有)
如果對象是一個數(shù)組, 那在對象頭中還必須有一塊數(shù)據(jù)用于記錄數(shù)組長度.
實例數(shù)據(jù)
實例數(shù)據(jù)部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內(nèi)容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。
對齊填充
第三部分對齊填充并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用。由于HotSpot VM的自動內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,換句話說,就是對象的大小必須是8字節(jié)的整數(shù)倍。而對象頭部分正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此,當(dāng)對象實例數(shù)據(jù)部分沒有對齊時,就需要通過對齊填充來補(bǔ)全。
32位虛擬機(jī)在不同狀態(tài)下markword結(jié)構(gòu)如下圖所示

其中輕量級鎖和偏向鎖是Java 6 對synchronized鎖進(jìn)行優(yōu)化后新增加的,稍后我們會簡要分析。這里我們主要分析一下重量級鎖也就是通常說synchronized的對象鎖,鎖標(biāo)識位為10,其中指針指向的是monitor對象(也稱為管程或監(jiān)視器鎖)的起始地址。每個對象都存在著一個monitor與之關(guān)聯(lián),對象與其monitor之間的關(guān)系有存在多種實現(xiàn)方式,如monitor可以與對象一起創(chuàng)建銷毀或當(dāng)線程試圖獲取對象鎖時自動生成,但當(dāng)一個monitor被某個線程持有后,它便處于鎖定狀態(tài)。在Java虛擬機(jī)(HotSpot)中,monitor是由ObjectMonitor實現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于HotSpot虛擬機(jī)源碼ObjectMonitor.hpp文件,C++實現(xiàn)的)
ObjectMonitor() {
_header = NULL;//markOop對象頭
_count = 0;
_waiters = 0,//等待線程數(shù)
_recursions = 0;//重入次數(shù)
_object = NULL;//監(jiān)視器鎖寄生的對象。鎖不是平白出現(xiàn)的,而是寄托存儲于對象中。
_owner = NULL;//初始時為NULL表示當(dāng)前沒有任何線程擁有該monitor record,當(dāng)線程成功擁有該鎖后保存線程唯一標(biāo)識,當(dāng)鎖被釋放時又設(shè)置為NULL
_WaitSet = NULL;//處于wait狀態(tài)的線程,會被加入到wait set;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;//處于等待鎖block狀態(tài)的線程,會被加入到entry set;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;// _owner is (Thread *) vs SP/BasicLock
}
ObjectMonitor中有兩個隊列,_WaitSet和_EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當(dāng)多個線程同時訪問一段同步代碼時,首先會進(jìn)入_EntryList集合,當(dāng)線程獲取到對象的monitor后進(jìn)入_Owner區(qū)域并把monitor中的owner變量設(shè)置為當(dāng)前線程同時monitor中的計數(shù)器count加1,若線程調(diào)用wait()方法,將釋放當(dāng)前持有的monitor,owner變量恢復(fù)為null,count自減1,同時該線程進(jìn)入WaitSet集合中等待被喚醒。若當(dāng)前線程執(zhí)行完畢也將釋放monitor(鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取monitor(鎖)。如下圖所示

由此看來,
monitor對象存在于每個Java對象的對象頭中(存儲的指針的指向),synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因,同時也是notify/notifyAll/wait等方法存在于頂級對象Object中的原因(關(guān)于這點稍后還會進(jìn)行分析),ok~,有了上述知識基礎(chǔ)后,下面我們將進(jìn)一步分析synchronized在字節(jié)碼層面的具體語義實現(xiàn)。
2、同步方法的實現(xiàn)原理
使用javap -v SynchronizedMethodTest.class反編譯
/**
* 此處省略大段代碼
*/
public synchronized void add();
descriptor: ()V
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
/**
* 此處省略大段代碼
*/
}
SourceFile: "SynchronizedMethodTest.java"
3、同步代碼塊的實現(xiàn)原理
使用javap -v SynchronizedBlockTest.class反編譯
/**
* 此處省略大段代碼
*/
public void add();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter //申請獲得對象的內(nèi)置鎖
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 //釋放對象內(nèi)置鎖
16: goto 24
19: astore_2
20: aload_1
21: monitorexit //出現(xiàn)異常,釋放對象內(nèi)置鎖
22: aload_2
23: athrow
24: return
/**
* 此處省略大段代碼
*/
}
SourceFile: "SynchronizedBlockTest.java"
從上述指令我們可以得出以下結(jié)論:
- 同步代碼塊是使用
monitorenter和monitorexit指令實現(xiàn)的,會在同步塊的區(qū)域通過監(jiān)聽器對象去獲取鎖和釋放鎖,從而在字節(jié)碼層面來控制同步scope。 - 同步方法和靜態(tài)同步方法依靠的是方法修飾符上的
ACC_SYNCHRONIZED實現(xiàn)。JVM根據(jù)該修飾符來實現(xiàn)方法的同步。當(dāng)方法調(diào)用時,調(diào)用指令將會檢查方法的ACC_SYNCHRONIZED訪問標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先獲取monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor。在方法執(zhí)行期間,其他任何線程都無法再獲得同一個monitor對象。
結(jié)束