说起线程池大家肯定不会陌生,在面试中属于必问的问题之一,特别是对于“高并发”有较高要求的企业,基本是必问点。网上关于线程池的文章和视频很多,本篇文章旨在帮助大家快速了解和掌握线程池的基本原理,对于高级应用不过多涉及。
一、并发队列
1. 并发队列概念
并发队列是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,当我们添加一个元素的时候,它会添加到队列的尾部,当我们获取一个元素时,它会返回队列头部的元素。
两种队列区别
入队时
非阻塞队列:当向队列中放入10个元素,此时队列已满,再放入第11个元素数据就会丢失。
阻塞队列:当队列已满了的时候,此时会进行等待,什么时候队列中有出队的元素,那么第11个再放进去。
出队时
非阻塞队列:如果队列中没有元素了,此时进行出队操作,往外取元素,得到的就是null。
阻塞队列:当队列中没有元素时,如果此时进行出队操作会等待,什么时候放进去,什么时候再取出来。
特别地,线程池就是基于阻塞队列实现的。
二、线程池简介
线程池是一种多线程处理形式,处理过程中将任务添加到队列,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务。执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。
简单来说,线程池就是线程的集合。
三、为什么需要线程池
为了便于分析,假设各阶段所花时间如上所示(当然线程各阶段实际所花时间极短,为毫秒级)。如果我们能省略其他阶段,每次线程直接运行任务,这样就可以单个线程处理任务就可以节省5秒。要实现这样的设想,我们可以使用线程池来处理,因为线程池中的线程是事先创建好的大量空闲线程,当队列中的任务进入外汇返佣线程池中,线程可以直接执行任务,执行完成后释放资源,继续处理下一任务。
举例来看:现有100个任务需要处理,一次最多创建10个线程。如果采用普通方式,一次创建10个线程处理10个任务,总共需60秒,而采用线程池的方式,一次执行10个任务,总共需要10秒。
综上所述:我们可以很明显的看出线程池在处理任务量极大的高并发系统中,具有很大的优势。
四、线程池的原理
1. ThreadPoolExecutor核心类
线程池的最上层接口是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);
特别地说明:
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条的任务时,根据原理图,此时队列已满,且核+新>最大,没有多余的线程执行任务,队列也无法装入,就会报错,拒绝任务。
五、线程池的分类
线程池可分为以下四类:
1. 可缓存:newCachedThreadPool
作用:创建一个根据需要创建新线程池的线程池。当旧线程释放资源后就可以使用旧线程。
特点:线程数灵活最大值为INTER.MAX_VALUE,底层采用一个近似无边界队列
2. 定长:newFixedThreadPool
作用:创建一个可重用固定线程数的线程池,以共享的无界队列来运行这些线程。
特点:线程处于一定量,可以很好的控制并发量
3. 定时:newScheduleThreadPool
作用:创建一个可延迟或延期运行的线程池。
特点:线程池中具有指定数量的线程,可定时或延迟执行,适用于周期性执行任务的场景。
4. 单例:newSingleThreadExecutor
作用:创建一个只有一个线程的线程池。且线程的存货时间是无限的,当该线程正繁忙时,对于新任务会进入无界的阻塞队列中。
特点:适用于一个一个任务执行的场景。
线程池四种创建方式
①newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
public static void main(String[] args) {
//可以缓存的线程池
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 100; i++) {
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(Thread.currentThread().getId());
}
});
}
}
线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
②newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
public static void main(String[] args) {
// 控制并发数的线程池
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
newFixedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
}
③newScheduledThreadPool
创建一个定长线程池,支持定时及周期性任务执行。
public static void main(String[] args) {
// 可以定时线程池
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3);
for (int i = 0; i < 10; i++) {
newScheduledThreadPool.schedule(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName());
}
}, 3, TimeUnit.SECONDS);//延迟3秒执行
}
}
④newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int x = i;
newSingleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":"+x);
}
});
}