Java併發編程實戰 03互斥鎖 解決原子性問題

文章系列

Java併發編程實戰 01併發編程的Bug源頭
Java併發編程實戰 02Java如何解決可見性和有序性問題java

摘要

在上一篇文章02Java如何解決可見性和有序性問題當中,咱們解決了可見性和有序性的問題,那麼還有一個原子性問題我們還沒解決。在第一篇文章01併發編程的Bug源頭當中,講到了把一個或者多個操做在 CPU 執行的過程當中不被中斷的特性稱爲原子性,那麼原子性的問題該如何解決。編程

同一時刻只有一個線程執行這個條件很是重要,咱們稱爲互斥,若是能保護對共享變量的修改時互斥的,那麼就能保住原子性。緩存

簡易鎖

咱們把一段須要互斥執行的代碼稱爲臨界區,線程進入臨界區以前,首先嚐試獲取加鎖,若加鎖成功則能夠進入臨界區執行代碼,不然就等待,直到持有鎖的線程執行了解鎖unlock()操做。以下圖:
互斥鎖1.jpg微信

可是有兩個點要咱們理解清楚:咱們的鎖是什麼?要保護的又是什麼?併發

改進後的鎖模型

在併發編程世界中,鎖和鎖要保護的資源是有對應關係的。
首先咱們須要把臨界區要保護的資源R標記出來,而後須要建立一把該資源的鎖LR,最後針對這把鎖,咱們須要在進出臨界區時添加加鎖lock(LR)操做和解鎖unlock(LR)操做。以下:
互斥鎖2.jpgapp

Java語言提供的鎖技術:synchronized

synchronized可修飾方法和代碼塊。加鎖lock()和解鎖unlock()都會在synchronized修飾的方法或代碼塊先後自動加上加鎖lock()和解鎖unlock()操做。這樣作的好處就是加鎖和解鎖操做會成對出現,畢竟忘了執行解鎖unlock()操做但是會讓其餘線程死等下去。
那咱們怎麼去鎖住須要保護的資源呢?在下面的代碼中,add1()非靜態方法鎖定的是this對象(當前實例對象),add2()靜態方法鎖定的是X.class(當前類的Class對象)性能

public class X {
    public synchronized void add1() {
        // 臨界區
    }
    public synchronized static void add2() {
        // 臨界區
    }
}

上面的代碼能夠理解爲這樣:this

public class X {
    public synchronized(this) void add() {
        // 臨界區
    }
    public synchronized(X.class) static void add2() {
        // 臨界區
    }
}

使用synchronized 解決 count += 1 問題

01 併發編程的Bug源頭文章當中,咱們提到過count += 1 存在的併發問題,如今咱們嘗試使用synchronized解決該問題。線程

public class Calc {
    private int value = 0;
    public synchronized int get() {
        return value;
    }
    public synchronized void addOne() {
        value += 1;
    }
}

addOne()方法被synchronized修飾後,只有一個線程能執行,因此必定能保證原子性,那麼可見性問題呢?在上一篇文章02 Java如何解決可見性和有序性問題當中,提到了管程中的鎖規則,一個鎖的解鎖 Happens-Before 於後續對這個鎖的加鎖。管程,在這裏就是synchronized(管程的在後續的文章中介紹)。根據這個規則,前一個線程執行了value += 1操做是對後續線程可見的。而查看get()方法也必須加上synchronized修飾,不然也無法保證其可見性。
上面這個例子以下圖:
互斥鎖3.jpgcode

那麼可使用多個鎖保護一個資源嗎,修改一下上面的例子後,get()方法使用this對象鎖來保護資源valueaddOne()方法使用Calc.class類對象來保護資源value,代碼以下:

public class Calc {
    private static int value = 0;
    public synchronized int get() {
        return value;
    }
    public static synchronized void addOne() {
        value += 1;
    }
}

上面的例子用圖來表示:
互斥鎖4.jpg

在這個例子當中,get()方法使用的是this鎖,addOne()方法使用的是Calc.class鎖,所以這兩個臨界區(方法)並無互斥性,addOne()方法的修改對get()方法是不可見的,因此就會致使併發問題。
結論:不可以使用多把鎖保護一個資源,但能使用一把鎖保護多個資源(這裏沒寫例子,只寫了一把鎖保護一個資源)

保護沒有關聯關係的多個資源

在銀行的業務當中,修改密碼和取款是兩個再常常不過的操做了,修改密碼操做和取款操做是沒有關聯關係的,沒有關聯關係的資源咱們可使用不一樣的互斥鎖來解決併發問題。代碼以下:

public class Account {
    // 保護密碼的鎖
    private final Object pwLock = new Object();
    // 密碼
    private String password;

    // 保護餘額的鎖
    private final Object moneyLock = new Object();
    // 餘額
    private Long money;

    public void updatePassword(String password) {
        synchronized (pwLock) {
            // 修改密碼
        }
    }

    public void withdrawals(Long money) {
        synchronized (moneyLock) {
            // 取款
        }
    }
}

分別使用pwLockmoneyLock來保護密碼和餘額,這樣修改密碼和修改餘額就能夠並行了。使用不一樣的鎖對受保護的資源進行進行更細化管理,可以提高性能,這種鎖叫作細粒度鎖。
在這個例子當中,你可能發現我使用了final Object來當成一把鎖,這裏解釋一下:使用鎖必須是不可變對象,若把可變對象做爲鎖,當可變對象被修改時至關於換鎖,並且使用LongInteger做爲鎖時,在-128到127之間時,會使用緩存,詳情可查看他們的valueOf()方法。

保護有關聯關係的多個資源

在銀行業務當中,除了修改密碼和取款的操做比較多以外,還有一個操做比較多的功能就是轉帳。帳戶 A 轉帳給 帳戶B 100元,帳戶A的餘額減小100元,帳戶B的餘額增長100元,那麼這兩個帳戶就是有關聯關係的。在沒有理解互斥鎖以前,寫出的代碼可能以下:

public class Account {
    // 餘額
    private Long money;
    public synchronized void transfer(Account target, Long money) {
        this.money -= money;
        if (this.money < 0) {
            // throw exception
        }
        target.money += money;
    }
}

在轉帳transfer方法當中,鎖定的是this對象(用戶A),那麼這裏的目標用戶target(用戶B)的能被鎖定嗎?固然不能。這兩個對象是沒有關聯關係的。正確的操做應該是獲取this鎖和target鎖才能去進行轉帳操做,正確的代碼以下:

public class Account {
    // 餘額
    private Long money;
    public synchronized void transfer(Account target, Long money) {
        synchronized(this) {
            synchronized (target) {
                this.money -= money;
                if (this.money < 0) {
                    // throw exception
                }
                target.money += money;
            }
        }
    }
}

在這個例子當中,咱們須要清晰的明白要保護的資源是什麼,只要咱們的鎖能覆蓋全部受保護的資源就能夠了
可是你覺得這個例子很完美?那就錯了,這裏面頗有可能會發生死鎖。你看出來了嗎?下一篇文章我就用這個例子來聊聊死鎖。

總結

使用互斥鎖最最重要的是:咱們的鎖是什麼?鎖要保護的資源是什麼?,要理清楚這兩點就好下手了。並且鎖必須爲不可變對象。使用不一樣的鎖保護不一樣的資源,能夠細化管理,提高性能,稱爲細粒度鎖

參考文章:
極客時間:Java併發編程實戰 03互斥鎖(上)
極客時間:Java併發編程實戰 04互斥鎖(下)

我的博客網址: https://colablog.cn/

若是個人文章幫助到您,能夠關注個人微信公衆號,第一時間分享文章給您
微信公衆號

相關文章
相關標籤/搜索