Java多线程之以7种方式让主线程等待子线程结束
记一次主线程等待子线程结束的多种方法的学习
在学习多线程时,最开始遇到的问题其实是“计算子线程运行时间”,写到最后发现本文和标题更为符合,但是仍然基于问题:“在主线程中获取子线程的运行时间”。
while循环
对于“主线程如何获取子线程总运行时间”的问题,最开始想到的是使用while循环进行轮询:
Thread t = new Thread(() -> {
//子线程进行字符串连接操作
int num = 1000;
String s = "";
for (int i = 0; i < num; i++) {
s += "Java";
}
System.out.println("t Over");
});
//开始计时
long start = System.currentTimeMillis();
System.out.println("start = " + start);
t.start();
long end = 0;
while(t.isAlive() == true){//t.getState() != State.TERMINATED这两种判断方式都可以
end = System.currentTimeMillis();
}
System.out.println("end = " + end);
System.out.println("end - start = " + (end - start));
但是这样太消耗CPU,所以我在while循环里加入了暂停:
while(t.isAlive() == true){
end = System.currentTimeMillis();
try {
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
}
这样做的结果虽然cpu消耗减少,但是数据不准确了
Thread的join()方法
接着我又找到了第二种方法:
long start = System.currentTimeMillis();
System.out.println("start = " + start);
t1.start();
try {
t.join();//注意这里
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("end = " + end);
System.out.println("end - Start:" + (end - start));
使用join()方法,join()方法的作用,是等待这个线程结束;(t.join()方法阻塞调用此方法的线程(calling thread),直到线程t完成,此线程再继续,这里贴个说的挺清楚的博客)
synchronized的等待唤醒机制
第二种方法的确实现了计时,接着我又想到了多线程的等待唤醒机制,思路是:子线程启动后主线程等待,子线程结束后唤醒主线程。于是有了下面的代码:
Object lock = new Object();
Thread t = new Thread(() -> {
int num = 1000;
String s = "";
for (int i = 0; i < num; i++) {
s += "Java";
}
System.out.println("t Over");
lock.notify();//子线程唤醒
});
//计时
long start = System.currentTimeMillis();
System.out.println("start = " + start);
//启动子线程
t.start();
try {
lock.wait();//主线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("end = " + end);
System.out.println("end - start = " + (end - start));
但是这样会抛出两个异常:
由于对wait()和notify()的理解并不是很深刻,所以我最开始并不清楚为什么会出现这样的结果,因为从报错顺序来看子线程并没有提前唤醒,于是我在segmentfault和CSDN都发出了提问,同时也询问了我一个很厉害的朋友,最后得出的结论是调用wait()方法时需要获取该对象的锁,Object文档里是这么说的:
The current thread must own this object's monitor.
IllegalMonitorStateException - if the current thread is not the owner of the object's monitor.
所以上面的代码需要改成这样:
Thread t = new Thread(() -> {
int num = 1000;
String s = "";
for (int i = 0; i < num; i++) {
s += "Java";
}
System.out.println("t Over");
synchronized (lock) {//获取对象锁
lock.notify();//子线程唤醒
}
});
//计时
long start = System.currentTimeMillis();
System.out.println("start = " + start);
//启动子线程
t.start();
try {
synchronized (lock) {//这里也是一样
lock.wait();//主线程等待
}
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("end = " + end);
System.out.println("end - start = " + (end - start));
这样的确得出了结果,但是我想知道两个线程的执行顺序,于是在wait和nitify前后分别加了一个输出,最后得出的运行结果是:
可以看出主线程先wait子线程再notify,也就是说,如果子线程在主线程wati前调用了nitify,会导致主线程无限等待,所以这个思路还是有一定的漏洞的。
关于wait和notify这里贴个挺清楚的博客
CountDownLatch
第四种方式可以等待多个线程结束,就是使用java.util.concurrent包下的CountDownLatch类(关于CountDownLatch的用法可以参考这篇简洁的博客)
简单来说,CountDownLatch类是一个计数器,可以设置初始线程数(设置后不能改变),在子线程结束时调用countDown()方法可以使线程数减一,最终为0的时候,调用CountDownLatch的成员方法wait()的线程就会取消BLOKED阻塞状态,进入RUNNABLE从而继续执行。下面上代码:
int threadNumber = 1;
final CountDownLatch cdl = new CountDownLatch(threadNumber);//参数为线程个数
Thread t = new Thread(() -> {
int num = 1000;
String s = "";
for (int i = 0; i < num; i++) {
s += "Java";
}
System.out.println("t Over");
cdl.countDown();//此方法是CountDownLatch的线程数-1
});
long start = System.currentTimeMillis();
System.out.println("start = " + start);
t.start();
//线程启动后调用countDownLatch方法
try {
cdl.await();//需要捕获异常,当其中线程数为0时这里才会继续运行
}catch (InterruptedException e){
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("end = " + end);
System.out.println("end - start = " + (end - start));
Future
又想到刚学习了线程池,线程池的submit()的返回对象Future接口有一个get()方法也可以阻塞当前线程(其实该方法主要用途是获取子线程的返回值),所以第五种方法也出来了:
ExecutorService executorService = Executors.newFixedThreadPool(1);
Thread t = new Thread(() -> {
int num = 1000;
String s = "";
for (int i = 0; i < num; i++) {
s += "Java";
}
System.out.println("t Over");
});
long start = System.currentTimeMillis();
System.out.println("start = " + start);
Future future = executorService.submit(t);//子线程启动
try {
future.get();//需要捕获两种异常
}catch (InterruptedException e){
e.printStackTrace();
}catch (ExecutionException e){
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("end = " + end);
System.out.println("end - start = " + (end - start));
executorService.shutdown();
这里, ThreadPoolExecutor 是实现了 ExecutorService的方法, sumbit的过程就是把一个Runnable接口对象包装成一个 Callable接口对象, 然后放到 workQueue里等待调度执行. 当然, 执行的启动也是调用了thread的start来做到的, 只不过这里被包装掉了. 另外, 这里的thread是会被重复利用的, 所以这里要退出主线程, 需要执行以下shutdown方法以示退出使用线程池. 扯远了.
这种方法是得益于Callable接口和Future模式, 调用future接口的get方法, 会同步等待该future执行结束, 然后获取到结果. Callbale接口的接口方法是 V call(); 是可以有返回结果的, 而Runnable的 void run(), 是没有返回结果的. 所以, 这里即使被包装成Callbale接口, future.get返回的结果也是null的.如果需要得到返回结果, 建议使用Callable接口.参见这篇博客
看到这个Callable突然想到之前看C#多线程的时候有说到回调的问题,因此先开个坑,下篇博文说说Java的Callable与callback问题,先贴个Callable的简单讲解
BlockingQueue
同时,在concurrent包中,还提供了BlockingQueue(队列)来操作线程,BlockingQueue的主要的用法是在线程间安全有效的传递数据,具体用法可以参见这篇博客,对于BlockingQueue说的非常详细。因此,第六种方法也出来了:
BlockingQueue queue = new ArrayBlockingQueue(1);//数组型队列,长度为1
Thread t = new Thread(() -> {
int num = 1000;
String s = "";
for (int i = 0; i < num; i++) {
s += "Java";
}
System.out.println("t Over");
try {
queue.put("OK");//在队列中加入数据
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long start = System.currentTimeMillis();
System.out.println("start = " + start);
t.start();
try {
queue.take();//主线程在队列中获取数据,take()方法会阻塞队列,ps还有不会阻塞的方法
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("end = " + end);
System.out.println("end - start = " + (end - start));
CyclicBarrier
那么,有没有第七种方式呢?当然有啦~,还是concurrent包,只不过这次试用CyclicBarrier类:
CyclicBarrier字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。
CyclicBarrier barrier = new CyclicBarrier(2);//参数为线程数
Thread t = new Thread(() -> {
int num = 1000;
String s = "";
for (int i = 0; i < num; i++) {
s += "Java";
}
System.out.println("t Over");
try {
barrier.await();//阻塞
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
});
long start = System.currentTimeMillis();
System.out.println("start = " + start);
t.start();
try {
barrier.await();//也阻塞,并且当阻塞数量达到指定数目时同时释放
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("end = " + end);
System.out.println("end - start = " + (end - start));
实际是上面这种方法是不太严谨的,因为在子线程阻塞之后如果还有代码是会继续执行的,当然本例中后面是没有代码可执行了,可以近似理解为是子线程的运行时间。
这里贴个CountDownLatch、CyclicBarrier和Semaphore的讲解博客
小结
至此,集齐了七颗龙珠,得出小结:
- while循环进行轮询
- Thread类的join方法
- synchronized锁
- CountDownLatch
- Future
- BlockingQueue
- CyclicBarrier
Java多线程之以7种方式让主线程等待子线程结束的更多相关文章
- Java并发编程原理与实战六:主线程等待子线程解决方案
本文将研究的是主线程等待所有子线程执行完成之后再继续往下执行的解决方案 public class TestThread extends Thread { public void run() { Sys ...
- Java 并发编程中的 CountDownLatch 锁用于多个线程同时开始运行或主线程等待子线程结束
Java 5 开始引入的 Concurrent 并发软件包里面的 CountDownLatch 其实可以把它看作一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是 ...
- 线程:Java主线程等待子线程结束
使用Thread.join()方法: public class App { public static void main(String[] args) { testMain(); } public ...
- JAVA进阶----主线程等待子线程各种方案比较(转)
创建线程以及管理线程池基本理解 参考原文链接:http://www.oschina.net/question/12_11255?sort=time 一.创建一个简单的java线程 在 Java 语言中 ...
- java多线程实现主线程等待子线程执行完问题
本文介绍两种主线程等待子线程的实现方式,以5个子线程来说明: 1.使用Thread的join()方法,join()方法会阻塞主线程继续向下执行. 2.使用Java.util.concurrent中的C ...
- Java实现主线程等待子线程
本文介绍两种主线程等待子线程的实现方式,以5个子线程来说明: 1.使用Thread的join()方法,join()方法会阻塞主线程继续向下执行. 2.使用Java.util.concurrent中的C ...
- Java多线程--让主线程等待子线程执行完毕
使用Java多线程编程时经常遇到主线程需要等待子线程执行完成以后才能继续执行,那么接下来介绍一种简单的方式使主线程等待. java.util.concurrent.CountDownLatch 使用c ...
- Java主线程等待子线程、线程池
public class TestThread extends Thread { public void run() { System.out.println(this.getName() + &qu ...
- Java线程池主线程等待子线程执行完成
今天讨论一个入门级的话题, 不然没东西更新对不起空间和域名~~ 工作总往往会遇到异步去执行某段逻辑, 然后先处理其他事情, 处理完后再把那段逻辑的处理结果进行汇总的产景, 这时候就需要使用线程了. 一 ...
随机推荐
- Spark教程——(8)本地执行spark-sql程序
在程序中设定Spark SQL的运行模式: //.setMaster("local")设置本地运行模式 val conf = new SparkConf().setAppName( ...
- 把链接生成二维码 二维码中间带有logo
在工程中引入三个文件jquery.qrcode.js.qrcode.js.utf.js.其中utf.js文件是防止链接中的参数出现中文乱码现象 jquery.qrcode.js文件 function ...
- 图论初步<蒟蒻专属文章>
前言: 图论乃noip之重要知识点,但有点难理解 本人因此吃过不少亏 因为本人实在太弱,所以此篇乃正宗<蒟蒻专属文章> 正文:(本文仅介绍图论中的重点.难点,其余部分略将或不讲) 图 ...
- Py西游攻关之基础数据类型(四)-字典
Py西游攻关之基础数据类型 - Yuan先生 https://www.cnblogs.com/yuanchenqi/articles/5782764.html 七 Dictionary(字典) 字典是 ...
- APNs推送的系统做法
1. #pragma mark - 远程推送注册获得device Token if (IOS_VERSION >= 10.0) { UNUserNotificationCenter * cent ...
- 还在用逆向工程?太Low了,试试通用Mapper吧!
什么是通用Mapper? 通用mapper 可以极大的方便开发人员进行ORM,提供极其方便的单表增删改查. 什么是通用mapper,一句话简单说,它就是个辅助mybatis极简单表开发的组件.它不是为 ...
- python语言入门
1.python语言是一种高级的脚本语言,诞生于1991年. 2.python是目前主流的编程语言,具有超高的人气,是因为它是目前大数据与人工智能的语言基础,应用范围非常广泛. 3.python语言是 ...
- 【剑指Offer面试编程题】题目1512:用两个栈实现队列--九度OJ
题目描述: 用两个栈来实现一个队列,完成队列的Push和Pop操作. 队列中的元素为int类型. 输入: 每个输入文件包含一个测试样例. 对于每个测试样例,第一行输入一个n(1<=n<=1 ...
- ionic3记录之APP运行时网络判断
判断设备网路是否正常: 安装插件: ionic cordova plugin add cordova-plugin-network-information npm install --save@nat ...
- python学习第二课——while循环
#while循环基础语句 while 1==1: print('OK') #死循环 #如何阻止死循环 count=0 while count<10: print('第'+(str)(count) ...