Java並發編程——共享模型之管程(死鎖、哲學家就餐問題、ReentrantLock、順序控制)


承接上文

https://www.cnblogs.com/wkfvawl/p/15489569.html

一、多把鎖

  • 小故事
  • 一間大屋子有兩個功能:睡覺、學習,互不相干
  • 現在小南要學習,小女要睡覺,但如果只用一間屋子(一個對象鎖)的話,那么並發度很低。小南獲得鎖之后, 學完習之后, 小女才能進來睡覺。
@Slf4j(topic = "c.BigRoom")
public class BigRoomTest {
    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(() -> bigRoom.sleep(), "小南").start();
        new Thread(() -> bigRoom.study(), "小女").start();
    }
}

@Slf4j(topic = "c.BigRoom")
class BigRoom {
    public void sleep() {
        synchronized (this) {
            log.debug("sleeping 2 小時");
            Sleeper.sleep(2);
        }
    }

    public void study() {
        synchronized (this) {
            log.debug("study 1 小時");
            Sleeper.sleep(1);
        }
    }
}

改進方法是准備多個房間(多個對象鎖)

小南, 小女獲取不同的鎖即可

@Slf4j(topic = "c.BigRoom")
class BigRoom {

    private final Object studyRoom = new Object();

    private final Object bedRoom = new Object();

    public void sleep() {
        synchronized (bedRoom) {
            log.debug("sleeping 2 小時");
            Sleeper.sleep(2);
        }
    }

    public void study() {
        synchronized (studyRoom) {
            log.debug("study 1 小時");
            Sleeper.sleep(1);
        }
    }

}

將鎖的粒度細分

  • 好處,是可以增強並發度
  • 壞處,如果一個線程需要同時獲得多把鎖,就容易發生死鎖

二、 活躍性

    因為某種原因,使得代碼一直無法執行完畢,這樣的現象叫做 活躍性
    活躍性相關的一系列問題都可以用 ReentrantLock 進行解決。

2.1、死鎖 (重點)

    有這樣的情況:一個線程需要 同時獲取多把鎖,這時就容易發生死鎖

如:線程1獲取A對象鎖, 線程2獲取B對象鎖; 此時線程1又想獲取B對象鎖, 線程2又想獲取A對象鎖; 它們都等着對象釋放鎖, 此時就稱為死鎖

public static void main(String[] args) {
    final Object A = new Object();
    final Object B = new Object();
    
    new Thread(()->{
        synchronized (A) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (B) {

            }
        }
    }).start();

    new Thread(()->{
        synchronized (B) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (A) {

            }
        }
    }).start();
}

發生死鎖的必要條件 (重點)   

互斥條件

在一段時間內,一種資源只能被一個進程所使用

請求和保持條件

進程已經擁有了至少一種資源,同時又去申請其他資源。因為其他資源被別的進程所使用,該進程進入阻塞狀態,並且不釋放自己已有的資源

不可搶占條件

進程對已獲得的資源在未使用完成前不能被強占,只能在進程使用完后自己釋放

循環等待條件

發生死鎖時,必然存在一個進程——資源的循環鏈。

定位死鎖的方法

方式一、JPS + JStack 進程ID

  •  jps先找到JVM進程

  • jstack 進程ID

 在Java控制台中的Terminal中輸入 jps 指令可以查看正在運行中的進程ID,使用 jstack 進程ID 可以查看進程狀態。

方式二、 jconsole檢測死鎖

打開jconsole,連接到死鎖程序的線程

死鎖舉例 - 哲學家就餐問題 (重點)

有五位哲學家,圍坐在圓桌旁。

    他們只做兩件事,思考和吃飯,思考一會吃口飯,吃完飯后接着思考。
    吃飯時要用兩根筷子吃,桌上共有 5 根筷子,每位哲學家左右手邊各有一根筷子。
    如果筷子被身邊的人拿着,自己就得等待

當每個哲學家即線程持有一根筷子時,他們都在等待另一個線程釋放鎖,因此造成了死鎖。

public class TestDeadLock {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("蘇格拉底", c1, c2).start();
        new Philosopher("柏拉圖", c2, c3).start();
        new Philosopher("亞里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();
    }
}

@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    @Override
    public void run() {
        while (true) {
            // 嘗試獲得左手筷子
            synchronized (left) {
                // 嘗試獲得右手筷子
                synchronized (right) {
                    eat();
                }
            }
        }
    }

    Random random = new Random();
    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(0.5);
    }
}

class Chopstick {
    String name;
    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

通過jps, jstack 進程id查看死鎖原因
Found one Java-level deadlock: 發現了一個Java級別的死鎖

Found one Java-level deadlock:
=============================
"阿基米德":
  waiting to lock monitor 0x000000001fd941a8 (object 0x000000076b735028, a cn.itcast.n4.deadlock.v1.Chopstick),
  which is held by "蘇格拉底"
"蘇格拉底":
  waiting to lock monitor 0x000000001ccd33c8 (object 0x000000076b735068, a cn.itcast.n4.deadlock.v1.Chopstick),
  which is held by "柏拉圖"
"柏拉圖":
  waiting to lock monitor 0x000000001ccd3318 (object 0x000000076b7350a8, a cn.itcast.n4.deadlock.v1.Chopstick),
  which is held by "亞里士多德"
"亞里士多德":
  waiting to lock monitor 0x000000001ccd0a88 (object 0x000000076b7350e8, a cn.itcast.n4.deadlock.v1.Chopstick),
  which is held by "赫拉克利特"
"赫拉克利特":
  waiting to lock monitor 0x000000001ccd0b38 (object 0x000000076b735128, a cn.itcast.n4.deadlock.v1.Chopstick),
  which is held by "阿基米德"

Java stack information for the threads listed above:
===================================================
"阿基米德":
        at cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41)
        - waiting to lock <0x000000076b735028> (a cn.itcast.n4.deadlock.v1.Chopstick)
        - locked <0x000000076b735128> (a cn.itcast.n4.deadlock.v1.Chopstick)
"蘇格拉底":
        at cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41)
        - waiting to lock <0x000000076b735068> (a cn.itcast.n4.deadlock.v1.Chopstick)
        - locked <0x000000076b735028> (a cn.itcast.n4.deadlock.v1.Chopstick)
"柏拉圖":
        at cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41)
        - waiting to lock <0x000000076b7350a8> (a cn.itcast.n4.deadlock.v1.Chopstick)
        - locked <0x000000076b735068> (a cn.itcast.n4.deadlock.v1.Chopstick)
"亞里士多德":
        at cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41)
        - waiting to lock <0x000000076b7350e8> (a cn.itcast.n4.deadlock.v1.Chopstick)
        - locked <0x000000076b7350a8> (a cn.itcast.n4.deadlock.v1.Chopstick)
"赫???克利特":
        at cn.itcast.n4.deadlock.v1.Philosopher.run(TestDeadLock.java:41)
        - waiting to lock <0x000000076b735128> (a cn.itcast.n4.deadlock.v1.Chopstick)
        - locked <0x000000076b7350e8> (a cn.itcast.n4.deadlock.v1.Chopstick)

Found 1 deadlock.

避免死鎖的方法

  • 在線程使用鎖對象時, 采用固定加鎖的順序, 可以使用Hash值的大小來確定加鎖的先后
  • 盡可能縮減加鎖的范圍, 等到操作共享變量的時候才加鎖
  • 使用可釋放的定時鎖 (一段時間申請不到鎖的權限了, 直接釋放掉)

 

2.2 活鎖

活鎖出現在兩個線程 互相改變對方的結束條件,誰也無法結束。

@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {
    static volatile int count = 10;
    static final Object lock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            // 期望減到 0 退出循環
            while (count > 0) {
                sleep(0.2);
                count--;
                log.debug("count: {}", count);
            }
        }, "t1").start();
        new Thread(() -> {
            // 期望超過 20 退出循環
            while (count < 20) {
                sleep(0.2);
                count++;
                log.debug("count: {}", count);
            }
        }, "t2").start();
    }
}

避免活鎖的方法

在線程執行時,中途給予 不同的間隔時間, 讓某個線程先結束即可。

死鎖與活鎖的區別        

  • 死鎖是因為線程互相持有對象想要的鎖,並且都不釋放,最后到時線程阻塞,停止運行的現象。
  • 活鎖是因為線程間修改了對方的結束條件,而導致代碼一直在運行,卻一直運行不完的現象。

2.3 飢餓

  • 某些線程因為優先級太低,導致一直無法獲得資源的現象。
  • 在使用順序加鎖時,可能會出現飢餓現象

三、 ReentrantLock (重點)

相對於synchronized,ReentrantLock 所具備的特點
 

支持鎖重入
        可重入鎖是指同一個線程如果首次獲得了這把鎖,那么因為它是這把鎖的擁有者,因此 有權利再次獲取這把鎖

可中斷
        lock.lockInterruptibly() : 可以被其他線程打斷的中斷鎖

可以設置超時時間
        lock.tryLock(時間) : 嘗試獲取鎖對象, 如果超過了設置的時間, 還沒有獲取到鎖, 此時就退出阻塞隊列, 並釋放掉自己擁有的鎖

可以設置為公平鎖
        (先到先得) 默認是非公平, true為公平 new ReentrantLock(true)

支持多個條件變量( 有多個waitset)
        (可避免虛假喚醒) - lock.newCondition()創建條件變量對象; 通過條件變量對象調用 await/signal方法, 等待/喚醒

synchronized是關鍵字級別的加鎖,ReentrantLock則是對象級別的,基本語法如下:

//獲取ReentrantLock對象
private ReentrantLock lock = new ReentrantLock();
//加鎖
lock.lock();
try {
    //需要執行的代碼
}finally {
    //釋放鎖
    lock.unlock();
}

3.1、支持鎖重入

  • 可重入鎖是指同一個線程如果首次獲得了這把鎖,那么因為它是這把鎖的擁有者,因此 有權利再次獲取這把鎖
  • 如果是不可重入鎖,那么第二次獲得鎖時,自己也會被鎖擋住
@Slf4j(topic = "c.TestReentrant")
public class TestReentrant {
    static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        method1();
    }

    public static void method1() {
        lock.lock();
        try {
            log.debug("execute method1");
            method2();
        } finally {
            lock.unlock();
        }
    }

    public static void method2() {
        lock.lock();
        try {
            log.debug("execute method2");
            method3();
        } finally {
            lock.unlock();
        }
    }

    public static void method3() {
        lock.lock();
        try {
            log.debug("execute method3");
        } finally {
            lock.unlock();
        }
    }
}

3.2、可中斷

(針對於lockInterruptibly()方法獲得的中斷鎖) 直接退出阻塞隊列, 獲取鎖失敗

synchronized 和 reentrantlock.lock() 的鎖, 是不可被打斷的; 也就是說別的線程已經獲得了鎖, 我的線程就需要一直等待下去. 不能中斷        可被中斷的鎖, 通過lock.lockInterruptibly()獲取的鎖對象, 可以通過調用阻塞線程的interrupt()方法


    如果某個線程處於阻塞狀態,可以調用其interrupt方法讓其停止阻塞,獲得鎖失敗
        處於阻塞狀態的線程,被打斷了就不用阻塞了,直接停止運行
    可中斷的鎖, 在一定程度上可以被動的減少死鎖的概率, 之所以被動, 是因為我們需要手動調用阻塞線程的interrupt方法;

測試使用lock.lockInterruptibly()可以從阻塞隊列中,打斷

 private static void test1() {
        ReentrantLock lock = new ReentrantLock();

        Thread t1 = new Thread(() -> {
            log.debug("啟動...");
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.debug("等鎖的過程中被打斷");
                return;
            }
            try {
                log.debug("獲得了鎖");
            } finally {
                lock.unlock();
            }
        }, "t1");

        //主線程上鎖
        lock.lock();
        log.debug("獲得了鎖");
        t1.start();
        try {
            sleep(1);
            t1.interrupt();
            log.debug("執行打斷");
        } finally {
            lock.unlock();
        }
    }

3.3、鎖超時 (lock.tryLock())

直接退出阻塞隊列, 獲取鎖失敗

防止無限制等待, 減少死鎖

  •  使用 lock.tryLock() 方法會返回獲取鎖是否成功。如果成功則返回true,反之則返回false。
  •  並且tryLock方法可以設置指定等待時間,參數為:tryLock(long timeout, TimeUnit unit) , 其中timeout為最長等待時間,TimeUnit為時間單位

獲取鎖的過程中, 如果超過等待時間, 或者被打斷, 就直接從阻塞隊列移除, 此時獲取鎖就失敗了, 不會一直阻塞着 ! (可以用來實現死鎖問題)

不設置等待時間, 立即失敗

@Slf4j(topic = "c.ReentrantTest")
public class ReentrantTest {

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("嘗試獲得鎖");
            // 此時肯定獲取失敗, 因為主線程已經獲得了鎖對象
            if (!lock.tryLock()) {
                log.debug("獲取立刻失敗,返回");
                return;
            }
            try {
                log.debug("獲得到鎖");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("獲得到鎖");
        t1.start();
        // 主線程2s之后才釋放鎖
        sleep(2);
        log.debug("釋放了鎖");
        lock.unlock();
    }
}

 設置等待時間, 超過等待時間還沒有獲得鎖, 失敗, 從阻塞隊列移除該線程

@Slf4j(topic = "c.ReentrantTest")
public class ReentrantTest {

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            log.debug("嘗試獲得鎖");
            try {
                // 設置等待時間, 超過等待時間 / 被打斷, 都會獲取鎖失敗; 退出阻塞隊列
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    log.debug("獲取鎖超時,返回");
                    return;
                }
            } catch (InterruptedException e) {
                log.debug("被打斷了, 獲取鎖失敗, 返回");
                e.printStackTrace();
                return;
            }
            try {
                log.debug("獲得到鎖");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        log.debug("獲得到鎖");
        t1.start();
//        t1.interrupt();
        // 主線程2s之后才釋放鎖
        sleep(2);
        log.debug("main線程釋放了鎖");
        lock.unlock();
    }
}

超時的打印

 中斷的打印

通過lock.tryLock()來解決, 哲學家就餐問題 (重點)

lock.tryLock(時間) : 嘗試獲取鎖對象, 如果超過了設置的時間, 還沒有獲取到鎖, 此時就退出阻塞隊列, 並釋放掉自己擁有的鎖

@Override
    public void run() {
        while (true) {
            // 獲得了left左手邊筷子 (針對五個哲學家, 它們剛開始肯定都可獲得左筷子)
            if (left.tryLock()) {
                try {
                    // 此時發現它的right筷子被占用了, 使用tryLock(),
                    // 嘗試獲取失敗, 此時它就會將自己左筷子也釋放掉
                    // 臨界區代碼
                    if (right.tryLock()) {//嘗試獲取右手邊筷子, 如果獲取失敗, 則會釋放左邊的筷子
                        try {
                            eat();
                        } finally {
                            right.unlock();
                        }
                    }
                } finally {
                    left.unlock();
                }
            }
        }
    }

3.4、公平鎖 new ReentrantLock(true)

 ReentrantLock默認是非公平鎖, 可以指定為公平鎖。

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

在線程獲取鎖失敗,進入阻塞隊列時,先進入的會在鎖被釋放后先獲得鎖。這樣的獲取方式就是公平的。一般不設置ReentrantLock為公平的, 沒必要,會降低並發度

Synchronized底層的Monitor鎖就是不公平的, 和誰先進入阻塞隊列是沒有關系的。

//默認是不公平鎖,需要在創建時指定為公平鎖
ReentrantLock lock = new ReentrantLock(true);

公平鎖與非公平鎖

公平鎖 (new ReentrantLock(true))

  • 公平鎖, 可以把競爭的線程放在一個先進先出的阻塞隊列上
  • 只要持有鎖的線程執行完了, 喚醒阻塞隊列中的下一個線程獲取鎖即可; 此時先進入阻塞隊列的線程先獲取到鎖

非公平鎖 (synchronized, new ReentrantLock())

  • 非公平鎖, 當阻塞隊列中已經有等待的線程A了, 此時后到的線程B, 先去嘗試看能否獲得到鎖對象. 如果獲取成功, 此時就不需要進入阻塞隊列了. 這樣以來后來的線程B就先活的到鎖了

所以公平和非公平的區別 : 線程執行同步代碼塊時, 是否回去嘗試獲取鎖, 如果會嘗試獲取鎖, 那就是非公平的, 如果不會嘗試獲取鎖, 直接進入阻塞隊列, 再等待被喚醒, 那就是公平的

如果不進如隊列呢? 線程一直嘗試獲取鎖不就行了?       

一直嘗試獲取鎖, 在synchronized輕量級鎖升級為重量級鎖時, 做的一個優化, 叫做自旋鎖, 一般很消耗資源, cpu一直空轉, 最后獲取鎖也失敗, 所以不推薦使用。在jdk6對於自旋鎖有一個機制, 在重試獲得鎖指定次數就失敗等等

3.5、條件變量

(可避免虛假喚醒) - lock.newCondition()創建條件變量對象; 通過條件變量對象調用await/signal方法, 等待/喚醒

  • Synchronized 中也有條件變量,就是Monitor監視器中的 waitSet等待集合,當條件不滿足時進入waitSet 等待
  • ReentrantLock 的條件變量比 synchronized 強大之處在於,它是 支持多個條件變量。
  • 這就好比synchronized 是那些不滿足條件的線程都在一間休息室等通知; (此時會造成虛假喚醒), 而 ReentrantLock 支持多間休息室,有專門等煙的休息室、專門等早餐的休息室、喚醒時也是按休息室來喚醒; (可以避免虛假喚醒)

使用要點:

  • await 前需要 獲得鎖
  • await 執行后,會釋放鎖,進入 conditionObject (條件變量)中等待
  • await 的線程被喚醒(或打斷、或超時)取重新競爭 lock ;競爭 lock 鎖成功后,從 await 后繼續執行
  • signal 方法用來喚醒條件變量(等待室)匯總的某一個等待的線程
  • signalAll方法, 喚醒條件變量(休息室)中的所有線程

 ReentrantLock可以設置多個條件變量(多個休息室), 相對於synchronized底層monitor鎖中waitSet

@Slf4j(topic = "c.ConditionVariable")
public class ConditionVariable {
    private static boolean hasCigarette = false;
    private static boolean hasTakeout = false;
    private static final ReentrantLock lock = new ReentrantLock();
    // 等待煙的休息室
    static Condition waitCigaretteSet = lock.newCondition();
    // 等外賣的休息室
    static Condition waitTakeoutSet = lock.newCondition();

    public static void main(String[] args) {

        new Thread(() -> {
            lock.lock();
            try {
                log.debug("有煙沒?[{}]", hasCigarette);

                while (!hasCigarette) {
                    log.debug("沒煙,先歇會!");
                    try {
                        // 此時小南進入到 等煙的休息室
                        waitCigaretteSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("煙來咯, 可以開始干活了");
            } finally {
                lock.unlock();
            }
        }, "小南").start();

        new Thread(() -> {
            lock.lock();
            try {
                log.debug("外賣送到沒?[{}]", hasTakeout);
                while (!hasTakeout) {
                    log.debug("沒外賣,先歇會!");
                    try {
                        // 此時小女進入到 等外賣的休息室
                        waitTakeoutSet.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("外賣來咯, 可以開始干活了");
            } finally {
                lock.unlock();
            }
        }, "小女").start();

        sleep(1);
        new Thread(() -> {
            lock.lock();
            try {
                log.debug("送外賣的來咯~");
                hasTakeout = true;
                // 喚醒等外賣的小女線程
                waitTakeoutSet.signal();
            } finally {
                lock.unlock();
            }
        }, "送外賣的").start();

        sleep(1);
        new Thread(() -> {
            lock.lock();
            try {
                log.debug("送煙的來咯~");
                hasCigarette = true;
                // 喚醒等煙的小南線程
                waitCigaretteSet.signal();
            } finally {
                lock.unlock();
            }
        }, "送煙的").start();
    }
}

四、同步模式之順序控制 (案例)

  • 假如有兩個線程, 線程A打印1, 線程B打印2.
  • 要求: 程序先打印2, 再打印1

4.1、Wait/Notify版本實現

里面一些代碼細節參見之前的博客,wait/notify的正確使用:https://www.cnblogs.com/wkfvawl/p/15489569.html#scroller-5

@Slf4j(topic = "c.Test25")
public class Test25 {
    //定義鎖對象
    static final Object lock = new Object();
    // 表示 t2 是否運行過
    static boolean t2runned = false;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                //使用while循環來解決虛假喚醒
                while (!t2runned) {
                    try {
                        // 進入等待(waitset), 會釋放鎖
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("1");
            }
        }, "t1");


        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                log.debug("2");
                t2runned = true;
                lock.notify();
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}

4.2、ReentrantLock的await/signal版本實現

@Slf4j(topic = "c.SyncPrintWaitTest")
public class SyncPrintWaitTest {

    public static final ReentrantLock lock = new ReentrantLock();
    public static Condition condi tion = lock.newCondition();
    // t2線程釋放執行過
    public static boolean t2Runned = false;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            lock.lock();
            try {
                // 臨界區
                while (!t2Runned) {
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("1");
            } finally {
                lock.unlock();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            lock.lock();
            try {
                log.debug("2");
                t2Runned = true;
                condition.signal();
            } finally {
                lock.unlock();
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}

4.3、使用LockSupport中的park/unpart

@Slf4j(topic = "c.SyncPrintWaitTest")
public class SyncPrintWaitTest {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
// 暫停 LockSupport.park(); log.debug(
"1"); }, "t1"); t1.start(); new Thread(() -> { log.debug("2");
// 喚醒t1 LockSupport.unpark(t1); },
"t2").start(); } }

五、同步模式之交替輸出

需求

  • 線程1 輸出 a 5次, 線程2 輸出 b 5次, 線程3 輸出 c 5次。現在要求輸出 abcabcabcabcabcab

5.1、wait/notify版本

@Slf4j(topic = "c.Test27")
public class Test27 {
    public static void main(String[] args) {
        // 最開始的等待標記是1 循環次數5次
        WaitNotify wn = new WaitNotify(1, 5);
        new Thread(() -> {
            wn.print("a", 1, 2);
        }).start();
        new Thread(() -> {
            wn.print("b", 2, 3);
        }).start();
        new Thread(() -> {
            wn.print("c", 3, 1);
        }).start();
    }
}

/*
輸出內容       等待標記     下一個標記
   a           1             2
   b           2             3
   c           3             1
 */
class WaitNotify {
    // 打印               a           1             2
    public void print(String str, int waitFlag, int nextFlag) {
        for (int i = 0; i < loopNumber; i++) {
            synchronized (this) {
                while(flag != waitFlag) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(str);
                // 修改等待標記 讓下一個線程打印
                flag = nextFlag;
                // 喚醒等待線程
                this.notifyAll();
            }
        }
    }

    // 當前等待標記
    private int flag;
    // 循環次數
    private int loopNumber;

    public WaitNotify(int flag, int loopNumber) {
        this.flag = flag;
        this.loopNumber = loopNumber;
    }
}

5.2、await/signal版本

@Slf4j(topic = "c.TestWaitNotify")
public class TestAwaitSignal {
    public static void main(String[] args) throws InterruptedException {
        AwaitSignal awaitSignal = new AwaitSignal(5);
        Condition a_condition = awaitSignal.newCondition();
        Condition b_condition = awaitSignal.newCondition();
        Condition c_condition = awaitSignal.newCondition();

        new Thread(() -> {
            awaitSignal.print("a", a_condition, b_condition);
        }, "a").start();

        new Thread(() -> {
            awaitSignal.print("b", b_condition, c_condition);
        }, "b").start();

        new Thread(() -> {
            awaitSignal.print("c", c_condition, a_condition);
        }, "c").start();

        Thread.sleep(1000);
        System.out.println("==========開始=========");
        awaitSignal.lock();
        try {
            a_condition.signal();  //首先喚醒a線程
        } finally {
            awaitSignal.unlock();
        }
    }
}

class AwaitSignal extends ReentrantLock {
    private final int loopNumber;

    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }
    // 參數1 打印內容;參數2 進入那一間休息室;參數3 下一間休息室
    public void print(String str, Condition condition, Condition next) {
        for (int i = 0; i < loopNumber; i++) {
            //加鎖 繼承自ReentrantLock
            lock();
            try {
                try {
                    //進入休息室等待
                    condition.await();
                    //System.out.print("i:==="+i);
                    System.out.print(str);
                    // 喚醒下一個休息室的線程
                    next.signal();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } finally {
                //解鎖
                unlock();
            }
        }
    }
}

5.3、LockSupport的park/unpark實現

park和unpark沒有對象鎖的概念了,停止和恢復線程的運行都是以線程自身為單位的,所以實現更為簡單。

@Slf4j(topic = "c.TestWaitNotify")
public class TestParkUnpark {
    static Thread a;
    static Thread b;
    static Thread c;

    public static void main(String[] args) {
        ParkUnpark parkUnpark = new ParkUnpark(5);

        a = new Thread(() -> {
            parkUnpark.print("a", b);
        }, "a");

        b = new Thread(() -> {
            parkUnpark.print("b", c);
        }, "b");

        c = new Thread(() -> {
            parkUnpark.print("c", a);
        }, "c");

        a.start();
        b.start();
        c.start();
        //主線程先喚醒a
        LockSupport.unpark(a);

    }
}

class ParkUnpark {
    private final int loopNumber;

    public ParkUnpark(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    public void print(String str, Thread nextThread) {
        for (int i = 0; i < loopNumber; i++) {
            //當前線程先暫停
            LockSupport.park();
            System.out.print(str);
            //喚醒下一個線程
            LockSupport.unpark(nextThread);
        }
    }
}

六、本章小結

本章我們需要重點掌握的是

  • 分析多線程訪問共享資源時,哪些代碼片段屬於臨界區

  • 使用 synchronized 互斥解決臨界區的線程安全問題

    • 掌握 synchronized 鎖對象語法

    • 掌握 synchronzied 加載成員方法和靜態方法語法

    • 掌握 wait/notify 同步方法

  • 使用 lock 互斥解決臨界區的線程安全問題

    • 掌握 lock 的使用細節:可打斷、鎖超時、公平鎖、條件變量

  • 學會分析變量的線程安全性、掌握常見線程安全類的使用

  • 了解線程活躍性問題:死鎖、活鎖、飢餓

  • 應用方面

    • 互斥:使用 synchronized 或 Lock 達到共享資源互斥效果

    • 同步:使用 wait/notify 或 Lock 的條件變量來達到線程間通信效果

  • 原理方面

    • monitor、synchronized 、wait/notify 原理(monitor是在jvm層面實現的,源碼是c++,java基本的monitor則是ReentrantLock,實現細節二者可以相互參照

    • synchronized 進階原理

    • park & unpark 原理

  • 模式方面

    • 同步模式之保護性暫停

    • 異步模式之生產者消費者

    • 同步模式之順序控制


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM