线程池

159 阅读12分钟

Executor組成

image.png

Executor

Executor是Java异步目标任务的“执行者”接口,其目标是来执行目标任务。

ExecutorService

ExecutorService继承于Executor。它是Java异步目标任务的“执行者服务”接口,对外提供异 步任务的接收服务,ExecutorService提供了“接收异步任务并转交给执行者”的方法,如submit系 列方法、invoke系列方法等。

Future<?> submit(Runnable task);


<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
    throws InterruptedException;

AbstractExecutorService

AbstractExecutorService是一个抽象类,它实现了ExecutorService接口,AbstractExecutorService存在的目的是为ExecutorService中的接口提供默认实现

ThreadPoolExecutor

ThreadPoolExecutor是JUC线程池的核心实现类。

ScheduledExecutorService

ScheduledExecutorService是一个接口,它继承于ExecutorService。它是一个可以完成“延时” 和“周期性”任务的调度线程池接口

ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor继承于ThreadPoolExecutor,它提供了ScheduledExecutorService线程池接口中“延时执行”和“周期执行”等抽象调度方法的具体实现。

Executors

Executors 是 个 静 态 工 厂 类 , 它 通 过 静 态 工 厂 方 法 返 回 ExecutorService 、 ScheduledExecutorService等线程池实例对象

Executors 的 4 种快捷创建线程池的方法

方法功能
newFixedThreadPool创建一个固定大小的线程池
newSingleThreadExecutor创建一个线程的线程池
newCachedThreadPool创建一个不限制线程数量的线程池,任何任务都会立即提交,但是空闲线程会回收
newScheduledThreadPool创建一个可定期或延期回收的线程池

newSingleThreadExecutor

1)单线程化的线程池中的任务,是按照提交的次序顺序执行的。
2)池中的唯一线程的存活时间是无限的。
3)当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队 列是无界的。

newFixedThreadPool

1)如果线程数没有达到“固定数量”,每次提交一个任务池内就创建一个新线程,直到线程 达到线程池固定的数量。 2)线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束, 那么线程池会补充一个新线程。 3)在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻 塞队列中(无界的阻塞队列)

newCachedThreadPool

1)在接收新的异步任务target执行目标实例时,如果池内所有线程繁忙,此线程池就会添加新 线程来处理任务。
2)此线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能 够创建的最大线程大小。
3)如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收空闲( 60秒不执行任务)线程

newScheduledThreadPool

该方法用于创建一个“可调度线程池”,即一个提供“延时”和“周期性”任务的调度功能 的ScheduledExecutorService类型的线程池。

//方法一:创建一个可调度线程池,池内仅含有一个线程

public static ScheduledExecutorService newSingleThreadScheduledExecutor();

//方法二:创建一个可调度线程池,池内含有N个线程,N的值为输入参数corePoolSize

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) ;

线程池的标准创建方式

//使用标准构造器构造一个普通的线程池

public ThreadPoolExecutor(
        int corePoolSize, // 核心线程数,即使线程空闲(Idle),也不会回收
        int maximumPoolSize, // 线程数的上限
        long keepAliveTime, TimeUnit unit, // 线程最大空闲(Idle)时长
        BlockingQueue<Runnable> workQueue, // 任务的排队队列
        ThreadFactory threadFactory, // 新线程的产生方式
        RejectedExecutionHandler handler) // 拒绝策略

corePoolSize和maximumPoolSize

corePoolSize用于定义线程池核心线程数,maximumPoolSize线程池最大的线程数。

  • 当线程池收到收到新任务,如果当前工作线程数少于corePoolSize,每个线程池任务就会创建一个核心线程,直到线程池核心线程等于核心线程数。
  • 如果当前工作线程多于corePoolSize数量,但小于maximumPoolSize数量,此时不会在创建新线程,仅当队列已满才会继续创建线程。通过设置corePoolSize和maximumPoolSize相同,可以创建一个固 定大小的线程池。
  • 当maximumPoolSize被设置为无界值(如Integer.MAX_VALUE)时,线程池可以接收任意 数量的并发任务。
  • corePoolSize和maximumPoolSize不仅能在线程池构造时设置,也可以调用setCorePoolSize() 和setMaximumPoolSize()两个方法进行动态更改。

BlockingQueue

BlockingQueue(阻塞队列)的实例用于暂时接收到的异步任务,如果线程池的核心线程都在 忙,那么所接收到的目标任务缓存在阻塞队列中

keepAliveTime

线程构造器的keepAliveTime(空闲线程存活时间)参数用于设置池内线程最大Idle(空闲)时 长或者说保活时长,如果超过这个时间,默认情况下Idle、非Core线程会被回收。

向线程池提交任务的两种方式

方式一:调用execute()方法
方式二:调用submit()方法

线程池的任务调度流程

  • 如果当前工作线程数量小于核心线程池数量,执行器总是优先创建一个任务线程,而不是从线程队列中获取一个空闲线程。

  • 如果线程池中总的任务数量大于核心线程池数量,新接收的任务将被加入到阻塞队列中一直到阻塞队列已满。在核心线程池数量已经用完、阻塞队列没有满的场景下,线程池不会为新任务创建一个新线程。

  • 当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行,一直到阻塞队列为空,其中所有的缓存任务被取光。

-在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。

  • 在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出maximumPoolSize。如果线程池的线程总数超过maximumPoolSize,线程池就会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略。

image.png

ThreadFactory

public interface ThreadFactory {

    Thread newThread(Runnable r);
}

在调用ThreadFactory的唯一方法newThread()创建新线程时,可以更改创建新线程的名称、线程组、优先级、守护进程状态等。如果newThread()返回值为null,表示线程工厂未能成功创建线程,线程池可能无法执行任何任务。

在创建新线程池时可以指定将使用ThreadFactory 实 例 。如 果 没 有 指 定 的 话 , 就 会 使 用Executors.defaultThreadFactory默认实例。使用默认的线程工厂实例所创建的线程全部位于同一个ThreadGroup(线程组)中,具有相同的NORM_PRIORITY(优先级为5),而且都是非守护进程状态。

public class SimpleThreadFactory implements ThreadFactory {

    @Override
    public Thread newThread(@NotNull Runnable runnable) {
        String threadName = "SimpleThreadFactory";
        Thread thread = new Thread(runnable,threadName);
        thread.setPriority(6);
        return thread;
    }
}

无界队列和有界队列

无界队列:当接收的任务数量超出corePoolSize数量时,则新任务可以被无限制地缓存到该阻塞队列中,直到资源耗尽。

有界队列:可以设置容量

任务阻塞队列

Java线程池使用BlockingQueue存放接收到的异步任务

1)ArrayBlockingQueue:是一个数组实现的有界阻塞队列(有界队列),队列中的元素按FIFO排序。ArrayBlockingQueue在创建时必须设置大小,接收的任务超出corePoolSize数量时,任务被缓存到该阻塞队列中,任务缓存的数量只能为创建时设置的大小,若该阻塞队列满,则会为新的任务创建线程,直到线程池中的线程总数大于maximumPoolSize。

2)LinkedBlockingQueue:是一个基于链表实现的阻塞队列,按FIFO排序任务,可以设置容量(有界队列),不设置容量则默认使用Integer.Max_VALUE作为容量(无界队列)。该队列的吞吐量高于ArrayBlockingQueue。

3)PriorityBlockingQueue:是具有优先级的无界队列。

4)DelayQueue:这是一个无界阻塞延迟队列,底层基于PriorityBlockingQueue实现,队列中每个元素都有过期时间,当从队列获取元素(元素出队)时,只有已经过期的元素才会出队,而队列头部的元素是最先过期的元素。快捷工厂方法Executors.newScheduledThreadPool所创建的线程池使用此队列

5)SynchronousQueue(同步队列):是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程的调用移除操作,否则插入操作一直处于阻塞状态,其吞吐量通常高于LinkedBlockingQueue。

调度器的钩子方法

ThreadPoolExecutor线程池调度器为每个任务执行前后都提供了钩子方法。  
protected void beforeExecute(Thread t, Runnable r) { } // 任务执行前
protected void afterExecute(Runnable r, Throwable t) { } // 任务执行后
protected void terminated() { } // 线程池执行结束后

1)beforeExecute:异步任务执行之前的钩子方法
线程池工作线程在异步执行完成的目标实例(如Runnable实例)前调用此钩子方法。此方法仍然由执行任务的工作线程调用。默认实现不执行任何操作,但可以在子类中对其进行自定义。

2)afterExecute:异步任务执行之后的钩子方法 线程池工作线程在异步执行目标实例后调用此钩子方法。此方法仍然由执行任务的工作线程调用。此钩子方法的默认实现不执行任何操作,可以在调度器子类中对其进行自定义。

3)terminated:线程池终止时的钩子方法 terminated钩子方法在Executor终止时调用,默认实现不执行任何操作

线程池的拒绝策略

任务被拒绝有两种情况:
1)线程池已经被关闭。
2)工作队列已满且maximumPoolSize已满

无论以上哪种情况任务被拒绝,线程池都会调用RejectedExecutionHandler实例的rejectedExecution()方法。RejectedExecutionHandler是拒绝策略的接口,JUC为该接口提供了以下几种实现:

AbortPolicy:拒绝策略。 使用该策略时,如果线程池队列满了,新任务就会被拒绝,并且抛出RejectedExecutionException异常。该策略是线程池的默认的拒绝策略。

DiscardPolicy:抛弃策略。 该策略是AbortPolicy的Silent(安静)版本,如果线程池队列满了,新任务就会直接被丢掉,并且不会有任何异常抛出。

DiscardOldestPolicy:抛弃最老任务策略。 抛弃最老任务策略,也就是说如果队列满了,就会将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列。因为队列是队尾进队头出,队头元素是最老的,所以每次都是移除对头元素后再尝试入队。

CallerRunsPolicy:调用者执行策略。 调用者执行策略。在新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务。

线程池的关闭

线程池的5种状态

1)RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。

2)SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕。

3)STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程。

4)TIDYING:该状态下所有任务都已终止或者处理完成,将会执行terminated()钩子方法。

5)TERMINATED:执行完terminated()钩子方法之后的状态。

关闭线程的方法

1)shutdown:是JUC提供一个有序关闭线程池的方法,此方法会等待当前工作队列中的剩余任务全部执行完成之后才会执行关闭,但是此方法被调用之后线程池的状态转变为SHUTDOWN,线程池不会再接收新的任务。

2)shutdownNow:是JUC提供一个立即关闭线程池的方法,此方法会打断正在执行的工作线程,并且会清空当前工作队列中的剩余任务,返回的是尚未执行的任务

3)awaitTermination:等待线程池完成关闭。在调用线程池的shutdown()与shutdownNow()方法时,当前线程会立即返回,不会一直等待直到线程池完成关闭。如果需要等到线程池关闭完成,可以调用awaitTermination()方法

ThreadLocal

如果程序创建了一个ThreadLocal实例,那么在访问这个变量的值时,每个线程都会拥有一个独立的、自己的本地值。“线程本地变量”可以看成专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。当线程结束后,每个线程所拥有的那个本地值会被释放。在多线程并发操作“线程本地变量”的时候,线程各自操作的是自己的本地值,从而规避了线程安全问题。

image.png

如果希望能从线程本地变量获取到初始值,而且也不想采用以上的“判空后设值”这种相对烦琐的方式,可以调用ThreadLocal.withInitial(...)静态工厂方法,在定义ThreadLocal对象时设置一个获取初始值的回调函数

ThreadLocal<Foo> LOCAL_FOO = ThreadLocal.withInitial(() -> new Foo());

ThreadLocal使用场景

(1)线程隔离
ThreadLocal的主要价值在于线程隔离,ThreadLocal中的数据只属于当前线程,其本地值对别 的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。另外,由于各个线程 之间的数据相互隔离,避免了同步加锁带来的性能损失,大大提升了并发性的性能。

(2)跨函数传递数据
由于ThreadLocal的特性,同一线程在某些地方进行设置,在随后的任意地方都可以获取到。 线程执行过程中所执行到的函数都能读写ThreadLocal变量的线程本地值,从而可以方便地实现跨 函数的数据传递。使用ThreadLocal保存函数之间需要传递的数据,在需要的地方直接获取,也能 避免通过参数传递数据带来的高耦合。