Java鎖 synchronized

Synchronized 關(guān)鍵字
喜歡底層源碼的朋友可以來(lái)交流探討马僻。交流群:818491202 驗(yàn)證:88

在java中,相信大家都用過(guò) synchronized 來(lái)解決多線程安全問(wèn)題,下面簡(jiǎn)單描述一下 synchronized 的相關(guān)特性.
被 synchronized 包含的代碼塊具有以下特性

原子性 同步的代碼塊操作不可中斷
可見(jiàn)性 同步代碼塊里的數(shù)據(jù)都是最新的,也就是主存數(shù)據(jù),并且操作完會(huì)立即刷新主存

這兩個(gè)特性將在下面的代碼示例中展示(更底層的留到更后面說(shuō))

synchronized 原子性的實(shí)現(xiàn)就是: 加鎖

java中的對(duì)象鎖有四種狀態(tài): 無(wú)鎖、偏向鎖饰躲、輕量級(jí)鎖薛闪、重量級(jí)鎖(monitor).從左往右逐漸升級(jí).

根據(jù)對(duì)象鎖的競(jìng)爭(zhēng),鎖會(huì)逐漸升級(jí),最后可能 升級(jí)成重量級(jí)鎖也就是 monitor.并且鎖只能升級(jí),不能降級(jí)
原子性
synchronized 保證了同步代碼塊里面的操作的原子性
代碼示例
public class SyncTest2 {
// 計(jì)數(shù)
static CountDownLatch cdl = new CountDownLatch(2);

public static void main(String[] args) throws Exception{
    LockObject lockObject = new LockObject();
    new Thread(new TestSyncRunnable(lockObject)).start();
    new Thread(new TestSyncRunnable(lockObject)).start();
    cdl.await();
}

static class LockObject{
    private int cnt;
}

static class TestSyncRunnable implements Runnable{

    private LockObject lockObject;

    TestSyncRunnable(LockObject lockObject){
        this.lockObject = lockObject;
    }

    @Override
    public void run() {
        synchronized(this.lockObject) {
            System.out.println(String.format("線程%s開(kāi)始執(zhí)行任務(wù)",Thread.currentThread()));
            for(int i = 0;i<10000000;i++) {
                this.lockObject.cnt++;

// System.out.println(String.format("線程%s執(zhí)行了第%s次",Thread.currentThread(),i));
}
System.out.println(String.format("線程%s執(zhí)行完畢了",Thread.currentThread()));
}
cdl.countDown();
}
}
上面這個(gè)結(jié)果,不管運(yùn)行幾次,結(jié)果都是一個(gè)執(zhí)行完畢了,另一個(gè)才開(kāi)始執(zhí)行,因?yàn)?synchronized 是包括了整個(gè)循環(huán)操作.

線程Thread[Thread-0,5,main]開(kāi)始執(zhí)行任務(wù)

線程Thread[Thread-0,5,main]執(zhí)行完畢了

線程Thread[Thread-1,5,main]開(kāi)始執(zhí)行任務(wù)

線程Thread[Thread-1,5,main]執(zhí)行完畢了
所有線程執(zhí)行完畢了,結(jié)果: cnt = 20000000
此時(shí)我們 將 synchronized 關(guān)鍵字去掉 ,run方法變成了如下
@Override
public void run() {
// synchronized(this.lockObject) {
System.out.println(String.format("線程%s開(kāi)始執(zhí)行任務(wù)",Thread.currentThread()));
for(int i = 0;i<10000000;i++) {
this.lockObject.cnt++;
}
System.out.println(String.format("線程%s執(zhí)行完畢了",Thread.currentThread()));
// }
cdl.countDown();
}
下面是輸出的結(jié)果,不管運(yùn)行了多少次,因?yàn)檠h(huán)次數(shù)比較多,所以 能很明顯的能看出來(lái),在第一個(gè)線程執(zhí)行完畢前,都會(huì)被第二個(gè)線程打斷
線程Thread[Thread-0,5,main]開(kāi)始執(zhí)行任務(wù)

線程Thread[Thread-1,5,main]開(kāi)始執(zhí)行任務(wù)

線程Thread[Thread-1,5,main]執(zhí)行完畢了

線程Thread[Thread-0,5,main]執(zhí)行完畢了

可見(jiàn)性

可見(jiàn)性表明在 synchronized 代碼塊中的對(duì)象,都會(huì)從主存中獲取最新數(shù)據(jù),并且在同步代碼塊結(jié)束后,會(huì)將最新數(shù)據(jù)寫(xiě)入主存.

本來(lái)想了一個(gè)例子,但是這個(gè)例子似乎不太恰當(dāng),由于i++這個(gè)操作并不是原子性并且java中似乎沒(méi)有 有原子性但是沒(méi)有可見(jiàn)性的東西.
在驗(yàn)證東西的時(shí)候,都是單一變量原則,我覺(jué)得無(wú)法完全證明,所以例子就不貼出來(lái)了.(如果各位有什么好的想法或者建議可以偷偷告訴我,真的可行的話我會(huì)偷偷在這加上個(gè)例子~~)

JVM 對(duì)象模型
這篇是講 synchronized 的,如果連模型都講了會(huì)不會(huì)太多(會(huì)的)?但是 synchronized 跟對(duì)象模型根本離不開(kāi),so就一起放在這里介紹吧~
當(dāng)我們申請(qǐng)一個(gè)java對(duì)象的時(shí)候,jvm將會(huì)構(gòu)造一個(gè) 包含三部分?jǐn)?shù)據(jù)的對(duì)象

對(duì)象頭 header - 包含對(duì)象的各種標(biāo)識(shí)

實(shí)例數(shù)據(jù) - java對(duì)象中擁有的具體字段

對(duì)齊填充 - 對(duì)象字段4字節(jié)對(duì)齊,對(duì)象地址必須是8字節(jié)的倍數(shù),不滿則補(bǔ).

Header
JVM 對(duì)象的 Header 由三部分組成

Mark Word 用于存儲(chǔ)對(duì)象的各種 標(biāo)志信息 (thread ID唇牧、 epoch风范、age、biasable摹恰、lock辫继、hashCode 等)

Class Metadata Address 指向class地址的指針(class要加載到內(nèi)存中,既然是內(nèi)存,那就肯定有個(gè)地址),用來(lái)標(biāo)記該對(duì)象的類型-class

Array length 如果該對(duì)象是數(shù)組的話,則會(huì)多4個(gè)字節(jié)(32bit)來(lái)存儲(chǔ)數(shù)組的長(zhǎng)度

image.png

Mark Word


image.png

可以看到有幾個(gè)屬性值,從左往右看:

thread ID (線程ID,記錄持有偏向鎖的線程ID最開(kāi)始是0,這個(gè)判斷偏向鎖是否要升級(jí)成輕量級(jí)鎖)

epoch (紀(jì)元,可以理解為用來(lái)記錄偏向鎖的 identifier )

age (對(duì)象的年齡,存活過(guò)了多少次gc)

biasable (是否開(kāi)啟偏向鎖,默認(rèn)開(kāi)啟,在某些已經(jīng)確定會(huì)發(fā)生競(jìng)爭(zhēng)的場(chǎng)景,關(guān)閉偏向鎖能提高效率 .開(kāi)啟的話值為1,否則為0,如下圖
就是開(kāi)啟的)

pointer to lock record (指向棧中鎖記錄的指針)

pointer to heavyweight monitor (指向監(jiān)視器的指針)

鎖標(biāo)識(shí)位 (這個(gè)東西是用來(lái)標(biāo)記當(dāng)前對(duì)象鎖狀態(tài),下面列出幾種狀態(tài)值)

01 無(wú)鎖、偏向鎖
00 輕量級(jí)鎖
10 重量級(jí)鎖
11 表明對(duì)象要被回收了, 標(biāo)記GC

鎖升級(jí)流程
1.開(kāi)始的時(shí)候?qū)ο笫菬o(wú)鎖的, 如果開(kāi)啟了偏向鎖,此時(shí)的 ThreadID 為0 ,表示沒(méi)有線程持有鎖.(禁用了偏向鎖的話,則從輕量級(jí)鎖開(kāi)始)
2.當(dāng)一個(gè)線程第一次給對(duì)象加鎖對(duì)時(shí)候,此時(shí) thread ID 會(huì) 從默認(rèn)的0改為該加鎖線程的ID,并且同一個(gè)線程可以多次獲取該鎖,也就是可重入.
3.重入流程: 當(dāng)還是偏向鎖并且獲取鎖的時(shí)候,根據(jù) 嘗試加鎖的線程ID和對(duì)象中存儲(chǔ)的 thread ID 比較 ,如果是同一個(gè)線程,則允許重入,即獲取鎖.
4.當(dāng)嘗試加鎖的線程ID跟當(dāng)前對(duì)象存儲(chǔ)的 thread ID不同 , 則表明有第二個(gè)線程來(lái)爭(zhēng)搶鎖(在變成輕量級(jí)鎖之前, threadID 是會(huì)一直保存著). 一旦(劃重點(diǎn): 一旦)有第二個(gè)線程來(lái)爭(zhēng)搶鎖的時(shí)候,就會(huì)轉(zhuǎn)變成輕量級(jí)鎖的結(jié)構(gòu),不管當(dāng)前對(duì)象是不是正被鎖著.(升級(jí)成輕量級(jí)鎖的時(shí)候 thread ID 已經(jīng)不存在)
5.當(dāng)變成輕量級(jí)鎖后,會(huì)進(jìn)行一定次數(shù)的自旋(實(shí)際上就是 循環(huán)CAS操作 ),自旋一定次數(shù)鎖都失敗后,鎖最后會(huì)升級(jí)成 重量級(jí)鎖(monitor) .
一些參數(shù)
禁用偏向鎖
偏向鎖的話,可以用以下參數(shù)關(guān)閉
// 禁用偏向鎖
-XX:-UseBiasedLocking
自旋次數(shù)
// 設(shè)置自旋次數(shù)
-XX:PreBlockSpin
// 禁用偏向鎖
-XX:-UseBiasedLocking
重量級(jí)鎖 - monitor
鎖競(jìng)爭(zhēng)嚴(yán)重的話,最后對(duì)象鎖會(huì)升級(jí)為重量級(jí)鎖-monitor
關(guān)于 monitor的結(jié)構(gòu) ,先來(lái)一張圖吧~

image.png

屬性說(shuō)明
_owner: 當(dāng)前鎖的持有者
EntryList: 正在阻塞爭(zhēng)搶鎖鎖的線程集合,沒(méi)有爭(zhēng)搶到鎖就會(huì)進(jìn)入該隊(duì)列
WaitSet: 調(diào)用 wait() 被掛起的線程集合
流程
(咳咳,這里簡(jiǎn)單的描述一下流程)

文章最后的參考鏈接有一個(gè)較詳細(xì)的 synchronized 源碼解讀鏈接

當(dāng)線程爭(zhēng)搶鎖失敗的時(shí)候,會(huì)進(jìn)入 EntryList 進(jìn)行阻塞.
當(dāng)持有鎖的線程調(diào)用 wait() 方法的時(shí)候,實(shí)際上是執(zhí)行了 monitorexit 放棄了鎖, 然后掛起線程,線程進(jìn)入 waitSet.
當(dāng)持有鎖的線程調(diào)用 notify()/notifyAll() 并且在同步代碼塊結(jié)束的時(shí)候 ,也就是調(diào)用了 monitorexit 的時(shí)候在 waitSet 中的線程才能獲取到鎖.

來(lái)個(gè)??
public class SyncDemo {
int i;

public void test1() {
    synchronized (this) {
        i++;
    }
}

public static void main(String[] args) throws InterruptedException {
    SyncDemo syncDemo = new SyncDemo();
    for (int i = 0; i < 100; i++) {
        syncDemo.test1();
    }
}

}
使用javap查看字節(jié)碼
// 地址太長(zhǎng)就省略前面的,知道是class就好了
javap -v -l -c /xxxxx/SyncDemo.class
java字節(jié)碼
public void test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter // 這個(gè)就是 synchronized 的 monitor, 先獲取鎖
4: aload_0
5: dup
6: getfield #2 // Field i:I , 獲取i的數(shù)值并入棧
9: iconst_1 // 將1(這里是int)入棧
10: iadd // 將棧頂2個(gè)int數(shù)值相加,結(jié)果入棧
11: putfield #2 // Field i:I , 從棧頂彈出并賦值給i
14: aload_1
15: monitorexit // 操作結(jié)束后釋放鎖
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return

通過(guò)字節(jié)碼可以看到,使用的 monitorenter 進(jìn)行加鎖 , 操作結(jié)束后 monitorexit 釋放鎖.
那如果有多個(gè)線程進(jìn)行鎖的爭(zhēng)搶呢?
像下面這樣的代碼
public static void main(String[] args) throws InterruptedException {
SyncDemo syncDemo = new SyncDemo();
// 啟動(dòng)兩個(gè)線程進(jìn)行爭(zhēng)搶
new Thread(new SyncDemoRunnable(syncDemo)).start();
new Thread(new SyncDemoRunnable(syncDemo)).start();
}
}
public class SyncDemoRunnable implements Runnable{
SyncDemo syncDemo;

SyncDemoRunnable(SyncDemo syncDemo){
    this.syncDemo = syncDemo;
}

@Override
public void run() {
    for (int i = 0; i < 10000000; i++) {
        synchronized (this.syncDemo) {
            this.syncDemo.i++;
        }
    }
}

}
這時(shí)候看javap的代碼甚至?xí)l(fā)現(xiàn) monitorentry 都不見(jiàn)了( 可能是使用的姿勢(shì)不對(duì)? ).
最后通過(guò)查看匯編代碼會(huì)發(fā)現(xiàn),實(shí)際上都有l(wèi)ock 前綴的指令,證明其是原子操作.

喜歡底層源碼的朋友可以來(lái)交流探討俗慈。交流群:818491202 驗(yàn)證:88

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末姑宽,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子闺阱,更是在濱河造成了極大的恐慌炮车,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酣溃,死亡現(xiàn)場(chǎng)離奇詭異瘦穆,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)救拉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)难审,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)瘫拣,“玉大人亿絮,你說(shuō)我怎么就攤上這事◆镏簦” “怎么了派昧?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)拢切。 經(jīng)常有香客問(wèn)我蒂萎,道長(zhǎng),這世上最難降的妖魔是什么淮椰? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任五慈,我火速辦了婚禮纳寂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘泻拦。我一直安慰自己毙芜,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布争拐。 她就那樣靜靜地躺著腋粥,像睡著了一般。 火紅的嫁衣襯著肌膚如雪架曹。 梳的紋絲不亂的頭發(fā)上隘冲,一...
    開(kāi)封第一講書(shū)人閱讀 51,365評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音绑雄,去河邊找鬼展辞。 笑死,一個(gè)胖子當(dāng)著我的面吹牛万牺,可吹牛的內(nèi)容都是我干的纵竖。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼杏愤,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼靡砌!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起珊楼,我...
    開(kāi)封第一講書(shū)人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤通殃,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后厕宗,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體画舌,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年已慢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了曲聂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡佑惠,死狀恐怖朋腋,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情膜楷,我是刑警寧澤旭咽,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站赌厅,受9級(jí)特大地震影響穷绵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜特愿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一仲墨、第九天 我趴在偏房一處隱蔽的房頂上張望勾缭。 院中可真熱鬧,春花似錦目养、人聲如沸漫拭。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)采驻。三九已至,卻和暖如春匈勋,著一層夾襖步出監(jiān)牢的瞬間礼旅,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工洽洁, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留痘系,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓饿自,卻偏偏與公主長(zhǎng)得像汰翠,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子昭雌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容