【多线程】深入浅出之Java线程池

目录

1. 什么是线程池及其优势

1.1 什么是线程池

1.2 使用线程池的优势

2. 线程池的创建

3. 线程池的七大参数

4. 线程池的工作流程

5. 线程池的生命周期

6. 如何使用线程池(实战)

7. 总结

1. 什么是线程池及其优势

1.1 什么是线程池

线程池是管理一系列线程的资源池,当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。

1.2 使用线程池的优势

总体来说,线程池有如下的优势:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

2. 线程池的创建

方式一:通过ThreadPoolExecutor构造函数来创建。

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                Executors.defaultThreadFactory(), defaultHandler);
    }

方式二:通过 Executor 框架的工具类 Executors 来创建。

主要介绍四种常见的线程池,需要注意,阿里巴巴《Java 开发手册》里禁止使用这种方式来创建线程池。

创建方式线程池描述
Executors.newFixedThreadPool(nThreads)
固定数目线程的线程池
Executors.newSingleThreadExecutor()
单线程的线程池
Executors.newCachedThreadPool()
可缓存线程的线程池
Executors.newScheduledThreadPool(corePoolSize)
定时及周期执行的线程池

3. 线程池的七大参数

1. corePoolSize

核心线程数,任务队列未达到队列容量时,最大可以同时运行的线程数量。

2. maximumPoolSize

线程池所能容纳的最大线程数,任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。

3. workQueue

任务队列,新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。它有 7 种类型的任务队列:

  • LinkedBlockingQueue底层数据结构是链表,如果不指定大小,默认大小是 Integer.MAX_VALUE。
  • SynchronousQueueSynchronousQueue 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理,否则新建一个线程来处理任务。
  • DelayedWorkQueueDelayedWorkQueue 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。DelayedWorkQueue 添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达 Integer.MAX_VALUE,所以最多只能创建核心线程数的线程。
  • ArrayBlockingQueue:一个有界的先进先出的阻塞队列,底层是一个数组,必须指定容量大小。
  • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。任务按照其自然顺序或通过构造器给定的 Comparator 来排序。
  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列,与LinkedBlockingQueue相比多了transfer和tryTranfer方法,该方法在有消费者等待接收元素时会立即将元素传递给消费者。
  • LinkedBlockingDeque:一个由链表结构组成的双端阻塞队列,可以从队列的两端插入和删除元素。

4. keepAliveTime

线程闲置超时时长,线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁。

5. unit

线程闲置超时时长,keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。

6. threadFactory

线程工厂,executor 创建新线程的时候会用到,threadFactory可以设置线程池名称、线程组、优先级等参数。比如给线程池的线程命名:

匿名内部类实现

        ThreadFactory threadFactory = new ThreadFactory() {
            private final AtomicInteger count = new AtomicInteger(1);

            @Override
            public Thread newThread(@NotNull Runnable runnable) {
                Thread thread = new Thread(runnable);
                thread.setName("线程" + count.getAndIncrement());
                return thread;
            }
        };

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2,
                4,
                100,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(4),
                threadFactory);
        return threadPoolExecutor;

或者通过Google工具包可以设置线程名

    ThreadFactory guavaThreadFactory = new ThreadFactoryBuilder().setNameFormat("pool").build();
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            2,
            4,
            100,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(4),
            guavaThreadFactory);

7. handler

拒绝策略,当达到最大线程数时需要执行的饱和策略。

  • AbortPolicy抛出 RejectedExecutionException来拒绝新任务的处理。
  • CallerRunsPolicy调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
  • DiscardPolicy不处理新任务,直接丢弃掉。
  • DiscardOldestPolicy此策略将丢弃最早的未处理的任务请求。

4. 线程池的工作流程

当应用程序提交一个任务时,线程池会根据当前线程的状态和参数决定如何处理这个任务。

  • 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。
  • 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
  • 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。
  • 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用RejectedExecutionHandler.rejectedExecution()方法。

当任务执行完毕后,线程并不会立即销毁,而是继续保持在池中等待下一个任务,当非核心线程空闲时间超出指定时间,线程会被回收。

5. 线程池的生命周期

线程池的生命周期主要可以分为以下五个阶段:

  • RUNNING表示可接受新任务,且可执行队列中的任务。

  • SHUTDOWN表示不接受新任务,但可执行队列中的任务。

  • STOP表示不接受新任务,且不再执行队列中的任务,且中断正在执行的任务。

  • TIDYING所有任务已经中止,且工作线程数量为0,最后变迁到这个状态的线程将要执行terminated()方法,只会有一个线程执行这个方法。

  • TERMINATED中止状态,已经执行完terminated()方法。

6. 如何使用线程池(实战)

假设现在有一个场景是有大量数据要对其进行分析然后返回分析后的结果,这些数据处理任务是独立的,可以并行执行以提高效率,故可以使用CompletableFuture+自定义线程池异步并发处理任务,提高处理的效率,代码如下:

/**
 * 自定义线程池
 */
@Configuration
public class ThreadPoolExecutorConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor() {
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("pool").build();
        return new ThreadPoolExecutor(
                2,
                4,
                100,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(1000),
                threadFactory);
    }
}
/**
 * 任务执行结果
 */
@Data
public class TaskResult {
    private int id;

    private String status;

    public TaskResult(int id, String status) {
        this.id = id;
        this.status = status;
    }
}
@SpringBootTest
@Slf4j
class DemoApplicationTests {
    @Resource
    private ThreadPoolExecutor threadPoolExecutor;

    // 主程序类
    @Test
    void test() {
        // 需要处理的数据
        List<Integer> task = getData();

        // 开始的时间
        long start = System.currentTimeMillis();

        // 返回执行的结果集
        List<CompletableFuture<TaskResult>> futures = new ArrayList<>();

        // 使用CompletableFuture+自定义线程池异步执行任务并返回执行的结果
        for (int data : task) {
            // 提交需要处理的任务到指定线程池,异步执行
            CompletableFuture<TaskResult> future = CompletableFuture
                    .supplyAsync(() -> dealData(data), threadPoolExecutor)
                    .exceptionally(e -> {
                        // 出现异常
                        log.error("任务{}执行异常:{}", data, e.getMessage(), e);
                        // 返回异常的结果
                        return new TaskResult(data, "failure");
                    });
            // future对象添加到集合中
            futures.add(future);
        }

        try {
            // 等待所有任务执行完毕
            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(20, TimeUnit.SECONDS);
        } catch (Exception e) {
            log.error("CompletableFuture异常: {}", e);
            // 出现异常
            return;
        }

        // 获取任务的执行结果
        List<TaskResult> taskResultList = new ArrayList<>();
        for (CompletableFuture<TaskResult> future : futures) {
            TaskResult result;
            try {
                result = future.get();
            } catch (Exception e) {
                // 获取任务结果异常
                log.error("获取任务结果异常:{}", e);
                return;
            }

            // 判断任务执行没有出现异常
            if (result != null && "success".equals(result.getStatus())) {
                taskResultList.add(result);
            } else {
                if (result != null) {
                    log.error("任务{}执行出现异常", result.getId());
                } else {
                    log.error("任务为null异常");
                }
            }
        }

        long execTime = System.currentTimeMillis() - start;
        log.info("执行耗时:{} ms", execTime);
    }


    // 模拟处理数据
    public static TaskResult dealData(int data) {
        log.info("模拟处理第{}条数据....", data);
        try {
            // 模拟任务执行时间,每 0.2s 执行一个任务
            Thread.sleep(100);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new TaskResult(data, "success");
    }

    // 模拟获取数据
    public static List<Integer> getData() {
        List<Integer> list = new ArrayList<>();
        for (int i = 1; i <= 100; i++) {
            list.add(i);
        }
        return list;
    }
}

7. 总结

线程池是一种管理和重用线程的机制,它通过有效地控制并发线程数量,提高系统性能和资源利用率,在实际应用中,我们可以通过合理使用线程池,并根据实际情况进行监控和调优,有效提高系统的性能和稳定性。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值