--------------------20180502更新-----------------------------
今天学习到了一个比较强大的类:ExecutorCompletionService,它是将 Executor和BlockQueue结合的jdk类,其实现的主要目的是:提交任务线程,每一个线程任务直线完成后,将返回值放在阻塞队列中,然后可以通过阻塞队列的take()方法返回 对应线程的执行结果!!
所以还可以这样写:
ExecutorCompletionServicecompletionService = new ExecutorCompletionService(Executors.newFixedThreadPool(5)); for(int i=0; i<10; i++) { int j = i; completionService.submit(()-> Thread.currentThread().getName() + "------>" + j); } try { for(int i=0; i<10; i++) { Future future = completionService.take(); if(future != null) { String str = future.get(); System.out.println(str); } } } catch (Exception e) { e.printStackTrace(); } System.out.println("---------->结束");
同样可以达到阻塞的效果!(注:其中有用到jdk1.8的lambda表达式~)
---------------------------------------------------------------------------------------------------------
之前我有写过一篇博客,是关于多线程写同一个sheet文件的。类似的场景很多,当我们想用多线程提高效率时,面临的关键问题就是线程安全和确定所有任务都完成。线程安全的问题那篇博客有说,就是确保对公共资源的写操作是安全的,比如List的add操作采用synchronized来包装或直接采用线程安全的集合;Sheet的addRow加锁等… 而本篇的重点是“如何确保所有任务都完成,才能进行下一步?”。
先来看现象:
public static void m() { for(int i=0; i<10; i++) { int j = i; new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "------>" + j); } }).start(); } System.out.println("---------->结束"); }
这段代码创建了10个线程,每个线程的任务会打印当前线程的名字,运行后发现可能出现以下结果(顺序不一定是下面这样):
Thread-1------>1
---------->结束
Thread-0------>0
Thread-2------>2
Thread-4------>4
Thread-3------>3
Thread-5------>5
Thread-6------>6
Thread-7------>7
Thread-8------>8
Thread-9------>9
会发现“---------->结束”没有在所有线程都运行完就打印出来了,映射到实际场景就是用多线程去帮我们干活,还没干完呢就直接下一步了,如此的话没有实际意义(除非这个多线程的任务是异步的,其他逻辑不需要等待它完成才能进行)。
我们知道,多线程执行任务可以用原始的线程提交(上述代码),也可以用线程池(比较推荐这种方式,便于对线程进行管理)。为了解决上述问题,可以用CountDownLatch计数器,计数器的初始大小要跟任务数的大小一致(跟线程数无关),每执行一次任务,计数器减一(countDown),await()方法会一直阻塞主线程,直到计数器的值减为0,才会释放锁,如此便可以达到确保所有任务都完成才继续下一步的效果。
先用原始线程结合计数器的方式来试试效果:
public static void m1() { CountDownLatch latch = new CountDownLatch(10); for(int i=0; i<10; i++) { int j = i; new Thread(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "------>" + j); latch.countDown(); } }).start(); } try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("---------->结束"); }
无论运行多少次,会发现"---------->结束"始终会在多线程所有任务都执行完毕后打印,比如某次结果:
Thread-0------>0
Thread-1------>1
Thread-2------>2
Thread-3------>3
Thread-4------>4
Thread-7------>7
Thread-8------>8
Thread-9------>9
Thread-5------>5
Thread-6------>6
---------->结束
再用线程池结合计数器的方式来尝试:
public static void m2() { CountDownLatch latch = new CountDownLatch(10); ExecutorService es = Executors.newFixedThreadPool(5); for(int i=0; i<10; i++) { int j = i; es.submit(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "------>" + j); latch.countDown(); } }); } try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("---------->结束"); }
同样地,无论运行多少次,"---------->结束"都会在所有任务都完成以后再进行!比如某次打印结果为:
pool-1-thread-2------>1
pool-1-thread-2------>5
pool-1-thread-3------>2
pool-1-thread-1------>0
pool-1-thread-1------>8
pool-1-thread-1------>9
pool-1-thread-3------>7
pool-1-thread-4------>3
pool-1-thread-2------>6
pool-1-thread-5------>4
---------->结束
这充分印证了CountDownLatch计数器的强大!下面我们再看一个比较容易忽略的方式:
public static void m3() { ExecutorService es = Executors.newFixedThreadPool(5); List> list = new ArrayList<>(); for(int i=0; i<10; i++) { int j = i; Future future = es.submit(new Callable () { @Override public String call() throws Exception { return Thread.currentThread().getName() + "------>" + j; } }); list.add(future); } try { for(Future future : list) { System.out.println(future.get()); } } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println("---------->结束"); }
Callable不用多说,它可以表示一个有返回值的线程,Future则用于接收返回的结果。Future的get方法具有阻塞作用,它会一直阻塞直至获取到结果。Callable&Future一般都是结合线程池来使用。
运行多次,也会发现"---------->结束"总是在最后运行的,同样达到了目的。
说到线程池管理线程,需要注意的是比如:
Executors.newFixedThreadPool(40)
实际上是new了一个corePoolSize=maximumPoolSize的特殊情况的线程池:
public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); }
Executors提供的静态方法创建线程池,内部都是去构造一个ThreadPoolExecutor,只是不同类型的线程池,
corePoolSize和maximumPoolSize的大小关系不同,还有采用的任务队列的类型也不同。
关于多线程和线程池的一些知识补充,见以下手工笔记:
1.一些多线程的基本概念:
2、线程池类型以及提交线程的过程:
3、常见的线程相关类的关系图: