【同步-專欄系列】2.利用互斥鎖解決原子性問題

原子性的定義:一個或多個操作在CPU的執行過程中,不被中斷的特性叫做原子性。
我們知道引起原子性問題的原因是「線程切換」。所以如果能夠禁止線程切換就解決問題了?而操作系統是依賴CPU中斷做線程切換的,那麼我們禁用CPU中斷不就行了嗎?
在單核CPU的環境下,這個答案是可行的。但是現在是多核CPU時代。在多核CPU場景下,假如同一時刻有兩個線程在運行,一個線程執行在CPU-1上,一個執行在CPU-2上。那麼禁止中斷,只能保證線程在CPU上的連續執行,並不能保證同一個時刻只有一個線程在執行。

「同一時刻只有一個線程在執行」,這個條件非常重要,我們稱之爲「互斥」。如果我們能保證對共享變量的修改是互斥的,那麼無論是單核CPU或者是多核CPU就能保證原子性了。
實現互斥,最容易想到就是鎖。

1.簡易鎖模型

在這裏插入圖片描述
我們將一段需要互斥執行的代碼稱爲臨界區。在進入臨界區之前,首先嚐試加鎖lock,如果加鎖成功進入臨界區,我們稱這個線程持有了鎖。否則就等待持有鎖的線程釋放鎖;持有鎖的線程執行完臨界區的代碼之後,釋放鎖unLock。
現在還有進一步的問題,我們鎖的是什麼,保護的又是什麼?

2.改進之後的鎖模型

在這裏插入圖片描述
解釋下上圖:我們把臨界區要保護的資源標記爲R;我們要爲受保護資源R創建一把鎖LR;針對這把鎖LR,我們在進出臨界區前後填上加鎖和解鎖操作。
這是通用的鎖模型,要注意的是鎖和受保護資源的關聯關係。避免出現類似「鎖自家門來保護他家資源」的問題。

3.Java語言提供的額鎖 synchronized

Java語言提供了synchronized關鍵字,實現了鎖的功能。它是JVM內部的實現的。synchronized關鍵字可以修改方法和代碼塊,如下:

public class SynchronizeApp {
    //修飾靜態方法
    public static synchronized void fun1(){
        // 臨界區
    }

    //修飾成員方法
    public synchronized void fun2(){
        // 臨界區
    }

    //修飾代碼塊
    private Object obj = new Object();
    public synchronized void fun3(){

        synchronized (obj){
            // 臨界區
        }
    }
}

對照上面的改進鎖模型,你可能發現並沒有什麼加鎖/解鎖操作啊。 其實加鎖lock和解鎖unLock是隱式的。.java文件在編譯時,會在方法或者代碼塊自動加上lock()方法和unlock()方法.。
你可能又有一個問題了,那麼加鎖和解鎖鎖定的對象是什麼呢?我就直接拋出結論了:
1.修飾非靜態方法,鎖定的是當前實例this
2.修飾靜態方法,鎖定的是當前類的Class對象

4.鎖和受保護資源的關係

一個合理的關係是:受保護資源和鎖之間的關聯關係是N:1的關係。一把鎖可以保護N個對象,但是不能用N把鎖來保護一個對象。我們舉這樣一個例子來說明:

class App{
 static long value = 0L;
    public synchronized long get(){
        return value;
    }

    public static synchronized void plus(){
        value++;
    }
}

我們用synchronized實現了一個自增操作。plus是+1操作,get是獲取操作。 注意,value是靜態成員,get和plus都使用了synchronized修飾,但是get是成員方法。這個例子中,受保護的資源時value,get和plus的鎖定對象分別是this,App.class。由於臨界區get和plus是由兩把鎖保護的,這兩把鎖並不存在互斥關係,臨界區plus對value修改對get沒有可見性保證,這就導致併發問題了。