从ArrayBlockingQueue源码的角度来分析Condition的await和signal方法的作用及原理

背景

先看一个现象,我们在创建线程池执行线程的时候,经常会写到如下的代码:

//new一个线程池对象
ThreadPoolExecutor executor=new ThreadPoolExecutor(2, 4, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread t=new Thread(r);
                        t.setName("abcdefg");
                        return t;
                    }
                },new ThreadPoolExecutor.AbortPolicy());
//执行一个runnable对象
        executor.execute(()->{
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
            }
        });

我们关注下ThreadPoolExecutor.execute这个方法之后,会发现,他这里会将我们传给他的这个Runnable对象包装成一个Worker对象,但其实最终执行的当然还是我们传进去的这个Runnable的run()方法,就像下面这样:

//线程池执行execute方法的时候会调用这个addWorker方法
private boolean addWorker(Runnable firstTask, boolean core) {
        //这里主要是做一些判断,然后把表示当前正在运行的线程数的计数器加1
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
//新建了一个worker
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    //启动这个新创建的线程
                  //t.start()->worker.run()->runWorker(this)->我们自己写的run方法的内容
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }
//这个runWorker方法是线程池在启动一个线程的时候所调用的
final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        //最终执行的还是我们自己传递进去的run方法的内容
                        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;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

看了上面的几段源码之后,我们大概知道了线程池是怎么创建线程去工作的了。。。

但我们接下来要说的就和Condition的await方法signal方法关系密切了。。

我们知道线程池中使用了一个BlockingQueue(阻塞队列)来解决了当新添加进来Runnable之时核心线程满了的问题,这个时候就把新添加进来的这个Runnable对象添加到BlockingQueue中记录起来。

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
//不能再添加核心线程了,那么放到阻塞队列中去保存起来
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            //重新检查一下,如果这个时候线程池不再是运行中状态了,就把这个任务从队列中删掉
            if (! isRunning(recheck) && remove(command))
                //如果删除成功,就执行拒绝策略
                reject(command);    
            //两种情况:1.线程池还处于运行中状态 2.线程池不运行了,但是这个任务删除失败了(似乎一般不会有这种删除失败的情况。。)
            else if (workerCountOf(recheck) == 0)
                //这个时候,如果正巧当前正在工作的线程数为0,那么就意味着没有线程处理这个runnable任务了,那么就创建一个非核心线程,去执行阻塞队列中的任务
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

而addWorker方法中,会开辟一个新的、任务内容为空的线程,再调用getTask()方法去BlockingQueue中去获取一个Runnable对象去执行,而这些Runnable对象就是之前因为核心线程满了而被offer进去的。其中getTask方法是这样执行的:

while (task != null || (task = getTask()) != null) 

这个方法会阻塞:

private Runnable getTask() {
        //是否超时,指的是获取Runnable对象这个行为
        boolean timedOut = false; // Did the last poll() time out?

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

            // Check if queue empty only if necessary.
            //如果线程池状态是结束等状态或者阻塞队列为空,则返回null
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                //如果阻塞队列是空的,那么工作线程数减1
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // 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;
            }

            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

别的先不用管,主要看这个方法:ArrayBlockingQueue.take()

(因为我这里用的是ArrayBlockingQueue来举个例子,不用纠结别的类型的BlockingQueue)

 public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

我们看到如果count==0的话,说明阻塞队列中没有runnable对象,那么就会调用await方法阻塞在那里,一直到队列中有元素为止。

此处,终于到了我们接下来要研究的Condition的await和signal方法了:

我们针对这两个问题做一个解答:

1.await方法为什么会阻塞?

2.什么时候接触阻塞状态?

我们可以将这个Condition对象想象成为一个防错机制,或者说是一个检查机制。首先我们通过

final ReentrantLock lock = this.lock;

 获得了锁对象,然后调用lock方法(这里先不管是否这把锁能够被打断)进入锁的内部,这一步只是为了防止在这把锁释放之前,运行的代码段是必须是线程安全的。而这时,还需要第二层判断那就是BlockingQueue里面必须要有可以取出来的元素,也就是Runnable对象,如果没有就一直等在那里直到等到有一个runnable对象。所以这里调用了

notEmpty.await();

 方法(等待元素非空)。而相对应的,总有另一个地方需要去通知这些被阻塞的线程,告诉他们:你们可以再去做新的判断非空的尝试了。所以在ArrayBlockingQueue.enqueue方法中我们看到了

private void enqueue(E e) {
        // assert lock.isHeldByCurrentThread();
        // assert lock.getHoldCount() == 1;
        // assert items[putIndex] == null;
        final Object[] items = this.items;
        items[putIndex] = e;
        if (++putIndex == items.length) putIndex = 0;
        count++;
        //最后,有一成功添加了一个Runnable对象,所以此时大概率会使由于元素为空而被阻塞的线程可以解除阻塞,因此signal通知他们解除他们的阻塞状态,做下一次的元素是非为空的判断了
        notEmpty.signal();
    }

至此,上面提出的两个问题都可以找到结论了。

最后贴一段《java核心技术》一书中对于Condition对象的await和signal方法的描述:

通常, 线程进人临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对
象来管理那些已经获得了一个锁但是却不能做有用工作的线程。在这一节里, 我们介绍
Java 库中条件对象的实现。(由于历史的原因, 条件对象经常被称为条件变量( conditional
variable )。)

。。。。这里书中给出了一个银行账户之间转账的例子。。。。
其中转账操作调用了transfer方法,而sufficientFunds是一个条件对象,代表着“有充足金额进行转账”的条件

如果 transfer 方法发现余额不足,它调用
sufficientFunds.await();
当前线程现在被阻塞了,并放弃了锁。我们希望这样可以使得另一个线程可以进行增加
账户余额的操作。

等待获得锁的线程和调用 await 方法的线程存在本质上的不同。一旦一个线程调用 await
方法, 它进人该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞
状态,直到另一个线程调用同一条件上的 signalAll 方法时为止。

这一调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集当中移出时,
它们再次成为可运行的,调度器将再次激活它们。同时, 它们将试图重新进人该对象。一旦
锁成为可用的,它们中的某个将从 await 调用返回, 获得该锁并从被阻塞的地方继续执行。
此时, 线程应该再次测试该条件。 由于无法确保该条件被满足—signalAll 方法仅仅是
通知正在等待的线程:此时有可能已经满足条件, 值得再次去检测该条件。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qq_34116044

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值