Java中多線程協調

使用 synchronized修飾,表示該方法是加鎖的方法。使用相同this鎖的方法,在任意時刻只有一個方法會被執行,在多線程中是競爭關係。除此之外多線程還存在依賴關係。例如,一個線程須等待另一個線程返回結果後,才能繼續執行。Java中提供了相應的機制。

1、synchronized、wait、notify

考慮一個實際的流水線作業場景,一個線程負責生產產品,另一個線程負責在流水線上裝配。由於生產時間不確定,爲了不錯過傳送帶上的產品,裝配線程需要不斷檢查當前傳送帶上有無產品。爲了簡化邏輯,這裏只有一個線程負責生產,一個線程負責裝配。

import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;

public class ThreadDispatch {
    public static void main(String[] args) {
        var pack = new PackQueue();
        // 負責每隔1秒裝入一次數據
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    pack.addPack(Integer.toString(i));
                }
            }
        };

        // 每隔0.6秒鐘取出數據
        Thread t2 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    try {
                        Thread.sleep(600);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    String s = pack.getPack();
                    System.out.println(s);
                }
            }
        };
        t1.start();
        t2.start();
    }
}

class PackQueue {
    private Queue<String> q = new LinkedList<>();

    public void addPack(String s) {
        this.q.add(s);
    }

    public String getPack() {
        if (this.q.isEmpty()) {
            return "empty";
        }
        return this.q.remove();
    }

}
//empty
//0
//empty
//1
//2
//empty
//3
//empty
//4
//5
//empty
//6
//empty
//7
//8
//empty
//9
//empty
//empty
//empty

以上邏輯是使用循環實現的。爲了不錯過傳送帶上的商品,裝配線程t2需不斷定時檢查。這對設置檢查的頻率提出了要求。頻率稍慢會錯過產品,頻率過快會浪費性能。最好的方式是,線程1生產好產品後,通知線程2裝配,這樣解決了“來不及”和“速度過快”的問題。

public class ThreadDispatch {
    public static void main(String[] args) {
        var pack = new PackQueue();
        // 負責每隔1秒裝入一次數據
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    pack.addPack(Integer.toString(i));
                }
            }
        };

        // 檢查有無數據,沒有就等待
        Thread t2 = new Thread() {
            @Override
            public void run() {
                try {
                    while(true) {
                        String s = pack.getPack();
                        System.out.println(s);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
            }
        };
        t1.start();
        t2.start();
    }
}

class PackQueue {
    private Queue<String> q = new LinkedList<>();

    public synchronized void addPack(String s) {
        this.q.add(s);
        this.notify();
    }

    public synchronized String getPack() throws InterruptedException {
        if (this.q.isEmpty()) {
        // return "empty";
            this.wait();
        }
        return this.q.remove();
    }
}
// 0
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9

優化之後,裝配線程t2 不再“無節制”檢查工作區,而是等待t1線程通知後工作。需要注意的是,這裏只有一個裝配線程t2,如果有多個線程,需要調用this.notifyAll取代this.notify,表示喚醒所有正在等待this鎖的線程。喚醒多個線程,最終也只會有一個線程獲取this鎖,其餘線程繼續等待。另外,t2 線程在 t1 線程停止生產後永遠也醒不過來了。考慮爲 t2 線程指定一個wait超時時間,超時後會自動醒來。

2、 ReentrantLock、Condition

java5中引入了高級的處理併發的java.util.concurrent包,相比較synchronized機制,提供了嘗試獲取鎖、超時等待等更多功能;不同於synchronized 在Java語言層面實現自動釋放鎖而不必考慮異常,ReentrantLock由Java代碼實現,因此需要正確捕獲異常和釋放鎖。使用 ReentrantLock 重寫示例:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class PackQueue {
    private Queue<String> q = new LinkedList<>();
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    public void addPack(String s) {
        lock.lock();
        try {
            this.q.add(s);
            condition.signalAll();
        } finally {
            lock.unlock();
        }

    }

    public String getPack() throws InterruptedException {
        lock.lock();
        try {
            if (this.q.isEmpty()) {
                condition.await(2, TimeUnit.SECONDS);
            }
            return this.q.remove();
        } finally {
            lock.unlock();
        }

    }
}

值得注意的是,判斷隊列爲空後,調用 condition.await(2, TimeUnit.SECONDS); 表示線程會自動超時醒來。由於此時隊列爲空,調用remove方法會報錯;不過線程可以自動喚醒了。總結一下:

 

 
synchronized
ReentrantLock
加鎖 通常使用 synchronized 修飾方法,表示使用this實例加鎖
private final Lock lock = new ReentrantLock();
lock.lock()
釋放鎖 自動釋放
lock.unlock();
線程等待
this.wait();
private final Condition condition = lock.newCondition();
condition.await();
線程喚醒
this.notify();
this.notifyAll();
condition. signal();
condition.signalAll();
鎖類型 可重入鎖 可重入鎖
嘗試獲取鎖 不支持
lock.tryLock(1, TimeUnit.SECONDS)

超時自動喚醒

不支持
condition.await(2, TimeUnit.SECONDS);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章