java synchronized關鍵字的用法

0.先導的問題代碼

    下面的代碼演示了一個計數器,兩個線程同時對i進行累加的操做,各執行1000000次.咱們指望的結果確定是i=2000000.可是咱們屢次執行之後,會發現i的值永遠小於2000000.這是由於,兩個線程同時對i進行寫入的時候,其中一個線程的結果會覆蓋另一個.java

public class AccountingSync implements Runnable {
    static int i = 0;
    public void increase() {
        i++;
    }

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

    public static void main(String[] args) throws InterruptedException {
        AccountingSync accountingSync = new AccountingSync();

        Thread t1 = new Thread(accountingSync);
        Thread t2 = new Thread(accountingSync);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(i);
    }
}

    要從根本上解決這個問題,咱們必須保證多個線程在對i進行操做的時候,要徹底的同步.也就是說到A線程對i進行寫入的時候,B線程不只不能夠寫入,連讀取都不能夠.
安全


1.synchronized關鍵字的做用

    關鍵字synchronized的做用其實就是實現線程間的同步.它的工做就是對同步的代碼進行加鎖,使得每一次,只能有一個線程進入同步塊,從而保證線程間的安全性.就像上面的代碼中,i++的操做只能同時又一個線程在執行.
ide

2.synchronized關鍵字的用法

  • 指定對象加鎖:對給定的對象進行加鎖,進入同步代碼塊要得到給定對象的鎖spa

  • 直接做用於實例方法:至關於對當前實例加鎖,進入同步代碼塊要得到當前實例的鎖(這要求建立Thread的時候,要用同一個Runnable的實例才能夠)線程

  • 直接做用於靜態方法:至關於給當前類加鎖,進入同步代碼塊前要得到當前類的鎖code

2.1指定對象加鎖

    下面的代碼,將synchronized做用於一個給定的對象.這裏有一個注意的,給定對象必定要是static的,不然咱們每次new一個線程出來,彼此並不共享該對象,加鎖的意義也就不存在了.
對象

public class AccountingSync implements Runnable {
    final static Object OBJECT = new Object();

    static int i = 0;
    public void increase() {
        i++;
    }

    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            synchronized (OBJECT) {
                increase();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new AccountingSync());
        Thread t2 = new Thread(new AccountingSync());

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(i);
    }
}

2.2直接做用於實例方法

    synchronized關鍵字做用於實例方法,就是說在進入increase()方法以前,線程必須得到當前實例的鎖.這就要求咱們,在建立Thread實例的時候,要使用同一個Runnable的對象實例.不然,線程的鎖都不在同一個實例上面,無從去談加鎖/同步的問題了.
同步

public class AccountingSync implements Runnable {
    static int i = 0;
    public synchronized void increase() {
        i++;
    }

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

    public static void main(String[] args) throws InterruptedException {
        AccountingSync accountingSync = new AccountingSync();

        Thread t1 = new Thread(accountingSync);
        Thread t2 = new Thread(accountingSync);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(i);
    }
}

    請注意main方法的前三行,說明關鍵字做用於實例方法上的正確用法.
源碼

2.3直接做用於靜態方法

    將synchronized關鍵字做用在static方法上,就不用像上面的例子中,兩個線程要指向同一個Runnable方法.由於方法塊須要請求的是當前類的鎖,而不是當前實例,線程間仍是能夠正確同步的.io

public class AccountingSync implements Runnable {
    static int i = 0;
    public static synchronized void increase() {
        i++;
    }

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

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new AccountingSync());
        Thread t2 = new Thread(new AccountingSync());

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(i);
    }
}

 


3.錯誤的加鎖

    從上面的例子裏,咱們知道,若是咱們須要一個計數器應用,爲了保證數據的正確性,咱們天然會須要對計數器加鎖,所以,咱們可能會寫出下面的代碼:

public class BadLockOnInteger implements Runnable {
    static Integer i = 0;
    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            synchronized (i) {
                i++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        BadLockOnInteger badLockOnInteger = new BadLockOnInteger();

        Thread t1 = new Thread(badLockOnInteger);
        Thread t2 = new Thread(badLockOnInteger);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(i);
    }
}

    當咱們運行上面代碼的時候,會發現輸出的i很小.這說明線程並無安全.

    要解釋這個問題,要從Integer提及:在Java中,Integer屬於不變對象,和String同樣,對象一旦被建立,就不能被修改了.若是你有一個Integer=1,那麼它就永遠都是1.若是你想讓這個對象=2呢?只能從新建立一個Integer.每次i++以後,至關於調用了Integer的valueOf方法,咱們看一下Integer的valueOf方法的源碼:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

    Integer.valueOf()其實是一個工廠方法,他會傾向於返回一個新的Integer對象,並把值從新複製給i;

    因此,咱們就知道問題所在的緣由,因爲在多個線程之間,因爲i++以後,i都指向了一個新的對象,因此線程每次加鎖可能都加載了不一樣的對象實例上面.解決方法很簡單,使用上面的3種synchronize的方法之一就能夠解決了.

相關文章
相關標籤/搜索