《提高能力,漲薪可待》—Java併發之Synchronized

Synchronized簡介

線程安全是併發編程中的相當重要的,形成線程安全問題的主要緣由:java

  • 臨界資源, 存在共享數據
  • 多線程共同操做共享數據

而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關鍵字後,代碼以下:app

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執行完以後才能執行,執行結果:ide

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的結果:this

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 作了優化,增長了從偏向鎖到輕量級鎖再到重量級鎖的過分,可是在最終轉變爲重量級鎖以後,性能仍然較低。

相關文章
相關標籤/搜索