Java中的线程池

线程池:一个管理线程的池子。

频繁创建新线程有什么缺点?

不受控风险

系统资源有限,每个人针对不同业务都可以手动创建线程,并且创建标准不一样(比如线程没有名字)。当系统运行起来,所有线程都在疯狂抢占资源,毫无规则,不好管控。

另外,过多的线程自然也会引起上下文切换的开销。

频繁创建开销大

new Thread() 在操作系统层面并没有创建新的线程;

真正转换为操作系统层面创建一个线程,还要调用操作系统内核的API,然后操作系统要为该线程分配一系列的资源。

new Object() 过程

Object obj = new Object();

  1. 分配一块内存 M
  2. 在内存 M 上初始化该对象
  3. 将内存 M 的地址赋值给引用变量 obj

创建线程过程

  1. JVM为一个线程栈分配内存,该栈为每个线程方法调用保存一个栈帧
  2. 每一栈帧由一个局部变量数组、返回值、操作数堆栈和常量池组成
  3. 一些支持本机方法的 jvm 也会分配一个本机堆栈
  4. 每个线程获得一个程序计数器,告诉它当前处理器执行的指令是什么
  5. 系统创建一个与Java线程对应的本机线程
  6. 将与线程相关的描述符添加到JVM内部数据结构中
  7. 线程共享堆和方法区域

总结为什么使用线程池

降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,可以不需要等到线程创建就能立即执行。提高线程的可管理性。
统一管理线程,避免系统创建大量同类线程而导致消耗完内存。

手写一个线程池

线程池参数

ThreadPoolExecutor 的通用构造函数:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);

  1. corePoolSize:当有新任务时,如果线程池中线程数没有达到核心线程池的大小corePoolSize,则会创建新的线程执行任务,否则将任务放入阻塞队列。当线程池中存活的线程数总是大于 corePoolSize 时,应该考虑调大 corePoolSize。
  2. maximumPoolSize:当阻塞队列填满时,如果线程池中线程数没有超过最大线程数maximumPoolSize,则会创建新的线程运行任务。如果线程池中线程数已经达到最大线程数maximumPoolSiz,则会根据拒绝策略处理新任务。非核心线程类似于临时借来的资源,这些线程在空闲时间超过 keepAliveTime 之后,就应该退出,避免资源浪费。
  3. BlockingQueue:阻塞队列,存储等待运行的任务。
  4. keepAliveTime:非核心线程空闲后,保持存活的时间,此参数只对非核心线程有效。设置为0,表示多余的空闲线程会被立即终止。
  5. TimeUnit:keepAliveTime的时间单位TimeUnit.DAYS

TimeUnit.HOURS
TimeUnit.MINUTES
TimeUnit.SECONDS
TimeUnit.MILLISECONDS
TimeUnit.MICROSECONDS
TimeUnit.NANOSECONDS

  1. ThreadFactory:每当线程池创建一个新的线程时,都是通过线程工厂方法来完成的。在 ThreadFactory 中只定义了一个方法 newThread,每当线程池需要创建新线程就会调用它。
public class MyThreadFactory implements ThreadFactory {
    private final String poolName;
    
    public MyThreadFactory(String poolName) {
        this.poolName = poolName;
    }
    
    public Thread newThread(Runnable runnable) {
        return new MyAppThread(runnable, poolName);//将线程池名字传递给构造函数,用于区分不同线程池的线程
    }
}
  1. RejectedExecutionHandler:当队列和线程池都满了的时候,根据拒绝策略处理新任务。
    • AbortPolicy:默认的策略,直接抛出RejectedExecutionException。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
    • DiscardPolicy:不处理,直接丢弃。建议是一些无关紧要的业务采用此策略。
    • DiscardOldestPolicy:将等待队列队首的任务丢弃,并执行当前任务。得根据实际业务是否允许丢弃老任务来认真衡量。
    • CallerRunsPolicy:由调用线程处理该任务CallerRunsPolicy:由调用线程处理该任务

线程池执行具体过程

  1. 当线程池里存活的线程数小于核心线程数corePoolSize时,这时对于一个新提交的任务,线程池会创建一个线程去处理任务。当线程池里面存活的线程数小于等于核心线程数corePoolSize时,线程池里面的线程会一直存活着,就算空闲时间超过了keepAliveTime,线程也不会被销毁,而是一直阻塞在那里一直等待任务队列的任务来执行。因为keepAliveTime只对非核心线程有效。
  2. 当线程池里面存活的线程数已经等于corePoolSize了,这是对于一个新提交的任务,会被放进任务队列workQueue排队等待执行。
  3. 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列也满了,假设maximumPoolSize>corePoolSize,这时如果再来新的任务,线程池就会继续创建新的线程来处理新的任务,直到线程数达到maximumPoolSize,就不会再创建了。
  4. 如果当前的线程数达到了maximumPoolSize,并且任务队列也满了,如果还有新的任务过来,那就直接采用拒绝策略进行处理。默认的拒绝策略是抛出一个RejectedExecutionException异常。

线程池是如何保活和回收的

我们知道线程池的作用就是提高线程的利用率,需要线程时,可以直接从线程池中获取线程直接使用,而不用创建线程,那线程池中的线程,在没有任务执行时,是如何保活的呢?

在runWorker方法里,线程会循环getTask()获取阻塞队列中的任务。

不断地的从阻塞队列中获取任务,主要调用的是workQueue.poll()方法或take(), 这两个方法都会阻塞式的从队列中获取元素,区别式poll()方法可以设置一个超时时间, take()不能设置超时时间,所以这也间接的使得线程池中的线程阻塞等待从而达到保活的效果。

当然并不是线程池中的所有线程都需要一直保活,比如只有核心线程需要保活,非核心线程就不需要保活,那非核心线程是怎么回收的呢?
底层是这样的,当一个线程处理完当前任务后,就会开始去阻塞队列中获取任务,只不过,在调用poll或take方法之前, 会判断当前线程池中有多少个线程,如果多余核心线程数(也就是wc > corePlloSize),那么timed为true,此时当前线程就会调用poll()并设置超时时间来获取阻塞队列中的任务,这样一旦时间到了还没有获取到任务,那么poll方法获取到的r就是null,返回给上一级,runWorker()里的getTask方法就获取到null了,此时while循环就会退出。那么就会调用processWorkerExit()方法,remove当前线程

这里其实可以看到timed还有一个参数,allowCoreThreadTimeOut,这个主要是用来控制核心线程是否可以回收,默认是false,上面是讨论默认值false的情况,即核心线程不会超时。如果为true,工作线程可以全部销毁

实际上,虽然有核心线程数,但线程并没有区分是核心还是非核心,并不是先创建的就是核心,超过核心线程数后创建的就是非核心,最终保留哪些线程,完全随机。

线程池大小怎么设置?

首先应该明确线程池大小设置的目的是什么?其实就是为了提高 CPU 的利用率

CPU利用率=CPU有效工作时间/CPU总的运行时间

如果线程池线程数量太小,当有大量请求需要处理,需要创建非核心线程池处理,导致系统响应比较慢,会影响用户体验,甚至会出现任务队列大量堆积任务导致OOM。

如果核心线程池数量太大,会导致其一直阻塞在那里等待任务队列的任务来执行,消耗内存。并且大量线程可能会同时抢占 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了执行效率。

根据线程数设置依据

最大线程数:原则上就是性能最高线程数,因为此时性能已经是最高,再设置比他大的线程数反而性能变低。极端情况下才会使用到最大线程数,正常情况下不应频繁出现超过核心线程数的创建。

核心线程数:基于性能考虑,及其他业务处理的最优效率考虑,估算平时的流量需要的线程数,设置核心线程数

阻塞队列:估算最大流量,设置阻塞队列长度

注意:需要通过压力测试来进行微调,只有经过压测的检验,才能最终保证的配置大小是准确的。

一般情况设置依据

一般用来计算核心线程数

CPU 密集型任务(N+1):

这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,多出来的一个线程是为了防止某些原因导致的线程阻塞(如IO操作,线程sleep,等待锁)而带来的影响。一旦某个线程被阻塞,释放了CPU资源,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

I/O 密集型任务(2N):

系统的大部分时间都在处理 IO 操作,此时线程可能会被阻塞,释放CPU资源,这时就可以将 CPU 交出给其它线程使用。因此在 IO 密集型任务的应用中,可以多配置一些线程,具体的计算方法:最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU核心数 * (1 + (IO耗时/CPU耗时)),一般可设置为2N

ExecutorService 线程池实例

但是阿里为什么不推荐使用Executors来创建线程池,这是为了让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors.newFixedThreadPool(10) :

固定大小 core = 自定义的线程数,但阻塞队列是无界队列,会OOM内存溢出

FixedThreadPool:

固定线程数的线程池。任何时间点,最多只有 nThreads 个线程处于活动状态执行任务。

public static ExecutorService newFixedThreadPool(int nThreads) {
	return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

为什么不建议使用?
使用无界队列 LinkedBlockingQueue(队列容量为 Integer.MAX_VALUE),运行中的线程池不会拒绝任务,即不会调用RejectedExecutionHandler.rejectedExecution()方法。maxThreadPoolSize 是无效参数,故将它的值设置为与 coreThreadPoolSize 一致。当添加任务的速度大于线程池处理任务的速度,可能会在队列堆积大量的请求,消耗很大的内存,甚至导致OOM。

keepAliveTime 也是无效参数,设置为0L,因为此线程池里所有线程都是核心线程,核心线程不会被回收(除非设置了executor.allowCoreThreadTimeOut(true))。

适用场景:适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。需要注意的是,FixedThreadPool 不会拒绝任务,在任务比较多的时候会导致 OOM。

SingleThreadExecutor

只有一个线程的线程池。

public static ExecutionService newSingleThreadExecutor() {
	return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

为什么不建议使用?
使用无界队列 LinkedBlockingQueue。线程池只有一个运行的线程,新来的任务放入工作队列,线程处理完任务就循环从队列里获取任务执行。保证顺序的执行各个任务。

适用场景:适用于串行执行任务的场景,一个任务一个任务地执行。在任务比较多的时候也是会导致 OOM。

CachedThreadPool

public static ExecutorService newCachedThreadPool() {
	return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}

为什么不建议使用?
core是0,最大线程数是Integer.MAX_VALUE,因此当添加任务的速度大于线程池处理任务的速度,可能会创建大量的线程,极端情况下,这样会导致耗尽 cpu 和内存资源,甚至导致OOM。

使用没有容量的SynchronousQueue作为线程池工作队列,当线程池有空闲线程时,SynchronousQueue.offer(Runnable task)提交的任务会被空闲线程处理,否则会创建新的线程处理任务。

适用场景:用于并发执行大量短期的小任务。CachedThreadPool允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

ScheduledThreadPoolExecutor

public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
}

在给定的延迟后运行任务,或者定期执行任务。

为什么不建议使用?
最大线程数是Integer.MAX_VALUE,因此当添加任务的速度大于线程池处理任务的速度,可能会创建大量的线程,极端情况下,这样会导致耗尽 cpu 和内存资源,甚至导致OOM。

Tomcat线程池

JDK 的线程池,是先使用核心线程数配置,接着使用队列长度,最后再使用最大线程配置。

Tomcat 的线程池,就是先使用核心线程数配置,再使用最大线程配置,最后才使用队列长度。

Tomcat中的最大核心线程默认是200,因此springboot使用Tomcat时,默认最大能同时处理200个请求

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值