线程池,顾名思义,用来存放线程的一个容器
技术的发展无非就是需求推动的,而技术领域的需求大部分都是快!再快!更快!
那么线程池出现的需求也就是痛点是什么呢?
第一、线程的创建和销毁是要占用一定的资源的,创建线程会直接向系统申请,调用系统函数进行分配资源。操作系统给线程分配内存、列入调度,同时线程还要进行上下文的切换。
第二、在JAVA中,线程的线程栈所占用的内存在Java堆外,不受Java程序控制,只受系统资源限制,默认一个线程的线程栈大小是1M(当然这个可以通过设置-Xss属性设置,但是要注意栈溢出问题)。如果每个请求都新建线程,1024个线程就会占用1个G内存,系统很容易崩溃。
第三、我们常用的多线程技术主要解决处理器单元内多个线程执行的问题,它的作用显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。在这种情况下经常会遇到T1+T3远远大于T2的情况,多线程反而成了负担。
为了解决这些痛点,出现了线程池这个概念。
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务加入队列,然后在线程创建后启动这些任务,如果先生超过了最大数量,超出的数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
他的主要特点为:线程复用、控制最大并发数、管理线程。
第一:降低资源消耗,通过重复利用自己创建的线程降低线程创建和销毁造成的消耗。
第二: 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行。
第三: 提高线程的可管理性。线程是稀缺资源,如果无限的创建,不仅会消耗资源,还会较低系统的稳定性,使用线程池可以进行统一分配,调优和监控。
java自带有一个线程池工厂,工厂里面的线程池分了如下几类:
Executors.newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
Executors.newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
Executors.newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
Executors.newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求
Executors.newWorkStealingPool:这个是jdk1.8新增的线程池,适合处理比较耗时的工作任务。
既然java1.8自带了这么多线程池,我们平时生产中用那个呢?
抱歉,都不用。
为啥呢?
先看一眼线程池工作流程:
如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
然后咱再看看阿里规约:
喏,阿里规约写得很明白,这四个线程池有两个不限制队列长度,有两个不限制线程数,这在极高并发下是非常危险的,比如阿里的双十二,绝对秒炸。而且,没有合适的拒绝策略,虽然这四个要么不限制队列长,要么不限制线程数的线程池看起来都用不到,所以拒绝策略就是抛个异常就没了。
这不坑爹呢。
虽然对于非互联网公司貌似也够用,最起码简单省事发面快,咳咳,被小时候安琪酵母的广告洗脑了。
那我们自己写吧,看了看上面工厂生产的前四个线程池,貌似都是用下面这个基础函数生成的:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
这几个参数都是什么意思呢?
corePoolSize 线程池中的核心线程数maximumPoolSize 池中最大线程数keepAliveTime 当线程数超过核心线程数时,这个代表空线程在被终止前最长生存时间unit 最长存活时间的单位workQueue 线程池的工作队列,这个队列仅保存被Runnable任务execute方法提交的任务threadFactory 执行程序创建新线程时要使用的工厂(一般用默认Executors.defaultThreadFactory()即可)workQueue 拒绝策略,表示当线程队列满了并且工作线程大于等于线程池的最大显示数(maxnumPoolSize)时如何来拒绝.
拒绝策略又有啥呢?
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
最初直观感受就是不能丢消息,用CallerRunsPolicy吧,主线程(暂且这么说吧,准确说是启动线程池的线程)跟着做业务。然后就发现,主线程一旦运行任务,即使线程池里的线程跑完任务都不会再进任务,因为主线程被占住了,直到主线程跑完一次业务,才能继续分配给线程池任务。问题很明显,业务流程比较耗时,线程池的一旦干完活,啥都干不了,都等着主线程消费队列的数据给新任务。其他三个策略,AbortPolicy直接抛异常,抛了能咋样,还是不知道要干啥;DiscardOldestPolicy丢弃老任务,丢消息,否了;DiscardPolicy丢弃,肯定否了。
我们要不然自己实现试试?
那我们尝试创建一个自己的线程池:
int processors = Runtime.getRuntime().availableProcessors();
int corePoolSize = processors+1;
int maximumPoolSize = corePoolSize*2;
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
5L,
TimeUnit.MINUTES,
new LinkedBlockingDeque<Runnable>(3),
Executors.defaultThreadFactory(),
(r, e) -> {
if (!e.isShutdown()) {
r.run();
} else {
LoggerFactory.getLogger(ThreadPoolTest.class).error("Task " + r.toString() + " rejected from " + e.toString());
}
}
);
拒绝策略我简单写了点,在具体情况下是可以根据需要自己实现。
我们在Thread类中,有一个State枚举类,为什么只有runnable 而没有running状态呢?
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
其实,这是因为现在的操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin式)。更复杂的可能还会加入优先级(priority)的机制。
这个时间分片通常是很小的,一个线程一次最多只能在 cpu 上运行比如10-20ms 的时间(此时处于 running 状态),时间片用后就要进行状态保存然后被切换下来放入调度队列的末尾等待再次调度(也即回到 ready 状态)。如果期间进行了 I/O 的操作还会导致提前释放时间分片,并进入等待队列;或者是时间分片没有用完就被抢占。
不计切换开销(每次在1ms 以内)的话,相当于1秒内有50-100次切换。事实上时间片经常没用完,线程就因为各种原因被中断,实际发生的切换次数还会更多。
时间分片也是可配置的,如果不追求在多个线程间很快的响应,也可以把这个时间配置得大一点,以减少切换带来的开销。
通常,Java的线程状态是服务于监控的,所以 Java 线程把调度委托给了操作系统,我们在虚拟机层面看到的状态实质是对底层状态的映射及包装。cpu 线程切换这么快,区分 ready 与 running 也没什么意义。因此,统一成为runnable 状态是不错的选择。
作者:Solid-Snaker
原文链接:https://blog.csdn.net/jcSongle/article/details/106089405