摘要
Lock在硬件层面依赖CPU指令,完全由Java代码完成,底层利用LockSupport类和Unsafe类进行操作,虽然锁有很多实现,但是都依赖AbstractQueuedSynchronizer类,我们用代码来讲下ReentrantLock的实现;
ReentrantLock调用过程
ReentrantLock类的API调用都委托给一个内部类 Sync ,而该类继承了AbstractQueuedSynchronizer类:
而Sync又分为两个子类:公平锁和非公平锁,默认为非公平锁:
Lock的调用过程如下图(其中涉及到 ReentrantLock类、Sync(抽象类)、AbstractQueuedSynchronizer类,NofairSync类,这些类将 Template方法用的淋漓尽致,相当赞):
先来一张类依赖图:
下边是调用lock API时的流程:
Lock API详解
自底而上来看,由被调用一步步向上分析
nofairTryAcquire
来看这段代码,首先获取当前状态(初始化为0),当它等于0的时候,代表还没有任何线程获得该锁,然后通过CAS(底层是通过CompareAndSwapInt实现)改变state,并且设置当前线程为持有锁的线程;其他线程会直接返回false;当该线程重入的时候,state已经不等于0,这个时候并不需要CAS,因为该线程已经持有锁,然后会重新通过setState设置state的值,这里就实现了一个偏向锁的功能,即锁偏向该线程;
addWaiter
只有当锁被一个线程持有,另外一个线程请求获得该锁的时候才会进入这个方法
首先,new一个节点,这个时候模式为:mode为 Node.EXCLUSIVE,默认为null即排它锁;
然后:
如果该队列已经有node即tail!=null,则将新节点的前驱节点置为tail,再通过CAS将tail指向当前节点,前驱节点的后继节点指向当前节点,然后返回当前节点;
如果队列为空或者CAS失败,则通过enq入队:
进队的时候,要么是第一个入队并且设置head节点并且循环设置tail,要么是add tail,如果CAS不成功,则会无限循环,直到设置成功,即使高并发的场景,也最终能够保证设置成功,然后返回包装好的node节点;
acquireQueued
该方法的主要作用就是将已经进入虚拟队列的节点进行阻塞,我们看到,如果当前节点的前驱节点是head并且尝试获取锁的时候成功了,则直接返回,不需要阻塞;
如果前驱节点不是头节点或者获取锁的时候失败了,则进行判定是否需要阻塞:
这段代码对该节点的前驱节点的状态进行判断,如果前驱节点已经处于signal状态,则返回true,表明当前节点可以进入阻塞状态;
否则,将前驱节点状态CAS置为signal状态,然后通过上层的for循环进入parkAndCheckInterrupt代码块park:
这个时候将该线程交给操作系统内核进行阻塞;
总体来讲,acquireQueued就是依靠前驱节点的状态来决定当前线程是否应该处于阻塞状态,如果前驱节点处于cancel状态,则丢弃这些节点,重新构建队列;
Unlock API详解
流程类似lock api相关类的流程,这里讲主要的代码,unlock相对的比较简单
首先 ReentrantLock 调用 Sync的release接口也就是AbstractQueuedSynchronizer的release接口
这个时候会先调用Sync的tryRelease,如果返回true,则释放锁成功
这个接口的作用很简单,如果不是获得锁的线程调用直接抛出异常,否则,如果当前state-releases==0也就是lock已经完全释放,返回true,清除资源;
这个返回free之后,release拿到head节点,进入以下代码:
这个作用即:当头结点的状态小于0,则将头结点的状态CAS为0,然后通过链表获取下一个节点,如果下一个节点为null或者不符合要求的状态,则从队尾遍历整个链表,直到遍历到离head节点最近的一个节点并且
等待状态符合预期,则将头结点的后继节点置为该节点;
对刚刚筛出来的符合要求的节点唤醒,也就是该节点获得 争夺 锁的权利;
这就是非公平锁的特点:在队列一直等待的线程不一定比后来的线程先获得锁,至此,unlock 已经解释完成