Java线程池

244 阅读8分钟

一、概述

因为通过直接创建子线程,无法控制创建线程的数量,以及线程之间的竞争,并且会占用大量的系统资源,以及频繁调用GC系统进行内存回收。


而使用线程池,即可通过在构造线程池时控制创建线程的数量,并重复利用线程。

其优势在于:

  1. 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
  2. 提供系统响应速度,当有任务到来时,无需等待新线程的创建边能立即执行。
  3. 方便线程并发数的管控,线程若是无限制的创建,不仅额外消耗大量的系统资源,更是占用过多资源而阻塞系统或OOM等状况。而线程池能有效管控线程、统一分配、调优,提高资源使用率。
  4. 线程池能提供定时、定期以及可控线程数等功能。


二、ThreadPoolExecutor线程池

1、构造方法

通过ThreadPoolExecutor来创建一个线程池:

ExecutorService service = new ThreadPoolExecutor(...);


下面具体看看ThreadPoolExecutor中的一个构造方法。

public ThreadPoolExecutor(int corePoolSize,
       int maximumPoolSize,
       long keepAliveTime,
       TimeUnit unit,
       BlockingQueue<Runnable> workQueue,
       ThreadFactory threadFactory,
       RejectedExecutionHandler handler)

其中参数具体含义

  • corePoolSize

线程池汇总的核心线程数,默认情况下,核心线程一直存活在线程池中,即便他们在线程池中处于闲置状态。除非将ThreadPoolExecutorallowCoreThreadTImeOut属性设为true,则处于闲置的核心线程在等待新任务时会有超时策略,这个超时时间由keepAliveTIme来指定。一旦超过锁设置的超时时间,闲置的核心线程就会被终止。


  • maximumPoolSize

线程池中所容纳的最大线程数,如果活动的线程达到该值,后续的新任务将会被阻塞。maximumPoolSize = corePoolSize + 非核心线程


  • keepAliveTime

非核心线程闲置时的超时时长,对应非核心线程,闲置时间超过该值,就会被回收。只有对ThreadPoolExecutorallowCoreThreadTimeOut属性设为true时,该超时时间才会对核心线程产生效果。


  • unit

用于指定keepAliveTime参数的时间单位。他是一个枚举,可以使用的单位(TimeUnit.DAYS、TimeUnit.HOURS、TimeUnit.MINUTES、TimeUnit.MILLISECONDS、TimeUnit.MICROSECONDS、TimeUnit.NANOSECONDS)


  • workQueue

线程池中保存等待执行的任务的阻塞队列。通过线程池中的execute()方法提交的Runnable对象都会存储在该队列中。常见的阻塞队列如下(图片来源于:github.com/LRH1993/and…):


也能通过实现BlockingQueue接口自定义的阻塞队列。


  • threadFactory

线程工厂,为线程池提供新线程的创建ThreadFactory是一个接口,里面只有一个newThread方法。默认使用DefaultThreadFactory类。


  • handler

RejectedExecutionHandler对象,而RejectedExecutionHander是一个接口,里面只有一个rejectedExecution()方法。当任务队列已满且线程池中的活动线程已经达到所限定的最大值或者是无法成功执行任务,这时ThreadPoolExecutor会调用RejectedExecutionHandler中的rejectedExecution()方法。在线程池中它默认是AbortPolicy,在无法处理新任务时抛出RejectedExecutionException异常。


2、ThreadPoolExecutor的使用

ExecutorService service = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, 
        new LinkedBlockingQueue<>());

对于ThreadPoolExecutor有多个构造方法,对于上面的构造方法中的其他参数都采用默认值。可以通过execute和submit两种方式来向线程池提交一个任务。

  • execute

使用execute来提交任务时,因为execute方法没有返回值,无法判断任务是否被线程池执行完成。

service.execute(new Runnable(){
      public void run(){
         System.out.println("execute method");
      }
});

  • submit

使用submit来提交方法,它会返回一个future,因而能通过future来判断是否任务执行完成,还可以通过future的get()方法来获取返回值。如果子线程任务没有完成,get()方法会阻塞知道任务完成,使用get(long timeout, TImeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务还没有执行完。

Future<Integer> future = service.submit(new Callable<Integer>{
   @Override
   public Integer call() throws Exception {
      System.out.println("submit method");
      return 2;
   }
));
try{
   Integer number = future.get();
}catch (ExecutionException e){
   e.printStackTrace();
}

其实在submit()方法也可以使用Runnable线程,只是返回值为null。


  • 线程池关闭

调用线程池的shutdow()或shutdownNow()方法来关闭线程池。

shutdow():将线程池状态设置成SHOWDOWN状态,然后中断所有没有正在执行任务的线程。

shutdownNow():将线程池的状态奢姿为STOP状态,然后中断所有任务(包括正在执行的)的线程,并返回等待执行的列表。


中断采用interrupt()方法,所以无法响应中断的任务可能永远无法终止。但是调用上述的两个关闭之一,isShutdown()方法返回值为true,当所有任务都已关闭,表示线程池关闭成功,则isTerminated()方法返回值为true。当需要立刻中断所有的线程,不一定需要执行完任务,可以直接调用showdownNow()方法。


三、线程池的执行流程


  1. 线程数没有超过核心线程数(corePoolSize),则直接启动一个核心线程来执行任务
  2. 线程池中的线程数量已经超过(corePoolSize),这时候任务就会被插入到任务队列总排队等待执行。
  3. 任务队列已满,因而无法将任务插入到任务队列中。但若线程数量没有超过设置的最大线程数(maximumPoolSize),那么可以启动一个非核心线程执行任务。
  4. 若线程数超过最大线程数(maximumPoolSize),则拒绝执行该任务,并调用RejectedExecutionHandler中的RejectedExecution方法通知调用者。


这儿补充下:队列的四种策略中,

SynchronousQueue 直接提交,也就是上面讲到的所有任务无需加入队列中等待。

LinkedBlockingQueue 无界队列,此时超过核心线程数后的任务全部加入队列等待,系统最多只能运行核心线程数量的线程。这种方法相当于控制了并发的线程数量。

ArrayBlockingQueue 有界队列 此时超过核心线程后的任务先加入队列等待超出队列范围后的任务就生成线程,但创建线程最多不超过线程池的最大允许值。


说简单点,就是先让任务主要是使用核心线程完成,但是队列容纳不了,则创建临时线程进行完成。


四、四种线程池类


四种线程池分别是:newFixedThreadPoolnewCachedThreadPoolnewScheduledThreadPoolnewSingleThreadExecutor,下面详细介绍这四种线程池


  • newFixedThreadPool

通过Executors中的newFixedThreadPool方法来创建,该线程池是一种只有核心线程的线程池。

ExecutorService service = Executors.newFixedThreadPool(4);

newFixedThreadPool只有核心线程,并且这些线程都不会被回收,也就是它能够更快速响应外界请求。

其具体使用ThreadPoolExecutor()构造方法如下:

public static ExecutorService newFixedThreadPool(int nThreads){
   return new ThreadPoolExecutor(nThreads, nThreads,
       0L, TimeUnit.MILLISECONDS,
       new LinkedBlockingQueue<Runnable>());
}

因为只有核心线程,并且不存在超时机制,因而采用LinkedBlockingQueue(无界队列),所以对于任务队列的大小没有限制。


  • newCachedThreadPool

通过Executors中的newCachedThreadPool方法创建只有临时线程,而无核心线程的线程池。

public static ExecutorService newCachedThreadPool(){
   return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
       60L, TimeOut.SECONDS,
       new SynchronousQueue<Runnable>());
}

从该构造方法可以看出,newCachedThreadPool中没有核心线程,而线程池最大数为Integer.MAX_VALUE,并且说明了闲置线程存活时间为60s,因而当没有任务时,该线程池几乎不占用任何系统资源

其采用的阻塞队列是SynchronousQueue,即没有任何容量的阻塞队列,即现有线程无法接受任务,则立即创建新的线程来执行任务。


  • newScheduledThreadPool

newScheduledThreadPool方法的创建如下:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize){
   return new ScheduledThreadPoolExecutor(corePoolSize);
}

public ScheduledThreadPoolExecutor(int corePoolSize){
   super(corePoolSize, Integer.MAx_VALUE, 0, NANOSECONDS,
       new DelayedWorkQueue());
}

newScheduledThreadPool的核心线程数固定,对应非核心线程几乎没有限制,只是当非核心线程闲置时立即被回收


创建一个可定时执行或周期执行任务的线程池:

ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
service.schedule(new Runnable() {
	public void run() {
		System.out.println(Thread.currentThread().getName()+"延迟三秒执行");
	}
}, 3, TimeUnit.SECONDS);
service.scheduleAtFixedRate(new Runnable() {
	public void run() {
		System.out.println(Thread.currentThread().getName()+"延迟三秒后每隔2秒执行");
	}
}, 3, 2, TimeUnit.SECONDS);

其中

schedule(Runnable command, long delay, TimeUnit unit):延迟一定时间后执行Runnable任务;

schedule(Callable callable, long delay, TimeUnit unit):延迟一定时间后执行Callable任务;

scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):延迟一定时间后,以间隔period时间的频率周期性地执行任务;

scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit):与scheduleAtFixedRate()方法类型。

scheduleWithFixedDelay()方法的周期时间间隔是以上一个任务执行结束到下一个任务开始执行的间隔;

scheduleAtFixedRate()方法的周期时间间隔是以上一个任务开始执行到下个任务开始执行的间隔。


  • newSingleThreadExecutor

该线程池只有一个核心线程,对于消息对垒没有大小限制,也意味着这个任务处于活动状态时,其他任务都须在队列中等待。


newSingleThreadExecutor将所有的外界任务统一到一个线程中,所有在这个任务执行之间不需要处理线程同步的问题。

public static ExecutorService newSingleThreadExecutor(){
   return new FinalizableDelegatedExecutorService(
        new ThreadPoolExecutor(1, 1, 0L, TimeOut.MILLISECONDS,
                  new LinkedBlockingQueue<Runnable>()));
}


五、线程池额使用技巧

  • CPU密集型任务 :线程池中的线程个数应尽量少,如配置N+1个线程的线程池(N:处理机的核数)
  • IO密集型任务:由于IO操作速度远低于CPU速度,那么在运行这类任务时,CPU绝大多数时间处于空闲状态,那么线程池可以配置尽量多的线程,以提高CPU利用率,如2 * N
  • 混合型任务:可拆分为CPU密集型任务和IO密集型任务,执行时间相近拆分再执行的吞吐率高于串行执行;若相差数据级的差距,则无拆分意义。