Java 线程池

一、线程池的实现原理

合理使用线程池能够带来3个好处

  • 降低资源消耗:通过重复利用已创建的线程来降低线程创建和销毁造成的消耗。
  • 提高相应速度:任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性:使用线程池对线程统一进行分配、调优和监控。

工作线程:线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会循环获取工作队列里的任务来执行。

1.1 线程池的处理流程

在这里插入图片描述

1.2 ThreadPoolExecutor 执行 execute() 方法
  • 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务,不论线程是否有空闲(执行这一步骤需要获取全局锁)。
  • 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue
  • 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(执行这一步骤需要获取全局锁),超过 corePoolSize 的线程在空闲时间超过 keepAliveTime 后销毁。
  • ThreadPoolExecutor已经关闭ThreadPoolExecutor已经饱和(达到了最大线程池且队列已满),任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

在这里插入图片描述
执行execute() 方法时,尽可能避免获取全局锁。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用的都是步骤2,而步骤2不需要获取全局锁

1.3 ThreadPoolExecutor 中线程执行任务

线程池中的线程执行任务分两种情况,如下。

  • execute()方法中创建一个线程时,会让这个线程执行当前任务。
  • 这个线程执行完上述任务后,会反复从BlockingQueue获取任务来执行。
    在这里插入图片描述
二、线程池的使用
2.1 ThreadPoolExecutor 的参数

我们可以通过ThreadPoolExecutor来创建一个线程池。

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
milliseconds,runnableTaskQueue, handler);

创建一个线程池时需要输入几个参数,如下。

  • 1. corePoolSize(核心线程池的大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
  • 2. BlockingQueue<Runnable> workQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:
    ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
    LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueueFixedThreadPoolSingleThreadExecutor 使用了这个队列。
    SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    PriorityBlockingQueue:一个具有优先级无限阻塞队列。
  • 3. maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务,线程执行完当前任务后会去队列中执行其他任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。
  • 4. ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。使用开源框架guava提供的ThreadFactoryBuilder可以快速给线程池里的线程设置有意义的名字,代码如下。
    new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();
  • 5. RejectedExecutionHandler(饱和策略):当队列线程池都满了,说明线程池处于饱和状态,或者线程池不处于运行中(已经执行了ShutDown()方法),那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。
    AbortPolicy:直接抛出异常。Executors 实现的创建的三种线程池都是这个饱和策略。
    CallerRunsPolicy:只用调用者所在线程来运行任务。
    DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    DiscardPolicy:不处理,丢弃掉。
    也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化存储不能处理的任务。
  • 6. keepAliveTime(线程活动保持时间):当线程数超过corePoolSize且线程池的工作线程空闲时间超过keepAliveTime,则结束线程。如果线程数不大于corePoolSize,空闲也不结束。
  • 7. TimeUnit(时间单位)keepAliveTime 参数的时间单位。
2.2 向线程池提交任务

可以使用两个方法向线程池提交任务,分别为execute()submit()方法。

execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功,execute()方法输入的任务是一个Runnable类的实例。

submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

2.3 关闭线程池

可以通过调用线程池的shutdownshutdownNow方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。

2.4 合理地配置线程池

CPU密集型任务应配置尽可能小的线程,如配置 N + 1(N是CPU核心个数)个线程的线程池,IO密集型的任务配置 2 * N

2.5 线程池的监控
  • taskCount:线程池需要执行的任务数量。
  • completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
  • largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。
  • getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减。
  • getActiveCount:获取活动的线程数。

通过扩展线程池进行监控。可以通过继承线程池来自定义线程池,重写线程池的
beforeExecuteafterExecuteterminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。

三、 Executor 框架

在这里插入图片描述
在 HotSpot VM 的线程模型中,Java线程被一对一映射为本地操作系统线程。

3.1 Executor 框架的结构
  • 任务:包括被执行任务需要实现的接口:Runnable接口或Callable接口。他们之间的区别是Runnable不会返回结果,而Callable可以返回结果。可以使用工厂类 Executors 把一个Runnable包装成一个Callable。
  • 任务的执行:包括任务执行机制的核心接口Executor,以及继承自ExecutorExecutorService接口。Executor框架有两个关键类实现了ExecutorService接口(ThreadPoolExecutorScheduledThreadPoolExecutor)。
  • 异步计算的结果:包括接口Future和实现Future接口的FutureTask类。
3.2 Executor 框架主要的类与接口

在这里插入图片描述
左上角的接口应为Runnable。
在这里插入图片描述

  • Executor 接口在JUC包中,可以管理 Thread 对象,从而简化并发编程。Executor 将任务的提交与任务的执行分离开来。
  • ExecutorService 接口继承自Executor 接口。相当于具有服务生命周期的Executor(例如关闭),知道如何构建恰当的上下文来执行Runnable对象。
  • ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。
  • ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。ScheduledThreadPoolExecutorTimer更灵活,功能更强大,并且Time是单个后台进程,前者可以是多个。
  • Future接口和实现Future接口的FutureTask类,代表异步计算的结果。
  • shutdown() 方法防止新任务被提交给这个Executor,shutdown() 不会影响已经提交的线程任务
  • shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。

可以把Runnable对象直接交给ExecutorService执行(ExecutorService.execute(Runnable command))。或者也可以把Runnable对象或Callable对象提交给ExecutorService执行(ExecutorService.submit(Runnable task)ExecutorService.submit(Callable<T>task))。

如果执行ExecutorService.submit(…),ExecutorService将返回一个实现Future接口的对象(到目前为止的JDK中,返回的是FutureTask对象)。由于FutureTask实现了Runnable,程序员也可以创建FutureTask,然后直接交给ExecutorService执行。最后,主线程可以执行FutureTask.get()方法(会阻塞线程)来等待任务执行完成。主线程也可以执行FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。

四、ThreadPoolExecutor

ThreadPoolExecutor 通常使用工厂类 Executors 来创建,Executors 类 提供了3种基于ThreadPoolExecutor类构造线程池模型的方法:

  • CachedThreadPool:通常一个任务创建一个新线程,多个线程同时进行,但如有线程执行完会复用。
  • FixedThreadPool:所有任务只能使用固定大小的线程,初始化时指定线程池大小,重复使用线程。
  • SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool,如果向它提交了多个线程任务,这些任务将排队运行,所有任务使用相同的线程。
4.1 CachedThreadPool

CachedThreadPool 通常一个任务创建一个新线程,多个线程同时进行,但如有线程执行完会复用

CachedThreadPool 是没有容量的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。

类定义
这里用SynchronousQueue,队列里不存储值,线程空闲最多存活60秒。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
class PrintRunnable implements Runnable {
    private static int i = 0;

    @Override
    public void run() {
        System.out.println(i++);
    }
}

public class CacheThreadPool {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool(); 
        // 静态方法,生成 CachedThreadPool
        for (int i = 0; i < 5; ++ i) {
        // 这里创建5个线程,因为有5个任务
            exec.execute(new PrintRunnable()); 
            // 参数为实现Runnable接口的类
        }
        exec.shutdown();
        // 作用是防止新任务被提交给这个Executor
        // shutdown() 不会影响已经提交的线程任务
    }
}

CachedThreadPool 的运行流程
在这里插入图片描述

  • 1)首先执行SynchronousQueue.offer(Runnable task)。如果当前maximumPool中有空闲线程正在执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成。否则执行下面的步骤。
  • 2)当初始maximumPool为空,或者maximumPool中当前没有空闲线程时,将没有线程执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤1)将失败。此时CachedThreadPool会创建一个新线程执行任务,execute()方法执行完成。
  • 3)在步骤2)中新创建的线程将任务执行完后,会执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这个poll操作会让空闲线程最多在SynchronousQueue中等待60秒钟。如果60秒钟内主线程提交了一个新任务(主线程执行步骤1)),那么这个空闲线程将执行主线程提交的新任务。否则,这个空闲线程将终止。由于空闲60秒的空闲线程会被终止,因此长时间保持空闲的CachedThreadPool不会使用任何资源。
4.2 FixedThreadPool

FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。

类定义

  • corePoolSizemaximumPoolSize 数量相同,自己设置。
  • keepAliveTime 为0。
  • TimeUnit 是千分之一秒。
  • workQueueLinkedBlockingQueue,一个基于链表结构的阻塞队列,队列容量为 Integer.MAX_VALUE,称为无界队列。
// Executors.java
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

// ThreadPoolExecutor.java
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
        Executors.defaultThreadFactory(), defaultHandler);
}

FixedThreadPool 的运行流程
在这里插入图片描述

  • 如果当前运行的线程数少于corePoolSize,则创建新线程来执行任务。
  • 在线程池完成预热之后(当前运行的线程数等于corePoolSize),将任务加入LinkedBlockingQueue。
  • 线程执行完1中的任务后,会在循环中反复从LinkedBlockingQueue获取任务来执行。

类使用

// 这里 FixedThreadPool 指定了容量为10的线程池,本例中先输出了0~9
// 约3秒后,其余的10个任务复用了之前的线程,输出了剩余的10~19。

class PrintRunnable implements Runnable {
    private static int i = 0;

    @Override
    public void run() {
        System.out.println(i++);
        try {
            Thread.sleep(3000);
        } catch (Exception e) {

        }
    }
}

public class CacheThreadPool {
    public static void main(String[] args) {
        ExecutorService exec = Executors.newFixedThreadPool(10); 
        // 静态方法,一次性生成大小为10的线程池
        for (int i = 0; i < 20; ++ i) {
        // 这里先运行10个线程,然后其余线程等待池中的线程空出再运行
            exec.execute(new PrintRunnable()); 
            // 参数为实现Runnable接口的类
        }
        exec.shutdown();
        // 作用是防止新任务被提交给这个Executor
        // shutdown() 不会影响已经提交的线程任务
    }
}
4.3 SingleThreadExecutor

类定义
相当于nThreads为1的FixedThreadPool

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

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,通常使用工厂类 Executors 来创建,它主要用来在给定的延迟之后运行任务,或者定期执行任务。Executors可以创建2种类型的ScheduledThreadPoolExecutor

  • ScheduledThreadPoolExecutor:包含若干个线程的ScheduledThreadPoolExecutor,适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程的数量的应用场景。
  • SingleThreadScheduledExecutor:只包含一个线程的ScheduledThreadPoolExecutor,适用于需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的应用场景。
5.1 ScheduledThreadPoolExecutor 类定义
  • 核心线程池的大小自己定义。
  • 使用DelayQueue队列,无界(最多存储Integer.MAX_VALUE个任务)。
// J.U.C.ScheduledThreadPoolExecutor.java
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

// J.U.C.ThreadPoolExecutor.java
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
5.2 ScheduledThreadPoolExecutor 的运行机制

在这里插入图片描述
ScheduledThreadPoolExecutor的执行主要分为两大部分:

  • 当调用ScheduledThreadPoolExecutorscheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法时,会向ScheduledThreadPoolExecutorDelayQueue添加一个实现了RunnableScheduledFuture接口的ScheduledFutureTask
  • 线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务。

DelayQueue 的排序:

前面我们提到过,ScheduledThreadPoolExecutor会把待调度的任务(ScheduledFutureTask)放到一个DelayQueue中。ScheduledFutureTask主要包含3个成员变量,如下。

  • long型成员变量time,表示这个任务将要被执行的具体时间。
  • long型成员变量sequenceNumber,表示这个任务被添加到ScheduledThreadPoolExecutor中的序号。
  • long型成员变量period,表示任务执行的间隔周期。

DelayQueue封装了一个PriorityQueue,这个PriorityQueue会对队列中的ScheduledFutureTask进行排序。排序时,time小的排在前面(时间早的任务将被先执行)。如果两个ScheduledFutureTask的time相同,就比较sequenceNumbersequenceNumber小的排在前面(也就是说,如果两个任务的执行时间相同,那么先提交的任务将被先执行)。

ScheduledThreadPoolExecutor 执行周期任务的过程:
在这里插入图片描述

  • 线程1从DelayQueue中获取已到期的ScheduledFutureTask(DelayQueue.take())。到期任务是指ScheduledFutureTasktime大于等于当时间。
  • 线程1执行这个ScheduledFutureTask
  • 线程1修改ScheduledFutureTasktime变量为下次将要被执行的时间。
  • 线程1把这个修改time之后的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())。
六、Future 接口

Future接口和实现Future接口的FutureTask类用来表示异步计算的结果。当我们把Runnable接口或Callable接口的实现类提交(submit)给ThreadPoolExecutor或ScheduledThreadPoolExecutor时,ThreadPoolExecutor或ScheduledThreadPoolExecutor会向我们返回一个FutureTask对象。

<T> Future<T> submit(Callable<T> task)  
<T> Future<T> submit(Runnable task, T result)  
Future<> submit(Runnable task)  
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值