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

  • 相关阅读:
    HDU 4278 Faulty Odometer 8进制转10进制
    hdu 4740 The Donkey of Gui Zhou bfs
    hdu 4739 Zhuge Liang's Mines 随机化
    hdu 4738 Caocao's Bridges tarjan
    Codeforces Gym 100187M M. Heaviside Function two pointer
    codeforces Gym 100187L L. Ministry of Truth 水题
    Codeforces Gym 100187K K. Perpetuum Mobile 构造
    codeforces Gym 100187J J. Deck Shuffling dfs
    codeforces Gym 100187H H. Mysterious Photos 水题
    windows服务名称不是单个单词的如何启动?
  • 原文地址:https://www.cnblogs.com/shangxiaofei/p/12594822.html
Copyright © 2011-2022 走看看