Java线程池详解(下)

线程池讲解(下)

六、Java内置线程池

1. newFixedThreadPool

1.1 介绍

创建一个可重用固定线程数的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,就将提交的任务放入到任务队列中。

通过 Executors 类中的相关源代码来看一下相关实现:

 /**
     * 创建一个可重用固定数量线程的线程池
     */
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

从上面源代码可以看出新创建的 FixedThreadPoolcorePoolSizemaximumPoolSize 都被设置为 nThreads,这个 nThreads 参数是我们使用的时候自己传递的。

1.2 执行过程

FixedThreadPoolexecute() 方法运行示意图(该图片来源:《Java 并发编程的艺术》):

在这里插入图片描述
说明:

  1. 如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务;
  2. 当前运行的线程数等于corePoolSize 后, 如果再来新任务的话,会将任务加入 LinkedBlockingQueue
  3. 线程池中的线程执行完手头的任务后,会在循环中反复从 LinkedBlockingQueue 中获取任务来执行;

1.3 缺点

FixedThreadPool 使用无界队列 LinkedBlockingQueue(队列的容量为 Integer.MAX_VALUE)作为线程池的任务队列,在任务比较多的时候会导致 OOM(内存溢出)。而且使用无界队列时maximumPoolSizekeepAliveTime都将是无效参数。

2. newSingleThreadExecutor

2.1 介绍

创建一个单线程化的线程池,即只创建唯一的工作线程来执行任务,保证所有任务都按照指定顺序(FIFO,LIFO,优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。

单工作线程最大的特点是可以保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

/**
     *返回只有一个线程的线程池
     */
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

从上面源代码可以看出新创建的 SingleThreadExecutorcorePoolSizemaximumPoolSize 都被设置为 1。其他参数和 FixedThreadPool 相同。

2.2 执行过程

SingleThreadExecutor 的运行示意图(该图片来源:《Java 并发编程的艺术》):
在这里插入图片描述
说明:

  1. 如果当前运行的线程数少于 corePoolSize,则创建一个新的线程执行任务;
  2. 当前线程池中有一个运行的线程后,将任务加入 LinkedBlockingQueue
  3. 线程执行完当前的任务后,会在循环中反复从LinkedBlockingQueue 中获取任务来执行;

2.3 缺点

SingleThreadExecutor 使用无界队列 LinkedBlockingQueue (队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列。SingleThreadExecutor 使用无界队列作为线程池的工作队列会对线程池带来的影响与 FixedThreadPool 相同。说简单点就是可能会导致 OOM

3. newCachedThreadPool

3.1 介绍

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

特点:工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger.MAX__VALUE),这样可灵活的往线程池中添加线程。
如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。

/**
     * 创建一个线程池,根据需要创建新线程,但会在先前构建的线程可用时重用它。
     */
    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }

CachedThreadPoolcorePoolSize 被设置为空(0),maximumPoolSize被设置为 Integer.MAX_VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于 maximumPool 中线程处理任务的速度时,CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。

3.2 执行过程

CachedThreadPoolexecute() 方法的执行示意图(该图片来源:《Java 并发编程的艺术》):

在这里插入图片描述
说明:

  1. 首先执行 SynchronousQueue.offer(Runnable task) 提交任务到任务队列。如果当前 maximumPool 中有空闲线程正在执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer 操作与空闲线程执行的 poll 操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成,否则执行下面的步骤 2;
  2. 当初始 maximumPool 为空,或者 maximumPool 中没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤 1 将失败,此时 CachedThreadPool 会创建新线程执行任务,execute 方法执行完成;

3.3 缺点

在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造
成系统OOM。

4. ScheduledThreadPoolExecutor

4.1 介绍

ScheduledThreadPoolExecutor 主要用来在给定的延迟后运行任务,或者周期性执行任务。 这个在实际项目中基本不会被用到,也不推荐使用,大家只需要简单了解一下即可。

ScheduledThreadPoolExecutor 使用的任务队列 DelayQueue 封装了一个 PriorityQueuePriorityQueue 会对队列中的任务进行排序,执行所需时间短的放在前面先被执行(ScheduledFutureTasktime 变量小的先执行),如果执行所需时间相同则先提交的任务将被先执行(ScheduledFutureTasksquenceNumber 变量小的先执行)。

4.2 执行过程

在这里插入图片描述
ScheduledThreadPoolExecutor 的执行主要分为两大部分:

  1. 当调用 ScheduledThreadPoolExecutorscheduleAtFixedRate() 方法或者 scheduleWithFixedDelay() 方法时,会向 ScheduledThreadPoolExecutorDelayQueue 添加一个实现了 RunnableScheduledFuture 接口的 ScheduledFutureTask
  2. 线程池中的线程从 DelayQueue 中获取 ScheduledFutureTask,然后执行任务。

ScheduledThreadPoolExecutor 为了实现周期性的执行任务,对 ThreadPoolExecutor做了如下修改:

  • 使用 DelayQueue 作为任务队列
  • 获取任务的方式不同
  • 执行周期任务后,增加了额外的处理

4.3 ScheduledThreadPoolExecutor 执行周期任务的步骤

在这里插入图片描述

  1. 线程 1 从 DelayQueue 中获取已到期的 ScheduledFutureTask(DelayQueue.take())。到期任务是指 ScheduledFutureTask的 time 大于等于当前系统的时间;
  2. 线程 1 执行这个 ScheduledFutureTask
  3. 线程 1 修改 ScheduledFutureTask 的 time 变量为下次将要被执行的时间;
  4. 线程 1 把这个修改 time 之后的 ScheduledFutureTask 放回 DelayQueue 中(DelayQueue.add())。

七、几种常见的对比

1. Runnable vs Callable

Runnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。

  • Callable规定重写的方法是call()Runnable规定重写的方法是run()
  • Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
  • Call()方法可以抛出异常,run()方法不可以。
  • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的
    方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任
    务的执行,还可获取执行结果。

工具类 Executors 可以实现将 Runnable 对象转换成 Callable 对象。(Executors.callable(Runnable task)Executors.callable(Runnable task, Object result))。

Runnable.java

@FunctionalInterface
public interface Runnable {
   /**
    * 被线程执行,没有返回值也无法抛出异常
    */
    public abstract void run();
}

Callable.java

@FunctionalInterface
public interface Callable<V> {
    /**
     * 计算结果,或在无法这样做时抛出异常。
     * @return 计算得出的结果
     * @throws 如果无法计算结果,则抛出异常
     */
    V call() throws Exception;
}

2. execute() vs submit()

  • execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  • submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法的话,如果在 timeout 时间内任务还没有执行完,就会抛出 java.util.concurrent.TimeoutException

示例1:

ExecutorService executorService = Executors.newFixedThreadPool(3);

Future<String> submit = executorService.submit(() -> {
    try {
        Thread.sleep(5000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "abc";
});

String s = submit.get();
System.out.println(s);
executorService.shutdown();

输出:

abc

示例2:

ExecutorService executorService = Executors.newFixedThreadPool(3);

Future<String> submit = executorService.submit(() -> {
    try {
        Thread.sleep(5000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "abc";
});

String s = submit.get(3, TimeUnit.SECONDS);
System.out.println(s);
executorService.shutdown();

输出:
在这里插入图片描述

3. shutdown() VS shutdownNow()

  • shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
  • shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。

shutdownNow()的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt()方法来中断线程,所以无法响应中断的任务可能无法终止。

4. isTerminated() VS isShutdown()

  • isShutDown 当调用 shutdown() 方法后返回为 true。
  • isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true

八、线程池大小确定

线程池的线程数量并不是越大越好。 就比如:并不是人多就能把事情做好,反而增加了沟通交流成本。你本来一件事情只需要 3 个人做,你硬是拉来了 6 个人,会提升做事效率嘛?应该并不会。 线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了上下文切换成本。

上下文切换:

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

线程池大小设置过大或者过小都会有问题,合适的才是最好。

如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。

但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。

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

  • 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 操作完成上。

九、综合案例

1. 商品秒杀

假如某网上商城推出活动,新上架10部新手机免费送客户体验,要求所有参与活动的人员在规定的时间同时参与秒杀挣抢。假如有20人同时参与了该活动,请使用线程池模拟这个场景,保证前10人秒杀成功,后10人秒杀失败。

思路:

  1. 既然商品总数量是10个,那么我们可以在创建线程池的时候初始化线程数是10个及以下,设计线程池最大数量为10个;
  2. 当某个线程执行完任务之后,可以让其他秒杀的人继续使用该线程参与秒杀;
  3. 使用synchronized控制线程安全,防止出现错误数据;

代码实现:

编写任务类MyTask.java

//任务类:包含了商品数量,客户名称,送手机的行为;
public class MyTask implements Runnable {
    //设计一个变量,用于表示商品的数量
    private static int id = 10;
    //表示客户名称的变量
    private String userName;

    public MyTask(String userName) {
        this.userName = userName;
    }

    @Override
    public void run() {
        //获取线程名称
        String name = Thread.currentThread().getName();
        System.out.println(userName+"正在使用"+name+"参与秒杀任务...");
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (MyTask.class){
            if(id>0){
                System.out.println(userName+"使用"+name+"秒杀:"+id-- +"号商品成功啦!");
            }else {
                System.out.println(userName+"使用"+name+"秒杀失败啦!");
            }
        }
    }
}

编写主程序类MyTest

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
//主程序类,测试任务类
public class MyTest {
    private static final int CORE_POOL_SIZE = 5;     //核心线程数
    private static final int MAX_POOL_SIZE = 10;     //最大线程数量
    private static final int QUEUE_CAPACITY = 10;   //任务队列长度
    private static final Long KEEP_ALIVE_TIME = 1L;  //最大空闲时间
    public static void main(String[] args) {
        //1:创建一个线程池对象
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME,
                TimeUnit.MINUTES,
                new ArrayBlockingQueue<>(QUEUE_CAPACITY)
        );
        //2:循环创建任务对象
        for (int i = 1; i <=20 ; i++) {
            MyTask myTask = new MyTask("客户"+i);
            pool.submit(myTask);
        }
        //3:关闭线程池
        pool.shutdown();
    }
}

运行结果:

//输出
客户1正在使用pool-1-thread-1参与秒杀任务...
客户2正在使用pool-1-thread-2参与秒杀任务...
客户5正在使用pool-1-thread-5参与秒杀任务...
客户18正在使用pool-1-thread-8参与秒杀任务...
客户4正在使用pool-1-thread-4参与秒杀任务...
客户17正在使用pool-1-thread-7参与秒杀任务...
客户3正在使用pool-1-thread-3参与秒杀任务...
客户16正在使用pool-1-thread-6参与秒杀任务...
客户19正在使用pool-1-thread-9参与秒杀任务...
客户20正在使用pool-1-thread-10参与秒杀任务...
客户20使用pool-1-thread-10秒杀:10号商品成功啦!
客户19使用pool-1-thread-9秒杀:9号商品成功啦!
客户5使用pool-1-thread-5秒杀:8号商品成功啦!
客户16使用pool-1-thread-6秒杀:7号商品成功啦!
客户2使用pool-1-thread-2秒杀:6号商品成功啦!
客户7正在使用pool-1-thread-2参与秒杀任务...
客户1使用pool-1-thread-1秒杀:5号商品成功啦!
客户8正在使用pool-1-thread-1参与秒杀任务...
客户3使用pool-1-thread-3秒杀:4号商品成功啦!
客户10正在使用pool-1-thread-9参与秒杀任务...
客户11正在使用pool-1-thread-10参与秒杀任务...
客户9正在使用pool-1-thread-5参与秒杀任务...
客户6正在使用pool-1-thread-6参与秒杀任务...
客户18使用pool-1-thread-8秒杀:3号商品成功啦!
客户12正在使用pool-1-thread-3参与秒杀任务...
客户4使用pool-1-thread-4秒杀:2号商品成功啦!
客户13正在使用pool-1-thread-8参与秒杀任务...
客户17使用pool-1-thread-7秒杀:1号商品成功啦!
客户15正在使用pool-1-thread-7参与秒杀任务...
客户14正在使用pool-1-thread-4参与秒杀任务...
客户8使用pool-1-thread-1秒杀失败啦!
客户6使用pool-1-thread-6秒杀失败啦!
客户14使用pool-1-thread-4秒杀失败啦!
客户9使用pool-1-thread-5秒杀失败啦!
客户11使用pool-1-thread-10秒杀失败啦!
客户12使用pool-1-thread-3秒杀失败啦!
客户13使用pool-1-thread-8秒杀失败啦!
客户10使用pool-1-thread-9秒杀失败啦!
客户7使用pool-1-thread-2秒杀失败啦!
客户15使用pool-1-thread-7秒杀失败啦!

2. 取款业务

设计一个程序,使用两个线程模拟在两个地点同时从一个账号中取钱。假如卡中一共有1000元,每个线程取800元,要求演示结果一个线程取款成功,剩余200元;另一个线程取款失败,余额不足。

思路:

  1. 线程池可以利用Executors工厂类的静态方法,创建线程池对象;
  2. 解决线程安全问题可以使用synchronized方法控制取钱的操作;
  3. 在取款前,先判断余额是否足够,且保证余额判断和取钱行为的原子性;

代码实现:

编写任务类MyTask.java

public class MyTask implements Runnable {
    //用户姓名
    private String userName;
    //取款金额
    private double money;
    //总金额
    private static double total = 1000;

    public MyTask(String userName, double money) {
        this.userName = userName;
        this.money = money;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        System.out.println(userName+"正在准备使用"+name+"取款:"+money+"元");
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (MyTask.class){
            if(total-money>0){
                System.out.println(userName+"使用"+name+"取款:"+money+"元成功,余额:"+(total-money));
                total-=money;
            }else {
                System.out.println(userName+"使用"+name+"取款:"+money+"元失败,余额:"+total);
            }
        }
    }
}

编写主程序类MyTest

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

public class MyTest {
    public static void main(String[] args) {
        //1:创建线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(2, new ThreadFactory() {
            int id = 1;

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "ATM" + id++);
            }
        });
        //2:创建两个任务并提交
        for (int i = 1; i <=2 ; i++) {
            MyTask myTask = new MyTask("客户" + i, 800);
            pool.submit(myTask);
        }
        //3:关闭线程池
        pool.shutdown();
    }
}

运行结果:
在这里插入图片描述

十、线程池最佳实践

1、使用 ThreadPoolExecutor 的构造函数声明线程池

线程池必须手动通过 ThreadPoolExecutor 的构造函数来声明,避免使用Executors 类的newFixedThreadPoolnewCachedThreadPool ,因为可能会有 OOM 的风险。

说白了就是:使用有界队列,控制线程创建数量。

除了避免 OOM 的原因之外,不推荐使用 Executors提供的两种快捷的线程池的原因还有:

  • 实际使用中需要根据自己机器的性能、业务场景来手动配置线程池的参数比如核心线程数、使用的任务队列、饱和策略等等。
  • 我们应该显示地给我们的线程池命名,这样有助于我们定位问题。

2、监测线程池运行状态

可以通过一些手段来监测线程池的运行状态,比如 SpringBoot 中的 Actuator 组件。

除此之外,我们还可以利用 ThreadPoolExecutor 的相关API做一个简陋的监控。从下图可以看出, ThreadPoolExecutor提供了获取线程池当前的线程数和活跃线程数、已经执行完成的任务数、正在排队中的任务数等等。

在这里插入图片描述

下面是一个简单的 Demo。printThreadPoolStatus()会每隔一秒打印出线程池的线程数、活跃线程数、完成的任务数、以及队列中的任务数。

/**
     * 打印线程池的状态
     * @param threadPool 线程池对象
*/
    public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) {
        ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, createThreadFactory("print-images/thread-pool-status", false));
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            log.info("=========================");
            log.info("ThreadPool Size: [{}]", threadPool.getPoolSize());
            log.info("Active Threads: {}", threadPool.getActiveCount());
            log.info("Number of Tasks : {}", threadPool.getCompletedTaskCount());
            log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size());
            log.info("=========================");
        }, 0, 1, TimeUnit.SECONDS);
    }

3、建议不同类别的业务使用不同的线程池

一般建议是不同的业务使用不同的线程池,配置线程池的时候根据当前业务的情况对当前线程池进行配置,因为不同的业务的并发以及对资源的使用情况都不同,重心优化系统性能瓶颈相关的业务。

试想这样一种极端情况:假如我们线程池的核心线程数为 n,父任务数量为 n,父任务下面有两个子任务,其中一个已经执行完成,另外一个被放在了任务队列中。由于父任务把线程池核心线程资源用完,所以子任务因为无法获取到线程资源无法正常执行,一直被阻塞在队列中。父任务等待子任务执行完成,而子任务等待父任务释放线程池资源,这也就造成了 “死锁”

在这里插入图片描述
解决方法也很简单,就是新增加一个用于执行子任务的线程池专门为其服务。

4、别忘记给线程池命名

初始化线程池的时候需要显示命名(设置线程池名称前缀),有利于定位问题

默认情况下创建的线程名字类似 pool-1-thread-n 这样的,没有业务含义,不利于我们定位问题。

给线程池里的线程命名通常有下面两种方式:

  • 利用 guava 的 ThreadFactoryBuilder
ThreadFactory threadFactory = new ThreadFactoryBuilder()
                        .setNameFormat(threadNamePrefix + "-%d")
                        .setDaemon(true).build();
ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory)
  • 自己实现ThreadFactory
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * 线程工厂,它设置线程名称,有利于我们定位问题。
 */
public final class NamingThreadFactory implements ThreadFactory {

    private final AtomicInteger threadNum = new AtomicInteger();
    private final ThreadFactory delegate;
    private final String name;

    /**
     * 创建一个带名字的线程池生产工厂
     */
    public NamingThreadFactory(ThreadFactory delegate, String name) {
        this.delegate = delegate;
        this.name = name; // TODO consider uniquifying this
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = delegate.newThread(r);
        t.setName(name + " [#" + threadNum.incrementAndGet() + "]");
        return t;
    }
}

5、正确配置线程池参数

参考链接:
https://javaguide.cn/java/concurrent/java-thread-pool-summary.html#%E4%B8%80-%E4%BD%BF%E7%94%A8%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E5%A5%BD%E5%A4%84

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值