Java 并发编程(线程池篇)

并发编程(线程池篇)


前言

设么是线程池?


线程池(Thread Pool)是一种基于池化思想管理线程的工具,经常出现在多线程服务器中,如MySQL。

线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。

而本文描述线程池是JDK中提供的ThreadPoolExecutor类。

不使用线程池有什么坏处?


  1. 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常大的性能消耗。
  2. 对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
  3. 系统无法合理管理内部的资源分布,会降低系统的稳定性。

当然,使用线程池可以带来一系列好处

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

怎么用?

1.线程池种类:


  1. 内存池(Memory Pooling):预先申请内存,提升申请内存速度,减少内存碎片。
  2. 连接池(Connection Pooling):预先申请数据库连接,提升申请连接的速度,降低系统的开销。
  3. 实例池(Object Pooling):循环使用对象,减少资源在初始化和释放时的昂贵损耗。
Java通过Executors提供了四种线程池:
  • newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。(线程最大并发数不可控制)
  • newFixedThreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool:创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
 /** 
     * 线程池初始化方法 
     *  
     * corePoolSize 核心线程池大小----10 
     * maximumPoolSize 最大线程池大小----30 
     * keepAliveTime 线程池中超过corePoolSize数目的空闲线程最大存活时间----30+单位TimeUnit 
     * TimeUnit keepAliveTime时间单位----TimeUnit.MINUTES 
     * workQueue 阻塞队列----new ArrayBlockingQueue<Runnable>(10)====10容量的阻塞队列 
     * threadFactory 新建线程工厂----new CustomThreadFactory()====定制的线程工厂 
     * rejectedExecutionHandler 当提交任务数超过maxmumPoolSize+workQueue之和时, 
     *                          即当提交第41个任务时(前面线程都没有执行完,此测试方法中用sleep(100)), 
     *                                任务会交给RejectedExecutionHandler来处理 
     */  

newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      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());
    }

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
工作队列种的类
  1. 直接队列 SychronousQueue 无缓存
  2. 无界队列 LinkedBlockingQueue 无限缓存
  3. 有界队列 ArryayBlockingQueue 自定义队列的长度

线程池应该手动创建还是自动创建

手动创建更好,因为这样可以让我们更加明确线程池的运行规则,避免资源耗尽的风险。
◆让我们来看看自动创建线程池(也就是直接调用JDK封装好的
构造函数)可能带来哪些问题

线程池手动创建还是自动创建?

◆正确的创建线程池的方法
根据不同的业务场景,自己设置线程池参数,比如我们的内存有多大,我们想给线程取什么名字等等

线程池的数量?

◆CPU密集型(加密、计算hash等) : 最佳线程数为CPU核心数的1-2倍左右。

◆耗时IO型(读写数据库、文件、网络读写等) : 最佳线程数-般会大于cpu核心数很多倍,以VM线程监控显示繁忙情况为依据,保证线程空闲可以衔接上,参考Brain Goetz推荐的计算方法:

◆线程数=CPU核心数* ( 1+平均等待时间/平均工作时间)

根据实际压测进行统计。

增减线程的特点


  1. 通过设置corePoolSize和maximumPoolSize相同,就可以创建固定大小的线程池。
  2. 线程池希望保持较少的线程数,并且只有在负载变得很大时才增加它。
  3. 通过设置maximumPoolSize为很高的值,例如Integer.MAX_ VALUE ,可以允许线程池容纳任意数量的并发任务。
  4. 是只有在队列填满时才 创建多于corePoolSize的线程,所以如果你使用的是无界队列(例如LinkedBlockingQueue ), 那么线程数就不会超过corePoolSize。

阻塞队列分析

  1. ◆FixedThreadPool和SingleThreadExecutor的Queue是LinkedBlockingQueue?
  2. ◆CachedThreadPool使用的Queue是SynchronousQueue ?
  3. ◆ScheduledThreadPool来说 ,它使用的是延迟队列DelayedWorkQueue

WorkStealingPool 新加入的线程池

-子任务
-不加锁
-不保证执行顺序
-窃取

使用场景:

  1. 矩阵
  2. 递归

如何正确的停止线程池

  1. shutdown
    1. 并不是马上停止,禁止提交新的任务。有新提交会报异常。
  2. isShutdown()
    1. 是否完全停止已经触发停止状态 ?
  3. isTerminated()
    1. 判断是否完全停止?
  4. shutdownNow()
    1. 立刻终止
  5. awaitTermination(3L, TimeUnit.SECONDS)
    1. 判断一个时间是否执行王完毕?true /false

一图胜千言
在这里插入图片描述
在这里插入图片描述

总结

1、用ThreadPoolExecutor自定义线程池,看线程是的用途,如果任务量不大,可以用无界队列,如果任务量非常大,要用有界队列,防止OOM
2、如果任务量很大,还要求每个任务都处理成功,要对提交的任务进行阻塞提交,重写拒绝机制,改为阻塞提交。保证不抛弃一个任务
3、最大线程数一般设为2N+1最好,N是CPU核数
4、核心线程数,看应用,如果是任务,一天跑一次,设置为0,合适,因为跑完就停掉了,如果是常用线程池,看任务量,是保留一个核心还是几个核心线程数
5、如果要获取任务执行结果,用CompletionService,但是注意,获取任务的结果的要重新开一个线程获取,如果在主线程获取,就要等任务都提交后才获取,就会阻塞大量任务结果,队列过大OOM,所以最好异步开个线程获取结果

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值