线程池
参考:https://www.cnblogs.com/jiawen010/p/11855768.html
一、线程池简介
1. 线程池的概念
线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好的提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束后,该线程并不会死亡,而是再次返回线程池中,成为空闲状态,等待执行下一个任务。
2. 线程的工作机制
在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,就把任务交给某个空间的线程。
一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
3. 使用线程池的原因
- 多线程运行时,系统不断的创建和关闭新线程,成本非常高,会过度消耗系统资源
- 提高系统响应速度,当有任务到达时,通过复用已有的线程,无需等待新线程的创建就能立即执行
- 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生 OOM,并且会造成 cpu 过度切换,影响系统性能
二、线程池主要参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) { this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler); }
1. corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于 corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于 corePoolSize 时。除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。
2. maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于 maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略此参数。
3. keepAliveTime(线程存活保持时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
4. workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。
5. threadFactory(线程工厂):用于创建新线程。threadFactory 创建的线程也是采用 new Thread() 方式,threadFactory 创建的线程名都有统一的风格:pool-m-thread-n (m 为线程池的编号, n 为线程池内线程编号)。
6. handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略。
线程池流程:判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则判断任务队列(等待线程执行的任务的队列)是否已满,没满则将新提交的任务添加在工作队列,已满则判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和策略。
分别判断:核心线程池 -> 工作队列 -> 整个线程池
三、线程池为什么要使用阻塞队列而不使用非阻塞队列?
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入 wait 状态,释放 CPU 资源。
当队列中有任务时,才唤醒对应线程从队列中取出消息进行执行。
使得线程不至于一直占用 cpu 资源。
四、如何配置线程池
CPU 密集型任务:尽量使用较小的线程池,一般为 CPU 核心数 +1。因为 CPU 密集型任务使得 CPU 使用率很高,若开过多的线程数,会造成 CPU 过度切换。
IO 密集型任务:可以使用稍大的线程池,一般为 2*CPU 核心数。IO 密集型任务 CPU 使用率并不高,因此可以让 CPU 在等待 IO 的时候由其他线程去处理别的任务,充分利用 CPU 时间。
混合型任务:可以将任务分成 IO 密集型和 CPU 密集型任务,然后分别用不同的线程池去处理。只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。因为如果划分后两个任务执行时间有数量级的差距,那么拆分没有意义。先执行完的任务要等后执行完的任务,最终的时间仍取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。(并不很理解)
五、四种常见的线程池详解
线程池的返回值 ExecutorService 是 Java 提供的用于管理线程池的类。该类有两个作用:控制线程数量和重用线程
1. Executors.newCacheThreadPool():可缓存线程池,先查看池中有没有以前建立的线程,如果有,就直接使用,如果没有,就建一个新的线程加入池中,缓存型池子通常用于执行一些生存期很短的异步型任务。
public class Test { public static void main(String[] args) { ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); for(int i=0; i<10; i++){ try{ Thread.sleep(1000); }catch (InterruptedException e){ e.printStackTrace(); } cachedThreadPool.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + " 正在被执行 "); try { Thread.sleep(2000); }catch (InterruptedException e){ e.printStackTrace(); } } }); } } }
这个线程池为无限大,当执行当前任务时,上一个任务已完成,会复用执行上一个任务的线程,而不是每次新建线程。
感觉有点问题,在任务执行特别多速度特别慢的时候,这里的线程池中线程数量就会急剧上升
2. Executors.newFixedThreadPool(int i):创建一个可重用固定个数的线程池,以共享的无界队列方式来运行这些线程。
public static void main(String[] args) { ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); for(int i=0; i<10; i++){ fixedThreadPool.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + " 正在被执行"); try{ Thread.sleep(2000); } catch(InterruptedException e){ e.printStackTrace(); } } }); } }
因为线程池大小为 3,每个任务输出打印结果后 sleep 2秒,所以每两秒打印 3 个结果。
定长线程池的大小最好根据系统资源进行设置。如 Runtime.getRuntime().availableProcessors()。
3. Executors.newScheduledThreadPool(int n):创建一个定长线程池,支持定时及周期性任务执行
public static void main(String[] args) { ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5); scheduledThreadPool.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("延迟 1 秒后每 3 秒执行一次"); } }, 1, 3, TimeUnit.SECONDS); }
4. Executors.newSingleThreadExecutor():创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有的任务按照指定顺序(FIFO,LIFO,优先级)执行。
public static void main(String[] args) { ExecutorService singleThreadPool = Executors.newSingleThreadExecutor(); for(int i=0; i<10; i++){ final int index = i; singleThreadPool.execute(new Runnable() { @Override public void run() { try{ System.out.println(Thread.currentThread().getName() + " 正在执行,打印的值是:" + index); Thread.sleep(500); }catch (InterruptedException e){ e.printStackTrace(); } } }); } }
六、缓冲队列 BlockingQueue 和自定义线程池 ThreadPoolExecutor
1. 缓冲队列 BlockingQueue 简介
BlockingQueue 是双缓冲队列。BlockingQueue 内部使用两条队列,允许两个线程同时向列队 一个存储一个取出操作。在保证并发安全的同时,提高了队列的存取效率。
2. 常用的几种 BlockingQueue
- ArrayBlockingQueue(int i):规定大小的 BlockingQueue,其构造必须指定大小。其所含的对象是 FIFO 顺序排序的
- LinkedBlockingQueue() 或者 (int i):大小不固定的 BlockingQueue,如果其构造时指定大小,生成的 BlockingQueue 有大小限制,不指定大小,其大小由 Integer.MAX_VALUE 来决定。其所含的对象是 FIFO 顺序排序的
- PriorityBlockingQueue() 或者 (int i):类似于 LinkedBlockingQueue,但是其所含对象的排序不是 FIFO,而是依据对象的自然顺序或者构造函数的 Comparator 决定
- SynchronizedQueue():特殊的 BlockingQueue,对其的操作必须是放和取的交替完成
3. 自定义线程池(ThreadPoolExecutor 和 BlockingQueue 连用)
自定义线程池,可以用 ThreadPoolExecutor 来创建,它有多个构造方法来创建线程池
public class MyThreadPoolExecutor { public static class Temphread implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + " 正在执行"); try{ Thread.sleep(1000); }catch (InterruptedException e){ e.printStackTrace(); } } } public static void main(String[] args) { BlockingQueue<Runnable> bq = new ArrayBlockingQueue<>(10); ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 6, 50, TimeUnit.SECONDS, bq); Runnable t1 = new Temphread(); Runnable t2 = new Temphread(); Runnable t3 = new Temphread(); Runnable t4 = new Temphread(); Runnable t5 = new Temphread(); Runnable t6 = new Temphread(); executor.execute(t1); executor.execute(t2); executor.execute(t3); executor.execute(t4); executor.execute(t5); executor.execute(t6); executor.shutdown(); } }