Java多執行緒之synchronized的小介紹

innoyiya發表於2018-02-01

首先先看看下面的程式碼:

public class checkSynchronized extends Thread{
    static volatile int i = 0;
    
    public static 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 checkSynchronized();
        Thread t2 = new checkSynchronized();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("i : " + i);
    }
}
複製程式碼

執行上述程式碼,你會發現大部分情況下i的最終值都會小於2000000的

為什麼加了volatile關鍵詞的變數依舊會出現執行緒安全問題呢?

這是因為volatile只保證可見性,不保證原子性

Java多執行緒之synchronized的小介紹
圖為兩條執行緒同時對i進行寫入時,一個執行緒的結果會覆蓋另一執行緒的結果,造成執行緒安全問題。 解決此問題就應該線上程甲進行寫入值時,執行緒乙不僅不能寫入、而且還不能讀取值,如果讀取值的話就會讀取到一箇舊值,依舊會造成執行緒安全問題。那該如何實現呢?

在這裡就要引出今天的主角了:"Synchronized"

關鍵字synchronized的作用就是實現執行緒間的同步問題,它能將同步區的程式碼進行加鎖,一次只能允許一條執行緒進入同步區,以保證同步區中的執行緒安全問題。 下面就來測試下該關鍵字的作用:

public class checkSynchronized extends Thread{
    Object lock;
    checkSynchronized(Object lock) {
        this.lock = lock;
    }
    static volatile int i = 0;

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

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

    public static void main(String args[]) throws InterruptedException {
        Object lock = new Object();
        Thread t1 = new checkSynchronized(lock);
        Thread t2 = new checkSynchronized(lock);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("i : " + i);
    }
}
複製程式碼

大家可以看到在run()方法里加了synchronized,並且指定了鎖物件。

執行結果:i : 2000000

在這裡簡單地整理下synchronized的多種用法:

  • 指定鎖物件,進入前須先獲得給定物件的鎖(如上述程式碼所述)
  • 加在例項方法,進入前須獲得當前例項的鎖
  • 加在靜態方法,進入前須獲得當前類的鎖

在這裡給出一段錯誤程式碼,大家需要明白:

public static void main(String args[]) throws InterruptedException {
        Object lock1 = new Object();    //生成第一個鎖
        Object lock2 = new Object();    //生成第二個鎖
        Thread t1 = new checkSynchronized(lock1);
        Thread t2 = new checkSynchronized(lock2);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("i : " + i);
    }
複製程式碼

這段程式碼表示出各自執行緒最終使用了各自的鎖,執行緒安全是無法保證的。

對於作用在例項方法上的也要注意,因為其可能會發生和上述相同原因的執行緒安全錯誤。

public class checkSynchronized implements Runnable{

    static volatile 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 {
        checkSynchronized onlyOne = new checkSynchronized();
        Thread t1 = new Thread(onlyOne);
        Thread t2 = new Thread(onlyOne);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("i : " + i);
    }
}
複製程式碼

上述程式碼的synchronized作用在例項方法上,鎖為當前例項。所以在main方法中只例項了一個checkSynchronized例項,因為鎖只需要一個、多了就會出現安全問題。

如果例項了兩個物件,則會出現安全問題(鎖多了和不加鎖就沒啥區別了),如下述程式碼:

public static void main(String args[]) throws InterruptedException {
        checkSynchronized onlyOne = new checkSynchronized();
        checkSynchronized wrong = new checkSynchronized();
        Thread t1 = new Thread(onlyOne);
        Thread t2 = new Thread(wrong);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("i : " + i);
    }
複製程式碼

解決的辦法非常容易,只需在加了鎖的例項方法加上"static"關鍵字就行。

public static synchronized void increase() 這樣鎖就作用在類方法上了。當執行緒要執行該同步方法時是請求當前類的鎖並非例項的鎖,所以再多的例項執行緒之間依舊能正確同步。

synchronized不僅用於執行緒同步、確保執行緒安全問題外,還能保證執行緒之間的可見性和有序性問題。相當於是volatile的升級版。但被synchronized限制的多執行緒之間是序列執行的,所帶來的效能消耗是很大的。

相關文章