JUC第23讲:Java线程池最佳实践

news2024/9/30 1:14:52

JUC第23讲:Java线程池最佳实践

本文是JUC第23讲,先介绍为什么使用线程池;然后结合实际业务,讲解如何使用线程池,以及使用过程中踩过的坑。

1、Java线程池概述

1.1、什么是线程池?

线程池是一种用于管理和复用线程的机制。

线程池的主要组成部分包括工作线程、任务队列、线程管理器等。线程池的设计有助于优化多线程程序的性能和资源利用,同时简化了线程的管理和复用的复杂性。

1.2、为什么要有线程池?

线程池能够对线程进行统一分配,调优和监控:

  • 控制和优化系统资源利用,线程池通过控制线程的数量,可以尽可能地压榨机器性能,提高系统资源利用率;
  • 提高响应速度,线程池可以预先创建线程且通过多线程并发处理任务,提升任务的响应速度及系统的并发性能;
  • 减少线程创建和销毁的开销,线程的创建和销毁需要消耗系统资源,线程池通过复用线程,避免了对资源的频繁操作,从而提高系统性能;

2、Java线程池执行流程

图片

问题1:线程池的核心线程可以回收吗?

ThreadPoolExecutor 默认不回收核心线程,但是提供了 allowCoreThreadTimeOut(boolean value) 方法,当参数为true时,可以在达到线程空闲时间后,回收核心线程,在业务代码中,如果线程池是周期性的使用,可以考虑将该参数设置为true;

ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 5,
                5L, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1));
// 默认情况下,核心线程不会因为空闲而被终止,我们可以通过以下方法改变这一行为
executor.allowCoreThreadTimeOut(true);

见demo10 核心线程回收

问题2:线程池在提交任务前,可以提前创建线程吗?

ThreadPoolExecutor 提供了两个方法:

1. prestartCoreThread(): 启动一个线程,等待任务,如果核心线程数已达到,这个方法返回false,否则返回true;

2. prestartAllCoreThreads(): 启动所有的核心线程,返回启动成功的核心线程数 。

通过这种设置,可以在提交任务前,完成核心线程的创建,从而实现线程池预热的效果;

// 预热单个核心线程
executorService.prestartCoreThread();
// 输出当前核心线程数
Thread.sleep(100);
System.out.println("Active threads after prestarting one core thread: " + executorService.getPoolSize());

// 预热所有核心线程
executorService.prestartAllCoreThreads();
Thread.sleep(2000);
// 输出当前核心线程数
System.out.println("Active threads after prestarting all core threads: " + executorService.getPoolSize());

见demo11 核心线程池预热

2.1、业务中如何使用线程池

场景1:需要快速响应用户请求

描述:用户发起的实时请求,服务追求响应时间。比如说用户要查看一个商品的信息,那么我们需要将商品维度的一系列信息如商品的价格、优惠、库存、图片等等聚合起来,展示给用户。

// 背景:获取sku信息 0.5s  获取图片信息 0.5s  获取类目属性信息 1s  获取库存信息 0.5s  获取商品详情信息 1s
// 如果顺序调用,那么,用户需要 3.5s 后才能看到商品详情页的内容,很显然是不能接受的。
// 如果有线程池同时完成这 5 步操作,也许只需要 1s 即可完成响应。
// TPS目标 1500  
// 机器配置:4C10G(JVM 6G)(6台)
// 核心数:读操作线程池,核心线程数和最大线程数:200个,阻塞队列长度:10000,并通过配置中心可以动态调整线程数和队列长度,保活时间为0,重写线程工厂
// 拒绝策略:重写拒绝策略,跟CallerRunsPolicy一样,超出后需要调用者线程处理,多了记录日志
// demo14
 public TestItemDetailThreadPool(int threadSize, int queueLength) {
        super(threadSize, threadSize, 0, TimeUnit.SECONDS, new LinkedBlockingDeque<>(queueLength),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r,
                                NAME + "-" + threadNum.getAndDecrement());
                    }
                }, new RewriteCallerRunsPolicy());
    }

    /**
     * 默认线程大小
     */
    public static final int DEFAULT_THREAD_SIZE = 100;

    /**
     * 默认队列大小
     */
    public static final int DEFAULT_QUEUE_SIZE = 10000;

    private static final AtomicInteger threadNum = new AtomicInteger(1);
}

/**
 * 线程池已经无法处理,重写饱和策略,跟CallerRunsPolicy一样,超出后需要调用者自己的线程处理,多了记录日志
 */
private static class RewriteCallerRunsPolicy implements RejectedExecutionHandler {

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        log.error("线程池:{} 需要处理的任务已经超过任务队列长度(当前队列长度:{}), 需要当前的工作线程自行处理当前任务", NAME, executor.getQueue().size());
        if (!executor.isShutdown()) {
            r.run();
        }
    }
}

场景2:快速处理批量任务

描述:网络货运项目,需要实现批量OCR识图功能,识图使用了腾讯云提供的功能,识别比较慢(大概2~3s),使用的业务方有运营平台和司机端,因此使用多线程加速OCR识图。

// 背景:司机端上传的认证信息,需要支持OCR识别,识别如下信息:身份证、行驶证、驾驶证识别;从业资格证、道路运输证,用户注册信息会批量上传
// 因此,使用ocr识图专用线程池,快速处理批量任务
// 机器配置:2C4G JVM(2G) 
// 核心数:核心线程数和最大线程数:5个,阻塞队列长度:100,保活时间为0,重写线程工厂
// 拒绝策略:重写拒绝策略,跟AbortPolicy一样,超出后直接抛出异常,多了记录日志
// 见demo12
@Bean(name = "asyncImgOcrThreadPool")
public Executor asyncImgOcrThreadPool() {
    final AtomicInteger threadNum = new AtomicInteger(1);
    return new LazyTraceThreadPoolExecutor(
            5,
            5,
            0,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100),
            r -> new Thread(r, "asyncImgOcrThreadPool-" + threadNum.getAndDecrement()),
            (r, executor) -> {
                log.error("线程池:{} 需要处理的任务已经超过任务队列长度(当前队列长度:{}), 直接抛出异常", "asyncImgOcrThreadPool", executor.getQueue().size());
                throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + executor);
            });
}

场景3:延迟执行线程池

描述:在智能策略中可以选择智能系统设置策略的执行条件,执行动作选择智能系统的操作,可通过定时、实时与手动方式执行策略。例如:创建一个每天中午12点执行一次,每次执行间隔为1小时的任务,执行5次。

// 背景:管理员填写智能策略,任务按配置周期执行,调用量较少
// 机器配置:无权限查看
// 核心数:核心线程数1,最大线程数:Integer.MAX_VALUE,阻塞队列长度:Integer.MAX_VALUE,保活时间为60s,
// 拒绝策略:默认的拒绝策略,AbortPolicy,超出后直接抛出异常
// 待改进的地方:由于阻塞队列为MAX_VALUE,存在OOM的风险,应该使用自定义线程池配置
// demo15
public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport implements AsyncListenableTaskExecutor, SchedulingTaskExecutor {
    private final Object poolSizeMonitor = new Object();
    private int corePoolSize = 1;
    private int maxPoolSize = Integer.MAX_VALUE; 
    private int keepAliveSeconds = 60;
    private int queueCapacity = Integer.MAX_VALUE;
    private boolean allowCoreThreadTimeOut = false;
}

3、Java线程池使用注意事项

问题1:如何选择合适的线程池参数?

public ThreadPoolExecutor(int corePoolSize, // 核心线程数
                              int maximumPoolSize, //最大线程数
                              long keepAliveTime, // 线程的空闲时间
                              TimeUnit unit, // 空闲时间的单位(秒、分、小时等等)
                              BlockingQueue<Runnable> workQueue, // 等待队列
                              ThreadFactory threadFactory, // 线程工厂
                              RejectedExecutionHandler handler) // 拒绝策略
// 拒绝策略
ThreadPoolExecutor.AbortPolicy //丢弃任务并抛出RejectedExecutionException异
ThreadPoolExecutor.DiscardPolicy //也是丢弃任务,但是不抛出异常
ThreadPoolExecutor.DiscardOldestPolicy //丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy //由调用线程直接处理该任务(可能为主线程Main),保证每个任务执行完毕

推荐使用自定义的线程工厂,重写创建线程的方法,支持自定义创建线程的名称、优先级等属性,方便排查问题。

自己扩展RejectedExecutionHandler接口,自定义拒绝策略

见场景1和场景2

问题2:如何选择合适的线程池参数?

1)根据任务场景选择

CPU 密集型任务(N+1) 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

I/O 密集型任务(2N) 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

2)根据线程池用途选择

用途一:快速响应用户请求

比如说用户查询商品详情页,会涉及查询商品关联的一系列信息如价格、优惠、库存、基础信息等,站在用户体验的角度,希望商详页的响应时间越短越好,此时可以考虑使用线程池并发地查询价格、优惠、库存等信息,再聚合结果返回,降低接口总rt。这种线程池用途追求的是最快响应速度,所以可以考虑不设置队列去缓冲并发任务,而是尽可能设置更大的corePoolSize和maxPoolSize

用途二:快速处理批量任务

比如说项目中在对接渠道同步商品供给时,需要查询大量的商品数据并同步给渠道,此时可以考虑使用线程池快速处理批量任务。这种线程池用途关注的是如何使用有限的机器资源,尽可能地在单位时间内处理更多的任务,提升系统吞吐量,所以需要设置阻塞队列缓冲任务,并根据任务场景调整合适的corePoolSize

问题3:如何正确地创建线程池对象?

第一点:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,同时规避资源耗尽的风险

Executors各个方法的弊端:

Executors是一个 java.util.concurrent 包中的工具类,可以方便的为我们创建几种特定参数的线程池。

  • FixedThreadPool:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM
  • CachedThreadPool:主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM
  • SingleThreadPool:单个线程的线程,核心线程数和最大线程数都是1,无界阻塞队列

第二点:推荐使用饿汉式的单例模式创建线程池对象,支持灵活的参数配置

public class TestThreadPool {

    /**
     * 线程池
     */
    private static ExecutorService executor = initDefaultExecutor();

    /**
     * 统一的获取线程池对象方法
     */
    public static ExecutorService getExecutor() {
        return executor;
    }

    private static final int DEFAULT_THREAD_SIZE = 16;
    private static final int DEFAULT_QUEUE_SIZE = 10240;

    private static ExecutorService initDefaultExecutor() {
        return new ThreadPoolExecutor(DEFAULT_THREAD_SIZE, DEFAULT_THREAD_SIZE,
                300, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(DEFAULT_QUEUE_SIZE),
                new ThreadPoolExecutor.CallerRunsPolicy());
    }
}

4、实战经验

4.1、相互依赖的子任务避免使用同一线程池

相互依赖的任务提交到同一线程池,父任务依赖子任务的执行结果,父任务get()执行结果时可能因为子任务还没执行完导致线程阻塞,如果提交的任务过多,线程池中的线程都被类似的父任务占用并阻塞,导致任务队列中的子任务没有线程去执行,最终出现线程饥饿的死锁现象

// 见demo3
// 存在的问题:父任务必须拿到子任务执行结果,才能结束
public static ExecutorService executor= TestThreadPool.getExecutor();

public static void main(String[] args) throws Exception {
    FatherTask fatherTask = new FatherTask();
    Future<String> future = executor.submit(fatherTask);
    future.get();
}

/**
 * 父任务,里面异步执行子任务
 */
static class FatherTask implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("开始执行父任务");
        SonTask sonTask = new SonTask();
        Future<String> future = executor.submit(sonTask);
        String s = future.get();
        System.out.println("父任务已拿到子任务执行结果");
        return s;
    }
}
/**
 * 子任务
 */
static class SonTask implements Callable<String> {
    @Override
    public String call() throws Exception {
        //处理一些业务逻辑
        System.out.println("子任务执行完成");
        return null;
    }
}

优化思路:

1)使用不同的线程池隔离有相互依赖的任务;

2)调用future.get()方法设置超时时间,这样做可以避免线程阻塞,但是依然会出现大量的超时异常。

4.2、合理选择submit()和execute()方法

execute(Runnable r):没有返回值,仅仅是把一个任务提交给线程池处理,轻量级方法,适用于处理不需要返回结果的任务;

submit(Runnable r):返回值为Future类型,future可以用来检查任务是否已经完成,获取任务的结果等,适用于需要处理返回结果的任务;

// demo13
// 线程池submit “吃”掉程序抛出的异常,导致我们对程序的错误一无所知。
// 程序没有任何日志记录,也没有任何错误提示,就好像一切正常。
public class TestTraceThreadpool {
    public static class DivTask implements Runnable {
        int a, b;
        public DivTask(int a, int b) {
            this.a = a;
            this.b = b;
        }
        @Override
        public void run() {
            double re = a / b;
            System.out.println(re); // 结果 25.0  33.0  50.0  100.0
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(
                3,
                5,
                5,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(1));
        // 存在问题的代码
        for (int i = 0; i < 5; i++) {
            executorService.submit(new DivTask(100, i));
        }
    }
}

问题1:为什么线程池内部的submit方法不抛出异常?

追踪代码到 futureTask

    public void run() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }

此时我们发现,异常已经被捕获了,并且调用了setexception方法。把我们的异常信息给了outcome。

    protected void setException(Throwable t) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = t;
            UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
            finishCompletion();
        }
    }

到此我们就明白了为什么submit没有抛出异常了!

异常堆栈缺失问题以及解决方法

解法1:不使用submit,而是使用execute方法,或者使用future接收返回值 ❎

  • 存在的问题:只能拿到部分堆栈信息,无法拿到线程池的调用者信息

解法2:使用增强trace的ThreadPool ✅

  • // 能拿到全部堆栈信息
    // demo13
    public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }
    
    @Override
    public void execute(Runnable command) {
        super.execute(wrap(command, clientTrace(), Thread.currentThread().getName()));
    }
    
    @Override
    public Future<?> submit(Runnable task) {
        return super.submit(wrap(task, clientTrace(), Thread.currentThread().getName()));
    }
    
    private Exception clientTrace() {
        return new Exception("client stack trace");
    }
    
    private Runnable wrap(Runnable task, Exception clientTrace, String clientThreadName) {
        return new Runnable() {
            @Override
            public void run() {
                try {
                    task.run();
                } catch (Exception e) {
                    clientTrace.printStackTrace();
                    throw e;
                }
            }
        };
    }
    

4.3、请捕获线程池中子任务的代码异常

// 见demo2
// 如果线程池中执行任务的线程异常,发生异常的线程会销毁吗?其他任务还能正常执行吗?
public static ExecutorService executor = TestThreadPool.getExecutor();

public static void main(String[] args) {

    executor.execute(() -> test("正常"));
    executor.execute(() -> test("正常"));
    executor.execute(() -> test("任务执行异常"));
    executor.execute(() -> test("正常"));
    executor.shutdown();
}

public static void test(String str) {
    String result = "当前ThreadName为" + Thread.currentThread().getName() + ":结果" + str;
    if (str.equals("任务执行异常")) {
        throw new RuntimeException(result + "****执行异常");
    } else {
        System.out.println(result);
    }
}

问题1:如果线程池中执行任务的线程异常,发生异常的线程会销毁吗?其他任务还能正常执行吗?

image-20240924161943403

可以发现

1)线程池中执行任务的线程异常,并不会影响其他任务的执行,而且execute()提交任务,直接打印了异常信息。

底层实现:会调用workers.remove()移除当前线程,并调用addWorker()重新创建新的线程。

结论业务代码中,请捕获子任务中的异常,否则会导致线程池中的工作线程频繁销毁、创建,造成资源浪费,违背了线程复用的设计原则。

5、踩坑记录

5.1、CallerRunsPolicy 拒绝策略踩坑,触发CallerRunsPolicy拒绝策略执行的任务,future.get(timeout)超时时间无效(会造成线上问题)

现象:拒绝策略配置为CallerRun,交给主线程去执行,在使用Future获取线程执行结果时,线程3并不是执行3s,而是10秒后得到响应结果。

// demo7
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS
                , new ArrayBlockingQueue<>(1)
                , new ThreadPoolExecutor.CallerRunsPolicy());
        executor.submit(() -> {
            System.out.println("线程1开始执行");
            sleepQuietly(2 * 60 * 1000);
            System.out.println("线程1结束执行");
        });
        executor.submit(() -> {
            System.out.println("线程2开始执行");
            sleepQuietly(2 * 60 * 1000);
            System.out.println("线程2结束执行");
        });
        long start = System.currentTimeMillis();
        Future<String> future = executor.submit(() -> {
            System.out.println("线程3开始执行");
            sleepQuietly(10 * 1000);
            System.out.println("线程3结束执行");
            return LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
        });
        System.out.println("获取线程3执行结果");
        String result = future.get(3, TimeUnit.SECONDS);
        System.out.printf("线程3执行结果:%s, 耗时:%d秒", result, (System.currentTimeMillis() - start) / 1000);

执行结果

image-20240924163026319

原因:如果配置为 CallerRun 拒绝策略,在最大线程池满了后,交给主线程执行,因此专给线程池配置的超时时间会失效,实际执行的如下这个方法,只取决于线程执行时间

 public V get() throws InterruptedException, ExecutionException {
     int s = state;
     if (s <= COMPLETING)
       s = awaitDone(false, 0L);
     return report(s);
 }

5.2、触发 DiscardPolicy 拒绝策略的任务,future.get() 将一直阻塞

现象:如果future.get() 没有配置超时时间,当前线程会一直阻塞,不可用

// demo6
// 我们的期望是:丢弃无法处理的任务,且不会抛出异常
// 但是实际上:future.get() 将一直阻塞
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 10, TimeUnit.SECONDS
                , new ArrayBlockingQueue<>(1)
                , new ThreadPoolExecutor.DiscardPolicy() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
                System.out.println("丢弃提交的任务");
                super.rejectedExecution(r, e);
            }
        });
        executor.submit(() -> {
            System.out.println("线程1开始执行");
            sleepQuietly(10 * 1000);
            System.out.println("线程1结束执行");
        });
        executor.submit(() -> {
            System.out.println("线程2开始执行");
            sleepQuietly(10 * 1000);
            System.out.println("线程2结束执行");
        });
        long start = System.currentTimeMillis();
        Future<String> future = executor.submit(() -> {
            System.out.println("线程3开始执行");
            sleepQuietly(10 * 1000);
            System.out.println("线程3结束执行");
            return LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
        });
        System.out.println("获取线程3执行结果");
        String result = future.get();
        System.out.printf("[永远不会执行]线程3执行结果:%s, 耗时:%d秒", result, (System.currentTimeMillis() - start) / 1000);

执行结果

image-20240924165210761

原因:线程的状态不是完成状态,会被await掉(底层为LockSupport的park方法),除非被唤醒

解法:捕获异常

5.3、大量线程池实例导致内存上涨,ThreadPoolExecutor重写了finalize方法,JVM对于此类对象的回收有专有机制,但缺点之一是回收延迟(从变为可回收到被真正回收的时间间隔)不确定。

案例如下

// demo4
sleepQuietly(2000);
int i = 0;
while (i++ < 100000) {
    sleepQuietly(10);
    Executors.newSingleThreadExecutor().submit(() -> {
        System.out.println(RandomUtils.nextDouble());
    });
}

开始执行后通过jmap命令多次查看Finalizer和ThreadPoolExecutor的数量:

jmap -histo jps | grep ThreadPoolDiscard | awk 'NR==1{print $1}' | grep -E ‘ThreadPoolExecutor$ | Finalizer$’

jmap -histo 29996 | grep -E ‘ThreadPoolExecutor$ | Finalizer$’ (先由jps找到对应的进程号)

image.png

最终JVM也发生OOM:

image.png

可以参考这篇文章:JEP 421: Deprecate Finalization for Removal

这个问题带来两个启发:

1)不要在代码中使用局部变量定义线程池对象,这样不仅会导致频繁创建线程池对象,违背了线程复用的设计原则,还有可能造成局部变量的线程池对象无法及时垃圾回收的内存泄漏问题;

2)业务代码中,优先定义静态内部类而不是非静态内部类,可以有效防止内存泄露的风险。

5.4、Spring的@Async注解,如果不使用自定义线程池,默认为核心数8、阻塞队列长度为Integer.MAX_VALUE,会有OOM的风险

解法:应该定义一个自定义的线程池配置,并限制线程数和队列长度。

以下是如何在Spring中配置自定义线程池的示例:

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Bean(name = "taskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10); // 设置核心线程数
        executor.setMaxPoolSize(20); // 设置最大线程数
        executor.setQueueCapacity(100); // 设置队列长度
        executor.setKeepAliveSeconds(60); // 设置线程空闲时间
        executor.setThreadNamePrefix("Async-"); // 设置线程名前缀
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 设置拒绝策略
        executor.initialize(); // 初始化线程池
        return executor;
    }
}

使用@Async注解时,你可以指定使用这个自定义的线程池,通过这种方式,你可以更好地控制线程池的行为,避免潜在的OOM风险。

@Service
public class AsyncService {

    @Async("taskExecutor")
    public CompletableFuture<String> asyncMethodWithConfiguredExecutor() {
        // ... 方法实现 ...
    }
}

5.5、创建线程或线程池时请指定有意义的线程名称,方便出错时排查

正例:自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给 whatFeatureOfGroup:

public class UserThreadFactory implements ThreadFactory {
    private final String namePrefix;
    private final AtomicInteger nextId = new AtomicInteger(1);
    // 定义线程组名称, 在利用 jstack 来排查问题时, 非常有帮助
    UserThreadFactory(String whatFeatureOfGroup) {
      	namePrefix = "FromUserThreadFactory's" + whatFeatureOfGroup + "-Worker-";
    }
    @Override
    public Thread newThread(Runnable task) {
        String name = namePrefix + nextId.getAndIncrement();
        Thread thread = new Thread(null, task, name, 0, false);
        System.out.println(thread.getName());
        return thread;
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2178300.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

彩虹易支付最新版源码及安装教程(修复BUG+新增加订单投诉功能)

该系统也没版本号&#xff0c;此版本目前是比较新的版本&#xff0c;增加了订单投诉功能&#xff0c;和一个好看的二次元模板。 此版本是全开源版&#xff0c;无一处加密文件,系统默认是安装后是打不开的&#xff0c; 本站特别修复了BUG文件&#xff0c;在PHP7.4环境下也没问…

数据结构和算法之树形结构(4)

文章出处&#xff1a;数据结构和算法之树形结构(4) 关注码农爱刷题&#xff0c;看更多技术文章&#xff01;&#xff01;&#xff01; 六、红黑树(接前篇) 红黑树是为了弥补AVL树在大规模频繁增删节点场景下性能不理想而设计出来的一种平衡二叉查找树。红黑树不是一种严…

cuda程序编译流程

cuda程序编译流程 本文以cuda example的matrixMul矩阵乘法为例说明cuda程序的编译流程。 1. 源代码 .cu 文件 在matrixMul示例中&#xff0c;源代码文件 matrixMul.cu 是典型的CUDA程序&#xff0c;包含以下部分&#xff1a; 流程图 主机代码&#xff08;Host Code&#xf…

Vivado - JTAG to AXI Master (GPIO、IIC、HLS_IP)

目录 1. 简介 2. JTAG to AXI Master 2.1 添加 IP Core 2.2 基本TCL命令 2.2.1 复位 JTAG-to-AXI Master 2.2.2 创建并运行写入传输事务 2.2.3 创建并运行读取传输事务 2.2.4 命令列表 2.3 帮助信息 2.4 创建TCL读写程序 2.4.1 Read proc 2.4.2 Write proc 2.4.3 …

嵌入式学习--LinuxDay03

嵌入式学习--LinuxDay03 shell脚本 1.1功能性语句 1.1.1说明性语句 1.1.2功能性语句 1&#xff09;read 2&#xff09;expr 3) test a)字符串 b)整数的测试 c)文件属性的测试 1.2结构性语句 1.2.1if语句 1.2.2case语句 1.2.3for循环 1.2.4while循环 1.2.5循环控制语句 shell脚本…

心觉:运用吸引力法则和开发潜意识的核心中的核心是什么?

吸引力法则的核心在于 思想的力量 和 频率的匹配。你所思考和感受的会吸引与你频率相匹配的事物和经历到你的生活中。具体来说&#xff1a; 明确意图和目标&#xff1a;清晰地知道你想要什么&#xff0c;并且用详细的方式描述它。这可以是通过写下目标、制作愿景板&#xff08;…

rocky9.2实现lvs(DR模式)+keepalived实现高可用的案例详解(双机热备、lvs负载均衡、对后端服务器健康检查)

文章目录 [TOC] 前言lvs(DR模式)的工作原理环境实现过程一、lvs1配置二、lvs2配置web1配置web2配置结果验证 总结 前言 想必能搜到这个也不是来看知识点的&#xff0c;这里就简单描述一下lvs的dr模式的工作原理&#xff0c;其他的就不过多阐述了,直接看操作步骤就好&#xff0…

rabbitMQ 简单使用

安装 rabbitMQ 下载地址&#xff1a;rabbitmq-3.12.0 安装 windows rabbitMQ 需要的命令 进入 rabbitMQ 的 sbin 目录后 cmd &#xff08;需要管理员权限&#xff09; rabbitmq-plugins.bat enable rabbitmq_management随后重启 rabbitMQ #关闭服务 net stop rabbitmq #开…

【机器学习(八)】分类和回归任务-因子分解机(Factorization Machines,FM)算法-Sentosa_DSML社区版

文章目录 一、算法概念二、算法原理&#xff08;一&#xff09; FM表达式&#xff08;二&#xff09;时间复杂度&#xff08;三&#xff09;回归和分类 三、算法优缺点&#xff08;一&#xff09;优点&#xff08;二&#xff09;缺点 四、FM分类任务实现对比&#xff08;一&…

YOLOV8在清微智能芯片的部署与实现(一)

现在以YOLOV8 为例&#xff0c;进行演示 文章目录 1. YOLOV8浮点模型训练1.1 准备数据集1.1.1 下载业务数据集1.1.2 下载开源数据集1.1.3 自定义数据集1.1.4 将数据转换为yolo训练数据格式 1.2 yolov8项目准备1.3 训练模型 2. YOLOV8浮点模型推理2.1 模型推理2.2 模型val.py评…

纯CSS实现有趣emoji切换开关

这是一个纯CSS创建的动画切换开关&#xff0c;它不仅能够在视觉上吸引用户&#xff0c;还能通过交互提供即时反馈。本文将解析源码的核心实现逻辑&#xff0c;这个项目的核心是使用CSS变量、3D变换和过渡效果来实现一个动态的、响应式的用户界面元素。 关键技术点 CSS变量&am…

[Python学习日记-31] Python 中的函数

[Python学习日记-31] Python 中的函数 简介 语法定义 函数的参数 简介 引子&#xff1a; 你是某公司的一个高级程序员&#xff0c;现在老板让你写一个监控程序&#xff0c;需要24小时全年无休的监控公司网站服务器的系统状况&#xff0c;当 CPU、Memory、Disk 等指标的使用…

基于SpringBoot+Vue+MySQL的体育商城系统

系统展示 用户前台界面 管理员后台界面 系统背景 随着互联网的飞速发展&#xff0c;电子商务已成为人们日常生活中不可或缺的一部分。体育用品市场作为其中的一个重要分支&#xff0c;也逐渐向线上转移。基于SpringBootVueMySQL的体育商城系统应运而生&#xff0c;旨在通过构建…

如何使用ssm实现基于Java的高校物业工程报修系统

TOC ssm736基于Java的高校物业工程报修系统jsp 绪论 1.1研究背景与意义 信息化管理模式是将行业中的工作流程由人工服务&#xff0c;逐渐转换为使用计算机技术的信息化管理服务。这种管理模式发展迅速&#xff0c;使用起来非常简单容易&#xff0c;用户甚至不用掌握相关的专…

一行命令将Cmder添加到系统右键菜单中----配置环境

第一步&#xff0c;去官网下载一个简版的文件 ** 第二步&#xff0c;将下载的文件解压后如图&#xff0c;找到Cmder.exe右键以管理员身份运行 第三步&#xff0c;在窗口输入cmder /register all然后回车 第四步&#xff0c;OK!不管在哪里都可以使用了&#xff0c;直接右键即可

vscode环境迁移

关注B站可以观看更多实战教学视频&#xff1a;hallo128的个人空间 vscode环境迁移 Setting 即可打开settings.json {"python.pythonPath": "/Users/apple/opt/anaconda3/bin/python","cmake.cmakePath": "/usr/local/bin/cmake",&qu…

[c++高阶]模版进阶

1.前言 在我们学习c的时候&#xff0c;常常会遇见要使用函数重载的情况。而当使用函数重载时&#xff0c;通常会使得我们编写很多重复的代码&#xff0c;这样就显得非常臃肿&#xff0c;并且效率非常的低下。 重载的函数仅仅只是类型不同&#xff0c;代码的复用率比较低&#x…

浮点数的这些特性你了解吗

问题1:下面的代码&#xff0c;输出结果是什么&#xff1a; public class CaclTest{public void test1(){float f 1.0F / 0.0F;System.out.println("f:" f)}public static void main(String[] args){CaclTest ct new CaclTest();ct.test1();}} A. 运行抛出异常:j…

7.数据结构与算法-循环链表

如果经常对首位元素进行操作&#xff0c;用尾元素更方便更快捷 两个循环链表合并

信息安全工程师(21)安全协议

前言 安全协议是建立在密码体制基础上的一种交互通信协议&#xff0c;它运用密码算法和协议逻辑来实现认证、密钥分配、数据机密性、完整性和抗否认性等安全目标。 一、定义与目的 安全协议旨在确保网络环境中信息交换的安全性&#xff0c;通过密码技术和协议逻辑来保护数据的机…