阅读前请确保对于Thread和Runnable熟悉,可以参考多线程,Thread和Runnable
本博客从源码层面讲起,到最后常用的四个线程池。
Executor
这是线程池实现的最底层的接口,只包含一个方法:
public interface Executor {
/**
* Executes the given command at some time in the future. The command
* may execute in a new thread, in a pooled thread, or in the calling
* thread, at the discretion of the {@code Executor} implementation.
*/
void execute(Runnable command);
}
可以看出,Executor是执行一个线程的(Runnable)的一个单元,即将Runnable放在Executor中执行。这意味着不需要在显式的声明一个Thread来执行一个线程了。
既然如此,那么重要的就在于如何实现execute方法了。
ExecutorService
接口,实现了Executor接口,另外提供了“停下”(shut down)的操作,并停止接受新的任务。
public interface ExecutorService extends Executor {
/**
* 官方解释是,停止接受新任务,已提交的任务则按序执行完毕,原话:
* allow previously submitted tasks to execute before terminating, but no new tasks will be accepted
*/
void shutdown();
/**
* 即在shutdown上做的跟进一步,已提交的任务停止执行,返回等待的任务
* 并尝试停下当前在执行的任务,原话是:
* prevents waiting tasks from starting and attempts to stop currently executing tasks
* 注意"attempt",该方法并不能保证能停下正在执行的任务
*/
List<Runnable> shutdownNow();
//提交一个任务,并返回该任务的Future类
Future<?> submit(Runnable task);
//执行给定的任务,所有执行完毕后,返回包含这些任务状态和结果的列表
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks);
//执行给定的任务,任何一个执行完毕,则返回该执行完的任务的状态和结果,并取消的没执行完的任务
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
AbstractExecutorService
抽象类,主要是提供了submit,invokeAny和invokeAll的具体实现。
public abstract class AbstractExecutorService implements ExecutorService {
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
submit方法,意为“提交”,和execute(执行)方法的区别在于,submit内部先将提交的任务包装成一个RunnableFuture,然后调用execute方法,并返回任务的结果。即submit可以取得任务的执行结果。
返回结果是通过RunnableFuture类实现的。即newTaskFor方法会将提交的任务包装成一个RunnableFuture并返回。
剩下的方法比如invokeAll和invokeAny等的实现都需要额外的类,具体实现就不贴了,没有什么很厉害的算法实现,要说的就是invokeAny的实现是借助ExecutorCompletionService来实现的。该类用于保证一个任务被执行完。具体等下个博客在分析吧。
ThreadPoolExecutor
最后来看看线程池ThreadPoolExecutor的实现,该类有2000多行,还是要花点耐心阅读的。
OK,一开始看看官方文档上对于ThreadPoolExecutor的具体介绍吧:
Thread pools address two different problems: they usually provide improved performance when executing large numbers of asynchronous tasks, due to reduced per-task invocation overhead, and they provide a means of bounding and managing the resources, including threads, consumed when executing a collection of tasks. Each ThreadPoolExecutor also maintains some basic statistics, such as the number of completed tasks.
翻译过来就是:
线程池主要解决了两个问题,1.当需要处理大量异步任务是,提供了更好的性能,原因是(线程池)降低了每个任务调用的消耗(相比使用Thread)2.提供了管理和绑定资源的手段;另外,线程池维护了一些基本的统计信息。
官方文档建议使用更容易的工厂方法,比如newCachedThreadPool,newFixedThreadPool,newSingleThreadExecutor,等来代替直接初始化,也就是我们常用到的四种线程池,这将在后文介绍。
线程池的状态和数量
介绍完该类的作用,直接看源码吧,首先说明线程池的5个状态:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED。
具体含义如下:
1. RUNNING:表示可以接受新任务,并处理队列的任务
2. SHUTDOWN:不允许接受新任务,但可以处理队列的任务
3. STOP:不允许接受新任务和处理队列的任务,并打断正在执行的任务
4. TIDYING:所有的任务执行完毕,workerCount为0,转变到TIDYING的线程会执行terminated()方法
5. TERMINATED:terminated()方法执行完毕
使用runState变量来表示。
以上5个状态时可以相互转换的,转换规则如下:
RUNNING -> SHUTDOWN:调用了shutdown()方法,而该方法一般是在finalize()里被调用的
(RUNNING or SHUTDOWN) -> STOP:调用了shutdownNow()方法
SHUTDOWN -> TIDYING:当任务队列为空并且线程池为空,则会发生这种状态的转变
TIDYING -> TERMINATED:terminated()方法执行完毕
需要说明的是,上述的状态转换是不能手动调的,当调用shutdown等方法或者出现其他异常时,这种状态的转换是会自动发生的。
另一个需要首先说明的变量是workerCount,该变量是记录有效线程数。
然后看下源码是如何表示上述两个变量的:
public class ThreadPoolExecutor extends AbstractExecutorService {
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
上述代码的阅读比较困难,需要结合注释观看。
我们知道int在java中是4个字节,因为使用补码,因此,最大可赋予的正整数是2^31-1,这个数大概是20亿。也意味着如果使用int来记录有效线程数,那么最大的线程容量是20亿。
同样我们也可以使用int来表达线程池的5个状态。
但ThreadPoolExecutor的源码是并发的大佬Doug Lea写的,怎么会使用一般的方法呢><.
源码中的解释是,int可以用来表达20亿,但我们不需要这么多,从二进制上来说,我们可以使用低位的信息来保存线程的数量,高位保存线程池的状态。OK,那具体如何实现呢?
我们先看下上述变量的具体大小,Integer.SIZE=32,因此COUNT_BITS = 29,剩下的变量使用二进制表示如下:
RUNNING: 111|00000000000000000000000000000
SHUTDOWN: 000|00000000000000000000000000000
STOP: 001|00000000000000000000000000000
TIDYING: 010|00000000000000000000000000000
TERMINATED: 011|00000000000000000000000000000
CAPACITY: 000|11111111111111111111111111111
仔细观察会发现,使用了整形的低29位表示线程的最大容量CAPACITY,即2^29-1.
剩下的高三位表示线程池的状态。
当我们需要改变状态时只需要改变高三位,改变线程数时,改变低29位即可。这就实现了将两个信息放在一个变量里面,即ctl。
然后使用逻辑运算,与或非等来取得各runState或者workerCount,源码中相应的方法如下:
public class ThreadPoolExecutor extends AbstractExecutorService {
...
// Packing and unpacking ctl
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
/*
* Bit field accessors that don't require unpacking ctl.
* These depend on the bit layout and on workerCount being never negative.
*/
private static boolean runStateLessThan(int c, int s) {
return c < s;
}
private static boolean runStateAtLeast(int c, int s) {
return c >= s;
}
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
上面的函数我们成为解包或者装包,即可以理解为解析ctl或者封装ctl。
线程池和队列
接着进入ThreadPoolExecutor中最重要的两个变量,线程池和任务队列。
public class ThreadPoolExecutor extends AbstractExecutorService {
...
private final BlockingQueue<Runnable> workQueue;
private final HashSet<Worker> workers = new HashSet<Worker>();
其中,workQueue是任务队列,workers是线程池。
其实到这里,关于两者间的区别,还有很多疑惑,我们在这里详细讲解下两者的区别。
线程池中包含线程,线程是用来消费任务的,可以认为任务需要线程承接再能执行。
这里简述一种线程池可能的工作状态:
线程池为空,任务队列为空,此时开始有任务到来,没来一个任务创建一个线程用于承接任务,并执行,直到线程数到达a后,将到来的任务放在任务队列中,再次期间任务队列中的任务数增加,线程数不变,直到任务队列满了,然后再次开始创建线程用于消费新来的任务,直到线程数达到b。此时拒绝任务,即新来的任务被忽视掉。
上述描述大致明确了线程和队列的功能。
下面举个实际的例子看下:
public class Test01 {
public static void main(String[] args){
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 6,
200, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(4));
for(int i=0;i<10;i++){
MyTask myTask = new MyTask(i);
executor.execute(myTask);
System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
executor.getQueue().size());
}
executor.shutdown();
}
}
class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
public void run() {
//System.out.println("正在执行task "+taskNum);
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//执行的结果如下:
//线程池中线程数目:1,队列中等待执行的任务数目:0
//线程池中线程数目:2,队列中等待执行的任务数目:0
//线程池中线程数目:3,队列中等待执行的任务数目:0
//线程池中线程数目:3,队列中等待执行的任务数目:1
//线程池中线程数目:3,队列中等待执行的任务数目:2
//线程池中线程数目:3,队列中等待执行的任务数目:3
//线程池中线程数目:3,队列中等待执行的任务数目:4
//线程池中线程数目:4,队列中等待执行的任务数目:4
//线程池中线程数目:5,队列中等待执行的任务数目:4
//线程池中线程数目:6,队列中等待执行的任务数目:4
从结果中可以看出首先是线程增加,然后是队列中的任务数增加,队列满后,再次增加线程数。
再次验证如下:
public class Test01 {
public static void main(String[] args){
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 6,
200, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(4));
for(int i=0;i<10;i++){
MyTask myTask = new MyTask(i);
executor.execute(myTask);
// System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
// executor.getQueue().size());
}
executor.shutdown();
}
}
class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
public void run() {
System.out.println("正在执行task "+taskNum);
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//一种可能的执行结果:
//正在执行task 0
//正在执行task 8
//正在执行task 7
//正在执行task 1
//正在执行task 9
//正在执行task 2
//正在执行task 3
//正在执行task 4
//正在执行task 5
//正在执行task 6
上述只是一种可能的结果,但无论随机性多大,最后四条输出任务绝对是3,4,5,6,这是因为这四条是任务队列中的,只有等线程空闲了才能执行任务队列中的任务。
两个变量的讲述暂时到此为止
补充:锁
源码中还有一行代码:
public class ThreadPoolExecutor extends AbstractExecutorService {
...
private final ReentrantLock mainLock = new ReentrantLock();
源码解释,任何对于workers线程池的操作都需要获得mainLock 锁。
为什么不使用synchronized同步方法呢?
有两点原因:1.使用锁更灵活,而synchronized消耗较多资源2.synchronized是非公平锁,即不是先来先得(FIFO)锁,因此可能产生大量的“打断”(interrupt)操作。
因此我们使用显式的锁机制。
其他变量
public class ThreadPoolExecutor extends AbstractExecutorService {
...
private volatile ThreadFactory threadFactory;
private volatile RejectedExecutionHandler handler;
private volatile long keepAliveTime;
private volatile boolean allowCoreThreadTimeOut;
private volatile int corePoolSize;
private volatile int maximumPoolSize;
虽然是其他变量,但这些变量多是我们能初始化的变量。
threadFactory:线程工厂,用于创建线程
handler:拒绝处理方法
keepAliveTime:多出的线程的等待时间
allowCoreThreadTimeOut:是否允许核心线程也按照keepAliveTime而消亡
corePoolSize:核心线程数
maximumPoolSize:最大线程数
理解了以上参数,就不难理解线程池的工作机制了。
源码解析暂时到此为止,等有时间了在
四个线程池
四个线程池都是通过Executors里的静态方法创建的,四个方法如下:
- newCachedThreadPool()
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
需要注意的是SynchronousQueue,SynchronousQueue内部没有数据缓存空间,这意味着可以认为SynchronousQueue长度也为0.
于是newCachedThreadPool返回的线程池的功能就很明确了:
来一个新任务,如果没有空闲的线程来承载该任务,就创建线程,否则用空闲的线程去承载新任务。如果空闲的线程超过60s没有接待新任务,则被销毁,知道线程数为0.
该线程池的特点是会立刻满足每一个任务,不让任何任务等待。
- newFixedThreadPool(n)
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
注意到初始化参数,该防范创建的最大线程数和核心线程数相等,并且使用链表队列,这意味着:
一开始会创建n个线程用于承载任务,任务数超过n后,则将任务塞在链表队列中,链表队列无限增长
- newScheduledThreadPool(n)
创建一个定长线程池,并且支持定时和周期性的执行任务.
这个就不看源码了,直接举例吧:
/**
* 定时执行
*/
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(20); // 长度20
// 延迟10s执行
scheduledThreadPool.schedule(task, 10, TimeUnit.SECONDS);
/**
*周期性执行
*/
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(20); // 长度20
// 延迟10s执行,每个5s执行一次。
scheduledThreadPool.scheduleAtFixedRate(task,10, 5, TimeUnit.SECONDS);
- newSingleThreadExecutor()
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
到这里,主要的四个线程池就讲解完毕了。
至于ThreadPoolExecutor的核心实现等有时间在总结
最后按照惯例,贴一下参考到的资料:
深入理解java之线程池
java四种线程池的使用