同步關鍵字synchronized
java關鍵字synchronized用來標記方法或者代碼塊是同步的。它是Java中解決並發問題的一種最常用的方法,作用主要有:(1)確保線程互斥的訪問同步代碼(2)保證共用變數的修改能夠及時可見。
synchronized總共有四種用法:
(1)修飾普通方法
(2)修飾靜態方法
(3)修飾普通方法中的代碼塊
(4)修飾靜態方法中的代碼塊 修飾普通方法
public class SynchronizedTest { //方法1,執行3秒 public synchronized void method1() { System.out.println("method1 start"); try { System.out.println("method1 execute"); Thread.sleep(3000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("method1 end"); } //方法2,執行1秒 public synchronized void method2() { System.out.println("method2 start"); try { System.out.println("method2 execute"); Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("method2 end"); } public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); //線程1 new Thread(new Runnable() { @Override public void run() { test.method1(); } }).start(); //線程2 new Thread(new Runnable() { @Override public void run() { test.method2(); } }).start(); }}
輸出:
method1 startmethod1 executemethod1 endmethod2 startmethod2 executemethod2 end
synchronized 關鍵字告訴Java該方法是同步的。同步,是同步在擁有該方法的對象上。對於一個執行個體來說,只有一個線程能夠在這個方法同步塊中運行。如果有多個執行個體存在,每一個執行個體對應一個線程。
在本例中,線程2需要等待線程1的method1執行完成才能開始執行method2方法。因為對於test執行個體來說,同一時間只有一個線程能夠運行它的同步方法。
如果把本例中的synchronized 關鍵字去掉,輸出為:
method1 startmethod1 executemethod2 startmethod2 executemethod2 endmethod1 end
因為線程1和線程2同時進入執行狀態,方法2執行速度比方法1快,所以方法2先執行完成,這個過程中線程1和線程2是同時執行的。 修飾靜態方法
public class SynchronizedTest { //方法1,執行3秒 public static synchronized void method1() { System.out.println("method1 start"); try { System.out.println("method1 execute"); Thread.sleep(3000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("method1 end"); } //方法2,執行1秒 public static synchronized void method2() { System.out.println("method2 start"); try { System.out.println("method2 execute"); Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("method2 end"); } public static void main(String[] args) { final SynchronizedTest test1 = new SynchronizedTest(); final SynchronizedTest test2 = new SynchronizedTest(); //線程1 new Thread(new Runnable() { @Override public void run() { test1.method1(); } }).start(); //線程2 new Thread(new Runnable() { @Override public void run() { test2.method2(); } }).start(); }}
輸出:
method1 startmethod1 executemethod1 endmethod2 startmethod2 executemethod2 end
對靜態方法的同步本質上是對類的同步(靜態方法本質上是屬於類的方法,而不是對象上的方法),所以即使test1和test2屬於不同的對象,但是它們都屬於SynchronizedTest類的執行個體,所以也只能挨個順序執行線程,不能並發執行。 修飾普通方法中的代碼塊
有時你不需要同步整個方法,而是同步方法中的一部分。
public class SynchronizedTest { //方法1,執行3秒 public void method1() { System.out.println("method1 start"); try { synchronized(this){ System.out.println("method1 execute"); Thread.sleep(3000); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("method1 end"); } //方法2,執行1秒 public void method2() { System.out.println("method2 start"); try { synchronized(this){ System.out.println("method2 execute"); Thread.sleep(1000); } } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("method2 end"); } public static void main(String[] args) { final SynchronizedTest test = new SynchronizedTest(); //線程1 new Thread(new Runnable() { @Override public void run() { test.method1(); } }).start(); //線程2 new Thread(new Runnable() { @Override public void run() { test.method2(); } }).start(); }}
輸出:
method1 startmethod1 executemethod2 startmethod1 endmethod2 executemethod2 end
雖然線程1和線程2都進入了對應的方法開始執行,但是線程2在進入同步塊之前,需要等待線程1中同步塊執行完成。
在同步構造器中用括弧括起來的對象叫做監視器對象。在上例中,使用了this,即為調用該方法的執行個體本身。上述代碼使用監視器對象同步,並使用調用方法本身的執行個體作為監視器對象。同一時間只有一個線程能夠運行同一個監視器對象的同步塊代碼。 修飾靜態方法中的代碼塊
這和“修飾普通方法中的代碼塊”非常類似,只需要把監視器對象改為類對象即可。以上邊的程式為例,把synchronized(this)改為synchronized(SynchronizedTest.class)即可。 synchronized原理 同步塊程式的反編譯
我們先通過反編譯下面的代碼來看看Synchronized是如何?對同步塊進行同步的:
package com.paddx.test.concurrent;public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("Method 1 start"); } }}
反編譯結果:
紅色框中的這兩條指令(monitorenter和monitorexit)的作用是什麼。先看monitorenter :
每個對象有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試擷取monitor的所有權,過程如下:
(1)如果monitor的進入數為0,則該線程進入monitor,然後將進入數設定為1,該線程即為monitor的所有者。
(2)如果線程已經佔有該monitor,只是重新進入,則進入monitor的進入數加1。
(3)如果其他線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數為0,再重新嘗試擷取monitor的所有權。
再看monitorexit:
執行monitorexit的線程必須是objectref所對應的monitor的所有者。
指令執行時,monitor的進入數減1,如果減1後進入數為0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去擷取這個monitor 的所有權。
通過上邊兩段描述,我們可以看出在同步代碼塊中synchronized的實現原理,是通過一個monitor的對象來完成,其實wait/notify等方法也依賴於monitor對象,這就是為什麼只有在同步的塊或者方法中才能調用wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException異常的原因。 同步方法的反編譯:
從反編譯的結果來看,方法的同步並沒有通過指令monitorenter和monitorexit來完成(理論上其實也可以通過這兩條指令來實現),不過其常量池中多了ACC_SYNCHRONIZED標示符。JVM就是根據該標示符來實現方法的同步的,具體的:
當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設定,如果設定了,執行線程將先擷取monitor,擷取成功之後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其他任何線程都無法再獲得同一個monitor對象。 其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過位元組碼來完成。 回顧
有了對Synchronized原理的認識,再來看上面的程式就可以迎刃而解了。
(1)“修飾普通方法”一節中的例子:
雖然method1和method2是不同的方法,但是這兩個方法都進行了同步,並且是通過同一個對象去調用的,所以調用之前都需要先去競爭同一個對象上的鎖(monitor),也就只能互斥的擷取到鎖,因此,method1和method2隻能順序的執行。
(2)“修飾靜態方法”一節中的例子:
雖然test1和test2屬於不同對象,但是test1和test2屬於同一個類的不同執行個體,由於method1和method2都屬於靜態同步方法,所以調用的時候需要擷取同一個類上的monitor(每個類只對應一個class對象),所以也只能順序的執行。
(3)“修飾普通方法中的代碼塊”一節的例子:
對於代碼塊的同步實質上需要擷取synchronized關鍵字後面括弧中對象的monitor,由於這段代碼中括弧的內容都是this,而method1和method2又是通過同一的對象去調用的,所以進入同步塊之前需要去競爭同一個對象上的鎖,因此只能順序執行同步塊。
原文來自:並發編程網-Java同步塊
liuxiaopeng-Java並發編程:Synchronized及其實現原理