JAVA多线程的几个使用场景

目录

场景一、需要查询接口集合,当接口因数据量过大导致超时,使用多线程查询。

1、代码实战片段

2、运行结果         

场景二、不需要查询结果,只用来推送接口,更新数据库状态。

1、代码实战片段

一、线程池拒绝策略使用说明

1.、AbortPolicy(中止策略)

2.、CallerRunsPolicy(调用者运行策略)

3.、DiscardPolicy(丢弃策略)

4.、DiscardOldestPolicy(丢弃最老策略)

二、使用线程池遇到的问题(踩坑) 

1、线程池中异常消失

2、绝策略设置错误导致接口超时

3、重复创建线程池导致内存溢出

4、共用线程池执行不同类型任务导致效率低下

5、使用 ThreadLocal 和线程池的不兼容问题



场景一、需要查询接口集合,当接口因数据量过大导致超时,使用多线程查询。

1、代码实战片段

   查询接口类:

public interface OfflineService {

    //查询接口,一次查询数据量非常大,导致超时

    Future<List<OfflineExpenseDTO>> queryExp(int pageNum, int pageSize);

}

  接口实现类: 

    @Override
    //线程池 异步多线程
    @Async("eventExecutor")
    public Future<List<OfflineExpenseDTO>> queryExp(int pageNum, int pageSize) {
        try {
            Thread.sleep(3000);
            log.error("3秒后进入查询,当前时间为:{}", new Date());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String result = "{\n" +
                "    \"body\": {\n" +
                "        \"code\": 0,\n" +
                "        \"type\": \"success\",\n" +
                "        \"message\": \"查询信息成功\",\n" +
                "        \"data\": [\n" +
                "            {\n" +
                "                \"servMattInstId\": \"32102******6181420\",\n" +
                "                \"areaCode\": \"32***9\",\n" +
                "                \"buzzType\": \"2\",\n" +
                "                \"certType\": \"01\",\n" +
                "                \"certNo\": \"32102******6181420\",\n" +
                "                \"psnName\": \"袁**\",\n" +
                "                \"tel\": \"15958614628\",\n" +
                "                \"linkmanName\": \"袁**\",\n" +
                "                \"linkmanType\": \"01\",\n" +
                "                \"linkmanCode\": \"32102******6181420\",\n" +
                "                \"sumfee\": \"5000.00\",\n" +
                "                \"bankNumber\": \"32102******6181420\",\n" +
                "                \"bankAcctname\": \"袁小霞\"\n" +
                "            }\n" +
                "        ]\n" +
                "    },\n" +
                "    \"code\": 200\n" +
                "}";
        JSONObject str = JSONObject.parseObject(result);
        if (str.getInteger("code") == 200) {
            List<OfflineExpenseDTO> list = str.getJSONObject("body").getJSONArray("data").toJavaList(OfflineExpenseDTO.class);
            return new AsyncResult<>(list);
        }
        return null;
    }

线程池配置: 

package cn.**.**.eventlog.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

/**
 * @Author:
 * @Date:2023/2/22 15:09
 * @Description:自定义线程池
 */
@Configuration
@EnableAsync
public class EventThreadPoolConfig {

    //设置核心线程数等于系统核数
    private static final int CODE_POOL_SEZE = Runtime.getRuntime().availableProcessors();             
    // 核心线程数(默认线程数)
    private static final int MAX_POOL_SEZE = CODE_POOL_SEZE * 2;// 最大线程数
    private static final int KEEP_ALIVE_TIME = 10;// 允许线程空闲时间(单位:默认为秒)
    private static final int QUEUE_CAPACITY = MAX_POOL_SEZE;// 缓冲队列数
    private static final String THREAD_NAME_PREFIX = "Event-Executor-"; // 线程池名前缀

    @Bean("eventExecutor")
    public ThreadPoolTaskExecutor getTaskExecutor() {
        System.out.println("CODE_POOL_SEZE" + CODE_POOL_SEZE);
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(CODE_POOL_SEZE);
        executor.setMaxPoolSize(MAX_POOL_SEZE);
        executor.setKeepAliveSeconds(KEEP_ALIVE_TIME);
        executor.setQueueCapacity(QUEUE_CAPACITY);
        executor.setThreadNamePrefix(THREAD_NAME_PREFIX);
        //拒绝策略:CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        //等待所有任务结束后在关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //初始化
        executor.initialize();
        return executor;
    }
}

使用多线程: 

    
@RestController
@RequestMapping("/oneThing/err/data")
public class ErrorDateController {

    //每个线程每次查询的条数
    private static final Integer LIMIT = 100;

    /**
    * 使用多线程
    */
     @GetMapping("/queryByThread")
    public List<OfflineExpenseDTO> test() throws ExecutionException, InterruptedException {
        Long s = System.currentTimeMillis();
        List<OfflineExpenseDTO> list = new ArrayList<>();
        //假设接口返回的总条数为120条。
        int total = 1200 ;
        //计算需要分几页查询
        int num = total % LIMIT == 0 ? total / LIMIT : total / LIMIT + 1;
        //接收线程返回结果
        List<Future<List<OfflineExpenseDTO>>> futureList = new ArrayList<>();
        for (int i = 0; i < num; i++) {
            int pageNum = i;
            int pageSize = LIMIT;
            Future<List<OfflineExpenseDTO>> future = offlineService.queryExp(pageNum, pageSize);
            //查询结果
            futureList.add(future);
        }
        //异步处理返回结果
        while (!futureList.isEmpty()) {
            Iterator<Future<List<OfflineExpenseDTO>>> iterator = futureList.iterator();
            while (iterator.hasNext()) {
                Future<List<OfflineExpenseDTO>> next = iterator.next();
                if (next.isDone()) {
                    List<OfflineExpenseDTO> item = next.get();
                    //取出查询结果
                    list.addAll(item);
                    //这步很重要,否则会一直循环导致内存溢出
                    iterator.remove();
                }
            }
        }
        Long s = System.currentTimeMillis();
        log.debug("使用多线程查询耗时(秒):", (e - s) / 1000);
        return list;
    }

}

不使用多线程: 

**
     * 不使用多线程查询
     *
     * @return
     * @throws ExecutionException
     * @throws InterruptedException
     */
    @GetMapping("/query")
    public List<OfflineExpenseDTO> query() throws ExecutionException, InterruptedException {
        Long s = System.currentTimeMillis();
        List<OfflineExpenseDTO> list = new ArrayList<>();
        //假设接口返回的总条数为520条。
        int total = 520;
        //计算需要分几页查询
        int num = total % LIMIT == 0 ? total / LIMIT : total / LIMIT + 1;
        for (int i = 0; i < num; i++) {
            int pageNum = i;
            int pageSize = LIMIT;
            List<OfflineExpenseDTO> itemList = offlineService.queryExp(pageNum, pageSize);
            list.addAll(itemList);
        }
        Long e = System.currentTimeMillis();
        log.debug("没使用多线程查询耗时(秒),time={}:", (e - s) / 1000);
        return list;
    }

2、运行结果         

1、使用多线程用时:3s。

2、不使用多线程用时:18s。

场景二、不需要查询结果,只用来推送接口,更新数据库状态。

1、代码实战片段

//每个线程推送的数据量 
private static final int THREAD_COUNT_SIZE = 5000;  
/**
     * 定时任务,将表中数据推送给第三方接口,并更新表中状态
     */
@Scheduled(cron = "0 0 2 * * ?")  
public void pushZwData() throws UnknownHostException {

            //要推送的数据
            List<GoverExpenseLogDO> list = offlineService.queryhOfflineList();
            if (!list.isEmpty()) {
                //线程数
                int round = list.size() / THREAD_COUNT_SIZE + 1;
                for (int i = 0; i < round; i++) {
                    int startIndex = i * THREAD_COUNT_SIZE;
                    //避免数组越界
                    int endIndex = list.size() < (i + 1) * THREAD_COUNT_SIZE ? list.size() : (i + 1) * THREAD_COUNT_SIZE;
                    pushData(startIndex, endIndex, list);
                }
            }
        
    }
   private void pushData(int startIndex, int endIndex, List<GoverExpenseLogDO> list) {
        for (int i = startIndex; i < endIndex; i++) {
            GoverExpenseLogDO item = list.get(i);
            //多线程推送
            offlineService.pushData(item);

        }
    }

 

一、线程池拒绝策略使用说明

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());

1.、AbortPolicy(中止策略)

特点:这是线程池的默认拒绝策略。当任务被拒绝时,它会抛出RejectedExecutionException异常,从而中断当前任务的执行流程。

应用场景:此策略适用于那些对任务执行有严格要求的场景,如关键业务处理。当系统无法继续处理新任务时,通过抛出异常可以迅速反馈问题,便于开发者及时定位和解决问题。

实战建议:对于关键业务,建议使用此策略,以确保在系统资源不足时能够及时发现问题。同时,应确保系统有完善的异常处理机制,以应对可能出现的异常情况。

2.、CallerRunsPolicy(调用者运行策略)

特点:当任务被拒绝时,它会被回退到调用者线程中执行。如果线程池已经关闭,则任务将被丢弃。

应用场景:此策略适用于那些不希望因为任务被拒绝而阻塞整个系统的场景。通过将任务回退到调用者线程执行,可以避免任务堆积导致的资源耗尽问题。

实战建议:在任务提交频率较高,但每个任务执行时间较短的场景下,使用此策略可以有效缓解线程池压力。然而,需要注意的是,如果调用者线程本身也需要执行其他重要任务,那么这种策略可能会导致调用者线程被大量任务占用,从而影响系统整体性能。

3.、DiscardPolicy(丢弃策略)

特点:当任务被拒绝时,它会被默默地丢弃,既不抛出异常也不执行任何任务。

应用场景:此策略适用于那些对任务执行结果不敏感的场景,如某些非关键业务的数据统计或日志记录。

实战建议:虽然此策略可以避免系统因任务堆积而崩溃,但也可能导致数据丢失或业务逻辑不完整。因此,在使用此策略时,应充分评估其对业务的影响,并采取适当的补偿措施。

4.、DiscardOldestPolicy(丢弃最老策略)

特点:当任务被拒绝时,它会尝试丢弃队列中等待时间最长的任务,并尝试执行新任务。

应用场景:此策略适用于那些对任务执行顺序要求不高的场景,且任务之间相对独立,不会因为某个任务的缺失而影响整体业务逻辑。

实战建议:使用此策略时,需要注意队列中任务的优先级和依赖关系,以免因丢弃重要任务而导致业务逻辑错误。同时,由于此策略会频繁地修改队列状态,因此可能会对系统性能产生一定影响。

二、使用线程池遇到的问题(踩坑) 

1、线程池中异常消失

线程池执行方法时要添加异常处理

@Test
public void test() throws Exception {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        5, 
        10, 
        60,
        TimeUnit.SECONDS, 
        new ArrayBlockingQueue<>(100000));
    Future<Integer> submit = threadPoolExecutor.execute(() -> {
        int i = 1 / 0; // 发生异常
        return i;
    });
}

如上代码,在线程池执行任务时,没有添加异常处理。导致任务内部发生异常时,内部错误无法被记录下来。

解决办法:

在线程池执行任务方法内添加 try/catch 处理,代码如下,

@Test
public void test() throws Exception {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        5, 
        10, 
        60,
        TimeUnit.SECONDS, 
        new ArrayBlockingQueue<>(100000));
    Future<Integer> submit = threadPoolExecutor.execute(() -> {
        try {
            int i = 1 / 0;
            return i;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return null;
        }
    });
}

2、绝策略设置错误导致接口超时

大多数人都只会用 CallerRunsPolicy 策略(由调用线程处理任务)。我吃过这个亏,因此也拿出来讲讲。

问题原因:

曾经有一个线上业务接口使用了线程池进行第三方接口调用,线程池配置里的拒绝策略采用的是 CallerRunsPolicy。示例代码如下,

// 某个线上线程池配置如下
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        50, // 最小核心线程数
        50, // 最大线程数,当队列满时,能创建的最大线程数
        60L, TimeUnit.SECONDS, // 空闲线程超过核心线程时,回收该线程的最大等待时间
        new LinkedBlockingQueue<>(5000), // 阻塞队列大小,当核心线程使用满时,新的线程会放进队列
        new CustomizableThreadFactory("task"), // 自定义线程名
        new ThreadPoolExecutor.CallerRunsPolicy() // 线程执行的拒绝策略
);

threadPoolExecutor.execute(() -> {
    // 调用第三方接口
    ...
});

第三方接口异常的情况下,线程池任务调用第三方接口一直超时,导致核心线程数、最大线程数堆积被占满、阻塞队列也被占满的情况下,也就会执行拒绝策略,但是由于使用的是 CallerRunsPolicy 策略,导致线程任务直接由我们的业务线程来执行。

因为第三方接口异常,所以业务线程执行也会继继续超时,线上服务采用的 Tomcat 容器,最终也就导致 Tomcat 的最大线程数也被占满,进而无法继续向外提供服务

解决办法:

首先我们要考虑业务接口的可用性,就算线程池任务被丢弃,也不应该影响业务接口。

在业务接口稳定性得到保证的情况下,在考虑到线程池任务的重要性,不是很重要的话,可以使用 DiscardPolicy 策略直接丢弃,要是很重要,可以考虑使用消息队列来替换线程池。

3、重复创建线程池导致内存溢出

这个问题的原因很简单,就是在一个方法内重复创建了线程池,在执行完之后却没有关闭。比较经典的就是在定时任务内使用线程池时有可能犯这个问题,示例代码如下,

@XxlJob("test")
public void test() throws Exception {
    // 某个线上线程池配置如下
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            50, // 最小核心线程数
            50, // 最大线程数,当队列满时,能创建的最大线程数
            60L, TimeUnit.SECONDS, // 空闲线程超过核心线程时,回收该线程的最大等待时间
            new LinkedBlockingQueue<>(5000), // 阻塞队列大小,当核心线程使用满时,新的线程会放进队列
            new CustomizableThreadFactory("task"), // 自定义线程名
            new ThreadPoolExecutor.CallerRunsPolicy() // 线程执行的拒绝策略
    );
    threadPoolExecutor.execute(() -> {
        // 任务逻辑
        ...
    });
}

当我们在定时任务中想使用线程池来缩短任务执行时间时,千万要注意别再任务内创建了线程池,一旦犯了,基本都会在程序运行一段时间后发现程序突然间就挂了,留下了一堆内存 dump 报错的文件 😂。

解决方法:

把线程池拿到外边,使用线程池单例,切勿重复创建线程池。示例代码如下,

// 某个线上线程池配置如下
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        50, // 最小核心线程数
        50, // 最大线程数,当队列满时,能创建的最大线程数
        60L, TimeUnit.SECONDS, // 空闲线程超过核心线程时,回收该线程的最大等待时间
        new LinkedBlockingQueue<>(5000), // 阻塞队列大小,当核心线程使用满时,新的线程会放进队列
        new CustomizableThreadFactory("task"), // 自定义线程名
        new ThreadPoolExecutor.CallerRunsPolicy() // 线程执行的拒绝策略
);
@XxlJob("test")
public void test() throws Exception {
    threadPoolExecutor.execute(() -> {
        // 任务逻辑
        // ...
    });
}

4、共用线程池执行不同类型任务导致效率低下

有时候,我们可能会想要节省线程资源,把不同类型的任务都放到同一个线程池中执行,这样做可能会导致一个任务影响另一个任务,甚至导致死锁的问题。

原因是,不同类型的任务可能有不同的执行时间、优先级、依赖关系等,如果放到同一个线程池中,就可能会出现以下几种情况:

  • 如果一个任务执行时间过长,或者出现异常,那么它就会占用线程池中的一个线程,导致其他任务无法及时得到执行,影响系统的吞吐量和响应时间。
  • 如果一个任务的优先级较低,或者不是很重要,那么它就可能抢占线程池中的一个线程,导致其他任务无法及时得到执行,影响系统的可用性和正确性。
  • 如果一个任务依赖于另一个任务的结果,或者需要等待另一个任务的完成,那么它就可能造成线程池中的一个线程被阻塞,导致其他任务无法及时得到执行,甚至导致死锁的问题。

解决方法也很简单,就是使用不同的线程池来执行不同类型的任务,根据任务的特点和重要性来分配线程资源,避免一个任务影响另一个任务。具体来说,有以下几个建议:

  • 对于主要的业务逻辑,使用一个专门的线程池,根据业务的并发度和响应时间,设置合适的线程池参数,保证业务的正常运行和高效处理。
  • 对于次要的日志记录、监控等,使用一个单独的线程池,根据任务的频率和重要性,设置合适的线程池参数,保证任务的异步执行和不影响主业务。
  • 对于有依赖关系的任务,使用一个单独的线程池,根据任务的数量和复杂度,设置合适的线程池参数,保证任务的有序执行和不造成死锁。

5、使用 ThreadLocal 和线程池的不兼容问题

ThreadLocal 是 Java 提供的一个工具类,它可以让每个线程拥有自己的变量副本,从而实现线程间的数据隔离,比如存储一些线程相关的上下文信息,如用户 ID、请求 ID 等。这看起来很有用,但是如果和线程池一起使用,就可能会出现一些意想不到的问题,比如数据错乱、内存泄漏等。

问题的原因是,ThreadLocal 和线程池的设计理念是相悖的,ThreadLocal 是基于线程的,而线程池是基于任务的。具体来说,有以下几个问题:

  • ThreadLocal 的变量是绑定在线程上的,而线程池的线程是可以复用的,如果一个线程执行完一个任务后,没有清理 ThreadLocal 的变量,那么这个变量就会被下一个执行的任务继承,导致数据错乱的问题。
  • ThreadLocal 的变量是存储在 Thread 类的一个 ThreadLocalMap 类型的属性中的,这个属性是一个弱引用的 Map,它的键是 ThreadLocal 对象,而值是变量的副本。如果 ThreadLocal 对象被回收,那么它的键就会失效,但是值还会保留在 Map 中,导致内存泄漏的问题。

解决方法也很简单,就是在使用 ThreadLocal 和线程池的时候,注意以下几点:

  • 在使用 ThreadLocal 的变量之前,要确保为每个线程设置了正确的初始值,避免使用上一个任务的遗留值。
  • 在使用 ThreadLocal 的变量之后,要及时地清理 ThreadLocal 的变量,避免变量的副本被下一个执行的任务继承,或者占用内存空间,导致内存泄漏的问题。可以使用 try-finally 语句,或者使用 Java 8 提供的 AutoCloseable 接口,来实现自动清理的功能。
        private static ThreadLocal<String> formulaEvaluatorThreadLocal = new ThreadLocal<>();
    
    
     /**
         * 初始化资源
         *
         * @param workbook workbook
         */
        private static void initResource(String ss) {
            
            formulaEvaluatorThreadLocal.set(ss);
        }
    
      /**
         * 释放资源
         */
        private static void releaseResource() {
            formulaEvaluatorThreadLocal.remove();
        }
  • 在使用 ThreadLocal 的时候,要注意线程池的大小和任务的数量,避免创建过多的 ThreadLocal 对象和变量的副本,导致内存占用过大的问题。可以使用一些工具,如 VisualVM,来监控线程池和 ThreadLocal 的状态,及时发现和解决问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值