Java学习(十五)-线程与线程池学习(Thread与ThreadPoolExecutor)

一、Thread

Java创建线程Thread的三种方式

1、通过继承Thread类创建线程
  • 单继承:编写简单,缺点是只能继承一个类,要是想同时继承其他业务类,不能实现;要想实现多继承,只能使用implements
2、通过实现Runnable接口来创建线程
  • 数据共享:Runnable是可以实现数据共享的,多个Thread可以同时加载一个runnable
  • 线程不安全:当各自Thread获得CPU时间片的时候开始运行Runnable,Runnable里面的资源是被共享的,所以使用Runnable更加的灵活,但是也容易造成线程不安全
3、通过实现Callable接口来创建线程
  1. 有返回值:Runnable是执行工作的独立任务,但是它不返回任何值。如果你希望任务在完成的能返回一个值,那么可以实现Callable接口而不是Runnable接口

二、ThreadPoolExecutor

2.1、线程池的概念

  1. 线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如MySQL。
  2. 使用线程池可以带来一系列好处:
    • 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
    • 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
    • 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
    • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

2.2、ThreadPoolExecutor的使用

 * corePoolSize: 线程池维护线程的最少数量
 * maximumPoolSize:线程池维护线程的最大数量
 * keepAliveTime:非核心线程最大存活时长, 线程池维护线程所允许的空闲时间
 * unit: 线程池维护线程所允许的空闲时间的单位
 * workQueue: 线程池所使用的缓冲队列
 * handler: 线程池对拒绝任务的处理策略
// 五个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,//核心线程数
                          int maximumPoolSize,//最大线程数
                          long keepAliveTime,//非核心线程最大存活时长
                          TimeUnit unit,//时长单位
                          BlockingQueue<Runnable> workQueue)//阻塞队列,存放着等待执行的线程任务

// 六个参数的构造函数-1
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory)//创建线程的工厂

// 六个参数的构造函数-2
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler)//拒绝策略,当线程池无法再接受任务时调用

// 七个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  • 必传入参

    • corePoolSize:指定了线程池中的核心线程数,即不会因线程空闲而被销毁的线程。线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务。它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去。

      核心线程:线程池中有两类线程,核心线程和非核心线程。核心线程默认情况下会一直存在于线程池中,即使这个核心线程什么都不干(铁饭碗),而非核心线程如果长时间的闲置,就会被销毁(临时工)。

    • maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量。

      该值等于核心线程数量 + 非核心线程数量。
      最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)。

    • keepAliveTime:非核心线程最大存活时长。

      非核心线程如果处于闲置状态超过该值,就会被销毁。如果设置allowCoreThreadTimeOut(true),则会也作用于核心线程。
      非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);非核心线程:当线程池中空闲线程数量超过corePoolSize时,多余的线程就叫非核心线程。

    • unit:keepAliveTime的单位。

      TimeUnit是一个枚举类型 ,包括以下属性:
      NANOSECONDS : 1微毫秒 = 1微秒 / 1000 MICROSECONDS : 1微秒 = 1毫秒 / 1000 MILLISECONDS : 1毫秒 = 1秒 /1000 SECONDS : 秒 MINUTES : 分 HOURS : 小时 DAYS : 天

    • workQueue:阻塞队列,存放着等待执行的Runnable任务对象。它一般分为直接提交队列、有界任务队列、*任务队列、优先任务队列、延迟队列几种。

      SynchronousQueue:同步队列,内部容量为0,每个put操作必须等待一个take操作,反之亦然。
      LinkedBlockingQueue:链式阻塞队列,底层数据结构是链表,默认大小是Integer.MAX_VALUE,也可以指定大小。
      ArrayBlockingQueue:数组阻塞队列,底层数据结构是数组,需要指定队列的大小。
      PriorityBlockingQueue:基于优先级的*阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),内部控制线程同步的锁采用的是非公平锁。
      DelayQueue:延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素

  • 选传入参

    • threadFactory:创建线程的工厂,一般用默认即可。可以自定义线程的名字,设置一些参数等等,如果不想自定义,可以使用默认的Executors.defaultThreadFactory()创建工厂。
    • handle:拒绝策略,当线程池数量大于maximumPoolSize,则执行拒绝策略。

      ThreadPoolExecutor.AbortPolicy:默认拒绝处理策略,丢弃任务并抛出RejectedExecutionException异常。
      ThreadPoolExecutor.DiscardPolicy:丢弃新来的任务,但是不抛出异常.
      ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列头部(最旧的)的任务,然后重新尝试执行程序(如果再次失败,重复此过程)。
      ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。

2.3、使用规范

  • 1、线程池数量设置

    io密集型程序:线程数=cpu核数+1;(计算密集型,但是任务越多,任务切换时间越多,则执行效率低,配置线程数尽可能小,则同时进行的任务数量等于cpu核数,线程数=cpu核数+1,处理任务中尽量少打日志,较少IO操作)
    cpu密集型程序:2*CPU核数;(涉及网络和磁盘IO操作,IO密集型任务线程并不是一直在执行任务,配置线程数尽可能多,如2*CPU核数)

  • 2、线程池的处理流程

    一个任务通过 execute(Runnable)方法被添加到线程池,任务就是一个 Runnable类型的对象,任务的执行方法就是 Runnable类型对象的run()方法
    当一个任务通过execute(Runnable)方法欲添加到线程池时:
    1、如果此时线程池中的数量<corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
    2、如果此时线程池中的数量= corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
    3、如果此时线程池中的数量>corePoolSize,缓冲队列workQueue满,并且线程池中的数量<maximumPoolSize,建新的线程来处理被添加的任务
    4、如果此时线程池中的数量>corePoolSize,缓冲队列workQueue满,并且线程池中的数量=maximumPoolSize,那么通过 handler所指定的策略来处理此任务。

    也就是处理任务的优先级为:
    核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
    当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。
    Java学习(十五)-线程与线程池学习(Thread与ThreadPoolExecutor)

2.4、四种常见线程池设置

1、Executors类中提供了几个静态方法实现了创建线程池
  • newCachedThreadPool
    核心线程数为0,非核心线程数为Integer.MAX_VALUE,所以这是一个线程只要空闲60秒就会被回收的线程池,适用于短时间高并发的处理业务,而在峰值过后并不会占用系统资源。

    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>(),
                                          threadFactory);
        }
    
  • newFixedThreadPool
    核心线程数量和总线程数量相等,都是传入的参数nThreads,这是一个池中都是核心线程的线程池,所有线程都不会销毁。执行任务的全是核心线程,当没有空闲的核心线程时,任务会进入到阻塞队列,直到有空闲的核心线程才会去从阻塞队列中取出任务并执行,也导致该线程池基本不会发生使用拒绝策略拒绝任务。还有因为LinkedBlockingQueue阻塞队列的大小默认是Integer.MAX_VALUE,如果使用不当,很可能导致内存溢出

    public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
    
  • newSingleThreadExecutor
    有且仅有一个核心线程的线程池( corePoolSize == maximumPoolSize=1),使用了LinkedBlockingQueue(容量很大),所以,不会创建非核心线程所有任务按照先来先执行的顺序执行。如果这个唯一的线程不空闲,那么新来的任务会存储在任务队列里等待执行。

    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>(),
                                        threadFactory));
        }
    
  • newScheduledThreadPool
    创建一个定长线程池,支持定时及周期性任务执行,这是一个支持延时任务执行的线程池。

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              //注意这里使用延迟队列
              new DelayedWorkQueue());
    }
    

    使用案例

    ScheduledExecutorService pool = Executors.newScheduledThreadPool(10);
    // 入参Runnable实例,延时时间,时间单位
    pool.schedule(() -> System.out.println("执行1"), 5, TimeUnit.SECONDS);
    pool.schedule(() -> System.out.println("执行2"), 4, TimeUnit.SECONDS);
    pool.schedule(() -> System.out.println("执行3"), 6, TimeUnit.SECONDS); 
    打印结果:
    执行2
    执行1
    执行3
    
2、newCachedThreadPool与newFixedThreadPool区别
  • 因为corePoolSize == maximumPoolSize ,所以FixedThreadPool只会创建核心线程。 而CachedThreadPool因为corePoolSize=0,所以只会创建非核心线程。
  • FixedThreadPoolgetTask()方法,如果队列里没有任务可取,线程会一直阻塞在LinkedBlockingQueue.take() ,线程不会被回收。CachedThreadPool会在60s后收回。
  • 由于线程不会被回收,会一直卡在阻塞,所以没有任务的情况下, FixedThreadPool占用资源更多
  • 都几乎不会触发拒绝策略,但是原理不同。FixedThreadPool是因为阻塞队列可以很大(最大为Integer.MAX_VALUE),故几乎不会触发拒绝策略;CachedThreadPool是因为线程池很大(最大为Integer.MAX_VALUE),几乎不会导致线程数量大于最大线程数,故几乎不会触发拒绝策略。

参考文档:
https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
https://blog.csdn.net/qq_41135605/article/details/11445310

上一篇:JUC- 线程池


下一篇:Java面试系列之并发编程专题-Java线程池灵魂拷问