线程池详解---线程复用、线程销毁

1.定义

  • 本文是基于jdk1.6对线程池(ThreadPoolExecutor)进行线程池执行Runnable任务主过程的剖析,jdk1.8及更高版本基本原理与1.6类似,但1.6主流程简单,没有进行太多优化,易于学习。

  • 在这里我们需要知道一些基本的常识,这是我们进行后续剖析源码的基础。

1.1.多态

public class People {

	public void say() {
		System.out.println("People........");
	}
	
	public static void main(String[] args) {
		People p = new Student();
		p.say();
	}
}

class Student extends People {
	@Override
	public void say() {
		System.out.println("Student........");
	}
}

// 输出结果
// Student........
// 子类继承父类,实现父类的方法,  People p = new Student();此处虽然是People指针p,但是实际创建的对象
// 是Student。p.say();其实程序走的是Student的say方法

1.2.实现线程任务的方法

// 1.实现Runnable接口
Runnable r = new MyRunnable();
Thread t = new Thread(r);
t.start();
// 这里是新创建了一个线程,然后用新创建的线程执行MyRunnable中的run方法,这是java底层自己实现的
// 这里还有一个执行MyRunnable任务的方法,那就是直接r.run(),但是这么执行的话不是新创建的线程执行的
// runnable中的run(),而是系统当前线程执行的run(),这个点在ThreadPoolExecutor执行runnable的时候多
// 次使用,需要重点注意


// 2.实现callable接口
Callable caller = new MyCallable();
ThreadPoolExecutor executor = new ThreadPoolExecutor();
executor.submit(caller);
// 实现Callable需要实现一个call()方法,然而执行Callable这个任务的时候,需要当前线程手动去调Callable的
// call()方法,线程池多次使用使用这种方式,去实现线程执行

1.3.阻塞队列

// 当然这里BlockingQueue有7个实现类
BlockingQueue queue = new BlockingQueue();

//这里,如果queue中没有数据,当前线程会一直阻塞在这里,但是当前线程可以被别的线程interrupt(),之后会抛出
// InterruptedException
queue.take();		

// 这里,如果queue中没有数据,当前线程会阻塞在这里10s后返回null,如果这在10s中有别的线程往queue放东西
// 了,那么queue就会解除阻塞,返回queue中的数据.
queue.poll(10, TimeUnit.SECONDS);	

2.线程池提交任务的方式

2.1.execute方式

​ 该方式执行完runnable是没有返回值的,也不能尝试取消线程执行的任务(对于当前正在执行的线程并不能真正的实现线程停止)。

2.2.submit方式

​ 该线程执行完runnable是有返回值的,也能尝试取消线程执行的任务,其实只能停止那些正在阻塞获取任务的线程,而且应保证如果有while(true)的话try{}catch(InterruptedException e){}在外面.

public class TestRunnable implements Runnable{
	
	BlockingQueue<String> queue;

	@Override
	public void run() {
        // 这样使用执行该runnable的线程t打断的时候才能终止该任务的执行
		try {
			while(true) {
				queue.take();
                // 业务代码
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

​ 大家看下ThreadPoolExecutor和AbstractExecutorService的关系,ThreadPoolExecutor是AbstractExecutorService的子类,我们通常创建线程池是new ThreadPoolExecutor()。ThreadPoolExecutor.submit()的时候是调用的父类的AbstractExecutorService.submit()方法,执行execute的时候是调用的自己的方法
在这里插入图片描述

// 这三个方法不管是传进来的是Runnble还是Callable,线程池都要把任务转换成RunnableFuture,其实是其实现
// 类FutureTask,后续线程任务取消的时候就是通过这个方法来取消的.

// 线程池任务执行分两部分
// 1.线程池执行任务
// 2.任务监测(FutureTask),线程任务取消的详细分解见我另外的博客
public Future<?> submit(Runnable task) {
	if (task == null)
		throw new NullPointerException();
	RunnableFuture<Object> ftask = newTaskFor(task, null);
	execute(ftask);
	return ftask;
}

public <T> Future<T> submit(Runnable task, T result) {
	if (task == null)
		throw new NullPointerException();
	RunnableFuture<T> ftask = newTaskFor(task, result);
	execute(ftask);
	return ftask;
}

public <T> Future<T> submit(Callable<T> task) {
	if (task == null)
		throw new NullPointerException();
	RunnableFuture<T> ftask = newTaskFor(task);
	execute(ftask);
	return ftask;
}

3.线程池执行线程任务

​ 线程池执行线程任务的流程如下:

  • 线程池的当前线程数小于核心线程数的时候,线程池创建线程并执行线程池提交的任务
  • 线程池的当前线程数等于核心线程数的时候,线程池会将线程任务提交到线程池中的任务队列里,核心线程执行完当前的任务时,会从任务队列继续取任务去执行
  • 线程池的当前线程数等于核心线程数的时候,并且核心线程都在执行线程任务的时候,而且线程池中的任务队列满了的时候,这时会创建新的线程(小于线程池最大线程数),执行刚刚被线程池提交的任务,执行完该任务之后,才会获取积压在任务队列的中的任务
  • 线程池的当前线程数等于最大线程数,而且线程池的任务队列已经满了,这时需要给一个拒绝策略,一般这个策略需要自定义,并记录下那些任务被拒绝了。

3.1.线程池基本参数

​ 现在我们看下线程池的核心参数

public ThreadPoolExecutor(
    // 核心线程数
	int corePoolSize,
    // 最大线程数
    int maximumPoolSize,
    // 非核心线程数允许保留的时间
	long keepAliveTime, 
    // 和keepAliveTime配对,决定单位是秒、分钟等
	TimeUnit unit,
    // 任务队列
	BlockingQueue<Runnable> workQueue, 
    // 线程工程,创建新线程用的,不必过于深究
	ThreadFactory threadFactory, 
    // 我们自定义的拒绝策略,或者ThreadPoolExecutor提供的默认拒绝策略,默认策略自己下去看下源码
	RejectedExecutionHandler handler) {
						  

}

3.2.线程执行详解

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    // 这里有两个条件判断
    // 1.如果当前线程池的线程数量 >= 核心线程数量,当线程池线程数量为空的时候,很明显这个表达式为false,
    // 会进入2号表达式;如果当线程数量 >= 核心线程数量为true时,明显不会再走2号表达式。这是断路或表达式语义。
    // 2.当1号表达式为false时,会进入到addIfUnderCorePoolSize(command)。
    //    1)当线程数量 < 核心线程数量,这个方法会创建一个线程并执行cammand(Runnable),并返回true;如
    //果没有后续任务提交进来,那么该线程会阻塞在阻塞队列的poll()方法上,之后在永久阻塞在take()方法上。
    //    2)当线程数量 = 核心线程数量,这个方法会返回false,然后在下一个if里面该任务会被
    // workQueue.offer(command),放到工作队列中。或许有人会问:1号表达式不是已经有判断poolSize >= 
    // corePoolSize,当poolSize = corePoolSize时,不是不会走2号表达式吗?好的,带着悬念我们来看下
    // addIfUnderCorePoolSize(command)做了些什么。
    if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
        //如果状态为RUNNING,而且当前的runnable还能放进workQueue里
        if (runState == RUNNING && workQueue.offer(command)) {
            // 如果submit或者execute任务的时候,线程池被shutdown了,需要把刚才提交的任务从工作队列中
            // 清除出去。
            if (runState != RUNNING || poolSize == 0)
                ensureQueuedTaskHandled(command);
            //如果当前线程数(poolSize)  > 最大线程数(maximumPoolSize)  或者状态不是RUNNING
        } else if (!addIfUnderMaximumPoolSize(command))
            //根据策略选择怎么拒绝该RUNNABLE command
            reject(command); // is shutdown or saturated
    }
}

3.2.1.addIfUnderCorePoolSize解析

private boolean addIfUnderCorePoolSize(Runnable firstTask) {
    Thread t = null;
    final ReentrantLock mainLock = this.mainLock;
    // 这里获取锁,一个ThreadPoolExecutor只有这一个锁,多个线程同时走这一个方法的时候只能顺序进入,等
    // 第一个线程unlock()了,第二个线程才能进来
    mainLock.lock(); 
    try {
        //如果当前线程的数量 < 核心线程数量。之前的悬念在这,理论上说当poolSize = corePoolSize,应该
        // 不会走到这里来啊?这里还要过滤下poolSize = corePoolSize的情况,并返回false呢?细心的同学
        // 应该发现了,没错那就是锁。在execute方法中判断poolSize >= corePoolSize的时候,有可能第一
        // 个线程还没有将自己新创建出来的线程累加到线程池的当前线程数上(poolSize),这时execute方法中判
        // 断poolSize有可能会比实际的少。那么问题来了,为什么这里能判断呢?因为这里有锁的存在,第一个线
        // 程当mainLock.unlock();未执行的时候,第二个线程只能在mainLock.lock(); 位置等待。而且本方
        // 法结束了,会将poolSize+1的,请继续往下看
        if (poolSize < corePoolSize && runState == RUNNING)
            // 在这里,将firstTask封装到了worker中,其实worker是一个实现了runnable接口的类,然后把
            // worker放到一个HashSet<Worker>中去持有,这个HashSet<Worker>是ThreadPoolExecutor
            // 一个成员变量。装来装去作用有两个
            // 1)线程防止被GC回收
            // 2)超过核心线程数的线程可以从HashSet<Worker>移除,以实现线程的销毁
            // 接下来看看addThread(firstTask)做了些什么
            t = addThread(firstTask);
    } finally {
        mainLock.unlock();
    }
    if (t == null)
        return false;
    
    // 这里就是新线程启动,并执行worker的run方法
    t.start();
    return true;
}
3.2.1.1.addThread解析
private Thread addThread(Runnable firstTask) {
    // 如果是通过threadPoolExecutor.execute执行的我们runnable,那么是将封装到runnable了Worker
    // 内,如果是threadPoolExecutor.submit执行的我们runnable,那么是将我们FutureTask封装到了Worker内。
	Worker w = new Worker(firstTask);
    // 这里是根据Worker创建线程,其实Worker也是Runnable
    // 这里可以理解为Thread t = new Thread(new MyRunnable())
    // 可想而知,当t.start()的时候,会不会去启动一个新的线程去MyRunnable的run方法吗?这里也类似,请看
    // Worker的数据结构,见3.2.1.2:
	Thread t = threadFactory.newThread(w);
	if (t != null) {
		w.thread = t;
		workers.add(w);
		int nt = ++poolSize;
		if (nt > largestPoolSize)
			largestPoolSize = nt;
	}
	return t;
}
3.2.1.2.Worker解析

worker详解示意图
worker详解示意图

// 这是worker代码
// 3.2.1的t.start()就是创建了个新线程然后走的这里
public void run() {
   try {
   	Runnable task = firstTask;
   	firstTask = null;
       // 重点来了,线程复用,从工作队列中重复获取runnable任务并执行,就在这个方法上
   	// 1、刚创建worker时,firstTask != null,从而task != null
   	// 2、这里相当于一个while(true)循环,前提是getTask()返回的不是null
   	//   当什么时候getTask()返回的是null呢?我们到3.2.1.3看下
   	while (task != null || (task = getTask()) != null) {
           // 这里就是执行的我们定义的runnable了
   		runTask(task);
   		task = null;
   	}
   } finally {
   	// workerDone意味着要从workers中将该worker销毁了,即线程池的线程数量要减少了,详见3.1.2.4
   	workerDone(this);
   }
}

private void runTask(Runnable task) {
   final ReentrantLock runLock = this.runLock;
   runLock.lock();
   try {
   	if (runState < STOP && Thread.interrupted() && runState >= STOP)
   		thread.interrupt();

   	boolean ran = false;
       // 啥也没做,这是protected的方法,可以自己实现重写这个方法,对线程池增强
   	beforeExecute(thread, task);
   	try {
           // 关键点来了,在这里。
           // 1、当线程池执行的是execute()方法时:还记得1.2中实现线程任务的两种方法吗?这里是就是用的
           //     实现runnable,而且是用的使用当前线程手动执行的r.run(),即Runnable r = new 
           //     MyRunnable(); r.run();这样当前线程就会执行我们定规的runnable了,而且当前线程会
           //     阻塞在这里;而当前线程就是我们3.2.1.1中创建出来的新线程,看懂了吗?
           // 2、当线程池执行的是submit()方法时:就涉及到FutureTask等任务取消相关内容,在另外章节梳
           // 理
   		task.run();
   		ran = true;
           // 啥也没做,这是protected的方法,可以自己实现重写这个方法,对线程池增强	
   		afterExecute(task, null);
   		++completedTasks;
   	} catch (RuntimeException ex) {
   		if (!ran)
   			afterExecute(task, ex);
   		throw ex;
   	}
   } finally {
   	runLock.unlock();
   }
}
3.1.2.3.getTask解析(线程复用)
// 根据worker的run方法可知,如果这个方法返回null,那么该线程就要从workers集合中移除,被销毁了
Runnable getTask() {
	for (;;) {
		try {
			int state = runState;
			if (state > SHUTDOWN)
				return null;
			Runnable r;
			// 这里获取任务的3个分支分很关键
			// 1、当该线程池被执行shutdown的时候,会把工作队列中的没执行完的任务拿出一个并执行
			// 	  如果执行完了,这里r = null,那么到了runTask()方法里面,会将该worker线程销毁
			if (state == SHUTDOWN) // Help drain queue
				r = workQueue.poll();
			// 2、如果线程池当前线程数 > 我们设置的核心线程数或者设置了允许核心线程超时退出(一般没人设
            //     置,初始值为false)
			// 还记得1.3.中阻塞队列的特性吗?这里会从工作队列获取务,workQueue.poll(keepAliveTime, 
            // TimeUnit.NANOSECONDS);这个方法效果是,如果workQueue中没有东西,即size = 0,当前线程
            // 会阻塞keepAliveTime秒,并在之后返回null;如果工作队列中有东西,那么不需要阻塞,直接返回一
            // 个任务可想而知,如果在阻塞的过程中,有其他线程往工作队列放东西,那么当前线程就会获取到其他线
            // 程放的任务,之后并执行. 如果阻塞keepAliveTime时间后工作队列中还没有任务,那么r = null,
            // 即当前线程逃脱不了被销毁的命运。这也就是超过核心线程数的线程,为什么会在keepAliveTime时
            // 间之后会被销毁,这里没用什么定时器,就是用了个阻塞队列的定时阻塞机制.
			else if (poolSize > corePoolSize || allowCoreThreadTimeOut)
				r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);
			// 3、当poolSize <= corePoolSize,即当前线程池线程数量不大于我们设置的核心线程数而且没有
            //   设置允许核心线程退出的,即allowCoreThreadTimeOut初始值为false;说明该线程池中均为核
            //   心线程如果工作队列中任务数为0的时候,那么会阻塞在这里,如果workQueue一直没东西,那么该
            //   线程会一直阻塞在这里,如果workQueue任务书不为0,那么就会获取到Runnable r,这里也解释
            //   了,为什么核心线程不会被销毁的机制,因为核心线程会一直阻塞,直到workQueue有任务为止,这
            //   里的r != null,会继续走run()方法中的 while (task = getTask() != null),一个类
            //   似于while(true)的机制
			else
				r = workQueue.take();
			if (r != null)
				return r;
			
            // 线程池的退出机制就不在这里讲了
            // 这里如果workqueue中任务数为0,会返回true
            // 如果线程池被执行了shutdown或者shutdownnow,这里也会为true
            // 这里线程池如果被shutdown了,会打断被上述第2、3分支阻塞的线程,然后会走第1个分支,并返回
            //     null,然后所有线程都会被从workers中remove掉,销毁线程池中所有线程
			if (workerCanExit()) {
				// 为啥需要interrupt所有线程呢?因为都在分支3上的take()阻塞着呢,就算从Workers中
                // remove掉了Worker,线程还是不会销毁的,所以需要interrupt所有线程,然后在remove才
                // 能销毁线程
				if (runState >= SHUTDOWN) // Wake up others
					interruptIdleWorkers();
				// 这里返回null的话该worker会被从workers中清除
				return null;
			}
			// Else retry
		} catch (InterruptedException ie) {
			// On interruption, re-check runState
		}
	}
}
3.1.2.4.workerDone解析(线程销毁)
void workerDone(Worker w) {
	final ReentrantLock mainLock = this.mainLock;
	mainLock.lock();
	try {
		// 销毁该线程之前将该线程执行完成的任务数添加到总线程池完成的总任务数当中
		completedTaskCount += w.completedTasks;
		// 大家还记得吧worker里面有一个thread变量就是用来存放线程的
		// 将worker从workers中清除,即根据GC的可达性分析法就再也获取不到该worker了
		// 命运只有一个,被垃圾回收器回收
		workers.remove(w);
		// 相应的线程池的数量要-1
		// 这里为什么poolSize为什么可能为0呢?
		// 在没有设置allowCoreThreadTimeOut参数的时候,只有一种可能,那就是被shutdown了
		// 如果设置了allowCoreThreadTimeOut = true,那么该线程池随时有可能进入TERMINATED状态
		if (--poolSize == 0)
			tryTerminate();
	} finally {
		mainLock.unlock();
	}
}

3.2.2.addIfUnderMaximumPoolSize解析

private boolean addIfUnderMaximumPoolSize(Runnable firstTask) {
    Thread t = null;
    final ReentrantLock mainLock = this.mainLock;
    // 这里的锁作用跟3.2.1的锁作用一样不再赘述。
    mainLock.lock();
    try {
        // 这里也一样,当线程池当前线程小于最大线程的时候,执行firstRunnable,执行完毕之后在从工作队列
        // 中获取新的任务
        if (poolSize < maximumPoolSize && runState == RUNNING)
            t = addThread(firstTask);
    } finally {
        mainLock.unlock();
    }
    if (t == null)
        return false;
    t.start();
    return true;
}
  • 9
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值