Java 线程池核心原理解析
Java线程池的基础用法
在深入了解Java线程池之前,我们先来回顾一下线程池的基础用法。
创建和使用
想要创建一个线程池有两种方法(其实底层都用的同一种):
- 使用excutors的静态方法创建,比如
ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
- 使用ThreadPoolExecutor的构造方法,比如
ThreadPoolExecutor singleThreadPool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
在使用线程池时用线程池的execute方法:
singleThreadPool.execute(() -> {
System.out.println("thread start");
});
为什么需要线程池
好好的线程多简单直接new Thread简单粗暴,为什么要引入线程池这玩意儿,搞起来还这么麻烦?
在高并发下这个当然是很有必要的(当然你说你基本不会有高并发,直接new Thread当然可以,还可以简化代码),在解释原因之前我们先看几个理论的东西,理论不能少啊哈哈:
线程的生命周期1
这个生命周期很难找到很底层的原理,只在一个技术群处得到一位大佬的分享:
New(新建)
:线程在编程语言层面被创建,JVM分配内存并初始化成员变量,不允许分配CPU执行Runable(就绪)
:线程对象调用start方法之后,线程处于就绪状态,jvm会为其创建方法调用栈和程序计数器,等待分配CPU执行Running(运行状态)
:处于就绪状态的线程获得了CPU,开始执行run方法里的方法体,线程处于运行状态休眠
:处于运行状态的线程失去所占用资源后,进入休眠状态。包含三种状态阻塞/有限时等待/无限时等待Terminated(线程终止)
:线程生命终止唤醒
:线程从休眠状态回到运行状态,重新获得CPU
新建
我们在Java中创建线程时,使用
new Thread();
调用Thread的构造方法,最终会执行到Thread的init方法:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// ......太长了就不贴了,主要内容与主题无关
}
在该方法中jvm为该线程对象分配内存,此时时不允许线程执行的。
就绪
创建完成线程对象后,我们就可以调用
thread.start();
来执行线程了:
最终会执行native方法start0:
private native void start0();
这个会在jvm层调用本地方法,如果想看源码的可以自选下载open-jdk查看,这里就不翻源码出来看了。
这个时候线程就会处于就绪状态,jvm会给这个线程分配线程栈和程序计数器,等待CPU进行调用。
运行
当获得CPU时间片后,线程就会进入运行状态。(什么时候获得CPU时间片这取决于操作系统的分配算法,与jvm没啥关系)
休眠
当线程因为某种原因而等待资源时(最常见的等待IO)时,线程会进入休眠状态,退出CPU分片,进入等待阻塞状态。
终止
当线程执行完所有操作或因为错误结束时,线程就会终止,退出CPU分片。
使用线程的代价
我们来梳理一下上面说的线程的生命周期,大概算一下使用线程的代价:
- 新建线程jvm分配内存,这个在jvm层面,代价微乎其微不予考虑;
- 线程进入就绪态会分配相应的线程栈并进入CPU等待队列,此时会进行CPU分片,这时候jvm需要用native方法进行系统调用,是存在一定的代价的
- 线程运行和阻塞都是CPU自身层面的基本没有代价
- 线程唤醒会重新获得CPU分片,此时存在一定代价
- 线程结束终止时会释放CPU的时间片,并交与jvm释放线程的内存,有一定代价
一般的:涉及到到jvm应用程序(用户态
)执行native系统调用(转入内核态
)时,都会存在较高的代价。
线程池帮我们做什么
既然使用线程是存在代价的,那么我们为什么还要用线程呢?
- 我们当然不用每次都用线程。假设我现在有一个任务,他需要花费T1时间来执行,我创建一个线程的代价需要T2时间,结束一个线程的代价需要T3时间。如果T2+T3 > T1。那么这种情况下是完全没有必要使用线程的。
此时线程池就应运而生了。线程池会在创建的时候初始化并为许多线程分配空间,而不需要每次使用线程的时候去创建,在使用完成后也可以回收后继续调用。创建线程步骤中代价较大是进入CPU等待队列并分配时间片,如果线程池能够降低代价的话,那么大胆的推测就来了:线程池中创建的核心线程是一直在进行自旋的!
线程池原理
多的不说,直接上源码,别忘了我们上面的大胆的推测。
创建线程池
先来看一下线程池的一些常量。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));// 1110-0000-0000-0000-0000-0000-0000-0000
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;// 0001-1111-1111-1111-1111-1111-1111-1111
private static final int RUNNING = -1 << COUNT_BITS;// 1110-0000-0000-0000-0000-0000-0000-0000 线程池正常运行
private static final int SHUTDOWN = 0 << COUNT_BITS;// 0000-0000-0000-0000-0000-0000-0000-0000 停止接受新任务
private static final int STOP = 1 << COUNT_BITS;// 0010-0000-0000-0000-0000-0000-0000-0000 中断所有任务并停止接收新任务
private static final int TIDYING = 2 << COUNT_BITS; //0100-0000-0000-0000-0000-0000-0000-0000
private static final int TERMINATED = 3 << COUNT_BITS;// 0110-0000-0000-0000-0000-0000-0000-0000
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; }
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
当我们new出线程池时,会执行线程池的构造方法:
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
进行入参效验和赋值,总的来说跟创建一个普通对象没什么区别。
- 这里的acc是获取了权限管理,来确保后面进入线程池的任务是否有权限执行。
使用线程池excute
在创建完线程池后我们就可以使用它来执行我们的线程任务。调用threadpool.execute(command)就会进入到下面的方法:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
// ****1****
if (workerCountOf(c) < corePoolSize) {
// ****2****
if (addWorker(command, true))
return;
c = ctl.get();
}
// ****3****
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// ****4****
if (! isRunning(recheck) && remove(command))
reject(command);
// ****5****
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// ****6****
else if (!addWorker(command, false))
reject(command);
}
如果你英语水平还行的话直接看jdk的注释就大概能懂什么意思了。这里我简单说说上面加标签的地方的含义
-
1.判断当前正在执行的work数量,如果小于初始化时的最大核心线程数,说明当前存在空闲核心线程;
-
2.直接将command加入到核心线程中去执行,如果加入成功后面的就不用看了;
-
3.当前没有空闲的核心线程,并且当前的线程池处于活动状态,则尝试将这个command加入到等待核心线程的队列中去;
-
4.这时候进行了一步recheck的操作,用来确保当前线程池的ctl状态可以执行任务。那么,什么操作会改变ctl呢?
private boolean compareAndIncrementWorkerCount(int expect) { return ctl.compareAndSet(expect, expect + 1); } private boolean compareAndDecrementWorkerCount(int expect) { return ctl.compareAndSet(expect, expect - 1); }
ThreadPoolExecutor中只有这两个方法使用CAS方法更新ctl的值,那么我们再看何时会调用这些方法
- 在addWorker(添加任务)中调用了compareAndIncrementWorkerCount增加ctl的值;
- 在getTask中调用了compareAndDecrementWorkerCount减小ctl的值;
- 在addWorkerFailed(添加任务失败)和processWorkerExit(任务因异常退出)中间接调用了compareAndDecrementWorkerCount减小ctl的值。
使用类似双重检查的逻辑来保证ctl的状态正确(严格意义上这个是不能绝对保证的,见双重检查)
-
5.如果当前没有活跃的核心线程,则使用addWorker来添加核心线程。
-
6.将当前的command加入到即将运行的队列中,如果加入失败(线程池超出等待队列上限或是一般异常),则拒绝此次任务。
调用的addWorkder用于创建核心线程或是将任务加入到核心线程/核心线程等待队列中去。
我们的线程池有三个位置来存放command:
- 当前正在执行的command由核心线程执行。
- 等待队列存放等待队列中的command。
- 剩余任务存放在等待入队的集合中。
图片转自博客园
线程池模型
这里就直接转载知乎的一片文章了。
线程池模型-转自知乎
最后再说一句,看源码不要太纠结,不需要每一行都看懂,能够明白思路就可以了。
(来自某群大佬的分享,不是原创) ↩︎