從此不怕Synchronized鎖

何故愁為河邊柳發表於2020-07-06
  • Synchronized的使用

​ Synchronized是通過監視器保證執行緒同步從而保證執行緒安全。但是Synchronized鎖可以鎖物件和鎖類,並會產生不同的效果,通過下面的案例徹底理解Synchronized鎖的使用方式。

即:

對於普通的同步方法,鎖是當前例項物件

對於靜態同步方法,鎖是該類

對於同步方法塊,鎖是Synchronized括號裡面配置的物件。

下面通過程式碼具體分析幾種情況。要想了解併發情況,首先我們必須知道,類資訊、例項物件分別存放在什麼位置。類的資訊,包括靜態變數都是存放在方法區中;而例項物件,包括類的成員變數,是存放在堆中。

1. 成員變數+普通同步方法+鎖

public class SynDemo implements Runnable {

    private int sum = 0;

    @Override
    public void run() {
        add();
    }

    public synchronized void add() {
        for (int i = 0; i < 1000; i++) {
            sum++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynDemo sync01 = new SynDemo();
        Thread thread1 = new Thread(sync01);
        Thread thread2 = new Thread(sync01);
        thread1.start();
        thread2.start();
        thread1.join();		//等待執行緒執行完
        thread2.join();		//等待執行緒執行完
        System.out.println(sync01.sum);
        System.out.println(new SynDemo().sum);
    }
}

result:
	2000
        0
public class SynDemo implements Runnable {

    private int sum = 0;

    @Override
    public void run() {
        add();
    }

    public synchronized void add() {
        for (int i = 0; i < 1000; i++) {
            sum++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynDemo sync01 = new SynDemo();
        SynDemo sync02 = new SynDemo();
        Thread thread1 = new Thread(sync01);
        Thread thread2 = new Thread(sync02);
        thread1.start();
        thread2.start();
        thread1.join();		//等待執行緒執行完
        thread2.join();		//等待執行緒執行完
        System.out.println(sync01.sum);
        System.out.println(sync02.sum);
    }
}

result:
	1000
        1000
        0

分析:

​ 理解了這兩個demo再去理解同步程式碼塊下的多執行緒安全問題,將會達到事半功倍的效果。上面兩個demo主要是想表達,成員變數和類的例項化物件一樣,都是在堆中建立,每次new物件,都會在堆中產生一個新的物件。所以第一個demo中,當線上程同步的情況下,兩個執行緒去操作同一個物件,最後的結果是2000;而第二個demo中,兩個執行緒去操作兩個例項化物件,所以每個物件的成員變數sum為1000。因此我們也可以發現,其實在第一個demo中才會有執行緒安全問題,在第二個demo中是不存線上程安全問題的,有疑問可以去掉鎖驗證一下。通過這個例子也可以去理解為什麼sping中多例是執行緒安全的。

2. 成員變數+同步程式碼塊+物件鎖

public class SynDemo implements Runnable {

    private int sum = 0;

    @Override
    public void run() {
        add();
    }

    private void add() {
        synchronized (this) {
            for (int i = 0; i < 1000; i++) {
                sum++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynDemo sync01 = new SynDemo();
        Thread thread1 = new Thread(sync01);
        Thread thread2 = new Thread(sync01);
        thread1.start();
        thread2.start();
        thread1.join();  //等待執行緒執行完
        thread2.join();  //等待執行緒執行完
        System.out.println(sync01.sum);
        System.out.println(new SynDemo().sum);
    }
}

result:
	2000
        0
public class SynDemo implements Runnable {

    private int sum = 0;

    @Override
    public void run() {
        add();
    }

    private void add() {
        synchronized (this) {
            for (int i = 0; i < 1000; i++) {
                sum++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynDemo sync01 = new SynDemo();
        SynDemo sync02 = new SynDemo();
        Thread thread1 = new Thread(sync01);
        Thread thread2 = new Thread(sync02);
        thread1.start();
        thread2.start();
        thread1.join();  //等待執行緒執行完
        thread2.join();  //等待執行緒執行完
        System.out.println(sync01.sum);
        System.out.println(sync02.sum);
        System.out.println(new SynDemo().sum);
    }
}

result:
	1000
	1000
	0

分析:

​ 同案例1一樣,Demo1為兩個執行緒執行一個例項化物件,但是加了Synchronized物件鎖,因此實現了同步,保證執行緒安全。Demo2為兩個執行緒執行兩個例項化物件,各自利用各自的成員變數sum,因此不會產生併發安全問題。

3. 成員變數+同步程式碼塊+類鎖

public class SynDemo implements Runnable {

    private int sum = 0;

    @Override
    public void run() {
        add();
    }

    private void add() {
        synchronized (SynDemo.class) {
            for (int i = 0; i < 1000; i++) {
                sum++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynDemo sync01 = new SynDemo();
        Thread thread1 = new Thread(sync01);
        Thread thread2 = new Thread(sync01);
        thread1.start();
        thread2.start();
        thread1.join();  //等待執行緒執行完
        thread2.join();  //等待執行緒執行完
        System.out.println(sync01.sum);
        System.out.println(new SynDemo().sum);
    }
}

result:
	2000
	0
public class SynDemo implements Runnable {

    private int sum = 0;

    @Override
    public void run() {

        add();
    }

    private void add() {
        synchronized (SynDemo.class) {
            for (int i = 0; i < 1000; i++) {
                sum++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynDemo sync01 = new SynDemo();
        SynDemo sync02 = new SynDemo();
        Thread thread1 = new Thread(sync01);
        Thread thread2 = new Thread(sync02);
        thread1.start();
        thread2.start();
        thread1.join();  //等待執行緒執行完
        thread2.join();  //等待執行緒執行完
        System.out.println(sync01.sum);
        System.out.println(sync02.sum);
        System.out.println(new SynDemo().sum);
    }
}

result:
	1000
	1000
	0

分析:

​ Demo1為兩個執行緒執行一個例項化物件,會產生併發安全問題,但是加了同步類鎖(可以理解為鎖的級別比物件鎖更高),當然也可以實現併發同步,保證執行緒安全。而Demo2同樣例項化兩個物件,各自操作各自的成員變數sum,也不會產生執行緒安全問題。

4. 靜態變數+普通方法+鎖

public class SynDemo implements Runnable {

    private static int sum = 0;

    @Override
    public void run() {
        add();
    }

    private synchronized void add() {
            for (int i = 0; i < 1000; i++) {
                sum++;
            }
    }

    public static void main(String[] args) throws InterruptedException {
        SynDemo sync01 = new SynDemo();
        Thread thread1 = new Thread(sync01);
        Thread thread2 = new Thread(sync01);
        thread1.start();
        thread2.start();
        thread1.join();  //等待執行緒執行完
        thread2.join();  //等待執行緒執行完
        System.out.println(sum);
    }
}

result:
	2000
public class SynDemo implements Runnable {

    private static int sum = 0;

    @Override
    public void run() {
        add();
    }

    private synchronized void add() {
            for (int i = 0; i < 1000; i++) {
                sum++;
            }
    }

    public static void main(String[] args) throws InterruptedException {
        SynDemo sync01 = new SynDemo();
        SynDemo sync02 = new SynDemo();
        Thread thread1 = new Thread(sync01);
        Thread thread2 = new Thread(sync02);
        thread1.start();
        thread2.start();
        thread1.join();  //等待執行緒執行完
        thread2.join();  //等待執行緒執行完
        System.out.println(sum);
    }
}

輸出結果不確定(存線上程安全問題)

分析:

​ 從案例4我們要注意,由成員變數換成靜態變數,而上面已經講過,靜態變數存放在方法區中,所有例項化物件共享一份。再看Demo1,兩個執行緒執行同一個例項化物件,然後新增的是物件鎖,因此該物件鎖能鎖住該例項化物件,實現同步,保證執行緒安全。

​ Demo2是兩個執行緒執行兩個例項化物件,新增的是物件鎖,相當於各自的物件鎖鎖住各自的物件,而靜態變數是類變數,存放在方法區中而不是堆中,此情況物件鎖並不能保證執行緒同步,因此會產生執行緒安全問題。

5. 靜態變數+靜態方法+鎖

public class SynDemo implements Runnable {

    private static int sum = 0;

    @Override
    public void run() {
        add();
    }

    static synchronized void add() {
        for (int i = 0; i < 1000; i++) {
            sum++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynDemo sync01 = new SynDemo();
        Thread thread1 = new Thread(sync01);
        Thread thread2 = new Thread(sync01);
        thread1.start();
        thread2.start();
        thread1.join();  //等待執行緒執行完
        thread2.join();  //等待執行緒執行完
        System.out.println(sum);
    }
}

result:
	2000
public class SynDemo implements Runnable {

    private static int sum = 0;

    @Override
    public void run() {
        add();
    }

    static synchronized void add() {
        for (int i = 0; i < 1000; i++) {
            sum++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynDemo sync01 = new SynDemo();
        SynDemo sync02 = new SynDemo();
        Thread thread1 = new Thread(sync01);
        Thread thread2 = new Thread(sync02);
        thread1.start();
        thread2.start();
        thread1.join();  //等待執行緒執行完
        thread2.join();  //等待執行緒執行完
        System.out.println(sum);
    }
}

result:
	2000

分析:

​ 該案例相比案例4,鎖由物件鎖換成類鎖,對於Demo1,兩個執行緒操作一個物件,毫無疑問會使其同步。而Demo2,兩個執行緒執行兩個例項化物件,由於使用的是類鎖,也會使執行緒同步,保證執行緒安全。

6. 靜態變數+同步程式碼塊+物件鎖

public class SynDemo implements Runnable {

    private static int sum = 0;

    @Override
    public void run() {
        add();
    }

    private void add() {
        synchronized (this) {
            for (int i = 0; i < 1000; i++) {
                sum++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynDemo sync01 = new SynDemo();
        Thread thread1 = new Thread(sync01);
        Thread thread2 = new Thread(sync01);
        thread1.start();
        thread2.start();
        thread1.join();  //等待執行緒執行完
        thread2.join();  //等待執行緒執行完
        System.out.println(sum);
    }
}

result:
	2000
public class SynDemo implements Runnable {

    private static int sum = 0;

    @Override
    public void run() {
        add();
    }

    private void add() {
        synchronized (this) {
            for (int i = 0; i < 1000; i++) {
                sum++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynDemo sync01 = new SynDemo();
        SynDemo sync02 = new SynDemo();
        Thread thread1 = new Thread(sync01);
        Thread thread2 = new Thread(sync02);
        thread1.start();
        thread2.start();
        thread1.join();  //等待執行緒執行完
        thread2.join();  //等待執行緒執行完
        System.out.println(sum);
    }
}

輸出結果不確定(存線上程安全問題)

分析:該案例和案例4一樣,新增物件鎖,只能保證同一物件的併發同步,不能保證不同物件同步。

7. 靜態變數+同步程式碼塊+類鎖

public class SynDemo implements Runnable {

    private static int sum = 0;

    @Override
    public void run() {
        add();
    }

    private void add() {
        synchronized (SynDemo.class) {
            for (int i = 0; i < 1000; i++) {
                sum++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynDemo sync01 = new SynDemo();
        Thread thread1 = new Thread(sync01);
        Thread thread2 = new Thread(sync01);
        thread1.start();
        thread2.start();
        thread1.join();  //等待執行緒執行完
        thread2.join();  //等待執行緒執行完
        System.out.println(sum);
    }
}

result:
	2000
public class SynDemo implements Runnable {

    private static int sum = 0;

    @Override
    public void run() {
        add();
    }

    private void add() {
        synchronized (SynDemo.class) {
            for (int i = 0; i < 1000; i++) {
                sum++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynDemo sync01 = new SynDemo();
        SynDemo sync02 = new SynDemo();
        Thread thread1 = new Thread(sync01);
        Thread thread2 = new Thread(sync02);
        thread1.start();
        thread2.start();
        thread1.join();  //等待執行緒執行完
        thread2.join();  //等待執行緒執行完
        System.out.println(sum);
    }
}

result:
	2000

分析:

​ 該案例同案例5一樣,新增類鎖,無論是多個執行緒操作一個例項化物件還是多個例項化物件,都能保證執行緒安全。

總結:

物件鎖只能保證各自例項化物件併發的執行緒安全問題。類鎖可以保證多個例項化多謝的安全問題

相關文章