Java线程池的使用

书接前文Java线程池及其实现原理

常用线程池有:

CachedThreadPool

FixedThreadPool

SingleThreadExecutor

ScheduledThreadPool

SingleThreadScheduledExecutor

Executors

.newCachedThreadPool();

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
内部实现:new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,

TimeUnit.SECONDS,new SynchronousQueue());

Executors

.newFixedThreadPool(int);

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
内部实现:new ThreadPoolExecutor(nThreads, nThreads,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue());

Executors

.newSingleThreadExecutor();

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照顺序执行。
内部实现:new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue())

Executors

.newScheduledThreadPool(int)

创建一个定长线程池,支持定时及周期性任务执行。
内部实现:new ScheduledThreadPoolExecutor(corePoolSize)
Executors
.newSingleThreadScheduledExecutor()
创建一个单线程的任务调度池(定时任务/延时任务)

线程池使用场景

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

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

阿里巴巴Java开发手册中对线程池的使用规范

  1. 【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯
  2. 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
    说明: 使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
  3. 【强制】线程池不允许使用 Executors 去创建,而是通过ThreadPoolExecutor的方式创建,这样的处理方式让写的Javer更加明确线程池的运行规则,规避资源耗尽的风险。
    说明: Executors 返回的线程池对象的弊端如下:
    1) FixedThreadPool 和 SingleThreadPool:
    允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
    2) CachedThreadPool 和 ScheduledThreadPool:
    允许的创建线程数量为 Integer.MAX_VALUE, 可能会创建大量的线程,从而导致 OOM。

综上所述,建议使用ThreadPoolExecutor executor = new ThreadPoolExecutor(...);

线程池关闭

如果线程池没有做好关闭,可能会导致oom,严重可能会导致服务挂掉。

手动线程池

手动线程池关闭分为:shutdown和shutdownNow。

executor.shutdown();

executor.shutdownNow();
区别:
shutdown主要做两件事:
  • 把线程池状态置为 SHUTDOWN 状态
  • 中断空闲线程
shutdownNow主要做三件事:
  • 把线程池状态置为 STOP 状态
  • 中断工作线程
  • 把线程池中的任务都 drain 出来并返回

综上,shutdown方法会等待把线程池中的任务都执行完,而shutdownNow会直接中断当前工作线程。因为,shutdown方法只是把空闲的 woker 置为中断,不影响正在运行的woker,并且会继续把待执行的任务给处理完。shutdonwNow方法则是把所有的 woker 都置为中断,待执行的任务全部抽出并返回。

日常工作中更多是使用 shutdown()。

如果要优雅的关闭线程池,则可以使用 awaitTermination() 和 JVM 的钩子来实现。
Thread shutdownHook = new Thread(() -> {
    executor.shutdown();
    try {
         executor.awaitTermination(3, TimeUnit.MINUTES);
     } catch (InterruptedException e) {
         log.info("等待超时,直接关闭");
     }
});
Runtime.getRuntime().addShutdownHook(shutdownHook);

自动关闭线程池

核心线程数为 0 并指定线程存活时间
@Test
public void test(){
     // 重点关注 corePoolSize 和 keepAliveTime,其他参数不重要
     ThreadPoolExecutor executor = new ThreadPoolExecutor(
                           0,
                           5,
                           15L,
                           TimeUnit.SECONDS,
                           new LinkedBlockingQueue<>(15));
     for (int i = 0; i < 20; i++) {
         executor.execute(() -> {
            // 简单地打印当前线程名称
            System.out.println(Thread.currentThread().getName());
         });
     }
}

缺点:如果将corePoolSize设置为0的话,新到来的任务会永远优先被放入任务队列,然后等待被处理,这显然会影响程序的执行效率。

通过 allowCoreThreadTimeOut 控制核心线程存活时间
@Test
public void test(){
    // 这里把corePoolSize设为5,keepAliveTime保持不变
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
                                    5,
                                    5,
                                    15L,
                                    TimeUnit.SECONDS,
                                    new LinkedBlockingQueue<>(15));
     // 允许核心线程超时销毁
     executor.allowCoreThreadTimeOut(true);
     for (int i = 0; i < 20; i++) {
         executor.execute(() -> {
            System.out.println(Thread.currentThread().getName());
         });
    }
}
线程池中的线程设置为守护线程
@Test
public void test(){
    // 这里把corePoolSize设为5,keepAliveTime保持不变
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
                5,
                5,
                15L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(15),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(@NotNull Runnable r) {
                        Thread thread = new Thread(r, r.hashCode()+"");
                        //设置成守护线程
                        thread.setDaemon(true);
                        return thread;
                    }
                });
     // 允许核心线程超时销毁
     executor.allowCoreThreadTimeOut(true);
     for (int i = 0; i < 20; i++) {
        executor.execute(() -> {
           System.out.println(Thread.currentThread().getName());
        });
     }
}

PS:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。

线程池大小设计

在设定线程池大小和任务队列容量时,需要进行合理的评估和测试。可以先根据预估的并发量进行初步设定,然后进性压力测试,观察系统的性能表现和资源使用情况,并根据实际情况进行调整。同时,还需要注意线程池的大小和任务队列容量不能过大或过小,以避免浪费资源或导致系统崩溃。

首先需要确定该应用的预估并发量,假设每秒钟需要处理 100 个请求。然后根据业务特点,可做如下设定:

  • 确定线程池大小:为了充分利用 CPU 和内存资源,可以将线程池大小设置为 CPU 核心数的两倍。假设 CPU 核心数为 8,那么线程池大小可设置为 16。
  • 确定任务队列容量:由于这里的任务是从数据库中读取数据并进行计算,因此任务执行时间相对较长,需要将任务队列容量设置得大一些。假设任务队列容量为 100。

通过Runtime.getRuntime().availableProcessors()获取当前CPU核心数。一般,IO密集型为2 * N;CPU计算密集级为1 + N。

局部使用

int size = Runtime.getRuntime().availableProcessors() * 2;
ThreadPoolExecutor executor = new ThreadPoolExecutor(
                                size,
                                size,
                                100,
                                TimeUnit.MILLISECONDS,
                                new ArrayBlockingQueue<>(100),
                                Executors.defaultThreadFactory(),
                                new ThreadPoolExecutor.DiscardOldestPolicy());
//修改报价单状态->已转为销售订单
executor.execute(() -> xx.changeQuoteStatus(accessToken, thirdId, userId));
Thread shutdownHook = new Thread(() -> {
    executor.shutdown();
    try {
         executor.awaitTermination(3, TimeUnit.MINUTES);
     } catch (InterruptedException e) {
         log.info("等待超时,直接关闭");
     }
});
Runtime.getRuntime().addShutdownHook(shutdownHook);

parallelStream并行流

parallelStream并行流的底层是ForkJoinPool,如果要设置并行流线程参数,需要

private int parallelThreads = Runtime.getRuntime().availableProcessors() * 2;
ForkJoinPool pool = new ForkJoinPool(parallelThreads);
ForkJoinTask task = pool.submit(() -> {
	Lists.partition(list, 100).parallelStream().forEach(...);
});
task.join();
pool.shutdown();

自定义局部线程池配置使用

配置

/**
 * @author lyonardo
 * @createTime 2019年04月11日 10:14:23
 * @Description
 */
@Configuration
@EnableAsync
public class TaskExecutorConfig {
    @Bean(name = "taskExecutor")
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        //线程池大小
        executor.setCorePoolSize(16);
        //最大线程池大小
        executor.setMaxPoolSize(16);
        //任务队列容量
        executor.setQueueCapacity(100);
        //线程前缀名
        executor.setThreadNamePrefix("taskExecutor-");
        // 任务拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        executor.initialize();
        return executor;
    }
}

使用

使用线程池时,可以通过注解 @Async 将方法标记为异步方法,让方法在另一个线程中执行:

@Test
@Async("taskExecutor")
public void testExecute(){
    log.info("213");
    log.info(Thread.currentThread().getName() + "---taskExecutor" + System.currentTimeMillis());
}

@Test
@Async//未指定线程池,则使用默认线程池SimpleAsyncTaskExecutor
public void testExecute1(){
    log.info("213");
    log.info(Thread.currentThread().getName() + "---taskExecutor" + System.currentTimeMillis());
}

在代码中,使用了 @Async 注解将 testExecute() 方法标记为异步方法,并指定了线程池名称为 "taskExecutor",表示该方法将在 "taskExecutor" 线程池中执行。这样就可以避免在主线程中等待查询和计算的结果,提高了应用的响应速度。

如果需要接收异步操作的结果,可以使用Future做如下操作:

@Async
public Future<Integer> calculateSum(int a, int b) {
    int sum = a + b;
   return new AsyncResult<Integer>(sum);
}

@Test
public void test() throws ExecutionException, InterruptedException {
    // 使用方法,等待所有线程执行完,在同一处理结果
    Future<Integer> futureResult = calculateSum(1, 2);
    //执行其他操作
    Integer result = futureResult.get(); //阻塞等待异步操作完成并获取结果
    log.info("result::"+result);
}

自定义全局线程池配置使用 

上面需在@Async()注解中指定使用自定义线程池才有效,如果即不想指定线程池,又不想使用默认线程池池—全局线程池。

定义全局线程池可以通过实现 AsyncConfigurer 或者继承 AsyncConfigurerSupport。

@Configuration
public class AsyncGlobalConfig extends AsyncConfigurerSupport {
    private static final String THREAD_PREFIX = "defineGlobalAsync-";

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setThreadNamePrefix(THREAD_PREFIX);
        executor.setCorePoolSize(3);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setKeepAliveSeconds(60);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        executor.initialize();
        return executor;
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Java线程池提供了一种线程复用的机制,以及一些控制和管理线程的方法。它可以帮助我们更好地管理线程,提高应用程序的性能。 以下是使用Java线程池的步骤: 1. 创建线程池对象 可以使用Executors类提供的静态方法来创建线程池对象,如下所示: ``` ExecutorService executor = Executors.newFixedThreadPool(10); ``` 这里使用了newFixedThreadPool方法来创建一个固定大小的线程池,最多可以同时执行10个任务。 2. 提交任务 将任务提交给线程池,如下所示: ``` executor.execute(new Runnable() { public void run() { // 执行任务的代码 } }); ``` 这里使用了execute方法将任务提交给线程池。你可以将任务封装在Runnable接口的实现类中,并将其作为参数传递给execute方法。 3. 关闭线程池 当你的应用程序不再需要线程池时,应该关闭它,以释放资源。可以使用shutdown方法来关闭线程池,如下所示: ``` executor.shutdown(); ``` 这里使用了shutdown方法来关闭线程池。调用shutdown方法后,线程池将不再接受新的任务,但会等待所有已提交的任务执行完毕,然后关闭线程池。 除了newFixedThreadPool方法外,Java线程池还提供了其他的线程池类型,如newCachedThreadPool、newSingleThreadExecutor等,可以根据需要选择合适的线程池类型。同时,线程池还提供了一些控制和管理线程的方法,如setCorePoolSize、setMaximumPoolSize、setKeepAliveTime等,可以根据需要进行配置。 ### 回答2: Java线程池是用来管理和利用线程的一种机制。它可以有效地将有限的资源(线程)进行分配和复用,提高程序的性能和资源利用率。 使用线程池可以带来一系列好处。首先,线程池能够控制并发的线程数量,避免过多的线程导致系统资源的浪费。其次,线程池可以重复利用已创建的线程,避免频繁地创建和销毁线程的开销。再次,线程池可以对线程进行管理,比如设置线程的优先级、超时时间等,提高了线程的效率。最后,线程池还可以提供线程的监控和统计信息,方便进行系统性能的调优。 在Java中,线程池一般会使用Executor框架来实现。常见的线程池实现类有ThreadPoolExecutor和ScheduledThreadPoolExecutor。 通过ThreadPoolExecutor类,我们可以创建一个线程池,并指定线程的初始大小、最大大小、线程空闲时的存活时间等参数。我们可以将任务提交给线程池执行,线程池会自动选择一个可用的线程来执行任务。当任务执行完毕后,线程会返回线程池继续等待下一个任务的分配。 使用线程池时,应该根据具体情况来配置线程池的参数。如果任务量较大,可以适当增加线程池的大小,以充分利用系统资源。如果任务量较小,可以适当减少线程池的大小,避免资源的浪费。同时,应该注意合理设置线程的优先级和超时时间,以保证任务的顺利执行。 总之,Java线程池使用能够提高程序的性能和资源利用率,减少线程的创建和销毁开销,对于多线程编程是非常有益的工具。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

李景琰

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

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

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

打赏作者

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

抵扣说明:

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

余额充值