上一篇<<Java高併發編程一--什麼是線程>>java
Synchronized是Java中解決併發問題的一種最經常使用的方法,也是最簡單的一種方法。Synchronized的做用主要有三個:(1)確保線程互斥的訪問同步代碼(2)保證共享變量的修改可以及時可見(3)有效解決重排序問題。從語法上講,Synchronized總共有三種用法:git
接下來我就經過幾個例子程序來講明一下這三種使用方式(爲了便於比較,三段代碼除了Synchronized的使用方式不一樣之外,其餘基本保持一致)。編程
package com.paddx.test.concurrent; public class SynchronizedTest { public void method1(){ System.out.println("Method 1 start"); try { System.out.println("Method 1 execute"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end"); } public void method2(){ System.out.println("Method 2 start"); try { System.out.println("Method 2 execute"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end"); } public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); new Thread(new Runnable() { @Override public void run() { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run() { test.method2(); } }).start(); } } /*執行結果以下,線程1和線程2同時進入執行狀態,線程2執行速度比線程1快, 因此線程2先執行完成,這個過程當中線程1和線程2是同時執行的。 Method 1 start Method 1 execute Method 2 start Method 2 execute Method 2 end Method 1 end*/
package com.paddx.test.concurrent; public class SynchronizedTest { public synchronized void method1(){ System.out.println("Method 1 start"); try { System.out.println("Method 1 execute"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end"); } public synchronized void method2(){ System.out.println("Method 2 start"); try { System.out.println("Method 2 execute"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end"); } public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); new Thread(new Runnable() { @Override public void run() { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run() { test.method2(); } }).start(); } } /*執行結果以下,跟代碼段一比較,能夠很明顯的看出,線程2須要等待線程1的method1執行完成才能開始執行method2方法。 Method 1 start Method 1 execute Method 1 end Method 2 start Method 2 execute Method 2 end*/
package com.paddx.test.concurrent; public class SynchronizedTest { public static synchronized void method1(){ System.out.println("Method 1 start"); try { System.out.println("Method 1 execute"); Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end"); } public static synchronized void method2(){ System.out.println("Method 2 start"); try { System.out.println("Method 2 execute"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end"); } public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); final SynchronizedTest test2 = new SynchronizedTest(); new Thread(new Runnable() { @Override public void run() { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run() { test2.method2(); } }).start(); } } /*執行結果以下,對靜態方法的同步本質上是對類的同步 (靜態方法本質上是屬於類的方法,而不是對象上的方法), 因此即便test和test2屬於不一樣的對象,可是它們都屬於SynchronizedTest類的實例, 因此也只能順序的執行method1和method2,不能併發執行。 Method 1 start Method 1 execute Method 1 end Method 2 start Method 2 execute Method 2 end*/
四、代碼塊同步安全
package com.paddx.test.concurrent; public class SynchronizedTest { public void method1(){ System.out.println("Method 1 start"); try { synchronized (this) { System.out.println("Method 1 execute"); Thread.sleep(3000); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 end"); } public void method2(){ System.out.println("Method 2 start"); try { synchronized (this) { System.out.println("Method 2 execute"); Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 2 end"); } public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); new Thread(new Runnable() { @Override public void run() { test.method1(); } }).start(); new Thread(new Runnable() { @Override public void run() { test.method2(); } }).start(); } } /*執行結果以下,雖然線程1和線程2都進入了對應的方法開始執行,可是線程2在進入同步塊以前, 須要等待線程1中同步塊執行完成。 Method 1 start Method 1 execute Method 2 start Method 1 end Method 2 execute Method 2 end*/
若是對上面的執行結果還有疑問,也先不用急,咱們先來了解Synchronized的原理,再回頭上面的問題就一目瞭然了。咱們先經過反編譯下面的代碼來看看Synchronized是如何實現對代碼塊進行同步的:多線程
package com.paddx.test.concurrent; public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("Method 1 start"); } } }
關於這兩條指令的做用,咱們直接參考JVM規範中描述:併發
monitorenter :ide
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.函數
這段話的大概意思爲:高併發
每一個對象有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的全部權,過程以下:this
一、若是monitor的進入數爲0,則該線程進入monitor,而後將進入數設置爲1,該線程即爲monitor的全部者。
二、若是線程已經佔有該monitor,只是從新進入,則進入monitor的進入數加1.
3.若是其餘線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲0,再從新嘗試獲取monitor的全部權。
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.
這段話的大概意思爲:
執行monitorexit的線程必須是objectref所對應的monitor的全部者。
指令執行時,monitor的進入數減1,若是減1後進入數爲0,那線程退出monitor,再也不是這個monitor的全部者。其餘被這個monitor阻塞的線程能夠嘗試去獲取這個 monitor 的全部權。
經過這兩段描述,咱們應該能很清楚的看出Synchronized的實現原理,Synchronized的語義底層是經過一個monitor的對象來完成,其實wait/notify等方法也依賴於monitor對象,這就是爲何只有在同步的塊或者方法中才能調用wait/notify等方法,不然會拋出java.lang.IllegalMonitorStateException的異常的緣由。
咱們再來看一下同步方法的反編譯結果:
package com.paddx.test.concurrent; public class SynchronizedMethod { public synchronized void method() { System.out.println("Hello World!"); } }
從反編譯的結果來看,方法的同步並無經過指令monitorenter和monitorexit來完成(理論上其實也能夠經過這兩條指令來實現),不過相對於普通方法,其常量池中多了ACC_SYNCHRONIZED標示符。JVM就是根據該標示符來實現方法的同步的:當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先獲取monitor,獲取成功以後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其餘任何線程都沒法再得到同一個monitor對象。 其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需經過字節碼來完成。
有了對Synchronized原理的認識,再來看上面的程序就能夠迎刃而解了。
一、代碼段2結果:
雖然method1和method2是不一樣的方法,可是這兩個方法都進行了同步,而且是經過同一個對象去調用的,因此調用以前都須要先去競爭同一個對象上的鎖(monitor),也就只能互斥的獲取到鎖,所以,method1和method2只能順序的執行。
二、代碼段3結果:
雖然test和test2屬於不一樣對象,可是test和test2屬於同一個類的不一樣實例,因爲method1和method2都屬於靜態同步方法,因此調用的時候須要獲取同一個類上monitor(每一個類只對應一個class對象),因此也只能順序的執行。
三、代碼段4結果:
對於代碼塊的同步實質上須要獲取Synchronized關鍵字後面括號中對象的monitor,因爲這段代碼中括號的內容都是this,而method1和method2又是經過同一的對象去調用的,因此進入同步塊以前須要去競爭同一個對象上的鎖,所以只能順序執行同步塊
能夠看下例子項目<<碼雲git _oo9>>
若一個程序或子程序能夠「在任意時刻被中斷而後操做系統調度執行另一段代碼,這段代碼又調用了該子程序不會出錯」,則稱其爲可重入(reentrant或re-entrant)的。即當該子程序正在運行時,執行線程能夠再次進入並執行它,仍然得到符合設計時預期的結果。與多線程併發執行的線程安全不一樣,可重入強調對單個線程執行時從新進入同一個子程序仍然是安全的。
通常而言,可重入的函數必定是線程安全的,反之則不必定成立。在不加鎖的前提下,若是一個函數用到了全局或靜態變量,那麼它不是線程安全的,也不是可重入的。若是咱們加以改進,對全局變量的訪問加鎖,此時它是線程安全的但不是可重入的,由於一般的枷鎖方式是針對不一樣線程的訪問(如Java的synchronized),當同一個線程屢次訪問就會出現問題。只有當函數知足可重入的四條條件時,纔是可重入的。
咱們回來看synchronized,synchronized擁有強制原子性的內部鎖機制,是一個可重入鎖。所以,在一個線程使用synchronized方法時調用該對象另外一個synchronized方法,即一個線程獲得一個對象鎖後再次請求該對象鎖,是永遠能夠拿到鎖的。
在Java內部,同一個線程調用本身類中其餘synchronized方法/塊時不會阻礙該線程的執行,同一個線程對同一個對象鎖是可重入的,同一個線程能夠獲取同一把鎖屢次,也就是能夠屢次重入。緣由是Java中線程得到對象鎖的操做是以線程爲單位的,而不是以調用爲單位的。
Synchronized是Java併發編程中最經常使用的用於保證線程安全的方式,其使用相對也比較簡單。可是若是可以深刻了解其原理,對監視器鎖等底層知識有所瞭解,一方面能夠幫助咱們正確的使用Synchronized關鍵字,另外一方面也可以幫助咱們更好的理解併發編程機制,有助咱們在不一樣的狀況下選擇更優的併發策略來完成任務。對平時遇到的各類併發問題,也可以從容的應對。