释放锁流程源码剖析

1 释放锁流程概述

ReentrantLock的unlock()方法不区分公平锁还是非公平锁。

  • 首先调用unlock()方法。

  • unlock()底层使用的是Sync.release(1)方法

  • public void unlock() {<!-- -->

    sync.release(1);

    }

release(1)方法会调用tryRelease(1)去尝试解锁。

public final boolean release(int arg) {<!-- -->
   //尝试释放锁
     if (tryRelease(arg)) {<!-- -->
         Node h = head;
         if (h != null && h.waitStatus != 0)
            //如果释放锁成功,而且等待队列不为空,且有一个以上的等待线程
            //因为只有下一个线程才能将前一个线程的waitStatus的状态改为-1,head表示当前执行的线程
            //当head不为空,且waitStatus !=0说明有等待线程初始化了等待队列,且将持有锁线程的
            //等待状态改为了-1,必然存在等待线程,将队头的第一个唤醒
             unparkSuccessor(h);
         return true;
     }
     return false;
 }

tryRelease(arg)尝试释放锁

@ReservedStackAccess
    protected final boolean tryRelease(int releases) {<!-- -->
        //释放一次锁,就将重入的次数减掉1
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        //如果锁得状态为1,则表示锁真正被释放了,将持有锁的线程置为null
        if (c == 0) {<!-- -->
            free = true;
            setExclusiveOwnerThread(null);
        }
        //否则,锁依然被持有,因为该锁被持锁线程重入了多次
        setState(c);
        return free;
    }

如果tryRelease()释放锁成功,且判断等待队列确实有阻塞线程,则尝试唤醒

private void unparkSuccessor(Node node) {<!-- -->
    //如果等待的线程状态<0,SIGNAL,将其设为0
     int ws = node.waitStatus;
     if (ws < 0)
         node.compareAndSetWaitStatus(ws, 0);
     Node s = node.next;
     //找一个符合条件,即真正在阻塞睡眠的线程
     if (s == null || s.waitStatus > 0) {<!-- -->
         s = null;
         for (Node p = tail; p != node && p != null; p = p.prev)
             if (p.waitStatus <= 0)
                 s = p;
     }
     //找到后,将其唤醒。有个疑问,头节点不变化嘛???
     if (s != null)
         LockSupport.unpark(s.thread);
 }

回答自己的疑问,为啥没有操作头节点呢?这是因为唤醒阻塞的第一个线程后,它会重新去获取锁,而不是直接将锁分配给它。

 final boolean acquireQueued(final Node node, int arg) {<!-- -->
     boolean interrupted = false;
     try {<!-- -->
         for (;;) {<!-- -->
             final Node p = node.predecessor();
             if (p == head && tryAcquire(arg)) {<!-- -->
                 setHead(node);
                 p.next = null; // help GC
                 return interrupted;
             }
             if (shouldParkAfterFailedAcquire(p, node))
             //从此处被唤醒后,重新进行循环,尝试去争抢锁,如果没抢到,则继续阻塞(非公平的时候)
             //当刚被唤醒,循环一次,此时p==head,同时如果tryAcquire(1)去获得锁,
             //如果获得成功将自己设置为head,
             //如果获得锁失败,则自己再自旋一次(因为在释放锁的时候,head的ws又重置为0了).
             //如果还是失败,则自己再次park()睡眠
                 interrupted |= parkAndCheckInterrupt();
         }
     } catch (Throwable t) {<!-- -->
         cancelAcquire(node);
         if (interrupted)
             selfInterrupt();
         throw t;
     }
 }

2 释放锁源码分析

public void unlock() {
// 释放锁资源不分为公平锁和非公平锁,都是一个sync对象
sync.release(1);
}
// 释放锁的核心流程
public final boolean release(int arg) {
// 核心释放锁资源的操作之一
if (tryRelease(arg)) {
// 如果锁已经释放掉了,走这个逻辑
Node h = head;
// h不为null,说明有排队的(录课时估计脑袋蒙圈圈。)
// 如果h的状态不为0(为-1),说明后面有排队的Node,并且线程已经挂起了。
if (h != null && h.waitStatus != 0)
 // 唤醒排队的线程
unparkSuccessor(h);
return true;
}
return false;
}
// ReentrantLock释放锁资源操作
protected final boolean tryRelease(int releases) {
// 拿到state - 1(并没有赋值给state)
int c = getState() - releases;
// 判断当前持有锁的线程是否是当前线程,如果不是,直接抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// free,代表当前锁资源是否释放干净了。
boolean free = false;
if (c == 0) {
// 如果state - 1后的值为0,代表释放干净了。
free = true;
// 将持有锁的线程置位null
setExclusiveOwnerThread(null);
}
// 将c设置给state
setState(c);
// 锁资源释放干净返回true,否则返回false
return free;
}
// 唤醒后面排队的Node
private void unparkSuccessor(Node node) {
// 拿到头节点状态
int ws = node.waitStatus;
if (ws < 0)
// 先基于CAS,将节点状态从-1,改为0
compareAndSetWaitStatus(node, ws, 0);
// 拿到头节点的后续节点。
Node s = node.next;
// 如果后续节点为null或者,后续节点的状态为1,代表节点取消了。
if (s == null || s.waitStatus > 0) {
s = null;
// 如果后续节点为null,或者后续节点状态为取消状态,从后往前找到一个有效节点环境
for (Node t = tail; t != null && t != node; t = t.prev)
// 从后往前找到状态小于等于0的节点
// 找到离head最新的有效节点,并赋值给s
if (t.waitStatus <= 0)
s = t;
}
// 只要找到了这个需要被唤醒的节点,执行unpark唤醒
if (s != null)
LockSupport.unpark(s.thread);
}

3 AQS常见的问题

3.1 AQS中为什么要有一个虚拟的head节点

因为AQS提供了ReentrantLock的基本实现,而在ReentrantLock释放锁资源时,需要去考虑是否需要执行unparkSuccessor方法,去唤醒后继节点。

因为Node中存在waitStatus的状态,默认情况下状态为0,如果当前节点的后继节点线程挂起了,那么就将当前节点的状态设置为-1。这个-1状态的出现是为了避免重复唤醒或者释放资源的问题。

因为AQS中排队的Node中的线程如果挂起了,是无法自动唤醒的。需要释放锁或者释放资源后,再被释放的线程去唤醒挂起的线程。 因为唤醒节点需要从整个AQS双向链表中找到离head最近的有效节点去唤醒。而这个找离head最近的Node可能需要遍历整个双向链表。如果AQS中,没有挂起的线程,代表不需要去遍历AQS双向链表去找离head最近的有效节点。为了避免出现不必要的循环链表操作,提供了一个-1的状态。如果只有一个Node进入到AQS中排队,所以发现如果是第一个Node进来,他必须先初始化一个虚拟的head节点作为头,来监控后继节点中是否有挂起的线程。

3. 2 AQS中为什么选择使用双向链表,而不是单向链表

首先AQS中一般是存放没有获取到资源的Node,而在竞争锁资源时,ReentrantLock提供了一个方法,lockInterruptibly方法,也就是线程在竞争锁资源的排队途中,允许中断。中断后会执行cancelAcquire方法,从而将当前节点状态置位1,并且从AQS队列中移除掉。如果采用单向链表,当前节点只能按到后继或者前继节点,这样是无法将前继节点指向后继节点的,需要遍历整个

AQS从头或者从尾去找。单向链表在移除AQS中排队的Node时,成本很高。

当前在唤醒后继节点时,如果是单向链表也会出问题,因为节点插入方式的问题,导致只能单向的去找有效节点去唤醒,从而造成很多次无效的遍历操作,如果是双向链表就可以解决这个问题。

相关推荐
北极无雪13 分钟前
Spring源码学习(拓展篇):SpringMVC中的异常处理
java·开发语言·数据库·学习·spring·servlet
VXbishe20 分钟前
(附源码)基于springboot的“我来找房”微信小程序的设计与实现-计算机毕设 23157
java·python·微信小程序·node.js·c#·php·课程设计
shan_shmily42 分钟前
算法知识点————贪心
算法
YONG823_API44 分钟前
电商平台数据批量获取自动抓取的实现方法分享(API)
java·大数据·开发语言·数据库·爬虫·网络爬虫
寂柒1 小时前
C++——模拟实现stack和queue
开发语言·c++·算法·list
扬子鳄0081 小时前
java注解的处理器
java
Amagi.1 小时前
Spring中Bean的作用域
java·后端·spring
熬夜学编程的小王1 小时前
C++类与对象深度解析(一):从抽象到实践的全面入门指南
c++·git·算法
CV工程师小林1 小时前
【算法】DFS 系列之 穷举/暴搜/深搜/回溯/剪枝(下篇)
数据结构·c++·算法·leetcode·深度优先·剪枝
Dylanioucn1 小时前
【分布式微服务云原生】掌握 Redis Cluster架构解析、动态扩展原理以及哈希槽分片算法
算法·云原生·架构