zoukankan      html  css  js  c++  java
  • 【java多线程】jdk的线程池

    一、JDK的线程池的核心参数

    • int corePoolSize :线程池中的核心线程数
    • int maximumPoolSize :线程池中的最大线程数
    • long keepAliveTime :除核心线程数以外,其他空闲线程最大存活的时间
    • TimeUnit unit :存活时间的单位
    • BlockingQueue<Runnable> workQueue :存放任务的队列
    • ThreadFactory threadFactory:线程池生产线程的工厂类
    • RejectedExecutionHandler handler:当队列已满线程池拒绝任务的策略
    • boolean allowCoreThreadTimeOut :是否允许核心线程超时(这个为true,当核心线程空闲到keepAliveTime后,会被回收。  为false时,核心线程空闲会一直阻塞在队列上等待新任务)

    二、JDK的线程池的状态

    运行状态

    状态描述

    线城池状态的表示

    RUNNING

    能接受新提交的任务,并且也能处理阻塞队列中的任务;

    RUNNING    = -1 << 29

    SHUTDOWN

    关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。

     
    SHUTDOWN   =  0 << 29

    STOP

    不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。

     
    STOP       =  1 << 29

    TIDYING

    如果所有的任务都已终止了,workerCount (有效线程数) 为0

     
    TIDYING    =  2 << 29

    TERMINATED

    在terminated() 方法执行完后进入该状态

     
    TERMINATED =  3 << 29

    RUNNING      -1<<29==>11100000 00000000 00000000 00000000 

    SHUTDOWN  0<<29==>00000000 00000000 00000000 00000000

    STOP           1<<29 ==>00100000  00000000  00000000  00000000

    TIDYING       2<<29 ==>01000000   00000000   00000000   00000000 

    TERMINATED 3<<29 ==>01100000 00000000 00000000 00000000

    CAPACITY (1<<29)-1 ==>00011111 11111111 11111111 11111111

    //这个代表线程的控制器:int类型为32位。 其中高3位表示线程池的状态,低29位表示线城池中的线程数量。

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    View Code

    //计算线城池中的数量:ctl & CAPACITY   也就意味着:线程池中线程的最大

    private static int workerCountOf(int c) { return c & CAPACITY; }

    private static int runStateOf(int c)     { return c & ~CAPACITY; } //计算当前运行状态
    private static int workerCountOf(int c)  { return c & CAPACITY; }  //计算当前线程数量
    private static int ctlOf(int rs, int wc) { return rs | wc; }   //通过状态和线程数生成ctl
    View Code

    三、JDK的线程池的调度机制

    任务调度是线程池的主要入口,当用户提交了一个任务,接下来这个任务将如何执行都是由这个阶段决定的。了解这部分就相当于了解了线程池的核心运行机制。

    首先所有任务的调度都是由execute方法完成的,这部分完成的工作是,检查现在线程池的运行状态、运行线程数、运行策略,决定接下来执行的流程,是直接申请线程执行或是缓冲到队列中执行亦或是直接拒绝该任务,其执行过程如下:

    1. 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。

    2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。

    3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。

    4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。

    5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

     四、JDK的线程池的任务缓存(阻塞队列成员)

    名称

    描述

    ArrayBlockingQueue

    一个用数组实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。支持公平锁和非公平锁。

    LinkedBlockingQueue

    一个由链表结构组成的有界队列,此队列按照先进先出(FIFO)的原则对元素进行排序。此队列的默认长度为Integer.MAX_VALUE,所以默认创建的该队列有容量危险。

    PriorityBlockingQueue

    一个支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()方法来指定元素排序规则,不能保证同优先级元素的顺序。

    DelayQueue

    一个实现PriorityBlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延时期满后才能从队列中获取元素。

    SynchronousQueue

    一个不存储元素的阻塞队列,每一个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁。SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时)创建新的线程,如果有空闲线程则会重复使用,线程空闲了60秒后会被回收。

    LinkedTransferQueue

    一个由链表结构组成的无界阻塞队列,相当于其它队列,LinkedTransferQueue队列多了transfer和tryTransfer方法。

    LinkedBlockingDeque

    一个由链表结构组成的双向阻塞队列。队列头部和尾部都可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半。

    五、JDK的线程池的拒绝策略

    接口:java.util.concurrent.RejectedExecutionHandler

    用户也可以实现该接口,自定义拒绝策略。

    名称

    描述

    ThreadPoolExecutor.AbortPolicy

    丢弃任务并抛出RejectedExecutionException异常。 这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。

    ThreadPoolExecutor.DiscardPolicy

    丢弃任务,但是不抛出异常。 使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。

    ThreadPoolExecutor.DiscardOldestPolicy

    丢弃队列最前面的任务,然后重新提交被拒绝的任务。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。

    ThreadPoolExecutor.CallerRunsPolicy

    由调用线程(提交任务的线程)处理该任务。这种情况是需要让所有任务都执行完毕,那么就适合大量计算的任务类型去执行,多线程仅仅是增大吞吐量的手段,最终必须要让每个任务都执行完毕。

    六、线程池中woker线程的管理

    线程池为了掌握线程的状态维护线程的生命周期,设计了线程池内的工作线程Worker。我们来看一下它的部分代码:

    private final class Worker
            extends AbstractQueuedSynchronizer
            implements Runnable
        {
            /**
             * This class will never be serialized, but we provide a
             * serialVersionUID to suppress a javac warning.
             */
            private static final long serialVersionUID = 6138294804551838833L;
    
            /** Thread this worker is running in.  Null if factory fails. */
            final Thread thread;
            /** Initial task to run.  Possibly null. */
            Runnable firstTask;
            /** Per-thread task counter */
            volatile long completedTasks;
    
            /**
             * Creates with given first task and thread from ThreadFactory.
             * @param firstTask the first task (null if none)
             */
            Worker(Runnable firstTask) {
                setState(-1); // inhibit interrupts until runWorker
                this.firstTask = firstTask;
                this.thread = getThreadFactory().newThread(this);
            }
    
            /** Delegates main run loop to outer runWorker  */
            public void run() {
                runWorker(this);
            }
    }
    View Code

    Worker这个工作线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask。thread是在调用构造方法时通过ThreadFactory来创建的线程,可以用来执行任务;firstTask用它来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况,如果这个值是null,那么也就是需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。

    Worker执行任务的模型如下图所示:

    线程池需要管理线程的生命周期,需要在线程长时间不运行的时候进行回收。线程池使用一张hashset表去持有线程的引用,这样可以通过添加移除引用这样的操作来控制线程的生命周期。这个时候重要的就是如何判断线程是否正在运行。

    Worker是通过继承AQS 同步管理器,使用其独占锁特性来实现的这个功能。没有使用可重入锁ReentrantLock,而是使用独占锁AQS。为的就是利用AQS不可重入的特性去反应线程现在的执行状态。

    1. lock方法一旦获取了独占锁,表示当前线程正在执行任务中;

    2. 如果正在执行任务,则不应该中断线程;

    3. 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断;

    4. 线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。

     Worker线程工作机制

    1、woker的工作流程

    • woker线程会循环从阻塞队列中获取任务。
    • 当线程数大于核心线程,则woker从队列获取任务是按超时方式获取:超时时间为线程允许存活的时间(超时后线程被回收)。
    • 当线程数小于核心线程数,则woker按永久阻塞的方式获取任务
     

    2、线程被回收

    • 当woker线程的锁状态是无锁的时候,才允许回收。woker线程回收自己,run方法执行结束,从线程池的hashset表中移除。
    • 线程被回收分两种情况:突然被回收(线程任务执行抛异常)和正常回收(1、队列中无任务,线程存活时间到期   2、线程被中断)。
    • woker突然回收,且线程池状态为运行状态:当前线程被回收,则会重新向线程池中添加一个新线程。
    • woker正常回收,存活时间到达或线程被中断,且线程池状态为运行状态:会判断当前线程池中的线程数量是否小于核心线程数。如果小于则重新向线程池中新增一个worker
     

    七、调度线程池

    八、线程池中线程数的大小计算方式

    线程池核心的问题就在于:线程池的参数并不好配置,线程池最关键的构造参数在于corePoolSize、maximumPoolSize,workQueue三个参数的设置,它们最大程度的决定了线程池的任务分配和线程分配策略。然而这三个参数并不好配,一方面线程池的运行机制不是很好理解,配置合理需要强依赖开发人员的个人经验和知识,另一方面,线程池执行的情况和任务类型相关性较大,IO密集型和CPU密集型的任务运行起来的情况差异非常大,这导致业界并没有一些成熟的经验策略帮助开发人员参考,下面罗列了几个业界线程池参数配置方案:

     

    方案

    问题

    1

    cup的个数(Ncpu)=当前机器的cpu核数(number of CPUs

    cup的利用率​(Rcpu)=目标cpu的利用率(0<= 利用率<=1)

    cpu的等待时间和计算时间百分比(W/C)=cpu的等待时间/cpu的计算时间

     

    保持处理器的理想利用率的最佳池大小是

    Nthreads=Ncpu*Rcpu*(1+W/C)

     

    出自《Java并发编程实践》

    该方案偏理论化。首先,线程计算的时间和等待的时间要如何确定呢?这个在实际开发中很难得到确切的值。另外计算出来的线程个数逼近线程实体的个数,Java线程池可以利用线程切换的方式最大程度利用CPU核数,这样计算出来的结果是非常偏离业务场景的

    2

    核心线程数=2*cpu的个数

    最大线程数=25*cpu的个数

    没有考虑应用中往往使用多个线程池的情况,统一的配置明显不符合多样的业务场景。

    3

    核心线程数=Tps*time

    最大线程数=tps*time*(1.7-2)

    这种计算方式,考虑到了业务场景,但是该模型是在假定流量平均分布得出的。业务场景的流量往往是随机的,这样不符合真实情况。

     调研了以上业界方案后,我们并没有得出通用的线程池计算方式,这样就导致了下面两个问题:

    1. 程序开发期难以敲定合适的线程池参数配置;

    2. 程序运行期难以更改线程池参数,需要重新修改程序再重新上线,投入成本巨大;

    九、二进制的原码,反码,补码是什么意思

    什么叫补码呢?这得从原码,反码说起。 

    原码:一个整数,按照绝对值大小转换成的二进制数,称为原码。

    比如 00000000 00000000 00000000 00000101 是 5的 原码。

    反码:将二进制数按位取反,所得的新二进制数称为原二进制数的反码。

    取反操作指:原为1,得0;原为0,得1。(1变0; 0变1)

    比如:将00000000 00000000 00000000 00000101每一位取反,得11111111 11111111 11111111 11111010。

    称:11111111 11111111 11111111 11111010 是 00000000 00000000 00000000 00000101 的反码。

    反码是相互的,所以也可称:

    11111111 11111111 11111111 11111010 和 00000000 00000000 00000000 00000101 互为反码。

     

    补码:反码加1称为补码。

    也就是说,要得到一个数的补码,先得到反码,然后将反码加上1,所得数称为补码。

    比如:00000000 00000000 00000000 00000101 的反码是:11111111 11111111 11111111 11111010。

    那么,补码为:

    11111111 11111111 11111111 11111010 + 1 = 11111111 11111111 11111111 11111011


    所以,-5 在计算机中表达为:11111111 11111111 11111111 11111011。转换为十六进制:0xFFFFFFFB。


    再举一例,我们来看整数-1在计算机中如何表示。

    假设这也是一个int类型,那么:


    1、先取1的原码:00000000 00000000 00000000 00000001

    2、得反码: 11111111 11111111 11111111 11111110

    3、得补码: 11111111 11111111 11111111 11111111

  • 相关阅读:
    hdu1828线段树(两次扫描+离散化)
    hdu1542线段树(扫描线+离散化)
    二分思想
    hdu2871线段树区间更新
    git初体验
    python笔记-模块
    python笔记-动态类型
    AWS上创建EKS(K8S)集群
    Akamai CDN刷新(通过Akamai cli 自动刷新)
    创建Akamai cdn api授权
  • 原文地址:https://www.cnblogs.com/shangxiaofei/p/12594822.html
Copyright © 2011-2022 走看看