初学线程池
参考
https://www.jianshu.com/p/7726c70cdc40
https://www.jianshu.com/p/47e903ab1bec
https://www.imooc.com/article/51147
https://blog.youkuaiyun.com/weixin_40271838/article/details/79998327
下面来了解一下什么是线程池。
由于我们在应用程序中使用线程时,可能需要多次创建并摧毁线程,这样的操作会导致内存资源的消耗。为了减少内存等系统资源的消耗,方便对内存的管理,Java提供了一种管理线程的概念——线程池。线程池由任务队列和工作线程组成,它可以重用线程来避免线程创建的开销,在任务过多时通过排队避免创建过多线程来减少系统资源消耗和竞争,确保任务有序完成。
1. 常见的线程池
超级接口Executor的继承图如下:
我们也可以通过Executors类的方法来创建不同功能的线程池:
(1)|newCachedThreadPool
用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
(2)newFixedThreadPool
创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
(3)newSingleThreadExecutor 创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
(4)newScheduledThreadPool 适用于执行延时或者周期性任务。
2. 线程池的优势
(1)降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
(3)方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或OOM(Out Of Memory)等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提高资源使用率;
(4)线程池还提供了定时、定期以及可控线程数等功能的线程池,功能更加强大。
3. 线程池的主要参数
ThreadPoolExecutor的构造:
我们来看最长的一个:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
(1)corePoolSize
线程池中的核心线程数,默认情况下,核心线程一直存活在线程池中,即便他们在线程池中处于闲置状态。除非我们将ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这时候处于闲置的核心线程在等待新任务到来时会有超时策略,这个超时时间由keepAliveTime来指定。一旦超过所设置的超时时间,闲置的核心线程就会被终止。
(2)maximumPoolSize
线程池中所容纳的最大线程数,如果活动的线程达到这个数值以后,后续的新任务将会被阻塞。包含核心线程数+非核心线程数。
(3)keepAliveTime(当线程等待任务的时间超过keepAliveTime将会被摧毁)
非核心线程闲置时的超时时长,对于非核心线程,闲置时间超过这个时间,非核心线程就会被回收。只有对ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这个超时时间才会对核心线程产生效果。
(4)unit
public enum TimeUnit
用于指定keepAliveTime参数的时间单位。可使用的单位有天、小时、分、秒、毫秒、微秒。
(5)workQueue
线程池中保存等待执行的任务的阻塞队列。通过线程池中的execute方法提交的Runable对象都会存储在该队列中。我们可以调用BlockingQueue的实现类如LinkedBlockingQueue、ArrayBlockingQueue、SynchronousBlockingQueue(仅在试图要移除元素时,该元素才存在,另一个线程才会插入元素)、PrioriBlockingQueue(具有优先级的无限阻塞队列),也可以通过实现BlockingQueue接口来自定义阻塞队列。
(6)threadFactory
此接口为线程池创建新线程。默认为DefaultThreadFactory类。
(7)handler(线程池workQueue的饱和策略,针对于有限的阻塞队列)
在ThreadPoolExecutor中有四个内部类实现了RejectedExecutionHandler接口:
1)CallerRunsPolicy:
2)AbortPolicy:
3)Discardpolicy:
4)DiscardOldestPolicy:
我们可以直接使用这4个RejectedExecutionHandler接口的实现类,也可以implements该接口自定义一个handler。
我们可以看下图小总结一下:
4. 线程池的执行流程
5. 配置线程池
(1)CPU密集型任务:
建议尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
(2)IO密集型任务:
建议可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
(3)混合型任务:
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。
6. 线程池的使用
/**
* @author BlueNfish
*/
public class ThreadPool {
/*
* 获取Java虚拟机返回的可用处理器的数目。例如1-4-2,即一个CPU插槽,每个CPU4个内核,2个超线程,则应该得到1*4*2=8,
* 但并不是所有的机器都能得到预期的数字。正如 Brian Goetz所指出的,“虚拟机其实不清楚什么是处理器,它只是去请求操作
* 系统返回一个值。同样的,操作系统也不知道怎么回事,它是去问的硬件设备。硬件会告诉它一个值,通常来说是硬件线程数。操作系
* 统相信硬件说的,而虚拟机又相信操作系统说的。”
*/
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// 最大核心线程数。
private static final int CORE_POOL_SIZE = Math.max(4, Math.min(CPU_COUNT-1, 5));
// 最大线程池容量。
private static final int MAX_POOL_SIZE = 10;
// 空闲超时摧毁时间。
private static final long KEEP_ALIVE_TIME = 2;
// 时间的单位。
private static final TimeUnit TIME_UNIT = TimeUnit.SECONDS;
// 工作队列。
private static final BlockingQueue<Runnable> WorkQueue = new LinkedBlockingQueue<>(10);
// 当执行的的任务数超过MAX_POOL_SIZE + workQueue.size()时,会抛出异常。可以适当调整workQueue的size。
private static ThreadPoolExecutor Thread_Pool_Executor = null;
static {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TIME_UNIT, WorkQueue,
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(false);
return t;
}
},new ThreadPoolExecutor.AbortPolicy()); // Abort策略是定义在ThreadPoolExecutor中的内部类。
threadPoolExecutor.allowCoreThreadTimeOut(true); // 设置核心线程在等待任务的时间超过keepAliveTime后会被摧毁。
Thread_Pool_Executor = threadPoolExecutor;
}
public void TaskFactory(ThreadPoolExecutor tpe, int taskCount) {
for(int i=0;i<taskCount;i++) {
Runnable r = new Runnable() {
@Override
public void run() {
String taskID = Integer.toString((int)Math.floor(Math.random()*(Math.pow(2, 31)-1)));
if(taskID.length()<Integer.toString(Integer.MAX_VALUE).length()) {
for(int i=0;i<Integer.toString(Integer.MAX_VALUE).length()-taskID.length()+1;i++) {
taskID = "0" + taskID;
}
}
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()
+"执行"+" Task"+taskID+" 完毕。");
} catch (Exception e) {
e.printStackTrace();
}
}
};
tpe.execute(r); // 把任务交给线程池。
}
}
public static void main(String[] args) {
ThreadPool tp = new ThreadPool();
tp.TaskFactory(Thread_Pool_Executor, 20);
}
}
当taskCount<= MAX_POOL_SIZE + workQueue.size()时,我们得到如下结果:
当taskCount> MAX_POOL_SIZE + workQueue.size()时,抛出RejectedExecutionException,但线程池依然执行任务。
7. 线程池为什么要使用阻塞队列?
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程wait而释放cpu资源。当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。使得在线程不至于一直占用cpu资源。
(线程执行完任务后通过循环再次从任务队列中取出任务进行执行,代码片段如下
while (task != null || (task = getTask()) != null) {})。
8. execute()和submit()
execute()执行一个任务没有返回值。submit()有三种返回值。
submit()和Future可以参考:
https://www.cnblogs.com/dolphin0520/p/3949310.html。