浅理解java中的线程池

Posted 穷少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅理解java中的线程池相关的知识,希望对你有一定的参考价值。

浅理解java中的线程池

线程池(thread pool)

1.线程池的概念

线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

2.线程池的工作机制

  • 在线程池的编程模式下,任务是提交给整个线程池,而不是直接提交给某个线程,线程池在拿到任务后,就在内部寻找是否有空闲的线程,如果有,则将任务交给某个空闲的线程。

  • 一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。

3.使用线程池的原因

  1. 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
  2. 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
  3. 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
  4. 提供更强大的功能,延时定时线程池。

4.线程池相关概念

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PLiYHPrE-1647320860560)(assets/images/image-20210817125446305.png)]

5.线程池的主要参数

/**
 * 从jdk1.5之后引入了线程池类,在package java.util.concurrent包下
 **/
public class ThreadPoolExecutor extends AbstractExecutorService 
    
    /**
     * 核心方法,创建线程池的构造函数,后面提到的newCachedThreadPool(),newFixedThreadPool(),newSingleThreadExecutor()方法创建的线程池,都是依赖这个核心方法,只是各自的参数不同
     **/
    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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    
    

参数作用
corePoolSize(核心线程的数量)当向线程池提交一个任务时,若线程池已创建的核心线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新核心线程来执行该任务,直到已创建的核心线程数大等于corePoolSize
maximumPoolSize(线程池最大大小)线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。临时线程可创建的数量=maximumPoolSize-corePoolSize
keepAliveTime(临时线程的空闲时间)线程池内,除了核心线程外,都可以算是临时线程,核心线程不能销毁,临时线程可以,临时线程的空闲时间超过keepAliveTime,那么这个线程就会被销毁
workQueue(任务队列)用于传输和保存等待执行任务的阻塞队列。
threadFactory(线程工厂)用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
handler(线程饱和策略)当线程池和队列都满了,再加入线程会执行此策略,所有的策略实现接口 java.util.concurrent.RejectedExecutionHandler

6.java中提供的线程池

Executors类提供了4种不同的线程池:newCachedThreadPool, newFixedThreadPool, newScheduledThreadPool, newSingleThreadExecutor

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

public class App 

    // 所有的线程池,都是通过java.util.concurrent.ThreadPoolExecutor类实现,只是构造参数不同
    public static void main(String[] args) throws Exception 
		
            ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(20);

        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);
    




工程方法corePoolSizemaximumPoolSizekeepAliveTimeworkQueue
Executors.newCachedThreadPool();0Integer.MAX_VALUE60sSynchronousQueue(同步,有界,队列)
Executors.newFixedThreadPool(nThread);nThreadnThread0LinkedBlockingQueue(链表,无界,队列)
Executors.newSingleThreadExecutor();110LinkedBlockingQueue
Executors.newScheduledThreadPool(nThread);nThreadInteger.MAX_VALUE10L(ms)DelayedWorkQueue(implement LinkedBlockingQueue)

6.1 newCachedThreadPool

用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换),此线程池的corePoolSize为0,maximumPoolSize为MAX,所以线程池能无限的创建临时线程,所有的线程在空闲后超过规定存活时间都可以被销毁,但是无限创建临时线程,也以为可能会导致CPU超载

6.2 newFixedThreadPool

创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重);注意:虽然线程的数量是固定的,但是任务队列是无界队列,可以存放任意个任务,也就可能导致OOM(Out Of Memory)异常

6.3 newSingleThreadExecutor

创建一个单线程的线程池,适用于需要保证顺序执行各个任务。任务队列是无界队列,可以存放任意个任务,也就可能导致OOM(Out Of Memory)异常

6.4 newScheduledThreadPool

适用于执行延时或者周期性任务。

7.自定义线程池

我们知道所有的线程池都是通过java.util.concurrent.ThreadPoolExecutor类实现,只是构造参数不同,在很多时候,我们的任务场景是不一样的,我们可以通过自定义线程

import java.util.concurrent.*;

public class App 

    // 核心线程的数量
    static int corePoolSize = 10;

    // 线程池最大线程数
    static int maximumPoolSize = Integer.MAX_VALUE;

    // 临时线程规定存活时间
    static long keepAliveTime = 60;

    // 存活时间的单位
    static TimeUnit unit = TimeUnit.MICROSECONDS;

    // 任务队列
    static SynchronousQueue<Runnable> workQueue = new SynchronousQueue<Runnable>();

    // 线程工厂
    static ThreadFactory threadFactory = Executors.defaultThreadFactory();

    // 线程池拒绝策略
    static RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.DiscardOldestPolicy();

    public static void main(String[] args) throws Exception 

        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, rejectedExecutionHandler);

    




7.1 如何配置线程池

CPU密集型任务

尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。

IO密集型任务

可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

混合型(CPU+IO)密集任务

可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。
因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

8.线程池的拒绝策略

拒绝策略提供顶级接口 RejectedExecutionHandler ,其中方法 rejectedExecution 即定制具体的拒绝策略的执行逻辑。

策略
CallerRunsPolicy如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务。适合任务较轻的场景,如果任务太重,容易导致程序阻塞
AbortPolicy(默认的拒绝策略)这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
DiscardPolicy丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
DiscardOldestPolicy丢弃队列最前面的任务,然后重新提交被拒绝的任务。

9.线程池的execute方法与submit方法

线程池内的execute方与submit方法都是用来提交任务到线程池内去执行,最大的区别就是,execute无返回值,submit有返回值

区别executesubmit
参数接受Runnable接口类型的参数即可接受Runnable类型参数,关键可以接受Callable类型参数
返回值void返回Future类型参数,我们可以通过Future.get方法获取线程执行完成的返回值,也可以捕捉线程内抛出的异常

10.线程池的状态

在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态:

volatile int runState;
static final int RUNNING    = 0;
static final int SHUTDOWN   = 1;
static final int STOP       = 2;
static final int TERMINATED = 3;

runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;

下面的几个static final变量表示runState可能的几个取值。

当创建线程池后,初始时,线程池处于RUNNING状态;

如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;

如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;

当线程池处于SHUTDOWNSTOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。

提交优先级与执行优先级

线程池的执行流程

提交优先级

  1. 提交任务给线程池,首先会提交给核心线程区
  2. 如果核心线程区内的线程都处于忙碌状态,就会将任务放置在任务队列内
  3. 如果任务队列已满,就会将任务交给临时线程区

执行优先级

  1. 核心线程不满时,优先创建核心线程,优先执行
  2. 核心线程满时,创建临时线程执行
  3. 核心线程与线程执行完毕,后再执行任务队列里面的任务

❓ 常见问题

线程池为什么需要使用(阻塞)队列?

因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。

线程池为什么要使用阻塞队列而不使用非阻塞队列?

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。

当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。
使得在线程不至于一直占用cpu资源。

📚references

  1. Java线程池详解 - 简书 (jianshu.com)
  2. 4种常用线程池介绍 - 创天创世纪 - 博客园 (cnblogs.com)

以上是关于浅理解java中的线程池的主要内容,如果未能解决你的问题,请参考以下文章

浅理解java中的线程池

java线程池源码的理解

Java核心深入理解线程池ThreadPool

深入理解Java线程池:ThreadPoolExecutor

线程池工作原理

说一说java线程池