线程池
1.说到线程,就不得不说线程的创建方式了
- 继承Thread 类,将子类对象传入Thread 的构造方法中
- 实现 Runnable 接口 ,将接口的实现类传入到 Thread 的构造方法
- 实现 Callable 接口 ,将接口的子类对象 传入FutureTask ,因为这个类实现 Runable 接口,就可以创建线程,但是这个是可以有返回值的
- 线程池创建 ( 这个是今天的重点)
2. 创建线程池的方法有哪些呢 ?
这个我们先绕一下弯子, 去查看线程池 ThreadPoolExecutor
的构造方法, 最后都调用的这个方法。
没有无参构造。
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 存活时间
TimeUnit unit, // 存活时间的单位
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler) {
// 拒绝策略
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
// 对每个数量的参数进行判断排除,如果不规范小于0 ,就会抛出异常
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
// 当传入的对象为空的时候也会抛出空指针异常
// 由此可见 都不能为空
// 当数据正确,才会将数据传入
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
线程的创建是由一个工具类 Executors 调用类中的静态方法来实现的
- newFixedThreadPool (int n) 创建固定的线程数
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
通过参数克制 ,最大线程数 == 核心线程数, 存活时间 ==0 , 只有创建了核心线程数。这样的好处就可以保证额外的线程数个数,且保证一直存活,可以工作。
特别的: new LinkedBlockingQueue () 无参构造中,默认容量是Integer.MAX_VALUE
- newSingleThreadExecutor() 创建一个简单的线程池。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
线程数只有一个 也没有非核心线程数,只有一个核心线程, 这样保证额外只有一个核心线程一个进行工作。
- newCachedThreadPool() 创建缓冲线程池
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
没有核心线程数, 但是非线程数是最大的数量Integer.MAX_VALUE
,没有上限, 且每个存活的时间为 60 秒 。
特别的: 这个阻塞队列 SynchronousQueue() 是没有容量的,有任务就会创建线程来执行
3. 线程池的工作流程应该是怎么样的?
- 首先当任务进来之后 ,会优先使用核心线程去执行任务,核心线程在线程池创建的时候就创建了出来。
- 当线程池慢了之后,就会加入阻塞队列阻塞。
- 当阻塞队列满了之后,就会创建急救线程 ( 非核心线程数= 最大线程数 - 核心线程数 )
- 但是急救线程数是有存活时间的( 存活 相应的时间)
- 当运行的线程数达到最大的时候,阻塞队列也慢的时候就会实行拒绝策略
4. ThreadFactory 是干什么的呢 ?
我们发现在 Executors 在内部实现了 ThreadFactory 接口, 同时也提供了默认实现,每次线程池在创建的时候都会创建 DefaultThreadFactory 。
特别的 : 这个类主要就是为我们创建的线程起名字的。 在底层创建这个类的实例对象的时候,会通过创建线程池的数量,合理的初始化每个默认线程工厂中的 前缀 。当创建先吃执行任务的时候, 创建线程的方法中,会将名称加入。
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
5. 线程池的拒绝策略
线程池提供了有四种拒绝策略 ,查看源码,我们发现四种实现都是在 ThreadPoolExecutor
内部, 内部类实现了 RejectedExecutionHandler
接口。
public interface RejectedExecutionHandler {
// 拒绝执行处理器
/**
* 在每个实现类中,对方法重写进行实现, r 就是传入的额外任务,而 executor 就是线程池本身
*/
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}
- CallerRunsPolicy 调用者执行政策
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
很明显是,先判断线程池的状态,如果没有 进入 shutdown 状态 ,(这个是线程池的一个状态,后面会提及),它是直接执行 run(),方法没有 start() ,说明没有开辟额外的线程,是调用者的主线程来执行这个任务。
- AbortPolicy 中止政策
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
这个很明显 是直接抛出了 RejectedExecutionException
,将错误信息打印出来,包含有任务信息 和 线程池信息 。这个也是线程池默认的策略
- DiscardPolicy 丢弃政策
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
方法空实现,很明显,任务被搁置了,没有任何处理,被抛弃了。
- DiscardOldestPolicy 抛弃最老的政策
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
先判断 线程池的状态是不是中止状态,然后再将先吃池中的阻塞队列 中最早到的那个进行抛弃,然后再将这个任务加入到线程池中。
6. 线程池的状态有哪些?
不说了 ,上源码。
// 这个原子类就是来保证状态码的改变,底层通过CAS 来实现状态码的变化。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 位移量: 状态码是32位 ,但是表示状态的是 前三位,Integer.size = 32
private static final int COUNT_BITS = Integer.SIZE - 3;
// 标记量: 通过运算后悔出现 29位都是1,且前三位为0 的情况,方便获取 状态码 和 线程数值
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;
// 高三位是状态码 , 低位是线程数量
// runState is stored in the high-order bits(运行码在高阶)
// 通过偏移量来设置状态
private static final int RUNNING = -1 << COUNT_BITS; // 101
private static final int SHUTDOWN = 0 << COUNT_BITS; // 000
private static final int STOP = 1 << COUNT_BITS; // 001
private static final int TIDYING = 2 << COUNT_BITS; // 010
private static final int TERMINATED = 3 << COUNT_BITS; // 011
TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
那么问题来了,这些状态之间如何转换的。
- RUNNING :在RUNNING状态下,线程池可以接收新的任务和执行已添加的任务。
线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建(比如调用Executors.newFixedThreadPool()或者使用ThreadPoolExecutor进行创建),就处于RUNNING状态,并且线程池中的任务数为0!线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
- SHUTDOWN :线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
当一个线程池调用shutdown()方法时,线程池由RUNNING -> SHUTDOWN。
- STOP : 线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在执行的任务。
调用线程池的shutdownNow()方法的时候,线程池由(RUNNING或者SHUTDOWN ) -> STOP。
- TIDYING :当所有的任务已终止,记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。
当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,会由STOP -> TIDYING。(terminated() 为空实现,可以由用户自己完成)
- TERMINATED :当钩子函数terminated()被执行完成之后,线程池彻底终止,就变成TERMINATED状态。
线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
如果不信:看官方解释
再看官方所说的状态转换
7. 线程池的方法有哪些?
- 执行任务的方法
public void execute(Runnable command)
- 关闭线程池
public void shutdown()
- 终止线程池
public List<Runnable> shutdownNow()
8.带有定时任务的线程池
之前介绍的都是ThreadPoolExecutor
,我们学习一下ScheduledThreadPoolExecutor
这个是继承了之前学习的线程池,有延时任务的线程池
- 创建的构造函数
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue(), threadFactory);
}
这个构造函数还是调用的父类的构造函数 ,核心线程数是构造是传入的,最大线程数是最大值,但是阻塞队列使用的是一种延时队列
- 执行任务的方法
有两种
1.就是之前的普通执行方法,executor,传入 Runnable 接口
public Future<?> submit(Runnable task) {
return schedule(task, 0, NANOSECONDS);
}
2.就是submit 方法 ,带有返回值的 ,传入 callable 接口
public <T> Future<T> submit(Callable<T> task) {
return schedule(task, 0, NANOSECONDS);
}
综上,其实只列举两个简单的方法,但是这个线程池,普遍都是可以进行延时的,而且是可以有返回值的,两个任务接口都可以有返回值,这也是这个线程池的一大特点,是对原有线程池进行继承和扩展。