写在前面
前面对synchronized进行了一个详细的介绍,包括对锁的升级和优化都进行了详细的总结,出来synchronized,还有lock可以在多线程并发下,保证数据的原子性,那么lock是如何来实现的?这就要从AQS讲起,话不多说
AQS
Java并发编程核心在于java.util.concurrent包而juc当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。
ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。而且它具有比synchronized更多的特性,比如它支持手动加锁与解锁,支持加锁的公平性。
ReentrantLock的使用:
ReentrantLock lock = new ReentrantLock(false);//false为非公平锁,true为公平锁
lock.lock() //加锁
// 代码逻辑
lock.unlock() //解锁
源码分析(基于公平锁的源码)
ReentrantLock的结构继承关系。
可以看到参数是false是非公平锁,true为公平锁。
无论公平还是非公平都继承了Sync。
Sync则集成了AbstractQueuedSynchronizer,也就是所要说的AQS。
发现AbstractQueuedSynchronizer还有一个父类AbstractOwnableSynchronizer。
在AbstractOwnableSynchronizer中仅只有一个Thread对象,这个就是当前持有锁的线程。
以上就是ReentrantLock的继承关系。
AbstractQueuedSynchronizer类
在AbstractQueuedSynchronizer类中,发现拥有Node、head、tail以及state四个属性。
- Node:是一种基于双向链表数据结构的队列,是FIFO先入先出线程等待队列。公平锁就是基于这个队列实现的。
- head:头节点。
- tail:尾节点。
- state:同步状态。可重入是基于这个状态实现,每重入一次,state加1,反之减1,直到为0。
接下来看看Node的结构。
Node是一个搞双向链表的队列,因此包含有prev和next,分别指向当前节点的前节点和下一个节点,以及thread当前节点的线程的引用。waitStatus表示当前节点的生命状态,有四个值分别是:CANCELLED = 1、SIGNAL = -1、CONDITION = -2、PROPAGATE = -3以及初始状态0(SIGNAL:可被唤醒,CANCELLED:出现异常,取消,CONDITION:条件等待,PROPAGATE:传播)。EXCLUSIVE和SHARED分别表示当前节点的模式分为独占和共享。
lock方法
lock方法进去可以发现,它是Sync的抽象方法,在公平锁和非公平锁中对其进行了实现。
在lock中调用了acquire方法。
先来看tryAcquire方法会返回true:加锁成功和false:加锁失败。如果加锁失败则会添加到等待队列中(addWaiter(Node.EXCLUSIVE))。
在公平锁和非公平锁中进行了实现。
首先,获取当前线程和获取state同步状态(在AbstractQueuedSynchronizer类中),如果为0,说明当前没有线程持有锁。接着,通过hasQueuedPredecessors() 判断是否有其他线程在排队,如果没有线程在排队,则进行CAS(即compareAndSetState(0, acquires)方法)操作,尝试将当前的state同步状态改为1,修改成功,设置ExclusiveOwnerThread独占线程为当前线程,即加锁成功。如下图。
如果获取state同步状态,不为0,则判断当前线程,是否是独占线程,即是否当前线程持有了锁,如果是则对state的值加1,即加锁成功。当前状态如下图。
如果加锁失败,则会调用addWaiter(Node.EXCLUSIVE)添加到等待队列中。
首先,会使用当前线程和独占模式,来构建一个独占的node节点。接着会将tail赋值给pred,因为是第一次进入,tail和head两个节点都为null,说明当前的等待队列是空的,因此不会走if判断,会进入enq(node)方法。如果等待队列不为空,则直接进入if判断在最后面加入节点。
这个方法可以看到,可能同一时间会有多个线程,执行入队,因此通过CAS操作,自旋的插入到队列中直到成功。如果队列为空的话,则需要进行初始化(Must initialize),会创建一个空的节点,这个节点没有包含任何的线程引用,head和tail同时指向这个空节点。如下图所示。
初始化等待队列完成后,才是真正的开始入队操作。入队也存在竞争,也使用CAS操作。如下图所示。
进入等待队列后,线程需要被阻塞,阻塞逻辑在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))中实现。
在这个方法中会发现,在进行阻塞前,会拿出head头节点再进行一次tryAcquire(arg)方法,进行抢锁操作,因为,线程阻塞需要从用户态到内核态的切换,尽可能的减少阻塞所带来的开销。
1.如果拿到锁,则会setHead(node)重新设置头节点,并头节点的下一个节点置空。
2.如果没有拿到锁,则进入阻塞逻辑。先进入shouldParkAfterFailedAcquire(p, node)方法。
可以看到,会去拿前驱节点的waitStatus状态来做判断,如果是SIGNAL状态则表示可以被唤醒,如果大于0则表示,当前节点存在异常。否则,使用CAS操作尝试将初始状态0改为SIGNAL状态。即通过前驱节点的waitStatus来对判断当前节点是否可以被唤醒。
首先第一轮循环进入shouldParkAfterFailedAcquire(p, node),修改head的状态,修改成SIGNAL,表示可以被唤醒。第二轮循环进入shouldParkAfterFailedAcquire(p, node),才判断ws=SIGNAL状态表示可以被唤醒,返回true。才会进入parkAndCheckInterrupt()方法。
调用LockSupport.park(),阻塞住线程。
以上即lock的整个源码流程。
阻塞住的线程再unlock方法中回去唤醒。
unlock方法
进入tryRelease(arg)方法。
即获state状态调用一次unlock方法就会减1。直到状态变为0,setExclusiveOwnerThread(null),将独占线程置为null,更新state状态。释放锁成功,首先会判断头节点的waitStatus不为0,才进入unparkSuccessor(h)唤醒下一个节点被阻塞的线程。
首先会判断ws的状态如果小于0,用CAS操作,将ws状态修改回0。因为,在上面lock的时候线程进入parkAndCheckInterrupt()方法会被阻塞在这里,不会往下去执行。这个时候在unlock的最后有unpark()操作,会唤醒线程继续往下parkAndCheckInterrupt()方法执行。在公平锁中则一定会是队列的第一个节点拿到锁,但是如果是非公平锁,则不一定是队列的第一个节点抢到锁,有可能被其他线程抢到锁,这个时候队列的第一个节点依然要进行阻塞操作,因此,将ws状态变回0,在第一次循环的时候又改为-1。
小结:在shouldParkAfterFailedAcquire(p, node)中会将head节点的waiteState改为-1,因为,持有锁的线程在释放锁的时候需要判断head节点的waiteState!=0,如果成立,会再把waiteState改为0。要想唤醒排队的第一个线程T1,T1被唤醒,准备继续走循环,抢锁(acquireQueued方法中)。抢锁,可能会失败(在非公平锁场景下),此时可能有其他线程T3持有了锁。T1可能再次被阻塞,head的节点状态再一次经历2次循环将waiteState改为-1。
最后
以上源码流程均是公平锁的逻辑。非公平锁的源码逻辑与公平锁的类似,差别仅在于有无hasQueuedPredecessors()方法的判断,在此不做赘述。