讀書筆記之《Java併發編程的藝術-第二章》之synchronized

在之前的文章中學習了volatile關鍵字,volatile可以保證變量在線程間的可見性,但他不能真正的保證線程安全。
/**
 * @author cenkailun
 * @Date 9/5/17
 * @Time 20:23
 */
public class ConcurrentAddWithVolatile implements Runnable {

    private static ConcurrentAddWithVolatile instance = new ConcurrentAddWithVolatile();
    private static volatile int i = 0;


    public static void increase() {
        i++;
    }

    public void run() {
        for (int j = 0; j < 1000000; j++) {
            i++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance,"線程1");
        Thread t2 = new Thread(instance, "線程2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

如上述代碼所示,如果說兩個線程是正確的併發執行的話,最後得到的結果應該是2000000,但結果往往是小於2000000。那麼這是爲什麼呢?

經過閱讀書籍,可以得知,i++的這個操作,其實是要分成3步。

1. 讀取i的當前值到操作棧
2. 對i的當前值+1
3. 寫回i+1後的值

經過了上述3步,才完成了i++ 的這個操作,volatile保證了寫回內存後,i的最新值能夠被其他線程獲取,但i++的這三個動作不是一個整體,即不是原子操作,是可以被拆開的。

比如,線程1和2同時讀取了i爲0,並各自在自己的線程中計算得到i=1,先後寫入這個i的值,導致雖然i++被執行了兩次,但是實際i的值只增加了1。

如果要解決這個問題,就要保證多個線程在對i進行++ 這個操作時完全同步,即i++的這三步是一起完成的,當線程1在寫入時,其他線程不能讀也不能寫,因爲在線程1寫完之前,其他線程讀到的肯定是一個過期的數據。

Java提供了synchronized來實現這個功能,保證多線程執行時候的同步,某一時刻只有一個線程可以對synchronized關鍵字保護起來的區域進行操作,相對於volatile來說是比較重量級的。

Java的synchronized關鍵字具體表現有以下三種形式:

  1. 作用於實例方法,鎖的是當前實例對象。

  2. 作用於靜態方法,鎖的是當前類。

  3. 作用於代碼塊,鎖的是Synchronized裏配置的對象。

下面是一個示例,將synchronized作用於一個給定對象instance,每當線程要進入被包裹的代碼塊,會請求instance的鎖。如果有其他線程已經持有了這把鎖,那麼新到的線程就必須等待,這樣保證了每次只有一個線程會執行i++操作。

/**
 * @author cenkailun
 * @Date 9/5/17
 * @Time 20:23
 */
public class ConcurrentAddWithVolatile implements Runnable {

    private static ConcurrentAddWithVolatile instance = new ConcurrentAddWithVolatile();
    private static volatile int i = 0;


    public static void increase() {
        i++;
    }

    public void run() {
        for (int j = 0; j < 1000000; j++) {
            synchronized (instance) {     //同步代碼塊
                i++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance,"線程1");
        Thread t2 = new Thread(instance, "線程2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

對於java中的代碼塊同步,JVM是基於進入和退出Monitor對象來實現代碼塊同步的,將monitorenter指令插入到同步代碼塊的開始位置,monitorexit插入到方法結束處和異常處,每一個對象都有一個monitor與之對應,當一個monitor被持有後,它將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖。

如下面字節碼所示,代表上文代碼中的同步代碼塊。

13: monitorenter
14: getstatic     #2                  // Field i:I
17: iconst_1
18: iadd
19: putstatic     #2                  // Field i:I
22: aload_2
23: monitorexit

對於實例方法或者靜態方法上加的synchronized關鍵字,在方法上會有一個標誌位代表,如下面字節碼所示。

public synchronized void increase();
flags: ACC_PUBLIC, ACC_SYNCHRONIZED

在我看來,synchronized相對於volatile的強大之處在於保證了線程安全性以及做到了線程同步,同時也能做到volatile提供的線程間可見性以及有序性。從可見性上來說,線程通過持有鎖的方式獲取變量的最新值。從有序性上來說,synchronized限制每次只有一個線程可以訪問同步的代碼,無論內部指令順序如何被打亂,jvm會保證最終執行的結果總是一樣,其他線程只能在獲得鎖後讀取結果數據,不會讀到中間值,所以有序性問題也得到了解決。
併發編程的水感覺好深。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章