说起线程池大家肯定不会陌生,在面试中属于必问的问题之一,特别是对于“高并发”有较高要求的企业,基本是必问点。网上关于线程池的文章和视频很多,本篇文章旨在帮助大家快速了解和掌握线程池的基本原理,对于高级应用不过多涉及。
并发队列是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。
并发队列分为阻塞队列和非阻塞队列,下面举例示意:
现有一个长度为10的队列,有11个元素需要放进去
示意图
入队时
非阻塞队列:当向队列中放入10个元素,此时队列已满,再放入第11个元素数据就会丢失。
阻塞队列:当队列已满了的时候,此时会进行等待,什么时候队列中有出队的元素,那么第11个再放进去。
出队时
非阻塞队列:如果队列中没有元素了,此时进行出队操作,往外取元素,得到的就是null。
阻塞队列:当队列中没有元素时,如果此时进行出队操作会等待,什么时候放进去,什么时候再取出来。
特别地,线程池就是基于阻塞队列实现的。
线程池是一种多线程处理形式,处理过程中将任务添加到队列,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务。执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
简单来说,线程池就是线程的集合。
线程的正常生命周期如下图所示:
为了便于分析,假设各阶段所花时间如上所示(当然线程各阶段实际所花时间极短,为毫秒级)。如果我们能省略其他阶段,每次线程直接运行任务,这样就可以单个线程处理任务就可以节省5秒。要实现这样的设想,我们可以使用线程池来处理,因为线程池中的线程是事先创建好的大量空闲线程,当队列中的任务进入线程池中,线程可以直接执行任务,执行完成后释放资源,继续处理下一任务。
举例来看:现有100个任务需要处理,一次最多创建10个线程。如果采用普通方式,一次创建10个线程处理10个任务,总共需60秒,而采用线程池的方式,一次执行10个任务,总共需要10秒。
综上所述:我们可以很明显的看出线程池在处理任务量极大的高并发系统中,具有很大的优势。
线程池的最上层接口是Executor,这个接口定义了一个核心方法execute(Runnablecommand),这个方法是用来传入任务的,最后被ThreadPoolExecutor类实现。而且ThreadPoolExecutor是线程池的核心类,此类的构造方法如下:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnitunit,BlockingQueue<Runnable>workQueue,RejectedExecutionHandler handler);
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
各个参数表示意义:
参数名 | 参数含义 |
---|---|
corePoolSize | 核心线程池大小,也即核心线程的数量 |
maximumPoolSize | 最大线程池大小,也即线程的最大数量 |
keepAliveTime | 空闲时间,是除核心线程之外的新创建线程的最大存活时间 |
TimeUnit | 时间单位 |
workQueue | 阻塞队列,用来存储等待的任务 |
threadFactory | 线程工厂,用来创建新线程 |
handler | 拒绝处理策略,当提交给线程池的任务量超过最大线程池大小+队列长度,就会采取拒绝处理策略 |
特别地说明:
workQueue一般有以下三种阻塞队列:
SynchronousQueue:直接提交,默认使用队列
ArrayBlockingQueue:有界队列
LinkedBlockingQueue:无界队列
threadFactory是当队列已满,但线程总数量<最大线程池大小时,线程池中用来创建新线程的线程工厂。一般有下列三种类型:
ArrayBlockingQueue:有界线程安全的阻塞队列。
LinkedBlockingQueue:并发安全的阻塞队列。
SynchronousQueue:同步队列。
handler触发时,有以下四种拒绝处理策略:
hreadPoolExecutor.AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
接下来就通过一个简单的实例并结合原理图来了解线程池的基本原理:
public class test02 { public static void main(String[] args) { ThreadPoolExecutor pool = new ThreadPoolExecutor(1,2,3, TimeUnit.SECONDS, new LinkedBlockingDeque<>(3)); //利用线程池中的线程开始执行任务 //执行第一个任务 pool.execute(new TestThread());
//队列有三个任务等待 pool.execute(new TestThread()); pool.execute(new TestThread()); pool.execute(new TestThread()); //执行第五个任务 pool.execute(new TestThread()); //执行第六个任务,拒绝任务报错 //pool.execute(new TestThread()); //当前线程池中有2个线程:1个核心线程 + 1个新创建的线程 = 最大线程数 //关闭线程池 pool.shutdown(); }
}
class TestThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
首先创建一个最简单类型的线程池,构造方法只有五个参数,每个参数意义如下:
1:核心线程数
2:最大线程数
3:空闲时间。新创建的线程执行任务后等待新任务的空闲时间
TimeUnit.SECONDS:时间单位,秒
new LinkedBlockingDeque:阻塞队列,长度为3
不执行第6条任务时的执行结果如下:
执行第6条任务时执行结果如下:
分析代码执行过程:
现有一线程池,里面只有一个核心线程thread1,第一个任务进入线程池中,由thread1执行,而2-4号线程处在队列中等待执行,当5号任务提交时,根据原理图,此时满足队列已满,且核+新<=最大,所以创建新线程thread2,由thread1和thread2分摊执行任务,由运行结果也可以看出,确实是分摊任务。
当加上第6条的任务时,根据原理图,此时队列已满,且核+新>最大,没有多余的线程执行任务,队列也无法装入,就会报错,拒绝任务。
线程池可分为以下四类:
作用:创建一个根据需要创建新线程池的线程池。当旧线程释放资源后就可以使用旧线程。
特点:线程数灵活最大值为INTER.MAX_VALUE,底层采用一个近似无边界队列
作用:创建一个可重用固定线程数的线程池,以共享的无界队列来运行这些线程。
特点:线程处于一定量,可以很好的控制并发量
作用:创建一个可延迟或延期运行的线程池。
特点:线程池中具有指定数量的线程,可定时或延迟执行,适用于周期性执行任务的场景。