线程池状态
ThreadPoolExecutor内部由一个32位的字段ctl来标识线程池的状态。其中前三位代表线程池的状态,后29位代表了线程池的数量。
线程池的状态共划分为RUNNING(111),SHUTDOWN(000),STOP(001),TIDYING(010),TERMINATED(011)。
默认情况下,TERMINATED>TUDYING>STOP>SHUTDOWN>RUNNING状态。第一位其实是正负号。
RUNNING代表了线程池正在运行。
SHUTDOWN代表了线程池不会接收新任务,但会将阻塞队列中的任务执行完成。
STOP代表线程池不会执行新任务,并且阻塞队列中的任务也要抛弃掉。
TIDYING代表了当前线程池任务全部执行完成,活动线程为0,线程池准备结束
TERMINATED代表当前线程池已经消亡。
之所以要这么设计,将线程池状态与线程池数量拼接到一块,目的是为了可以使用CAS对结果一次性完成更改,保护数据安全。
线程池的构造方法
线程池的构造方法有很多,最底层的是七个结果的这种:
corePoolSize代表了核心线程数,最多保留的线程数。
maximumPoolSize,代表当前线程池的最大线程数,corePoolSize+救急线程的数量
keepaliveTime,代表了当前线程池中,救急线程的存活时间。
unit,救急线程存活的时间单位。
workQueue,代表了阻塞队列,就是当核心线程数足够后放入至阻塞队列中。
ThreadFactory,线程工厂,用来创建线程的工厂。
handler,任务拒绝策略,默认情况下抛一个异常。
可以看一下调用了该构造的其它构造方法,也就是四类线程池的构造方法,后续介绍。先说一下流程。
1.当线程池创建对象时,会首先校验corePoolSize是否满了,如果没有满,则创建新的线程来执行任务。
2.如果corePoolSize满了,判断workQueue是否满了,如果没有满,那么会将当前任务存放至workQueue中。
3.如果workQueue都满了,那么会创建救急线程,最大数量为maximumSize-corePoolSize的数量,如果救急线程也满了,那么只能调用任务拒绝策略来对结果进行结束了。默认拒绝策略是hander,抛出了一个RejectExecutionException。
其实还有其它的策略方式,我直接复制粘贴了。
AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
CallerRunsPolicy 让调用者运行任务
DiscardPolicy 放弃本次任务
DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方
便定位问题
Netty 的实现,是创建一个新线程来执行任务
ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
4.当高峰使用期过去后,会将创建的救急线程经过keepaliveTime以及TimeUnit的结果给销毁了。
根据构造方法,说一下各类线程池。
newFixedThreadPool
newFixedThreadPool,代表了定长线程池。看一下构造函数的结构。
这四类线程池,都是引用的ThreadPoolExecutor的构造函数,其中,corePoolSize,以及maximumPoolSize都是设计的入参的线程数,过期时间为0,代表了当前线程池之中没有救急线程,阻塞队列采用的是LinkedBlockedQueue队列,这个队列是无界队列,如果创建的线程大于corePoolSize,就将线程放入至阻塞队列中,然后等待核心线程处理完成后进行消费。
这种适合处理任务量已知,相对比较耗时的任务。
newCatchedThreadPool
缓冲类线程池,核心线程为0,默认创建的线程都是救急线程,没有核心线程。在业务逻辑处理完成后,60s后没有操作会被销毁。阻塞队列采用的是SynchronousQueue来进行阻塞。
这种线程池会根据任务数来进行不断的增长,没有上限,当任务执行完毕后,60s后线程被释放。适合任务比较密集,但执行时间较短的现象。
队列采用了SynchronousQueue来进行存放,这种队列的特点是,没有容量,当没有线程试图取的时候,任务是存放不进队列的。
newSingleThreadExecutor
默认情况下,线程池中的corePoolSize与maximumPoolSize长度都是1,只会创建一个线程,剩下进入的任务都会存放到LinkedBlockingQueue中。来对任务进行串行执行。
该线程池相比于当独创建一个线程的好处是,如果线程因为异常情况消亡后,会自动新创建一个线程来继续执行任务。保证线程池的工作。
newFixedThreadPool,与newSingleThreadExecutor有点类似,但是newFixedThreadPool对外暴露的是ThreadPoolExecutor,这种方式可以强转更改线程数的,而newSingleThreadExecutor则是被装饰了一层,从而不会被强转。
常用的方法
execute,submit,invokeAll(在这个里面我看到了future的get方法),invokeAny,shutdown,shutdownNow,isTerminated,isShutDown。
package com.bo.threadstudy.eight;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class ThreadPoolTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// fixedThreadPoolExecute();
// newCatchedThreadPoolTest();
//其实我上方的使用都是阻塞式方式,对性能的提升并不友好,真正的操作是多个操作并行进行,我这种样例还是按串行走了
// newSingleThreadExecutorTest();
shutdownNowTest();
}
//TODO 这个shutdownNow我没有测试好,后续再说
private static void shutdownNowTest() throws InterruptedException {
//执行方法,submit以及execute已经演示过了,试一下invokeAny以及invokeAll
ExecutorService executorService = Executors.newFixedThreadPool(10);
List<Callable<Integer>> runnables = new ArrayList<>();
AtomicInteger atomicInteger = new AtomicInteger(10);
// invokeAllTest(executorService, runnables, atomicInteger);
// invokeAnyTest(executorService, runnables, atomicInteger);
//测试一下线程停止的方法
for (int i = 0; i < 20; i++) {
runnables.add(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
Thread.sleep(1000);
log.debug(atomicInteger.getAndDecrement()+"");
return 1;
}
});
}
//执行所有任务,我看了下,之所以shutDown的原因是源码离调用了future的get方法,卡住了,自然就出问题了
executorService.invokeAll(runnables);
//立刻停止线程池
executorService.shutdownNow();
log.debug(executorService.isShutdown()+"");
log.debug(executorService.isTerminated()+"");
// executorService.shutdown();
}
private static void invokeAnyTest(ExecutorService executorService, List<Callable<Integer>> runnables, AtomicInteger atomicInteger) throws InterruptedException, ExecutionException {
//invokeAny相当于将线程中的所有任务都执行,取结果,取最快执行完成的那一个
for (int i = 0; i < 20; i++) {
runnables.add(() -> {
//我这里配合了CAS使用,结果是没有问题的,但是在主线程中,输出的结果顺序是不一样的
return atomicInteger.decrementAndGet();
});
}
Integer integer = executorService.invokeAny(runnables);
log.debug(integer+"");
}
private static void invokeAllTest(ExecutorService executorService, List<Callable<Integer>> runnables, AtomicInteger atomicInteger) throws InterruptedException {
for (int i = 0; i < 20; i++) {
runnables.add(() -> {
//我这里配合了CAS使用,结果是没有问题的,但是在主线程中,输出的结果顺序是不一样的
return atomicInteger.decrementAndGet();
});
}
List<Future<Integer>> futures = executorService.invokeAll(runnables);
futures.forEach(integerFuture -> {
try {
log.debug(integerFuture.get()+"");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
});
}
private static void newSingleThreadExecutorTest() {
ExecutorService executorService2 = Executors.newSingleThreadExecutor();
final int[] i = {10};
for (int i1 = 0; i1 < 10; i1++) {
//这不用Atomic问题也没事,因为只是一个线程在执行,不过只写成i--还是会报错的,这可能就是规范方面的事
executorService2.execute(() -> {
i[0] = i[0] -1;
log.debug("{}", i[0]);
});
}
//线程池停止,原先的服务没有停的,现在的任务停了
executorService2.shutdown();
}
private static void newCatchedThreadPoolTest() throws InterruptedException, ExecutionException {
AtomicInteger atomicInteger = new AtomicInteger(10);
ExecutorService executorService1 = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
//这儿的话肯定是线性执行了,因为有get阻塞,atomicInteger有点多余
Future<Integer> submit = executorService1.submit(() -> {
return atomicInteger.decrementAndGet();
});
log.debug(submit.get()+"");
}
}
private static void fixedThreadPoolExecute() {
//newFixedThreadPool,定长线程池的execute执行
ExecutorService executorService = Executors.newFixedThreadPool(10);
AtomicInteger atomicInteger = new AtomicInteger(10);
for (int i = 0; i < 10; i++) {
executorService.execute(() -> {
log.debug(atomicInteger.decrementAndGet()+"");
});
}
}
}
newSchudledThreadPool
任务调度线程池,这个线程池采用的也是ThreadPoolExecutor的实现,可以看到corePoolSize是传入的参数,救急线程数无限制。生存时间是0?只是队列采用了delayedWorkQueue。
这个线程池可以设置定时任务来执行,即可以在规定的时间来对任务进行执行。
内部的话,主要方法就三个,
schedule()方法,多久之后执行当前任务,只执行一次。
scheduleAtFixedRate,设计一个初始化时间,然后在间隔多久时间后开始执行,如果执行的时间超过间隔时间,会按执行时间先走。执行多次的任务。
scheduledWithFixedDelay,即执行完当前任务后,间隔多久来执行这个任务,执行多次的任务。
package com.bo.threadstudy.eight;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Slf4j
public class NewSchudledThreadPoolTest {
public static void main(String[] args) {
//设置长度为10吧
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
// extracted(scheduledExecutorService);
extracted1(scheduledExecutorService);
//这个任务的第三个参数,是指的当前时间执行完成之后的间隔,即两秒执行完任务,等待1秒后继续执行任务
// scheduledExecutorService.scheduleWithFixedDelay(() -> {
// try {
// Thread.sleep(2000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// log.debug("我在测试");
// },0,1,TimeUnit.SECONDS);
// //执行多次,就是从线程池中直接再取一个线程出来进行操作,明白了
// scheduledExecutorService.scheduleWithFixedDelay(() -> {
// try {
// Thread.sleep(2000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// log.debug("我在测试");
// },0,2,TimeUnit.SECONDS);
}
private static void extracted1(ScheduledExecutorService scheduledExecutorService) {
//定时任务
scheduledExecutorService.scheduleAtFixedRate(() -> {
//看看任务执行完之前是否会走任务,该方法必须得执行完成毕后,才可以走下一趟
//这个其实是我执行完成后,然后超过了当前规定的时间,那么下一个方法立刻执行,不考虑别的
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("我在测试");
},0,1500,TimeUnit.MILLISECONDS);
}
private static void extracted(ScheduledExecutorService scheduledExecutorService) {
scheduledExecutorService.schedule(() -> {
//1秒后执行
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("我在测试");
scheduledExecutorService.shutdown();
},1, TimeUnit.SECONDS);
}
}
线程池异常抓取
在线程池执行任务出现异常时,execute会把异常给爆出来,而submit是不会爆异常的,所以针对这种现象,有两种解决方案:
try-catch抓取异常
利用future来返回结果,在返回结果后这个异常就可以爆出来了。
package com.bo.threadstudy.eight;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* 关于线程处理异常
*/
@Slf4j
public class ThreadPoolException {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
Future<Integer> submit = executorService.submit(() -> {
//试了一下,executor方法是可以抛出异常的,而submit是没有办法抛出的,两种解决方法
//第一种,trycatch抛出异常,第二种通过返回结果抛出异常
int i = 1 / 0;
return i;
});
log.debug(submit.get()+"");
}
}
ForkJoinPool
forkJoinPool是1.7引入的线程池,该线程池的目的,是采用了分治思想来,对CPU密集型运算来进行拆分。
所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计
算,如归并排序、斐波那契数列、都可以用分治思想进行求解
Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率
Fork/Join 默认会创建与 cpu 核心数大小相同的线程池
其实总结就是一句话,涉及到动态规划,递归类型的算法,可以采用ForkJoinPool线程池来进行操作。
假设我内部存在多个递归调用,如果是单线程的话,一个递归在执行,另一个递归得等待这个递归执行完成后,才能继续执行,就像快速排序一般,存在两个递归的这种。但如果使用forkJoinPool的方式,则开两个线程处理任务,相互之间并不影响。性能提升。
package com.bo.threadstudy.eight;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
@Slf4j
public class ForkJoinPoolTest {
public static void main(String[] args) {
// ForkJoinPool forkJoinPool = new ForkJoinPool(10);
// log.debug(forkJoinPool.invoke(new ForkTask(6))+"");
ForkJoinPool forkJoinPool = new ForkJoinPool(10);
log.debug(forkJoinPool.invoke(new ForkTaskNew(1,7))+"");
}
}
@Slf4j
class ForkTaskNew extends RecursiveTask<Integer>{
private Integer begin;
private Integer end;
public ForkTaskNew(Integer begin, Integer end) {
this.begin = begin;
this.end = end;
}
@Override
protected Integer compute() {
//起始值初始值比较,作为终止条件
if(begin == end){
return begin;
}
if(begin == end - 1){
return begin+end;
}
//我这块写的性能不够快,二分法相比递归其实更快,因为多用了线程
// ForkTaskNew forkTaskNew = new ForkTaskNew(begin + 1, end - 1);
// forkTaskNew.fork();
// Integer i = 0;
// try {
// i = forkTaskNew.get();
// } catch (InterruptedException e) {
// e.printStackTrace();
// } catch (ExecutionException e) {
// e.printStackTrace();
// }
//
// return begin+end+i;
Integer mid = (begin+end)/2;
ForkTaskNew forkTaskNew1 = new ForkTaskNew(begin, mid);
forkTaskNew1.fork();
ForkTaskNew forkTaskNew2 = new ForkTaskNew(mid+1, end);
forkTaskNew2.fork();
Integer i1 = null;
Integer i2 = null;
try {
i1 = forkTaskNew1.get();
i2 = forkTaskNew2.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return i1+i2;
}
}
/**
* 提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)或 RecursiveAction(没有返回值),例如下
* 面定义了一个对 1~n 之间的整数求和的任务
*/
@Slf4j
class ForkTask extends RecursiveTask<Integer>{
private Integer cur;
public ForkTask(Integer cur) {
this.cur = cur;
}
//在这里实现一个累加的方法,利用递归的方式来添加
//后期我可以试一下快速排序使用forkJoinPool来优化,面试官虽然想要的肯定不是这个答案
//这个有点类似Runable中的run方法,代表了执行任务
@Override
protected Integer compute() {
//此次先以线性的方式来表达一下,
// return addTest01();
//累加的方式,就像原先的方式,实际运行的次数很明显是cur,而如果从中间取值,那么创建的线程就是n/2
return null;
}
private Integer addTest01() {
if(cur == 1){
log.debug("递归运行到最下层");
return 1;
}
ForkTask forkTask = new ForkTask(cur - 1);
//异步执行当前任务
forkTask.fork();
Integer value = null;
try {
value = cur+forkTask.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return value;
}
}