线程通信的一百种写法

文章目录

      • 题目
      • 方式1:wait notify
      • 方式2:LockSupport
      • 方式3:Atomic
      • 方式4:忙循环
      • 方式5:ReentrantLock Condition
      • 方式6:ReentrantLock 2 Condition
      • 方式7:BlockingQueue
      • 方式8:TransferQueue
      • 总结

线程通信是多线程应用开发中最常见的操作,除了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以及其他方式来实现。

你可能感兴趣的:(java并发,JDK)