前言
線程并發(fā)系列文章:
Java 線程基礎(chǔ)
Java 線程狀態(tài)
Java “優(yōu)雅”地中斷線程-實(shí)踐篇
Java “優(yōu)雅”地中斷線程-原理篇
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有誤
Java Unsafe/CAS/LockSupport 應(yīng)用與原理
Java 并發(fā)"鎖"的本質(zhì)(一步步實(shí)現(xiàn)鎖)
Java Synchronized實(shí)現(xiàn)互斥之應(yīng)用與源碼初探
Java 對象頭分析與使用(Synchronized相關(guān))
Java Synchronized 偏向鎖/輕量級鎖/重量級鎖的演變過程
Java Synchronized 重量級鎖原理深入剖析上(互斥篇)
Java Synchronized 重量級鎖原理深入剖析下(同步篇)
Java并發(fā)之 AQS 深入解析(上)
Java并發(fā)之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 詳解
Java 并發(fā)之 ReentrantLock 深入分析(與Synchronized區(qū)別)
Java 并發(fā)之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(應(yīng)用篇)
最詳細(xì)的圖文解析Java各種鎖(終極篇)
線程池必懂系列
上篇文章從無到有分析了如何實(shí)現(xiàn)"鎖"贪壳,雖然僅僅實(shí)現(xiàn)了最簡單的鎖摩瞎,但"鎖"的精華已經(jīng)提取出來了欺栗,有了這些知識新娜,本篇將分析系統(tǒng)提供的鎖-synchronized關(guān)鍵字的使用與實(shí)現(xiàn)成艘。
通過本篇文章安吁,你將了解到:
1未辆、synchronized 如何使用
2、synchronized 源碼初探
3汇四、總結(jié)
1泞莉、synchronized 如何使用
多線程訪問臨界區(qū)
由上篇文章可知,多線程訪問臨界區(qū)需要鎖:
臨界區(qū)可以是一段代碼船殉,也可以是某個(gè)方法鲫趁。
synchronized 各種使用方式
按鎖作用區(qū)域劃分,可分為兩類:
修飾方法
修飾方法又分為兩類:實(shí)例方法與靜態(tài)方法利虫。先來看看實(shí)例方法:
實(shí)例方法
public class TestSynchronized {
//共享變量
private int a = 0;
public static void main(String args[]) {
final TestSynchronized testSynchronized = new TestSynchronized();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
int count = 0;
while (count < 10000) {
testSynchronized.func1();
count++;
}
System.out.println("a = " + testSynchronized.getA() + " in thread1");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
int count = 0;
while (count < 10000) {
testSynchronized.func1();
count++;
}
System.out.println("a = " + testSynchronized.getA() + " in thread2");
}
});
t2.start();
try {
t1.join();
t2.join();
//等待t1,t2執(zhí)行完畢挨厚,再打印結(jié)果
System.out.println("a = " + testSynchronized.getA() + " in mainThread");
} catch (Exception e) {
}
}
private synchronized void func1() {
//修改a
a++;
}
private int getA() {
return a;
}
}
以上兩個(gè)線程t1、t2都需要修改共享變量a的值糠惫,同時(shí)調(diào)用TestSynchronized 的對象方法: func1()進(jìn)行自增疫剃。每個(gè)線程調(diào)用func1() 10000次,循環(huán)結(jié)束后線程停止運(yùn)行硼讽。理論上每個(gè)線程都對a的值增加了10000次巢价,也就是說最后a的值應(yīng)為為:a==20000,來看看在主線程里打印a的最終值:
可以看出,多線程訪問的結(jié)果正確壤躲,說明synchronized修飾的實(shí)例方法能夠正確實(shí)現(xiàn)了多線程并發(fā)城菊。
靜態(tài)方法
再來看看靜態(tài)方法:
public class TestSynchronized {
//共享變量
private static int a = 0;
public static void main(String args[]) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
int count = 0;
while (count < 10000) {
func1();
count++;
}
System.out.println("a = " + getA() + " in thread1");
}
});
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
int count = 0;
while (count < 10000) {
func1();
count++;
}
System.out.println("a = " + getA() + " in thread2");
}
});
t2.start();
try {
t1.join();
t2.join();
//等待t1,t2執(zhí)行完畢,再打印結(jié)果
System.out.println("a = " + getA() + " in mainThread");
} catch (Exception e) {
}
}
private static synchronized void func1() {
//修改a
a++;
}
private static int getA() {
return a;
}
}
相對于修飾實(shí)例方法碉克,只是更改了a為static類型凌唬,并且將func1()變?yōu)殪o態(tài)方法,最終的結(jié)果與前面實(shí)例方法是一致的漏麦。
說明synchronized修飾的靜態(tài)方法能夠正確實(shí)現(xiàn)了多線程并發(fā)客税。
修飾代碼塊
synchronized 修飾方法時(shí)(靜態(tài)方法/實(shí)例方法),在進(jìn)入方法前先申請鎖撕贞,退出方法后釋放鎖更耻。假若有個(gè)方法里執(zhí)行的操作比較多,而需要并發(fā)訪問的就只有一小段捏膨,如果為了這小段臨界區(qū)將方法用synchronized修飾酥夭,那么將是大材小用。為此synchronized提供了修飾一段代碼塊的方法脊奋。
按鎖類型劃分,修飾代碼塊也分為兩類:
獲取對象鎖
//聲明鎖對象
private static Object object = new Object();
private void func1() {
//無需互斥訪問的區(qū)域
int b = 1000;
int c = 0;
if (c < b) {
c++;
}
//修改a
//需要互斥訪問的區(qū)域
synchronized (object) {
a++;
}
}
可以看出雖然func1方法里有其它操作疙描,但是對于多線程操作不敏感诚隙,只有共享變量a需要互斥訪問,因此僅僅需要對操作a使用synchronized修飾起胰。
synchronized (object) 表示獲取實(shí)例對象:object的鎖久又。
獲取類鎖
再來看看如何使用類鎖:
private void func1() {
//無需互斥訪問的區(qū)域
int b = 1000;
int c = 0;
if (c < b) {
c++;
}
//修改a
//需要互斥訪問的區(qū)域
synchronized (TestSynchronized.class) {
a++;
}
}
這次沒有實(shí)例化對象了,而是直接使用TestSynchronized.class效五,表示獲取TestSynchronized 類鎖地消。
小結(jié)
將上述關(guān)系用圖表示:
1、無論是修飾方法還是代碼塊畏妖,最終都是獲取對象鎖(類鎖是Class對象的鎖)
2脉执、實(shí)例方法與對象鎖獲取的是同一把鎖(普通對象鎖)
3、靜態(tài)方法與類鎖獲取的是同一把鎖(類鎖-Class對象鎖)
對象鎖
private void func1() {
synchronized (this) { }
}
private synchronized void func2() {
}
private void func3() {
}
func1()與func2()都需要獲取對象鎖(this指的是本對象戒劫,也就是調(diào)用方法的對象本身)半夷,因此兩者的訪問是互斥的,而訪問func3()則不受影響迅细。
類鎖
private void func1() {
synchronized (TestSynchronized.class) { }
}
private static synchronized void func2() {
}
private static synchronized void func3() {
}
private static void func4() {
}
func1()巫橄、func2()、func3()都需要獲取類鎖茵典,此處的類鎖為TestSynchronized.class 對象湘换,因此三者的訪問是互斥的,而訪問func4()則不受影響。
由此可知:
1彩倚、類鎖與對象鎖互不影響
2筹我、多線程需要獲取"同一把鎖"才能實(shí)現(xiàn)互斥
2、synchronized 源碼初探
上面的例子離不開synchronized 修飾符署恍,這是個(gè)關(guān)鍵字崎溃,JVM是如何識別這個(gè)關(guān)鍵字的呢?首先來看看synchronized編譯后的結(jié)果:
修飾代碼塊
先來看Demo:
public class TestSynchronized {
//共享變量
int a = 0;
Object object = new Object();
public static void main(String args[]) {
}
private void add() {
synchronized (object) {
a++;
}
}
}
以上是使用對象鎖修飾了代碼塊《⒅剩現(xiàn)在將它編譯為.class文件袁串,定位到TestSynchronized.java 文件目錄,打開命令行呼巷,輸入如下命令:
javac TestSynchronized.java
與TestSynchronized.java文件同目錄下將生成TestSynchronized.class囱修。
.class 文件肉眼看不出所以然,因此將它反編譯看看王悍,依然在同級目錄下使用如下命令:
javap -verbose -p TestSynchronized.class
然后命令行輸出一串結(jié)果破镰,當(dāng)然如果你覺得不方便查看,可以將輸出結(jié)果放在文件里压储,使用如下命令:
javap -verbose -p TestSynchronized.class > mytest.txt
來看看輸出的重點(diǎn)內(nèi)容:
上圖重點(diǎn)圈出了兩個(gè)指令:monitorenter與monitorexit鲜漩。
- monitorenter 表示獲取鎖
- monitorexit 表示釋放鎖
- 兩者之間的操作就是被鎖住的臨界區(qū)
其中monitorexit 有兩個(gè),后面一個(gè)是發(fā)生異常時(shí)會執(zhí)行
monitorenter/monitorexit 指令對應(yīng)代碼
monitorenter/monitorexit 指令對應(yīng)的代碼在哪呢集惋?
網(wǎng)上有不同的解釋孕似,我傾向于:https://github.com/farmerjohngit/myblog/issues/13 中所作的分析:
- 在Hotspot中只用到了模板解釋器(templateTable_x86_64.cpp)
,字節(jié)碼解釋器(bytecodeInterpreter.cpp)根本就沒用到- 模板解釋器里都是匯編代碼刮刑,字節(jié)碼解釋器用的是C++實(shí)現(xiàn)的喉祭,兩者邏輯是大同小異的,為了更方便閱讀以字節(jié)碼解釋器為例
monitorenter指令對應(yīng)代碼:
在bytecodeInterpreter.cpp#1804行雷绢。
monitorexit指令對應(yīng)代碼:
在bytecodeInterpreter.cpp#1911行泛烙。
由以上可知,我們找到了monitorenter/monitorexit 指令對應(yīng)的代碼入口翘紊,也就是指令具體的實(shí)現(xiàn)位置蔽氨。
修飾方法
先來看Demo:
public class TestSynchronized {
//共享變量
int a = 0;
Object object = new Object();
public static void main(String args[]) {
}
private synchronized void add() {
a++;
}
}
同樣的使用javap指令,結(jié)果如下:
與修飾代碼塊不一樣的是:并沒有monitorenter/monitorexit 指令帆疟,但是多了ACC_SYNCHRONIZED 標(biāo)記孵滞,這個(gè)標(biāo)記是怎么解析的呢?
先看看鎖的入口和出口對應(yīng)的代碼:
方法鎖入口
在bytecodeInterpreter.cpp#643行鸯匹。
上圖標(biāo)紅的部分從名字可以看出判斷該方法是否是同步方法坊饶,若是同步方法,則進(jìn)行獲取鎖的步驟殴蓬。
尋找is_synchronized()函數(shù)匿级,在method.hpp里蟋滴。
繼續(xù)看accessFlags.hpp:
最終看jvm.h
可以看出:
用synchronized關(guān)鍵字修飾方法后,反編譯出來的代碼里帶有:ACC_SYNCHRONIZED 標(biāo)記與JVM里的JVM_ACC_SYNCHRONIZED 對應(yīng)痘绎,而這個(gè)參數(shù)最終使用的地方是通過is_synchronized()函數(shù)用來判斷是否是同步方法津函。
方法鎖出口
方法結(jié)束后運(yùn)行此段代碼,里邊判斷是否是同步方法孤页,進(jìn)而進(jìn)行釋放鎖等操作尔苦。
3、總結(jié)
synchronized修飾代碼塊和方法行施,兩者異同:
1允坚、修飾代碼塊時(shí)編譯后會在臨界區(qū)前后加入monitorenter、monitorexit 指令
2蛾号、修飾方法時(shí)進(jìn)入/退出方法時(shí)會判斷ACC_SYNCHRONIZED 標(biāo)記是否存在
3稠项、不管是用monitorenter/monitorexit 還是ACC_SYNCHRONIZED,最終都是在對象頭上做文章鲜结,都需要獲取鎖展运。
了解了synchronized使用及其源碼入口,接下來將深入探析其工作機(jī)制精刷。下篇將會分析無鎖拗胜、偏向鎖、輕量級鎖怒允、重量級鎖的實(shí)現(xiàn)機(jī)制埂软。
本文基于jdk8。