對於synchronized你們應該都很熟悉,主要做用是在多線程併發時,保證線程訪問共享數據時的線程安全。html
它的做用有三點:java
它的用法主要是從兩個維度上來區分:安全
這個對象是新建的,跟其餘對象無關:數據結構
public class SynchronizeDemo implements Runnable {
@Override
public void run() {
test1();
}
private void test1(){
System.out.println(Thread.currentThread().getName() + "_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (new SynchronizeDemo()){
try {
System.out.println(Thread.currentThread().getName() + "_start_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "_end_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
SynchronizeDemo sd1 = new SynchronizeDemo();
Thread thread1 = new Thread(new SynchronizeDemo(),"thread1");
Thread thread2 = new Thread(new SynchronizeDemo(),"thread2");
Thread thread3 = new Thread(sd1,"thread3");
Thread thread4 = new Thread(sd1,"thread4");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
複製代碼
運行結果如圖多線程
四個線程同時開始,同時結束,由於做爲鎖的對象與線程是屬於不一樣的實例併發
無所謂哪一個類,都會被攔截oracle
private void test2(){
System.out.println(Thread.currentThread().getName() + "_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (SynchronizeDemo.class){
try {
System.out.println(Thread.currentThread().getName() + "_start_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "_end_: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製代碼
運行結果以下:jvm
能夠看到,類鎖一次只能經過一個。ide
就是把synchronized (SynchronizeDemo.class)改成synchronized (this)工具
控制檯打印結果
可能這顯示結果有點歧義,其實多運行幾回咱們會發現,1和2是同時結束的,3和4永遠有前後,由於3,4同屬於一個實例
private synchronized void test4(){
...
}
複製代碼
打印的結果以下:
thread1_: 22:42:04
thread3_: 22:42:04
thread2_: 22:42:04
thread3_start_: 22:42:04
thread1_start_: 22:42:04
thread2_start_: 22:42:04
thread1_end_: 22:42:06
thread3_end_: 22:42:06
thread2_end_: 22:42:06
thread4_: 22:42:06
thread4_start_: 22:42:06
thread4_end_: 22:42:08
複製代碼
對於非靜態方法,同一個實例的線程訪問會被攔截,非同一實例能夠同時訪問,即此時默認的就是對象鎖(this)
在上面方法上加static
thread1_: 22:42:42
thread1_start_: 22:42:42
thread1_end_: 22:42:44
thread4_: 22:42:44
thread4_start_: 22:42:44
thread4_end_: 22:42:46
thread3_: 22:42:46
thread3_start_: 22:42:46
thread3_end_: 22:42:48
thread2_: 22:42:48
thread2_start_: 22:42:48
thread2_end_: 22:42:50
複製代碼
同樣的能夠看出來,靜態方法默認使用的就是類鎖
實際上,在JVM中,只區分兩種不一樣的用法,修飾代碼塊與修飾方法,咱們能夠查看SE8規範,docs.oracle.com/javase/spec…
多說無益,直接看它的字節碼
public class Test {
public static void main(String[] args) {
}
public synchronized void test1() {
}
public void test2() {
synchronized (this) {
}
}
}
複製代碼
最簡單的程序,經過使用javap -v Test.class
來查看它的字節碼(注意是class文件,不是java文件)
public synchronized void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this LTest;
public void test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter //監視器進入,獲取鎖
4: aload_1
5: monitorexit //監視器退出,釋放鎖
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return
複製代碼
能夠看到,果真字節碼中,synchronized修飾代碼塊時,是使用monitorenter
和monitorexit
來控制,而synchronized修飾方法的時候,是使用ACC_SYNCHRONIZED
標識。
本質上都是對一個對象的monitor進行獲取,而這個獲取的過程是排他的,也就是同一時刻只能有一個線程得到同步塊對象的監視器monitor。
線程執行到monitorenter指令時,會嘗試獲取對象所對應的monitor全部權,也就是嘗試獲取鎖,執行到monitorexit,也就是釋放全部權,釋放鎖。
要想理清synchronized的鎖的原理,須要掌握兩個重要的概念:
在Hotspot虛擬機中,對象在內存中的存儲佈局,能夠分爲三塊:對象頭Header,實例數據Instance Data,對齊填充Padding。
Hotspot虛擬機的對象頭包含了兩部分信息:
32位HotSpot虛擬機的對象頭存儲結構以下
爲了驗證上圖的正確,咱們能夠查看hotspot的源碼
在線地址:hg.openjdk.java.net/jdk8u/jdk8u…
public:
// Constants
enum { age_bits = 4,//分代年齡
lock_bits = 2,//鎖標識
biased_lock_bits = 1,//是否偏向鎖
max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits,//hask
cms_bits = LP64_ONLY(1) NOT_LP64(0),
epoch_bits = 2//偏向時間戳
};
複製代碼
hash:保存對象的哈希碼
age:對象的分代年齡
biased_lock:偏向鎖標識位
lock:鎖狀態標識位
JavaThread*:保存持有偏向鎖的線程ID
epoch:保存偏向時間戳
因此,對象頭中的Mark Word,synchronized源碼就是用了對象頭中的Mark Word來標識對象加鎖狀態。
Monitor Record是線程私有的數據結構,每個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。每個被鎖住的對象都會和一個monitor record關聯(對象頭的MarkWord中的LockWord指向monitor record的起始地址),同時monitor record中有一個Owner字段存放擁有該鎖的線程的惟一標識,表示該鎖被這個線程佔用。以下圖所示爲Monitor Record的內部結構
Candidate:用來避免沒必要要的阻塞或等待線程喚醒,由於每一次只有一個線程可以成功擁有鎖,若是每次前一個釋放鎖的線程喚醒全部正在阻塞或等待的線程,會引發沒必要要的上下文切換(從阻塞到就緒而後由於競爭鎖失敗又被阻塞)從而致使性能嚴重降低。Candidate只有兩種可能的值0表示沒有須要喚醒的線程1表示要喚醒一個繼任線程來競爭
簡單總結一下,同步塊使用monitorenter和monitorexit指令,而同步方法是依靠方法修飾符上的flag——ACC_SYNCHRONIZED來完成的。其本質都是對一個對象監視器monitor進行獲取,這個獲取過程是排他的,也就是同一時刻只能有一個線程得到由synchronized所保護的對象的監視器。而這個監視器,也能夠理解爲一個同步工具,它是由java對象進行描述的,在Hotspor中,是經過ObjectMonitor來實現,每一個對象中自然都內置了一個ObjectMonitor對象。
在java中,synchronized在編譯後,會在同步塊的先後分別造成一個monitorenter和monitorexit這兩個字節碼指令,這兩個字節碼都須要一個reference類型的參數來指明要鎖定和解鎖的對象,若是java程序中明確指定了對象,那就是這個對象的reference,若是沒有指明,那麼根據synchronized修飾的是實例方法仍是類方法,去取對應的對象實例或者類Class對象來作鎖對象。
在執行monitorenter時,首先會嘗試獲取對象的鎖,若是這個對象沒有鎖,或者當前線程已經擁有了這個對象的鎖,那個鎖的計數器加1,相應的,在執行monitorexit時指令時,會將鎖計數器減1,當計數器爲0時,這個鎖就被釋放。若是獲取對象鎖失敗,那當前線程就要阻塞等待,直到對象鎖被另外一個線程釋放爲止。
synchronized同步塊對同一線程來講是可重入的,不會出現本身把本身鎖死的狀況,其次,同步塊在已進入的線程執行完成前,會阻塞後面的其餘線程進入。咱們知道,Java的線程是映射到操做系統中的的原生線程上的,若是要阻塞或者喚醒一個線程,都須要操做系統來幫忙,這就須要咱們從用戶態切換到核心態,所以這個狀態轉換是很是耗費CPU。若是這個代碼很是簡單的同步塊,可能切換狀態的時間比代碼執行時間還長。因此synchronized是一個重量級的操做,虛擬機自己也作了大量的優化,引入了偏向鎖,輕量級鎖,重量級鎖等,這一部分鎖的升級,能夠等之後有時間了,再慢慢探討。固然還能夠引入重入鎖,解決synchronized過於重量的問題。
參考
《深刻理解Java虛擬機》
下面是個人公衆號,歡迎你們關注我