線程通信的一百種寫法


線程通信是多線程應用開發中最常見的操作,除了wait notify方法,jdk提供了多種多樣的api可以實現線程通信。
下面通過一個簡單的題目看看jdk的多種方式實現線程通信。

題目

使用兩個線程,一個線程打印1-26,另一個打印A-Z,交替打印。

方式1:wait notify

看到題目首先想到的就是wait notify,線程1每打印一次就等待,並喚醒線程2,線程2打印完,也等待並喚醒線程1
代碼實現:
首先準備數據

public class PrintDemo {
    private static final int LENGTH = 26;
    private static int[] arrI = new int[LENGTH];
    private static char[] arrC = new char[LENGTH];

	//init data
    static{
        for (int i = 0; i < LENGTH; i++) {
            arrI[i] = i+1;
            arrC[i] = (char) ('A' + i);
        }
    }
    //...  

wait notify方式:啓動兩個線程輪流打印。代碼注意的點就是,wait 和 notify方法必須在同步塊內使用,for循環結束要notify阻塞線程,否則程序不會退出。

public void printWait(){
        new Thread(() -> {
            synchronized (this){
                for (int i : arrI) {
                    System.out.print(i);
                    try {
                        this.notify();
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //notify blocked Thread after print otherwise the program won't exit
                this.notify();
            }
        },"t1").start();
        new Thread(() -> {
            synchronized (this){
                for (char i : arrC) {
                    try {
                        System.out.print(i);
                        this.notify();
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //notify blocked Thread after print otherwise the program won't exit
                this.notify();
            }
        },"t2").start();
    }

最終程序輸出
1A2B3C4D5E6F7G8H9I10J11K12L13M14N15O16P17Q18R19S20T21U22V23W24X25Y26Z
如果要控制字母先打印,可以用CountDownLatch控制,當然也可以在t1線程把wait提到print前面,下面貼出用CountDownLatch的代碼:

public void printWaitCDL(){
        CountDownLatch latch = new CountDownLatch(1);
        new Thread(() -> {
            try {
                latch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (this){
                for (int i : arrI) {
                    System.out.print(i);
                    try {
                        this.notify();
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //notify blocked Thread after print otherwise the program won't exit
                this.notify();
            }
        },"t1").start();
        new Thread(() -> {
            synchronized (this){
                for (char i : arrC) {
                    try {
                        System.out.print(i);
                        latch.countDown();
                        this.notify();
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //notify blocked Thread after print otherwise the program won't exit
                this.notify();
            }
        },"t2").start();
    }

注意不能把latch.await()放到synchronized裏面,否則會死鎖。

方式2:LockSupport

	Thread t1 = null , t2 = null;
    public void printLockSupport(){
        t1 = new Thread(()->{
            for (int i : arrI) {
                System.out.print(i);
                LockSupport.unpark(t2);
                LockSupport.park();
            }
        },"t1");
        t2 = new Thread(()->{
            for (char i : arrC) {
                LockSupport.park();
                System.out.print(i);
                LockSupport.unpark(t1);
            }
        },"t1");
        t1.start();
        t2.start();
    }

原理是一樣的,只不過LockSupport不用必須在synchronized裏面使用,LockSupport允許精準喚醒某個線程。

方式3:Atomic

利用Atomic類的可見性,也可以完成功能,思路是設置一個共享變量控制當前輪到哪個線程執行。

	static volatile boolean t1turn = true;
    static AtomicInteger count = new AtomicInteger(0);
    void printAtomic(){
        new Thread(()->{
            while (count.get()<LENGTH){
                while (t1turn){
                    System.out.print(arrI[count.get()]);
                    t1turn = false;
                }
            }
        },"t1").start();
        new Thread(()->{
            while (count.get()<LENGTH){
                while (!t1turn){
                    System.out.print(arrC[count.getAndIncrement()]);
                    t1turn = true;
                }
            }
        },"t2").start();
    }

這裏設置了兩個共享變量,一個用於控制線程執行權,一個用作循環的計數。這裏volatile和Atomic可以起到同樣的效果,關鍵就是保證變量的可見性。

方式4:忙循環

上面的實現方式可用一個忙循環改造一下。

enum Turn {T1,T2}
    volatile Turn turn = Turn.T1;
    void printVolatile(){
        new Thread(()->{
            for (int i : arrI) {
                while (turn != Turn.T1){}
                System.out.print(i);
                turn = Turn.T2;
            }
        },"t1").start();
        new Thread(()->{
            for (char i : arrC) {
                while (turn != Turn.T2){}
                System.out.print(i);
                turn = Turn.T1;
            }
        },"t2").start();
    }

以上兩種方式區別就是不進行線程掛起,通過循環判斷volatile共享變量的狀態來實現交替打印,實際上並沒有線程通信。

方式5:ReentrantLock Condition

wait notify + synchronized 這一套也可以用 ReentrantLock的Condition來代替:

ReentrantLock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    void printLockCondition(){
        new Thread(()->{
            try {
                lock.lock();
                for (int i : arrI) {
                    System.out.print(i);
                    condition.signal();
                    condition.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
            	//let other Thread run, and unlock
                condition.signal();
                lock.unlock();
            }
        },"t1").start();
        new Thread(()->{
            try {
                lock.lock();
                for (char i : arrC) {
                    System.out.print(i);
                    condition.signal();
                    condition.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                condition.signal();
                lock.unlock();
            }
        },"t2").start();
    }

方式6:ReentrantLock 2 Condition

可以用兩個Condition,優化一下Condition的實現方式,實現精準喚醒某個線程。

	ReentrantLock lock2 = new ReentrantLock();
    Condition conditionT1 = lock2.newCondition();
    Condition conditionT2 = lock2.newCondition();
    void printLock2Condition(){
        new Thread(()->{
            try {
                lock.lock();
                for (int i : arrI) {
                    System.out.print(i);
                    conditionT2.signal();
                    conditionT1.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
            	//let other Thread run, and unlock
                conditionT2.signal();
                lock.unlock();
            }
        },"t1").start();
        new Thread(()->{
            try {
                lock.lock();
                for (char i : arrC) {
                    System.out.print(i);
                    conditionT1.signal();
                    conditionT2.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                conditionT1.signal();
                lock.unlock();
            }
        },"t2").start();
    }

方式7:BlockingQueue

要實現線程阻塞自然可以想到阻塞隊列,利用BlockingQueue的take阻塞方法,也可以實現兩個線程交替輸出。

	static BlockingQueue<Integer> q1 = new ArrayBlockingQueue<>(1);
    static BlockingQueue<Character> q2 = new ArrayBlockingQueue<>(1);
    void printBlockingQueue(){
        new Thread(()->{
            for (int i : arrI) {
                try {
                    q1.put(i);
                    System.out.print(q2.take());//blocked
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t1").start();
        new Thread(()->{
            for (char i : arrC) {
                try {
                    System.out.print(q1.take());
                    q2.put(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t2").start();
    }

方式8:TransferQueue

BlockingQueue的take方法是獲取的時候阻塞,還可以用TransferQueue的transfer方法,放入就阻塞,直到元素被取走。

	static TransferQueue<String> tq = new LinkedTransferQueue<>();
    void printTransferQueue(){
        new Thread(()->{
            for (int i : arrI) {
                try {
                    tq.transfer(String.valueOf(i));
                    System.out.print(tq.take());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t1").start();
        new Thread(()->{
            for (char i : arrC) {
                try {
                    System.out.print(tq.take());
                    tq.transfer(String.valueOf(i));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        },"t2").start();
    }

總結

兩個線程交替執行,可以利用wait notify方法,volatile變量線程可見性,Lock API,LockSupport的park unpark方法,阻塞隊列API以及其他方式來實現。

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