java对共享变量的操作和管理使用的是MESAmonitor模型。下图是Java基于AQS实现的MESA监控模型:上图中包含三个知识点:MESA监控模型封装了共享变量和对共享变量的操作。要进入监视器,线程必须获取锁。如果锁失败,则进入入口等待队列阻塞等待。如果线程获得了锁,它就进入了监视器。但是在进入monitor的时候,不一定能马上操作共享变量,而是要看条件变量是否满足。如果没有,只能进入条件变量等待队列阻塞等待。在条件变量等待队列中,如果被其他线程唤醒,可能无法立即操作共享变量,而是需要去入口等待队列重新排队,等待获取锁。本文主要讲解monitor模型中的条件变量等待队列。1官方示例先来看官方示例代码:,数数;publicvoidput(Objectx)throwsInterruptedException{lock.lock();try{while(count==items.length)notFull.await();items[putptr]=x;if(++putptr==items.length)putptr=0;++count;notEmpty.signal();}finally{lock.unlock();}}publicObjecttake()throwsInterruptedException{lock.lock();try{while(count==0)notEmpty.await();Objectx=items[takeptr];if(++takeptr==items.length)takeptr=0;--count;notFull.signal();returnx;}finally{lock.unlock();}}}这段代码两个条件变量分别定义了notFull和notEmpty,如下:如果items数组已满,不满足notFull变量,线程需要进入notFull条件等待队列等待。当take方法取一个数组元素时满足notFull条件,notFull条件等待队列中的等待线程被唤醒。如果items数组为空,不满足notEmpty变量,线程需要进入notEmpty条件等待队列等待。当put方法添加一个数组元素时,满足notEmpty条件,notEmpty条件等待队列中的等待线程被唤醒。条件变量绑定在Lock上,示例代码使用ReentrantLock。在执行await和signal方法时,必须先获取锁。2原理介绍JavaAQS的条件变量等待队列是基于Condition和ConditionObject接口实现的。URM类图如下:Condition接口主要定义了以下三个方法:await:进入条件等待队列signal:唤醒条件等待队列中的元素signalAll:唤醒条件等待队列中的所有元素3await条件等待队列与入口等待队列有两点不同:虽然两者共享Node类,但是条件等待队列是单向队列,入口等待队列是双向队列,条件队列的条目中下一个节点的引用是nextWaiter,条目等待队列中下一个节点的引用是next。条件等待队列中元素的waitStatus必须为-2。await方法的流程如下图所示:3.1输入条件等待队列对应的方法addConditionWaiter,有三种情况:如果队列为空,则新建节点,如下图:如果队列不为空,则最后一个元素的waitStatus为-2,如下图:队列不为空,最后一个元素的waitStatus不为-2,如下图:如你所愿看,在这种情况下,waitStatus不是-2的元素将从队列的第一个元素中检查并从队列中移除。3.2释放锁AQS的并发锁是基于状态变量实现的。线程进入条件等待队列后,需要释放锁,即状态会变为0,释放操作会唤醒进入等待队列中的线程。对应fullyRelease方法,返回值为状态值savedState减去释放锁。3.3阻塞和等待锁释放后,线程被阻塞,自旋等待被唤醒。3.4唤醒后当前线程被唤醒后主要有四个动作:转移到入口等待队列,将waitStatus改为0。waitStatus等于0表示中间状态,当前节点后面的节点已经唤醒,但是当前节点线程尚未执行完毕。重新获取锁。如果获取成功,则当前线程成为入口等待队列的头节点,interruptMode设置为1。如果当前节点在条件等待队列中有后继节点,则移除其中waitStatus!=-2的节点条件等待队列,即状态被取消的节点。如果interruptMode不等于0,则处理中断。3.5Adetail上面提到的interruptMode有3个取值:0:不被中断-1:中断后抛出InterruptedException。在这种情况下,当前线程在收到信号之前被阻塞和中断。1:重新进入中断状态,这种情况说明当前线程在收到信号后被阻塞中断。3.6ExtendedAQS还提供了其他几种await方法,如下:awaitUninterruptibly:不需要处理中断。awaitNanos:自旋等待唤醒过程中有超时限制,超时后会转入入口等待队列。awaitUntil:自旋等待唤醒过程中有一个截止时间,时间一到就转入入口等待队列。4信号唤醒条件等待队列中的元素,首先判断当前线程是否持有独占锁,如果没有则抛出异常。唤醒条件队列中的元素会从第一个元素开始,也就是firstWaiter。根据firstWaiter的waitStatus是否为-2,有两种情况。4.1waitStatus==-2条件队列第一个节点进入入口等待队列等待获取锁,如下图:这里需要注意两点:如果入口等待队列中尾节点的waitStatus小于等于0,则加入后需要将firstWaiter老尾节点设置为-1(表示后面的节点等待当前节点唤醒),如下图:如果waitStatus重置失败,解停节点firstWaiter。如果入口等待队列中尾节点的waitStatus大于0,则unpark该节点firstWaiter。4.2waitStatus!=-2如果firstWaiter的waitStatus不等于-2,则查找firstWaiter的nextWaiter,直到找到一个waitStatus等于-2的节点,然后将这个节点加入到入口等待队列的尾部,如下图所示:4.3waitStatus修改以上无论哪种情况,waitStatus在进入入口等待队列前都必须被CAS修改为0。5signalAll了解了signal的逻辑之后,signalAll的逻辑就很好理解了。首先判断当前线程是否持有独占锁,如果没有则抛出异常。将条件等待队列中的所有节点依次加入到入口等待队列中。如下图:6用例6.1示例代码JavaConcurrentPackagesAQS中使用Condition的类很多,如下图所示:这里我们以CyclicBarrier为例进行说明。CyclicBarrier就是让一组线程一起等待对方到达一个barrier点。从Cyclic可以看出Barrier是可以回收的,即线程释放后可以继续使用。看下面的示例代码:publicstaticvoidmain(String[]args){CyclicBarriercyclicBarrier=newCyclicBarrier(2,()->{System.out.println("Threadinthebarrierexecutioniscomplete");});ExecutorServiceexecutorService=Executors。newFixedThreadPool(2);executorService.submit(()->{try{System.out.println("线程1:"+Thread.currentThread().getName());cyclicBarrier.await();}catch(Exceptione){e.printStackTrace();}});executorService.submit(()->{try{System.out.println("线程2:"+Thread.currentThread().getName());cyclicBarrier.await();}catch(Exceptione){e.printStackTrace();}});executorService.shutdown();}执行结果:thread1:pool-1-thread-1thread2:pool-1-thread-2threadsinthefence执行完成6.2原理CyclicBarrier初始化时,会指定线程数count。每个线程执行完逻辑后,调用CyclicBarrier的await方法。该方法先将count减1,然后调用Condition的await让当前线程进入condition等待队列。当最后一个线程将count减1,count等于0时,会调用Condition的signalAll方法唤醒所有线程。7总结Java的监控模型采用MESA模型。在基于AQS的MESA模型中,入口等待队列使用双向队列实现,并发锁使用可变状态实现,条件等待队列使用Condition实现。在AQS的实现中,术语同步队列用于表示双向队列。本文使用入口等待队列来描述,是为了更好的配合监控模型进行讲解。AQS的Condition中,通过await方法将当前线程放入条件等待队列阻塞等待,notify用于唤醒条件等待队列中的线程。线程被唤醒后不能立即执行,而是进入入口等待队列等待获取锁。
