JUC线程池(二): 【面试高频】一文搞定对线程池的疑问 - ThreadPoolExecutor详解

线程池的作用
线程池能够对线程统一分配、调优和监控:

  • 提高线程的可管理性
  • 提高响应速度,因为线程池已经创建好了
  • 降低资源消耗,避免线程频繁创建和销毁

一. 使用ThreadPoolExecutor

JDK 1.5开始,把工作单元和执行机制分开,工作单元由 Runnable 和 Callable 提供,执行机制由 Executor提供。

1. ExecutorService 使用固定线程执行多个任务

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class SimpleThreadPool {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            Runnable worker = new WorkerThread("" + i);
            executor.execute(worker);
          }
          //当10个任务都执行完毕之后,就不再接收任务了。
        executor.shutdown(); // This will make the executor accept no new threads and finish all existing threads in the queue。
        //等待所有的线程执行完毕,然后等待关闭线程池
        while (!executor.isTerminated()) { // Wait until all threads are finish,and also you can use "executor.awaitTermination();" to wait
        }
        System.out.println("Finished all threads");
    }
}



public class WorkerThread implements Runnable {
    private String command;
    public WorkerThread(String s){
        this.command=s;
    }
 
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+" Start. Command = "+command);
        processCommand();
        System.out.println(Thread.currentThread().getName()+" End.");
    }
 
    private void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
 
    @Override
    public String toString(){
        return this.command;
    }
}

通过Executors.newFixedThreadPool(5) 我们创建了一个有5个线程的线程池,然后通过重复调用executor.execute(worker) 去执行分配给线程池的10个任务。

它将启动五个线程,先处理五个任务,其他工作在等待队列里,一旦有任务完成,空闲下来的线程就会执行等待队列里的任务。

pool-1-thread-2 Start. Command = 1
pool-1-thread-4 Start. Command = 3
pool-1-thread-1 Start. Command = 0
pool-1-thread-3 Start. Command = 2
pool-1-thread-5 Start. Command = 4
pool-1-thread-4 End.
pool-1-thread-5 End.
pool-1-thread-1 End.
pool-1-thread-3 End.
pool-1-thread-3 Start. Command = 8
pool-1-thread-2 End.
pool-1-thread-2 Start. Command = 9
pool-1-thread-1 Start. Command = 7
pool-1-thread-5 Start. Command = 6
pool-1-thread-4 Start. Command = 5
pool-1-thread-2 End.
pool-1-thread-4 End.
pool-1-thread-3 End.
pool-1-thread-5 End.
pool-1-thread-1 End.
Finished all threads

 

2. 使用ThreadPoolExecutor管理线程池

按照一定规则创建线程池

我们可以在创建 ThreadPoolExecutor 实例时指定活动线程的数量,我们也可以限制线程池的大小并且创建我们自己的 RejectedExecutionHandler 实现来处理不能适应工作队列的工作

查询executor的状态

ThreadPoolExecutor 提供了一些方法,我们可以使用这些方法来查询 executor 的当前状态,线程池大小,活动线程数量以及任务数量。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
 
public class WorkerPool {

    public static void main(String args[]) throws InterruptedException{
        RejectedExecutionHandlerImpl rejectionHandler = new RejectedExecutionHandlerImpl();
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        //creating the ThreadPoolExecutor
        ThreadPoolExecutor executorPool = new ThreadPoolExecutor(
                2,  //corePoolSize
                4, //maximumPoolSize
                10,  //keepAliveTime
                TimeUnit.SECONDS, 
                new ArrayBlockingQueue<Runnable>(2), //workQueue
                threadFactory,  //threadFactory
                rejectionHandler); //handler
                
       //此线程用于查看线程池的状态
        MyMonitorThread monitor = new MyMonitorThread(executorPool, 3);
        Thread monitorThread = new Thread(monitor);
        monitorThread.start();

        //submit work to the thread pool
        for(int i=0; i<10; i++){
            executorPool.execute(new WorkerThread("cmd"+i));
        }
         
        Thread.sleep(30000);
        //shut down the pool
        executorPool.shutdown();
        //shut down the monitor thread
        Thread.sleep(5000);
        monitor.shutdown();
         
    }
}




//工作单元:执行任务时,获取线程池当前的状态
import java.util.concurrent.ThreadPoolExecutor;
 
public class MyMonitorThread implements Runnable
{
    private ThreadPoolExecutor executor;
     
    private int seconds;
     
    private boolean run=true;
 
    public MyMonitorThread(ThreadPoolExecutor executor, int delay)
    {
        this.executor = executor;
        this.seconds=delay;
    }
     
    public void shutdown(){
        this.run=false;
    }
 
    @Override
    public void run()
    {
        while(run){
                System.out.println(
                    String.format("[monitor] [%d/%d] Active: %d, Completed: %d, Task: %d, isShutdown: %s, isTerminated: %s",
                        this.executor.getPoolSize(),
                        this.executor.getCorePoolSize(),
                        this.executor.getActiveCount(),
                        this.executor.getCompletedTaskCount(),
                        this.executor.getTaskCount(),
                        this.executor.isShutdown(),
                        this.executor.isTerminated()));
                try {
                    Thread.sleep(seconds*1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
        }
             
    }
}

//处理不能插入到工作队列的工作
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
 
public class RejectedExecutionHandlerImpl implements RejectedExecutionHandler {
 
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println(r.toString() + " is rejected");
    }
 
}

//运行结果
pool-1-thread-1 Start. Command = cmd0
pool-1-thread-4 Start. Command = cmd5
cmd6 is rejected
pool-1-thread-3 Start. Command = cmd4
pool-1-thread-2 Start. Command = cmd1
cmd7 is rejected
cmd8 is rejected
cmd9 is rejected
[monitor] [0/2] Active: 4, Completed: 0, Task: 6, isShutdown: false, isTerminated: false
[monitor] [4/2] Active: 4, Completed: 0, Task: 6, isShutdown: false, isTerminated: false
pool-1-thread-4 End.
pool-1-thread-1 End.
pool-1-thread-2 End.
pool-1-thread-3 End.
pool-1-thread-1 Start. Command = cmd3
pool-1-thread-4 Start. Command = cmd2
[monitor] [4/2] Active: 2, Completed: 4, Task: 6, isShutdown: false, isTerminated: false
[monitor] [4/2] Active: 2, Completed: 4, Task: 6, isShutdown: false, isTerminated: false
pool-1-thread-1 End.
pool-1-thread-4 End.
[monitor] [4/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [0/2] Active: 0, Completed: 6, Task: 6, isShutdown: true, isTerminated: true
[monitor] [0/2] Active: 0, Completed: 6, Task: 6, isShutdown: true, isTerminated: true


初始化 ThreadPoolExecutor 时,保持线程池初始大小为2个线程,最大为4个线程、工作队列大小为2。

假设已经有4个正在运行的任务,此时又来了一批任务需要执行,那会保留到工作队列,但只保留两个,其他任务交给RejectedExecutionHandlerImpl处理。

 
 

二. ThreadPoolExecutor使用详解

1. 线程池简单描述

java线程池可以简单认为由一个线程集合(workerSet)和一个阻塞队列(workQueue)组成。
 
当client向线程池提交任务时,线程池先把任务放到阻塞队列中。workerSet中的线程会不断从workQueue中获取任务执行,如果workQueue没有任务时,worker就会堵塞,直到队列中有了任务就取出来继续执行。

在这里插入图片描述
 

2. Execute执行逻辑

先看下什么是ctl(control的简写):

是一个AtomicInteger变量,一共32位:高三位是线程池的状态(Running、Shutdown、Stop、TIDYING、Terminate);低29位存储当前有效线程数。

    // runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

    // Packing and unpacking ctl
    private static int runStateOf(int c)     { return c & ~CAPACITY; }
    private static int workerCountOf(int c)  { return c & CAPACITY; }
    private static int ctlOf(int rs, int wc) { return rs | wc; }

 

   public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        //判断工作线程是否小于核心线程数
        if (workerCountOf(c) < corePoolSize) {
        //创建一个core线程,如果失败则更新ctl状态
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //如果工作线程大于核心线程数,判断线程池的状态是否running,且能否加到队列中
        if (isRunning(c) && workQueue.offer(command)) {
            //双重检查:
            int recheck = ctl.get();
            //如果线程池不是running ,那么就从列表中移除,并执行reject策略
            if (!isRunning(recheck) && remove(command))
                reject(command);
             //如果移除失败,则判断工作线程是否为0,如果为0,就创建一个非核心线程   
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //如果线程池挂了或者阻塞队列满,则Runnable 进入到reject策略执行(具体可以自己实现)
        else if (!addWorker(command, false))
            reject(command);
    }

当一个任务提交到线程池之后:

  1. 当工作线程数 < corePoolSize。如果是则创建一个core线程如果创建失败则更新ctl状态。
  2. 当工作线程数 > corePoolSize,判断线程池是否running,且是否加到阻塞队列成功
    如果是:就双重检查:如果线程池不是running,那就从阻塞队列中去除,并将任务加到拒绝策略中
    如果线程池挂了或当前工作线程大于最大线程数,那么就将任务加到reject策略执行。

 

3. ThreadPoolExecutor参数详解

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

参数描述
corePoolSize
1. 核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize, 即使有其他空闲线程能够执行新来的任务, 也不会继续创建线程;
2. 如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;
3. 如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
maximumPoolSize
1. 最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程去执行,前提是当前线程数小于最大线程数。
2. 如果阻塞队列是无界的,那最大线程数不起作用。
keepAliveTime
线程空闲时线程的存活时间;默认情况下,只有线程数大于核心线程数时才有效
unit
keepAliveTime的单位
workQueue
阻塞队列:用来保存等待执行的任务
threadFactory
创建线程的工厂,可以通过自定义,设置一个有辨识度的线程名。默认是DefaultThreadFactory
handler
线程池饱和策略,当阻塞队列满了,且没有可用线程,如果继续提交任务,必须采取一种策略处理该任务:
1. AbortPolicy: 直接抛出异常,默认策略;
2. CallerRunsPolicy用调用者所在的线程来执行任务;
3. DiscardOldestPolicy: 丢弃阻塞队列中靠最前的任务,并执行当前任务;
4. DiscardPolicy : 直接丢弃任务;
5. 实现RejectedExecutionHandler接口,自定义饱和策略

 

4. 三种方式创建线程池

4.1. newFixedThreadPool

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

  • 因为coreThreads等于maxThreads,所以线程的数量达到max线程数时,即使没有可执行的任务,线程池也不会释放线程。
  • 会创建一个无界的工作队列,此时饱和策略将失效。

 

3.2. newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
  • 会初始化线程为1,如果该线程异常结束,会重新创建一个新的线程继续执行。因为只有一个线程,可以保证任务的顺序执行。
  • 因为使用了无界队列,所以饱和策略失效。

 

3.3. newCachedThreadPool

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

  • 线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列;
  • 没有任务执行时,超过60s会释放线程,当新提交任务时,没有空闲线程会创建新线程,有一定开销

 

5. 关闭线程池

遍历线程池中的所有线程,然后逐个调用线程的interrupt方法来中断线程。

方法解释
shutdown将线程池里的线程状态设置成SHUTDOWN状态, 然后中断所有没有正在执行任务的线程.
shutdownNow将线程池里的线程状态设置成STOP状态, 然后停止所有正在执行或暂停任务的线程

只要调用这两个关闭方法中的任意一个, isShutDown() 返回true.
当所有任务都成功关闭了, isTerminated()返回true。

 
 

三. 为什么线程池不允许使用Executors去创建?

建议通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors各个方法的弊端:

  • newFixedThreadPool和newSingleThreadExecutor : 堆积的请求处理队列可能会非常耗费内存,甚至OOM。
  • newCachedThreadPool和newScheduledThreadPool :可能会创建非常多的线程,设置OOM。

1. 推荐的创建方式

推荐方式 1:commons-lang3

ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
        new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());

 

推荐方式 2 : com.google.guava

ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();

//Common Thread Pool
ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

// excute
pool.execute(()-> System.out.println(Thread.currentThread().getName()));

 //gracefully shutdown
pool.shutdown();

 

推荐方式 3 :spring配置线程池

自定义线程工厂bean需要实现ThreadFactory,可参考该接口的其它默认实现类,使用方式直接注入bean调用execute(Runnable task)方法即可。

    <bean id="userThreadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <property name="corePoolSize" value="10" />
        <property name="maxPoolSize" value="100" />
        <property name="queueCapacity" value="2000" />

    <property name="threadFactory" value= threadFactory />
        <property name="rejectedExecutionHandler">
            <ref local="rejectedExecutionHandler" />
        </property>
    </bean>
    
    //in code
    userThreadPool.execute(thread);

 

2. 配置线程池需要考虑因素

从任务的优先级、任务执行的长短、任务的性质(CPU/IO密集),任务的依赖关系分析。并且尽可能的使用有界的工作队列。

性质不同的任务可用使用不同规模的线程池分开处理:

  • CPU密集型任务:尽可能少的线程,使用Ncpu + 1,CPU执行快,线程切换会造成CPU的消耗,所以要减少线程切换带来的资源消耗。
  • IO密集型任务:尽可能多的线程,2 * Ncpu ,比如数据库连接池。IO执行速度慢,而且IO操作不占用CPU,不要让CPU闲下来,通过多线程,提高执行速度。
  • 混合型:当CPU密集型的任务与IO密集型任务的执行时间差别较小时,拆分成两个线程池。因为IO需要多线程来提高速度,此时创建多个线程CPU频繁切换消耗CPU资源,不能发挥出CPU性能,导致CPU密集型任务执行的也不好。
  • 依赖型任务:比如有个任务需要等待数据库的结果,这时等待的时间越长,CPU空闲就越长,所以线程数量大些,别让CPU闲着,充分利用CPU资源。

IO密集型任务:文件读写、DB读写、网络请求等
CPU密集型任务:计算型代码、Bitmap转换、Gson转换等

 
看一个公式:最佳线程数目 =

((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8。那线程数为:((0.5+1.5)/0.5)8=32。

结论:
线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。 以上公式与之前的CPU和IO密集型任务设置线程数基本吻合。

 
 

参考:
《Java并发编程艺术》
https://pdai.tech/md/java/thread/java-thread-x-juc-executor-ThreadPoolExecutor.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

roman_日积跬步-终至千里

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值