编程语言
首页 > 编程语言> > 【Java多线程】2. 线程池、阻塞队列

【Java多线程】2. 线程池、阻塞队列

作者:互联网

目录

线程池/Executor框架

Executor和所有线程池都由JDK实现,位于JUC包中。

为什么要用线程池:

  1. 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,执行多个任务。
  2. 可以根据系统的承受能力,调整线程池中工作线线程的数目。执行线程过少浪费系统资源,过多造成系统拥挤效率不高,甚至由于消耗太多的内存导致死机。
  3. 还可以提供定时执行、定期执行、线程中断等功能。

Executor 框架管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。

Java中,线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。

基本参数与工作逻辑

1. 基本参数

  1. corePoolSize:最大核心线程数量
  2. maximumPoolSize:最大线程(核心+非核心)数量
  3. keepAliveTimeTimeUnit:前者是线程退出需要满足的空闲时间,不允许小于0。后者是前者的单位。
  4. allowCoreThreadTimeout:是否允许核心线程退出,默认为false。一般不启用,想让没有任务时所有线程都退出可以把corePoolSize设为0。
  5. workQueue:用来存储等待执行任务的阻塞队列。类型为BlockingQueue<Runnable>,一般在ArrayBlockingQueueLinkedBlockingQueueSynchronousQueue三种阻塞队列中选择。
  6. threadFactory:用来创建线程的线程工厂。
  7. handler:拒绝策略。提供了以下4种取值,都是rejectedExecution的一种实现,也可以自定义:
  1. ThreadPoolExecutor.AbortPolicy(默认):丢弃任务,并抛出RejectedExecutionException异常。
  2. ThreadPoolExecutor.DiscardPolicy:丢弃任务,但不抛出异常。
  3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。
  4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 

2. 处理任务

  1. 若当前线程数小于corePoolSize,创建一个线程来执行任务。
  2. 若线程数大于等于corePoolSize,且任务队列未满,将任务放入任务队列。
  3. 若线程数大于等于corePoolSize,且任务队列已满,若线程数小于maximumPoolSize,创建一个线程
  4. 当线程数大于等于corePoolSize,且任务队列已满,若线程数等于maximumPoolSize,按设定的拒绝策略来处理。

3. 关闭线程

规则:

  1. 默认allowCoreThreadTimeOut为false,当有线程空闲时间达到keepAliveTime,且当前线程数量超过corePoolSize时,关闭此空闲线程。
  2. 若设置allowCoreThreadTimeOut为true,只要线程空闲时间达到keepAliveTime,无论当前线程数为多少,都会关闭此空闲线程。

详细来说,有下面几种情况

  1. keepAliveTime>0 且 allowCoreThreadTimeout为false:空闲线程超时退出,保留核心线程数
  2. keepAliveTime==0 且 allowCoreThreadTimeout为false:线程完成任务立刻退出,保留核心线程数
  3. keepAliveTime>0 且 allowCoreThreadTimeout为true:空闲线程超时退出,不保留核心线程数
  4. keepAliveTime==0 且 allowCoreThreadTimeout为true:不允许。

常见线程池

Executors主要提供了4种线程池:

  1. CachedThreadPool(没有核心线程,非核心数不限制)
  2. FixedThreadPool(定长,且只有核心线程)
  3. SingleThreadExecutor 单一线程池
  4. ScheduledThreadPool 调度线程池

以及 ForkJoinPool。

1. CachedThreadPool

没有核心线程,非核心数不限制。corePoolSize为0,maximumPoolSizeInteger.MAX_VALUEkeepAliveTime为60秒,任务队列使用的是SynchronousQueue

当有任务时创建线程来执行任务,没有任务时超过60秒回收线程。即一个任务创建一个线程,任务队列中永远没有任务。

适用于耗时少,任务量大的情况。

示例
public static void main(String[] args) {
    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    for (int i = 0; i < 5; i++) {
        cachedThreadPool.execute(/*这里参数为任务类的实例*/);
    }
    cachedThreadPool.shutdown();
}

2. FixedThreadPool

定长,且只有核心线程。corePoolSize为构造给定参数nThreads,maximumPoolSize也为nThreads,keepAliveTime为0,任务队列使用的是LinkedBlockingQueue

可控制线程最大并发数,超出的线程会在队列中等待。

3. SingleThreadExecutor

单一线程池。相当于大小为 1 的 FixedThreadPool,只会用唯一的工作线程来执行任务,保证所有任务按照阻塞队列规定的出列顺序执行。

规定的出列顺序有:FIFO、LIFO、优先级。

4. ScheduledThreadPool

调度线程池。定长,支持定时及周期性任务执行。

与Timer比较

Timer的内部只有一个线程,如果有多个任务的话就会顺序执行,这样延迟时间和循环时间就会出现问题。

而ScheduledExecutorService不会出现这种情况,更加安全。

因此在对延迟任务和循环任务要求严格的时候,应考虑使用ScheduledExecutorService比Timer。

展开代码
//注意这里的声明不太一样,用了ExecutorService的子类ScheduledExecutorService
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
//延时三秒执行
scheduledThreadPool.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println("delay 3 seconds");
    }
}, 3, TimeUnit.SECONDS);

//延时1秒,并且每三秒执行一次
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        System.out.println("delay 1 seconds, and excute every 3 seconds");
    }
}, 1, 3, TimeUnit.SECONDS);

5. ForkJoinPool

阻塞队列 BlockingQueue

阻塞:就是在某些情况下会挂起线程。(而一旦条件满足,被挂起的线程又会自动被唤醒。)

线程池两种常见的阻塞情况:

  1. 队列为空时,消费者被阻塞,直到队列中加入数据。
  2. 队列已满时,生产者被阻塞,直到队列中有空的位置。

JUC中的 BlockingQueue 接口继承了Queue接口,提供了以下两种阻塞方法

  1. put(E e):如果阻塞队列没有空间,则调用此方法的线程会被阻塞直到有空间再继续。
  2. take():若阻塞队列为空,则调用此方法的线程会被阻塞直到有元素再继续。

具体实现有

1.ArrayBlockingQueue

基于数组实现的阻塞队列。

内部维护了一个定长数组,来缓存队列中的数据对象,以及两个整形变量,分别标识着队列的头部和尾部在数组中的位置。ArrayBlockingQueue 在插入或删除元素时不会产生或销毁任何额外的对象。

生产者和消费者共用同一个锁对象,因此二者的操作无法真正并行运行。而在创建ArrayBlockingQueue时,可以控制内部锁是否采用公平锁,默认采用非公平锁。

2. LinkedBlockingQueue

基于链表实现的阻塞队列。

内部维护了一个链表,来缓存队列中的数据对象。(区别 1

生产者和消费者分别采用了独立的锁,因此二者可以并行地操作队列中的数据,以此提高了整个队列的并发性能。(区别 2

如果构造一个LinkedBlockingQueue,而没有指定其容量大小,会默认一个类似无限大小的容量(Integer.MAX_VALUE)。这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。

LinkedBlockingQueue 在插入或删除元素时会生成一个额外的Node对象,在长时间需要高效并发处理大批量数据的系统中,对于GC的影响还是存在一定的影响。(区别 3

LinkedBlockingQueue 只能使用默认的非公平锁。(区别 4

标签:Java,队列,keepAliveTime,阻塞,corePoolSize,任务,线程,多线程
来源: https://www.cnblogs.com/BWSHOOTER/p/14372865.html