一、synchronized 實(shí)現(xiàn)同步示例
public class MyThread extends Thread{
private int count = 0;
@ Override
public synchronized void run() {
for (int i = 0; i < 5; i++) {
try {
System.out.println("線程名:"+Thread.currentThread().getName() + ":" + (count++));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class MyTest
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread1 = new Thread( myThread, "SyncThread1");
Thread thread2 = new Thread( myThread, "SyncThread2");
thread1.start();
thread2.start();
}
}
分析:
通過
new MyThread()
創(chuàng)建了一個(gè)對象myThread
,這時(shí)候堆中就存在了共享資源myThread
,然后對myThread
對象創(chuàng)建兩個(gè)線程,那么thread1線程和thread2線程就會(huì)共享myThread
。thread1.start()
和thead2.start()
開啟了兩個(gè)線程,CPU會(huì)隨機(jī)調(diào)度這兩個(gè)線程。假如thread1
先獲得synchronized
鎖,那么thread1先把run()
執(zhí)行完,然后釋放鎖。接著thread1
獲得synchronized
鎖,thread2把run()
執(zhí)行完,然后釋放鎖。
二、synchronized 典型錯(cuò)誤示例
public class MyThread implements Runnable{
private int count = 0;
@ Override
public synchronized void run() {
for (int i = 0; i < 5; i++) {
try {
System.out.println("線程名:"+Thread.currentThread().getName() + ":" + (count++));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class MyTest
public static void main(String[] args) {
System.out.println("使用關(guān)鍵字synchronized每次調(diào)用進(jìn)行new SyncThread()");
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
Thread thread1 = new Thread(myThread1, "SyncThread1");
Thread thread2 = new Thread(myThread2, "SyncThread2");
thread1.start();
thread2.start();
}
}
分析:
通過
new MyThread()
創(chuàng)建了兩個(gè)對象myThread1
和myThread2
,這時(shí)候堆中就存在了共享資源myThread1
和myThread2
,然后對myThread1
對象創(chuàng)建一個(gè)線程thread1
,對myThread2
對象創(chuàng)建一個(gè)線程thread2
,那么thread1
線程就會(huì)共享myThread1
,thread2
線程就會(huì)共享myThread2
,thread1.start()
和thead2.start()
開啟了兩個(gè)線程,CPU會(huì)隨機(jī)調(diào)度這兩個(gè)線程。那么thread1
線程在執(zhí)行的時(shí)候就會(huì)獲取共享資源myThread1.run()
的鎖,thread2
線程在執(zhí)行的時(shí)候就會(huì)獲取共享資源myThread2.run()
的鎖,顯然兩個(gè)線程共享的都不是同一個(gè)資源。
三、Java 對象頭與 Monitor
下面先來了解一個(gè)概念Java對象頭,這對深入理解synchronized實(shí)現(xiàn)原理非常關(guān)鍵。
在JVM中,對象在內(nèi)存中的布局分為三塊區(qū)域:對象頭、實(shí)例數(shù)據(jù)、填充數(shù)據(jù)
- 實(shí)例變量:存放類的屬性數(shù)據(jù)信息,包括父類的屬性信息.
- 填充數(shù)據(jù):由于虛擬機(jī)要求對象起始地址必須是8字節(jié)的整數(shù)倍。
而對于頂部,則是Java頭對象
,它實(shí)現(xiàn)synchronized的鎖對象的基礎(chǔ),這點(diǎn)我們重點(diǎn)分析它,一般而言,synchronized使用的鎖對象是存儲(chǔ)在Java對象頭里的,jvm中采用2個(gè)字來存儲(chǔ)對象頭(如果對象是數(shù)組則會(huì)分配3個(gè)字,多出來的1個(gè)字記錄的是數(shù)組長度),其主要結(jié)構(gòu)是由Mark Word
和 Class Metadata Address
組成,其結(jié)構(gòu)說明如下表:
其中Mark Word
字段的存儲(chǔ)結(jié)構(gòu)會(huì)根據(jù)鎖狀態(tài)而發(fā)生變化:
這里我們重點(diǎn)分析一下重量級鎖也就是通常說synchronized的對象鎖
,鎖標(biāo)識位為10,其中指針指向的是monitor對象
的起始地址。
每個(gè)對象都存在著一個(gè) monitor 與之關(guān)聯(lián),對象與其 monitor 之間的關(guān)系有存在多種實(shí)現(xiàn)方式,如monitor可以與對象一起創(chuàng)建銷毀或當(dāng)線程試圖獲取對象鎖時(shí)自動(dòng)生成,但當(dāng)一個(gè) monitor 被某個(gè)線程持有后,它便處于鎖定狀態(tài)。
在Java虛擬機(jī)(HotSpot)中,monitor是由ObjectMonitor實(shí)現(xiàn)的,其主要數(shù)據(jù)結(jié)構(gòu)如下(位于HotSpot虛擬機(jī)源碼ObjectMonitor.hpp文件,C++實(shí)現(xiàn)的)
ObjectMonitor() {
_header = NULL;
_count = 0; //記錄個(gè)數(shù)
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //處于wait狀態(tài)的線程,會(huì)被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //處于等待鎖block狀態(tài)的線程,會(huì)被加入到該列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor
中有兩個(gè)隊(duì)列,_WaitSet
(等待狀態(tài)) 和 _EntryList
(阻塞狀態(tài)),都是用來保存ObjectWaiter對象列表
( _WaitSet
和 _EntryList
中的線程都會(huì)被封裝成ObjectWaiter對象
),_owner
指向持有ObjectMonitor對象
的線程,當(dāng)多個(gè)線程同時(shí)訪問一段同步代碼時(shí),首先會(huì)進(jìn)入_EntryList
集合,當(dāng)線程獲取到對象的monitor
后進(jìn)入_Owner
區(qū)域,并把monitor
中的owner
變量設(shè)置為當(dāng)前線程,同時(shí)monitor
中的計(jì)數(shù)器count
加1,若線程調(diào)用 wait()
方法,將釋放當(dāng)前持有的monitor
,owner
變量恢復(fù)為null,count
自減1,同時(shí)該線程進(jìn)入WaitSet
集合中等待被喚醒。若當(dāng)前線程執(zhí)行完畢也將釋放monitor
(鎖)并復(fù)位變量的值,以便其他線程進(jìn)入獲取monitor
(鎖)。
四、synchronized代碼塊底層原理
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ù)
//構(gòu)造函數(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方法實(shí)現(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 //注意此處,進(jìn)入同步方法
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"
我們主要關(guān)注字節(jié)碼中的如下代碼
3: monitorenter //進(jìn)入同步方法
//..........省略其他
15: monitorexit //退出同步方法
16: goto 24
//省略其他.......
21: monitorexit //退出同步方法
從字節(jié)碼中可知同步語句塊的實(shí)現(xiàn)使用的是monitorenter
和 monitorexit
指令,其中monitorenter
指令指向同步代碼塊的開始位置,monitorexit
指令則指明同步代碼塊的結(jié)束位置,當(dāng)執(zhí)行monitorenter
指令時(shí),當(dāng)前線程將試圖獲取 objectref
(即對象鎖) 所對應(yīng)的 monitor
的持有權(quán),當(dāng) objectref
的 monitor
的進(jìn)入計(jì)數(shù)器為 0,那線程可以成功取得 monitor
,并將計(jì)數(shù)器值設(shè)置為 1,取鎖成功。如果當(dāng)前線程已經(jīng)擁有 objectref
的 monitor
的持有權(quán),那它可以重入這個(gè) monitor
(關(guān)于重入性稍后會(huì)分析),重入時(shí)計(jì)數(shù)器的值也會(huì)加 1。倘若其他線程已經(jīng)擁有 objectref
的 monitor
的所有權(quán),那當(dāng)前線程將被阻塞,直到正在執(zhí)行線程執(zhí)行完畢,即monitorexit
指令被執(zhí)行,執(zhí)行線程將釋放 monitor
(鎖)并設(shè)置計(jì)數(shù)器值為0 ,其他線程將有機(jī)會(huì)持有 monitor
。值得注意的是編譯器將會(huì)確保無論方法通過何種方式完成,方法中調(diào)用過的每條 monitorenter
指令都有執(zhí)行其對應(yīng) monitorexit
指令,而無論這個(gè)方法是正常結(jié)束還是異常結(jié)束。為了保證在方法異常完成時(shí) monitorenter
和 monitorexit
指令依然可以正確配對執(zhí)行,編譯器會(huì)自動(dòng)產(chǎn)生一個(gè)異常處理器,這個(gè)異常處理器聲明可處理所有的異常,它的目的就是用來執(zhí)行 monitorexit
指令。從字節(jié)碼中也可以看出多了一個(gè)monitorexit
指令,它就是異常結(jié)束時(shí)被執(zhí)行的釋放monitor
的指令。
五、synchronized方法底層原理
方法級的同步是隱式,即無需通過字節(jié)碼指令來控制的,它實(shí)現(xiàn)在方法調(diào)用和返回操作之中。JVM可以從方法常量池中的方法表結(jié)構(gòu)(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標(biāo)志區(qū)分一個(gè)方法是否同步方法。當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì) 檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先持有monitor(虛擬機(jī)規(guī)范中用的是管程一詞), 然后再執(zhí)行方法,最后再方法完成(無論是正常完成還是非正常完成)時(shí)釋放monitor。在方法執(zhí)行期間,執(zhí)行線程持有了monitor,其他任何線程都無法再獲得同一個(gè)monitor。如果一個(gè)同步方法執(zhí)行期間拋 出了異常,并且在方法內(nèi)部無法處理此異常,那這個(gè)同步方法所持有的monitor將在異常拋到同步方法之外時(shí)自動(dòng)釋放。
補(bǔ)充:“獲取monitor” 在第四點(diǎn)提到過,就是把monitor中owner變量設(shè)置為當(dāng)前線程,同時(shí)monitor中的計(jì)數(shù)器count加1
六、Java虛擬機(jī)對synchronized的優(yōu)化
鎖的狀態(tài)總共有四種:無鎖狀態(tài)
、偏向鎖
、輕量級鎖
、重量級鎖
。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(自動(dòng)的)
,但是鎖的升級是單向的,也就是說只能從低到高升級,不會(huì)出現(xiàn)鎖的降級。、
偏向鎖
鎖偏向是一種針對加鎖操作的優(yōu)化手段。它的核心思想是:如果一個(gè)線程獲得了鎖,那么鎖就進(jìn)入偏向模式。當(dāng)這個(gè)線程再次請求鎖時(shí),無須再做任何同步操作。這樣就節(jié)省了大量有關(guān)鎖申請的操作,從而提高了程序性能。
因此,對于幾乎沒有鎖競爭的場合,偏向鎖有比較好的優(yōu)化效果,因?yàn)檫B續(xù)多次極有可能是同一個(gè)線程請求相同的鎖。而對于鎖競爭比較激烈的場合,其效果不佳。因?yàn)樵诟偁幖ち业膱龊?,最有可能的情況是每次都是不同的線程來請求相同的鎖。這樣偏向模式會(huì)失效,因此還不如不啟用偏向鎖。
輕量級鎖
倘若偏向鎖失敗,虛擬機(jī)并不會(huì)立即升級為重量級鎖,它還會(huì)嘗試使用一種稱為輕量級鎖的優(yōu)化手段(1.6之后加入的),此時(shí)Mark Word 的結(jié)構(gòu)也變?yōu)檩p量級鎖的結(jié)構(gòu)。
輕量級鎖能夠提升程序性能的依據(jù)是“對絕大部分的鎖,在整個(gè)同步周期內(nèi)都不存在競爭”,注意這是經(jīng)驗(yàn)數(shù)據(jù)。需要了解的是,輕量級鎖所適應(yīng)的場景是不同的線程交替執(zhí)行同一同步塊的場合,如果存在不同線程在同一時(shí)間訪問同一鎖的場合,就會(huì)導(dǎo)致輕量級鎖膨脹為重量級鎖。
自旋鎖
輕量級鎖失敗后,虛擬機(jī)為了避免線程真實(shí)地在操作系統(tǒng)層面掛起,還會(huì)進(jìn)行一項(xiàng)稱為自旋鎖的優(yōu)化手段。這是基于在大多數(shù)情況下,線程持有鎖的時(shí)間都不會(huì)太長,如果直接掛起操作系統(tǒng)層面的線程可能會(huì)得不償失,畢竟操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到核心態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時(shí)間,時(shí)間成本相對較高,因此自旋鎖會(huì)假設(shè)在不久將來,當(dāng)前的線程可以獲得鎖,因此虛擬機(jī)會(huì)讓當(dāng)前想要獲取鎖的線程做幾個(gè)空循環(huán)(這也是稱為自旋的原因),一般不會(huì)太久,可能是50個(gè)循環(huán)或100循環(huán),在經(jīng)過若干次循環(huán)后,如果得到鎖,就順利進(jìn)入臨界區(qū)。如果還不能獲得鎖,那就會(huì)將線程在操作系統(tǒng)層面掛起,這就是自旋鎖的優(yōu)化方式,這種方式確實(shí)也是可以提升效率的。最后沒辦法也就只能升級為重量級鎖了。
重量級鎖
上述已經(jīng)講得 fi~ 常 清楚了。文章來源:http://www.zghlxwxcb.cn/news/detail-444815.html
鎖消除
消除鎖是虛擬機(jī)另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java虛擬機(jī)在JIT編譯時(shí)(可以簡單理解為當(dāng)某段代碼即將第一次被執(zhí)行時(shí)進(jìn)行編譯,又稱即時(shí)編譯),通過對運(yùn)行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節(jié)省毫無意義的請求鎖時(shí)間,如下StringBuffer的append是一個(gè)同步方法,但是在add方法中的StringBuffer屬于一個(gè)局部變量,并且不會(huì)被其他線程所使用,因此StringBuffer不可能存在共享資源競爭的情景,JVM會(huì)自動(dòng)將其鎖消除。文章來源地址http://www.zghlxwxcb.cn/news/detail-444815.html
/**
* Created by zejian on 2017/6/4.
* Blog : http://blog.csdn.net/javazejian [原文地址,請尊重原創(chuàng)]
* 消除StringBuffer同步鎖
*/
public class StringBufferRemoveSync {
public void add(String str1, String str2) {
//StringBuffer是線程安全,由于sb只會(huì)在append方法中使用,不可能被其他線程引用
//因此sb屬于不可能共享的資源,JVM會(huì)自動(dòng)消除內(nèi)部的鎖
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");
}
}
}
到了這里,關(guān)于【并發(fā)編程】深入理解Java并發(fā)之synchronized實(shí)現(xiàn)原理的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!