线程池的作用
线程池的作用是用来控制系统为某一任务运行的线程(Thread)的数量,处理过程中将任务(Runnable)放入队列,然后在线程创建后启动这些任务,如果工作线程的数量超过线程池允许的最大数量,那么超过数量的任务在队列中排队等候,等待其他线程执行完毕,再从队列中取出任务执行。
线程池的特点
- 线程复用
- 控制最大并发数
- 管理线程
线程池的构成
- 线程池管理器:用于创建并管理线程池
- 工作线程:线程池中执行任务的线程
- 任务:每个任务必须实现Runnable接口,这样才能被调度执行
- 任务队列:用于存放待处理的任务,通常是阻塞队列
java中线程池的实现
java中的线程池是通过Executor框架实现的,包含了Executor、Executors、ExecutorService、ThreadPoolExecutor、Callable和Future、FutureTask等。
- Executor:顶层接口,内部包含一个execute方法。
- ExecutorService:继承Executor,增加了提交任务和关停线程池相关方法。
- AbstractExecutorService:实现了ExecutorService。
- ThreadPoolExecutor:线程池核心类,各线程池本质就是初始化了不同参数的ThreadPoolExecutor。
- Executors:工厂类,提供了初始化各种线程池的方法。
- Callable和Future、FutureTask等是为了获取多线程执行结果的类,上一篇文章有述。
ThreadPoolExecutor对象的属性
- corePoolSize:线程池的核心线程数。
- maximumPoolSize:线程池的最大线程数。
- keepAliveTime:线程空闲的存活时间,当线程池中线程数量超过corePoolSize时,多余的空闲线程的存活时间,即多长时间会被销毁。
- unit:keepAliveTime的单位。
- workQueue:任务队列,用来保存尚未被执行的任务。
- threadFactory:线程工厂,用于创建线程。
- handler:拒绝策略,当任务来不及处理的时候,如何拒绝任务。
线程池的原理
- 线程池被初始化时,里面没有线程,当提交一个任务时,线程池创建一个新的线程执行任务,继续提交任务继续创建,直到当前线程数等于corePoolSize;如果当前线程数等于corePoolSize,且继续提交任务,继续提交的任务将会被保存到阻塞队列中,等待被执行。如果创建线程池时调用prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
- 继续提交任务,导致阻塞队列也满了的时候,再提交任务,那么如果当前线程数小于maximumPoolSize,会继续创建线程立刻运行这个任务。
- 如果阻塞队列满了,且当前线程数大于等于maximumPoolSize且继续提交任务,这时需要一种拒绝策略来处理超额提交的任务。
- 如果线程无事可做,超过一定时间(keepALiveTime)时,线程池会判断,如果当前线程数大于corePoolSize,那么这个线程会被回收。所以当线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
线程池的拒绝策略
- AbortPolicy:直接抛出异常,阻止系统正常运行。
- CallerRunsPolicy:直接在调用者线程中运行当前被丢弃的任务,可能会导致提交线程的性能急剧下降。
- DiscardOldestPolicy:丢弃最老的一个请求,也就是丢弃即将被执行的一个任务,并且再次提交当前任务。
- DiscardPolicy:立即丢弃当前提交的任务,不做任何处理,如果允许任务丢失,这是一种最好的方案。
- 自定义RejectedExecutionHandler接口,例如记录日志,发送报警等。
java中的四种线程池
- newFixedThreadPool
创建一个固定线程数的线程池,其中corePoolSize等于maximumPoolSize,使用LinkedBlockingQueue阻塞队列作为阻塞队列,线程未被显示关闭之前一直存在,也就是说线程池没有任务可执行时,也不会释放线程。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
- newCachedThreadPool
创建一个可以缓存线程的线程池,内部使用SynchronousQueue作为阻塞队列,由于SynchronousQueue无界阻塞队列不存储元素,因此线程池中的线程数可以达到Integer.MAX_VALUE,当提交新任务时,会重用以前的线程,如果没有可用的,则创建一个线程并添加到线程池中;如果一个线程空闲且超过keepAliveTime(默认是60s),则会被回收。因此,长时间保持空闲的线程池也不会使用任何资源,适用于很多短期任务的执行场景。public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
- newScheduledThreadPool
创建一个线程池,可以在给定延迟后执行任务或定期的执行任务,用法如下
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
scheduledThreadPool.schedule(newRunnable() {
@Override
public void run() {
System.out.println("延迟三秒");
}
}, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){
@Override
public void run() {
System.out.println("延迟 1 秒后每三秒执行一次");
}
},1,3,TimeUnit.SECONDS);
- newSingleThreadExecutor
初始化一个单线程的的线程池,内部使用LinkedBlockingQueue作为阻塞队列,如果该线程异常结束,会创建一个新的线程继续执行任务。唯一的线程可以保证提交任务的顺序执行。public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
阻塞队列
- ArrayBlockingQueue -
公平
由数组结构实现的有界阻塞队列。元素按照先进先出(FIFO)的元素排序,初始化时必须指定队列容量。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞所有生产者线程或消费者线程时,可以按照阻塞的顺序访问队列。即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。可以在初始化时创建一个公平的阻塞队列:ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
- LinkedBlockingQueue -
两个独立锁提高并发
基于链表的阻塞队列,按照FIFO的元素对元素进行排序,若无指定,队列默认初始化容量为Integer.MAX_VALUE。性能比ArrayBlockingQueue高,原因是其对于生产者端和消费者端分别采用了独立的锁来控制数据的同步,在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。 - PriorityBlockingQueue - 支持排序优先级的无界队列
默认情况下元素采用自然顺序升序排列,若非指定,默认队列容量为11。可以自定义元素的compareTo方法或在创建队列时传图Comparator比较器来实现内部元素的自定义排序。注意,不能保证同优先级元素的顺序。 - SynchronousQueue -
不存储元素,可用于传递数据
是一个不存储元素的阻塞队列,默认是非公平的,可以在初始化的时候指定公平性。每一个put操作必须等待一个take操作,否则不能继续添加元素元素。SynchronousQueue负责把生产者线程处理的数据传递给消费者线程,队列本身不存储任何元素。吞吐量要高于LinkedBlockingQueue和ArrayBlockingQueue。 - DelayQueue -
用户缓存失效,定时任务
是一个支持延迟获取元素的无界阻塞队列,队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素。主要应用场景如下:- 缓存系统设计:可以用DelayQueue来保存缓存元素的有效期,用另一个线程轮循DelayQueue,一旦能从DelayQueue中获取元素,就表示缓存到期了。
- 定时任务调度:使用DelayQueue保存当天将会执行的任务和延迟时间,一旦能从DelayQueue中获取任务就开始执行。
- LinkedTranferQueue -
增加非阻塞方法
由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,增加了tryTransfer和transfer方法。- transfer方法:如果当前有消费者正在等待接受元素,transfer方法可以直接把生产者传入的元素立刻transfer给消费者。如果没有消费者等待接受元素,transfer会将元素放在队列的tail节点,并等到该元素被消费了才返回。
- tryTransfer方法:用来试探生产者传入的元素能否直接传递给消费者。如果没有消费者等待接受元素则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接受,方法立即返回。而transfer方法必须等到消费者消费了才返回。
- 带有时间限制的tryTransfer方法则试图把生产者传入的元素直接传递给消费者,如果没有消费者消费该元素则等待指定的时间再返回;如果超时还没消费元素,则返回false;如果在超时时间内消费了元素,则返回true。
tryTransfer(E e, long timeout, TimeUnit unit)
- LinkedBlockingDeque -
双端阻塞队列
是一个由链表结构组成的双端阻塞队列。可以从队列的两端插入和移除元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque增加了addFirst、addLast、offerFirst、offerLast、peekFirst、peekLast等方法,以First单词结尾的方法标识操作队列的第一个元素;以Last单词结尾的方法,标识操作队列的最后一个元素。双向队列可以运用在”工作窃取“模式中。