线程池深入浅出
线程池在高并发编程中是一定会使用到的!接下来一起看看吧
一、什么是线程池?
线程池从名字上来看就是存放线程的池子,那为什么需要它?或者它有什么优势?
线程的创建和销毁是一件非常消耗资源的操作。
有时我们实际的业务还没有线程的创建开销大,
线程池的作用便是提前创建好一些线程,我们
需要的时候直接来取,使用完毕后不是关闭,
而是将线程归还到池子中。这样我们便
节省了线程的创建和销毁!提高系统的性能
有了线程池后
二、线程池原理
流程:
流程:
向线程池中添加任务时,线程池进行如下判断
1.是否达到最大核心线程数?若没有则创建线程处理任务,反之则判断工作对列是否满?
2.若工作队列未满则当前任务加入工作队列中,反之判断是否达到最大核心数
3.若未达到最大核心数,则创建线程处理任务。反之使用饱和策略处理
帮助理解:
假如一个公司有5个人比作核心线程数,当老板下达新任务时如果有的人是空闲状态
那么就去处理这个任务。如果这5个人都在工作,那么就先把该任务放下等工作结束
后就去做。可是你想想长时间我们也不干啊,我们也是有“底线的”。一旦超过
这个底线怎么办?我们可以在招人来做!但是招人也是有限度的,老板也不愿意招
太多的人啊。如果没有超过人数限制我们就招人处理。一旦空闲下来,为了节约成本
会将后来的人撤退的!!!有种卸磨杀驴的感觉……
三、线程池的核心api
线程池的具体实现类是ThreadPoolExecutor
通过上边的小故事,会更容易理解参数的意义
ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
其中有四个参数是必须参数:
--------------------必需参数-----------------------------------
int corePoolSize
核心线程数,注意:当任务提交到线程池中时,会创建一个线程去处理任务
即时有空闲的线程,除非工作线程大于核心线程了prestartAllCoreThreads()方法会提前创建好核心线程
int maximumPoolSize
线程池允许的最大线程数,当工作队列已满,并没有的达到最大线程数时将会创建新的线程
long keepAliveTime
线程池的工作线程空闲时,存活的时间。如果线程处于空闲状态,并超过了这个时间将会被回收
如果任务较多的话,或者执行时间较短的话。我们适当增加这个数值,来避免线程频繁的回收和创建
TimeUtil util
keepAliveTime的单位。就是一个时间单位,可以是时分秒……
workQueue
缓存待处理任务的阻塞队列(好比你在工作,交给你新任务的最大可接受程度和策略)
-------------------可选参数------------------------------------------
threadFactory
线程池中创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字
handler
饱和策略,当线程池无法处理新来的任务了,那么需要提供一种策略处理提交的新任务
任务的执行:
execute() 方法处理任务
注:任务处理的流程遵守上述流程
线程池的使用步骤:
1.创建线程池对象
2.执行任务
3.关闭线程池
简单案例:
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(3,5,10, TimeUnit.SECONDS,new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory());
for (int i = 0; i < 10; i++) {
poolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"得到执行");
}
});
}
poolExecutor.shutdown();
}
}
四、常见的工作队列
补充:
有界队列和无界队列
有界队列在达到队列饱和并达到最大线程数时会执行饱和策略
无界队列因为队列是没有限制的,可以一直放任务将导致设置的最大线程数无效
比较常见的工作队列
ArrayBlockingQueue
基于数组结构的有界阻塞队列,先进先出
LinkedBlockingQueue
基于链表的有界阻塞队列,先进先出,效率高于ArrayBlockQueue。
静态工厂Executors.newFixedThreadPool()使用该队列。
不指定容量时将使用默认值 Integer.MAX_VALUE
PriorityBlockingQueue
优先级队列,线程按照按照优先级进行排序
SynchronousQueue
一个不存储元素的阻塞队列,每个插入操作必须等到另外一个线程调用移除操作,
否则插入操作一直处理阻塞状态。
Executors.newCachedThreadPool使用的就是该工作队列
SynchronousQueue 代码的演示:
import java.util.concurrent.*;
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
//创建一个线程池,同步队列采用SynchtonousQueue队列
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(3,10,10, TimeUnit.SECONDS,new SynchronousQueue<>());
for (int i = 0; i < 10; i++) {
int j = i;
poolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"得到执行"+"处理任务"+j);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
poolExecutor.shutdown();
}
}
该队列的特殊之处在于:放入队列中的元素必须需要一个线程去获取它!否则会放入失败或者一直阻塞直至有线程取走。
注:使用该策略的话,如果线程处理时间过程会导致新来的任务去创建新的线程导致创建大量的线程甚至OOM
五、自定义线程工厂
自定义创建工厂需要实现 java.util.concurrent.ThreadFactory接口中的 Thread newThread(Runnable r)方法,参数为传入的任务,需要返回一个工作线程
//自定义一个原子性的计数器
static AtomicInteger threadNum = new AtomicInteger();
public static void main(String[] args) {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(3, 10, 20, TimeUnit.SECONDS, new SynchronousQueue<>(), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("自定义线程池"+threadNum.getAndIncrement());
return thread;
}
});
需要注意的是,自定义线程工厂是可选参数。我们不实现的话Executors会默认帮我们创建线程工厂的
六、常见的饱和策略
当线程的队列已满,并达到最大线程数时。线程池将会把线程传递给饱和策略进行处理
饱和策略实现的都实现了RejectedExecutionHandler接口,该接口中有一个方法
void rejectedExecution(Runnable r, ThreadPoolExecutor executor)
r:需要执行的任务
executor:线程池对象
AbortPolicy :直接抛出异常 (默认)
CallerRunsPolicy :由调用线程处理任务(谁调用谁处理)
DiscardOldestPolicy :丢弃最早的任务,尝试让该任务加入
DiscardPolicy :不处理,直接丢弃
自定义饱和策略
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadPoolExecutorTest {
static AtomicInteger threadNum = new AtomicInteger();
public static void main(String[] args) {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(3, 5, 20, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), new RejectedExecutionHandler() {
//自定义的饱和策略
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println(new Thread(r).getName()+"不能正常执行");
}
});
for (int i = 0; i < 10; i++) {
int j = i;
poolExecutor.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"得到执行"+"处理任务"+j);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
poolExecutor.shutdown();
}
}
分析:
上述代码中核心代码数位3,最大线程数5.工作队列为3。而我们需要
创建10个线程那就表明一定会有2个线程交给我们的饱和策略去处理。
结果:
七、线程池的关闭
线程池中有shutdown和shutdownNow两个方法
shutdown()
线程池会将正在处理的任务,以及已经提交的任务都处理结束后才退出
shutdownNow()
线程池会将正在处理的任务执行结束,那些已经提交的任务直接丢弃。
当线程池调用shutdown()或者shutdownNow()方法时。
线程池会遍历线程池中的所有工作线程。调用他们的interrupt()中断方法。
八、线程池的拓展
我们可以在执行任务前后完成一些操作。类似于spring aop的前置通知后置通知等
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(3, 5, 20, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println(new Thread(r).getName()+"不能正常执行");
}
}){
//线程池关闭的时候触发的方法
@Override
protected void terminated() {
super.terminated();
System.out.println("线程池关闭");
}
//线程池执行任务前调用的方法
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
}
//线程池执行任务后触发的方法
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
}
};
和spring中几种通知很类似
protected void afterExecute(Runnable r, Throwable t)
任务执行后调用,第一个参数表示任务,第二个参数表示异常对象
protected void terminated()
线程池关闭的时候触发
protected void beforeExecute(Thread t, Runnable r)
执行任务前触发,第一个参数表示执行任务的线程,第二个参数表示任务
线程执行任务前一定会触发beforeExecute()方法,最后在线程池关闭的时候触发terminated()方法