1. 線程安全問題
??在進(jìn)行多線程編程的時(shí)候,有可能會出現(xiàn)多個線程同時(shí)訪問同一資源的情況,這種資源可以是變量借嗽,對象,文件转培,數(shù)據(jù)庫表等恶导,這時(shí)候就有可能出現(xiàn)最終訪問結(jié)果不一致的情況。來舉一個最簡單的例子浸须,下單與庫存的問題:
- 下單的時(shí)候惨寿,先獲取剩余庫存邦泄;
- 如果還有庫存,下單成功裂垦,庫存減1顺囊,如果沒有庫存,下單失斀堵!特碳;
如果線程thread-1和線程thread-2,同一時(shí)刻晕换,都讀取到庫存還剩1午乓,然后兩個線程都執(zhí)行下單成功,這時(shí)就會出現(xiàn)庫存超賣的情況闸准。
這其實(shí)就是一種線程安全問題硅瞧,也就是多個線程訪問同一資源時(shí),會導(dǎo)致程序的運(yùn)行結(jié)果并不是我們期望的結(jié)果恕汇。而這里的資源被稱為臨界資源或共享資源,臨界資源可以時(shí)一個對象或辖,對象中的屬性瘾英,一個文件,一個數(shù)據(jù)庫等颂暇,不過方法內(nèi)部的局部變量并不是臨界資源缺谴,因?yàn)榉椒ㄊ窃跅I蠄?zhí)行的,而Java棧是線程私有的耳鸯。
2. synchronized同步代碼塊
??基本上所有的并發(fā)模式在解決線程安全問題時(shí)湿蛔,都是采用的序列化訪問臨界資源
的方案,也就是同一時(shí)刻县爬,只能有一個線程訪問臨界資源阳啥,也稱為同步互斥訪問。通常實(shí)現(xiàn)就是對臨界資源加一個鎖财喳,當(dāng)訪問完臨界資源后釋放鎖察迟,讓其他線程訪問,而synchronized關(guān)鍵字就是其中的一種實(shí)現(xiàn)方式耳高。
synchronized扎瓶,同步代碼塊,是Java內(nèi)置的一種鎖機(jī)制泌枪,其中包含了兩部分概荷,一部分是鎖的對象引用,另一部分是鎖保護(hù)的代碼塊碌燕。其中該同步代碼塊的鎖就是方法調(diào)用所在的對象误证,而靜態(tài)的synchronized方法的鎖是class對象继薛。
首先,我們要知道每個對象都有一個內(nèi)部鎖雷厂,這些鎖被稱為內(nèi)置鎖(Intrinsic Lock)或者監(jiān)視鎖(Monitor Lock)惋增,線程在進(jìn)入同步代碼塊之前會自動獲得鎖,并且在退出同步代碼塊時(shí)自動釋放鎖改鲫。并且該鎖有一個內(nèi)部條件诈皿,由鎖來管理那些試圖進(jìn)入synchronized方法的線程,由條件來管理那些調(diào)用wait的線程像棘。所以在多線程中要訪問某個對象前稽亏,必須要獲取了該對象的鎖才能訪問。
Java中缕题,synchronized關(guān)鍵字用于同步代碼塊(變量或者類名)和方法(實(shí)例方法或靜態(tài)方法)截歉,當(dāng)某個線程調(diào)用synchronized所修飾的方法或代碼塊時(shí),這個線程便獲得了該對象的鎖烟零,其他線程只能處于等待或阻塞中瘪松,等該線程執(zhí)行代碼塊完成釋放鎖后才能執(zhí)行。
2.1 synchronized修飾方法
我們先來看一個簡單的例子:
public class ThreadTest {
public static void main(String[] args) {
final Test test = new Test();
new Thread(() -> test.test(Thread.currentThread())).start();
new Thread(() -> test.test(Thread.currentThread())).start();
}
}
class Test {
void test(Thread thread) {
for (int i = 0; i < 10; i++) {
System.out.println(thread.getName() + ":" + i);
}
}
}
首先锨阿,我們不使用synchronized關(guān)鍵字宵睦,查看下打印結(jié)果(截取部分):
Thread-0:0
Thread-0:1
Thread-0:2
Thread-0:3
Thread-1:0
Thread-1:1
Thread-1:2
Thread-0:4
Thread-1:3
Thread-0:5
可以看到,兩個線程在同時(shí)執(zhí)行test方法墅诡,然后我們給test方法添加synchronized參數(shù)壳嚎,再次查看下結(jié)果:
Thread-0:0
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
Thread-0:5
Thread-0:6
Thread-0:7
Thread-0:8
Thread-0:9
Thread-1:0
Thread-1:1
可以看到,Thread-1是等Thread-0插入完成之后才進(jìn)行的末早,它們之間是一種順序執(zhí)行的關(guān)系烟馅。
不過可能需要注意下:
當(dāng)一個線程正在訪問一個對象的synchronized方法,那么其他線程不能訪問該對象的其他synchronized方法然磷,但是可以訪問該對象的非synchronized方法郑趁。這個原因很簡單,因?yàn)橐粋€對象只有一把鎖姿搜,當(dāng)一個線程獲取了該對象的鎖之后穿撮,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized方法痪欲,而訪問非synchronized方法是不需要獲取對象鎖的悦穿。
2.2 synchronized代碼塊
synchronized代碼塊格式如下:
synchronized (object) {
}
當(dāng)某個線程執(zhí)行這段代碼塊時(shí),該線程會獲取對象object的鎖业踢,從而使得其他線程無法同時(shí)訪問該代碼塊栗柒。而object可以是this,代表調(diào)用這個方法的對象的鎖,也可以是類中的一個屬性瞬沦,代表獲取該屬性的鎖太伊,而如果沒有明確的對象作為鎖,也可以創(chuàng)建一個特殊的變量來充當(dāng)鎖逛钻。而針對上面例子中的test方法僚焦,可以修改為:
void test(Thread thread) {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.println(thread.getName() + ":" + i);
}
}
}
synchronized代碼塊可以實(shí)現(xiàn)只對需要同步的地方進(jìn)行同步,而不用像synchronized方法曙痘,會對整個方法進(jìn)行同步芳悲。
2.3 synchronized靜態(tài)方法
先說一下,除了每個對象有一個對象鎖之外边坤,每個類還有一個類對象的內(nèi)部鎖名扛,也可以稱為類鎖,用于對static方法的線程同步控制茧痒。那么肮韧,也就是說:
如果一個線程執(zhí)行一個對象的synchronized修飾的instance方法,而另一個線程執(zhí)行該對象所屬類的synchronized的static方法旺订,這時(shí)候不會發(fā)生互斥現(xiàn)象弄企,因?yàn)樗鼈兊逆i類型都不一樣,一個是對象鎖区拳,一個是類鎖拘领,所以不存在互斥線程。
2.4 synchronized類
synchronized鎖整個類的形式如下:
synchronized (MyClass.class) {
}
這種情況下劳闹,這個類對應(yīng)的class對象就會被鎖住。因?yàn)閟ynchronized鎖的是同一個對象的同步代碼塊洽瞬,而如果我們想某段代碼在多線程且多個對象的訪問下也線程同步本涕,我們就可以通過這種方式。當(dāng)然還有一種方式伙窃,就是在synchronized的括號中定義一個固定對象菩颖。
3. synchronized字節(jié)碼
3.1 反編譯synchronized同步代碼塊
??為了更深入的理解synchronized,我們使用javap來反編譯一下synchronized代碼塊为障,來了解一下在字節(jié)碼層面的執(zhí)行過程晦闰,在開發(fā)工具IDEA中配置External Tools即可,比如對如下代碼塊進(jìn)行反編譯:
public void test(Thread thread) {
synchronized (this) {
System.out.println(thread.getName() + ":");
}
}
public void test(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=2
0: aload_0
1: dup
2: astore_2
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: new #3 // class java/lang/StringBuilder
10: dup
... 省略
32: aload_2
33: monitorexit
34: goto 42
37: astore_3
38: aload_2
39: monitorexit
40: aload_3
synchronized是通過monitorenter
和monitorexit
這兩條指令實(shí)現(xiàn)了鎖的獲取和釋放過程鳍怨。我們來看下JVM規(guī)范中這兩條指令的描述呻右,先看monitorenter
:
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
- If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
- If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
- If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
這段話的大概意思是:每個對象有一個監(jiān)視器鎖(monitor),當(dāng)monitor被占用時(shí)就會處于鎖定狀態(tài)鞋喇,線程執(zhí)行monitorenter指令時(shí)嘗試獲取monitor的所有權(quán)声滥,過程如下:
- 如果monitor的進(jìn)入數(shù)為0,則該線程進(jìn)入monitor侦香,然后將進(jìn)入數(shù)設(shè)置為1落塑,該線程即為monitor的所有者纽疟。
- 如果線程已經(jīng)占有該monitor,只是重新進(jìn)入憾赁,則進(jìn)入monitor的進(jìn)入數(shù)加1.
- 如果其他線程已經(jīng)占用了monitor污朽,則該線程進(jìn)入阻塞狀態(tài),直到monitor的進(jìn)入數(shù)為0龙考,再重新嘗試獲取monitor的所有權(quán)蟆肆。
再來看一下monitorexit
指令:
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
JVM規(guī)范地址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorexit
這段話的大概意思為:
- 執(zhí)行monitorexit的線程必須是objectref所對應(yīng)的monitor的所有者。
- 指令執(zhí)行時(shí)洲愤,monitor的進(jìn)入數(shù)減1颓芭,如果減1后進(jìn)入數(shù)為0,那線程退出monitor柬赐,不再是這個monitor的所有者亡问。其他被這個monitor阻塞的線程可以嘗試去獲取這個 monitor 的所有權(quán)。
??其實(shí)從字節(jié)碼的角度我們可以看出肛宋,Synchronized代碼塊的底層是通過一個monitor的對象來完成的州藕,分別通過
monitorenter
和monitorexit
指令來指向同步代碼塊的開始和結(jié)束位置。值得注意的是編譯器將會確保無論方法通過何種方式完成酝陈,方法中的每條monitorenter
指令都將對應(yīng)于一條monitorexit
指令床玻。并且編譯器會自動產(chǎn)生一個異常處理器來處理所有的異常,它的目的就是用來執(zhí)行monitorexit
指令沉帮。所以可以看到锈死,字節(jié)碼中多了一個monitorexit
指令,它就是異常結(jié)束時(shí)被執(zhí)行的釋放monitor 的指令穆壕。
3.2 反編譯synchronized方法
??synchronized方法與synchronized代碼塊有些不同待牵,方法的同步是一種隱式的同步,即無需通過字節(jié)碼指令來控制的喇勋。JVM可以從方法常量池中的方法表結(jié)構(gòu)(method_info Structure) 中的 ACC_SYNCHRONIZED 訪問標(biāo)志區(qū)分一個方法是否同步方法缨该。
??當(dāng)方法調(diào)用時(shí),調(diào)用指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標(biāo)志是否被設(shè)置川背,如果設(shè)置了贰拿,執(zhí)行線程將先持有monitor對象,然后執(zhí)行方法熄云,執(zhí)行完成釋放monitor對象膨更。同樣,在方法執(zhí)行期間缴允,執(zhí)行線程持有了monitor询一,其他任何線程都無法再獲得同一個monitor。
我們同樣來看一下反編譯之后的代碼:
public synchronized void test(Thread thread) {
System.out.println(thread.getName() + ":");
}
public synchronized void test(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=2, args_size=2
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/Thread.getName:()Ljava/lang/String;
14: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17: ldc #7 // String :
19: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
28: return
其中,flags的ACC_PUBLIC表示方法訪問類型是public健蕊, ACC_SYNCHRONIZED表示該方法是同步方法菱阵。
- 從字節(jié)碼層面可以看出,同步方法并沒有同步代碼塊的
monitorenter
指令和monitorexit
指令缩功,取得代之的確實(shí)是ACC_SYNCHRONIZED標(biāo)識晴及,JVM通過該訪問標(biāo)志來辨別一個方法是否聲明為同步方法,從而執(zhí)行相應(yīng)的同步調(diào)用嫡锌。
- 再多說下虑稼,在Java早期版本中,synchronized屬于重量級鎖势木,效率低下蛛倦,因?yàn)楸O(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的Mutex Lock來實(shí)現(xiàn)的,而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換是需要從用戶態(tài)轉(zhuǎn)換到核心態(tài)啦桌,這個狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時(shí)間溯壶,時(shí)間成本相對較高,這也是為什么早期的synchronized效率低的原因甫男。慶幸的是在Java 6之后Java官方對從JVM層面對synchronized較大優(yōu)化且改,所以現(xiàn)在的synchronized鎖效率也優(yōu)化得很不錯了,Java 6之后板驳,為了減少獲得鎖和釋放鎖所帶來的性能消耗又跛,引入了輕量級鎖和偏向鎖。
4. 注意事項(xiàng)及總結(jié)
內(nèi)部鎖有一些局限性:
- 無法中斷一個正在試圖獲得鎖的線程若治;
- 獲取鎖時(shí)無法設(shè)置超時(shí)時(shí)間慨蓝;
- 每個鎖只有一個單一的條件,這對于復(fù)雜的場景端幼,可能不夠礼烈;
最后,我們來總結(jié)下:
- Java的內(nèi)置鎖其實(shí)就是一種互斥鎖静暂,這意味著最多只有一個線程能持有這種鎖济丘,因此谱秽,由這個鎖保護(hù)的同步代碼塊會以原子方式執(zhí)行洽蛀,多個線程在執(zhí)行該代碼塊時(shí)也不會相互干擾。
- synchronized只能防止多個線程訪問同一個對象的同步代碼塊疟赊,如果是多個對象的話郊供,那其實(shí)synchronized就沒什么作用。還有一點(diǎn)近哟,synchronized鎖的是對象驮审,而不是代碼塊,這點(diǎn)要注意下。
- 對于synchronized方法或者synchronized代碼塊疯淫,當(dāng)出現(xiàn)異常時(shí)地来,JVM會自動釋放當(dāng)前線程占用的鎖,異常也只會在當(dāng)前線程拋出熙掺,不會影響到其他線程未斑,因此不會由于異常導(dǎo)致出現(xiàn)死鎖現(xiàn)象。
- 我們在用synchronized的時(shí)候币绩,要注意減小鎖的粒度蜡秽,也就是能減少同步代碼塊的范圍就盡量減小,能在代碼塊加同步就不要在整個方法上加同步缆镣。
本文參考自:
海子-Java并發(fā)編程:synchronized
Java并發(fā)編程:Synchronized及其實(shí)現(xiàn)原理
《Java并發(fā)編程實(shí)戰(zhàn)》