Java线程池 ThreadPoolExecutor详解(一) -- 基于JDK1.8

目录

 

1、为什么需要线程池?

(1)、线程池好处是:

2、Executors常用线程池种类

(1)、通过Executors工具类可以创建哪几种类型线程池

a、newFixedThreadPool(nThreads)

b、 newCachedThreadPool()

c、newScheduledThreadPool(corePoolSize)

d、newSingleThreadExecutor

(2)、阻塞队列与非阻塞队列区别

(3)、Executors静态工厂方法有哪些缺点?

· newFixedThreadPool

· newSingleThreadExecutor

· newCachedThreadPool

· newScheduledThreadPool

(4)、怎么估计线程池大小

(5)、线程池类常用方法


1、为什么需要线程池?

现代操作系统调度的最小执行任务单元是线程,而面对多个任务同时提交的时候,

我们使用多线程方式,能够很好的利用现代多核CPU并行处理任务的性能优势;

但是这些线程的频繁创建和销毁、一个线程销毁后让出使用权给已经就绪的线程导致的线程上下文切换(线程之间转让cpu的使用权)、创建多余线程维护他们存活的成本,这些都会占用很大的系统资源开销;

使用池化技术来管理一些资源,由线程池统一管理线程的创建和销毁,线程池技术很好的解决了上面的问题;它预先创建了一定数量的线程,并且可以重复使用较为固定数目的线程来完成任务的执行。

 

(1)、线程池好处是:

减少不必要的内存和CPU消耗:通过重复利用已创建的线程,来减少频繁线程创建和销毁;

第二:提升了任务执行速度:当任务到达时,任务无限再另外创建线程,利用已有线程直接执行;

第三:提高任务整体管理可控性:根据系统承受能力和业务实际情况,调整线程池中线程数, 使用线程池进行统一分配、调度任务,线程任务提交过多的执行策略等;

线程池ThreadPoolExecutor的基本结构,在IDEA中,右击concurrent包,显示Diagrams,结构图如下:

2、Executors常用线程池种类

(1)通过Executors工具类可以创建哪几种类型线程池

在Java中其实有很多这样的命名写法:

例如Executor是线程池接口,Executors就是线程池工具类;

Array数组,Arrays数组工具类;

Collection是集合类接口,Collections是集合类工具类;

 

下面来看Executors工具类中提供的创建线程池的方法:

 

a、newFixedThreadPool(nThreads)

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

创建固定大小的线程池。创建核心线程数nThreads,最大线程数nThreads的线程池,队列采用阻塞队列LinkedBlockingQueue(默认大小Integer.MAX_VALUE);每提交一个任务就创建一个线程,直到线程数达到nThreads。线程池的大小达到nThreads后,尽管keepAliveTime为0,因为核心线程数等于最大线程数,所以闲置线程不会被回收,线程池数量一直保持在nThreads;如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

这种类型的线程池,适用于处理并发相对比较稳定的任务;

 

b、 newCachedThreadPool()

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

创建一个可缓存的线程池。核心线程数0,最大线程数Integer.MAX_VALUE的线程池,队列采用SynchronousQueue;不像ArrayBlockingQueue或LinkedListBlockingQueue,SynchronousQueue内部并没有数据缓存空间;

如果线程池的大小超过了处理任务所需要的线程,60秒过后回收闲置多余线程,当任务数重新增加时,线程池又可以添加新的线程来处理任务。此线程池不会对线程池大小做限制,线程池大小基本依赖于JVM能够创建的最大线程大小。

这种类型的线程池,既可接受吞吐量高的并发,又能在并发小的时候减少创建线程,节省资源,可伸缩性好;

 

c、newScheduledThreadPool(corePoolSize)

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

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

创建核心线程数corePoolSize,最大线程数Integer.MAX_VALUE的线程池,

采用延迟队列DelayedWorkQueue;

这种类型的线程池,主要执行可延迟性的,可定时周期控制的的任务;

d、newSingleThreadExecutor

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

创建一个单线程的线程池。这个线程池只有一个线程在工作,

也就是相当于单线程串行执行所有任务。

如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。

此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

创建核心线程数1,最大线程数1的线程池,队列采用LinkedBlockingQueue

(默认大小Integer.MAX_VALUE);

 

(2)阻塞队列与非阻塞队列区别

线程池为什么必须用阻塞队列?

线程池执行的任务,很明显是一个生产者-消费者模式,然而非阻塞队列只能保证一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了;

但是阻塞队列可以通过阻塞可以保留住当前想要继续入队的任务:

使用非阻塞队列它不会对当前线程产生阻塞,那么在面对生产者-消费者模式的时候,就必须额外地实现同步策略以及线程间唤醒策略,我们自己实现起来就非常麻烦,然而阻塞队列很好的帮我们实现了这一功能;

 

(3)、Executors静态工厂方法有哪些缺点?

· newFixedThreadPool

缺点:采用阻塞队列,LinkedBlockingQueue<Runnable>,创建的时候,并没有指定队列容量大小,此队列默认大小为Integer.MAX_VALUE;

 

· newSingleThreadExecutor

缺点:跟上面一样,LinkedBlockingQueue默认大小,Integer.MAX_VALUE;

并且还是单线程串行执行所有任务,不适合并发场景;

 

· newCachedThreadPool

缺点:创建的时候,采用了SynchronousQueue,一个不存储元素的阻塞队列,

但是maximumPoolSize最大线程数为Integer.MAX_VALUE;

并且一个任务失败,其它任务受到影响;

 

· newScheduledThreadPool

缺点:采用DelayedWorkQueue,延迟队列,默认大小16,同样maximumPoolSize最大线程数为Integer.MAX_VALUE;

 

并在《阿里巴巴Java开发手册》章节:“一、编程规约->(六) 并发处理->4.”这条规则中找到如下建议:

 

综上所述,我们不建议使用Executors工具类来创建线程池,不管是最大线程数过量,还是队列大小过量,都有可能导致OOM内存溢出问题;所以建议用线程池类ThreadPoolExecutor自带构造方法自己根据适合的参数创建线程池。

我们可以使用google的guava中ThreadFactoryBuilder来作为线程池的线程创建工厂参数,给线程池取名字,设置为守护线程等更方便。如下示例代码:

// myThreadPool-%d中的 ” %d ” 在String中表示整数的意思:
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("myThreadPool-%d").build();
ExecutorService pool = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors()+1, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(100), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

 

(4)、怎么估计线程池大小

如果是偏CPU计算型:应用需要非常多的CPU计算资源,为了避免过多的线程上下文切换,比较理想方案是:核心线程数大小 = CPU内核线程数+1。

 

如果是偏IO操作型:如果线程需要操作文件,读写数据库,这些涉及到IO操作的任务,因为IO涉及到阻塞等待,在阻塞等待期间,我们可以多设置一些线程数,去处理后续任务需要准备执行的工作,前面的线程准备好后,可以再继续执行。因此对于IO操作的任务,我们可以多设置线程池中的核心线程数,这样就能让在等待IO的这段时间内,线程可以去做其它事,提高并发处理效率。比较合适的方案是:比较理想方案是:核心线程数大小 = 2*CPU内核线程数+1。

 

(5)线程池类常用方法

a、线程池执行任务方法:

· 没有返回结果execute方法public void execute(Runnable command)

· 有返回结果submit方法public <T> Future<T> submit(Callable<T> task)

 

b、线程池的关闭ThreadPoolExecutor提供了两个方法,用于线程池的关闭:

· shutdown():不会立即终止线程池,而是要等所有任务缓

存队列中的任务都执行完后才终止,但再也不会接受新的任务;

· shutdownNow():立即终止线程池,并尝试打断正在执行的任务,

清空任务缓存队列,返回尚未执行的任务;

下一篇:

Java线程池ThreadPoolExecutor详解(二) --基于JDK1.8

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值