Java线程池

本文详细介绍了Java线程池的概念和使用,包括Executor框架、ThreadPoolExecutor构造参数、任务队列类型、线程工厂、拒绝策略以及如何选择合适的线程池大小。还探讨了如何扩展线程池以处理异常和打印线程信息,以及线程池中线程存活时间的实现机制。
摘要由CSDN通过智能技术生成

一、线程池简介

多线程可以提高计算机的使用效率和程序的性能,但无限制的使用线程,除了使线程的创建和销毁变得更加频繁影响系统性能外,更会耗尽CPU和内存资源,因此对线程的使用应该有一个度,使线程数量在有限的范围内。

和数据库连接池类似,我们可以使用线程池让创建的线程进行复用,从而避免系统频繁的创建和销毁线程,并使线程数量在一定范围内。

二、Executor

Executor是Java为了更好的控制多线程而提供的一套框架,其本质就是一个线程池,主要包含以下成员:
在这里插入图片描述
这些成员均位于java.util.concurrent包中,其中 ThreadPoolExecutor 表示一个线程池,Executors 相当于线程池工厂,通过Executors可以获取一个拥有特定功能的线程池,主要有以下工厂方法:

  • newFixedThreadPool() 方法,该方法返回一个固定线程数量的线程池,当有一个新任务提交时,线程池中若有空闲线程,则立即执行,若没有,则新任务会暂时保存包一个任务队列中,等到线程池中有空闲线程时再处理任务队列中的任务。
public static ExecutorService newFixedThreadPool(int nThreads)
  • newSingleThreadExecutor() 方法,该方法返回一个只有一个线程的线程池,多余任务会被保存在一个任务队列中,等到线程空闲时按照先进先出的顺序执行任务队列中的任务。
public static ExecutorService newSingleThreadExecutor()
  • newCachedThreadPool() 方法,该方法返回一个会根据实际情况调整数量的线程池,所谓根据实际情况,即若有空闲线程可以复用,则优先使用空闲线程。若所有线程均在工作,又有新的任务提交,则创建新的线程处理任务。所有线程在当前任务执行完毕后,都将返回线程池进行复用。
public static ExecutorService newCachedThreadPool()
  • newSingleThreadScheduledExecutor() 方法,该方法返回一个ScheduledExecutorService对象,线程池大小为1。ScheduledExecutorService接口在ExecutorService接口之上扩展了在给定时间执行某任务的功能,如在某个固定的延时之后执行,或者周期性执行某个任务。
public static ScheduledExecutorService newSingleThreadScheduledExecutor()
  • newScheduledThreadPool() 方法,该方法也返回一个ScheduledExecutorService对象,但该线程池可以指定线程数量。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

三、ThreadPoolExecutor

1、ThreadPoolExecutor的主要构造函数

虽然newFixedThreadPool()方法、newSingleThreadExecutor()和newCachedThreadPool()方法创建的线程池有不同的功能特点,但它们都只是ThreadPoolExecutor类的封装:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, 
                                  nThreads,
                                  0L, 
                                  TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueueRunnable());


public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueueRunnable()));



public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueueRunnable());

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

其中:

  • corePoolSize:指定线程池中的线程数量;
  • maximumPoolSize:指定线程池中的最大线程数量;
  • keepAliveTime:当线程池线程数量超过corePoolSize时,多余空闲线程的存活时间;
  • unit:keepAliveTime的时间单位;
  • workQueue:任务队列,存放被提交但尚未被执行的任务;
  • threadFactory:线程创建工厂,一般用默认的即可;
  • handler:当没有空闲线程处理新提交任务时的拒绝策略。

2、任务队列workQueue参数详解

workQueue参数是一个BlockingQueue接口的对象,根据队列功能分类,在ThreadPoolExecutor的构造函数中可用以下几种BlockingQueue:

  1. SynchronousQueue,直接提交队列,SynchronousQueue是一个特殊的BlockingQueue,没有容量,每一个插入操作都要等待一个相应的删除操作,每一个删除操作都要等待对应的插入操作。提交的任务不会被真实的保存,而总是将新任务提交给线程执行,如果没有空闲线程,则尝试创建新的线程,如果线程数量已达到最大值,则执行拒绝策略。
  2. ArrayBlockingQueue,有界的任务队列,ArrayBlockingQueue是一个必须设置容量的队列,当有新的任务需要执行时,如果线程池的实际线程数小于corePoolSize,则创建新的线程来执行,若大于corePoolSize,则会将新任务加入等待队列。如果等待队列也满了,无法加入,则在总线程数不大于maximumPoolSize的前提下,创建新的线程执行任务,如果大于maximumPoolSize,则执行拒绝策略。因此,除非系统非常繁忙,否则线程数通常维持在corePoolSize。
  3. LinkedBlockingQueue,无界的任务队列,当有新的任务到来,系统的线程数小于corePoolSize时,线程池会生成新的线程执行任务,但当系统的线程数达到corePoolSize后,就不会继续增加,若后续仍有新的任务加入,而又没有空闲的线程资源,则任务直接进入队列等待,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。
  4. PriorityBlockingQueue,优先任务队列,是一个特殊的无界队列,可以控制任务的执行先后顺序,根据任务自身的优先级顺序先后执行。

拒绝策略handler详解
拒绝策略是在线程池中的线程已经用完,无法继续为新任务服务时的补救措施。JDK内置了四种拒绝策略:

  1. AbortPolicy:该策略会直接抛出异常,阻止系统正常工作。
  2. CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务,这样不会真的丢弃任务,但是任务提交线程的性能极有可能会急剧下降。
  3. DiscardOldestPolicy:该策略将丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
  4. DiscardPolicy:该策略会直接丢弃无法处理的任务,不做任何处理。

拒绝策略均是实现了RejectedExecutionaHandler接口的类,所以也可以自己扩展该接口来定义拒绝策略。

public interface RejectedExecutionalHandler{
  void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}  

例如:

public class RejectHandlerExample{
  public static class MyTask implements Runnable{
  
    @Override
    public void run(){
      System.out.println(System.currentTimeMillis() + "Thread ID" + Thread.currentThread().getId());
      try{
        Thread.sleep(100);
      }catch(InterruptedException e){
        e.printStackTrace();
      } 
    }
  }
  public static void main(String[] args)throws InterruptedException{
    MyTask task = new MyTask();
    ExecutorService es = new ThreadPoolExecutor(5,5,0L,TimeUnit.MILLISECONDS,
                              new LinkedBlockingQueue<Runnable>(10),
                              Executors.defaultThreadFactory(),
                              new RejectedExecutionHandler(){
                                @Override
                                public void rejectedExecution(Runnable r,ThreadPoolExecutor executor){
                                  System.out.println(r.toString()+"被拒绝");
                                } 
                              });
    for(int i=0;i<Integer.MAX_VALUE;i++){
      es.submit(task);
      Thread.sleep(10);
    }
   }
 }                                        

该例中,线程数量固定只有5个,并且每个任务的运行时间有100毫秒,任务添加的速度远远大于任务完成的速度,同时任务队列的容量只有10,因此会很快产生被抛弃的任务。

3、线程创建工厂ThreadFactory

ThreadFactory是一个接口,它只有一个用来创建线程的方法:

Thread newThread(Runnable r);

当线程池需要新建线程时,就会调用这个方法。使用自定义线程创建工厂可以使我们对创建的线程进行相关处理,如将线程池中的线程都设置为守护线程:

public class MyThreadFactory implements ThreadFactory{
  @Override
  public Thread newThread(Runnable r){
    Thread t = new Thread(r);
    t.setDaemon(true);
    System.out.println("创建线程" + t);
    return t;
  }
}    
  

4、线程池扩展

ThreadPoolExecutor提供了beforeExecute()、**afterExecute()terminated()**三个接口方法,可以通过这三个方法对线程池进行扩展。在执行每一个任务前都会调用beforeExecute()方法,每个任务执行完后都会调用afterExecute()方法,在线程池关闭时会调用terminated()方法。在实际应用中,最简单的应用场景就是在这些扩展方法中输出一些有用的调试信息,以帮助系统诊断故障。

public class ExtendThreadPool{
  public static class MyTask implements Runnable{
    public String name;
    public MyTask(String name){
      this.name = name;
    }
    @Override
    public void run(){
      System.out.println("正在执行线程, ThreadID 为" + Thread.currentThread().getId());
      try{
        Thread.sleep(100);
      }catch(InterruptedException e){
        e.printStackTrace();
      }
    }
  }
  public static void main(String[] args)throws InterruptedException{
    ExecutorService es = new ThreadPoolExecutor(5,5,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()){
    @Override
    protected void beforeExecute(Thread t, Runnable r){
      System.out.println("准备执行"+((MyTask)r).name);
    }
    @Override
    protected void afterExecute(Runnable r,Throwable t){
      System.out.println("执行完成"+((MyTask)r).name);   
    }
    @Override
    protected void terminated(){
      System.out.println("线程池关闭");
    } 
  };
   for(int i=0;i<5;i++){
     MyTask task = new MyTask("任务" + i);
     // 使用execute而不是submit可以在抛出异常时得到部分堆栈信息
     es.execute(task);
     Thread.sleep(100);
   }
   // 关闭线程池
   es.shutdown();
  }
}                

在这里插入图片描述
shutdown()方法是一个关闭线程池比较安全的方法,并不会立即终止所有任务,而是会等待所有任务执行完成后,再关闭线程池。

5、选择合理的线程池线程数量

线程池中的线程数量过多会使系统负载过大,而太少又无法充分发挥系统的性能。一般来说,确定线程池的大小需要考虑CPU数量,内存大小等因素,有一个估算线程池大小的经验公式:

线程数量 = 目标CPU使用率 * CPU数量 * (1 + 等待时间 / 计算时间)

比如,目标CPU使用率为80%,CPU核心数为10,等待时间即任务提交后直到被执行期间的时间,假设允许等待100毫秒,计算时间即任务的执行时间,假设也为100毫秒,因此
线程数量 = 80% * 10 * (1 +1)= 16

6、打印异常信息

在使用线程池时,线程池有可能会隐藏某些异常,而不会将其抛出,没有任何错误提示,因此可以通过对ThreadPoolExecutor类进行扩展来将这些异常堆栈信息打印出来:

public class TraceThreadPoolExecutor extends ThreadPoolExecutor{
  public TraceThreadPoolExecutor(int corePoolSize,int maxinumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue){
    super(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue);
  }
  @Override
  public void execute(Runnable task){
    super.execute(wrap(task,clientTrace(),Thread.currentThread().getName()));
  }  
  @Override
  public Future<?> submit(Runnable task){
    return super.submit(wrap(task,clientTrace(),Thread.currentThread().getName()));
  }
  private Exception clientTrace(){
    return new Exception("Client stack trace");
  } 
  // clientStack为一个异常,里面保存的提交任务的线程的堆栈信息,异常发生时,这个异常就会被打印
  private Runnable wrap(final Runnable task,final Exception clientStack,String clientThreadName){
    return new Runnable(){
      @Override
      public void run(){
        try{
          task.run();
        }catch(Exception e){
          clientStack.printStackTrace();
          throw e;
        }
      }
    };
  }
}               

7、线程池中的存活时间如何实现

runWorker(Worker w)方法,线程池在启动后会通过该方法一直循环的从任务队列中获取任务来执行,获取任务的最大等待时间就是存活时间。如果没有获取到任务,说明线程池空闲,会调用processWorkerExit(Worker w, boolean completedAbruptly)做终止线程的处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值