并发相关知识总结(四)

7 篇文章 0 订阅

知识点(第八、九章)

1、CountDownLatch

基本的用法就是定义计数器N,并通过await方法阻塞线程,直到N变成0。和join最直观的使用区别就是不用手动的去挂起了,一两个线程可能并不会显得多方便,但是当线程数量较大时,区别还是很明显的。

2、CyclicBarrier

这个我也没用过,其字面意思就是可循环使用的屏障,让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

默认的构造方法为CyclicBarrier(int parties),参数表示屏障拦截的线程数量,各自线程调用await方法告诉CyclicBarrier我已经到达屏障,然后当前线程被阻塞。同时提供更高级的构造函数CyclicBarrier(int parties, Runnable barrierAction),用于在线程到达屏障时优先执行barrierAction。跟CountDownLatch的最大区别即CyclicBarrier可以重用。

3、Semaphore

Semaphore翻译过来就是信号量,用于控制同时访问特定资源的线程数量,通过acquire和release获取信号量(有点类似lock的底层实现),其构造方法也是一个整形的数字,代表可用的许可证书,用于控制并发数量。

4、Exchanger

在看这本书前,这个我甚至都没听过,它是一个用于线程间协作的工具类,用于进行线程间的数据交换,通过exchange方法执行线程之间的数据交换。

5、线程池

面试必问点,使用线程池有以下三个好处:

  • 降低资源消耗:通过重复利用已经创建的线程降低线程创建和销毁造成的消耗,这一点将在后面介绍,线程池是如何复用线程的;
  • 提高响应速度:当任务到达时,可以不用等到线程创建就能立即执行(这里没明白执行具体是指什么);
  • 提高线程的可管理性:实现线程的统一分配、调优和监控;

实现原理(书上原图):

可以看到有如下三个判断(参数前面已经介绍过):

1)核心线程(corePoolSize)是否已满,如果不是,则创建一个新的工作线程来执行任务,如果核心线程池中的线程都在执行任务,则进入下一个判断;

2)判断线程池的工作队列(runnableTaskQueue)是否已满,如果没有,则将任务入队列,如果满了,则进入下一个判断;

3)线程池判断线程池的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务,如果满了,则交给饱和策略(RejectedExecutionHandler)来处理这个任务;

因此ThreadPoolExecutor的executor方法包括以下四种情况,用图来表示如下:

这里猜想下前文中提高响应速度的意思,在执行execute方法时,通过线程池的调度,尽可能的避免全局锁,因为当线程数量大于corePoolSize时,任务加入等待队列,而这个步骤是不需要获取锁的,作者说的“执行”应该是指加入等待队列吧,通过避免锁而提高吞吐量,但实际上其线程中的业务并没有真正开始执行。这里贴一下核心的execute方法源码:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();

    int c = ctl.get();
    //1
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //2
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            //3
            reject(command);
        else if (workerCountOf(recheck) == 0)
            //4
            addWorker(null, false);
    }
    //5
    else if (!addWorker(command, false))
        //6
        reject(command);
}

1对应第一种情况,2对应第二种情况,3的作用是检查线程池状态,如果此时线程池不可用或非运行,则将这个加入的任务删掉,并交由拒绝Handler处理,其实仔细思考一下,这里为什么不将所有的任务都交由拒绝Handler处理,因为线程池的状态是可恢复的,此时要做的仅仅应该是拒绝新的任务,4发现没有worker,则补充一个worker,5对应第三种情况,6则对应第四种情况,这就是线程池的实现原理,线程的复用其实是worker的复用,线程池创建线程时,会将线程封装成工作线程worker,这里再贴一下worker的run源码:

public void run() {
    runWorker(this);
}
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
       //1
        while (task != null || (task = getTask()) != null) {
            w.lock();
           //2
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
               //3
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                 //4
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;       //5
    } finally {
        //6
        processWorkerExit(w, completedAbruptly);
    }
}

1进入循环,从getTask获取要执行的任务,直到返回null。这里达到了线程复用的效果,让线程处理多个任务。2保证了线程池在STOP状态下线程是中断的,非STOP状态下线程没有被中断。3调用了run方法,任务的真正执行即在这一步。执行前后提供了beforeExecute和afterExecute两个空方法,可以由子类实现。4中completedTasks统计worker执行了多少任务,最后累加进completedTaskCount变量,可以调用相应方法返回一些统计信息。5的变量completedAbruptly表示worker是否异常终止,执行到这里代表执行正常,后续的方法需要这个变量,6调用processWorkerExit结束。

这里涉及到一个关键的方法:getTask,这个方法负责从等待队列中获取任务,再贴一下源码:

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        //1
        // Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);
        //2
        // Are workers subject to culling?
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
       //3
        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

可以看到,通过一个无限的循环不停的执行以下三个步骤:

1、检查线程池状态,rs>=stop代表线程池为不可用的状态,empty即等待队列为空,这个判断的意思就是如果线程池为shutdown状态且等待队列不为空,则继续执行完等待队列里的任务,如果为不可用状态(STOP、TIDYING、TERMINATED)则不再执行剩余的任务。

2、这个判断绕了我很久,大致的意思就是三个判断:(1)空闲是否超时,超时则终止;(2)在运行过程中worker数量是否大于maxpoolsize(setMaxPoolSize方法导致最大线程数量在运行时变更);(3)等待队列为空或worker数量大于等于1,即在等待队列非空时,至少保留一个worker。这三个判断只要有一个成立都不能处理等待队列中的任务。

3、2不成立则调到该步骤,从等待队列中取任务(take为阻塞式,前面已经介绍过)。

再来看看前面finally块中的processWorkerExit方法:

private void processWorkerExit(Worker w, boolean completedAbruptly) {
   //1
    if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
        decrementWorkerCount();

  //2
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        completedTaskCount += w.completedTasks;
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }

   //3
    tryTerminate();

    int c = ctl.get();
    //4
    if (runStateLessThan(c, STOP)) {
        if (!completedAbruptly) {
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        addWorker(null, false);
    }
}

1判断worker是否被中断,如果是,则对workerCount的减一,如果没有的话,在getTask里面已经对wc做了减一操作;2计算已处理的worker数量,并且在集合workers中去除;3尝试终止线程池;4处理线程池还是RUNNING或SHUTDOWN状态时,如果worker是异常结束,那么会直接addWorker。如果allowCoreThreadTimeOut=true,并且等待队列有任务,至少保留一个worker;如果allowCoreThreadTimeOut=false,workerCount不少于corePoolSize。

线程池的关闭:可以通过shutdown或shutdownNow两种方法来关闭线程池,它们的原理都是遍历线程池中的工作线程,逐个调用interrupt方法,无法响应中断的任务可能永远无法终止,两种方法区别如下:

  • shutdown:不能再提交任务,已经提交的任务可继续运行;
  • shutdownNow:不能再提交任务,已经提交但未执行的任务不能运行,在运行的任务可继续运行,但会被中断,返回已经提交但未执行的任务。

线程池大小的配置:

实习的时候听讲座的人说过一句话印象特别深刻:大池小队列,小池无队列;即判断线程池中的任务特性,如果是IO密集型则用大池小队列,让它多去切换提高吞吐量,如果是CPU密集型,则使用小池无队列,尽量减少线程的切换导致执行效率降低。CPU密集型任务线程池大小一般配置为:N+1,IO密集型配置为2N,N为CPU核数,扩展下,Netty中的EventLoopGroup线程池的默认大小即为2N。

线程池的监控:

taskCount:线程池需要执行的任务数量;

completedTaskCount:线程池在运行过程中已完成的任务数量,小于或者等于taskCount;

largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过;

getPoolSize:线程池的线程数量,如果线程池不销毁,线程也不会销毁,因此这个大小只会递增;

getActiveCount:获取活动的线程数;

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值