共计 7883 个字符,预计需要花费 20 分钟才能阅读完成。
世界以痛吻我,要我报之以歌 —— 泰戈尔《飞鸟集》
尽管通常每个子线程只须要实现本人的工作,然而有时咱们心愿多个线程一起工作来实现一个工作,这就波及到线程间通信。
对于线程间通信本文波及到的办法和类包含:thread.join()
、object.wait()
、object.notify()
、CountdownLatch
、CyclicBarrier
、FutureTask
、Callable
。
接下来将用几个例子来介绍如何在 Java 中实现线程间通信:
- 如何让两个线程顺次执行,即一个线程期待另一个线程执行实现后再执行?
- 如何让两个线程以指定的形式有序相交执行?
- 有四个线程:A、B、C、D,如何实现 D 在 A、B、C 都同步执行结束后执行?
- 三个运动员离开筹备,而后在每个人筹备好后同时开始跑步。
- 子线程实现工作后,将后果返回给主线程。
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
上述代码的执行流程如下:
- 首先咱们创立一个由 A 和 B 共享的对象锁:
lock = new Object()
; - 当 A 拿到锁时,先打印 1,而后调用
lock.wait()
办法进入期待状态,而后交出锁的控制权; - B 不会被执行,直到 A 调用该
lock.wait()
办法开释控制权并且 B 取得锁; - B 拿到锁后打印 1,2,3,而后调用
lock.notify()
办法唤醒正在期待的 A; - 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
用来实现这种类型的通信。它的根本用法是:
- 创立一个计数器,并设置一个初始值,
CountdownLatch countDownLatch = new CountDownLatch(3)
; - 调用
countDownLatch.await()
进入期待状态,直到计数值变为 0; - 在其余线程调用
countDownLatch.countDown()
,该办法会将计数值减一; - 当计数器的值变为
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
,其根本用法为:
- 首先创立一个公共对象
CyclicBarrier
,并设置同时期待的线程数,CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
- 这些线程同时开始筹备,筹备好后,须要期待他人筹备好,所以调用
cyclicBarrier.await()
办法期待他人; - 当指定的须要同时期待的线程都调用了该
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
到线程池来治理执行。
参考文章:
https://www.tutorialdocs.com/…