当项目中某个业务需要调用另外N个服务接口,然后根据返回的结果做筛选再返回给前端。当然最简单的做法就是N个接口串行调用,但是如果每个接口调用的时间都在1秒以上那么N个接口调用完毕就需要耗费N秒,这在项目中是不可接受的,因此这个时候就需要用多线程处理。
而主线程要等待所有子线程任务执行完毕后再往下同步执行的实现方式,主要有以下几种方法:
- 1.使用synchronized获得同步锁
- 2.CountDownLatch
- 3.Thread Join
- 4.CyclicBarrier
- 5.Future
- 6.CompletableFuture
方法一: 使用synchronized获得同步锁
这是最基础的方式,就是放置一个公用的static变量,假如有10个线程,每个线程处理完上去累加下结果,然后后面用一个死循环(或类似线程阻塞的方法),去数这个结果,达到10个,说明大家都执行完了,就可以执行后续的事情了,这个想法虽然土鳖,但是基本上跟语言无关,几乎所有主流编程语言都支持。
public class ThreadLockTest { public static Integer flag = 0;//公用变量 public static void main(String[] args) throws Exception { ThreadLockTest testObj = new ThreadLockTest(); final int threadNum = 10; for (int i = 0; i < threadNum; i++) { new Thread(new MyRunable(i, testObj)).start(); } while (true) { if (testObj.flag >= threadNum) { System.out.println("-----------\n所有thread执行完成!"); break; } Thread.sleep(10); } } static class MyRunable implements Runnable { int _i = 0; ThreadLockTest _test; public MyRunable(int i, ThreadLockTest test) { this._i = i; this._test = test; } @Override public void run() { try { Thread.sleep((long) (Math.random() * 10)); System.out.println("thread " + _i + " done"); //利用synchronized获得同步锁 synchronized (_test) { _test.flag += 1; } System.out.println("thread " + _i + " => " + _test.flag);//测试用 } catch (Exception e) { e.printStackTrace(); } } } }
方法二: CountDownLatch
使用CountDownLatch 这个类是在JDK1.5就已经提供了,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。
其实,CountDownLatch实现方式,跟上面的方法一是类似的。它是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,接下来等待的线程就可以恢复工作了。
下面咱们简单看下源码:
countDownLatch类中只提供了一个构造器:
//参数count为计数值 public CountDownLatch(int count) { };
而类中有三个方法是最重要的:
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行 public void await() throws InterruptedException { }; //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行 public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; //将count值减1 public void countDown() { };
以下例子就是一个很经典的CountDownLatch的用法
import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class CountDownLatchTest { public static void countDownLatchTest() { long time = System.currentTimeMillis(); final CountDownLatch countDownLatch = new CountDownLatch(5); ExecutorService es = Executors.newFixedThreadPool(5); for (int i = 0; i < 5; i++) { final int num = i; es.execute(new Runnable() { @Override public void run() { try { Thread.sleep(num * 1000); } catch (InterruptedException e) { e.printStackTrace(); } /** * 使用CountDownLatch时要注意异常情况,一旦没处理好导致countDownLatch.countDown()没执行会引起线程阻塞,导致CPU居高不下**/ /*if (num == 3) { System.out.println(Integer.parseInt("1.233")); }*/ countDownLatch.countDown(); System.out.println(Thread.currentThread().getName() + "运行结束 运行时间为:" + num + "秒 countDownLatch=" + countDownLatch.getCount()); } }); } es.shutdown(); try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("总耗时===" + (System.currentTimeMillis() - time)); } public static void main(String[] args) { countDownLatchTest(); } }
可以看到最终耗时跟线程中最耗时的线程有关,但是使用CountDownLatch有一定风险,如果运行中没有捕获相关异常很容易导致CPU居高不下从而导致整个项目无法运行(想测试的同学可以把countDownLatchTest中的注解打开)。
那么遇到这种问题如何处理,当然把整个代码try catch是一种解决方式。另外一种比较优雅的解决方式是使用countDownLatch.await(5, TimeUnit.SECONDS) 代替countDownLatch.await(),方法中的那2个参数分别是超时时间和超时单位,如果线程在规定的时间内没有处理完成则主线程被自动唤醒继续执行下一步操作。
以上方法可以实现对应功能但是有以下缺点:
- (1)容易出错,如果没有捕获异常或没设置超时时间很容易造成服务器死机;
- (2)没有返回值,当然可以用静态变量存储(不推荐);
方法三: Thread Join
join()方法的作用,让父线程等待子线程结束之后才能继续运行。
官方的解释是,当我们调用某个线程的这个方法时,这个方法会挂起调用线程,直到被调用线程结束执行,调用线程才会继续执行。
通过查看源码,会看到Join方法实现是通过wait方法,当main线程调用thread.join()时候,main线程会获得线程对象thread的锁(wait 意味着拿到该对象的锁),调用该对象的wait(等待时间),直到该对象唤醒main线程 ,比如退出后。
以下例子也是很经典的用法:
public class JoinTest { public static void main(String[] args) { joinTest(); } public static void joinTest() { long time = System.currentTimeMillis(); int threadNum = 5; Thread[] threads = new Thread[threadNum]; for (int i = 1; i <= threadNum; i++) { final int num = i; threads[i - 1] = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(num * 1000); System.out.println(Thread.currentThread().getName() + "耗时:" + num + "秒"); } catch (InterruptedException e) { e.printStackTrace(); } } }); threads[i - 1].start(); } for (int i = 0; i < threads.length; i++) { try { threads[i].join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("总耗时===" + (System.currentTimeMillis() - time)); } }
这个方法的效果跟上面使用CountDownLatch方式差不多,优缺点也一样,所以就不在过多探讨了。