Java併發(十五)----synchronized解決共享的問題

爲了避免臨界區的競態條件發生,有多種手段可以達到目的。

  • 阻塞式的解決方案:synchronized,Lock

  • 非阻塞式的解決方案:原子變量

此次介紹使用阻塞式的解決方案:synchronized,來解決上述問題,即俗稱的【對象鎖】,它採用互斥的方式讓同一時刻至多隻有一個線程能持有【對象鎖】,其它線程再想獲取這個【對象鎖】時就會阻塞住。這樣就能保證擁有鎖的線程可以安全的執行臨界區內的代碼,不用擔心線程上下文切換

注意

雖然 java 中互斥和同步都可以採用 synchronized 關鍵字來完成,但它們還是有區別的:

  • 互斥是保證臨界區的競態條件發生,同一時刻只能有一個線程執行臨界區代碼

  • 同步是由於線程執行的先後、順序不同、需要一個線程等待其它線程運行到某個點

  • synchronized 只能鎖對象

1、synchronized

語法

synchronized(對象) // 線程1獲得對象後, 線程2不能獲得此對象,陷入阻塞(blocked)
{
    臨界區
}

解決上述問題(兩個線程對初始值爲 0 的靜態變量一個做自增,一個做自減,各做 5000 次,結果是 0 嗎?)

static int counter = 0;
static final Object room = new Object();
​
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            synchronized (room) {
                counter++;
            }
        }
    }, "t1");
​
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            synchronized (room) {
                counter--;
            }
        }
    }, "t2");
​
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    log.debug("{}",counter);
}

輸出,可以嘗試運行多次,結果都是0。

2、分析

如下圖所示

你可以做這樣的類比:

  • synchronized(對象) 中的對象,可以想象爲一個房間(room),有唯一入口(門)房間只能一次進入一人進行計算,線程 t1,t2 想象成兩個人

  • 當線程 t1 執行到 synchronized(room) 時就好比 t1 進入了這個房間,並鎖住了門拿走了鑰匙,在門內執行 count++ 代碼

  • 這時候如果 t2 也運行到了 synchronized(room) 時,它發現門被鎖住了,只能在門外等待,發生了上下文切換,阻塞住了

  • 這中間即使 t1 的 cpu 時間片不幸用完,被踢出了門外(不要錯誤理解爲鎖住了對象就能一直執行下去哦),這時門還是鎖住的,t1 仍拿着鑰匙,t2 線程還在阻塞狀態進不來,只有下次輪到 t1 自己再次獲得時間片時才能開門進入

  • 當 t1 執行完 synchronized{} 塊內的代碼,這時候纔會從 obj 房間出來並解開門上的鎖,喚醒 t2 線程把鑰匙給他。t2 線程這時纔可以進入 obj 房間,鎖住了門拿上鑰匙,執行它的 count-- 代碼

 

用圖來表示

3、思考

synchronized 實際是用對象鎖保證了臨界區內代碼的原子性,臨界區內的代碼對外是不可分割的,不會被線程切換所打斷。

爲了加深理解,請思考下面的問題

  • 如果把 synchronized(obj) 放在 for 循環的外面,如何理解?-- 原子性,意味者鎖住了整個循環。

  • 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 會怎樣運作?-- 鎖對象,要保護共享資源,必須對同一個對象進行加鎖

  • 如果 t1 synchronized(obj) 而 t2 沒有加會怎麼樣?如何理解?-- 鎖對象,t2沒有鎖,就不會被阻塞,就會正常執行。要保護共享資源,所有線程都必須要枷鎖。

4、面向對象改進

把需要保護的共享變量放入一個類

class Room {
    int value = 0;
​
    public void increment() {
        synchronized (this) {
            value++;
        }
    }
​
    public void decrement() {
        synchronized (this) {  // 保護自己的對象
            value--;
        }
    }
​
    public int get() {
        synchronized (this) {
            return value;
        }
    }
}
​
@Slf4j
public class Test1 {
    
    public static void main(String[] args) throws InterruptedException {
        Room room = new Room();
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.increment();
            }
        }, "t1");
​
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 5000; j++) {
                room.decrement();
            }
        }, "t2");
        t1.start();
        t2.start();
​
        t1.join();
        t2.join();
        log.debug("count: {}" , room.get());
    }
}

5、方法上的 synchronized

加在成員方法上

class Test{
    public synchronized void test() {
    
    }
}
等價於
class Test{
    public void test() {
        synchronized(this) {
        
        }
    }
}

加在靜態方法上

class Test{
    public synchronized static void test() {
​
    }
}
等價於
class Test{
    public static void test() {
        synchronized(Test.class) {  // 鎖住類對象
            
        }
    }
}

不加 synchronzied 的方法就好比不遵守規則的人,不去老實排隊(好比翻窗戶進去的)

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章