JUC线程池 - ThreadPoolExecutor详解

前言

对于线程池,我们需要更多关注的是 ThreadPoolExecutor 这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的,对于这个类也是需要更深入的学习


1. 常用参数详解

从 ThreadPoolExecutor 类的一个构造方法来看

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

1.1 核心参数:

  • corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
  • maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

当一个任务提交至线程池后的处理逻辑:

  1. 线程池首先当前运行的线程数量是否少于 corePoolSize,如果是,则创建一个新的工作线程来执行任务。
  2. 如果没有空闲的线程执行该任务且当前的线程数等于 corePoolSize同时 BlockingQueue未满,则将任务加入 BlockingQueue,而不添加新的线程。
  3. 如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数小于 maximumPoolSize,则创建新的线程执行任务。
  4. 如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数等于 maximumPoolSize,则根据构造函数中的 handler 指定的策略来拒绝新的任务。

另:

  1. 线程池并没有标记哪个线程是核心线程,哪个是非核心线程,线程池只关心核心线程的数量。
  2. ThreadPoolExecutor 创建新线程时,通过 CAS 来更新线程池的状态

1.2 其他常见参数:

  • keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁

  • unit : keepAliveTime 参数的时间单位

  • threadFactory :executor 创建新线程的时候会用到

  • handle :饱和策略


ThreadPoolExecutor 饱和策略定义

  • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException 来拒绝新任务的处理
  • ThreadPoolExecutor.CallerRunsPolicy:由向线程池提交任务的线程来执行该任务
  • ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉
  • ThreadPoolExecutor.DiscardOldestPolicy: 丢弃最早提交但是未处理的任务

2. ThreadPoolExecutor 例子

创建一个任务,该任务是 Runnable 类型

public class RunnableTask implements Runnable {
	//用来标识当前线程所运行的任务
    int number;

    public ThreadTask(int number) {
        this.number = number;
    }

    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + " Start -- " + number);
            Thread.sleep(10000);
            System.out.println(Thread.currentThread().getName() + " End -- " + number);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

使用 ThreadPoolExecutor 线程池运行任务

public class ThreadPoolTest {
    /** 线程池对应参数 */
    private static final Integer corePoolSize = 4;
    private static final Integer maximumPoolSize = 8;
    private static final Long keepAliveTime = (long) 8;
    private static final TimeUnit timeUnit = TimeUnit.SECONDS;
    private static final BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(16);
    private static final RejectedExecutionHandler handle = new ThreadPoolExecutor.AbortPolicy();

    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, workQueue, handle);

        for (int i = 0; i < 4; i++) {
            RunnableTask runnableTask = new RunnableTask(i);
            threadPoolExecutor.execute(threadTask);
        }
        threadPoolExecutor.shutdown();
        while (!threadPoolExecutor.isTerminated()) {
        }
        System.out.println("Finished all threads");
    }
}

示例中创建了固定大小为四的线程池,然后分配了五个工作,因为线程池大小为四,它将启动四个工作线程先处理四个工作,其他的工作则处于等待状态,一旦有工作完成,空闲下来工作线程就会拾取等待队列里的其他工作进行执行。这四个线程会一直存在,直到线程池消亡。

以下是程序的运行输出:

pool-1-thread-4 Start -- 3
pool-1-thread-1 Start -- 0
pool-1-thread-2 Start -- 1
pool-1-thread-3 Start -- 2
pool-1-thread-4 End -- 3
pool-1-thread-1 End -- 0
pool-1-thread-4 Start -- 4
pool-1-thread-2 End -- 1
pool-1-thread-3 End -- 2
pool-1-thread-4 End -- 4
Finished all threads

我们不仅可以在创建 ThreadPoolExecutor 实例时指定活动线程的数量,也可以限制线程池的大小并且创建我们自己的 RejectedExecutionHandler 实现来处理被拒绝的任务

自定义拒绝处理器:

public class RejectedExecutionHandlerImpl implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println(r.toString() + "被拒绝了");
    }
}

将 ThreadPoolTest.java 中的线程池参数稍微改动一下,将阻塞队列减少为1,由于线程池最大为8个,阻塞队列只能存放一个任务,当提交10个任务时,将有1个任务被自定义的饱和策略拒绝

public class ThreadPoolTest {
    /** 线程池对应参数 */
    private static final Integer corePoolSize = 4;
    private static final Integer maximumPoolSize = 8;
    private static final Long keepAliveTime = (long) 8;
    private static final TimeUnit timeUnit = TimeUnit.SECONDS;
    /** 减少了阻塞队列 */
    private static final BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(1);
    /** 设置为自己的实现 */
    private static final RejectedExecutionHandler handle = new RejectedExecutionHandlerImpl();

    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, workQueue, handle);
		//提交十个任务
        for (int i = 0; i < 10; i++) {
            ThreadTask threadTask = new ThreadTask(i);
            threadPoolExecutor.execute(threadTask);
        }
        threadPoolExecutor.shutdown();
        while (!threadPoolExecutor.isTerminated()) {
        }
        System.out.println("Finished all threads");
    }
}

执行结果:

cn.testproject.thread.runnable.ThreadTask@457e2f02被拒绝了
pool-1-thread-1 Start -- 0
pool-1-thread-3 Start -- 2
pool-1-thread-5 Start -- 5
pool-1-thread-7 Start -- 7
pool-1-thread-4 Start -- 3
pool-1-thread-2 Start -- 1
pool-1-thread-8 Start -- 8
pool-1-thread-6 Start -- 6
pool-1-thread-4 End -- 3
pool-1-thread-3 End -- 2
pool-1-thread-1 End -- 0
pool-1-thread-7 End -- 7
pool-1-thread-8 End -- 8
pool-1-thread-5 End -- 5
pool-1-thread-4 Start -- 4
pool-1-thread-6 End -- 6
pool-1-thread-2 End -- 1
pool-1-thread-4 End -- 4
Finished all threads

3. 推荐使用 ThreadPoolExecutor 构造函数创建线程池

在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式。这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors 返回的线程池允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量线程,从而导致 OOM。


4. 线程池设置设置多大比较合适

有一个简单并且适用面比较广的公式:

  • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

如何判断是 CPU 密集任务还是 IO 密集任务?

CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。单凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。


5. 线程池中任务是如何关闭的?

线程池使用 shutdown()shutdownNow() 方法关闭任务

  • shutdownNow(): 线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行

  • shutdown():线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ThreadPoolExecutorJava 中的一个线程池实现,它提供了一种管理线程的机制,可以有效地控制线程的数量,避免因为线程过多而导致系统资源的浪费和性能下降。 ThreadPoolExecutor 的主要构造函数如下: ``` public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) ``` 其中,各个参数的含义如下: - `corePoolSize`:核心线程数,即线程池中保持的最少线程数。 - `maximumPoolSize`:线程池所能容纳的最大线程数。 - `keepAliveTime`:线程池中超过 `corePoolSize` 的空闲线程能够存活的最长时间。 - `unit`:`keepAliveTime` 的时间单位。 - `workQueue`:任务队列,用于保存等待执行的任务。 - `threadFactory`:线程工厂,用于创建新线程。 - `handler`:拒绝策略,用于当任务队列满了且当前线程数已达到最大线程数时如何处理新任务。 ThreadPoolExecutor 在初始化时会创建 `corePoolSize` 个线程,并将剩余的任务添加到任务队列 `workQueue` 中。当任务队列满了时,如果当前线程数小于 `maximumPoolSize`,则会创建新的线程来执行任务;如果当前线程数已达到最大线程数,则会根据拒绝策略 `handler` 来处理新任务。 ThreadPoolExecutor 还提供了一些方法,如 `execute()`、`submit()`、`shutdown()` 等,用于提交任务、关闭线程池等操作。需要注意的是,当使用完线程池后,应该及时调用 `shutdown()` 方法来关闭线程池以释放资源。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值