码迷,mamicode.com
首页 > 其他好文 > 详细

ReentrantLock 源代码之我见

时间:2021-02-09 12:01:31      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:接口   prot   子类   ast   tst   pat   获取   rds   case   

ReentrantLock,英文意思是可重入锁。从实际代码实现来说,ReentrantLock也是互斥锁(Node.EXCLUSIVE)。与互斥锁对应的的,还有共享锁Node.SHARED
ReentrantLock 集成了Lock接口,Lock接口主要功能有上锁lock()、尝试上锁tryLock()、规定时间内尝试上锁tryLock(long time, TimeUnit unit)、释放锁unlock()。

ReentrantLock有个内部的抽象类Sync,这个Sync类继承了AbstractQueuedSynchronizer类,内部定义了抽象上锁方法lock(),还有非公平尝试上锁nonfairTryAcquire(int acquires),
尝试释放锁tryRelease(int releases) 、是否持有互斥锁isHeldExclusively()等方法。
Sync 有两个子类,非公平锁NonfairSync和公平锁FairSync。两个子类,都实现了抽象的方法上锁lock(),同时还有一个尝试上锁tryAcquire(int acquires)。在Sync的子类实现中,这个
tryAcquire(int acquires)的形参acquires都是1,表示加锁1次。这个加锁次数,维护在AQS里面的变量state中,这个后面会讲。

ReentrantLock 类内部,还有上锁lock()、尝试上锁tryLock()、规定时间内尝试上锁tryLock(long time, TimeUnit unit)、释放锁unlock()、获取加锁次数getHoldCount()

获取等待的条件hasWaiters()等。其中,最重要,也是最常用的,是lock()、unlock()、tryLock()这些。
----------------------------------------------------------------------------------------------------------------

挑主要的方法来讲。

先介绍上锁lock()。
 public void lock() {
        sync.lock();
    }

在这个方法中,sync.lock(),是一个策略模式,由子类的实现而确定。如果子类是公平锁FairSync,则调用FairSync的lock()方法;否则,调用非公平锁

NonfairSync的lock()方法。

先看公平锁的lock(),代码如下

final void lock() {
            acquire(1);
        }
//加锁
public final void acquire(int arg) {
//如果尝试上锁上锁,并且获取队列成功,则当前线程自中断。
if (!tryAcquire(arg) && //这里的tryAcquire,由子类实现,如下面的代码2
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt(); // 自中断
}

//通过自旋的方式获取同步状态。所谓自旋,说白了,就是死循环for(;;)。这个方法返回中断状态
//代码1
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) { //如果前驱节点是头节点并且获取锁成功,则把头节点设置成当前的节点。并且把前驱节点的next设置为null,方便gc。这里再次调用了tryAcquire
                setHead(node);
p.next = null; // help GC //注意这里的写法。因为当前节点已经成为头部节点,当前节点的线程关闭后,当前节点也会被回收。那么当前节点的前驱节点的next,需要设置成null,否则gc不会回收当前节点。
failed = false;
return interrupted; //返回当前的节点的中断状态:false
}
if (shouldParkAfterFailedAcquire(p, node) && //是否应该挂起失败的线程
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

//是否应该挂起失败的线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//如果前驱节点,已经是SIGNAL,也就是-1,那么直接返回true,表示可以挂起。因为,前驱节点释放锁后,会唤醒后续的的节点
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) { //如果前驱节点已经被注销,也就是waitStatus > 0(大于0 的只有被注销的状态),则执行下面的循环
        /*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do { //这里不断循环,直到前驱节点的状态<=0。当等于0的时候,表示没有状态。当小于0的时候,有-1 -2 -3三种情况。其中,-3是共享模式才有,表示节点可传播。-2则是表示节点是处于条件Condition队列。-1才表示节点处于等待队列。
node.prev = pred = pred.prev; //其实就是常用的for循环的变种而已
        } while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don‘t park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL); //采用CAS操作,设置前驱节点的状态为-1,表示释放锁后,会唤醒后驱节点
}
return false;
}
/挂起并设置中断
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}

//代码4
//节点取消获取锁
private void cancelAcquire(Node node) {
// Ignore if node doesn‘t exist
if (node == null)
return;

node.thread = null;

// Skip cancelled predecessors
//这里跳过前驱节点。这里的实现挺巧妙的
Node pred = node.prev;
while (pred.waitStatus > 0) //如果前驱节点的状态>0,也就是已经被取消了,则循环向前查找前驱节点,直到前驱节点的状态 < = 0,也就是SIGNAL -1状态或者PROPAGATE -2传播状态。传播状态只在共享模式下才有用
node.prev = pred = pred.prev;

// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next; //前驱节点的后驱节点

// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED; //设置当前节点状态为取消,也就是1

// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) { //如果当前节点是末尾节点,当前节点n会被设置成CANCEL,则把等待队列的末尾节点设置成当前节点的前驱节点,也就是第n-1个节点被设置成了末尾节点
compareAndSetNext(pred, predNext, null); //将前驱节点的后驱节点设置为null,因为当前节点已经设置成了CANCELLED了,前驱节点正式成为末尾节点,也就不会再由后驱节点
} else {
// If successor needs signal, try to set pred‘s next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head && //这里的说明,在下面说明1处详述
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next; //当前节点的后驱节的成下一个节点
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next); //前驱节点的后驱节点本来是当前节点,现在将前驱节点的后驱节点,设置成当前节点的下一个节点。
} else { //直接后激活驱节点。park是挂起unpark是激活
unparkSuccessor(node);
}

node.next = node; // help GC //将节点的后驱节点设置成自身,方便gc。这里要注意的是,不同于其他变量,设置成null
}
}
//说明1:如果当前节点不是头节点且线程不是空,有以下几种场景:1、前驱节点的状态已经是唤醒状态SIGNAL -1, 2、如果前驱节点不是SINGAL,会可能=0(没有状态) 或者=-3(共享模式的传播状态),则设置前驱节点的状态为SIGNAL

//代码5
//激活后驱节点(使后驱节点可用)
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0) //如果当前节点的状态<0,即唤醒SINGAL -1状态 ,或者等待条件CONDITION -2状态 ,或者PROPAGATE -3 传播状态(共享模式),则将节点的状态设置成0(没有状态)
compareAndSetWaitStatus(node, ws, 0);

/*要激活的线程通常是在后驱节点上持有(这句话怎么意思?我的理解是,当前节点的后驱节点持有的线程,会被激活。也就是后驱节点的线程,会变成可用状态)。
*如果后驱节点已经被取消或者被设置成null,则从末尾节点开始往前搜索,直接找到一个不是null又不是取消的节点。
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) //这里从末尾节点开始循环,直到当前节点的下一个非CANCEL&非null的节点,可以参考下图
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); //使后驱节点持有的线程可用。但是只是使线程可用,不保证线程会被执行。
}
技术图片

 

 

//代码2
protected
final boolean tryAcquire(int acquires) {
//先拿到当前线程
final Thread current = Thread.currentThread();
//获取上锁的次数
int c = getState(); if (c == 0) { //如果上锁次数为0,则证明锁空闲 if (!hasQueuedPredecessors() && //如果没有前驱节点Node,则证明当前节点是头节点。使用CAS方法,设置上锁次数。这个的次数,保存在AQS的state里面 compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); //设置锁的持有者为当前线程 return true; } } else if (current == getExclusiveOwnerThread()) { //如果锁由当前线程持有,则上锁次数+acquires。这个acquires总是1 int nextc = c + acquires; if (nextc < 0) //校验参数合法行 throw new Error("Maximum lock count exceeded"); setState(nextc); //设置加锁次数 return true; } return false; }

公平锁,总是先选择第一个节点加锁。如果锁已经被当前线程持有,当前线程再次获取锁的时候,会成功,加锁次数+1。这里体现的,就是ReenTrantLock的可重入性。

下面介绍释放锁的方法。事实上,公平锁和非公平锁的释放,都是调用了父类Sync的方法

public void unlock() {
        sync.release(1);  //这里调用的是父类AQS的释放锁的方法
    }

//释放锁
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; //因为是公平锁,永远是头节点获取到锁,也就永远从头节点开始释放锁 if (h != null && h.waitStatus != 0) unparkSuccessor(h); //代码5的激活后驱节点线程 return true; } return false; }


尝试释放锁,调用的是父类Sync的方法,如下:

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;         //加锁次数叠减。这里的releases总是1
            if (Thread.currentThread() != getExclusiveOwnerThread())   //如果释放锁的线程不是排他锁的持有线程,则抛出异常
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);        //如果加锁次数已经是0,则设置锁的持有现场为null
            }
            setState(c);          //设置加锁次数
            return free;
        }

 

下面介绍非公平锁

 

final void lock() {
            if (compareAndSetState(0, 1))   //先采用 CAS操作尝试获取锁,成功则把当前线程设置成排他(互斥)锁线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);                //否则,执行加锁操作。这里的加锁操作acquire(1),和公平锁的代码一模一样。唯一的区别,是加锁时候调用的tryAcquire,各自实现而已。
        }

 protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);    //这里的nonfairTryAcquire ,直接是调用父类Sync的方法。
}
//非公平锁尝试获取锁。由此可见,ReentrantLock的默认锁,是非公平锁。

final
boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); //获取当前线程 int c = getState(); //确认加锁次数。加锁次数维护在超类AQS的state里。这个state 是由volatile里(注意这个volatile内存言语的作用,是用于共享变量在多线程即时可见。
//也就是一个线程改变了state,另一个线程马上能够看见。这个内存言语,是实现并发的基础之一)
if (c == 0) { //加锁次数为0,证明锁还没有被获取 if (compareAndSetState(0, acquires)) { //CAS操作,加锁。这里的acquires在ReentrantLock里,总是1 setExclusiveOwnerThread(current); //设置当前线程持有排他锁 return true; } } else if (current == getExclusiveOwnerThread()) { //如果加锁次数大于1,且是当前线程持有锁,则加锁次数累加 int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); //设置加锁次数 return true; } return false; }

由上面的公平锁和非公平锁的实现可以看到,实现大同小异,都是调用超类AQS的 acquire(int arg) 方法(acquire(int arg)是一个模板方法模式代码)。

不同的是,公平锁总是第一个节点才能获取到锁,这里并不符合计算机的资源最大使用思想。而非公平锁,则是由jvm调度。因此ReentrantLock默认使用的是非公平锁。

公平锁和非公平锁,都有各自的tryAcquire方法

ReentrantLock实现的基础,是AQS的虚拟双向队列CLH,具体表现在代码里,则是Node节点。AQS的队列有两种,一种是资源队列(用于唤醒等操作),一种条件队列(用于条件达到Condition)

Node节点,在AQS里面,是由volatile这个关键字,volatile同时又是内存言语。volatile的修饰,可以使功节点对于不同的线程即时可见。这是关键字,是并发的基础之一。

当一个线程想获取锁,被阻塞的时候,表现在代码里面,就是一个死循环for(;;),直到当前线程所在的节点获取到锁。

这里是类似于监听事件的原理:利用Node节点的修饰符volatile的特性。当另一个节点a(线程)释放了锁的时候,另一个线程b马上可以检测到。如果是节点b是节点a的后驱节点,则节点b可以获取到锁,而b的后驱节点c

则需要等待b释放锁后,再通知后驱节点c。这样c节点的线程,就实际形成了阻塞。

 

 ---------------------------------------------------------------

个人水平有限,请各位大佬指点。 

 

 

 

 

ReentrantLock 源代码之我见

标签:接口   prot   子类   ast   tst   pat   获取   rds   case   

原文地址:https://www.cnblogs.com/drafire/p/14387037.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!