如何在Java中实现线程间通信
虽然通常每个子线程只需要完成自己的任务,但有时我们可能希望多个线程一起工作来完成一个任务,这涉及到线程间的通信。
将使用几个示例来解释如何在Java中实现线程间通信:
- 如何使两个线程按照顺序执行?
- 如何使两个线程以指定的方式有序交叉
- 有四个线程:A、B、C、D, D在A、B和C同步执行完成后执行
- 三名运动员分开准备,然后他们在每个运动员准备好后开始跑步
- 子线程完成任务后,它会将结果返回给主线程
一、如何使两个线程按照顺序执行?
假设有两个线程:线程A和线程B,两个线程都可以顺序打印出三个数字(1-3)。
private static void demo1(){ Thread A = new Thread(new Runnable() { @Override public void run() { printNumber("A"); } }); Thread B = new Thread(new Runnable() { @Override public void run() { printNumber("B"); } }); A.start(); B.start();}
printNumber(String),用于依次打印1、2、3三个数字
private static void printNumber(String threadName) { int i = 0; while (i++ < 3) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(threadName + ": print:" + i); }}
我们得到的结果是:
A: print:1 B: print:1 B: print:2 A: print:2 B: print:3 A: print:3
可以同时看到A和B打印数字。 那么,如果我们希望B在A打印完毕后开始打印怎么办?可以使用该thread.join()
方法,代码如下:
private static void demo2(){ Thread A = new Thread(new Runnable() { @Override public void run() { printNumber("A"); } }); Thread B = new Thread(new Runnable() { @Override public void run() { System.out.println("B开始等待A..."); try { A.join(); } catch (InterruptedException e) { e.printStackTrace(); } printNumber("B"); } }); B.start(); A.start();}
现在等到的结果是:
B开始等待A... A: print:1 A: print:2 A: print:3 B: print:1 B: print:2 B: print:3
所以我们可以看到A.join()
方法可以使B等到A完成打印。
二、如何使两个线程以指定的方式有序交叉?
在A打印1之后开始打印B的1、2、3,等B打印完成,继续打印A的2、3。 可以利用 object.wait()
和 object.notify()
private static void demo3(){ Object lock = new Object(); Thread A = new Thread(new Runnable() { @Override public void run() { synchronized (lock) { System.out.println("A---1"); try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("A---2"); System.out.println("A---3"); } } }); Thread B = new Thread(new Runnable(){ @Override public void run() { synchronized (lock) { System.out.println("B---1"); System.out.println("B---2"); System.out.println("B---3"); lock.notify(); } } }); A.start(); B.start();}
结果如下:
A---1B---1B---2B---3A---2A---3
- 首先,我们创建一个由A和B共享的对象锁:
Object lock = new Object();
- 当A获得锁定时,它首先打印1,然后调用
lock.wait();
方法使其进入等待状态,然后移交锁定控制 - 在A调用
lock.wait();
释放控制的方法并且B获得锁定之前,B不会被执行 - B在获得锁定后打印1、2、3,然后调用
lock.notify();
方法唤醒正在等待的A - A被唤醒后继续打印剩余的2、3
修改demo3方法,加入日志添加到上面的代码中,以便更容易理解。
private static void demo3(){ Object lock = new Object(); Thread A = new Thread(new Runnable() { @Override public void run() { System.out.println("A正在等待锁 "); synchronized (lock) { System.out.println("A得到锁"); System.out.println("A---1"); try { System.out.println("A准备进入等待状态,放弃对锁的控制"); lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("B唤醒A,A重新得到锁"); System.out.println("A---2"); System.out.println("A---3"); } } }); Thread B = new Thread(new Runnable(){ @Override public void run() { System.out.println("B正在等待锁 "); synchronized (lock) { System.out.println("B---1"); System.out.println("B---2"); System.out.println("B---3"); System.out.println("B打印结束,准备调用lock.notify()"); lock.notify(); } } }); A.start(); B.start();}
结果如下:
A正在等待锁 A得到锁A---1A准备进入等待状态,放弃对锁的控制B正在等待锁 B---1B---2B---3B打印结束,准备调用lock.notify()B唤醒A,A重新得到锁A---2A---3
三、D在A、B和C同步执行完成后执行?
前面介绍thread.join()
方法允许一个线程在等待另一个线程完成后继续执行。但是我们如果将A、B、C有序的连接到D线程中,将会依次执行A、B、C,我们是希望它们三个同步允许。 要实现的目标:三个线程A、B、C同时开始运行,每个线程在完成独立运行后通知D。在A、B、C完成之前,D不会执行。所以我们用CountdownLatch
来实现这种类型的通信。它的基本用法:
- 创建一个计数器,并且设置一个初始值,
CountDownLatch countDownLatch = new CountDownLatch(3);
- 在等待线程中调用
countDownLatch.wait();
,进入等待状态,直到计数值变为0 - 在其他线程中调用
countDownLatch.countDown();
,计数值减1 - 当
countDown()
其他线程中的方法将计数值变为0时,countDownLatch.wait();
等待线程中的方法将立即退出并且继续执行
代码如下:
private static void demo4(){ int woeker = 3; CountDownLatch countDownLatch = new CountDownLatch(woeker); Thread D = new Thread(new Runnable(){ @Override public void run() { System.out.println("D正在等待其他三个线程"); try { countDownLatch.await(); System.out.println("全部完成,D开始执行"); } catch (InterruptedException e) { e.printStackTrace(); } } }); D.start(); for(char threadName='A';threadName<='C';threadName++){ final String tName = String.valueOf(threadName); new Thread(new Runnable(){ @Override public void run() { System.out.println("开始执行线程:"+tName); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(tName+"执行完成"); countDownLatch.countDown(); } }).start(); } // for(char threadName='A';threadName<='C';threadName++)}
执行结果:
D正在等待其他三个线程开始执行线程:A开始执行线程:C开始执行线程:BC执行完成B执行完成A执行完成全部完成,D开始执行
实际上CountDownLatch
本身就是计时器,我们将初始计数值设置为3,当D运行时,它首先会调用countDownLatch.await();
来检查计数器值是否为0,如果不为0,它将保持等待状态,A、B、C各自调用countDownLatch.countDown();
,在完成后将计数器递减1。当这三个完成时,计数器的值为0,然后D中的countDownLatch.await();
被触发,开始执行D。 因此CountDownLatch
适用于一个线程需要等待多个线程的情况。
四、3名运动员准备跑步
三名运动员分开准备,然后他们在每个运动员准备好后开始跑步。 三个线程A、B、C中的每一个都需要单独准备,然后他们三个都准备好后,开始同时执行。 在上面说CountDownLatch
计时器,完成计数的时候,只有一个线程的一个await()
方法会得到响应,所以多线程不能再同一时间被触发。 为了实现多线程相互等待的效果,我们可以使用CyclicBarrier
,基本用法:
- 首先创建一个公共对象
CyclicBarrier
,并且设置同时等待的线程数,CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
- 这些线程开始同时准备,准备好之后,他们需要等待其它线程完成准备,
cyclicBarrier.await();
等待其他人 - 当等待指定的线程调用
cyclicBarrier.await();
时,就说明这些线程已经准备就绪,那么这些线程将开始同时继续执行。
代码如下:
private static void demo5(){ int runner = 3; CyclicBarrier cyclicBarrier = new CyclicBarrier(runner); final Random random = new Random(); for(char runnerName = 'A'; runnerName <= 'C'; runnerName++){ final String rName = String.valueOf(runnerName); new Thread(new Runnable(){ [@Override](https://my.oschina.net/u/1162528) public void run() { long prepareTime = random.nextInt(10000) + 100; System.out.println(rName + ":准备的时间:" + prepareTime); try { Thread.sleep(prepareTime); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(rName + "准备完毕,等待其他"); try { cyclicBarrier.await(); } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } //所有人都准备完成 System.out.println(rName + "开始执行"); } }).start(); }}
结果如下:
A:准备的时间:6960C:准备的时间:1079B:准备的时间:2713C准备完毕,等待其他B准备完毕,等待其他A准备完毕,等待其他A开始执行C开始执行B开始执行
五、子线程完成任务后,将结果返回给主线程
在实际开发中,我们经常需要创建子线程来执行一些耗时的任务。然后将执行结果传递给主线程。 在创建线程时,通常我们会使用Runnable
。
public interface Runnable { public abstract void run();}
其中该run()
在执行后不会返回任何结果,如果你想返回结果怎么办? 可以使用Callable
:
@FunctionalInterfacepublic interface Callable{ /** * Computes a result, or throws an exception if unable to do so. * * [@return](https://my.oschina.net/u/556800) computed result * [@throws](https://my.oschina.net/throws) Exception if unable to compute a result */ V call() throws Exception;}
如何将子线程的结果传回去? 例如,子线程计算从1到100的总和,并将结果返回给主线程。
private static void demo6(){ Callablecallable = new Callable (){ [@Override](https://my.oschina.net/u/1162528) public Integer call() throws Exception { System.out.println("任务开始"); Thread.sleep(1000); int result = 0; for(int i=0;i<=100;i++){ result += i; } System.out.println("任务结束并且返回结果"); return result; } }; FutureTask futureTask = new FutureTask<>(callable); new Thread(futureTask).start(); try { System.out.println("主线程调用 futureTask.get()之前"); System.out.println("Result:" + futureTask.get()); System.out.println("主线程调用 futureTask.get()之后"); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); }}
结果如下:
调用 futureTask.get()之前任务开始任务结束并且返回结果Result:5050调用 futureTask.get()之后
可以看出当主线程调用futureTask.get()
方法时,它会阻塞主线程,然后Callable
开始在内部执行,并且返回操作结果,然后futureTask.get()
获取结果,主线程恢复运行。
在这里我们了解到FutureTask
和Callable
可以直接在主线程中获取子线程的结果,但是他们会阻塞主线程,如果你不想阻止主线程,可以考虑使用ExecutorService
线程池来管理执行。
本文参考: