??xml version="1.0" encoding="utf-8" standalone="yes"?>
对concurrent包的学习(fn)打算先从Lock的实现开始,因而自然而然的就端v?jin)AbstractQueuedSynchronizerQ然而要Lq个cȝ源码q不是那么容易,因而我开始问自己一个问题:(x)如果自己要去实现q个一个Lock对象Q应该如何实现呢Q?br />
要实现Lock对象Q首先理解什么是锁?我自׃~程角度单的理解Q所谓锁对象Q互斥锁Q就是它能保证一ơ只有一个线E能q入它保护的临界区,如果有一个线E已l拿到锁对象Q那么其他对象必让权等待,而在该线E退?gu)个?f)界区旉要唤醒等待列表中的其他线E。更学术一些,《计机操作pȝ?/a>中对同步机制准则的归UIP50Q:(x)
说了(jin)那么多,其实对互斥锁很简单,只需要一个标CQ如果该标记位ؓ(f)0Q表C没有被占用Q因而直接获得锁Q然后把该标C|ؓ(f)1Q此时其他线E发现该标记位已l是1Q因而需要等待。这里对q个标记位的比较q设值必L原子操作Q而在JDK5以后提供的atomic包里的工L(fng)可以很方便的提供q个原子操作。然而上面的四个准则应该漏了(jin)一点,即释N的线E(q程Q和得到锁的U程Q进E)(j)应该是同一个,像一把钥匙对应一把锁Q理想的Q,所以一个非常简单的Lockcd以这么实玎ͼ(x)
一个简单的试Ҏ(gu)Q?br />
然而这个SpinLock其实q不需要stateq个字段Q因为owner的赋g否也是一U状态,因而可以用它作ZU互斥状态:(x)
q在操作pȝ中被定义为整形信号量Q然而整形信号量如果没拿到锁?x)一直处?#8220;忙等”状态(没有遵@有限{待和让权等待的准则Q,因而这U锁也叫Spin LockQ在短暂的等待中它可以提升性能Q因为可以减线E的切换Qconcurrent包中的Atomic大部分都采用q种机制实现Q然而如果需要长旉的等待,“忙等”?x)占用不必要的CPU旉Q从而性能?x)变的很差,q个时候就需要将没有拿到锁的U程攑ֈ{待列表中,q种方式在操作系l中也叫记录型信号量Q它遵@?jin)让权等待准则(当前没有实现有限{待准则Q。在JDK6以后提供?jin)LockSupport.park()/LockSupport.unpark()操作Q可以将当前U程攑օ一个等待列表或一个线E从q个{待列表中唤醒。然而这个park/unpark的等待列表是一个全局的等待列表,在unpartk的时候还是需要提供需要唤醒的Thread对象Q因而我们需要维护自q{待列表Q但是如果我们可以用JDK提供的工L(fng)ConcurrentLinkedQueueQ就非常Ҏ(gu)实现Q如LockSupport文档中给出来?a >代码事例Q?br />
在该代码事例中,有一个线E等待队列和锁标记字D,每次调用lock时先当前线E放入这个等待队列中Q然后拿出队列头U程对象Q如果该U程对象正好是当前线E,q且成功 使用CAS方式讄locked字段Q这里需要两个同时满I因ؓ(f)可能出现一个线E已l从队列中移除了(jin)但还没有unlockQ此时另一个线E调用lockҎ(gu)Q此旉列头的线E就是第二个U程Q然而由于第一个线E还没有unlock或者正在unlockQ因而需要用CAS原子操作来判断是否要parkQ,表示该线E竞争成功,获得锁,否则当前线EparkQ这里之所以要攑֜ while循环中,因ؓ(f)park操作可能无理p?spuriously)Q如文档中给出的描述Q?br />
我在实现自己的类时就被这?#8220;无理p?#8221;坑了(jin)好久。对于已l获得锁的线E,该U程从等待队列中U除Q这里由于ConcurrentLinkedQueue是线E安全的Q因而能保证每次都是队列头的U程得到锁,因而在得到锁匙队列头U除。unlock逻辑比较单,只需要将locked字段打开Q设|ؓ(f)falseQ,唤醒QunparkQ队列头的线E即可,然后该线E会(x)l箋在lockҎ(gu)的while循环中l竞争unlocked字段Qƈ它自己从线E队列中U除表示获得锁成功。当然安全v见,最好在unlock中加入一些验证逻辑Q如解锁的线E和加锁的线E需要相同?br />
单v见,队列头是一个v点的placeholderQ每个调用lock的线E都先将自己竞争攑օq个队列,每个队列头后一个线E(NodeQ即是获得锁的线E,所以我们需要有head Node字段用以快速获取队列头的后一个NodeQ而tail Node字段用来快速插入新的NodeQ所以关键在于如何线E安全的构徏q个队列Q方法还是一L(fng)Q用CAS操作Q即CASҎ(gu)自p|成tail|然后重新构徏q个列表Q?br />
在当前线ENode以线E安全的方式攑օq个队列后,lock实现相对比较简单了(jin)Q如果当前Node是的前驱是headQ该U程获得锁,否则park当前U程Q处理park无理p回的问题Q因而将park攑օwhile循环中(该实现是一个不可重入的实现Q:(x)
unlock的实现需要考虑多种情况Q如果当前Node(head.next)有后驱,那么直接unpark该后驱即可;如果没有Q表C当前已l没有其他线E在{待队列中,然而在q个判断q程中可能会(x)有其他线E进入,因而需要用CAS的方式设|tailQ如果设|失败,表示此时有其他线E进入,因而需要将该新q入的线Eunpark从而该新进入的U程在调用park后可以立卌回(q里的CAS和enqueue的CAS都是对tail操作Q因而能保证状态一_(d)(j)Q?br />
具体的代码和试cd以参考查?a >q里?br />
其实直到自己写完q个cd才直到者其实这是一个MCS锁的变种Q因而这个实现每个线Epark在自w对应的node上,而由前一个线Eunpark它;而AbstractQueuedSynchronizer是CLH锁,因ؓ(f)它的park由前q态决定,虽然它也是由前一个线Eunpark它。具体可以参?a >q里?/p>
自旋锁是指当一个线E尝试获取某个锁Ӟ如果该锁已被其他U程占用Q就一直@环检锁是否被释放,而不是进入线E挂h睡眠状态?/p>
自旋锁适用于锁保护的(f)界区很小的情况,临界区很的话,锁占用的旉很短?/p>
SimpleSpinLock里有一个owner属性持有锁当前拥有者的U程的引用,如果该引用ؓ(f)nullQ则表示锁未被占用,不ؓ(f)null则被占用?/p>
q里用AtomicReference是ؓ(f)?jin)用它的原子性的compareAndSetҎ(gu)QCAS操作Q,解决?jin)多U程q发操作D数据不一致的问题Q确保其他线E可以看到锁的真实状?br />
Ticket Lock 是ؓ(f)?jin)解决上面的公^性问题,cM于现实中银行柜台的排队叫P(x)锁拥有一个服务号Q表C正在服务的U程Q还有一个排队号Q每个线E尝试获取锁之前先拿一个排队号Q然后不断轮询锁的当前服务号是否是自q排队P如果是,则表C己拥有了(jin)锁,不是则l轮询?/p>
当线E释NӞ服务号?Q这样下一个线E看到这个变化,退?gu)旋?/p>
Ticket Lock 虽然解决?jin)公qx的问题Q但是多处理器系l上Q每个进E?U程占用的处理器都在d同一个变量serviceNum Q每ơ读写操作都必须在多个处理器~存之间q行~存同步Q这?x)导致繁重的pȝȝ和内存的量Q大大降低系l整体的性能?/p>
下面介绍的CLH锁和MCS锁都是ؓ(f)?jin)解册个问题的?/p>
MCS 来自于其发明人名字的首字母:(x) John Mellor-Crummey和Michael Scott?/p>
CLH的发明h是:(x)CraigQLandin and Hagersten?/p>
MCS Spinlock 是一U基于链表的可扩展、高性能、公q的自旋锁,甌U程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少?jin)不必要的处理器~存同步的次敎ͼ降低?jin)ȝ和内存的开销?/p>
CLH锁也是一U基于链表的可扩展、高性能、公q的自旋锁,甌U程只在本地变量上自旋,它不断轮询前q状态,如果发现前驱释放?jin)锁q束自旋?br />
下图是CLH锁和MCS锁队列图C:(x)
差异Q?/p>
注意Q这里实现的锁都是独占的Q且不能重入的?/strong>
What’s the difference between concurrency and parallelism?
Explain it to a five year old.
Concurrent = Two queues and one coffee machine.
Parallel = Two queues and two coffee machines.
前文Q?a style="color: #ca0000; text-decoration: none;">深入JVM锁机?synchronizedQ分析了(jin)JVM中的synchronized实现Q本文l分析JVM中的另一U锁Lock的实现。与synchronized不同的是QLock完全用Java写成Q在javaq个层面是无关JVM实现的?/p>
在java.util.concurrent.locks包中有很多Lock的实现类Q常用的有ReentrantLock、ReadWriteLockQ实现类ReentrantReadWriteLockQ,其实现都依赖java.util.concurrent.AbstractQueuedSynchronizerc,实现思\都大同小异,因此我们以ReentrantLock作ؓ(f)讲解切入炏V?/p>
l过观察ReentrantLock把所有Lock接口的操作都委派C个SynccMQ该cȝ承了(jin)AbstractQueuedSynchronizerQ?/p>
Sync又有两个子类Q?/p>
昄是ؓ(f)?jin)支持公q锁和非公^锁而定义,默认情况下ؓ(f)非公q锁?/p>
先理一下Reentrant.lock()Ҏ(gu)的调用过E(默认非公q锁Q:(x)
q些讨厌的Template模式D很难直观的看到整个调用过E,其实通过上面调用q程?qing)AbstractQueuedSynchronizer的注释可以发玎ͼAbstractQueuedSynchronizer中抽象了(jin)l大多数Lock的功能,而只把tryAcquireҎ(gu)延迟到子cM实现。tryAcquireҎ(gu)的语义在于用具体子类判断hU程是否可以获得锁,无论成功与否AbstractQueuedSynchronizer都将处理后面的流E?/p>
单说来,AbstractQueuedSynchronizer?x)把所有的hU程构成一个CLH队列Q当一个线E执行完毕(lock.unlock()Q时?x)激z自q后节点Q但正在执行的线Eƈ不在队列中,而那些等待执行的U程全部处于d状态,l过调查U程的显式阻塞是通过调用LockSupport.park()完成Q而LockSupport.park()则调用sun.misc.Unsafe.park()本地Ҏ(gu)Q再q一步,HotSpot在Linux中中通过调用pthread_mutex_lock函数把线E交l系l内核进行阻塞?/p>
该队列如图:(x)
与synchronized相同的是Q这也是一个虚拟队列,不存在队列实例,仅存在节点之间的前后关系。o(h)人疑惑的是ؓ(f)什么采用CLH队列呢?原生的CLH队列是用于自旋锁Q但Doug Lea把其攚wؓ(f)d锁?/p>
当有U程竞争锁时Q该U程?x)首先尝试获得锁Q这对于那些已经在队列中排队的线E来说显得不公^Q这也是非公q锁的由来,与synchronized实现cMQ这样会(x)极大提高吞吐量?/p>
如果已经存在RunningU程Q则新的竞争U程?x)被q加到队,具体是采用基于CAS的Lock-Free法Q因为线Eƈ发对Tail调用CAS可能?x)导致其他线ECASp|Q解军_法是循环CAS直至成功。AbstractQueuedSynchronizer的实现非常精巧,令h叹ؓ(f)观止Q不入细节难以完全领?x)其_NQ下面详l说明实现过E:(x)
nonfairTryAcquireҎ(gu)是lockҎ(gu)间接调用的第一个方法,每次h锁时都会(x)首先调用该方法?/p>
该方法会(x)首先判断当前状态,如果c==0说明没有U程正在竞争该锁Q如果不c !=0 说明有线E正拥有?jin)该锁?/p>
如果发现c==0Q则通过CAS讄该状态gؓ(f)acquires,acquires的初始调用gؓ(f)1Q每ơ线E重入该锁都?1Q每ơunlock都会(x)-1Q但?旉N。如果CAS讄成功Q则可以预计其他MU程调用CAS都不?x)再成功Q也p为当前线E得C(jin)该锁Q也作ؓ(f)RunningU程Q很昄q个RunningU程q未q入{待队列?/p>
如果c !=0 但发现自己已l拥有锁Q只是简单地++acquiresQƈ修改status|但因为没有竞争,所以通过setStatus修改Q而非CASQ也是说这D代码实C(jin)偏向锁的功能Qƈ且实现的非常漂亮?br />
addWaiterҎ(gu)负责把当前无法获得锁的线E包装ؓ(f)一个Noded到队:(x)
其中参数mode是独占锁q是׃n锁,默认为nullQ独占锁。追加到队尾的动作分两步Q?/p>
下面是enqҎ(gu)Q?/p>
该方法就是@环调用CASQ即使有高ƈ发的场景Q无限@环将?x)最l成功把当前U程q加到队(或设|队_(d)(j)。总而言之,addWaiter的目的就是通过CAS把当前现在追加到队尾Qƈq回包装后的Node实例?/p>
把线E要包装为Node对象的主要原因,除了(jin)用Node构造供虚拟队列外,q用Node包装?jin)各U线E状态,q些状态被_ֿ(j)设计Z些数字|(x)
acquireQueued的主要作用是把已l追加到队列的线E节点(addWaiterҎ(gu)q回|(j)q行dQ但d前又通过tryAccquire重试是否能获得锁Q如果重试成功能则无需dQ直接返?/p>
仔细看看q个Ҏ(gu)是个无限循环Q感觉如果p == head && tryAcquire(arg)条g不满_@环将永远无法l束Q当然不?x)出现死循环Q奥U在于第12行的parkAndCheckInterrupt?x)把当前U程挂vQ从而阻塞住U程的调用栈?/p>
如前面所qͼLockSupport.park最l把U程交给pȝQLinuxQ内核进行阻塞。当然也不是马上把请求不到锁的线E进行阻塞,q要(g)查该U程的状态,比如如果该线E处于Cancel状态则没有必要Q具体的(g)查在shouldParkAfterFailedAcquire中:(x)
(g)查原则在于:(x)
M看来QshouldParkAfterFailedAcquire是靠前l节点判断当前线E是否应该被dQ如果前l节点处于CANCELLED状态,则顺便删除这些节炚w新构造队列?/p>
xQ锁住线E的逻辑已经完成Q下面讨锁的q程?/p>
h锁不成功的线E会(x)被挂起在acquireQueuedҎ(gu)的第12行,12行以后的代码必须{线E被解锁锁才能执行,假如被阻塞的U程得到解锁Q则执行W?3行,卌|interrupted = trueQ之后又q入无限循环?/p>
从无限@环的代码可以看出Qƈ不是得到解锁的线E一定能获得锁,必须在第6行中调用tryAccquire重新竞争Q因为锁是非公^的,有可能被新加入的U程获得Q从而导致刚被唤醒的U程再次被阻塞,q个l节充分体现?#8220;非公q?#8221;的精髓。通过之后要介绍的解锁机制会(x)看到Q第一个被解锁的线E就是HeadQ因此p == head的判断基本都?x)成功?/p>
x可以看到Q把tryAcquireҎ(gu)延迟到子cM实现的做法非常精妙ƈh极强的可扩展性,令h叹ؓ(f)观止Q当然精妙的不是q个Templae设计模式Q而是Doug Lea寚wl构的精?j)布局?/p>
解锁代码相对单,主要体现在AbstractQueuedSynchronizer.release和Sync.tryReleaseҎ(gu)中:(x)
class AbstractQueuedSynchronizer
class Sync
tryRelease与tryAcquire语义相同Q把如何释放的逻辑延迟到子cM。tryRelease语义很明:(x)如果U程多次锁定Q则q行多次释放Q直至status==0则真正释NQ所谓释N卌|status?Q因为无竞争所以没有用CAS?/p>
release的语义在于:(x)如果可以释放锁,则唤醒队列第一个线E(HeadQ,具体唤醒代码如下Q?/p>
q段代码的意思在于找出第一个可以unpark的线E,一般说来head.next == headQHead是W一个线E,但Head.next可能被取消或被置为nullQ因此比较稳妥的办法是从后往前找W一个可用线E。貌似回溯会(x)D性能降低Q其实这个发生的几率很小Q所以不?x)有性能影响。之后便是通知pȝ内核l箋该线E,在Linux下是通过pthread_mutex_unlock完成。之后,被解锁的U程q入上面所说的重新竞争状态?/p>
AbstractQueuedSynchronizer通过构造一个基于阻塞的CLH队列容纳所有的dU程Q而对该队列的操作均通过Lock-FreeQCASQ操作,但对已经获得锁的U程而言QReentrantLock实现?jin)偏向锁的功能?/p>
synchronized的底层也是一个基于CAS操作的等待队列,但JVM实现的更_Q把{待队列分ؓ(f)ContentionList和EntryListQ目的是Z(jin)降低U程的出列速度Q当然也实现?jin)偏向锁Q从数据l构来说二者设计没有本质区别。但synchronizedq实C(jin)自旋锁,q对不同的pȝ和硬件体p进行了(jin)优化Q而Lock则完全依靠系l阻塞挂L(fng)待线E?/p>
当然Lock比synchronized更适合在应用层扩展Q可以承AbstractQueuedSynchronizer定义各种实现Q比如实现读写锁QReadWriteLockQ,公^或不公^锁;同时QLock对应的Condition也比wait/notify要方便的多、灵zȝ多?/p>
目前在Java中存在两U锁机制Qsynchronized和LockQLock接口?qing)其实现cLJDK5增加的内容,其作者是大名鼎鼎的ƈ发专家Doug Lea。本文ƈ不比较synchronized与LockC孰劣Q只是介l二者的实现原理?/p>
数据同步需要依赖锁Q那锁的同步又依赖谁Qsynchronizedl出的答案是在Y件层面依赖JVMQ而Lockl出的方案是在硬件层面依赖特D的CPU指o(h)Q大家可能会(x)q一步追问:(x)JVM底层又是如何实现synchronized的?
本文所指说的JVM是指Hotspot?u23版本Q下面首先介lsynchronized的实玎ͼ(x)
synrhronized关键字简z、清晰、语义明,因此即有了(jin)Lock接口Q用的q是非常q泛。其应用层的语义是可以把M一个非null对象作ؓ(f)"?Q当synchronized作用在方法上Ӟ锁住的便是对象实例(thisQ;当作用在?rn)态方法时锁住的便是对象对应的Class实例Q因为Class数据存在于永久带Q因此静(rn)态方法锁相当于该cȝ一个全局锁;当synchronized作用于某一个对象实例时Q锁住的便是对应的代码块。在HotSpot JVM实现中,锁有个专门的名字Q对象监视器?/span>当多个线E同时请求某个对象监视器Ӟ对象监视器会(x)讄几种状态用来区分请求的U程Q?/p>
ContentionListq不是一个真正的QueueQ而只是一个虚拟队列,原因在于ContentionList是由Node?qing)其next指针逻辑构成Qƈ不存在一个Queue的数据结构。ContentionList是一个后q先出(LIFOQ的队列Q每ơ新加入Node旉?x)在队头q行Q通过CAS改变W一个节点的的指针ؓ(f)新增节点Q同时设|新增节点的next指向后箋节点Q而取得操作则发生在队。显?dng)该结构其实是个Lock-Free的队列?/p>
因ؓ(f)只有OwnerU程才能从队֏元素Q也即线E出列操作无争用Q当然也避免了(jin)CAS的ABA问题?/p>