ReentrantLock获取锁、释放锁流程(下)addW

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

addWaiter 新增节点


private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

//插入节点,头节点/尾节点都是一个空节点,必要时初始化队列
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // 初始化队列
            if (compareAndSetHead(new Node())) //设置头节点
                tail = head;
        } else {
            node.prev = t;
            // 将新的节点设置成最后的节点的下一个
            if (compareAndSetTail(t, node)) { // cas将节点设置为尾节点,如果别的线程使用了unpark()从后面遍历,不会因为关系没有建立成功而失败
            //分析该节点的前置节点在cas之前就执行成功了,如果在cas执行期间,其他线程执行了unpark()了,从前面遍历 而恰巧这时候t.next =node 还没执行,关系没建立好,就会出现
                t.next = node;
                return t;
            }
        }
    }
}

复制代码

流程说明

1、先创建一个节点,如果尾节点不为空,说明之前这个队列已经实例化好了,直接将新创建的节点通过cas设置成尾节点即可。

2、如果尾节点是空的,说明等待队列还没有实例化出来,先要实例化一个节点 enq(), enq()函数就是创建等待队列的,头节点是个空节点。 注意在cas的操作的时候,新节点的前置节点,和后置节点是分开的。

acquireQueued aqs管理节点

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)) {
                setHead(node);//头节点设为自己,节点设置:node.prev = null    node.thread = null,头节点都是一个空节点
                p.next = null; // 前任节点 = null 垃圾回收 hlpe gc
                failed = false;
                return interrupted;
            }
            // 如果非头节点或者获取锁失败
            //1、如果是新节点,第一次回返回false 第二次判断正常返回true
            //2、parkAndCheckInterrupt()
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
复制代码

流程总结:

for循环中

1、先获取node节点的前置节点,判断是否为head节点,如果是head节点那么就去抢锁,如果成功了获取到锁,返回。

2、如果获取失败或者前置节点不是head

3、shouldParkAfterFailedAcquire() 的作用就是来给该节点设置"闹铃的",这个闹铃的意思是:该线程堵塞后,谁来唤醒该节点?

4、第一次上闹铃是不成功,只会找到合适的前置节点,这里的合适节点,比如该节点的前置节点是过期的或者无效的,那么就要跨过该节点,找个这个节点的前置节点。

5、成功之后,调用LockSupport.park()将该线程进行堵塞。

以上过程就是上锁的流程,我们再次进行规整一下:

获取锁的总结

1、如果是非公平第一次进来会通过cas设置state(0,1) 如果设置成功那么就获取锁,否者执行获取锁的流程,而公平锁不会去枪锁,直接走获取锁的流程
2、先执行tryAcquire(),公平和非公平的差距在于,如果这时候没有线程持有锁,非公平会直接枪锁,而公平会多一步查看等待队列是否有线程在排队。
如果这时候锁被其他线程持有那么获取锁失败,如果判断获取锁的线程是自己那么是自己state+1(可重入)
3、tryAcquire失败后,执行addWaiter(),构建一个新节点,这时候会判断如果队列还没有初始化,会初始化一个队列(第一个节点是个空的,设置为新增节点的头节点),末尾节点cas成新的节点,并返回lastNode(自己)。
   acquireQueued()AQS对生成的节点进行管理:下面是循环获取该节点的前置节点,如果是头节点(空)并且获取到锁了,那么将自己设置头节点(里面的属性部分清空,thread),前置节点设置null。
成功之后返回该线程是否被中断。如果没有抢锁成功 或者不是前置节点不是头节点,分两步判断:

  1、该节点的前置节点是否为signal,因为本线程要进行线程挂起,让前面的线程进行叫醒。如果前面的线程不是signal,那么第一次会将前面线程设置成signal,返回false,  新的一轮继续,否则是true
  2、前面那一步是true的话,说明该线程进行堵塞,如果被前置节点唤醒之后,再次进行判断,最终返回出去true
所以后面的情况会循环两次                  
注意点:非公平锁可能会抢多次锁,而非简简单单得一次,不管是等待队列还是堵塞队列,基本上头节点算是一个空的,但是在解锁得时候还是通过头节点判来释放的
复制代码

unLock() 释放锁

public final boolean release(int arg) {
    if (tryRelease(arg)) { //释放锁成功
        Node h = head;
        if (h != null && h.waitStatus != 0) //后面还需要唤醒
            unparkSuccessor(h);
        return true;
    }
    return false;
}
//释放锁:获取state,判断是否<0  如果等于0 说明成功,否者就是重入锁减-1
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);//锁持有线程设置null
    }
    setState(c);
    return free;
}
复制代码

释放锁流程总结:

1、state -1 等于0 释放锁结束

  如果头节点不为空且状态>0这个头节点是获取到锁的线程,如果这个线程在等待队列中排队,那么后面第一个线程会设置个表示signal,这个相当于是闹铃,后面线程挂起,如果你释放了麻烦叫叫我, 
  
  如果有这个需求就会唤醒后面的线程,是从后面倒序唤醒,为什么?
  
  个人理解因为在获取的时候会把head节点的next去掉,找不到下一个节点。
复制代码