Java中实现线程间通信的实例教程
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
Java中实现线程间通信的实例教程
⽬录
前⾔
1. 如何让两个线程依次执⾏?
2. 如何让两个线程按照指定的⽅式有序相交?
3. 线程 D 在A、B、C都同步执⾏完毕后执⾏
4. 三个运动员分开准备同时开跑
5. ⼦线程将结果返回给主线程
总结
前⾔
虽然通常每个⼦线程只需要完成⾃⼰的任务,但是有时我们希望多个线程⼀起⼯作来完成⼀个任务,这就涉及到线程间通信。
关于线程间通信本⽂涉及到的⽅法和类包括:thread.join()、object.wait()、object.notify()、CountdownLatch、CyclicBarrier、FutureTask、Callable。
接下来将⽤⼏个例⼦来介绍如何在Java中实现线程间通信:
1. 如何让两个线程依次执⾏,即⼀个线程等待另⼀个线程执⾏完成后再执⾏?
2. 如何让两个线程以指定的⽅式有序相交执⾏?
3. 有四个线程:A、B、C、D,如何实现 D 在 A、B、C 都同步执⾏完毕后执⾏?
4. 三个运动员分开准备,然后在每个⼈准备好后同时开始跑步。
5. ⼦线程完成任务后,将结果返回给主线程。
1. 如何让两个线程依次执⾏?
假设有两个线程:A 和 B,这两个线程都可以按照顺序打印数字,代码如下:
public class Test01 {
public static void main(String[] args) throws InterruptedException {
demo1();
}
public static void demo1() {
Thread a = new Thread(() -> {
printNumber("A");
});
Thread b = new Thread(() -> {
printNumber("B");
});
a.start();
b.start();
}
public static void printNumber(String threadName) {
int i = 0;
while (i++ < 3) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadName + " print: " + i);
}
}
}
得到的结果如下:
A print: 1
B print: 1
B print: 2
A print: 2
A print: 3
B print: 3
可以看到 A 和 B 同时打印数字,如果我们希望 B 在 A 执⾏完成之后开始执⾏,那么可以使⽤ thread.join() ⽅法实现,代码如下:
public static void demo2() {
Thread a = new Thread(() -> {
printNumber("A");
});
Thread b = new Thread(() -> {
System.out.println("B 等待 A 执⾏");
try {
a.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
printNumber("B");
});
a.start();
b.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 完成打印。
thread.join() ⽅法的作⽤就是阻塞当前线程,等待调⽤ join() ⽅法的线程执⾏完毕后再执⾏后⾯的代码。
查看 join() ⽅法的源码,内部是调⽤了 join(0) ,如下:
public final void join() throws InterruptedException {
join(0);
}
查看 join(0) 的源码如下:
// 注意这⾥使⽤了 sychronized 加锁,锁对象是线程的实例对象
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
// 调⽤ join(0) 执⾏下⾯的代码
if (millis == 0) {
// 这⾥使⽤ while 循环的⽬的是为了避免虚假唤醒
// 如果当前线程存活则调⽤ wait(0), 0 表⽰永久等待,直到调⽤ notifyAll() 或者 notify() ⽅法
// 当线程结束的时候会调⽤ notifyAll() ⽅法
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
从源码中可以看出 join(long millis) ⽅法是通过 wait(long timeout) (Object 提供的⽅法)⽅法实现的,调⽤ wait ⽅法之前,当前线程必须获得对象的锁,所以此 join ⽅法使⽤了 synchronized 加锁,锁对象是线程的实例对象。
其中 wait(0)⽅法会让当前线程阻塞等待,直到另⼀个线程调⽤此对象的 notify() 或者 notifyAll() ⽅法才会继续执⾏。
当调⽤ join ⽅法的线程结束的时候会调⽤ notifyAll() ⽅法,所以 join() ⽅法可以实现⼀个线程等待另⼀个调⽤ join() 的线程结束后再执⾏。
虚假唤醒:⼀个线程在没有被通知、中断、超时的情况下被唤醒;
虚假唤醒可能导致条件不成⽴的情况下执⾏代码,破坏被锁保护的约束关系;
为什么使⽤ while 循环来避免虚假唤醒:
在 if 块中使⽤ wait ⽅法,是⾮常危险的,因为⼀旦线程被唤醒,并得到锁,就不会再判断 if 条件⽽执⾏ if 语句块
外的代码,所以建议凡是先要做条件判断,再 wait 的地⽅,都使⽤ while 循环来做,循环会在等待之前和之后对
条件进⾏测试。
2. 如何让两个线程按照指定的⽅式有序相交?
如果现在我们希望 B线程在 A 线程打印 1 后⽴即打印 1,2,3,然后 A 线程继续打印 2,3,那么我们需要更细粒度的锁来控制执⾏顺序。
在这⾥,我们可以利⽤ object.wait() 和 object.notify() ⽅法,代码如下:
public static void demo3() {
Object lock = new Object();
Thread A = new Thread(() -> {
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(() -> {
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
上述代码的执⾏流程如下:
1. ⾸先我们创建⼀个由 A 和 B 共享的对象锁: lock = new Object();
2. 当A拿到锁时,先打印1,然后调⽤lock.wait()⽅法进⼊等待状态,然后交出锁的控制权;
3. B 不会被执⾏,直到 A 调⽤该lock.wait()⽅法释放控制权并且 B 获得锁;
4. B拿到锁后打印1,2,3,然后调⽤lock.notify()⽅法唤醒正在等待的A;
5. A 唤醒后继续打印剩余的 2,3。
为了便于理解,我将上⾯的代码添加了⽇志,代码如下:
public static void demo3() {
Object lock = new Object();
Thread A = new Thread(() -> {
System.out.println("INFO:A 等待获取锁");
synchronized (lock) {
System.out.println("INFO:A 获取到锁");
System.out.println("A 1");
try {
System.out.println("INFO:A 进⼊ waiting 状态,放弃锁的控制权");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("INFO:A 被 B 唤醒继续执⾏");
System.out.println("A 2");
System.out.println("A 3");
}
});
Thread B = new Thread(() -> {
System.out.println("INFO:B 等待获取锁");
synchronized (lock) {
System.out.println("INFO:B 获取到锁");
System.out.println("B 1");
System.out.println("B 2");
System.out.println("B 3");
System.out.println("INFO:B 执⾏结束,调⽤ notify ⽅法唤醒 A");
lock.notify();
}
});
A.start();
B.start();
}
得到的结果如下:
INFO:A 等待获取锁
INFO:A 获取到锁
A 1
INFO:A 进⼊ waiting 状态,放弃锁的控制权
INFO:B 等待获取锁
INFO:B 获取到锁
B 1
B 2
B 3
INFO:B 执⾏结束,调⽤ notify ⽅法唤醒 A
INFO:A 被 B 唤醒继续执⾏
A 2
A 3
3. 线程 D 在A、B、C都同步执⾏完毕后执⾏
thread.join() 前⾯介绍的⽅法允许⼀个线程在等待另⼀个线程完成运⾏后继续执⾏。
但是如果我们将A、B、C依次加⼊到D线程中,就会让A、B、C依次执⾏,⽽我们希望它们三个同步运⾏。
我们要实现的⽬标是:A、B、C三个线程可以同时开始运⾏,各⾃独⽴运⾏完成后通知D;D 不会开始运⾏,直到 A、B 和 C 都运⾏完毕。
所以我们 CountdownLatch ⽤来实现这种类型的通信。
它的基本⽤法是:
1. 创建⼀个计数器,并设置⼀个初始值, CountdownLatch countDownLatch = new CountDownLatch(3);
2. 调⽤countDownLatch.await()进⼊等待状态,直到计数值变为0;
3. 在其他线程调⽤countDownLatch.countDown(),该⽅法会将计数值减⼀;
4. 当计数器的值变为 0 时,countDownLatch.await()等待线程中的⽅法会继续执⾏下⾯的代码。
实现代码如下:
public static void runDAfterABC() {
int count = 3;
CountDownLatch countDownLatch = new CountDownLatch(count);
new Thread(() -> {
System.out.println("INFO: D 等待 A B C 运⾏完成");
try {
countDownLatch.await();
System.out.println("INFO: A B C 运⾏完成,D 开始运⾏");
System.out.println("D is working");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
for (char threadName = 'A'; threadName <= 'C' ; threadName++) {
final String name = String.valueOf(threadName);
new Thread(() -> {
System.out.println(name + " is working");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " finished");
countDownLatch.countDown();
}).start();
}
}
得到的结果如下:
INFO: D 等待 A B C 运⾏完成
A is working
B is working
C is working
C finished
B finished
A finished
INFO: A B C 运⾏完成,D 开始运⾏
D is working
其实CountDownLatch它本⾝就是⼀个倒数计数器,我们把初始的count值设置为3。
D运⾏的时候,⾸先调⽤该countDownLatch.await()⽅法检查计数器的值是否为0,如果不是0则保持等待状态. A、B、C 运⾏完毕后,分别使⽤countDownLatch.countDown()⽅法将倒数计数器减1。
计数器将减为 0,然后通知await()⽅法结束等待,D开始继续执⾏。
因此,CountDownLatch适⽤于⼀个线程需要等待多个线程的情况。
4. 三个运动员分开准备同时开跑
这⼀次,A、B、C这三个线程都需要分别准备,等三个线程都准备好后开始同时运⾏,我们应该如何做到这⼀点?CountDownLatch可以⽤来计数,但完成计数的时候,只有⼀个线程的⼀个await()⽅法会得到响应,所以多线程不能在同⼀时间被触发。
为了达到线程相互等待的效果,我们可以使⽤该CyclicBarrier,其基本⽤法为:
1. ⾸先创建⼀个公共对象CyclicBarrier,并设置同时等待的线程数,CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
2. 这些线程同时开始准备,准备好后,需要等待别⼈准备好,所以调⽤cyclicBarrier.await()⽅法等待别⼈;
3. 当指定的需要同时等待的线程都调⽤了该cyclicBarrier.await()⽅法时,意味着这些线程准备好了,那么这些线程就会开
始同时继续执⾏。
想象⼀下有三个跑步者需要同时开始跑步,所以他们需要等待其他⼈都准备好,实现代码如下:
public static void runABCWhenAllReady() {
int count = 3;
CyclicBarrier cyclicBarrier = new CyclicBarrier(count);
Random random = new Random();
for (char threadName = 'A'; threadName <= 'C' ; threadName++) {
final String name = String.valueOf(threadName);
new Thread(() -> {
int prepareTime = random.nextInt(10000);
System.out.println(name + " 准备时间:" + prepareTime);
try {
Thread.sleep(prepareTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " 准备好了,等待其他⼈");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(name + " 开始跑步");
}).start();
}
}
得到结果如下:
A 准备时间:1085
B 准备时间:7729
C 准备时间:8444
A 准备好了,等待其他⼈
B 准备好了,等待其他⼈
C 准备好了,等待其他⼈
C 开始跑步
A 开始跑步
B 开始跑步
CyclicBarrier 的作⽤就是等待多个线程同时执⾏。
5. ⼦线程将结果返回给主线程
在实际开发中,往往我们需要创建⼦线程来做⼀些耗时的任务,然后将执⾏结果传回主线程。
那么如何在 Java 中实现呢?⼀般在创建线程的时候,我们会把 Runnable 对象传递给 Thread 执⾏,Runable 的源码如下:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
可以看到 Runable 是⼀个函数式接⼝,该接⼝中的 run ⽅法没有返回值,那么如果要返回结果,可以使⽤另⼀个类似的接⼝Callable。
函数式接⼝:只有⼀个⽅法的接⼝
Callable 接⼝的源码如下:
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
可以看出,最⼤的区别Callable在于它返回的是泛型。
那么接下来的问题是,如何将⼦线程的结果传回去呢?Java 有⼀个类,FutureTask,它可以与⼀起⼯作Callable,但请注意,get⽤于获取结果的⽅法会阻塞主线程。
FutureTask 本质上还是⼀个 Runnable,所以可以直接传到 Thread 中。
⽐如我们想让⼦线程计算1到100的总和,并将结果返回给主线程,代码如下:
public static void getResultInWorker() {
Callable<Integer> callable = () -> {
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()");
Integer result = futureTask.get();
System.out.println("执⾏的结果:" + result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
得到的结果如下:
开始执⾏ futureTask.get()
⼦任务开始执⾏
⼦任务执⾏完成并返回结果
执⾏的结果:5050
可以看出在主线程调⽤futureTask.get()⽅法时阻塞了主线程;然后Callable开始在内部执⾏并返回操作的结果;然后futureTask.get()得到结果,主线程恢复运⾏。
在这⾥我们可以了解到,FutureTask和Callable可以直接在主线程中获取⼦线程的结果,但是它们会阻塞主线程。
当然,如果你不希望阻塞主线程,可以考虑使⽤ExecutorService把FutureTask到线程池来管理执⾏。
参考⽂章:
…
总结
到此这篇关于Java中实现线程间通信的⽂章就介绍到这了,更多相关Java线程间通信内容请搜索以前的⽂章或继续浏览下⾯的相关⽂章希望⼤家以后多多⽀持!。