最近在带一些新员工做项目时发现有一些员工对JAVA线程的理解不够深入,出现一些比较低级的错误。因此产生写这篇文章的想法,一来记录一下遇到的问题和解决的方法另外自己也复习一下线程的用法。
需求1:项目中某个业务需要调用另外N个服务接口,然后根据返回的结果做筛选再返回给前端。
当然最简单的做法就是N个接口串行调用,但是如果每个接口调用的时间都在1秒以上那么N个接口调用完毕就需要耗费N秒,这在项目中是不可接受的。因此要求队员要用多线程处理。那么主线程要等待所有子线程任务执行完毕主要有以下几种方法:
方法1:使用CountDownLatch 这个类是在JDK1.5就已经提供了,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。
以下例子就是一个很经典的CountDownLatch的用法
public static void countDownLatchTest(){
long time = System.currentTimeMillis() ;
final CountDownLatch countDownLatch = new CountDownLatch(5) ;
for(int i=0;i<5;i++){
final int num = i ;
new Thread(new Runnable() {
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());
}
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("总耗时==="+(System.currentTimeMillis()-time));
}
public static void main(String[] args){
countDownLatchTest() ;
}
输出:
Thread-0运行结束 运行时间为:0秒 countDownLatch=4
Thread-1运行结束 运行时间为:1秒 countDownLatch=3
Thread-2运行结束 运行时间为:2秒 countDownLatch=2
Thread-3运行结束 运行时间为:3秒 countDownLatch=1
Thread-4运行结束 运行时间为:4秒 countDownLatch=0
总耗时===4028
可以看到最终耗时跟线程中最耗时的线程有关,但是使用CountDownLatch有一定风险,如果运行中没有捕获相关异常很容易导致CPU居高不下从而导致整个项目无法运行(想测试的同学可以把countDownLatchTest中的注解打开),那么遇到这种问题如何处理,当然把整个代码try catch是一种解决方式。另外一种比较优雅的解决方式是使用countDownLatch.await(5, TimeUnit.SECONDS) 代替countDownLatch.await(),方法中的那2个参数分别是超时时间和超时单位,如果线程在规定的时间内没有处理完成则主线程被自动唤醒继续执行下一步操作。
以上方法可以实现对应功能但是有以下缺点:
1.容易出错,如果没有捕获异常或没设置超时时间很容易造成服务器死机(*作者团队就有队员犯过这种错误,而且还是3年以上的老员工)
2.没有返回值,当然可以用静态变量存储(不推荐)
方法2:利用Thread.join方法
以下例子就是一个很经典的用法
public static void joinTest(){
long time = System.currentTimeMillis() ;
Thread[] threads = new Thread[5] ;
for(int i=1;i<=5;i++){
final int num = i ;
threads[i-1] = new Thread(new Runnable() {
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));
}
输出结果:
Thread-0耗时:1秒
Thread-1耗时:2秒
Thread-2耗时:3秒
Thread-3耗时:4秒
Thread-4耗时:5秒
总耗时===5005
方法2的效果跟方法1的差不多,优缺点也一样,所以就不在过多探讨了
方法3:使用Future,利用Future.get()来实现需求
以下例子就是一个很经典的Future的用法
public static void futureTest(){
long time = System.currentTimeMillis() ;
ExecutorService executorService = Executors.newFixedThreadPool(5) ;
List<Future<String>> list = new ArrayList<Future<String>>() ;
for(int i=1;i<=5;i++){
final int num = i ;
list.add( executorService.submit(new Callable<String>() {
public String call() throws Exception {
Thread.sleep(num*1000);
return Thread.currentThread()+": 耗时=="+num+" 秒";
}
})
);
}
for(Future<String> future:list){
try {
System.out.println(future.get(5,TimeUnit.SECONDS));
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("总耗时==="+(System.currentTimeMillis()-time));
executorService.shutdownNow() ;
}
public static void main(String[] args){
futureTest() ;
}
输出:
Thread[pool-1-thread-1,5,main]: 耗时==1 秒
Thread[pool-1-thread-2,5,main]: 耗时==2 秒
Thread[pool-1-thread-3,5,main]: 耗时==3 秒
Thread[pool-1-thread-4,5,main]: 耗时==4 秒
Thread[pool-1-thread-5,5,main]: 耗时==5 秒
总耗时===5013
可以看到3种效果是一致的。方法3相对方法1,方法2而言,代码相对复杂一点,不过有返回值。不管采用那种方式都要注意设置超时时间,不然很容易引起系统崩溃。