BruceBat's Blog
BruceBat's Blog

竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生


  • 首页

  • 归档

  • 分类

  • 标签

  • 搜索
NIO I/O 计算机科学 操作系统 设计模式 随记 WebSocket 计算机网络 注册中心 经典电影 xxl-job 分布式 分布式任务调度 MySQL DevOps Docker 多线程 有趣的问题 Mybatis-Plus Mybatis Java 数据结构

Java多线程探索(一):为什么要使用ThreadPoolExecutor?

发表于 2020-03-05 | 分类于 Java | 0 | 阅读次数 641

人生苦短,不如养狗

一、前言

  前段时间闲鱼在重新翻看《阿里巴巴Java开发手册》时看到

【强制】线程池不允许使用Executors创建,而是使用ThreadPoolExecutor的方式创建。 那么为什么这么规定呢?

二、一些关于多线程的灵魂发问

  在开始回答上面的问题之前,我们先来回答一下多线程相关的问题热热身。

1.为什么要使用多线程?

  其实可以将这个问题替换成,使用单线程处理问题有什么不足?单线程意味着所有的线程都是串行工作,也就是每个线程都必须等待上一个线程全部处理完成之后才能开始工作。当某个线程需要处理一个极大的文件时,此时用户就只能呆呆地等在电脑前直到这个线程处理完成之后才能进行下一项任务的处理。
  而当引入多线程的概念之后,所有的线程可以并发的进行工作。注意,这里的并发执行指的是同一段时间内同时进行,但是从微观来看仍然是串行进行(CPU是单核的情况)。 那么有同学会疑惑,既然微观上仍然是串行,为什么说多线程在用户体验上会由于单线程。这里就要归功于线程调度策略,在引入多线程的概念之后,每个线程在使用CPU时都有固定的时间片,如果执行时间超过规定的时间片,那么就需要将CPU让给其他的线程进行使用。从宏观上来看,多个线程同时在进行工作,也就是上面所说的同一段时间同时进行。除此以外,当线程出现由于IO操作等发生阻塞时,也会将资源让给其他线程进行使用。因此,从用户角度来说,多线程的引入提升了用户的体验感。

这里只是介绍了最基本的一些线程调度的知识,诸如线程状态流转等知识,大家可以自行Google,这里就不再赘述。

2.在什么场景下使用多线程?

  回答了上面的问题,那么新的问题来了:我们什么时候应该使用多线程呢?从上一个问题的回答中我们可以发现,当对用户响应要求比较高,同时又允许用户并发访问的时候,此时就非常适合使用多线程。还有一种情况就是程序存在需要耗时计算或者处理大文件时,出现阻塞情况时,为了提高程序的响应,会通过创建子线程的方式来进行任务的处理。

3.为什么要使用线程池?

  既然多线程这么优秀,那是否能够肆无忌惮的去使用呢?
  当然不行。任何看似优秀的东西,使用都是需要付出代价的。就好比女生的化妆品、包包,好看吗?好看。想拥有吗?想。但是一个问题,这些东西都很贵啊。
  线程的使用也是这样。每一个线程的创建都需要占用内存,线程的调度也需要CPU进行协调和管理,同时线程之间对共享资源的访问还会导致线程安全问题(这里暂时不讨论线程安全问题),等等一系列需要考虑的问题。这些都是使用多线程的“成本”。
  所以无限制、不加管理的使用线程是不可能的,那么如何合理使用多线程呢?—— 线程池。固定的创建一些线程放在线程池中,当有任务来时从中获取线程进行任务的执行,如果线程池中所有的线程都正在使用时,那么将任务放置在阻塞队列中等待空闲的线程池。这样的设计理念既能保证享受到多线程的优势,又能防止无限制、无管理的使用线程的危害。

三、为什么要使用ThreadPoolExecutor而不是Executors?

  经过了这么多的灵魂拷问,终于来到了今天最重要的问题:为什么要使用ThreadPoolExecutor?
  其实答案很简单,来欣赏一下Executors提供的基础的四种线程池,欣赏完之后,大家就应该明白了:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
   // 这里底层使用的是ThreadPoolExecutor
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

  可以看到这四种线程池底层使用的都是ThreadPoolExecutor,只不过对于相应的参数Executors已经贴心的帮开发们设置好了。但正是Executors将底层的具体细节进行封装,使得开发无法进行线程池执行过程的掌控和根据实际情况进行线程池的修改。对于多线程的使用来说,这是一个很危险的事情。所以,为了能够让开发能够详细了解到线程池得运作机制,在《阿里巴巴Java开发手册》中推荐使用ThreadPoolExecutor而不是Executors来创建线程池。

四、一个简单的小例子

  话不多说,下面我们就用一个例子赏析一下ThreadPoolExecutor是如何工作的。

  具体使用到的代码如下:
TestThreadFactory

public class TestThreadFactory implements ThreadFactory {

    /**
     * 姓名前缀
     */
    private final String namePrefix;

    private final AtomicInteger nextId = new AtomicInteger(0);

    public TestThreadFactory(String whatFeatureOfGroup){
        this.namePrefix = "From TestThreadFactory's " + whatFeatureOfGroup + "-Worker-";
    }

    @Override
    public Thread newThread(Runnable task) {
        String name = namePrefix
                + nextId.getAndIncrement();
        Thread thread = new Thread(null, task, name, 0);
        System.out.println(thread.getName());
        return thread;
    }
}

ThreadPoolManager

public class ThreadPoolManager {

    /**
     * 核心线程大小
     */
    private int corePoolSize;
    /**
     * 最大线程池大小
     */
    private int maximumPoolSize;
    /**
     * 保持时间
     */
    private int keepAliveTime;

    /**
     * 时间单位
     */
    private TimeUnit timeUnit;

    /**
     * 阻塞队列
     */
    private BlockingQueue blockingQueue;

    /**
     * 线程工厂
     */
    private ThreadFactory threadFactory;

    /**
     * 线程饱和策略
     */
    private RejectedExecutionHandler handler;

    /**
     * 构造函数
     *
     * @param corePoolSize
     * @param maximumPoolSize
     * @param keepAliveTime
     * @param timeUnit
     * @param blockingQueue
     * @param threadFactory
     * @param handler
     */
    public ThreadPoolManager(int corePoolSize, int maximumPoolSize, int keepAliveTime, TimeUnit timeUnit, BlockingQueue blockingQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler){
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.keepAliveTime = keepAliveTime;
        this.timeUnit = timeUnit;
        this.blockingQueue = blockingQueue;
        this.threadFactory = threadFactory;
        this.handler = handler;
    }


    /**
     * 创建线程池
     *
     * @return
     */
    public ThreadPoolExecutor createThreadPool(){
        return new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, blockingQueue, threadFactory, handler);
    }
}

TestMain

public class TestMain {

    public static void main(String[] args) {
        ThreadPoolManager threadPoolManager = new ThreadPoolManager(1,2,3,
                TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(3), new TestThreadFactory("测试"), new ThreadPoolExecutor.AbortPolicy());
        ThreadPoolExecutor threadPoolExecutor = threadPoolManager.createThreadPool();
        for(int i = 0; i<5; i++){
            try {
                threadPoolExecutor.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + " test is running");
                    try {
         // 用于更方便的看清执行情况
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                });
            } catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

  可以看到,代码中一共发起了5个执行请求,但是线程池中只创建了2个线程进行请求的执行。在这一过程中极大的减少了线程创建的开销,同时线程池还提供了任务执行调度管理相关的功能。具体细节下一篇文章中闲鱼会和大家一起赏析。

五、总结

  经过上面的分析,相信大家应该对为什么不建议直接使用Executors中封装好的线程池方法来来使用线程池这个问题有了一个基础的了解。简单来说,就是希望大家不要只求便捷,因为在工程中,越简单的东西反而适用的场景越少,只有根据实际场景使用正确的方案才能获取最好的结果。

本文使用 mdnice 排版

brucebat wechat
一个闲鱼程序猿的微信公众号
  • 本文作者: brucebat
  • 本文链接: https://www.swzgeek.com/archives/java多线程探索一为什么要使用threadpoolexecutor
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# NIO # I/O # 计算机科学 # 操作系统 # 设计模式 # 随记 # WebSocket # 计算机网络 # 注册中心 # 经典电影 # xxl-job # 分布式 # 分布式任务调度 # MySQL # DevOps # Docker # 多线程 # 有趣的问题 # Mybatis-Plus # Mybatis # Java # 数据结构
Java中的数据结构(三):队列(下)
Java多线程探索(二):优秀的ThreadPoolExecutor到底是如何工作的?
  • 文章目录
  • 站点概览
brucebat

brucebat

一个有梦想的咸鱼程序猿

46 日志
8 分类
22 标签
RSS
Github E-mail
Creative Commons
© 2020 — 2023 brucebat
苏ICP备20002207号-1

苏公网安备 32011302320859号

0%