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)度
Mark Word
可以看到有幾個(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)一張圖吧~
屬性說(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