如何在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---1
B---1
B---2
B---3
A---2
A---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---1
A准备进入等待状态,放弃对锁的控制
B正在等待锁
B---1
B---2
B---3
B打印结束,准备调用lock.notify()
B唤醒A,A重新得到锁
A---2
A---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
开始执行线程:B
C执行完成
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:准备的时间:6960
C:准备的时间:1079
B:准备的时间:2713
C准备完毕,等待其他
B准备完毕,等待其他
A准备完毕,等待其他
A开始执行
C开始执行
B开始执行
五、子线程完成任务后,将结果返回给主线程
在实际开发中,我们经常需要创建子线程来执行一些耗时的任务。然后将执行结果传递给主线程。 在创建线程时,通常我们会使用Runnable
。
public interface Runnable {
public abstract void run();
}
其中该run()
在执行后不会返回任何结果,如果你想返回结果怎么办? 可以使用Callable
:
@FunctionalInterface
public interface Callable<V> {
/**
* 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(){
Callable<Integer> callable = new Callable<Integer>(){
[@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<Integer> 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
线程池来管理执行。
本文参考:https://www.tutorialdocs.com/article/java-inter-thread-communication.html