线程池的参数详解

一、ThreadPoolExecutor的方法说明

为了方便测试,先学习一下相关的方法

1.继承关系

在这里插入图片描述
Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的
ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等抽象类
AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法
ThreadPoolExecutor继承了类AbstractExecutorService。

2.方法说明

(1)向线程池提交任务

execute():实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行

executor.execute(() -> {
     // do something 1
});

submit():是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果

executor.submit(new Runnable() {
    @Override
    public void run() {
        //方法
    }
});
(2)关闭线程池

shutdown():优雅关闭线程池,使用局部变量的时候必须关闭,否则会造成线程泄露,使用全局变量的时候一般不关闭线程池

shutdownNow():强制关闭线程池

线程池的状态
上图是线程池的五种状态,如果线程池被定义为局部变量,则用完之后必须关闭线程池。在实际应用中,线程池通常被定义为静态全局变量,这个时候不需要关闭

(3)获取线程池中线程和任务的数目

getQueue() :获取阻塞队列,可以通过调用size()方法获取队列中等待执行的任务数目

getPoolSize() :线程池中线程数目

getActiveCount():线程池中的活跃线程数目

getCorePoolSize():线程池中的核心线程数目

getCompletedTaskCount():已执行完毕的任务数目

二、线程池的参数

public ThreadPoolExecutor(int corePoolSize,  // 线程池的核心线程数
                          int maximumPoolSize, // 线程池的最大线程数
                          long keepAliveTime, // 当线程数大于核心时,多余的空闲线程等待新任务的存活时间。
                          TimeUnit unit, // keepAliveTime的时间单位
                          ThreadFactory threadFactory, // 线程工厂
                          BlockingQueue<Runnable> workQueue,// 用来储存等待执行任务的队列
                          RejectedExecutionHandler handler // 拒绝策略
                          ) 

1.corePoolSize

线程池保留的最小线程数。如果线程池中的线程少于此数目,则在执行execut()时创建。

2.maximumPoolSize

线程池中允许拥有的最大线程数。
如果对于和核心线程和最大线程依然有疑惑,不用急,后面会有详细的说明

3&4.keepAliveTime、unit

当线程闲置时,保持线程存活的时间。
默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

5.threadFactory

使用默认的即可

6.workQueue

工作队列,存放提交的等待任务,其中有队列大小的限制。

ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列(数组结构可配合指针实现一个环形队列)。
LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列,而在未指明容量时,容量默认为Integer.MAX_VALUE。
LinkedBlockingDeque: 使用双向队列实现的双端阻塞队列,双端意味着可以像普通队列一样FIFO(先进先出),可以以像栈一样FILO(先进后出)
PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列,对元素没有要求,可以实现Comparable接口也可以提供Comparator来对队列中的元素进行比较,跟时间没有任何关系,仅仅是按照优先级取任务。
DelayQueue:同PriorityBlockingQueue,也是二叉堆实现的优先级阻塞队列。要求元素都实现Delayed接口,通过执行时延从队列中提取任务,时间没到任务取不出来。
SynchronousQueue: 一个不存储元素的阻塞队列,消费者线程调用take()方法的时候就会发生阻塞,直到有一个生产者线程生产了一个元素,消费者线程就可以拿到这个元素并返回;生产者线程调用put()方法的时候就会发生阻塞,直到有一个消费者线程消费了一个元素,生产者才会返回。

缓存队列(阻塞队列)及排队策略

(1)直接提交(同步提交),用SynchronousQueue。特点是不放在队列里,直接提交给线程,如果没有线程,则新建一个。

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

(2)无限提交,用类似LinkedBlockingQueue无界队列。特点是保存所有核心线程处理不了的任务,队列无上限,maximumPoolSize没有意义。

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

(3)有限提交,用类似ArrayBlockingQueue有界队列。特点是可以保存超过核心线程的任务,并且队列也是有上限的。最常用的排队策略。可以避免资源浪费,在一定程度上降低了吞吐量,当队列饱和就会执行拒绝任务

7.handler

拒绝策略,有以下四种取值:
AbortPolicy(默认):丢弃任务并抛出RejectedExecutionException异常。
CallerRunsPolicy:由调用线程处理该任务。(例如io操作,线程消费速度没有NIO快,可能导致阻塞队列一直增加,此时可以使用这个模式)
DiscardPolicy:丢弃任务,但是不抛出异常。 (可以配合这种模式进行自定义的处理方式)
DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务(重复执行)

三、线程池的工作原理

(1)如果当前运行的线程少于corePoolSize(核心线程数),则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
(2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue(阻塞队列/任务队列)。
(3)如果无法将任务加入BlockingQueue(队列已满),则在非corePool中创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。
(4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并执行线程饱和策略,如:RejectedExecutionHandler.rejectedExecution()方法。
在这里插入图片描述
注意:
(1)线程池初始化时,是空的。如果线程池中的线程数少于核心线程数,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务。
(2)如果阻塞队列已满,且当前线程数<maximumPoolSize,则新建线程执行该任务。而不是新建线程,从阻塞队列里take任务来执行,所以这里并不是先来先执行的。
(3)阻塞队列用于存放任务,无法执行任务,起到一个缓冲的作用。阻塞队列的长度与最大线程数目无关。

1.为什么要区分核心线程和非核心线程

创建线程是有代价的,不能每次要执行一个任务时就创建一个线程,但是也不能在任务非常多的时候,只有少量的线程在执行,这样任务是来不及处理的,而是应该创建合适的足够多的线程来及时的处理任务。随着任务数量的变化,当任务数明显很小时,原本创建的多余的线程就没有必要再存活着了,因为这时使用少量的线程就能够处理的过来了,所以说真正工作的线程的数量,是随着任务的变化而变化的。

2.这种设计方式有什么好处

ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。

3.线程池核心线程数配置推荐

CPU密集型任务:尽量压榨CPU,参考值设置为CPU的个数+1
IO密集型任务:参考值可以设置为CPU的个数 * 2

四、下面给出一个测试的用例

package thread.threadPool;

import java.util.concurrent.*;

public class NewFixedThreadPoolTest {

    // 创建一个可重用固定个数的线程池
    private static ThreadPoolExecutor  fixedThreadPool = new ThreadPoolExecutor(1, 2,
            0L, TimeUnit.MILLISECONDS,
            new ArrayBlockingQueue<Runnable>(1));


    public static void main(String[] args) {
        fixedThreadPool.execute(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        printCount();
        System.out.println("加入第一个任务,线程池刚刚初始化,没有可以执行任务的核心线程,创建一个核心线程来执行任务");

        fixedThreadPool.execute(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        printCount();
        System.out.println("加入第二个任务,没有可以执行任务的核心线程,且任务数大于corePoolSize,新加入任务被放在了阻塞队列中");

        fixedThreadPool.execute(new Runnable() {
            public void run() {
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        printCount();
        System.out.println("加入第三个任务,此时,阻塞队列已满,新建非核心线程执行新加入任务");

        try {
            Thread.sleep(600);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        printCount();
        System.out.println("第一个任务执行完毕,核心线程空闲,阻塞队列的任务被取出来,使用核心线程来执行");

        try {
            Thread.sleep(600);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        printCount();
        System.out.println("第二个任务执行完毕,核心线程空闲,非核心线程在执行第三个任务");

        try {
            Thread.sleep(600);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        printCount();
        System.out.println("第三个任务执行完毕,非核心线程被销毁,核心线程保留");

    }

    private static void printCount() {

        System.out.println("------------------------------------");
        System.out.println("当前活跃线程数:"+fixedThreadPool.getActiveCount());
        System.out.println("当前核心线程数:"+fixedThreadPool.getCorePoolSize());
        System.out.println("阻塞队列中的任务数:"+fixedThreadPool.getQueue().size());
    }

}

五、Executors框架创建四种线程池

使用不同的参数,可以创造不同的线程池,常用的以下四种:
(不建议使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式可以让各位更加明确线程池的运行规则,规避资源耗尽的风险。)

1.newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

2.newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()。

3.newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
 scheduledThreadPool.schedule(new Runnable() {

@Override
public void run() {
    System.out.println("delay 3 seconds");
}
}, 3, TimeUnit.SECONDS);

表示延迟3秒执行。定期执行示例代码如下:

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
    System.out.println("delay 1 seconds, and excute every 3 seconds");
}
}, 1, 3, TimeUnit.SECONDS);

//        scheduledThreadPool.scheduleAtFixedRate(()-> {
//                System.out.println("delay 3 seconds"+Thread.currentThread().getName());
//       }, 1,1, TimeUnit.SECONDS);


特别提示:通过ScheduledExecutorService执行的周期任务,如果任务执行过程中抛出了异常,那么ScheduledExecutorService就会停止执行任务,而且也不会再周期地执行该任务了。所以如果想保持任务周期执行,需要catch一切可能的异常。

4.newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值