在学习多线程时,最开始遇到的问题实际上是“计算子线程运行时间”,写到最后发现本文和标题更为符合,可是仍然基于问题:“在主线程中获取子线程的运行时间”。html
对于“主线程如何获取子线程总运行时间”的问题,最开始想到的是使用while循环进行轮询:java
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循环里加入了暂停:segmentfault
while(t.isAlive() == true){ end = System.currentTimeMillis(); try { Thread.sleep(10); }catch (InterruptedException e){ e.printStackTrace(); } }
这样作的结果虽然cpu消耗减小,可是数据不许确了api
接着我又找到了第二种方法:数组
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完成,此线程再继续,这里贴个说的挺清楚的博客)安全
第二种方法的确实现了计时,接着我又想到了多线程的等待唤醒机制,思路是:子线程启动后主线程等待,子线程结束后唤醒主线程。因而有了下面的代码:多线程
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文档里是这么说的:oracle
The current thread must own this object's monitor.
IllegalMonitorStateException - if the current thread is not the owner of the object's monitor.学习
因此上面的代码须要改为这样:this
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这里贴个挺清楚的博客
第四种方式能够等待多个线程结束,就是使用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));
又想到刚学习了线程池,线程池的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的简单讲解
同时,在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));
那么,有没有第七种方式呢?固然有啦~,仍是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的讲解博客
至此,集齐了七颗龙珠,得出小结: