Java并发工具线程池2

线程池

线程池就是创建一个容器存放所有的线程,实现线程的复用。

什么是线程池

在 Java 中,如果每个请求到达就创建一个新线程,创建和销毁线程花费的时间和消耗的系统 资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。

如果在一个 Jvm 里创建太多的线程,可能会使系统由于过度消耗内存或“线程上下文切换过度”而导致系 统资源不足 为了解决这个问题,就有了线程池的概念。
线程池的核心逻辑是提前创建好若干个线程放在一 个容器中。如果有任务需要处理,则将任务直接分配给线程池中的线程来执行就行,任务处 理完以后这个线程不会被销毁,而是等待后续分配任务。同时通过线程池来重复管理线程还 可以避免创建大量线程增加开销。

合理的使用线程池,可以带来一些好处

  1. 降低创建线程和销毁线程的性能开销
  2. 提高响应速度,当有新任务需要执行是不需要等待线程创建就可以立马执行
  3. 合理的设置线程池大小可以避免因为线程数超过硬件资源瓶颈带来的问题

如果实现线程的复用?

线程run方法执行结束后,线程就结束了。那么如何实现这个线程继续复用?

run(){
	while(true){
	//需要执行的业务逻辑
	}
}

除此之外,还需要用阻塞队列。

在这里插入图片描述

Java 提供的线程池 API

public class Test implements Runnable{
 @Override
public void run() { 
try {
	Thread.sleep(10);
	} catch (InterruptedException e) {
          e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()); }
static ExecutorService service=Executors.newFixedThreadPool(3); public static void main(String[] args) {
	for(int i=0;i<10;i++) { 
	service.execute(new Test());
	}
	service.shutdown(); }
}

运行结果:

pool-1-thread-2
pool-1-thread-1
pool-1-thread-3
pool-1-thread-2
pool-1-thread-3
pool-1-thread-1
pool-1-thread-3
pool-1-thread-2
pool-1-thread-1
pool-1-thread-3

我们发现就只有pool-1-thread-2、pool-1-thread-1、pool-1-thread-3这三个线程一直在跑,并没有其它线程。
在 Executors 里面提供了几个线程池的工厂方法,
newFixedThreadPool: 该方法返回一个固定数量的线程池,线程数不变,当有一个任务提交 时,若线程池中空闲,则立即执行,若没有,则会被暂缓在一个任务队列中,等待有空闲的 线程去执行。
newSingleThreadExecutor: 创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓 在任务队列中。
newCachedThreadPool:返回一个可根据实际情况调整线程个数的线程池,不限制最大线程 数量,若用空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程会在 60 秒 后自动回收
newScheduledThreadPool: 创建一个可以指定线程的数量的线程池,但是这个线程池还带有 延迟和周期性执行任务的功能,类似定时器。

ThreadpoolExecutor

上面提到的四种线程池的构建,都是基于 ThreadpoolExecutor 来构建的。
线程池和 ThreadPoolThread 有哪些构造参数:

public ThreadPoolExecutor(int corePoolSize, //核心线程数量
int maximumPoolSize, //最大线程数
long keepAliveTime, //超时时间,超出核心线程数量以外的线程空余存活时间
TimeUnit unit, //存活时间单位
BlockingQueue<Runnable> workQueue, //保存执行任务的队列 
ThreadFactory threadFactory,//创建新线程使用的工厂 
RejectedExecutionHandler handler //当任务无法执行的时候的处理方式
)

线程池初始化以后做了什么事情

线程池初始化时是没有创建线程的,线程池里的线程的初始化与其他线程一样,但是在完成任务以后,该线程不会自行销毁,而是以挂起的状态返回到线程池。
直到应用程序再次向线程池发出请求时,线程池里挂起的线程就会再度激活执行任务。这样既节省了建立线程所造成的性能损耗,也可以让多个任务反复重用同一线程,从而在应用程序生存期内节约大量开销

newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads){ return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

1、FixedThreadPool 的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超 过核心线程数后,任务都会被放到阻塞队列中。
2、选用的阻塞队列是 LinkedBlockingQueue,使用的是默认容量 Integer.MAX_VALUE, 相当于没有上限。
newFixedThreadPool线程池执行任务的流程如下:

  1. 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务。
  2. 线程数等于核心线程数后,将任务加入阻塞队列
  3. 由于队列容量非常大,可以一直添加
  4. 执行完任务的线程反复去队列中取任务执行
    用途:FixedThreadPool 用于负载比较大的服务器,为了资源的合理利用,需要限制当前线 程数量。

newCachedThreadPool

public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

CachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空 闲线程,若无可回收,则新建线程;
并且没有核心线程,非核心线程数无上限,但是每个空闲 的时间只有 60 秒,超过后就会被回收。

newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定 顺序(FIFO, LIFO, 优先级)执行

线程池的实现原理

ThreadPoolExecutor 是线程池的核心,提供了线程池的实现。 ScheduledThreadPoolExecutor 继承了 ThreadPoolExecutor,并另外提供一些调度方法以支 持定时和周期任务。
Executers 是工具类,主要用来创建线程池对象 我们把一个任务提交给线程池去处理的时候,线程池的处理过程是什么样的呢?首先直接来看看定义
在这里插入图片描述

源码分析& execute

基于源码入口进行分析,先看 execute 方法

public void execute(Runnable command) { 
if (command == null)
throw new NullPointerException(); 
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {//1.当前池中线程比核心数少,新建一个线 程执行任务
	if (addWorker(command, true)) return;
	c = ctl.get(); 
}
if (isRunning(c) && workQueue.offer(command)) {//2.核心池已满,但任务队列 未满,添加到队列中
	int recheck = ctl.get();
//任务成功添加到队列以后,再次检查是否需要添加新的线程,因为已存在的线程可能被销毁了 
if (! isRunning(recheck) && remove(command))
	reject(command);//如果线程池处于非运行状态,并且把当前的任务从任务队列中 移除成功,则拒绝该任务
else if (workerCountOf(recheck) == 0)//如果之前的线程已被销毁完,新建一个 线程
	addWorker(null, false);
}
else if (!addWorker(command, false))//3.核心池已满,队列已满,试着创建一个新 线程
	reject(command); //如果创建新线程失败了,说明线程池被关闭或者线程池完全满了, 拒绝任务
}

ctl 的作用

在线程池中,ctl 贯穿在线程池的整个生命周期中。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING,
0));

它是一个原子类,主要作用是用来保存线程数量和线程池的状态。其实比较有意思,他用到了位运算
一个 int 数值是 32 个 bit 位,这里采用高 3 位来保存运行状态,低 29 位来保存线程数量。
那么同理可得其他的状态的 bit 位表示:

private static final int COUNT_BITS = Integer.SIZE - 3; //32-3
private static final int CAPACITY = (1 << COUNT_BITS) - 1; //将 1 的二进制 向右位移 29 位,再减 1 表示最大线程容量
//运行状态保存在 int 值的高 3 位 (所有数值左移 29 位)
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;//所有的任务都已结束,线程数量为 0,处于该状态的线程池即将调用 terminated()方法
private static final int TERMINATED = 3 << COUNT_BITS;// terminated()方法 执行完成

addWorker

如果工作线程数小于核心线程数的话,会调用 addWorker,顾名思义,其实就是要创建一个 工作线程。代码可以看出线程池 初始化时是没有创建线程的
我们来看看源码的实现,其实就做了两件事。
1)才用循环 CAS 操作来将线程数加 1;
2)新建一个线程并启用。

private boolean addWorker(Runnable firstTask, boolean core) { retry: //goto语句,避免死循环
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
       // Check if queue empty only if necessary.
 /*
 如果线程处于非运行状态,并且 rs 不等于 SHUTDOWN 且 firstTask 不等于空且且 workQueue 为空,直接返回 false(表示不可添加 work 状态)
1. 线程池已经 shutdown 后,还要添加新的任务,拒绝
2. (第二个判断)SHUTDOWN 状态不接受新任务,但仍然会执行已经加入任务队列的任 务,所以当进入 SHUTDOWN 状态,而传进来的任务为空,并且任务队列不为空的时候,是允许添加 新线程的,如果把这个条件取反,就表示不允许添加 worker 
*/    
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty())) 
	return false;
for (;;) { //自旋
int wc = workerCountOf(c);//获得Worker工作线程数
//如果工作线程数大于默认容量大小或者大于核心线程数大小,则直接返回 false 表示不 能再添加 worker。
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize)) return false;
if (compareAndIncrementWorkerCount(c))//通过cas来增加工作线程数, 如果 cas 失败,则直接重试
 break retry;
c = ctl.get(); // Re-read ctl //再次获取ctl的值
if (runStateOf(c) != rs) //这里如果不想等,说明线程的状态发生了变化,继续重试
continue retry;
} }

上面这段代码主要是对 worker 数量做原子+1 操作,下面的逻辑才是正式构建一个 worker

//上面这段代码主要是对 worker 数量做原子+1 操作,下面的逻辑才是正式构建一个 worker 
boolean workerStarted = false; //工作线程是否启动的标识
boolean workerAdded = false; //工作线程是否已经添加成功的标识 Worker w = null;
try {
w = new Worker(firstTask);//构建一个Worker,这个worker是什么呢?我们 可以看到构造方法里面传入了一个 Runnable 对象
final Thread t = w.thread; //从 worker 对象中取出线程 
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); //这里有个重入锁,避免并发问题 
try {
	int rs = runStateOf(ctl.get());
//只有当前线程池是正在运行状态,[或是 SHUTDOWN 且 firstTask 为空],才能添加到 workers 集合中	
if (rs < SHUTDOWN ||(rs == SHUTDOWN && firstTask == null)) {
//任务刚封装到 work 里面,还没 start,你封装的线程就是 alive,几个意思?肯定是要抛异常出去的
if (t.isAlive()) // precheck that t is startable 
	throw new IllegalThreadStateException();
workers.add(w); //将新创建的Worker添加到workers集合中 
int s = workers.size();
//如果集合中的工作线程数大于最大线程数,这个最大线程数表示线程池曾经出现过的最大线程数
if (s > largestPoolSize)
	largestPoolSize = s; //更新线程池出现过的最大线程数 
	workerAdded = true;//表示工作线程创建成功了
}
} finally {
	mainLock.unlock(); //释放锁
}
if (workerAdded) {//如果worker添加成功
	t.start();//启动线程
	workerStarted = true; 
	}
}
} finally {
if (! workerStarted)
	addWorkerFailed(w); //如果添加失败,就需要做一件事,就是递减实际工作线程数(还记得我们最开始的时候增加了工作线程数吗) 
}
return workerStarted;//返回结果

Worker 类说明

我们发现 addWorker 方法只是构造了一个 Worker,并且把 firstTask 封装到 worker 中,它是 做什么的呢?我们来看看

  1. 每个worker,都是一条线程,同时里面包含了一个firstTask,即初始化时要被首先执行的任务.
  2. 最终执行任务的,是 runWorker()方法
    Worker 类继承了 AQS,并实现了 Runnable 接口,注意其中的 firstTask 和 thread 属性:

runWorker 方法

前面已经了解了 ThreadPoolExecutor 的核心方法 addWorker,主要作用是增加工作线程, 而 Worker 简单理解其实就是一个线程,里面重新了 run 方法,这块是线程池中执行任务的 真正处理逻辑,也就是 runWorker 方法,这个方法主要做几件事

  1. 如果 task 不为空,则开始执行 task
  2. 如果 task 为空,则通过 getTask()再去取任务,并赋值给 task,如果取到的 Runnable 不为空,则 执行该任务
  3. 执行完毕后,通过 while 循环继续 getTask()取任务
  4. 如果 getTask()取到的任务依然是空,那么整个 runWorker()方法执行完毕

getTask

worker 线程会从阻塞队列中获取需要执行的任务,这个方法不是简单的 take 数据,我们来 分析下他的源码实现。

你也许好奇是怎样判断线程有多久没有活动了,是不是以为线程池会启动一个监控线程,专 门监控哪个线程正在偷懒?想太多,

其实只是在线程从工作队列 poll 任务时,加上了超时 限制,如果线程在 keepAliveTime 的时间内 poll 不到任务,那我就认为这条线程没事做, 可以干掉了,看看这个代码片段你就清楚了。

private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {//自旋
	int c = ctl.get();
	int rs = runStateOf(c);
	/*
	对线程池状态的判断,两种情况会 workerCount-1,并且返回 null
1. 线程池状态为 shutdown,且 workQueue 为空(反映了 shutdown 状态的线程池还是
要执行 workQueue 中剩余的任务的)
2. 线程池状态为 stop(shutdownNow()会导致变成 STOP)(此时不用考虑 workQueue
的情况)
	*/
	 // Check if queue empty only if necessary. 
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
          decrementWorkerCount();
	return null;//返回 null,则当前 worker 线程会退出 
	}
	int wc = workerCountOf(c);
	// timed变量用于判断是否需要进行超时控制。
// allowCoreThreadTimeOut默认是false,也就是核心线程不允许进行超时; // wc > corePoolSize,表示当前线程池中的线程数量大于核心线程数量;
// 对于超过核心线程数量的这些线程,需要进行超时控制
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
/*
1. 线程数量超过maximumPoolSize可能是线程池在运行时被调用了setMaximumPoolSize() 被改变了大小,否则已经 addWorker()成功不会超过 maximumPoolSize
2. timed && timedOut 如果为 true,表示当前操作需要进行超时控制,并且上次从阻塞队列中 获取任务发生了超时.其实就是体现了空闲线程的存活时间
*/
	 //这个if 判断,目的是控制线程池的有效线程数量
if ((wc > maximumPoolSize || (timed && timedOut)) && (wc > 1 || workQueue.isEmpty())) {
	if (compareAndDecrementWorkerCount(c))
		return null; 
	continue;
} try {
/*
根据 timed 来判断,如果为 true,则通过阻塞队列 poll 方法进行超时控制,如果在 keepaliveTime 时间内没有获取到任务,则返回 null.
否则通过 take 方法阻塞式获取队列中的任务
*/
	Runnable r = timed ?workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
	workQueue.take();
	if (r != null)//如果拿到的任务不为空,则直接返回给worker进行处理
		return r;
	timedOut = true;//如果 r==null,说明已经超时了,设置timedOut=true,在下次自旋的时候进行回收	
	} catch (InterruptedException retry) {
	timedOut = false; // 如果获取任务时当前线程发生了中断,则设置 timedOut 为false 并返回循环重试 
	}
	} 
}	

由上文中的分析可以 知道,在执行 execute 方法时,如果当前线程池的线程数量超过了 corePoolSize 且小于 maximumPoolSize,并且 workQueue 已满时,则可以增加工作线程,
但这时如果超时没有 获取到任务,也就是 timedOut 为 true 的情况,说明 workQueue 已经为空了,也就说明了 当前线程池中不需要那么多线程来执行任务了,可以把多于 corePoolSize 数量的线程销毁 掉,保持线程数量在 corePoolSize 即可。

什么时候会销毁?当然是 runWorker 方法执行完之后,也就是 Worker 中的 run 方法执行 完,由 JVM 自动回收。
getTask 方法返回 null 时,在 runWorker 方法中会跳出 while 循环,然后会执行 processWorkerExit 方法。

processWorkerExit

runWorker 的 while 循环执行完毕以后,在 finally 中会调用 processWorkerExit,来销毁工作线 程。

execute 后续逻辑分析

1、如果核心线程数已满,说明这个时候不能再创建核心线程了,于是走第二个判断
2、第二个判断逻辑比较简单,如果线程池处于运行状态并且任务队列没有满,则将任务添加到队列中
3、第三个判断,核心线程数满了,队列也满了,那么这个时候创建新的线程也就是(非核心线程)
4、如果非核心线程数也达到了最大线程数大小,则直接拒绝任务

if (isRunning(c) && workQueue.offer(command)) {//2.核心池已满,但任务队列未 满,添加到队列中
	int recheck = ctl.get(); //任务成功添加到队列以后,再次检查是否需要添加新的线程,因为已存在的线程可能被销毁了
if (! isRunning(recheck) && remove(command)) 
	reject(command);//如果线程池处于非运行状态,并且把当前的任务从任务队列中移除成功,则拒绝该任务
else if (workerCountOf(recheck) == 0)//如果之前的线程已被销毁完,新建一个线程
	addWorker(null, false);
}
else if (!addWorker(command, false)) //3.核心池已满,队列已满,试着创建一个新 线程
reject(command); //4、如果创建新线程失败了,说明线程池被关闭或者线程池完全满 了,拒绝任务

拒绝策略

1、AbortPolicy:直接抛出异常,默认策略;
2、CallerRunsPolicy:用调用者所在的线程来执行任务;
3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务; 4、DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录 日志或持久化存储不能处理的任务

线程池使用注意

线程池的构建不允许使用 Executors 去创建,而是通过ThreadPoolExecutor 的方式。
简单分析下,用 Executors 使得用户不 需要关心线程池的参数配置,意味着大家对于线程池的运行规则也会慢慢的忽略。这会导致 一个问题,
eg:
比如我们用 newFixdThreadPool 或者 singleThreadPool.允许的队列长度为 Integer.MAX_VALUE,如果使用不当会导致大量请求堆积到队列中导致 OOM 的风险。
newCachedThreadPool,允许创建线程数量为Integer.MAX_VALUE,也可能会导致大量 线程的创建出现 CPU 使用过高或者 OOM 的问题
而如果我们通过 ThreadPoolExecutor 来构造线程池的话,我们势必要了解线程池构造中每个 参数的具体含义,使得开发者在配置参数的时候能够更加谨慎。

如何合理配置线程池的大小

在遇到这类问题时,先冷静下来分析

  1. 需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型
  2. 每个任务执行的平均时长大概是多少,这个任务的执行时长可能还跟任务处理逻辑是否涉 及到网络传输以及底层系统资源依赖有关系

eg1:如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu 的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行 线程数,加入 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上 下文切换反而使得效率降低
eg2:如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态, 导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。一般可以配置 cpu 核心数的 2 倍。
有一个公式:线程池设定最佳线程数目 = (线程池设定的线程等待时间+线程 CPU 时间)/ 线程 CPU 时间 )* CPU 数目
这个公式的线程 cpu 时间是预估的程序单个线程在 cpu 上运行的时间(通常使用 loadrunner 测试大量运行次数求出平均值)

线程池中的线程初始化

默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。
在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:
prestartCoreThread():初始化一个核心线程;
prestartAllCoreThreads():初始化所有核心线 程

ThreadPoolExecutor tpe=(ThreadPoolExecutor)service; tpe.prestartAllCoreThreads();

线程池的关闭

ThreadPoolExecutor 提供了两个方法,用于线程池的关闭,分别是 shutdown()和 shutdownNow(),
其中:shutdown():不会立即终止线程池,而是要等所有任务缓存队列中 的任务都执行完后才终止,但再也不会接受新的任务。
shutdownNow():立即终止线程池,并 尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。

线程池容量的动态调整

ThreadPoolExecutor 提供了动态调整线程池容量大小的方法:setCorePoolSize()和 setMaximumPoolSize(),
setCorePoolSize:设置核心池大小
setMaximumPoolSize:设置线 程池最大能创建的线程数目大小

任务缓存队列及排队策略

在前面我们多次提到了任务缓存队列,即 workQueue,它用来存放等待执行的任务。
workQueue 的类型为 BlockingQueue,通常可以取下面三种类型:

  1. ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
  2. LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为 Integer.MAX_VALUE;
  3. SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

线程池的监控

如果在项目中大规模的使用了线程池,那么必须要有一套监控体系,来指导当前线程池的状 态,当出现问题的时候可以快速定位到问题。而线程池提供了相应的扩展方法,我们通过重 写线程池的 beforeExecute、afterExecute 和 shutdown 等方式就可以实现对线程的监控。

public class Demo extends ThreadPoolExecutor {
// 保存任务开始执行的时间,当任务结束时,用任务结束时间减去开始时间计算任务执行时间 
private ConcurrentHashMap<String,Date> startTimes;
	public Demo(int corePoolSize, int maximumPoolSize, long 		keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> 	workQueue) {
	super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
	this.startTimes=new ConcurrentHashMap<>(); 
	}
@Override
public void shutdown() { System.out.println("已经执行的任务数:"+this.getCompletedTaskCount()+"," + "当前活动线程数:"+this.getActiveCount()+",当前排队线程数:"+this.getQueue().size()); System.out.println();
super.shutdown(); 
}
//任务开始之前记录任务开始时间
@Override
protected void beforeExecute(Thread t, Runnable r) {
	startTimes.put(String.valueOf(r.hashCode()),new Date());
	super.beforeExecute(t, r); 
	}
@Override
protected void afterExecute(Runnable r, Throwable t) {
	Date startDate = startTimes.remove(String.valueOf(r.hashCode())); 
	Date finishDate = new Date();
	long diff = finishDate.getTime() - startDate.getTime(); // 统计任务耗时、初始线程数、核心线程数、正在执行的任务数量、
// 已完成任务数量、任务总数、队列里缓存的任务数量、
// 池中存在的最大线程数、最大允许的线程数、线程空闲时间、线程池是否关闭、线程池 是否终止
	System.out.print("任务耗时:"+diff+"\n"); 
	System.out.print("初始线程数:"+this.getPoolSize()+"\n"); 			
	System.out.print("核心线程数:"+this.getCorePoolSize()+"\n"); 
	System.out.print("正在执行的任务数量:"+this.getActiveCount()+"\n"); 
	System.out.print("已经执行的任务数:"+this.getCompletedTaskCount()+"\n"); 
	System.out.print("任务总数:"+this.getTaskCount()+"\n"); 		
	System.out.print("最大允许的线程数:"+this.getMaximumPoolSize()+"\n");
	System.out.print("线程空闲时间:"+this.getKeepAliveTime(TimeUnit.MILLISECONDS)+"\n"); 			
	System.out.println();
	super.afterExecute(r, t); 
	}
public static ExecutorService newCachedThreadPool() {
	return new Demo1(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new
SynchronousQueue ());
   }
}

Callable/Future 使用及原理分析

线程池的执行任务有两种方法,一种是 submit、一种是 execute; 这两个方法是有区别的,那么基于这个区别我们再来看看。

execute 和 submit 区别

  1. execute 只可以接收一个 Runnable 的参数

  2. execute 如果出现异常会抛出

  3. execute 没有返回值

  4. submit 可以接收 Runable 和 Callable 这两种类型的参数

  5. 对于 submit 方法,如果传入一个 Callable,可以得到一个 Future 的返回值

  6. submit 方法调用不会抛异常,除非调用 Future.get

Callable/Future 案例演示

Callable/Future 和 Thread 之类的线程构建最大的区别在于,能够很方便的获取线程执行完 以后的结果。首先来看一个简单的例子

public class CallableDemo implements Callable<String> {
@Override
public String call() throws Exception {
	Thread.sleep(3000);//阻塞案例演示
return "hello world"; 
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
	CallableDemo callableDemo=new CallableDemo(); 
	FutureTask futureTask=new FutureTask(callableDemo); 
	new Thread(futureTask).start(); 
	System.out.println(futureTask.get());
} }

运行结果:

等了3秒钟后,才拿到返回结果。

想一想我们为什么需要使用回调呢? 那是因为结果值是由另一线程计算的,当前线程是不知 道结果值什么时候计算完成,所以它传递一个回调接口给计算线程,当计算完成时,调用这 个回调接口,回传结果值
所以我们来看一下 Future/Callable 是如何实现的

Callable/Future 原理分析

在刚刚实现的 demo 中,我们用到了两个 api,分别是 CallableFutureTask
Callable 是一个函数式接口,里面就只有一个 call 方法。子类可以重写这个方法,并且这个方法会有一个返回值

@FunctionalInterface
public interface Callable<V> { /**
* Computes a result, or throws an exception if unable to do so. *
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception; 
}

FutureTask

FutureTask 的类关系图如下,它实现 RunnableFuture 接口,那么这个 RunnableFuture 接口 的作用是什么呢。
在讲解 FutureTask 之前,先看看 Callable, Future, FutureTask 它们之间的关系图,如下:
在这里插入图片描述
RunnableFuture 是一个接口,它继承了 Runnable 和 Future 这两个接口,Runnable 太熟悉 了,那么 Future 是什么呢?
Future 表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获 取任务的结果和取消任务等。

public interface Future<V> {
	boolean cancel(boolean mayInterruptIfRunning);
	// 当前的 Future 是否被取消,返回 true 表示已取消 
	boolean isCancelled();
	// 当前 Future 是否已结束。包括运行完成、抛出异常以及取消,都表示当前 Future 已结束 
	boolean isDone();
	// 获取 Future 的结果值。如果当前 Future 还没有结束,那么当前线程就等待, // 直到 Future 运行结束,那么会唤醒等待结果值的线程的。
	V get() throws InterruptedException, ExecutionException;
	// 获取 Future 的结果值。与 get()相比较多了允许设置超时时间 
	V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException;

FutureTask 是 Runnable 和 Future 的结合,如果 我们把 Runnable 比作是生产者,Future 比作是消费者,那么 FutureTask 是被这两者共享的, 生产者运行 run 方法计算结果,消费者通过 get 方法获取结果。
作为生产者消费者模式,有一个很重要的机制,就是如果生产者数据还没准备的时候,消费 者会被阻塞。当生产者数据准备好了以后会唤醒消费者继续执行。 这个有点像我们上次可分析的阻塞队列,那么在 FutureTask 里面是基于什么方式实现的呢?

state 的含义

表示 FutureTask 当前的状态,分为七种状态

// NEW 新建状态,表示这个 FutureTask 还没有开始运行
private static final int NEW = 0; 
// COMPLETING 完成状态, 表示 FutureTask 任务已经计算完毕了
// 但是还有一些后续操作,例如唤醒等待线程操作,还没有完成。
private static final int COMPLETING = 1; 
// FutureTask任务完结,正常完成,没有发生异常
private static final int NORMAL = 2;
 // FutureTask任务完结,因为发生异常。
private static final int EXCEPTIONAL = 3; 
// FutureTask任务完结,因为取消任务
private static final int CANCELLED = 4;
// FutureTask任务完结,也是取消任务,不过发起了中断运行任务线程的中断请求
private static final int INTERRUPTING = 5;
// FutureTask任务完结,也是取消任务,已经完成了中断运行任务线程的中断请求
private static final int INTERRUPTED = 6;

get 方法

get 方法就是阻塞获取线程执行结果,这里主要做了两个事情

  1. 判断当前的状态,如果状态小于等于 COMPLETING,表示 FutureTask 任务还没有完结,
    所以调用 awaitDone 方法,让当前线程等待。
  2. report 返回结果值或者抛出异常
public V get() throws InterruptedException, ExecutionException { 
	int s = state;
	if (s <= COMPLETING)
	s = awaitDone(false, 0L); 
	return report(s);
}

awaitDone

如果当前的结果还没有被执行完,把当前线程线程和插入到等待队列。
被阻塞的线程,会等到 run 方法执行结束之后被唤醒

report

report 方法就是根据传入的状态值 s,来决定是抛出异常,还是返回结果值。这个两种情况都 表示 FutureTask 完结了

private V report(int s) throws ExecutionException { 
	Object x = outcome;//表示call的返回值
	if (s == NORMAL) // 表示正常完结状态,所以返回结果值
		return (V)x;
	// 大于或等于 CANCELLED,都表示手动取消 FutureTask 任务, // 所以抛出 CancellationException 异常
	if (s >= CANCELLED)
		throw new CancellationException();
	// 否则就是运行过程中,发生了异常,这里就抛出这个异常
	throw new ExecutionException((Throwable)x); 
}

线程池对于 Future/Callable 的执行

我们现在再来看线程池里面的 submit 方法,就会很清楚了。

public class CallableDemo implements Callable<String> {
@Override
public String call() throws Exception {
	Thread.sleep(3000);//阻塞案例演示
	return "hello world"; 
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
	ExecutorService es=Executors.newFixedThreadPool(1);
	CallableDemo callableDemo=new CallableDemo(); 
	Future 	future=es.submit(callableDemo); 	
	System.out.println(future.get());//future.get()这里是阻塞的
} }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值