在Java并发的内存模型中,理解为多个进程(线程)读取共享资源时存在竞争条件。在计算机中,同步器被设计用来协调进程(线程)之间的执行顺序。同步器的作用类似于登机安检人员,协调乘客有序通过。在Java中,同步器可以理解为根据自身状态协调线程执行顺序的对象。比如锁(Lock)、信号量(Semaphore)、屏障(CyclicBarrier)、阻塞队列(BlockingQueue)。这些同步器在功能设计上各不相同,但在内部实现上却有一些共同点。同步器同步器的设计一般包括几个方面:状态变量设计(同步器内部状态)、访问条件设置、状态更新、等待方式、通知策略。访问条件是控制线程是否可以执行(访问共享对象)的条件,它们往往与状态变量密切相关。通知策略是在线程释放锁状态后通知其他等待线程的一种方式。一般有以下几种情况通知所有等待的线程。通知1个随机的N个等待线程。通知1具体N个等待线程看下面例子,同步器publicclassLock{//状态变量isLockedprivatebooleanisLocked=false;publicsynchronizedvoidlock()throwsInterruptedException{//访问条件当isLocked=false时获取访问否则等待while(isLocked){//阻塞wait();}//状态更新线程获取访问权限isLocked=true;}publicsynchronizedvoidunlock(){//状态updatethreadreleasesaccesspermissionisLocked=false;//通知策略object.notify|object.notifyAllnotify();}}我们使用一个计数信号量来控制并发执行操作的数量。这里模拟了一个连接池。publicclassPoolSemaphore{//状态变量actives计数器privateintactives=0;privateintmax;publicPoolSemaphore(intmax){this.max=max;}publicsynchronizedvoidacquire()throwsInterruptedException{//当访问条件激活次数小于最大限制时,获取访问权限otherwisewaitwhile(this.actives==max)wait();//状态更新线程获得访问权限this.actives++;//通知策略object.notify|object.notifyAllthis.notify();}publicsynchronizedvoidrelease()throwsInterruptedException{//访问条件的激活次数不为0时,获取访问权限orwaitwhile(this.actives==0)wait();//状态更新线程获取访问权限this.actives--;//通知strategyobject.notify|object.notifyAllthis.notify();}}在原子指令同步器的设计中,最重要的操作逻辑是“如果满足条件,则更新状态变量来标记线程获取或释放访问权”,以及这部歌剧化应该是原子的。例如,test-and-set计算机原子指令表示如果满足条件则设置一个新值。functionLock(boolean*lock){while(test_and_set(lock)==1);}另外还有很多原子指令fetch-and-addcompare-and-swap,注意这些指令需要硬件支持才能生效。在同步操作中,使用计算机原子指令可以避免锁,提高效率。java中没有test-and-set支持,但是java.util.concurrent.atomic为我们提供了很多原子类API,支持getAndSet和compareAndSet操作。看下面的例子,主要区别是等待方式不同,上面是通过wait()进行阻塞等待,下面是非阻塞循环。publicclassLock{//状态变量isLockedprivateAtomicBooleanisLocked=newAtomicBoolean(false);publicvoidlock()throwsInterruptedException{//等待模式变为自旋等待while(!isLocked.compareAndSet(false,true));//状态更新线程获得访问权限isLocked。set(true);}publicsynchronizedvoidunlock(){//状态更新线程释放访问权限isLocked.set(false);}}关于阻塞的解释阻塞是指进程或线程状态恢复后需要dump才能继续执行。这种操作成本高,重量大,线程基于进程是比较轻量级的。线程阻塞在不同的编程平台上的实现方式不同。比如Java是基于JVM的,所以是由JVM来实现的。在《Java Concurrency in Practice》中,作者提到竞争同步可能需要操作系统活动,这会增加成本。没有获得锁的线程在竞争锁时必须阻塞。JVM可以通过自旋等待(反复尝试获取锁直到成功)或者操作系统挂起被阻塞的线程来实现阻塞。哪个更有效取决于上下文切换开销与锁可用之前的时间。对于短暂的等待,最好使用自旋等待;对于更长的等待时间,最好使用暂停。一些JVM根据过去等待时间的分析数据自适应地在两者之间进行选择,但大多数JVM只是挂起等待锁的线程。从上面可以看出JVM实现阻塞有两种方式:自旋等待。简单理解就是不挂起执行,循环等待执行,适合短期场景。线程被操作系统挂起。JVM中通过-XX:+UseSpinning开启自旋等待,-XX:PreBlockSpi=10指定最大自旋次数。AQSAQS是AbstractQueuedSynchronizer的缩写。本节仅对AQS进行简要阐述,并不全面。java.util.concurrent包中的ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier等都是基于AQS同步器实现的。状态变量用intstate表示,状态的获取和更新是通过以下API操作进行的。intgetState()voidsetState(intnewState)booleancompareAndSetState(intexpect,intupdate)状态值在不同的API中有不同的含义。比如ReentrantLock表示持有锁的线程获取锁的次数,Semaphore表示剩余许可数。关于等待方式和通知策略的设计,AQS通过维护一个先进先出的同步队列(Syncqueue)来进行同步管理。当多个线程竞争共享资源时,它们会被阻塞并入队。线程阻塞和唤醒是通过LockSupport.park/unparkAPI实现的。它定义了两种资源共享方法。Exclusive(独占,只有一个线程可以执行,比如ReentrantLock)Share(共享,多个线程可以同时执行,比如Semaphore/CountDownLatch)每个节点包含waitStatus(节点状态),prev(前驱),next(successor)、thread(入队时的线程)、nextWaiter(条件队列的后继节点)和waitStatus有如下值。CANCELLED(1)表示线程已被取消。当发生超时或中断时,节点状态变为取消状态,此后状态不会改变。SIGNAL(-1)表示后继节点等待前驱的唤醒。当后继节点加入队列时,前驱状态将更新为SIGNAL。CONDITION(-2)表示线程正在条件队列中等待。当其他线程调用Condition.signal()方法时,处于CONDITION状态的节点会从Condition队列转移到Sync队列,等待获取锁。PROPAGATE(-3)在共享模式下,当前节点释放后,保证后继节点得到有效通知。(0)节点加入队列时的默认状态。AQS尝试通过几个关键的APItryAcquire(int)独占方式来获取资源。成功返回真,否则返回假。tryRelease(int)独占模式下,尝试释放资源,成功返回true,否则返回false。tryAcquireShared(int)在共享模式下,尝试获取资源。失败返回负数,成功返回零和正数,并指示剩余资源。tryReleaseShared(int)在共享模式下,尝试释放资源,如果释放后允许后续等待节点唤醒,则返回true,否则返回false。isHeldExclusively()确定线程是否独占持有资源。acquire(intarg)publicfinalvoidacquire(intarg){if(//尝试直接获取资源,成功则直接返回!tryAcquire(arg)&&//线程阻塞在等待获取资源的同步队列中。等待中中断时process,returntrue,否则为falseacquireQueued(//将线程标记为独占并加入同步队列的尾部。addWaiter(Node.EXCLUSIVE),arg))selfInterrupt();}release(intarg)publicfinalbooleanrelease(intarg){//尝试释放资源if(tryRelease(arg)){Nodeh=head;if(h!=null&&h.waitStatus!=0)//唤醒下一个线程(后继节点)unparkSuccessor(h);returntrue;}returnfalse;}privatevoidunparkSuccessor(Nodenode){...Nodes=node.next;//寻找后继节点if(s==null||s.waitStatus>0){//没有后继节点或节点被取消s=null;//为(Nodet=tail;t!=null&&t!=node;tt=t.prev)if(t.waitStatus<=0)s=t;}if(s!=null)LockSupport.unpark寻找有效等待节点(s.thread);//wake}小结本文记录一些常用的特性并发编程中的同步器设计。并简单介绍了Java中的AQS。
