Valentine 轉載請標明出處。
synchronized的使用
在多線程并發編程中synchronized一直是元老級的角色,很多人都會稱呼它為重量級鎖。但是隨著Java SE 1.6對synchronized進行了各種優化之后,有些情況下它就并不那么重了,Java SE 1.6中為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了偏向鎖和輕量級鎖,以及鎖的存儲結構和升級。
synchronized的使用示例
public class Demo {
private static int count = 0;
private static int count1 = 0;
private static int count2 = 0;
private static void inc() {
synchronized (Demo.class) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
private synchronized void inc1() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count1++;
}
private void inc2() {
synchronized (this) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count2++;
}
}
public static void main(String[] args) throws InterruptedException {
Demo demo = new Demo();
for (int i = 0; i < 1000; i++) {
new Thread(() -> Demo.inc()).start();
/*new Thread(() -> {
demo.inc1();
}).start();
new Thread(() -> {
demo.inc2();
}).start();*/
new Thread(() -> {
Demo demo1 = new Demo();
demo1.inc1();
}).start();
new Thread(() -> {
Demo demo2 = new Demo();
demo2.inc2();
}).start();
}
Thread.sleep(3000);
System.out.println("運行結果:" + count);
System.out.println("運行結果1:" + count1);
System.out.println("運行結果2:" + count2);
}
}
public class SynchronizedDemoTest {
private static Object object = new Object();
public static void main(String[] args) throws Exception {
synchronized (object) {
}
}
public static synchronized void method() {
}
}
輸出結果如圖:
synchronized有三種方式來加鎖,分別是
1、修飾實例方法,作用于當前實例加鎖,進入同步代碼之前要獲得當前實例的鎖
2、修飾靜態方法,作用于當前類對象加鎖,進入同步代碼之前要獲得當前類對象的鎖
3、修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼塊之前要獲得給定對象的鎖
synchronized括號后面的對象
synchronized括號后面的對象是一把鎖,在java中任意一個對象都可以成為鎖,簡單來說,我們把object比喻成一個key,擁有這個key的線程才能執行這個方法,拿到這個key以后在執行方法過程中,這個key是隨身攜帶的,并且只有一把。如果后續的線程想訪問當前方法,因為沒有key所以不能訪問只能在門口等著,等之前的線程把key放回去。所以synchronized鎖定的對象必須是同一個,如果是不同對象,就意味著是不同的房間的要是,對于訪問者來說是沒有任何影響的。
synchronized的字節碼指令
通過javap -v SynchronizedDemoTest .class (會輸出行號、本地變量表信息、反編譯匯編代碼、輸出當前類用到的常量池等信息) 來查看對應的字節碼指令,對于同步塊的實現使用了monitorenter和monitorexit指令,這兩個指令隱式地執行了lock和unlock操作,用于提供原子性的保證。
monitorenter指令插入到同步代碼塊開始的位置,monitorexit指令插入到同步代碼塊結束的位置,jvm需要保證每個monitorenter都有一個monitorexit對應。
這兩個指令,本質上是對一個對象的監視器(monitor)進行獲取,這個過程是排他的,也就是說同一時刻只有一個線程獲得由synchronized所保護對象的監視器。
線程執行到monitorenter指令時,會嘗試獲取對象所對應的monitor所有權,也就是嘗試獲取對象的鎖,而執行monitorexit就是釋放monitor的所有權。
同步代碼塊使用了 monitorenter 和 monitorexit 指令實現。
同步方法中依靠方法修飾符上的 ACC_SYNCHRONIZED 實現。
public static void main(java.lang.String[]) throws java.lang.Exception;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // Field object:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter // 監視器進入,獲取鎖
6: aload_1
7: monitorexit //監視器退出,釋放鎖
8: goto 16
11: astore_2
12: aload_1
13: monitorexit //監視器退出,釋放鎖
14: aload_2
15: athrow
16: return
public static synchronized void method();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 13: 0
synchronized的鎖的原理
jdk1.6以后對synchronized鎖進行了優化,包含偏向鎖、輕量級鎖、重量級鎖,在了解synchronized之前,我們要了解兩個重要的概念,對象頭和monitor。
Java對象頭
在hotspot虛擬機中,對象在內存中的布局分為三塊區域:對象頭、實例數據和對齊填充,Java對象頭是實現synchronized的鎖對象的基礎,一般而言,synchronized使用的鎖對象是存儲在Java對象頭里,它是輕量級鎖和偏向鎖的關鍵。
Mark Word
Mark Word是用于存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程id、偏向時間戳等等。Java對象頭一般占有兩個機器碼(在32位虛擬機中,1個機器碼等于4字節,也就是32bit)
在源碼中的體現
如果想更深入了解對象頭在JVM源碼中的定義,需要關心幾個文件,oop.hpp/markOop.hpp
oop.hpp,每個Java Object在JVM內部都有一個native的C++對象 oop/oopDesc 與之對應,現在oop.hpp中看oopDesc的定義
_mark被生命在oopDesc類的頂部,所以這個_mark可以認為是一個頭部,上面講過頭部保存了一些重要的狀態和標識信息,在markOop.hpp文件中有一些注釋說明markOop的內存布局,如圖
Monitor
monitor可以理解為一個同步工具,也可以描述為一種同步機制。所有的Java對象是天生的monitor,每個object對象里的markOop->monitor()可以保存ObjectMonitor的對象,從源碼層面分析monitor對象:
1、oop.hpp下的oopDesc類是JVM對象的頂級基類,所以每個object對象都包含markOop
2、markOop.hpp中markOopDesc繼承自oopDesc,并擴展了自己的monitor方法,這個方法返回一個ObjectMonitor指針對象
3、objectMonitor.hpp在hotspot虛擬機中,采用ObjectMonitor類來實現monitor,如圖
synchronized的鎖升級和獲取過程
了解了對象以及monitor以后,接下來去分析synchronized的鎖的實現,就比較容易理解了。前面講過synchronized的鎖是進行過優化的,引入了偏向鎖、輕量級鎖,鎖的級別從低到高逐步升級,無鎖->偏向鎖->輕量級鎖->重量級鎖。
自旋鎖(CAS)
自旋鎖就是讓不滿足條件的線程等待一段時間,而不是立即掛起,看持有鎖的線程是否能夠很快釋放鎖,實現自旋的方式其實就是一段沒有任何意義的循環。
雖然它通過占用處理器的時間來避免線程切換帶來的開銷,但是如果持有鎖的線程不能在很快釋放鎖,那么自旋的線程就會浪費處理器的資源,因為它不會做任何有意義的工作。所以,自旋等待的時間或者次數是有一個限度的,如果自旋超過了定義的時間仍然沒有獲取到鎖,則該線程應該被掛起。
偏向鎖
大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一個線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程訪問同步快并獲取鎖的時候,會在對象頭和棧幀中的鎖記錄里面存儲偏向鎖的線程ID,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需要簡單地測試一下對象頭的Mark Word里是否存儲著指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖,如果測試失敗,則需要再測試一下Mark Word中偏向鎖的表示是否設置成1 (表示當前是偏向鎖),如果沒有設置,則使用CAS競爭鎖,如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
輕量級鎖
引入輕量級鎖的主要目的是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。當關閉偏向鎖功能或者多個線程競爭偏向鎖升級為輕量級鎖,則會嘗試獲取輕量級鎖。
重量級鎖
重量級鎖通過對象內部的監視器(monitor)實現,其中monitor的本質是依賴于底層操作系統的Mutex Lock實現,操作系統實現線程之間的切換需要從用戶態到內核態的切換,切換成本非常高。上面在講Java對象頭的時候,講到了monitor這個對象,在hotspot虛擬機中,通過ObjectMonitor類實現monitor,它的鎖的獲取過程的體現會簡單很多。
wait和notify
wait和notify是用來讓線程進入等待狀態以及使得線程喚醒的兩個操作
public class ThreadWait extends Thread {
private final Object lock;
ThreadWait(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println("開始執行 thread wait");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行結束 thread wait");
}
}
}
public class ThreadNotify extends Thread {
private final Object lock;
ThreadNotify(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println("開始執行 thread notify");
lock.notify();
System.out.println("執行結束 thread notify");
}
}
}
public class ThreadWaitNotifyDemo {
public static void main(String[] args) {
Object lock = new Object();
ThreadWait threadWait = new ThreadWait(lock);
threadWait.start();
ThreadNotify threadNotify = new ThreadNotify(lock);
threadNotify.start();
}
}
輸出結果
wait和notify的原理
調用wait方法,首先會獲取監視器鎖,獲得成功后,會讓當前線程進入等待隊列并且釋放鎖;然后當其他線程調用notify或者notifyAll以后,會選擇從等待隊列中喚醒任意一個線程,而執行完notify方法以后,并不會立馬喚醒線程,原因是當前的線程仍然持有這把鎖,處于等待狀態的線程無法獲得鎖,必須要等到當前的線程執行完monitorexit指令后,也就是鎖被釋放后,處于等待隊列中的線程才可以開始競爭鎖,如圖
wait和notify為什么需要在synchronized里面?
wait方法的語義有兩個,一個是釋放當前的對象鎖、另一個是使得當前線程進入阻塞隊列,而這些操作都和監視器是相關的,所以wait必須要獲得一個監視器鎖;而對于notify來說也是一樣,它是喚醒一個線程,既然要去喚醒,首先得知道它在哪里,所以就必須要找到這個對象獲取到這個對象鎖,然后到這個對象的等待隊列中去喚醒一個線程。