【七】多线程 —— 线程池

一、为什么要用线程池

一个线程执行某个任务的时间片可以分成3块,创建线程T1,执行任务T2,销毁线程T3,现实中往往是 T1+T3>T2,也就是真正执行任务花费的时间很短,反而是创建与销毁线程更耗时,也是就是若我们需要多个线程去执行任务时,线程的创建和销毁会占用更多资源。于是有人提出提前创建一堆线程,然后把它们放在一个容器中统一进行管理,需要用的时候就直接拿出来用,用完之后再放回池子里。这样就不会在线程的创建和销毁上浪费时间。

上面的“池子”就是线程池,很明显线程池可以给我们带来很多好处:

  • 低资源消耗,降低了频繁创建线程和销毁线程的开销
  • 提高响应速度
  • 提高线程的可管理性,可以对线程进行一些操作,方便管理线程

池化技术有很多, 比如线程池数据库连接池HTTP连接池等等。

线程池的好处:

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

二、线程池中的核心参数

这个池子给我们带来很多好处,但这个池子不是没有边界的,需要一些参数来限制这个池子。

corePoolSize

核心线程的最大个数,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。

非核心线程:当等待队列满了,如果当前线程数没有超过最大线程数,则会新建线程执行任务,那么核心线程和非核心线程到底有什么区别呢?说出来你可能不信,本质上它们没有什么区别,创建出来的线程也根本没有标识去区分它们是核心还是非核心的,线程池只会去判断已有的线程数(包括核心和非核心)去跟核心线程数和最大线程数比较,来决定下一步的策略。

maximumPoolSize

线程池最大线程数,它表示在线程池中最多能创建多少个线程。线程数量超过这个值就会抛异常。

keepAliveTime

表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize。即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是allowCoreThreadTimeOut(true)方法可以使得线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0。

unit是参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:

TimeUnit.DAYS;               //天
TimeUnit.HOURS;             //小时
TimeUnit.MINUTES;           //分钟
TimeUnit.SECONDS;           //秒
TimeUnit.MILLISECONDS;      //毫秒
TimeUnit.MICROSECONDS;      //微妙
TimeUnit.NANOSECONDS;       //纳秒

workQueue

任务队列,是一个阻塞队列(需要等上一个任务处理完),用来存储等待执行的任务,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:ArrayBlockingQueue;LinkedBlockingQueue;SynchronousQueue。

ArrayBlockingQueue: 这是一个由数组实现的容量固定的有界阻塞队列,,此队列按 FIFO(先进先出)原则对元素进行排序
SynchronousQueue: 没有容量,不能缓存数据;每个put必须等待一个take; offer()的时候如果没有另一个线程在poll()或者take()的话返回false。静态工厂方法Executors.newCachedThreadPool使用了这个队列。
LinkedBlockingQueue: 这是一个由单链表实现的默认无界的阻塞队列。LinkedBlockingQueue提供了一个可选有界的构造函数,而在未指明容量时,容量默认为Integer.MAX_VALUE。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。

threadFactory

线程工厂,主要用来创建线程。

handler

表示当拒绝处理任务时的策略,有以下四种取值:

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 

三、线程的任务处理流程

在这里插入图片描述
当在execute(Runnable)方法中提交新任务并且少于corePoolSize线程正在运行时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求。 如果有多于corePoolSize但小于maximumPoolSize线程正在运行,则仅当队列已满时才会创建新线程。 通过设置corePoolSize和maximumPoolSize相同,您可以创建一个固定大小的线程池。 通过将maximumPoolSize设置为基本上无界的值,例如Integer.MAX_VALUE,您可以允许池容纳任意数量的并发任务。 通常,核心和最大池大小仅在构建时设置,但也可以使用setCorePoolSize和setMaximumPoolSize进行动态更改。

四、 Executor、ExecutorService、Executors

Executor 是一个抽象层面的核心接口,它定义了execute()方法,用来接收一个Runnable接口的对象。

ExecutorService 接口继承了Executor 接口,是Executor 的子接口。ExecutorService 接口对 Executor 接口进行了扩展,提供了返回 Future 对象,终止,关闭线程池等方法。当调用 shutDown 方法时,线程池会停止接受新的任务,但会完成正在 pending 中的任务。Executor接口中execute()方法不返回任何结果,而ExecutorService接口中submit()方法可以通过一个 Future 对象返回运算结果。通过 ExecutorService.submit() 方法返回的 Future 对象,还可以取消任务的执行。Future 提供了 cancel() 方法用来取消执行 pending 中的任务。

Executors 类提供了若干个静态方法,用于生成不同类型的线程池。但线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,下面将介绍。

五、ThreadPoolExecutor

讲完了上面的核心参数就可以看看怎么创建线程池了,ThreadPoolExecutor是线程池的核心类,有4个构造方法可以得到我们需要的线程池:

 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
   
   
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
}
 
 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
   
   
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
}
 
 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
   
   
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
 }
 
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
   
   
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

六、常见的线程池及其使用场景

在 Executors 类里面提供了一些静态工厂,生成一些常用的线程池。

newFixedThreadPool

创建固定大小的线程池,每提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

特点

  • 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
  • 阻塞队列是无界的,可以放任意数量的任务
  • 适用于任务量已知,相对耗时的任务

newCachedThreadPool(推荐使用)

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。

特点

  • 没有核心线程,最大线程数为Integer.MAX_VALUE,所有创建的线程都是救急线程 (可以无限创建),空闲时生存时间为60秒
  • 阻塞队列使用的是SynchronousQueue,没有容量,没有线程来取是放不进去的,只有当线程取任务时,才会将任务放入该阻塞队列中
  • 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况

newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

使用场景:

  • 希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。 任务执行完毕,这唯一的线程也不会被释放。
  • 和自己创建单线程执行任务的区别:自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而newSingleThreadExecutor线程池还会新建一个线程,保证池的正常工作
  • Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改

newScheduledThreadPool

创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:

  • 1)newFixedThreadPool和newSingleThreadExecutor:
      主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
  • 2)newCachedThreadPool和newScheduledThreadPool:
      主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

在『任务调度线程池』功能加入之前,可以使用java.util.Timer来实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务。

@Slf4j(topic = "guizy.TestTimer")
public class TestTimer {
   
   
    public static void main(String[] args) {
   
   
        Timer timer = new Timer();
        TimerTask task1 = new TimerTask() {
   
   
            @Override
            public void run() {
   
   
                log.debug("task 1");
                Sleeper.sleep(2);
            }
        };

        TimerTask task2 = new TimerTask() {
   
   
            @Override
            public void run() {
   
   
                log.debug("task 2");
            }
        };
		// 使用timer添加两个任务, 希望他们都在1s后执行
		// 由于timer内只有一个线程来执行队列中的任务, 所以task2必须等待task1执行完成才能执行
        timer.schedule(task1, 1000);
        timer.schedule(task2, 1000);
    }
}
08:21:17.548 guizy.TestTimer [Timer-0] - task 1
08:21:19.553 guizy.TestTimer [Timer-0] - task 2

使用 ScheduledExecutorService:

public class TestTimer {
   
   
    public static void main(String[] args) {
   
   
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);

        executor.schedule(() -> System.out.println("任务1, 执行时间:" + new Date()), 1000, TimeUnit.MILLISECONDS);

        executor.schedule(() -> System.out.println("任务2, 执行时间:" + new Date()), 1000, TimeUnit.MILLISECONDS);
    }
}
任务1, 执行时间:Sun Jan 03 08:53:54 CST 2021
任务2, 执行时间:Sun Jan 03 08:53:54 CST 2021

七、如何合理配置线程池的大小

线程池究竟设成多大是要看你给线程池处理什么样的任务,任务类型不同,线程池大小的设置方式也是不同的。

任务一般可分为:CPU密集型、IO密集型、混合型,对于不同类型的任务需要分配不同大小的线程池。

  • CPU密集型任务
    尽量使用较小的线程池,一般为CPU核心数+1。
    因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。额外的一个线程用于应对偶尔的任务阻塞或意外的上下文切换。
  • IO密集型任务
    可以使用稍大的线程池,一般为2*CPU核心数。
    IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。
  • 混合型任务
    可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。
    只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
    因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

怎么合理设置线程池的参数?

为了说明合理设置的条件,我们首先确定有以下几个相关参数:
1.tasks,程序每秒需要处理的最大任务数量(假设系统每秒任务数为100~1000)
2.tasktime,单线程处理一个任务所需要的时间(每个任务耗时0.1秒)
3.responsetime,系统允许任务最大的响应时间(每个任务的响应时间不得超过2秒)

corePoolSize:

每个任务需要tasktime秒处理,则每个线程每秒可处理1/tasktime个任务。系统每秒有tasks个任务需要处理,则需要的线程数为:tasks/(1/tasktime)。即tasks*tasktime个线程数。假设系统每秒任务数为100到1000之间,每个任务耗时0.1秒,则需要100x0.1至1000x0.1,即10到100个线程。那么corePoolSize应该设置为大于10。
具体数字最好根据二八原则,即80%情况下系统每秒任务数,若系统80%的情况下任务数小于200,最多时为1000,则corePoolSize可设置为20。

queueCapacity:任务队列的长度

队列的大小决定了系统能够缓冲多少任务。队列过大可能导致任务等待时间过长,队列过小则可能导致频繁地创建新线程‌。

任务队列的长度通常设计为:核心线程数除以单个任务执行时间再乘以2,即 queueCapacity = corePoolSize / 任务执行时间 * 2。例如,如果核心线程数为10,单个任务执行时间为0.1秒,则队列长度可以设计为200‌。
如果队列长度设置过大,会导致任务响应时间过长,如以下写法:
LinkedBlockingQueue queue = new LinkedBlockingQueue();
这实际上是将队列长度设置为Integer.MAX_VALUE,将会导致线程数量永远为corePoolSize,再也不会增加,当任务数量陡增时,任务响应时间也将随之陡增。

maxPoolSize:最大线程数

当系统负载达到最大值时,核心线程数已无法按时处理完所有任务,这时就需要增加线程。每秒200个任务需要20个线程,那么一个任务需要的线程数=20/200=0.1,那么当每秒达到1000个任务时,则需要==(1000-queueCapacity)*(20/200)==,即60个线程,可将maxPoolSize设置为60。

keepAliveTime:

线程数量只增加不减少也不行。当负载降低时,可减少线程数量,如果一个线程空闲时间达到keepAliveTime,该线程就退出。默认情况下线程池最少会保持corePoolSize个线程。keepAliveTime设定值可根据任务峰值持续时间来设定。

以上关于线程数量的计算并没有考虑CPU的情况。若结合CPU的情况,比如,当线程数量达到50时,CPU达到100%,则将maxPoolSize设置为60也不合适,此时若系统负载长时间维持在每秒1000个任务,则超出线程池处理能力,应设法降低每个任务的处理时间(tasktime)。

八、怎么理解无界队列和有界队列

有界队列
就是有固定大小的队列。比如设定了固定大小的 LinkedBlockingQueue,又或者大小为 0,只是在生产者和消费者中做中转用的 SynchronousQueue。
无界队列
指的是没有设置固定大小的队列。这些队列的特点是可以直接入列,直到溢出。当然现实几乎不会有到这么大的容量(超过 Integer.MAX_VALUE),所以从使用者的体验上,就相当于 “无界”。比如没有设定固定大小的 LinkedBlockingQueue。与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续增加,若后续仍有新的任务加入,而没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略。

常见的有界队列为:

  • ArrayBlockingQueue 基于数组实现的阻塞队列
  • LinkedBlockingQueue其实也是有界队列,但是不设置大小时就时Integer.MAX_VALUE,内部是基于链表实现的
  • SynchronousQueue
    比较奇葩,内部容量为零,适用于元素数量少的场景,尤其特别适合做交换数据用

常见的无界队列:

  • ConcurrentLinkedQueue无锁队列,底层使用CAS操作,通常具有较高吞吐量,但是具有读性能的不确定性,弱一致性——不存在如ArrayList等集合类的并发修改异常,通俗的说就是遍历时修改不会抛异常
  • PriorityBlockingQueue 具有优先级的阻塞队列 DelayedQueue 延时队列
  • LinkedTransferQueue 简单的说也是进行线程间数据交换的利器

九、线程池是有哪些放弃策略?

  1. AbortPolicy:ThreadPoolExecutor中默认的拒绝策略就是AbortPolicy。直接抛出异常也不处理
  2. CallerRunsPolicy:在任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务;主线程直接执行该任务,执行完之后尝试添加下一个任务到线程池中,这样可以有效降低向线程池内添加任务的速度。
  3. DiscardPolicy:采用这个拒绝策略,会让被线程池拒绝的任务直接抛弃,不会抛异常也不会执行。
  4. DiscardOldestPolicy:当任务被拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务加入。

建议大家用 CallerRunsPolicy 策略,因为当队列中的任务满了之后,如果直接抛异常,那么这个任务就会被丢弃。如果是 CallerRunsPolicy 策略,则会用主线程去执行,也就是同步执行,这样操作最起码任务不会被丢弃。

#十、自定义一个简单的线程池
在这里插入图片描述

  • 阻塞队列中维护了由主线程(或者其他线程)所产生的的任务
  • 主线程类似于生产者,产生任务并放入阻塞队列中
  • 线程池类似于消费者,得到阻塞队列中已有的任务并执行

自定义线程池的实现步骤 :

  • 步骤1:自定义拒绝策略接口
  • 步骤2:自定义任务阻塞队列
  • 步骤3:自定义线程池
  • 步骤4:测试
@FunctionalInterface // 接口中只有一个方法,加上这个注解,实现lambda简写
interface RejectPolicy<T> {
   
   
    void reject(BlockingQueue<T> queue, T task);
}
public class BlockingQueue<T> {
   
   
    // 1、任务队列
    private Deque<T> queue = new ArrayDeque<>();

    // 2、锁
    private ReentrantLock lock = new ReentrantLock();

    // 3、生产者的条件变量 (当阻塞队列塞满任务的时候, 没有空间, 此时进入条件变量中等待)
    private Condition fullWaitSet = lock.newCondition();

    // 4、消费者的条件变量 (当没有任务可以消费的时候, 进入条件变量中等待)
    private Condition emptyWaitSet = lock.newCondition();

    // 5、阻塞队列的容量
    private int capacity;

    public BlockingQueue(int capacity) {
   
   
        this.capacity = capacity;
    }

    // 获取队列大小
    public int size() {
   
   
        lock.lock();
        try {
   
   
            return queue.size();
        } finally {
   
   
            lock.unlock();
        }
    }

    // 从阻塞队列中获取任务, 如果没有任务,会一直等待
    public T take() {
   
   
        lock.lock();
        try {
   
   
            // 阻塞队列是否为空
            while (queue.isEmpty()) {
   
   
                // 进入消费者的条件变量中等待,此时没有任务供消费
                try {
   
   
                    emptyWaitSet.await();
                } catch (InterruptedException e) {
   
   
                    e.printStackTrace();
                }
            }
            // 阻塞队列不为空, 获取队列头部任务
            T t = queue.removeFirst();
            fullWaitSet.signal(); // 唤醒生产者进行生产, 此时阻塞队列没有满
            return t;
        } finally {
   
   
            lock.unlock();
        }
    }

    // 从阻塞队列中获取任务, 如果没有任务, 会等待指定的时间
    public T 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

沙滩de流沙

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

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

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

打赏作者

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

抵扣说明:

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

余额充值