【Java併發編程的藝術】第二章讀書筆記之synchronized關鍵字

在以前的文章中學習了volatile關鍵字,volatile能夠保證變量在線程間的可見性,但他不能真正的保證線程安全。java

/** * @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步。bash

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寫完以前,其餘線程讀到的確定是一個過時的數據。jvm

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

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

  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的全部權,即嘗試得到對象的鎖。code

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

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會保證最終執行的結果老是同樣,其餘線程只能在得到鎖後讀取結果數據,不會讀到中間值,因此有序性問題也獲得瞭解決。

相關文章
相關標籤/搜索