Java synchronized 多線程同步問題詳解

版權聲明:本文由吳仙傑創做整理,轉載請註明出處:http://www.javashuo.com/article/p-flychtvo-gh.htmljava

1. 引言

在 Java 多線程編程中,咱們常須要考慮線程安全問題,其中關鍵字 synchronized 在線程同步中就扮演了很是重要的做用。shell

下面就對 synchronized 進行詳細的示例講解,其中本文構建 thread 的寫法是採用 Java 8 新增的 Lambda 表達式。若是你對 Lambda 表達式還不瞭解,能夠查看我以前的文章《Java 8 Lambda 表達式詳解》。編程

2. synchronized 鎖的是什麼

首先咱們明確一點,synchronized 鎖的不是代碼,鎖的都是對象segmentfault

鎖的對象有如下幾種:安全

  • 同步非靜態方法(synchronized method),鎖是當前對象的實例對象多線程

  • 同步靜態方法(synchronized static method),鎖是當前對象的類對象(Class 對象)異步

  • 同步代碼塊一(synchronized (this)synchronized (類實例對象)),鎖是小括號 () 中的實例對象性能

  • 同步代碼塊二(synchronized (類.class)),鎖是小括號 () 中的類對象(Class 對象)優化

2.1 實例對象鎖與類對象鎖

1)實例對象鎖,不一樣的實例擁有不一樣的實例對象鎖,因此對於同一個實例對象,在同一時刻只有一個線程能夠訪問這個實例對象的同步方法;不一樣的實例對象,不能保證多線程的同步操做。this

2)類對象鎖(全局鎖),在 JVM 中一個類只有一個與之對應的類對象,因此在同一時刻只有一個線程能夠訪問這個類的同步方法。

3. 示例分析

3.1 同步非靜態實例方法

同步非靜態方法,實際上鎖定的是當前對象的實例對象。在同一時刻只有一個線程能夠訪問該實例的同步方法,但對於多個實例的同步方法,不一樣實例之間對同步方法的訪問是不受同步影響(synchronized 同步失效)。

首先咱們嘗試下,synchronized 同步失敗的狀況:

package com.wuxianjiezh.demo.threadpool;

public class MainTest {

    public static void main(String[] args) {
        Bank xMBank = new Bank();
        Bank xGBank = new Bank();
        Bank xHBank = new Bank();
        Thread xMThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小明");
        Thread xGThread = new Thread(() -> xGBank.deposit(xGBank, 200), "小剛");
        Thread xHThread = new Thread(() -> xHBank.deposit(xHBank, 200), "小紅");
        xMThread.start();
        xGThread.start();
        xHThread.start();
    }
}

class Bank {

    private int money = 1000;

    public synchronized void deposit(Bank bank, int money) {
        // synchronized (this) { // 同步方法塊(實例對象)
        // synchronized (bank) { // 同步方法塊(實例對象)
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + "--當前銀行餘額爲:" + this.money);
            this.money += money;
            System.out.println(threadName + "--存入後銀行餘額爲:" + this.money);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        // }
    }
}

運行結果:

小明--當前銀行餘額爲:1000
小剛--當前銀行餘額爲:1000
小明--存入後銀行餘額爲:1200
小紅--當前銀行餘額爲:1000
小剛--存入後銀行餘額爲:1200
小紅--存入後銀行餘額爲:1200

從上面的運行結果,咱們發現對 Bankmoney 的操做並無同步,synchronized 失效了?

這是由於實例對象鎖,只對同一個實例生效,對同一個對象的不一樣實例不保證同步。


修改上述代碼,實現同步操做。這裏將有兩種方案:只實例化一個或在方法塊中的鎖定類對象。

方案1、多個線程只對同一個實例對象操做:

package com.wuxianjiezh.demo.threadpool;

public class MainTest {

    public static void main(String[] args) {
        Bank xMBank = new Bank();
        // Bank xGBank = new Bank();
        // Bank xHBank = new Bank();
        Thread xMThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小明");
        Thread xGThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小剛");
        Thread xHThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小紅");
        xMThread.start();
        xGThread.start();
        xHThread.start();
    }
}

class Bank {

    private int money = 1000;

    public synchronized void deposit(Bank bank, int money) {
        // synchronized (this) { // 同步方法塊(實例對象)
        // synchronized (bank) { // 同步方法塊(實例對象)
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + "--當前銀行餘額爲:" + this.money);
            this.money += money;
            System.out.println(threadName + "--存入後銀行餘額爲:" + this.money);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        // }
    }
}

運行結果:

小明--當前銀行餘額爲:1000
小明--存入後銀行餘額爲:1200
小紅--當前銀行餘額爲:1200
小紅--存入後銀行餘額爲:1400
小剛--當前銀行餘額爲:1400
小剛--存入後銀行餘額爲:1600
...

能夠看到,結果正確執行。由於對於同一個實例對象,各線程之間訪問其中的同步方法是互斥的。


方案2、在方法塊中鎖定類對象:

package com.wuxianjiezh.demo.threadpool;

public class MainTest {

    public static void main(String[] args) {
        Bank xMBank = new Bank();
        Bank xGBank = new Bank();
        Bank xHBank = new Bank();
        Thread xMThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小明");
        Thread xGThread = new Thread(() -> xGBank.deposit(xGBank, 200), "小剛");
        Thread xHThread = new Thread(() -> xHBank.deposit(xHBank, 200), "小紅");
        xMThread.start();
        xGThread.start();
        xHThread.start();
    }
}

class Bank {

    private int money = 1000;

    public void deposit(Bank bank, int money) {
        synchronized (Bank.class) { // 全局鎖
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + "--當前銀行餘額爲:" + this.money);
            this.money += money;
            System.out.println(threadName + "--存入後銀行餘額爲:" + this.money);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

運行結果:

小明--當前銀行餘額爲:1000
小明--存入後銀行餘額爲:1200
小紅--當前銀行餘額爲:1000
小紅--存入後銀行餘額爲:1200
小剛--當前銀行餘額爲:1000
小剛--存入後銀行餘額爲:1200

思考:從結果中咱們發現,線程是同步操做了,但爲何在咱們的 money 怎麼才 1200 啊?

要回答上面問題也很簡單,首先線程是同步操做了,這個沒有疑問,說明咱們的全局鎖生效了,那爲何錢少了,由於咱們這裏 mew 了三個對象,三個對象都有各自的 money,他們並不共享,因此最後都是 1200,最終一共仍是增長了 6000,錢一點沒有少喔。

那有沒有辦法,讓這些線程間共享 money 呢?方法很簡單,只要設置 moneystatic 便可。

3.1.1 對同步代碼塊優化的思考

對於一個方法,可能包含多個操做部分,而每一個操做部分的消耗各不相同,並且並非全部的操做都是須要同步控制的,那麼,是否能夠將那些影響效率,又不須要同步操做的內容,提取到同步代碼塊外呢?

請看如下示例:

package com.wuxianjiezh.demo.threadpool;

public class MainTest {

    public static void main(String[] args) {
        Bank xMBank = new Bank();
        Bank xGBank = new Bank();
        Bank xHBank = new Bank();
        Thread xMThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小明");
        Thread xGThread = new Thread(() -> xGBank.deposit(xGBank, 200), "小剛");
        Thread xHThread = new Thread(() -> xHBank.deposit(xHBank, 200), "小紅");
        xMThread.start();
        xGThread.start();
        xHThread.start();
    }
}

class Bank {

    private int money = 1000;

    public void deposit(Bank bank, int money) {
        String threadName = Thread.currentThread().getName();
        synchronized (Bank.class) { // 同步方法塊(實例對象)
            System.out.println(threadName + "--當前銀行餘額爲:" + this.money);
            this.money += money;
            System.out.println(threadName + "--存入後銀行餘額爲:" + this.money);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            // 假設這裏是很是耗時,而且不須要同步控制的操做
            Thread.sleep(2000);
            System.out.println(threadName + "--和錢無關,不須要同步控制的操做");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

運行結果:

小明--當前銀行餘額爲:1000
小明--存入後銀行餘額爲:1200
小紅--當前銀行餘額爲:1000
小紅--存入後銀行餘額爲:1200
小剛--當前銀行餘額爲:1000
小剛--存入後銀行餘額爲:1200
小明--和錢無關,不須要同步控制的操做
小紅--和錢無關,不須要同步控制的操做
小剛--和錢無關,不須要同步控制的操做

這時發現,各線程雖然都有本身的實例化對象,但其中操做 money 的部分是同步的,對於與 money 無關的操做則又是異步的。

結論:能夠經過減小同步區域來優化同步代碼塊。

3.1.2 對同步代碼塊優化的思考(進階)

咱們知道同步的對象不是實例對象就是類對象。如今假設一個類有多個同步方法,那麼當某個線程進入其中一個同步方法時,這個類的其它同步方法也會被鎖住,形成其它與當前鎖定操做的同步方法毫無關係的同步方法也被鎖住,最後的結果就是影響了整個多線程執行的性能,使本來不須要互斥的方法也都進行了互斥操做。好比:

package com.wuxianjiezh.demo.threadpool;

public class MainTest {

    public static void main(String[] args) {
        Bank xMBank = new Bank();
        Thread xMThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小明");
        Thread xGThread = new Thread(xMBank::showInfo, "小剛");
        xMThread.start();
        xGThread.start();
    }
}

class Bank {

    private int money = 1000;

    public void deposit(Bank bank, int money) {
        long begin = System.currentTimeMillis();

        String threadName = Thread.currentThread().getName();
        synchronized (this) { // 同步方法塊(實例對象)
            this.money += money;
            try {
                System.out.println(threadName + "--當前銀行餘額爲:" + this.money);
                // 模擬一個很是耗時的操做
                Thread.sleep(5000);
                System.out.println(threadName + "--存入後銀行餘額爲:" + this.money);

                long end = System.currentTimeMillis();
                System.out.println(threadName + "--存入耗時:" + (end - begin));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 一個與資金操做沒有任務關係的同步方法
     */
    public void showInfo() {
        long begin = System.currentTimeMillis();

        String threadName = Thread.currentThread().getName();
        synchronized (this) {
            try {
                System.out.println(threadName + "--開始查看銀行信息");
                Thread.sleep(5000);
                System.out.println(threadName + "--銀行詳細信息...");

                long end = System.currentTimeMillis();
                System.out.println(threadName + "--查看耗時:" + (end - begin));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

運行結果:

小明--當前銀行餘額爲:1200
小明--存入後銀行餘額爲:1200
小明--存入耗時:5000
小剛--開始查看銀行信息
小剛--銀行詳細信息...
小剛--查看耗時:10000

從運行結果中,咱們看到小剛這個線程無緣無故多等了 5 秒鐘,嚴重影響了線程性能。


針對上面的狀況,咱們能夠採用多個實例對象鎖的方案解決,好比:

package com.wuxianjiezh.demo.threadpool;

public class MainTest {

    public static void main(String[] args) {
        Bank xMBank = new Bank();
        Thread xMThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小明");
        Thread xGThread = new Thread(xMBank::showInfo, "小剛");
        xMThread.start();
        xGThread.start();
    }
}

class Bank {

    private int money = 1000;

    private final Object syncDeposit = new Object(); // 同步鎖
    private final Object syncShowInfo = new Object(); // 同步鎖

    public void deposit(Bank bank, int money) {
        long begin = System.currentTimeMillis();

        String threadName = Thread.currentThread().getName();
        synchronized (this.syncDeposit) { // 同步方法塊(實例對象)
            this.money += money;
            try {
                System.out.println(threadName + "--當前銀行餘額爲:" + this.money);
                // 模擬一個很是耗時的操做
                Thread.sleep(5000);
                System.out.println(threadName + "--存入後銀行餘額爲:" + this.money);

                long end = System.currentTimeMillis();
                System.out.println(threadName + "--存入耗時:" + (end - begin));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 一個與資金操做沒有任務關係的同步方法
     */
    public void showInfo() {
        long begin = System.currentTimeMillis();

        String threadName = Thread.currentThread().getName();
        synchronized (this.syncShowInfo) {
            try {
                System.out.println(threadName + "--開始查看銀行信息");
                Thread.sleep(5000);
                System.out.println(threadName + "--銀行詳細信息...");

                long end = System.currentTimeMillis();
                System.out.println(threadName + "--查看耗時:" + (end - begin));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

運行結果:

小剛--開始查看銀行信息
小明--當前銀行餘額爲:1200
小剛--銀行詳細信息...
小明--存入後銀行餘額爲:1200
小明--存入耗時:5000
小剛--查看耗時:5000

咱們發現,兩個線程間同步被取消了,性能問題也解決了。

總結:能夠建立不一樣同步方法的不一樣同步鎖(減少鎖的範圍)來優化同步代碼塊。

3.2 同步靜態方法

同步靜態方法,鎖的是類對象而不是某個實例對象,因此能夠理解爲對於靜態方法的鎖是全局的鎖,同步也是全局的同步。

package com.wuxianjiezh.demo.threadpool;

public class MainTest {

    public static void main(String[] args) {
        Bank xMBank = new Bank();
        Bank xGBank = new Bank();
        Bank xHBank = new Bank();
        Thread xMThread = new Thread(() -> xMBank.deposit(xMBank, 200), "小明");
        Thread xGThread = new Thread(() -> xGBank.deposit(xGBank, 200), "小剛");
        Thread xHThread = new Thread(() -> xHBank.deposit(xHBank, 200), "小紅");
        xMThread.start();
        xGThread.start();
        xHThread.start();
    }
}

class Bank {

    private static int money = 1000;

    public synchronized static void deposit(Bank bank, int money) {
        // synchronized (Bank.class) { // 全局鎖
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + "--當前銀行餘額爲:" + Bank.money);
        Bank.money += money;
        System.out.println(threadName + "--存入後銀行餘額爲:" + Bank.money);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // }
    }
}

運行結果:

小明--當前銀行餘額爲:1000
小明--存入後銀行餘額爲:1200
小紅--當前銀行餘額爲:1200
小紅--存入後銀行餘額爲:1400
小剛--當前銀行餘額爲:1400
小剛--存入後銀行餘額爲:1600

4. 總結

同步鎖 synchronized 要點:

  1. synchronized 鎖的不是代碼,鎖的都是對象

  2. 實例對象鎖:同步非靜態方法(synchronized method),同步代碼塊(synchronized (this)synchronized (類實例對象))。

  3. 類對象(Class 對象)鎖:同步靜態方法(synchronized static method),同步代碼塊(synchronized (類.class))。

  4. 相同對象的不一樣的實例擁有不一樣的實例對象鎖,但類對象鎖(全局鎖)有僅只有一個。

  5. 優化同步代碼塊的方式有,減小同步區域或減少鎖的範圍。

相關文章
相關標籤/搜索