Java併發學習筆記(三):Wait\Notify、保護性暫停、生產者消費者、Park\Unpark、線程狀態轉換、活躍性、ReentryantLock、順序控制

一、Wait和Notify

1、原理

Wait和Notify用於等待。其原理爲:

  • Owner 線程發現條件不滿足,調用 wait 方法,即可進入 WaitSet 變爲 WAITING 狀態
  • WAITING 線程會在 Owner 線程調用 notify 或 notifyAll 時喚醒,但喚醒後並不意味者立刻獲得鎖,仍需進入 EntryList 重新競爭
  • BLOCKED 和 WAITING 的線程都處於阻塞狀態,不佔用 CPU 時間片
  • BLOCKED 線程會在 Owner 線程釋放鎖時喚醒

在這裏插入圖片描述

2、相關API

  • obj.wait() 讓進入 object 監視器的線程到 waitSet 等待,注意必須是獲得對象鎖的像鎖的線程才能調用。wait方法會釋放對象的鎖,進入 WaitSet 等待區,從而讓其他線程就機會獲取對象的鎖。無限制等待,直到 notify 爲止
  • obj.wait(long n)有時限的等待, 到 n 毫秒後結束等待,或是被 notify
  • obj.notify()在 object 上正在 waitSet 等待的線程中挑一個喚醒
  • obj.notifyAll() 讓 object 上正在 waitSet 等待的線程全部喚醒
    final static Object obj = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (obj) {
                log.debug("執行....");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代碼....");
            }
        }).start();

        new Thread(() -> {
            synchronized (obj) {
                log.debug("執行....");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代碼....");
            }
        }).start();

        sleep(2);
        log.debug("喚醒 obj 上其它線程");
        synchronized (obj) {
            obj.notify();
            // obj.notifyAll();  
        }
    }

notify 的結果:

20:00:53.096 [Thread-0] c.TestWaitNotify - 執行.... 
20:00:53.099 [Thread-1] c.TestWaitNotify - 執行.... 
20:00:55.096 [main] c.TestWaitNotify - 喚醒 obj 上其它線程 
20:00:55.096 [Thread-0] c.TestWaitNotify - 其它代碼.... 

notifyAll 的結果:

19:58:15.457 [Thread-0] c.TestWaitNotify - 執行.... 
19:58:15.460 [Thread-1] c.TestWaitNotify - 執行.... 
19:58:17.456 [main] c.TestWaitNotify - 喚醒 obj 上其它線程 
19:58:17.456 [Thread-1] c.TestWaitNotify - 其它代碼.... 
19:58:17.456 [Thread-0] c.TestWaitNotify - 其它代碼.... 

3、wait和sleep的異同

  • 它們 狀態都是 TIMED_WAITING
  • sleep 是 Thread 方法,而 wait 是 Object 的方法
  • sleep 不需要強制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用
  • sleep 在睡眠的同時,不會釋放對象鎖的,但 wait 在等待的時候會釋放對象鎖

4、wait/notify使用

    synchronized(lock){
        while (條件不成立) { //方法之虛假喚醒
            lock.wait();	//進入等待狀態
        }
    }

    synchronized(lock){
        lock.notifyAll(); //喚醒所有,再通過條件判斷喚醒的是否是自己
    }

二、同步設計模式之保護性暫停

1、定義

保護性暫停即 Guarded Suspension,用在一個線程等待另一個線程的執行結果

  • 一個結果需要從一個線程傳遞到另一個線程,讓他們關聯同一個 GuardedObject
  • 如果有結果不斷從一個線程到另一個線程那麼可以使用消息隊列(見生產者/消費者)
  • JDK 中,join 的實現、Future 的實現,採用的就是此模式
  • 因爲要等待另一方的結果,因此屬於同步模式

在這裏插入圖片描述

2、實現

該模式的實現主要依靠了wait和notifyAll方法,和上面的使用類似

/**
 * 保護性暫停設計模式實現
 */
class GuardedObject {

    private Object response;
    private final Object lock = new Object();

    public Object get(){
        synchronized (lock){
            while (response == null){
                try {
                    lock.wait(); //等待喚醒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        return response;
    }

    public void put(Object obj){
        synchronized (lock){
            response = obj;
            lock.notifyAll(); //喚醒
        }
    }
}

測試:

    public static void main(String[] args) {
        GuardedObject guarded = new GuardedObject();
        
        //t1線程等待response結果
        new Thread(()->{
            System.out.println("t1等待response");
            Object obj = guarded.get();
            System.out.println("t1獲得response");
            System.out.println("response = " + (String)obj);
        }, "t1").start();

        //t2線程設置response結果
        new Thread(()->{
            try {
                Thread.sleep(2000);
                System.out.println("t2設置response");
                guarded.put("123");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();

    }

結果:

t1等待response
t2設置response
t1獲得response
response = 123

3、帶有超時效果的保護性暫停

設置等待一段時間後仍沒有收到response就自動喚醒

/**
 * 保護性暫停設計模式實現
 */
class GuardedObject {

    private Object response;
    private final Object lock = new Object();

    public Object get(long timeout){
        synchronized (lock){
            long begin = System.currentTimeMillis(); //開始時間
            long timePassed = 0;
            while (response == null){
                long lastTime = timeout - timePassed; //剩餘時間
                if(lastTime <= 0)
                    break;
                try {
                    lock.wait(lastTime); //等待喚醒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                timePassed = System.currentTimeMillis() - begin; //已過去的時間
            }
        }
        return response;
    }

    public void put(Object obj){
        synchronized (lock){
            response = obj;
            lock.notifyAll(); //喚醒
        }
    }
}

4、Join的原理

Join的實現原理其就是我們上面的帶有超時效果的保護性暫停,其中只有兩點不同:

  • 當超時時間millis爲0時,表示一直等待,沒有超時時間
  • 環形的條件不是獲取某個值,而是線程結束

下面是join方法的源碼

    public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (millis == 0) {  //如果millis=0,表示不設置超時時間
            while (isAlive()) { //判斷線程是否存活
                wait(0);
            }
        } else {
            while (isAlive()) { //判斷線程是否存活
                long delay = millis - now; //剩餘時間
                if (delay <= 0) {
                    break;
                }
                wait(delay); //等待delay時間
                now = System.currentTimeMillis() - base; //已經過的時間
            }
        }
    }

5、多任務版 GuardedObject

引入:圖中 Futures 就好比居民樓一層的信箱(每個信箱有房間編號),左側的 t0,t2,t4 就好比等待郵件的居民,右 側的 t1,t3,t5 就好比郵遞員

在這裏插入圖片描述

分析:如果需要在多個類之間使用 GuardedObject 對象,作爲參數傳遞不是很方便,因此設計一個用來解耦的中間類, 這樣不僅能夠解耦【結果等待者】和【結果生產者】,還能夠同時支持多個任務的管理。

實現

新增 id 用來標識 Guarded Object

class GuardedObject {

    //多個GuardedObject時用於標識
    private int id;

    private Object response;

    public GuardedObject(int id) {
        this.id = id;
    }

    public Object get(long timeout){
        synchronized (this){
            long begin = System.currentTimeMillis(); //開始時間
            long timePassed = 0;

            while (response == null){

                long lastTime = timeout - timePassed; //剩餘時間
                if(lastTime <= 0)
                    break;

                try {
                    this.wait(lastTime); //等待喚醒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                timePassed = System.currentTimeMillis() - begin; //已過去的時間
            }
        }
        return response;
    }

    public void put(Object obj){
        synchronized (this){
            response = obj;
            this.notifyAll(); //喚醒
        }
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

中間解耦類:使用線程安全的Map來存儲GuardedObject,用於解耦

class MailBoxs{

    private static Map<Integer, GuardedObject> map = new Hashtable();

    private static int i = 1;

    //生成唯一id
    private  static synchronized int generateId(){
        return i++;
    }

    //生成GuardedObject
    public static GuardedObject createGuardedObject(){
        GuardedObject go = new GuardedObject(generateId());
        map.put(go.getId(), go);
        return go;
    }

    public static GuardedObject getGuardedObject(int i){
        return map.remove(i);
    }

    public static Set<Integer> getIds(){
        return map.keySet();
    }
}

業務相關類

//收信人
class People extends Thread{
    @Override
    public void run() {
        GuardedObject go = MailBoxs.createGuardedObject();
        System.out.println("開始收信" + go.getId());
        go.get(20000);
        System.out.println("收到信" + go.getId());
    }
}

//送信人
class Postman extends Thread{

    private int id;
    private String mail;

    public Postman(int id, String mail) {
        this.id = id;
        this.mail = mail;
    }

    @Override
    public void run() {
        GuardedObject go = MailBoxs.getGuardedObject(id);
        System.out.println("開始送信" + go.getId() + "內容爲" + mail);
        go.put(mail);

    }
}

測試

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++){
            new People().start();
        }
        Thread.sleep(3000);
        for (int id : MailBoxs.getIds()){
            new Postman(id, "內容"+id).start();
        }

    }

結果

開始收信1
開始收信2
開始收信3
開始送信2內容爲內容2
開始送信3內容爲內容3
收到信2
收到信3
開始送信1內容爲內容1
收到信1

三、異步設計模式之生產者/消費者

1、定義

要點:用於線程間通信的一種異步模式

  • 與前面的保護性暫停中的 GuardObject 不同,不需要產生結果和消費結果的線程一一對應
  • 消費隊列可以用來平衡生產和消費的線程資源
  • 生產者僅負責產生結果數據,不關心數據該如何處理,而消費者專心處理結果數據
  • 消息隊列是有容量限制的,滿時不會再加入數據,空時不會再消耗數據
  • JDK 中各種阻塞隊列,採用的就是這種模式

在這裏插入圖片描述

2、實現

消息類,包含id和消息體

//線程安全的消息類
final class Massage{

    private int id;

    private Object value;

    //沒有set方法,只能創建時初始化
    public Massage(int id, Object value) {
        this.id = id;
        this.value = value;
    }

    public int getId() {
        return id;
    }

    public Object getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "Massage{" +
                "id=" + id +
                ", value=" + value +
                '}';
    }
}

異步消息隊列實現

class MassageQueue{

    //消息隊列
    private LinkedList<Massage> queue = new LinkedList<>();

    //消息隊列的容量
    private int capcity;

    public MassageQueue(int capcity) {
        this.capcity = capcity;
    }

    //消費者消費消息
    public Massage get(){
        synchronized (queue){
            //檢查隊列是否爲空
            while (queue.isEmpty()) {
                try {
                    System.out.println("消費隊列已空");
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //從隊列的頭部獲取元素返回
            Massage massage = queue.removeFirst();
            System.out.println("消費者消息"+massage.getId());
            queue.notifyAll();
            return massage;
        }
    }

    //生產者者生產消息
    public void put(Massage massage){
        synchronized (queue) {
            //檢查隊列是否已滿
            while (queue.size() >= capcity) {
                try {
                    System.out.println("消息隊列是否已滿");
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + "生產消息"+massage.getId());
            queue.addLast(massage);
            queue.notifyAll();
        }
    }
}

測試:創建了三個生產者,一個消費者,消息隊列的容量爲2

    public static void main(String[] args) {
        MassageQueue queue = new MassageQueue(2);

        for (int i = 1; i <= 3; i++) {
            int finalI = i;
            new Thread(()->{
                queue.put(new Massage(finalI, "消息"+finalI));
            }, "生產者"+i).start();
        }

        new Thread(()->{
            while (true){
                try {
                    Thread.sleep(1000);
                    Massage massage = queue.get();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "消費者").start();
    }

結果:

生產者3生產消息3
生產者2生產消息2
消息隊列是否已滿
消費者消息3
生產者1生產消息1
消費者消息2
消費者消息1
消費隊列已空

四、 Park和 Unpark

它們是 LockSupport 類中的方法,用於暫停和喚醒線程

// 暫停當前線程 
LockSupport.park(); 
 
// 恢復某個線程的運行 
LockSupport.unpark(暫停線程對象);

先pack再unpack

Thread t1 = new Thread(() -> {
    log.debug("start...");
    sleep(1);
    log.debug("park...");
    LockSupport.park();
    log.debug("resume...");
}, "t1");
t1.start();

sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);

結果:

18:42:52.585 c.TestParkUnpark [t1] - start... 
18:42:53.589 c.TestParkUnpark [t1] - park... 
18:42:54.583 c.TestParkUnpark [main] - unpark... 
18:42:54.583 c.TestParkUnpark [t1] - resume... 

先unpack再pack

Thread t1 = new Thread(() -> {
    log.debug("start...");
    sleep(2);
    log.debug("park...");
    LockSupport.park();
    log.debug("resume...");
}, "t1");
t1.start();

sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);

結果:同樣可以解鎖,沒有順序要求

18:43:50.765 c.TestParkUnpark [t1] - start... 
18:43:51.764 c.TestParkUnpark [main] - unpark... 
18:43:52.769 c.TestParkUnpark [t1] - park... 
18:43:52.769 c.TestParkUnpark [t1] - resume... 

特點
與 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必須配合 Object Monitor 一起使用(必須使用synchronized加鎖),而 park,unpark 不必
  • park & unpark 是以線程爲單位來【阻塞】和【喚醒】線程,而 notify 只能隨機喚醒一個等待線程,notifyAll 是喚醒所有等待線程,就不那麼【精確】
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

park unpark 原理
每個線程都有自己的一個 Parker 對象,由三部分組成 _counter , _cond 和 _mutex

1、當前線程調用 Unsafe.park() 方法時(先於unpark)

  • (2)檢查 _counter ,如果爲 0,獲得 _mutex 互斥鎖

  • (3)線程進入 _cond 條件變量阻塞

  • (4)設置 _counter = 0
    在這裏插入圖片描述
    2、 調用 Unsafe.unpark(Thread_0) 方法

  • (1)調用 Unsafe.unpark(Thread_0) 方法,設置 _counter 爲 1

  • (2)喚醒 _cond 條件變量中的 Thread_0

  • (3) Thread_0 恢復運行

  • (4) 設置 _counter 爲 0

在這裏插入圖片描述

3、先調用 調用 Unsafe.unpark(Thread_0) 方法,再調用park方法

  • (1)調用 Unsafe.unpark(Thread_0) 方法,設置 _counter 爲 1
  • (2)當前線程調用 Unsafe.park() 方法
  • (3)檢查 _counter ,本情況爲 1,這時線程無需阻塞,繼續運行
  • (4)設置 _counter 爲 0
    在這裏插入圖片描述

五、重新理解線程狀態轉換 ★

Java線程轉換如下圖所示:
在這裏插入圖片描述
1、NEW–>RUNABLE

  • 當調用 t.start()方法時,由 NEW --> RUNNABLE

2、RUNABL<–>WATING

t 線程用 synchronized(obj) 獲取了對象鎖後

  • 調用 obj.wait()方法時,t 線程從RUNNABLE --> WAITING
  • 調用 obj.notify()obj.notifyAll()t.interrupt()
    • 如果競爭失敗,t 線程從 WAITING --> RUNNABLE,線程進入Monitor中EntryList
    • 如果競爭成功,t 線程從 WAITING --> BLOCKED,Monitor中Owner指向該線程

3、RUNNABLE <–> WAITING

  • 當前線程調用t.join()方法時,當前線程RUNNABLE --> WAITING
    • 注意是當前線程t 線程對象的監視器上等待
  • t 線程運行結束,或調用了當前線程的 interrupt() 時,當前線程從 WAITING --> RUNNABLE

4、RUNNABLE <–> WAITING

  • 當前線程調用LockSupport.park()方法會讓當前線程從 RUNNABLE --> WAITING
  • 調用 LockSupport.unpark(目標線程)或調用了線程 的interrupt(),會讓目標線程從 WAITING --> RUNNABLE

5、RUNNABLE <–> TIMED_WAITING

t 線程用 synchronized(obj)獲取了對象鎖後

  • 調用 obj.wait(long n)方法時,t 線程從 RUNNABLE --> TIMED_WAITING
  • t 線程等待時間超過了 n 毫秒,或調用 obj.notify()obj.notifyAll()t.interrupt()
    • 競爭鎖成功,t 線程從 TIMED_WAITING --> RUNNABLE
    • 競爭鎖失敗,t 線程從 TIMED_WAITING --> BLOCKED

6、 RUNNABLE <–> TIMED_WAITING

  • 當前線程調用t.join(long n)方法時,當前線程從 RUNNABLE --> TIMED_WAITING
    • 注意是當前線程t 線程對象的監視器上等待
  • 當前線程等待時間超過了 n 毫秒,或t 線程運行結束,或調用了當前線程的interrupt() 時,當前線程從 TIMED_WAITING --> RUNNABLE

7、RUNNABLE <–> TIMED_WAITING

  • 當前線程調用 Thread.sleep(long n) ,當前線程從 RUNNABLE --> TIMED_WAITING

  • 當前線程等待時間超過了 n 毫秒,當前線程從 TIMED_WAITING --> RUNNABLE

8、RUNNABLE <–> TIMED_WAITING

  • 當前線程調用 LockSupport.parkNanos(long nanos)LockSupport.parkUntil(long millis)時,當前線程從 RUNNABLE --> TIMED_WAITING
  • 調用LockSupport.unpark(目標線程)或調用了線程 的 interrupt(),或是等待超時,會讓目標線程從 TIMED_WAITING--> RUNNABLE

9、 RUNNABLE <–> BLOCKED

  • t 線程用 synchronized(obj) 獲取了對象鎖時如果競爭失敗,從 RUNNABLE --> BLOCKED
  • 持 obj 鎖線程的同步代碼塊執行完畢,會喚醒該對象上所有 BLOCKED 的線程重新競爭,如果其中 t 線程競爭 成功,從 BLOCKED --> RUNNABLE,其它失敗的線程仍然 BLOCKED

10、RUNNABLE <–> TERMINATED

  • 當前線程所有代碼運行完畢,進入 TERMINATED

六、多把鎖

將鎖的粒度細分

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

七、活躍性

活躍性關注的是“某件正確的事情最終會發生”。

例如,如果A線程等待B線程釋放其持有的資源,而B線程永遠都不釋放該資源,那麼線程A就會永遠的等待下去。這樣就不具備活躍性。

1、死鎖

一個線程需要同時獲取多把鎖,這時就容易發生死鎖,例如:t1 線程 獲得 A對象 鎖,接下來想獲取 B對象 的鎖 。t2 線程 獲得 B對象 鎖,接下來想獲取 A對象 的鎖 如:

        Object A = new Object();
        Object B = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (A) {
                log.debug("lock A");
                sleep(1);
                synchronized (B) {
                    log.debug("lock B");
                    log.debug("操作...");
                }
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            synchronized (B) {
                log.debug("lock B");
                sleep(0.5);
                synchronized (A) {
                    log.debug("lock A");
                    log.debug("操作...");
                }
            }
        }, "t2");
        t1.start();
        t2.start();

上面的代碼永遠都不會執行完畢,因爲發生了死鎖。

2、定位死鎖

檢測死鎖可以使用 jconsole工具,或者使用 jps 定位進程 id,再用 jstack 定位死鎖

首先使用jps查看進程ID
在這裏插入圖片描述
再使用jstack <進程ID>,查看當前的狀態:
在這裏插入圖片描述
在這裏插入圖片描述

同樣使用jconsole可以查看當前是否存在死鎖:連接當前進程,點擊線程---->檢測死鎖。

在這裏插入圖片描述

就當能看到當前死鎖的信息
在這裏插入圖片描述

3、活鎖

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

    static volatile int count = 10;
    static final Object lock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            while (count > 0) {
                try {
                    Thread.sleep(200);
                    count--;
                    System.out.println("t1 count: " + count);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1").start();
        
        new Thread(() -> {
            while (count < 20) {
                try {
                    Thread.sleep(200);
                    count++;
                    System.out.println("t2 count: " + count);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t2").start();
    }

上面程序雖然沒有互相持有資源,但是仍然會一直運行下去,這是因爲產生了活鎖。

在併發應用程序中,通過等待隨機長度的時間或者回退可以有效避免活鎖的發生。

4、飢餓

很多教程中把飢餓定義爲,一個線程由於優先級太低,始終得不到 CPU 調度執行,也不能夠結束,飢餓的情況不 易演示,講讀寫鎖時會涉及飢餓問題

先來看看使用順序加鎖的方式解決之前的死鎖問題
在這裏插入圖片描述
順序加鎖的解決方案

在這裏插入圖片描述

但是順序加鎖很有可能產生飢餓。

八、ReentrantLock

1、引入:哲學家進餐問題:

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

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

在這裏插入圖片描述

哲學家進餐進餐問題如果不加於干預就很容易產生死鎖問題,如果使用順序加鎖的方法解決死鎖又很容易產生飢餓的現象。這時候就需要使用ReentrantLock來解決

2、介紹

相對於 synchronized 它具備如下特點

  • 可中斷
  • 可以設置超時時間
  • 可以設置爲公平鎖
  • 支持多個條件變量

與 synchronized 一樣,都支持可重入

基本語法:

// 獲取鎖 
reentrantLock.lock(); 
try {    
	// 臨界區 
} finally {    
    // 釋放鎖    
    reentrantLock.unlock(); 
}

3、可重入

可重入是指同一個線程如果首次獲得了這把鎖,那麼因爲它是這把鎖的擁有者,因此有權利再次獲取這把鎖
如果是不可重入鎖,那麼第二次獲得鎖時,自己也會被鎖擋住

示例:

    private static final ReentrantLock lock = new ReentrantLock();

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

    public static void m1(){
        lock.lock();
        try {
            System.out.println("m1 in");
            m2();
        } finally {
            lock.unlock();
        }
    }

    public static void m2() {
        lock.lock();
        try {
            System.out.println("m2 in");
        } finally {
            lock.unlock();
        }
    }

結果如下圖所示,m1和m2中都對同一個ReentrantLock對象加了鎖,說明可能重入沒有問題。

在這裏插入圖片描述

4、可打斷性

可打斷表示加鎖時如果失敗進入阻塞隊列,則可以進行打斷,可以使用ReentrantLock::lockInterruptibly()方法設置,示例:

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                System.out.println("嘗試加鎖");
                lock.lockInterruptibly();//設置可打斷鎖
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("加鎖失敗,被打斷");
                return;
            }
            try {
                System.out.println("獲得鎖");
            } finally {
                lock.unlock();
            }
        }, "t1");

        lock.lock();
        t1.start();

        Thread.sleep(1000);
        System.out.println("打斷加鎖");
        t1.interrupt();
    }

結果:
在這裏插入圖片描述

5、鎖超時

鎖超時是指嘗試加鎖時,如果超過一段時間仍然獲得不到鎖就會自動放棄加鎖,可以使用使用ReentrantLock::trylock()方法設置,示例:

1)立即失敗

    public static void main(String[] args) {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            System.out.println("t1嘗試獲得鎖");
            if (!lock.tryLock()) {
                System.out.println("獲得鎖失敗, 返回");
                return;
            }
            try {
                System.out.println("t1獲得鎖成功");
            } finally {
                lock.unlock();
            }
        }, "t1");

        System.out.println("main獲得鎖");
        lock.lock();

        t1.start();
    }

結果:

在這裏插入圖片描述

2)超時失敗

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        Thread t1 = new Thread(() -> {
            System.out.println("t1嘗試獲得鎖");
            try {
                if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                    System.out.println("獲得鎖失敗, 返回");
                    return;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("被打斷,返回");
                return;
            }
            try {
                System.out.println("t1獲得鎖成功");
            } finally {
                lock.unlock();
            }
        }, "t1");

        System.out.println("main獲得鎖");
        lock.lock();
        t1.start();

        Thread.sleep(2000);
        lock.unlock();
    }

結果:
在這裏插入圖片描述

6、解決哲學家進餐問題

使用synchronized時會出現死鎖
在這裏插入圖片描述
可以更改爲使用ReentrantLock::trylock()方法,這樣就不會出現場死鎖
在這裏插入圖片描述

7、公平鎖

這裏的公平是指阻塞隊列(EntryList)中的線程按照先進先出的順序獲得鎖,ReentrantLock和Synchronized 默認是不公平的,也就是阻塞隊列中的線程通過爭搶的方式獲得鎖。

ReentrantLock可以使用通過構造函數ReentrantLock lock = new ReentrantLock(false);設置爲公平鎖,公平鎖一般沒有必要,會降低併發度,後面分析原理時會講解

8、條件變量

synchronized 中也有條件變量,就是我們講原理時那個 waitSet 休息室,當條件不滿足時進入 waitSet 等待 ReentrantLock 的條件變量比 synchronized 強大之處在於,它是支持多個條件變量的,這就好比

  • synchronized 是那些不滿足條件的線程都在一間休息室等消息
  • 而 ReentrantLock 支持多間休息室,有專門等煙的休息室、專門等早餐的休息室、喚醒時也是按休息室來喚 醒

使用要點:

  • await 前需要獲得鎖 await 執行後,會釋
  • 放鎖,進入 conditionObject 等待
  • await 的線程被喚醒(或打斷、或超時)取重新競爭 lock 鎖
  • 競爭 lock 鎖成功後,從 await 後繼續執行

語法:

        ReentrantLock lock = new ReentrantLock(); //創建ReentrantLock對象
        Condition condition = lock.newCondition();//創建條件變量對象
        
        new Thread(()->{
            lock.lock();	//加鎖
            try {
                condition.await();	//condition條件不滿足,進入等待隊列。與wait對應
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();
        
        Thread.sleep(1000);
        condition.signal();	 //喚醒等待condition條件變量的線程,與Notify對應

九、同步模式之順序控制

1、固定運行順序

比如,必須先 2 後 1 打印

1)wait notify 版

    //用來同步的對象
    static Object obj = new Object();
    // t2 運行標記, 代表 t2 是否執行過
    static boolean t2runed = false;

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                // 如果 t2 沒有執行過
                while (!t2runed) {
                    try {
                        // t1 先等一會
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();

                    }
                }
            }
            System.out.println(1);
        });

        Thread t2 = new Thread(() -> {
            System.out.println(2);
            synchronized (obj) {
                //修改運行標記
                t2runed = true;
                // 通知 obj 上等待的線程(可能有多個,因此需要用 notifyAll)
                obj.notifyAll();
            }
        });

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

2)park unpark版

可以看到,上面的實現上很麻煩:

  • 首先,需要保證先 wait 再 notify,否則 wait 線程永遠得不到喚醒。因此使用了『運行標記』來判斷該不該 wait
  • 第二,如果有些干擾線程錯誤地 notify 了 wait 線程,條件不滿足時還要重新等待,使用了 while 循環來解決 此問題
  • 最後,喚醒對象上的 wait 線程需要使用 notifyAll,因爲『同步對象』上的等待線程可能不止一個

park 和 unpark 方法比較靈活,他倆誰先調用,誰後調用無所謂。並且是以線程爲單位進行『暫停』和『恢復』, 不需要『同步對象』和『運行標記』

可以使用 LockSupport 類的 park 和 unpark 來簡化上面的題目:

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            System.out.println("1");
            LockSupport.park(); //暫停等待
        }, "t1");
        t1.start();


        new Thread(()->{
            System.out.println("2");
            LockSupport.unpark(t1); //喚醒t1
        }, "t2").start();
    }

2、交替輸出

線程 1 輸出 a 5 次,線程 2 輸出 b 5 次,線程 3 輸出 c 5 次。現在要求輸出 abcabcabcabcabc 怎麼實現

1) wait notify 版

打印控制類,用flag變量控制本次應該打印的a,loopNums控制打印次數

class SyncWaitNotify{

    //用於標記本次需要喚醒線程
    private int flag;
    //打印次數
    private int loopNums;

    public SyncWaitNotify(int flag, int loopNums) {
        this.flag = flag;
        this.loopNums = loopNums;
    }

    public void print(String msg, int flag){
        for (int i = 0; i < loopNums; i++) {
            synchronized (this) {
                while (this.flag != flag){ //如果自己不滿足條件,就一直等待
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(msg);
                this.flag = (flag + 1) % 3; //設置下一次喚醒的flag
                this.notifyAll(); //喚醒所有
            }
        }
    }
}

測試:

    public static void main(String[] args) {
        SyncWaitNotify waitNotify = new SyncWaitNotify(0, 5);
        new Thread(()->{
            waitNotify.print("a", 0);
        }).start();
        new Thread(()->{
            waitNotify.print("b", 1);
        }).start();
        new Thread(()->{
            waitNotify.print("c", 2);
        }).start();
    }

結果:

在這裏插入圖片描述

2)、Await和Signal實現

使用Condition作爲條件變量,使用await和Signal用於等待和喚醒

class SyncAwaitSignal extends ReentrantLock {
    private int loopNums;

    public SyncAwaitSignal(int loopNums) {
        this.loopNums = loopNums;
    }

    public void print(String msg, Condition current, Condition next){
        for (int i = 0; i < loopNums; i++) {
            lock();
            try {
                //等待信號
                current.await();
                //喚醒成功執行輸出,並喚醒下一個
                System.out.print(msg);
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                unlock();
            }
        }
    }
}

測試:

    public static void main(String[] args) throws InterruptedException {
        SyncAwaitSignal awaitSignal = new SyncAwaitSignal(5);
        Condition condition_a = awaitSignal.newCondition();
        Condition condition_b = awaitSignal.newCondition();
        Condition condition_c = awaitSignal.newCondition();

        new Thread(()->{
            awaitSignal.print("a", condition_a, condition_b);
        }).start();
        new Thread(()->{
            awaitSignal.print("b", condition_b, condition_c);
        }).start();
        new Thread(()->{
            awaitSignal.print("c", condition_c, condition_a);
        }).start();

        Thread.sleep(1000);
        System.out.println("開始");
        //運行開始時,給喚醒a
        awaitSignal.lock();
        try {
            condition_a.signal();
        } finally {
            awaitSignal.unlock();
        }
    }

結果:
在這裏插入圖片描述
3)、 Park Unpark

class SyncPackUnPack {
    private int loopNums;

    public SyncPackUnPack(int loopNums) {
        this.loopNums = loopNums;
    }
    
    public void print(String msg, Thread next){
        for (int i = 0; i < loopNums; i++) {
            //等待喚醒
            LockSupport.park();
            //輸出並喚醒下一個線程
            System.out.print(msg);
            LockSupport.unpark(next);
        }
    }
}

測試:

    static Thread t1, t2, t3;

    public static void main(String[] args) {
        SyncPackUnPack packUnPack = new SyncPackUnPack(5);
        t1 = new Thread(()->{
            packUnPack.print("a", t2);
        });
        t2 = new Thread(()->{
            packUnPack.print("b", t3);
        });
        t3 = new Thread(()->{
            packUnPack.print("c", t1);
        });

        t1.start();
        t2.start();
        t3.start();

        LockSupport.unpark(t1);
    }

結果:
在這裏插入圖片描述

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