并发编程⼯具-线程池核⼼参数和⼯作原理
⽬录
线程池绝对的⾯试⾼频,确实因为多线程是解决并发问题特别是提升某些核⼼项⽬接⼝的利器,但是使⽤不好也存在⼤量的问题,那么搞清楚线程池的⼯作原理尤为重要。之前接触线程池基本都存在于⾯试阶段,但是当第⼀次在项⽬上看别⼈使⽤线程池解决并⾏任务时特别的震惊,项⽬上⼀个接⼝并⾏了17个⼦任务。现在已经基本在项⽬上都会使⽤线程池来解决核⼼问题,理解也⽐较深了才敢写这⽅⾯的博客【当然,理解线程池的原理就更为重要了,否则就是像在裸奔】。
⾸先我们知道Java的线程是与操作系统的线程⼀⼀映射,并且创建和销毁线程的代价⾮常⼤,那么线程复⽤就是基本思想,但是需要针
对不同的执⾏任务(有的⾮常耗时,有的在短时间内需要处理⼤量的请求,波峰过后若线程还⼀直存在那么也是在消耗资源)。
1、线程池的结构
说到线程池⾸先想到的就是Executor的⼦类ThreadPoolExecutor,在创建之前我们先看看继承体系结构和每⼀层定义的接⼝⽅法,先
看⽗类:
1)、顶层接⼝是Executor
public interface Executor {
void execute(Runnable var1);
}
2)、接⼝ExecutorService
public interface ExecutorService extends Executor {
// 1、线程关闭相关⽅法,这我们在两阶段终⽌模式中分析过
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long var1, TimeUnit var3) throws InterruptedException;
// 2、提交单个任务,有Runnable、Callable的任务
<T> Future<T> submit(Callable<T> var1);
<T> Future<T> submit(Runnable var1, T var2);
Future<?> submit(Runnable var1);
// 3、批量执⾏任务,等待最晚的任务执⾏完成返回(可以设置超时时间),我在项⽬上使⽤⾮常多,当然该场景还可以使⽤
// CountDownLatch协调,或者使⽤CompletionService执⾏,后续使⽤项⽬代码对⽐
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> var1) throws InterruptedException;
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> var1, long var2, TimeUnit var4) throws InterruptedException;
// 4、批量执⾏任务,只要⼀个得到任务即可返回(这种主要⽤于获取相同的任务,拿到最快的⼀个即返回),
// 坏处浪费资源,好处就是防⽌最慢的任务拖长性能耗时
<T> T invokeAny(Collection<? extends Callable<T>> var1) throws InterruptedException, ExecutionException;
<T> T invokeAny(Collection<? extends Callable<T>> var1, long var2, TimeUnit var4) throws InterruptedException, ExecutionException, TimeoutException; }
3)、抽象类AbstractExecutorService
完成了对⼤部分上层接⼝的实现,如常⽤的submit、invokeAll等。
继承的常⽤⼦类:
1)、ScheduledThreadPoolExecutor
同样是juc包下⾯的类,看名字就知道是 定时任务 + 线程池
2)、ThreadPoolTeskExecutor
这个主要是Spring的异步编程机制,@Async的时候会⽤到,可以查看
3)、Tomcat的ThreadPoolExecutor
名字是⼀样的,但是继承⾃juc的ThreadPoolExecutor,主要在与官⽅的线程池的⼯作原理,在解决io
型(Tomcat基本都是io型任务)时不是很适⽤,所以Tomcat做了⼀定的修改,后⾯专门分析。
2、线程池的创建⽅式
juc官⽅提供了Executors类的快速创建线程池的⼯具,但是底层也是使⽤了ThreadPoolExecutor的全参构造器,屏蔽了底层的实现,但是是⾮常可怕的。所以阿⾥开发规范明确规定了不能使⽤Executors进⾏创建线程池。
1)、newFixedThreadPool
public static ExecutorService newFixedThreadPool(int var0) {
return new ThreadPoolExecutor(var0, var0, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
public LinkedBlockingQueue() {
this(2147483647);
}
核⼼线程和最⼤线程数⼀样,所以超时时间和超时单位没有意义了,但是使⽤了LinkedBlockingQueue⽆界队列⽆参构造,当任务⾮常多时根本没有执⾏拒绝策略的时候,⾮常危险。
2)、newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new Executors.FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()));
}
核⼼线程和最⼤线程⼀样,都是1,那么同样超时时间和超时单位就没有意义了,同样使⽤了⽆界队列存放任务,问题同样如上。
3)、newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new
SynchronousQueue());
}
队列是本⾝没什么问题,核⼼线程为0,超时时间和单位都正常。但是最⼤线程数为Integer.MAX_VALUE,当任务⾮常多时创建1万个线程服务器早就崩了(Java线程与内核线程⼀⼀对应)。
4)、newScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int var0) {
return new ScheduledThreadPoolExecutor(var0);
}
public ScheduledThreadPoolExecutor(int var1) {
super(var1, 2147483647, 0L, TimeUnit.NANOSECONDS, new
ScheduledThreadPoolExecutor.DelayedWorkQueue());
}
最⼤线程数也是Integer.MAX_VALUE,与上⾯存在相同相同的风险(线程创建过多,导致服务崩溃)。
所以,Executors创建的线程池,要不使⽤的队列太长要不可能创建的线程数太⼤,存下相当的风险。在项⽬上使⽤时最好直接使⽤ThreadPoolExecutor的全参数构造创建,⾃⼰⼼中有数,如下需要懂得核⼼参数和⼯作原理。
3、核⼼参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
this.mainLock = new ReentrantLock();
this.workers = new HashSet();
if (var1 >= 0 && var2 > 0 && var2 >= var1 && var3 >= 0L) {
if (var6 != null && var7 != null && var8 != null) {
this.maximumPoolSize = var2;
this.workQueue = var6;
this.keepAliveTime = Nanos(var3);
this.threadFactory = var7;
this.handler = var8;
} else {
throw new NullPointerException();
}
} else {
throw new IllegalArgumentException();
}
}
1)、核⼼线程
核⼼线程数可以理解为常驻线程,当任务来了之后马上就可以响应的快速反应部队,但是核⼼线程数也不能过多因为没有任务时也在占⽤资源。当然我们可以调⽤ allowCoreThreadTimeOut ⽅法允许核⼼线程在超时时间(超时单位)后进⾏销毁。当线程池创建后默认其中是没有创建线程池的,但是我
们可以调⽤prestartAllCoreThreads或 prestartCoreThread ⽅法来预热核⼼线程,⽽并不是等到接到第⼀个任务后才创建核⼼线程,对于抢购等性质的系统⽐较适⽤。
2)、最⼤线程
最⼤线程数很明细就是最多能创建的线程数,最⼤线程数字包含了核⼼线程数字。最⼤线程数的销毁跟超时时间和单位本⾝有关,但是最⼤线程数是在任务队列装满溢出的情况下才会创建,我有⼀年多时间理解的都是超过核⼼线程数就会创建。所以,这种机制并不适合纯io型任务,所以Tomcat的线程池就是基于juc线程池改造,超过核⼼线程就创建,超过最⼤线程才放⼊队列,队列满了才执⾏拒绝策略。
3)、超时时间和超时单位
那么在核⼼线程到最⼤线程数的线程,超时时间配合超时单位之后,就进⾏销毁。如上⾯说的,如果设置
了allowCoreThreadTimeOut则核⼼线程也可以根据该超时规则销毁。
4)、⼯作队列
⼯作队列是存放来不及处理的Callable或者Runnable回调任务,⼤多数情况下我们使⽤LinkedBlockingQueue是AQS的⼦类。队列本⾝也⽐较重要,当线程执⾏完任务后会到队列中获取,那么队列是否先进先出,性能等。这与任务本⾝的性质有关,如果不怕任务等太久或者饥饿,那么可以不使⽤先进先出;如果队列的任务都是瞬时任务量特别⾼,但是任务执⾏时间会⾮常短,那么可以把队列的长度设置的⼤⼀点。否则,队列放不下,⽽执⾏拒绝策略本⾝也是对应⽤的保护,对调⽤者的快速失败,也未必不好。
5)、线程创建⼯⼚
当需要创建新的线程时,则使⽤传⼊的⼯⼚直接创建,如果我们没有传⼊,则会使⽤默认线程
池,Executors.defaultThreadFactory();线程组使⽤了System或者当前线程池的,使⽤AtomicInteger来为创建的每个线程的名称后⾯加⼀个数字,但是创建的线程的前缀使⽤类似“pool-3-thread-1”这样的字样。但是我们在线上使⽤时,特别是在⽇志中查问题时,
给线程起⼀个有意义的名字是⾮常重要的。所以⼀般在项⽬上使⽤时,最好使⽤⾃⼰的 能创建具有业务意义的线程的线程⼯⼚。
static class DefaultThreadFactory implements ThreadFactory {
java线程池创建的四种
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager var1 = SecurityManager();
this.namePrefix = "pool-" + AndIncrement() + "-thread-";
}
}
6)、拒绝策略
juc提供了四种拒绝策略,默认为AbortPolicy;
1)、AbortPolicy【直接抛出异常给调⽤者处理】
public static class AbortPolicy implements RejectedExecutionHandler {
public AbortPolicy() { }
public void rejectedExecution(Runnable var1, ThreadPoolExecutor var2) {
throw new RejectedExecutionException("Task " + String() + " rejected from " + String());
}
}
2)、CallerRunsPolicy【让调⽤者⾃⼰执⾏】
之前的时候我⼀直认为返回给调⽤者⾃⼰执⾏,那么就不会丢弃认为,后⾯才意识到如果再交给调⽤者,并且调⽤者本⾝认为就⽐较重那么更加容易出问题。
public static class CallerRunsPolicy implements RejectedExecutionHandler {
public CallerRunsPolicy() {}
public void rejectedExecution(Runnable var1, ThreadPoolExecutor var2) {
if (!var2.isShutdown()) {
var1.run();
}
}
}
3)、DiscardOldestPolicy【丢弃最⽼的任务】
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() {}
public void rejectedExecution(Runnable var1, ThreadPoolExecutor var2) {
if (!var2.isShutdown()) {
}
}
}
4)、DiscardPolicy【丢⼈当前任务】
public static class DiscardPolicy implements RejectedExecutionHandler {
public DiscardPolicy() {}
// 丢弃当前任务就是⽅法体为空,什么都不做
public void rejectedExecution(Runnable var1, ThreadPoolExecutor var2) {}
}
4、⼯作原理
我有在项⽬上看见过,要执⾏⼀个并⾏⽅法时,创建线程池⽅法执⾏完就销毁的,那线程池的⼯作原理基本没有意义了。所以,线程池正常情况下是与项⽬的⽣命周期⼀致的启动如Tomcat时(或推迟到真的有任务调⽤时)创建,Tomcat等服务销毁时销毁。⼯作原理图:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论