知识梳理系列之一——多线程

知识梳理系列之一——多线程

本文简明的梳理下多线程基本知识

几个问题

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的类型:
    FutureTask UML图
    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的原理

异步任务的基本认识:

  1. 构造时指定Params, Progress, Result三个泛型
  2. 在耗时操作执行之前,可以在onPreExecute()方法中预处理;
  3. 在doInBackgroud()方法中执行耗时操作;
  4. 在doInBackgroud()方法中可以调用publishProgress()可以在onProressUpdate()中更新进度;
  5. 任务执行完成时,在onPostExecute()方法中获取结果;

主要看下doInBackgroud()方法是怎样在线程中执行并最终得到结果的:

  1. 在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();
  1. 在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());
	        //...
	    }
	};
}
  1. 在调用execute(Params…)方法时,触发executeOnExecutor方法,使用Runnable队列执行器执行并传入参数;
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
	return executeOnExecutor(sDefaultExecutor, params);
}
  1. 其中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);
		}
	}
}
  1. 队列执行器向队列中递交(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
}
  1. 构造方法中初始化的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线程池原理

常用的线程池比如:

  1. SingleThreadPool(维护一个单线程的线程池);
  2. FixedThreadPool(维护一个指定核心线程数个数的线程池,并且所有线程都是核心线程);
  3. CacheThreadPool(维护一个最大为Integer.MAX_VALUE的非核心线程池,所有线程都是非核心线程);
  4. ScheduleThreadPool(可以调度在合适时间执行任务的线程池)

看一下他们的实现方式就发现1~3都是由ThreadPoolExecutor来实现,4特殊一点是ScheduleThreadPoolExecutor

下面看下UML图
在这里插入图片描述

ThreadPoolExecutor的重要成员变量和execute逻辑:

  1. 表示线程池状态和线程池中线程个数的原子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 Status0b Value (hight 3 bit)
RUNNING111 (二进制,负数)
SHUTDOWN000
STOP001
TIDYING010
TERMI011
// 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;
  1. 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中线程池、事件循环(附)

专题系列再总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值