线程池
-
基本概念
-
为什么会有线程池?
在实际开发中,我们通过开启多个线程去提高应用程序的使用率,让系统和程序达到最佳效率,但是我们知道,每次创建和销毁一个线程都是要消耗系统资源的,线程少时这不是问题,但当线程数达到一定数量时就会耗尽系统CPU和内存资源,也会造成GC频繁回收和停顿,影响系统的性能,所以线程池就应运而生了。 -
线程池的作用?
线程池就是提前创建若干个线程,如果有任务需要处理,线程池里的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的,所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。
-
-
线程池的种类
Executors是jdk里面提供的创建线程池的工厂类,它默认提供了4种常用的线程池应用,而不必我们去重复构造。
-
newFixedThreadPool
固定线程池,当这个线程池被创建的时候,池里的线程数就已经固定了。当需要运行的线程数量大体上变化不大时,适合使用这种线程池。固定数量还有一个好处,它可以一次性支付高昂的创建线程的开销,之后再使用的时候就不再需要这种开销。
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory); }
-
newCachedThreadPool
带缓冲线程池,每次有任务提交到线程池的时候,如果池中没有空闲的线程,线程池就会为这个任务创建一个线程,如果有空闲的线程,就会使用已有的空闲线程执行任务。有的人可能会有个疑惑:这样线程不就越来越多了吗?其实不是的,这个线程池还有一个销毁机制,如果一个线程60秒之内没有被使用过,这个线程就会被销毁,这样就节省了很多资源。CachedThreadPool是一个比较通用的线程池,它在多数情况下都能表现出优良的性能。编码的时候,遇事不决,用它。
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
-
newSingleThreadExecutor
单线程线程池,所有提交的这个线程池的任务都会按照提交的先后顺序排队执行。单个线程执行有个好处:由于任务之间没有并发执行,因此提交到线程池种的任务之间不会相互干扰。程序执行的结果更具有确定性。
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
-
newScheduledThreadPool
调度线程池,即按一定的周期执行任务,即定时任务,对ThreadPoolExecutor进行了包装而已。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); }
-
-
线程池的使用
-
实现Runnable接口的对象,举个🌰:
class ExampleThread implements Runnable { @Override public void run() { System.out.println("Do something"); } } public class UseThreadPool { public static void main(String[] args) { //这里我们没有新建一个线程去启动,而是直接调用线程池的execute将ExampleThread的对象传入进去 //Executors.newCachedThreadPool()中可以根据需求替换为上面四种线程池中的任意一种 ExecutorService cachedTP = Executors.newCachedThreadPool(); //execute()方法可以接收任何实现Runnable接口的类的对象 cachedTP.execute(new ExampleThread()); //别忘了调用shutdown()方法关闭线程池,关闭之后线程池不再接收其他任务。 cachedTP.shutdown(); } }
-
实现Callable接口的对象,举个🌰:
class ExampleThread implements Callable<String> { @Override public String call() { return "Some Value"; } } public class UseThreadPool { public static void main(String[] args) { ExecutorService cachedTP = Executors.newCachedThreadPool(); //submit()方法接收实现Callable接口的的对象,同时还有一个重载的方法,重载方法接收实现Runnable方法的类。所以不管是实现了Runnable还是Callable的类都可以作为它的参数。 //submit()方法还有一个Future类型的返回值,Future用于获取线程的返回值,Future是一个有泛型的类,泛型的类型与Callable的泛型相同。 Future<String> future = cachedTP.submit(new ExampleThread()); try { //future.get()方法,这个方法会阻塞当前线程,一直等到线程池里相应的线程执行结束的时候当前线程才会解除阻塞。因此,这个get()方法也是只有等到不得不用返回值的时候才会调用,否则会影响程序运行的效率。 String value = future.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } //调用这个方法会向所有的线程发一个中断信号(如果线程选择忽略,这个线程还是会继续执行),并且不再执行排队中的任务,将排队中的任务作为返回值返回(List<Runnable>) cachedTP.shutdownNow(); } }
-
拒绝策略
任务队列总有占满的时候,这是再submit()提交新的任务会怎么样呢?RejectedExecutionHandler接口为我们提供了控制方式,也就是拒绝策略,线程池默认的拒绝行为是AbortPolicy,也就是抛出RejectedExecutionHandler异常,该异常是非受检异常,很容易忘记捕获。如果不关心任务被拒绝的事件,可以将拒绝策略设置成DiscardPolicy,这样多余的任务会悄悄的被忽略。
ExecutorService executorService = new ThreadPoolExecutor(2, 2, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(512), new ThreadPoolExecutor.DiscardPolicy());// 指定拒绝策略
- AbortPolicy:抛出RejectedExecutionException
- DiscardPolicy:什么也不做,忽略
- DiscardOldestPolicy:丢弃执行队列中最老的任务,尝试为当前提交的任务腾出位置
- CallerRunsPolicy:直接由提交任务者执行这个任务
-
异常捕获
线程池的处理结果、以及处理过程中的异常都被包装到Future中,并在调用Future.get()方法时获取,执行过程中的异常会被包装成ExecutionException,submit()方法本身不会传递结果和任务执行过程中的异常。获取执行结果的代码可以这样写:
ExecutorService executorService = Executors.newFixedThreadPool(4); Future<Object> future = executorService.submit(new Callable<Object>() { @Override public Object call() throws Exception { throw new RuntimeException("exception in call~");// 该异常会在调用Future.get()时传递给调用者 } }); try { Object result = future.get(); } catch (InterruptedException e) { // interrupt } catch (ExecutionException e) { // exception in Callable.call() e.printStackTrace(); }
-
使用总结
- execute()与submit()的区别
execute()只能接收实现Runnable接口的类的对象,没有返回值
submit()都可以接收,且返回一个Future对象,Future的get方法会阻塞当前线程,也可以在主线程中通过get方法捕获线程中的异常。 - shutdown()与shutdownNow()的区别
shutdown():关闭之后不再接受新的任务,之前提交的任务等执行结束再关闭线程池。
shutdownNow():不再接受新的任务,试图停止池中的任务再关闭线程池,返回所有未处理的线程list列表。 - 避免使用无界队列
不要使用Executors.newXXXThreadPool()快捷方法创建线程池,因为这种方式会使用无界的任务队列,为避免OOM,我们应该使用ThreadPoolExecutor的构造方法手动指定队列的最大长度:ExecutorService executorService = new ThreadPoolExecutor(2, 2, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOM new ThreadPoolExecutor.DiscardPolicy());
- execute()与submit()的区别
注:想要深入理解线程池的请参考:线程池原理
-