1. synchronized簡(jiǎn)介
synchronized作為java關(guān)鍵字,是一種多線程同步的手段《裕可以保證資源在多線程共享的情況下的正確性恼策。舉反例鸦致,如下代碼就是線程不安全的:
class Unsafe {
int count = 0;
public void increment() {
count++;
}
public static void main(String[] args) throws InterruptedException {
Unsafe unsafe = new Unsafe();
for (int i = 0; i < 100; i++) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(1);
unsafe.increment();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
Thread.currentThread().sleep(1000);
System.out.println(unsafe.count);
}
}
啟動(dòng)100個(gè)線程對(duì)共享的變量count進(jìn)行+1操作,但是輸出結(jié)果并不是想象中的100,有可能是99、98分唾、97等抗碰,但是如果對(duì)方法increment()加上synchronized進(jìn)行修飾,程序就能表現(xiàn)正常的輸出結(jié)果100绽乔。為什么synchronized會(huì)有這樣的魔力弧蝇?其原理是什么樣子的呢?
2.從java虛擬機(jī)內(nèi)存層次簡(jiǎn)述例子的執(zhí)行過(guò)程
JVM是以進(jìn)程為單位的折砸,所有的同步討論范疇都是在多線程的范圍內(nèi)看疗。
就以上面的栗子作為案例來(lái)敘述,先看下內(nèi)存結(jié)構(gòu)圖
簡(jiǎn)單描述下單個(gè)線程進(jìn)行方法調(diào)用的時(shí)候執(zhí)行步驟:
1. 線程調(diào)用increment方法睦授,讀取Heap中count的值鹃觉,并將count拷貝一個(gè)副本到當(dāng)前堆棧中。
2. 將副本中的count值加1睹逃。
3. 將副本中的count值重新放到Heap中盗扇。
每次加1的操作都包含了3個(gè)具體的步驟,很明顯不是原子操作沉填,在多線程環(huán)境下肯定存在線程安全性的問(wèn)題疗隶。
如果在increment方法上加上synchronized修飾,則執(zhí)行過(guò)程變成如下的過(guò)程:
1. 搶占unsafe對(duì)象的Monitor lock(監(jiān)視器鎖翼闹,見(jiàn)下面的詳解)
2. 搶占到鎖后執(zhí)行上面1~3的步驟斑鼻,搶占不到則進(jìn)入阻塞狀態(tài),等待監(jiān)視器鎖的釋放猎荠,再次進(jìn)行監(jiān)視器鎖的搶占坚弱。
3. 監(jiān)視器鎖
在JAVA世界中萬(wàn)物皆對(duì)象,每個(gè)對(duì)象都提供了與之相關(guān)的監(jiān)視器鎖关摇。java虛擬機(jī)可以支持方法級(jí)的同步和方法內(nèi)部一段指令序列的同步荒叶,這兩種方式的同步結(jié)構(gòu)都是使用監(jiān)視器鎖來(lái)支持的。
1. 方法級(jí)的同步
class Unsafe {
private int count = 0;
public synchronized void increment() {
count++;
}
public static void main(String[] args) {
Unsafe unsafe = new Unsafe();
unsafe.increment();
}
}
方法級(jí)的同步是隱式的输虱,即無(wú)需通過(guò)字節(jié)碼指令來(lái)控制些楣。虛擬機(jī)可以從方法常量池的方法表結(jié)構(gòu)中的ACC_SYNCHRONIZED訪問(wèn)標(biāo)志得知一個(gè)方法是否聲明為同步方法。當(dāng)方法調(diào)用時(shí)宪睹,調(diào)用指令將會(huì)檢查方法的ACC_SYNCHRONIZED訪問(wèn)標(biāo)志是否被設(shè)置愁茁,如果設(shè)置了,則要求當(dāng)前線程現(xiàn)持有當(dāng)前監(jiān)視器鎖亭病,退出時(shí)(不管是正常退出還是異常退出)則是否當(dāng)前持有的監(jiān)視器鎖鹅很。應(yīng)用到如上代碼中,則在main線程調(diào)用unsafe.increment()
方法時(shí)則需要首先獲取當(dāng)前對(duì)象監(jiān)視器鎖罪帖,在退出方法執(zhí)行時(shí)則釋放持有的監(jiān)視器鎖促煮。
2. 方法內(nèi)部同步
public class Unsafe {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
public static void main(String[] args) {
Unsafe unsafe = new Unsafe();
unsafe.increment();
}
}
通過(guò)javap工具反編譯出increment方法相關(guān)的字節(jié)碼
public void increment();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: dup
6: getfield #2 // Field count:I
9: iconst_1
10: iadd
11: putfield #2 // Field count: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:
from to target type
4 16 19 any
19 22 19 any
同步一段指令集序列通常是由java語(yǔ)言中synchronized語(yǔ)句塊表示的食听,java虛擬機(jī)的指令集有monitorenter(第3行字節(jié)碼)和monitorexit(第15行字節(jié)碼)兩條指令來(lái)支持同步原語(yǔ)。
應(yīng)用到increment()
方法時(shí)污茵,進(jìn)入到同步代碼塊之前樱报,首先借助monitorenter指令獲取當(dāng)前unsafe對(duì)象的監(jiān)視器鎖,然后完成count加1的操作泞当,在退出同步代碼塊時(shí)借助monitorexit完成對(duì)監(jiān)視器鎖的釋放迹蛤。
4. 應(yīng)用
對(duì)于方法外部和內(nèi)部的同步操作,底層的實(shí)現(xiàn)原理雖然是不同的襟士,但是表現(xiàn)的結(jié)果是一致的盗飒。但是在實(shí)際應(yīng)用的時(shí)候還是要注意區(qū)分對(duì)象鎖和類對(duì)象鎖的區(qū)別。
1. 對(duì)象鎖
1.1 this對(duì)象
public class Unsafe {
private int count = 0;
public void increment() {
// 借助的是當(dāng)前unsafe對(duì)象的監(jiān)視器鎖
synchronized (this) {
count++;
}
}
public static void main(String[] args) {
Unsafe unsafe = new Unsafe();
unsafe.increment();
}
}
這里increment()
方法借助的是new出來(lái)的unsafe對(duì)象相關(guān)的對(duì)象鎖來(lái)實(shí)現(xiàn)的線程安全陋桂。
1.2 任意Object對(duì)象
public class Unsafe {
private int count = 0;
private Object lock = new Object();
public void increment() {
// 這里借助object對(duì)象的監(jiān)視器鎖
synchronized (lock) {
count++;
}
}
public static void main(String[] args) {
Unsafe unsafe = new Unsafe();
unsafe.increment();
}
}
監(jiān)視器鎖是所有Object對(duì)象相關(guān)的逆趣,所以任意一個(gè)對(duì)象都可以使用如上的寫(xiě)法。這里借助的是new出來(lái)的object對(duì)象相關(guān)的監(jiān)視器鎖來(lái)實(shí)現(xiàn)的線程安全嗜历。針對(duì)同一類new出來(lái)的不同的對(duì)象宣渗,所以它們之間是沒(méi)鎖競(jìng)爭(zhēng)關(guān)系的。
2. 類對(duì)象鎖
2.1 類
public class Unsafe {
private int count = 0;
public void increment() {
synchronized (Unsafe.class) {
count++;
}
}
public static void main(String[] args) {
Unsafe unsafe = new Unsafe();
unsafe.increment();
}
}
2.2 靜態(tài)方法
public class Unsafe {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static void main(String[] args) {
Unsafe unsafe = new Unsafe();
unsafe.increment();
}
}
因?yàn)閷?duì)象的類的信息是在jvm中的方法區(qū)(永久代)保存的梨州,是被所有的線程共享的痕囱,靜態(tài)方法和成員變量也是被共享的。所以即便將上面的Unsafe
new出不同的對(duì)象暴匠,它們之間還是存在鎖競(jìng)爭(zhēng)關(guān)系的鞍恢。
寫(xiě)在最后:最近一段時(shí)間都在為年初跳槽做技術(shù)上的準(zhǔn)備。之前一直對(duì)原理性的東西似懂非懂每窖,希望能通過(guò)寫(xiě)博客的過(guò)程細(xì)化自己對(duì)技術(shù)細(xì)節(jié)的了解帮掉,也希望博客的內(nèi)容能給廣大的java道友提供一些的幫助和提升。由于筆者水平有限窒典,如果內(nèi)容有誤蟆炊,希望大家批評(píng)指出。
參考文獻(xiàn)
. 《深入理解Java虛擬機(jī)》 周志明 著