知识梳理系列之一——多线程
本文简明的梳理下多线程基本知识
知识梳理系列之一
几个问题
1. 为什么要有多线程
总结:
在做复杂计算时(比如在CPU中做复杂的算术或者逻辑计算、GPU中栅格化图像数据等)或者远程(网络/磁盘文件流等)数据获取时,一般需要耗费大量时间,如果由主线程执行这些操作将会造成主线程阻塞,这种情况是不应出现的。因此,需要创建工作线程并在其中执行。
2. 有哪些方式实现在工作线程中执行操作
常用的包括创建Thread/异步任务AsyncTask/HandlerThread和IntentService/线程池等几类
2.1 Thread/Runnable/FutrueTask 知识及其优缺点
Thread 最普通的使用方式:
// 1. Thread构造时,会调用Thread init()方法初始化, 对线程名、Group、TID、Target(Runnable)、线程优先级、
// deamon(布尔值是否是守护线程)、stackSize(线程栈内存大小)等进行赋值,Thread本身实现了Runnable接口,run()方法执行的就是target.run()
Thread t = new Thread(new Runnable() {
//匿名内部类实现Runnable,也可以自定义集成或声明为局部或成员变量
@Override
public void run() {
// do something...
}
});
// 2. 必须调用start(),才是真正启动了一个线程,
// 因为start()同步方法中调用了native方法nativeCreate(java.lang.Thread, long, boolean) 才真正创建了线程
t.start();
在可执行任务接口(Runnable)中实现要执行的业务,构造Java Thread类的实例,通过Thread.start()方法来启动这个线程并最终执行Runnable.run()方法。
- Runnable与Callable接口:Runnable接口的run方法是void的,没有返回值,当我们需要用线程执行一个任务并且得到返回值时,就可以使用Callable接口,Callable接口支持一个泛型在调用call方法后返回这个泛型的结果,但是Thread是不可以直接使用的,target是Runnable型。于是产生了一个FutureTask的类型:
Callable在FutureTask构造时初始化,run()方法中,触发callable.call()方法,得到result调用set(V)方法保存,并触发finishCompletion()、done()(一个空实现方法)方法,可重写done()方法,通过get() (调用report(int))获取结果;
即: run() --> callable.call() --> set(result) --> finishCompletion() --> done() --> 重写done() 中调用get() --> report() 获取任务结果。
FutureTask是AsnycTask的实现基础
在使用Thread创建单个线程执行耗时任务的场景下,线程个数有限的前提下是可以的选择,当线程个数过多,比如高并发的环境下,创建大量的Thread将会:
1. 每个线程要分配栈内存,消耗大量资源;
2. 频繁进行线程间切换,显著降低性能;
此时Thread的缺点就凸显了出来。
2.2 AsyncTask的原理
异步任务的基本认识:
- 构造时指定Params, Progress, Result三个泛型
- 在耗时操作执行之前,可以在onPreExecute()方法中预处理;
- 在doInBackgroud()方法中执行耗时操作;
- 在doInBackgroud()方法中可以调用publishProgress()可以在onProressUpdate()中更新进度;
- 任务执行完成时,在onPostExecute()方法中获取结果;
主要看下doInBackgroud()方法是怎样在线程中执行并最终得到结果的:
- 在AsyncTask类加载时,创建了三个Exector对象:一个backupExecutors(用于处理不可执行的任务)、一个ThreadPoolExecutor线程池对象、一个管理Runnable双头队列的执行器;其中线程池是一个核心线程为1,非核心线程为20的线程池;
// AsyncTask.java
// 1. 不可执行任务的处理器
private static ThreadPoolExecutor sBackupExecutor;
// 2. 真正执行耗时操作的线程池
public static final Executor THREAD_POOL_EXECUTOR;
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE/*1*/, MAXIMUM_POOL_SIZE/*20*/, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), sThreadFactory);
//...
THREAD_POOL_EXECUTOR = threadPoolExecutor;
}
// 3. 带双头队列的执行器 管理任务队列
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
- 在AsyncTask实例化时,初始化了MainHandler、Callable、FutureTask;
// AsyncTask.java
public AsyncTask(@Nullable Looper callbackLooper) {
mHandler = ...;
mWorker = new WorkerRunnable<Params, Result>() {
// 实现了call方法 在后面会被调用
public Result call() throws Exception {
//...
// 1. 设置优先级
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
// 2. 触发耗时业务方法
result = doInBackground(mParams);
// 3. 传递结果
postResult(result);
return result;
}
};
mFuture = new FutureTask<Result>(mWorker) {
@Override
protected void done() {// 确保结果被回传
postResultIfNotInvoked(get());
//...
}
};
}
- 在调用execute(Params…)方法时,触发executeOnExecutor方法,使用Runnable队列执行器执行并传入参数;
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
- 其中executeOnExecutor会检查状态、调用onPreExecute对数据预处理(如果重写了的话,否则空实现)、使用队列执行器执行队列中的任务,要执行的任务是FutureTask;(这里有个疑惑:我们也没有显示创建这个FutureTask对象,他是构造方法创建的,怎么知道我要执行什么呢?)
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
Params... params) {
//... 检查和更新状态
// 1. 数据预处理
onPreExecute();
// 2. 对Callable(的子类)赋值参数
mWorker.mParams = params;
// 3. 使用任务队列管理执行器执行 传入了AsyncTask构造方法中实例化的FutureTask对象
exec.execute(mFuture);
return this;
}
private static class SerialExecutor implements Executor {
// 管理的双头队列
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;
// 4. 上步的exec默认就是这个执行器,接下来执行此方法
public synchronized void execute(final Runnable r) {
// 5. 向队列中递交任务,被递交的匿名内部类Runnable不会立即执行而是等其他执行器从此队列取出然后执行内部类的run,进而执行FutureTask的run
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();// 此处就是执行FutureTask.run的
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
// 6. 取出任务使用线程池执行
scheduleNext();
}
}
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
// 此时执行的就是FutureTask的run 进而执行Callable的call 进而执行doInBackground
// mActive就是从mTasks中取出的Runnable
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
- 队列执行器向队列中递交(offer)任务,在线程池中取出(poll)任务,此时执行FutureTask的run方法,;这时候就进入了2.1中补充内容的流程:
// FutureTask.java
// run() --> callable.call() --> set(result) --> finishCompletion() --> done() --> 重写done() 中调用get() --> report()
public void run() {
// ...
try {
// 1. 调用Callable.call()
result = c.call();
ran = true;
} catch (Throwable ex) {
// ...
}
if (ran)
// 2. 执行set
set(result);
// ...
}
protected void set(V v) {
//...
outcome = v;// 1. 对Result outcome赋值
finishCompletion();// 2. 触发finishCompletion
}
private void finishCompletion() {
//...
done(); //调用done 最终执行FutueTask的重写的done
}
- 构造方法中初始化的Callable就被触发了,进入其重写的call方法,设置了线程优先级和调用doInBackgroud();至此工作线程开始执行自定的耗时业务!并在得到结果时触发postResult–>postExecute;
2.3 HandlerThread和IntentService
2.3.1 HandlerThread
对于主线程,由于创建时由Zygote孵化出来,在Framework中就执行了prepareMainLooper()、loop(),因此主线程在启动时就已经有Looper了,而工作线程如果想使用Handler,需要手动的prepare、loop、quit。于是出现了HandlerThread,由官方封装的无需手动操作的带Looper的工作线程。
public class HandlerThread extends Thread {
// 1. 构造方法中执行了父类Thread的构造方法和优先级的初始化
// 2. 在调用start后出发run执行了下面的操作
@Override
public void run() {
Looper.prepare();// 2.1 轮询器准备
synchronized (this) {//2.2 同步获取轮询器引用 并释放对象锁
mLooper = Looper.myLooper();
notifyAll();
}
onLooperPrepared();// 2.3 准备完有一个方法可以按需重写做业务操作
Looper.loop();// 2.4 开始轮询
}
}
HandlerThread完成了Looper的准备、轮询,这个时候只需要创建一个绑定这个Looper的Handler,然后向Handler发送消息,即可在handleMessage方法中执行耗时操作。这一点不同于Thread在Runnable的run方法,线程池在Executor.execute方法中执行耗时操作,在工作线程Handler执行完的结果也可以确切的在用消息机制传递到主线程Looper的Handler更新UI;
2.3.2 IntentService
通过下面的源码步骤可以明显看出IntentService就是在Service中使用HandlerThread
其优点是在需要使用Service来在后台执行耗时任务时,无需创建和管理工作线程,而可以直接在onHandleIntent方法中执行耗时操作,并且执行完成后,会自动帮我们stopSelf。
在多次start相同的IntentService时,onStartCommand会依次向ServiceHandler中发消息,并在完成任务后自动停止,注意不要重写onBind 不要使用bind方式启动IntentService。
// IntentService.java
@Override
public void onCreate() {
super.onCreate();
// 1. 创建并启动了HandlerThread
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();
// 2. 获取HandlerThread的Looper并绑定Handler
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}
private final class ServiceHandler extends Handler {
//...
@Override
public void handleMessage(Message msg) {
// 3. 调用此方法执行消息处理
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}
2.4 Executors线程池原理
常用的线程池比如:
- SingleThreadPool(维护一个单线程的线程池);
- FixedThreadPool(维护一个指定核心线程数个数的线程池,并且所有线程都是核心线程);
- CacheThreadPool(维护一个最大为Integer.MAX_VALUE的非核心线程池,所有线程都是非核心线程);
- ScheduleThreadPool(可以调度在合适时间执行任务的线程池)
看一下他们的实现方式就发现1~3都是由ThreadPoolExecutor来实现,4特殊一点是ScheduleThreadPoolExecutor
下面看下UML图
ThreadPoolExecutor的重要成员变量和execute逻辑:
- 表示线程池状态和线程池中线程个数的原子Int值——ctl
// 初始值是0b1110 0000 0000 0000 0000 0000 0000 0000 表示线程池RUNNING状态 池中0个线程
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
- 再看下ctlOf方法和几个状态:
其中COUNT_BITS=29(十进制),各个状态值都是左移29位表示舍弃低29位,意思就是高3位的是状态值数据:各个状态高三位的数据是:
ctlOf就是一个逻辑与操作;
Run Status | 0b Value (hight 3 bit) |
---|---|
RUNNING | 111 (二进制,负数) |
SHUTDOWN | 000 |
STOP | 001 |
TIDYING | 010 |
TERMI | 011 |
// 0b1110 0000 0000 0000 0000 0000 0000 0000
private static final int RUNNING = -1 << COUNT_BITS;
// 0b0000 0000 0000 0000 0000 0000 0000 0000 在线程池调用shutdown时设为此状态 与STOP的区别是停止接受任务,但已经入队的任务会被执行完,再完全终止
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 0b0010 0000 0000 0000 0000 0000 0000 0000 在线程池调用shutdownNow时设为此状态 停止接受任务,并尝试中断正在执行的任务,返回被中断的任务列表,再完全终止
private static final int STOP = 1 << COUNT_BITS;
// 0b0100 0000 0000 0000 0000 0000 0000 0000 完全终止前的一个中间状态
private static final int TIDYING = 2 << COUNT_BITS;
// 0b1100 0000 0000 0000 0000 0000 0000 0000 线程池完全终止
private static final int TERMINATED = 3 << COUNT_BITS;
- execute的逻辑:
主要看下逻辑主线,细节省略研究。
public void execute(Runnable command) {
...
//1. 根据ctl低29位判断线程个数是否少于核心线程数,是则调用addWorker(command, true)
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 接addWorker方法第5步,返回false的情况:
// a. 线程池被关闭释放(除了SHUTDOWN时任务未执行完时提交空任务);
// b. 线程个数超过核心线程个数。
//
// 3. 当线程池是RUNNING状态,向阻塞式队列添加任务,队列未满添加成功返回true,进入代码块;否则跳过
// Fixed/Single线程池传入的是ListedBlockingQueue(由链表实现的阻塞式队列,FIFO,队列的容量是Integer.MAX_VALUE)
// offer是非阻塞方法,直接向队列中添加任务,如果队列满了返回false
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 重新检查运行状态,当线程池不是RUNNING,则移除任务,移除成功进入代码块(拒绝执行任逻辑)
if (! isRunning(recheck) && remove(command))
...
else if (workerCountOf(recheck) == 0)
// 4. 线程池仍然是RUNNING状态或者非RUNNING没有移除成功,线程池线程个数为0,添加非核心线程,
// 在Cache线程池(全部非核心),线程执行完任务全部超时退出后会执行这一步
addWorker(null, false);
}
// 5. 添加非核心线程执行任务,此时要求保证线程个数不能超过最大线程数,否则进入代码块(拒绝执行任逻辑)
else if (!addWorker(command, false))
...
}
小结:
在execute方法中主要是通过检查线程池中的线程个数来决定是a.新增线程执行、b.放入任务队列、c.拒绝执行 的方式。
如果 线程个数 < 核心线程数,直接创建启动新线程执行任务;
如果 线程个数 > 核心线程数,根据是RUNNING状态尝试向任务队列添加任务;
如果非RUNNING,或者RUNNING但任务入队不成功(任务队列满了),那么判断线程个数 < 最大线程个数,尝试启动非核心线程执行任务,超过最大线程个数或者线程池在退出释放就会拒绝执行。
下面是更具体的源码分析:
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
...
for (;;) {
// 2. 根据ctl低29位判断线程个数是否大于核心线程数(此时core=true,未超过压力值(2^29-1)
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 3. 线程个数未发生变化,增加一个线程个数退出循环;否则重新获取ctl并继续循环
// * 在线程退出释放或启动失败时会减小这个值
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get();
...
}
}
...
// 4. 创建Worker实例(封装了用工厂创建线程),将其添加到集合用于管理,添加成功标记为true时启动线程
w = new Worker(firstTask);
final Thread t = w.thread;
...
workers.add(w);
...
workerAdded = true;
...
if (workerAdded) {
// 5. 启动线程...
t.start();
workerStarted = true;
}
...
if (! workerStarted) // 添加失败回调
addWorkerFailed(w);
return workerStarted;
}
在Worker实例中的线程启动后,调用run() --> runWorker() -->提交的任务的run()
final void runWorker(Worker w) {
...
while (task != null || (task = getTask()) != null) {
...
// 5. task就是execute中提交的任务,在线程池中有两个来源一个是Worker实例创建是初始化的,一个是阻塞队里中的,只有任务都为空才会退出循环、退出线程;
task.run();
...
}
...
processWorkerExit(w, completedAbruptly);
}
private Runnable getTask() {
...
for (;;) {
...
// allow这个是false 先不管
// 如果线程个数大于核心线程数,阻塞指定的保活时间后返回,有可能阻塞或得到了队列中的任务也有可能是null;
// 如果小于或等于核心线程数,一直阻塞直到队列中有任务!
// 这里保证了不关闭线程池,核心线程一直存活,而非核心线程超时退出释放!!!
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
...
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
...
}
}
总结:
在线程池中核心线程、非核心线程实质上并没有用于确定身份的标记;
只要小于核心线程数,创建出来就是核心线程,大于就是非核心,区别在于核心执行完任务会在阻塞队列中等任务不会被退出释放,而非核心执行完任务并且队列中在超时前没有任务就会退出;
换言之,线程池总是维护指定个数的核心线程长期执行任务,非核心只在任务爆满时临时创建并执行,之后非核心会超时退出释放。
3.并发变成Netty中线程池、事件循环(附)
专题系列再总结