《提升能力,漲薪可待》—Java併發之Synchronized
Synchronized簡介
執行緒安全是併發程式設計中的至關重要的,造成執行緒安全問題的主要原因:
-
臨界資源, 存在共享資料
-
多執行緒共同操作共享資料
而Java關鍵字synchronized,為多執行緒場景下防止臨界資源訪問衝突提供支援, 可以保證在同一時刻,只有一個執行緒可以執行某個方法或某個程式碼塊操作共享資料。
即當要執行程式碼使用synchronized關鍵字時,它將檢查鎖是否可用,然後獲取鎖,執行程式碼,最後再釋放鎖。而synchronized有三種使用方式:
-
synchronized方法: synchronized當前例項物件,進入同步程式碼前要獲得當前例項的鎖
-
synchronized靜態方法: synchronized當前類的class物件 ,進入同步程式碼前要獲得當前類物件的鎖
-
synchronized程式碼塊:synchronized括號裡面的物件,對給定物件加鎖,進入同步程式碼庫前要獲得給定物件的鎖
Synchronized方法
首先看一下沒有使用synchronized關鍵字,如下:
public class ThreadNoSynchronizedTest { public void method1(){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("method1"); } public void method2() { System.out.println("method2"); } public static void main(String[] args) { ThreadNoSynchronizedTest tnst= new ThreadNoSynchronizedTest(); Thread t1 = new Thread(new Runnable() { @Override public void run() { tnst.method1(); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { tnst.method2(); } }); t1.start(); t2.start(); } }
在上述的程式碼中,method1比method2多了2s的延時,因此在t1和t2執行緒同時執行的情況下,執行結果:
method2
method1
當method1和method2使用了synchronized關鍵字後,程式碼如下:
public synchronized void method1(){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("method1"); } public synchronized void method2() { System.out.println("method2"); }
此時,由於method1佔用了鎖,因此method2必須要等待method1執行完之後才能執行,執行結果:
method1
method2
因此synchronized鎖定是當前的物件,當前物件的synchronized方法在同一時間只能執行其中的一個,另外的synchronized方法需掛起等待,但不影響非synchronized方法的執行。下面的synchronized方法和synchronized程式碼塊(把整個方法synchronized(this)包圍起來)等價的。
public synchronized void method1(){ } public void method2() { synchronized(this){ } }
Synchronized靜態方法
synchronized靜態方法是作用在整個類上面的方法,相當於把類的class作為鎖,示例程式碼如下:
public class TreadSynchronizedTest { public static synchronized void method1(){ try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("method1"); } public static void method2() { synchronized(TreadTest.class){ System.out.println("method2"); } } public static void main(String[] args) { Thread t1 = new Thread(new Runnable() { @Override public void run() { TreadSynchronizedTest.method1(); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { TreadSynchronizedTest.method2(); } }); t1.start(); t2.start(); } }
由於將class作為鎖,因此method1和method2存在著競爭關係,method2中synchronized(ThreadTest.class)等同於在method2的宣告時void前面直接加上synchronized。上述程式碼的執行結果仍然是先打印出method1的結果:
method1
method2
Synchronized程式碼塊
synchronized程式碼塊應用於處理臨界資源的程式碼塊中,不需要訪問臨界資源的程式碼可以不用去競爭資源,減少了資源間的競爭,提高程式碼效能。示例程式碼如下:
public class TreadSynchronizedTest { private Object obj = new Object(); public void method1(){ System.out.println("method1 start"); synchronized(obj){ try { Thread.sleep(2000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("method1 end"); } } public void method2() { System.out.println("method2 start"); // 延時10ms,讓method1線獲取到鎖obj try { Thread.sleep(10); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } synchronized(obj){ System.out.println("method2 end"); } } public static void main(String[] args) { TreadSynchronizedTest tst = new TreadSynchronizedTest(); Thread t1 = new Thread(new Runnable() { @Override public void run() { tst.method1(); } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { tst.method2(); } }); t1.start(); t2.start(); } }
執行結果如下:
method1 start
method2 start
method1 end
method2 end
上述程式碼中,執行method2方法,先打印出 method2 start, 之後執行同步塊,由於此時obj被method1獲取到,method2只能等到method1執行完成後再執行,因此先列印method1 end,然後在列印method2 end。
Synchronized原理
synchronized 是JVM實現的一種鎖,其中鎖的獲取和釋放分別是monitorenter 和 monitorexit 指令。
加了 synchronized 關鍵字的程式碼段,生成的位元組碼檔案會多出 monitorenter 和 monitorexit 兩條指令,並且會多一個 ACC_SYNCHRONIZED 標誌位,
當方法呼叫時,呼叫指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設定,如果設定了,執行執行緒將先獲取monitor,獲取成功之後才能執行方法體,方法執行完後再釋放monitor。
在方法執行期間,其他任何執行緒都無法再獲得同一個monitor物件。其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過位元組碼來完成。
在Java1.6之後,sychronized在實現上分為了偏向鎖、輕量級鎖和重量級鎖,其中偏向鎖在 java1.6 是預設開啟的,輕量級鎖在多執行緒競爭的情況下會膨脹成重量級鎖,有關鎖的資料都儲存在物件頭中。
-
偏向鎖:在只有一個執行緒訪問同步塊時使用,通過CAS操作獲取鎖
-
輕量級鎖:當存在多個執行緒交替訪問同步快,偏向鎖就會升級為輕量級鎖。當執行緒獲取輕量級鎖失敗,說明存在著競爭,輕量級鎖會膨脹成重量級鎖,當前執行緒會通過自旋(通過CAS操作不斷獲取鎖),後面的其他獲取鎖的執行緒則直接進入阻塞狀態。
-
重量級鎖:鎖獲取失敗則執行緒直接阻塞,因此會有執行緒上下文的切換,效能最差。
鎖優化-適應性自旋(Adaptive Spinning)
從輕量級鎖獲取的流程中我們知道,當執行緒在獲取輕量級鎖的過程中執行CAS操作失敗時,是要通過自旋來獲取重量級鎖的。問題在於,自旋是需要消耗CPU的,如果一直獲取不到鎖的話,那該執行緒就一直處在自旋狀態,白白浪費CPU資源。
其中解決這個問題最簡單的辦法就是指定自旋的次數,例如讓其迴圈10次,如果還沒獲取到鎖就進入阻塞狀態。但是JDK採用了更聰明的方式——適應性自旋,簡單來說就是執行緒如果自旋成功了,則下次自旋的次數會更多,如果自旋失敗了,則自旋的次數就會減少。
鎖優化-鎖粗化(Lock Coarsening)
鎖粗化的概念應該比較好理解,就是將多次連線在一起的加鎖、解鎖操作合併為一次,將多個連續的鎖擴充套件成一個範圍更大的鎖。舉個例子:
public class StringBufferTest { StringBuffer stringBuffer = new StringBuffer(); public void append(){ stringBuffer.append("a"); stringBuffer.append("b"); stringBuffer.append("c"); } }
這裡每次呼叫stringBuffer.append方法都需要加鎖和解鎖,如果虛擬機器檢測到有一系列連串的對同一個物件加鎖和解鎖操作,就會將其合併成一次範圍更大的加鎖和解鎖操作,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖。
鎖優化-鎖消除(Lock Elimination)
鎖消除即刪除不必要的加鎖操作。根據程式碼逃逸技術,如果判斷到一段程式碼中,堆上的資料不會逃逸出當前執行緒,那麼可以認為這段程式碼是執行緒安全的,不必要加鎖。看下面這段程式:
public class SynchronizedTest02 { public static void main(String[] args) { SynchronizedTest02 test02 = new SynchronizedTest02(); for (int i = 0; i < 10000; i++) { i++; } long start = System.currentTimeMillis(); for (int i = 0; i < 100000000; i++) { test02.append("abc", "def"); } System.out.println("Time=" + (System.currentTimeMillis() - start)); } public void append(String str1, String str2) { StringBuffer sb = new StringBuffer(); sb.append(str1).append(str2); } }
雖然StringBuffer的append是一個同步方法,但是這段程式中的StringBuffer屬於一個區域性變數,並且不會從該方法中逃逸出去,所以其實這過程是執行緒安全的,可以將鎖消除。
Sychronized缺點
Sychronized會讓沒有得到鎖的資源進入Block狀態,爭奪到資源之後又轉為Running狀態,這個過程涉及到作業系統使用者模式和核心模式的切換,代價比較高。
Java1.6為 synchronized 做了優化,增加了從偏向鎖到輕量級鎖再到重量級鎖的過度,但是在最終轉變為重量級鎖之後,效能仍然較低。
往期文章:
-
《提升能力,漲薪可待》-Java併發之AQS全面詳
-
java多執行緒併發系列--基礎知識點(筆試、面試必備)
-
...
各位看官還可以嗎?喜歡的話,動動手指點個