1. 程式人生 > >《提升能力,漲薪可待》—Java併發之Synchronized

《提升能力,漲薪可待》—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多執行緒併發系列--基礎知識點(筆試、面試必備)

  • ...

各位看官還可以嗎?喜歡的話,動動手指點個