一、ThreadPoolExecutor:
线程池实现类ThreadPoolExecutor是Executor框架最核心的类,继承自Executor接口,里面有一个execute方法,用来执行线程,线程池主要提供一个队列,队列中保存着所有等待状态的线程。
ThreadPoolExecutor类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法基础上产生的。
public ThreadPoolExecutor(int corePoolSize,//核心线程数
int maximumPoolSize,//最大线程数
long keepAliveTime,//空闲存活时间
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//线程池任务队列
ThreadFactory threadFactory,//创建线程的工厂
RejectedExecutionHandler handler) {//拒绝策略
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), handler);
}
参数介绍:
1、corePoolSize 核心线程数: 是指线程池中长期存活的线程数(如果当前核心线程数没有达到corePoolSize,即使当前核心线程池有空闲线程,也会创建新的线程来执行提交任务。如果调用了prestartCoreThread()
或者prestartAllCoreThread()
,线程池创建的时候所有的核心线程都会被创建并启动);
2、maximumPoolSize 最大线程数: 线程池允许创建的最大线程数量,当线程数池的任务队列满了之后,可以创建的最大线程数;
注意:最大线程数 maximumPoolSize 的值不能小于核心线程数 corePoolSize,否则在程序运行时会报 IllegalArgumentException 非法参数异常。
3、keepAliveTime,空闲线程存活时间: 如果当前线程池的线程个数已经超过了corePoolSize,并且线程空闲时间超过了keepAliveTime的话,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗;
4、TimeUtil,时间单位: 空闲线程存活时间的描述单位,此参数配合参数3使用;
TimeUtil有7个参数:
- TimeUnit.DAYS:天
- TimeUnit.HOURS:小时
- TimeUnit.MINUTES:分
- TimeUnit.SECONDS:秒
- TimeUnit.MILLISECONDS:毫秒
- TimeUnit.MICROSECONDS:微妙
- TimeUnit.NANOSECONDS:纳秒
5、BlockingQueue 阻塞队列: 线程池存放任务的队列,用来存储线程池的所有待执行任务。
它可以设置以下几个值:
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列,按照先入先出的原则对元素进行排序,不保证线程公平访问队列,使用较少。
- LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列,队列默认和最长长度为Integer.MAX_VALUE,队列按照先入先出的原则对元素进行排序,使用较多。
- 吞吐量通常要高于ArrayBlockingQueue
- Executors.newFixedThreadPool()使用了这个队列
- SynchronousQueue:一个不存储元素(无容量)的阻塞队列,即直接提交给线程不保持它们,每一个put操作都必须等待一个take操作,否则不能继续添加元素。支持公平访问队列,常用于生产者消费者模型,吞吐量较高,使用较多。
- 每个插入操作必须等到另一个线程的移除操作,否则插入操作一直处于阻塞状态
- 吞吐量通常要高于LinkedBlockingQueue
- Executors.newCachedThreadPool使用了这个队列
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列,使用较少。
- DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素。
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
比较常用的是LinkedBlockingQueue,线程池的排队策略和BlockingQueue息息相关。
6、ThreadFactory 线程工厂: 线程池创建线程调用的工厂方法,通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等。
线程工厂的使用实例如下:
public static void main(String[] args) {
//创建线程工厂
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
//创建线程池中的线程
Thread thread = new Thread(r);
//设置线程名称
thread.setName("Thread-" + r.hashCode());
//设置线程优先级(最大值:10)
thread.setPriority(Thread.MAX_PRIORITY);
//......
return thread;
}
};
//创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10,
0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), threadFactory);
//使用自定义的线程工厂
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread();
System.out.println(String.format("线程:%s,线程优先级:%d", thread.getName(), thread.getPriority()));
}
});
}
以上程序运行结果:
线程:Thread-495623552,线程优先级:10
Process finished with exit code -1
从上述执行结果可以看出,自定义线程工厂起作用了,线程的名称和线程优先级都是通过线程工厂设置的。
7、RejectedExecutionHandler 拒绝策略: 当线程池任务超出线程池队列可以存储的最大值后,执行的策略。
默认有以下四种策略:
- AbortPolicy:拒绝并抛出异常;
- CallerRunsPolicy:使用当前调用的线程来执行此任务;
- DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务;
- DiscardPolicy:忽略并抛弃当前任务。
线程池的默认策略是:AbortPolicy(拒绝并抛出异常)。
二、工作过程:
1、通过execute方法添加任务时,如果工作线程数小于corePoolSize,则创建一个新线程并执行该任务。注:新创建的线程会通过while循环不断从阻塞队列获取任务执行。
2、如果工作线程数大于等于corePoolSize,则将任务添加到阻塞队列。
3、如果阻塞队列满了,则判断工作线程是否小于maximumPoolSize,如果小于则创建新线程并执行该任务,否则调用handler拒绝策略。
三、执行任务逻辑:(原码分析)
线程调用runWork方法,通过while循环从getTask方法获取任务并执行。只要getTask方法不返回null,线程就不会退出。
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
四、获取任务过程:
getTask方法实际上是从workQueue阻塞队列取任务。如果工作线程数大于corePoolSize,则会调用workQueue的poll方法获取任务,超时时间是keepAliveTime,如果超过了keepAliveTime时长,poll返回了null,上面提到的while循环就会退出,线程也就退出了。如果工作线程数小于等于corePoolSize,则会调用workQueue的take方法阻塞在当前直到有新的任务进来。
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
五、使用方法:
1、直接new一个对象:
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
8, 8, 0, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(30),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
2、通过Spring配置:
<bean id="prestoTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="4"/>
<property name="maxPoolSize" value="8"/>
<property name="keepAliveSeconds" value="200"/>
<property name="queueCapacity" value="100"/> <!-- 拒绝策略 -->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy"/>
</property>
<property name="threadNamePrefix" value="prestoThread"/>
</bean>
六、合理的配置线程池:
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
- 任务的性质:CPU密集型任务,IO密集型任务和混合型任务;
- 任务的优先级:高、中和低;
- 任务的执行时间:长、中和短;
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
任务性质不同的任务可以用不同规模的线程池分开处理:
- CPU密集型任务配置尽可能少的线程数量,如配置CPU个数+1的线程数的线程池。
- IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如配置两倍CPU个数+1。
- 混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。
并且,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。
当然具体合理线程池值大小,需要结合系统实际情况,在大量的尝试下比较才能得出,以上只是前人总结的规律。
最佳线程数目 = (线程等待时间+线程CPU时间) / 线程CPU时间 * CPU数目
比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。这个公式进一步转化为:
最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
线程池大小最好是corePoolSize和maximumPoolSize保持一致,不要超过CPU核心数的3倍,如果纯计算任务(包括同机房的redis服务),只有很少的网络或者磁盘IO,那么线程池的大小设置为CPU的2倍比较合理。