synchronized 這個關鍵字,我相信對於並發編程有一定了解的人,一定會特別熟悉,對於一些可能在多線程環境下可能會有並發問題的代碼,或者方法,直接加上synchronized,問題就搞定了。
但是用歸用,你明白它為什么要這么用?為什么就能解決我們所說的線程安全問題?
下面,可樂將和大家一起深入的探討這個關鍵字用法。
1、示例代碼結果?
首先大家看一段代碼,大家想想最后的打印count結果是多少?
1 package com.ys.algorithmproject.leetcode.demo.concurrency.synchronizedtest; 2 3 4 /** 5 * Create by ItCoke 6 */ 7 public class SynchronizedTest implements Runnable{ 8 9 public static int count = 0; 10 11 @Override 12 public void run() { 13 addCount(); 14 15 } 16 17 public void addCount(){ 18 int i = 0; 19 while (i++ < 100000) { 20 count++; 21 } 22 } 23 24 public static void main(String[] args) throws Exception{ 25 SynchronizedTest obj = new SynchronizedTest(); 26 Thread t1 = new Thread(obj); 27 Thread t2 = new Thread(obj); 28 t1.start(); 29 t2.start(); 30 t1.join(); 31 t2.join(); 32 System.out.println(count); 33 34 } 35 36 37 }
代碼很簡單,主線程中啟動兩個線程t1和t2,分別調用 addCount() 方法,將count的值都加100000,然后調用 join() 方法,表示主線程等待這兩個線程執行完畢。最后打印 count 的值。
應該沒有答案一定是 200000 的同學吧,很好,大家都具備一定的並發知識。
這題的答案是一定小於等於 200000,至於原因也很好分析,比如 t1線程獲取count的值為0,然后執行了加1操作,但是還沒來得及同步到主內存,這時候t2線程去獲取主內存的count值,發現還是0,然后繼續自己的加1操作。也就是t1和t2都執行了加1操作,但是最后count的值依然是1。
那么我們應該如何保證結果一定是 200000呢?答案就是用 synchronized。
2、修飾代碼塊
直接上代碼:

1 package com.ys.algorithmproject.leetcode.demo.concurrency.synchronizedtest; 2 3 4 /** 5 * Create by ItCoke 6 */ 7 public class SynchronizedTest implements Runnable{ 8 9 public static int count = 0; 10 11 private Object objMonitor = new Object(); 12 13 @Override 14 public void run() { 15 addCount(); 16 17 } 18 19 public void addCount(){ 20 synchronized (objMonitor){ 21 int i = 0; 22 while (i++ < 100000) { 23 count++; 24 } 25 } 26 27 } 28 29 public static void main(String[] args) throws Exception{ 30 SynchronizedTest obj = new SynchronizedTest(); 31 Thread t1 = new Thread(obj); 32 Thread t2 = new Thread(obj); 33 t1.start(); 34 t2.start(); 35 t1.join(); 36 t2.join(); 37 System.out.println(count); 38 39 } 40 41 42 }
我們在 addCount 方法體中增加了一個 synchronized 代碼塊,將里面的 while 循環包括在其中,保證同一時刻只能有一個線程進入這個循環去改變count的值。
3、修飾普通方法

1 package com.ys.algorithmproject.leetcode.demo.concurrency.synchronizedtest; 2 3 4 /** 5 * Create by ItCoke 6 */ 7 public class SynchronizedTest implements Runnable{ 8 9 public static int count = 0; 10 11 private Object objMonitor = new Object(); 12 13 @Override 14 public void run() { 15 addCount(); 16 17 } 18 19 public synchronized void addCount(){ 20 int i = 0; 21 while (i++ < 100000) { 22 count++; 23 } 24 25 } 26 27 public static void main(String[] args) throws Exception{ 28 SynchronizedTest obj = new SynchronizedTest(); 29 Thread t1 = new Thread(obj); 30 Thread t2 = new Thread(obj); 31 t1.start(); 32 t2.start(); 33 t1.join(); 34 t2.join(); 35 System.out.println(count); 36 37 } 38 39 40 }
對比上面修飾代碼塊,直接將 synchronized 加到 addCount 方法中,也能解決線程安全問題。
4、修飾靜態方法
這個我們就不貼代碼演示了,將 addCount() 聲明為一個 static 修飾的方法,然后在加上 synchronized ,也能解決線程安全問題。
5、原子性、可見性、有序性
通過 synchronized 修飾的方法或代碼塊,能夠同時保證這段代碼的原子性、可見性和有序性,進而能夠保證這段代碼的線程安全。
比如通過 synchronized 修飾的代碼塊:
其中 objMonitor 表示鎖對象(下文會介紹這個鎖對象),只有獲取到這個鎖對象之后,才能執行里面的代碼,執行完畢之后,在釋放這個鎖對象。那么同一時刻就會只有一個線程去執行這段代碼,把多線程變成了單線程,當然不會存在並發問題了。
這個過程,大家可以想象在公司排隊上廁所的情景。
對於原子性,由於同一時刻單線程操作,肯定能夠保證原子性。
對於有序性,在JMM內存模型中的Happens-Before規定如下,所以也是能夠保證有序性的。
程序的順序性規則(Program Order Rule):在一個線程內,按照控制流順序,書寫在前面的操作先行發生於書寫在后面的操作。
最后對於可見性,JMM內存模型也規定了:
對一個變量執行unlock操作之前,必須先把此變量同步回主內存中(執行store、write操作)。
大家可能會奇怪,synchronized 並沒有lock和unlock操作啊,怎么也能夠保證可見性,大家不要急,其實JVM對於這個關鍵字已經隱式的實現了,下文看字節碼會明白的。
6、鎖對象
大家要注意,我在通過synchronized修飾同步代碼塊時,使用了一個 Object 對象,名字叫 objMonitor。而對於修飾普通方法和靜態方法時,只是在方法聲明時說明了,並沒有鎖住什么對象,其實這三者都有各自的鎖對象,只有獲取了鎖對象,線程才能進入執行里面的代碼。
1、修飾代碼塊:鎖定鎖的是synchonized括號里配置的對象 2、修飾普通方法:鎖定調用當前方法的this對象 3、修飾靜態方法:鎖定當前類的Class對象
多個線程之間,如果要通過 synchronized 保證線程安全,獲取的要是同一把鎖。如果多個線程多把鎖,那么就會有線程安全問題。如下:

1 package com.ys.algorithmproject.leetcode.demo.concurrency.synchronizedtest; 2 3 4 /** 5 * Create by ItCoke 6 */ 7 public class SynchronizedTest implements Runnable{ 8 9 public static int count = 0; 10 11 12 13 @Override 14 public void run() { 15 addCount(); 16 17 } 18 19 public void addCount(){ 20 Object objMonitor = new Object(); 21 synchronized(objMonitor){ 22 int i = 0; 23 while (i++ < 100000) { 24 count++; 25 } 26 } 27 } 28 29 public static void main(String[] args) throws Exception{ 30 SynchronizedTest obj = new SynchronizedTest(); 31 Thread t1 = new Thread(obj); 32 Thread t2 = new Thread(obj); 33 t1.start(); 34 t2.start(); 35 t1.join(); 36 t2.join(); 37 System.out.println(count); 38 39 } 40 41 42 }
我們把原來的鎖 objMonitor 對象從全局變量移到 addCount() 方法中,那么每個線程進入每次進入addCount() 方法都會新建一個 objMonitor 對象,也就是多個線程用多把鎖,肯定會有線程安全問題。
7、可重入
可重入什么意思?字面意思就是一個線程獲取到這個鎖了,在未釋放這把鎖之前,還能進入獲取鎖,如下:
在 addCount() 方法的 synchronized 代碼塊中繼續調用 printCount() 方法,里面也有一個 synchronized ,而且都是獲取的同一把鎖——objMonitor。
synchronized 是能夠保證這段代碼正確運行的。至於為什么具有這個特性,可以看下文的實現原理。
8、實現原理
對於如下這段代碼:

1 package com.ys.algorithmproject.leetcode.demo.concurrency.synchronizedtest; 2 3 /** 4 * Create by YSOcean 5 */ 6 public class SynchronizedByteClass { 7 Object objMonitor = new Object(); 8 9 public synchronized void method1(){ 10 System.out.println("Hello synchronized 1"); 11 } 12 13 public synchronized static void method2(){ 14 System.out.println("Hello synchronized 2"); 15 } 16 17 public void method3(){ 18 synchronized(objMonitor){ 19 System.out.println("Hello synchronized 2"); 20 } 21 22 } 23 24 public static void main(String[] args) { 25 26 } 27 }
我們可以通過兩種方法查看其class文件的匯編代碼。
①、IDEA下載 jclasslib 插件
然后點擊 View——Show Bytecode With jclasslib
②、通過 javap 命令
javap -v 文件名(不要后綴)
注意:這里生成匯編的命令是根據編譯之后的字節碼文件(class文件),所以要先編譯。
③、修飾代碼塊匯編代碼
我們直接看method3() 的匯編代碼:
對於上圖出現的 monitorenter 和 monitorexit 指令,我們查看 JVM虛擬機規范:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html,可以看到對這兩個指令的介紹。
下面我們說明一下這兩個指令:
一、monitorenter
每個對象與一個監視器鎖(monitor)關聯。當monitor被占用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:
1、如果 monitor的進入數為0,則該線程進入monitor,然后將進入數設置為1,該線程即為monitor的所有者。
2、如果線程已經占有該monitor,只是重新進入,則進入monitor的進入數加1.
3.如果其他線程已經占用了monitor,則該線程進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權。
二、monitorexit
執行monitorexit的線程必須是object ref所對應的monitor的所有者。
指令執行時,monitor的進入數減1,如果減1后進入數為0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個 monitor 的所有權。
通過上面介紹,我們可以知道 synchronized 底層就是通過這兩個命令來執行的同步機制,由此我們也可以看出synchronized 具有可重入性。
③、修飾普通方法和靜態方法匯編代碼
可以看到都是通過指令 ACC_SYNCHRONIZED 來控制的,雖然沒有看到方法的同步並沒有通過指令monitorenter和monitorexit來完成,但其本質也是通過這兩條指令來實現。
當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設置,如果設置了,執行線程將先獲取monitor,獲取成功之后才能執行方法體,方法執行完后再釋放monitor。在方法執行期間,其他任何線程都無法再獲得同一個monitor對象。 其實和修飾代碼塊本質上沒有區別,只是方法的同步是一種隱式的方式來實現。
9、異常自動unlock
可能會有細心的朋友發現,我在介紹 synchronized 修飾代碼塊時,給出的匯編代碼,用紅框圈住了兩個 monitorexit,根據我們前面介紹,獲取monitor加1,退出monitor減1,等於0時,就沒有鎖了。那為啥會有兩個 monitorexit,而只有一個 monitorenter 呢?
第 6 行執行 monitorenter,然后第16行執行monitorexit,然后執行第17行指令 goto 25,表示跳到第25行代碼,第25行是 return,也就是直接結束了。
那第20-24行代碼中是什么意思呢?其中第 24 行指令 athrow 表示Java虛擬機隱式處理方法完成異常結束時的監視器退出,也就是執行發生異常了,然后去執行 monitorexit。
進而可以得到結論:
synchronized 修飾的方法或代碼塊,在執行過程中拋出異常了,也能釋放鎖(unlock)
我們可以看如下方法,手動拋出異常:
然后獲取其匯編代碼,就只有一個 monitorexit 指令了。