线程池详解

744 阅读8分钟

Java线程池

线程池是企业开发中常用的一种多线程处理形式,线程池中维护了一定数量的线程,线程池内的线程可以并发执行外部提交的多个任务。在开发过程中,合理地使用线程池能够带来许多好处。

一、线程池的优点:

1.降低资源消耗。线程池可以充分复用线程,让线程持续不断地处理任务,有效避免频繁创建和销毁线程造成的资源消耗。
2.提高响应速度。当任务到达时,任务可以不需要等待线程的创建就立即执行
3.提高线程的可管理性。线程是属于一种稀缺资源,如果无限制地创建线程,不仅会消耗系统资源,还会降低系统的稳定性

二、创建线程池的四种方法

(1) 创建一个只有一个线程的线程池Executors.newSingleThreadExecutor()。它只会用一个唯一的工作线程来执行任务,如果这个唯一线程因为异常结束,那么会有一个新的工作线程来替代它,它必须保证前一项任务执行后,才能执行后一项任务。

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

(2) 创建包含n个线程的线程池Executors.newFixedThreadPool(n)。容易内存溢出

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

(3) 创建一个可伸缩的线程池,Executors.newCachedThreadPool()。即如果线程池中的线程数量操作处理的需要,可灵活收回空闲线程,若无可回收则创建新的线程。从以下可以看出,它可容纳的最大线程数量为Integer.MAX_VALUE,所以比较容易出现内存溢出。

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

(4)创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。Executors.newScheduledThreadPool,newScheduledThreadPool是Executor的一个静态方法,在newScheduledThreadPool()方法中,调用ScheduledThreadPoolExecutor的构造方法,ScheduledThreadPoolExecutor是继承ThreadPoolExecutor并实现了ScheduledExecutorService接口的类,在ScheduledThreadPoolExecutor构造方法中,调用其父类构造方法ThreadPoolExecutor来创建线程.

  • 源码
  (1) ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
    
  (2)public class Executors {
        public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize){
            return new ScheduledThreadPoolExecutor(corePoolSize);
        }
     }
     
  (3)public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService {
    
        public ScheduledThreadPoolExecutor(int corePoolSize) {
            super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
            }
        }
  • 创建例子
 ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3); 
     scheduledThreadPool.schedule(new Runnable(){ 
         @Override 
         public void run() {
             System.out.println("延迟三秒");
         }
     }, 3 , TimeUnit.SECONDS);
     
    scheduledThreadPool.scheduleAtFixedRate(new Runnable(){ 
         @Override 
         public void run() {
             System.out.println("延迟 1 秒后每三秒执行一次");
         }
     }, 1 , 3 , TimeUnit.SECONDS);

其实,我们通过查看以上创建线程池的三种方法的源码可以发现,他们在创建线程池的时候都会调用下面这个重载的构造方法ThreadPoolExecutor(),根据传入不同的参数,构成出适用于不同应用场景的线程池。

三、ThreadPoolExecutor介绍

ThreadPoolExecutor就是线程池。一般我们通过Executors工厂类的方法,通过传入不同的参数,就可以构造适用于不同应用场景下的ThreadPoolExecutor线程池。在阿里巴巴的开发规约中明确指出,线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor去创建,因为这样的处理方式可以让我们更加明确线程池的运行规则,规避资源耗尽的风险。 那么,在使用ThreadPoolExecutor创建线程池之前,我们需要了解并熟悉一下ThreadPoolExecutor的构造方法:

public ThreadPoolExecutor(int corePoolSize, //核心线程池的大小
                          int maximumPoolSize, //最大的核心线程池的数量
                          long keepAliveTime,	//没人调用的存活时间,即最大空闲时间
                          TimeUnit unit,	//超时单位
                          BlockingQueue<Runnable> workQueue,  //阻塞队列
                          ThreadFactory threadFactory, // 线程工厂,用来创建线程的
                          RejectedExecutionHandler handler  //拒绝策略
  1. int corePoolSize:线程池内的核心线程的数量。如果属性allowCoreThreadTimeOut为false,默认也为false,那么核心线程即使处于空闲状态也不会被收回。
  2. int maximumPoolSize: 线程池可容纳的最大线程数量。
  3. long keepAliveTime: 空闲线程等待新任务的最大等待时间。
  4. TimeUnit unit : keepAliveTime的时间单位
  5. BlockingQueue < Runnable> workQueue :阻塞队列。workQueue可以是ArrayBlockingQueue、LinkedBlockingQueue或者DelayQueue等任意一种阻塞队列。newFixedThreadPool()方法创建的线程池使用的就是LinkedBlockingQueue,在搞并发环境下如果提交到线程池的任务过多,就会造成LinkedBlockingQueue阻塞队列中存储的任务过多,在极端情况下可能造成内存溢出。
  6. TreadFactory threadFactory:线程工厂,用来创建线程的
  7. RejectedExecutionHandler handler:拒绝策略,主要有4种,后面介绍
  • 通俗的解释就是,如果一个银行柜台有5个窗口,其中有2个窗口是开通有工作人员在处理业务,那么这里的核心线程池的大小就是2,而最大的核心线程池数量就是5,当来办理业务的人数增加时,就可以开通另外3个窗口的工作人员来处理业务;最大空闲时间,就是如果除了那2个核心的办理业务的窗口外,另外的开通的窗口如果超过最大空闲时间,即没有客户来办理业务,那么该窗口就会被收回;阻塞队列就是银行里的等候区,阻塞队列的长度就是等候区所能容纳的最大人数,如果超过此阈值,就不再让客户进来办理业务了。这样解释,就应该可以很清楚地知道各个参数代表的是什么意思。

四、4种拒绝策略

1. AbortPolicy 当线饱和后,抛出RejectedExecutionException异常

/**
 * 拒绝策略1 AbortPolicy ->超出最大承载量抛java.util.concurrent.RejectedExecutionException
 */
public class demo1 {
    public static void main(String[] args) {
        //自定义线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2,
                5,
                0,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        try{
            //最大承载(即能处理的最大任务数量):等于LinkedBlockingDeque的capacity(即我们设置的阻塞队列的长度) + maximumPoolSize
            for (int i = 1; i <=9; i++) {
                //超过maximumPoolSize+capacity的大小,会被拒绝并抛出java.util.concurrent.RejectedExecutionException异常
                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName());
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}
输出:
    pool-1-thread-1
    pool-1-thread-2
    pool-1-thread-3
    pool-1-thread-4
    pool-1-thread-1
    pool-1-thread-2
    pool-1-thread-5
    pool-1-thread-3
    java.util.concurrent.RejectedExecutionException: Task com.gjy.demo.ThreadPool.demo1$$Lambda$1/1096979270@7ba4f24f rejected from java.util.concurrent.ThreadPoolExecutor@3b9a45b3[Running, pool size = 5, active threads = 5, queued tasks = 0, completed tasks = 3]
    	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
    	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
    	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
    	at com.gjy.demo.ThreadPool.demo1.main(demo1.java:30)

2. CallerRunsPolicy 当线程饱和后,由提交任务的线程代替执行任务

/**
 * 拒绝策略2 CallerRunsPolicy ->超出最大承载量时返回主线程进行处理
 */
public static void main(String[] args) {
    //自定义线程池
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
            2,
            5,
            0,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<>(3),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.CallerRunsPolicy());

    try{
        //最大承载(即能处理的最大任务数量):等于LinkedBlockingDeque的capacity(即我们设置的阻塞队列的长度) + maximumPoolSize
        for (int i = 1; i <=9; i++) {

            threadPool.execute(()->{
                System.out.println(Thread.currentThread().getName());
            });
        }
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        threadPool.shutdown();
    }
}
输出:
        pool-1-thread-1
        pool-1-thread-3
        pool-1-thread-2
        main
        pool-1-thread-4
        pool-1-thread-3
        pool-1-thread-5
        pool-1-thread-1
        pool-1-thread-2


3. DiscardPolicy 丢弃阻塞队列中最早入队的任务

/**
 * 拒绝策略3 DiscardPolicy ->超出最大承载量时丢弃任务,不抛出异常
 */
public class demo3 {
    public static void main(String[] args) {
        //自定义线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2,
                5,
                0,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardPolicy());

        try{
            //最大承载(即能处理的最大任务数量):等于LinkedBlockingDeque的capacity(即我们设置的阻塞队列的长度) + maximumPoolSize
            for (int i = 1; i <=9; i++) {

                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName());
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}
输出:
        pool-1-thread-1
        pool-1-thread-3
        pool-1-thread-2
        pool-1-thread-4
        pool-1-thread-3
        pool-1-thread-1
        pool-1-thread-5
        pool-1-thread-2

4. DiscardOldestPolicy

/**
 * 拒绝策略3 DiscardOldestPolicy ->超出最大承载量时丢弃队列里最久未处理的请求,然后重试,不会抛出异常
 */
public class demo4 {
    public static void main(String[] args) {
        //自定义线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                2,
                5,
                0,
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy());

        try{
            //最大承载(即能处理的最大任务数量):等于LinkedBlockingDeque的capacity(即我们设置的阻塞队列的长度) + maximumPoolSize
            for (int i = 1; i <=9; i++) {

                threadPool.execute(()->{
                    System.out.println(Thread.currentThread().getName());
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

ThreadPoolExecutor工作流程

  • 工作流程如图: image.png
  • 当调用execute()方法提交任务到线程池执行时,线程池的状态变化如下:
  1. 当正在运行的线程数量小于核心线程数时,线程池创建新线程用于执行提交到线程池的任务
  2. 当正在运行的线程数量大于等于核心线程数量时,任务将进入阻塞队列,等待核心线程执行。如果阻塞队列还没有达到最大容量,则入队成功,否则走第三步
  3. 如果阻塞队列满了,并且正在运行的线程数量小于maximumPoolSize,则创建非核心线程执行任务,否则创建失败,走第四步拒绝策略
  4. 如果阻塞队列满了,并且正在运行的线程数量等于maximumPoolSize,则线程池执行拒绝处理程序
  5. 一个线程完成任务后。就会从阻塞队列中去取一个任务来执行
  6. 当一个线程空闲时间超过设定值时,线程池会判断,如果当前正在运行的线程数大于核心线程数时,那么这个超过空闲时间的线程就会被停掉。因此,当所有任务执行完成后,线程池的线程数量会收缩到核心线程数。

五、 Executors 和Executor的区别?

  • Executors是一个工具类,通过它我们可以根据不同需求创建不同的线程池,例如上面我们就是利用Executors工具类来创建线程池的。
  • Executor是Java里面线程池的顶级接口,但严格意义上Executor并不是一个线程池,而是一个线程执行工具,可以用来执行我们的线程任务的,而真正的线程池接口是ExecutorService

六、线程池ThreadPoolExecutor的状态

  1. Running: 正常状态,接收新的任务,处理阻塞队列中的任务
  2. ShutDown: 不接受新的任务提交,但会继续处理阻塞队列中的任务
  3. Stop: 不接受新的任务提交,不再处理阻塞队列中的任务,中断正在执行任务的线程
  4. Tidying: 所有的任务都销毁了,workCount为0,线程池的状态转换为Tidying状态时,会执行钩子方法terminated()
  5. Terminated: terminated()方法结束后,线程池状态就变为Terminated
  • 线程池各状态间的转换如图:

image.png