ReentrantLock源码分析

reentranLock介绍

  • java中提供的锁:synchronized、lock锁

  • reenatranLock就是一个互斥锁,可以让多线程执行期间,只有一个线程在执行指定的一段代码

  • 使用方式

java 复制代码
public static void main(String[] args) {
    ReentrantLock  lock=new ReentrantLock();
    try{
        lock.lock(); 
        
        //执行业务逻辑
    }finally {
      lock.unlock();  
    }
}

reentranLock的lock方法源码

在进入到lock方法后,发现内部调用了sync.lock()方法,去找方法的实现时,发现了两个实现

  • FairSync:公平锁 每个线程都会在执行lock方法时,会先查看是否有线程排队,如果有,直接去排队。如果没有才会去尝争一下锁资源。
  • NonfairSync: 非公平锁 每个线程都会在执行lock方法时,先尝试获取锁资源,获取不到再排队。

如果需要使用公平锁:在new ReentrantLock时,传入参数true

如果需要使用非公平锁:直接无参构造

更加推荐非公平锁,非公平锁效率比公平锁高

java 复制代码
/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

非公平lock方法源码

java 复制代码
//非公平锁的lock方法
final void lock() {
//以CAS的方式,尝试的state从0改为1
    if (compareAndSetState(0, 1))
    //证明修改state成功,也就是代码获取锁成功
    //将当前线程社设置到exclusiveOwnerThread(AQS中),代表当前线程拿着锁资源(和后面的可重入锁有关系
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

公平锁的lock源码

java 复制代码
final void lock() {
    acquire(1);
}

分析AQS

  • AQS就是AbstractQueuedSynchronized类,AQS内部维护着一个队列,还有三个核心属:state、head 、tail
  • 线程在竞争ReentrantLock的锁资源时,只要通过CAS将state从0设置为1,即代表竞争锁资源成功

reentranLock的acquire方法源码

java 复制代码
//公平锁还是非公平锁都会调用当前的acquire方法
public final void acquire(int arg) {
//tryAcquire方法,分为两种实现。第一种是公平锁,第二种是肥公平锁
//公平锁操作:如果state为0,再看看是否有线程排队,如果有我就去排队。如果是锁重入的操作,直接获取锁
//非公平锁操作:如果state为0,直接尝试CAS修改,如果是锁重入的操作,直接获取锁
    if (!tryAcquire(arg) &&
    //addWaiter 方法:在线程没有通过tryAcquire拿到锁资源时,需要将当前线程封装为node对象,去AQS内部排队。
    //acquireQueued方法:查看当前线程是否排在队伍前面,如果是就尝试获取锁资源。如果长时间没拿到锁,也要将当前线程挂起
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

reentranLock的tryAcquire方法源码

tryAcquire方法是AQS提供的,内部并没有任何实现,需要继承AQS的类自己去实现逻辑代码

查看到tryAcquire在ReentrantLock中提供了两种实现:公平锁、非公平锁

java 复制代码
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

非公平锁实现

java 复制代码
//非公平锁实现
final boolean nonfairTryAcquire(int acquires) {
//拿到当前线程
    final Thread current = Thread.currentThread();
    //拿到AQS的state值
    int c = getState();
    //如果state为0,这就代表当前没有线程占用资源
    if (c == 0) {
   //直接基于CAS的方式,尝试修改state,从0改为1,如果成功就代表拿到锁资源
        if (compareAndSetState(0, acquires)) {
        //将exclusiveOwnerThread属性设置为当前线程
            setExclusiveOwnerThread(current);
            //返回true
            return true;
        }
    }
    //说明state肯定不为0,不为0就代表当前lock被线程占用
    //判断占用资源的线程是不是当前线程
    else if (current == getExclusiveOwnerThread()) {
    //锁重入操作 对state+1
        int nextc = c + acquires;
        //判断锁重入是否已经达到最大值
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
            //将AQS的state设置好
        setState(nextc);
        //返回true
        return true;
    }
    return false;
}

公平锁实现

java 复制代码
//公平锁实现
protected final boolean tryAcquire(int acquires) {
//获取当前线程
    final Thread current = Thread.currentThread();
    //过去state
    int c = getState();
    //没有线程占用锁资源
    if (c == 0) {
    //首先查看,有没有线程排队
        if (!hasQueuedPredecessors() &&
        //如果没有线程排队,CAS尝试获取锁资源
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

tryAcquire公平锁与非公平锁区别

tryAcquire方法公平锁与非公平锁区别就是,当判断state为0之后

  • 公平锁会先查看是否有线程正在排队,如果有返回false,如果没有线程排队,执行CAS尝试获取锁资源
  • 非公平锁不管有没有线程排队,直接以CAS的方式尝试获取锁资源

reentranLock的addWaiter方法源码

在线程执行tryAcquire方法没有获取到锁资源之后,会返回false,再配置上if中的!操作,会执行&&后面的方法,而在acquireQueued(addWaiter(Node.EXCLUSIVE),arg)的参数中执行了addWaiter,要将当前获取锁失败的线程封装为Node,排队到AQS的队列中。

java 复制代码
//获取锁失败,封装node,排队到AQS的队列中
private Node addWaiter(Node mode) {
//将线程封装为node对象
    Node node = new Node(Thread.currentThread(), mode);
    // 获取到tail节点,prev
    Node pred = tail;
    //如果tail节点不为null
    if (pred != null) {
    //将当前节点的prev,指向tail
        node.prev = pred;
        //为了避免并发问题,以CAS的方式将tail指向当前线程
        if (compareAndSetTail(pred, node)) {
       //将之前的tail的tailnext,指向当前节点
            pred.next = node;
            //返回当前节点
            return node;
        }
    }
    enq(node);
    return node;
}

整体逻辑为,先初始化Node节点,将当前线程传入,并且标识为互斥锁。 尝试将当前Node插入到AQS队列的末尾

  • 队列为空:执行enq,先初始化空Node作为头,然后再将当前Node插入,作为tail
  • 队列不为空:直接将当前Node插入,作为tail

reentranLock的acquireQueued方法源码

首先查看当前node是否排在队列的第一个位置(不算head,因为第一个节点是 head),是的话尝试获取锁资源直接再次执行tryAcquire方法竞争锁资源; 不是的话尝试将当前线程挂起(为什么是尝试,主要在挂起的时候要保证前一个节点是正常的,可能是排的时间太长了后面就不排了,但是node没有移除掉,是取消状态),最终排在有效节点后才会将当前线程挂起

1、以下代码中如果当前node为head的下一个,直接尝试获取锁资源

java 复制代码
//如果是队列前面,竞争锁资源,非队列前面,挂起线程
final boolean acquireQueued(final Node node, int arg) {
//竞争锁资源失败
    boolean failed = true;
    try {
    //线程中断标识
        boolean interrupted = false;
        //死循环
        for (;;) {
        //predecessor就是获取当前节点的上一个节点
            final Node p = node.predecessor();
            //如果当前节点的上一个节点是head,如果是head后面的节点,就执行tryAcquire方法,竞争资源
            if (p == head && tryAcquire(arg)) {
            //竞争资源成功,进入当前业务代码
            //因为当前线程已经拿到锁资源,将当前线程的node设置为head,并且将node中 prev和head致为null
                setHead(node);
                //将之前的头节点的next置为null,让GC将之前的head回收
                p.next = null; // help GC
                //将获取锁失败的标识置为false
                failed = false;
                //返回线程中断标识,默认情况为false
                return interrupted;
            }
            //见下方说明
            if (shouldParkAfterFailedAcquire(p, node) &&
            //见下方说明
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

2、尝试将当前线程挂起,涉及到了判断以及LockSupport方法

java 复制代码
//判断当前线程是否可以挂起
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//拿到了上一个节点的状态
    int ws = pred.waitStatus;
    // 如果ws为-1(Node.SIGNAL=-1),直接返回true,当前节点可以挂起,
    if (ws == Node.SIGNAL)
        return true;
        //如果ws>0,说明肯定是cancelled状态(只有cancelled=1),绕过这个节点,找上一个节点的上一个
    if (ws > 0) {
 //循环,直到找到上一个节点为小于等于0的节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
    //可能为0 -2 -3 ,直接以cas的方式将节点状态改为-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

//找到上一个节点状态是正常的后,就可以调用当前方法将线程挂起
private final boolean parkAndCheckInterrupt() {
//直接使用Unsade类的part方法挂起线程
    LockSupport.park(this);
    return Thread.interrupted();
}

reentranLock的unlock方法源码

  • unlock释放锁操作不为公平锁和非公平锁,都是执行sync的release方法

  • 释放锁的核心,就是将state从大于0的数值更改为0即为释放锁成功

  • 并且unlock方法应该会涉及到将AQS队列中阻塞的线程进行唤醒,阻塞用的park方法,唤醒必然是unpart方法

csharp 复制代码
public void unlock() {
//每次只释放1
    sync.release(1);
}

reentranLock的release方法源码

不分公平与非公平

  • 在释放锁时,是有state被减为0之后,才会去唤醒AQS队列中被挂起的线程
  • 在唤醒挂起线程时,如果head的next状态不正确,会从后往前找到离head最近的节点进行唤醒
  • 为什么从后去、往前找?(addWaiter的是先将prev指针赋值,最后才会将上一个节点的next指针赋值,为了避免丢失节点或者跳过节点,必须从后往前找)
java 复制代码
// 释放锁操作  其中arg为1
public final boolean release(int arg) {
//先查看tryRelease方法,具体见下方
    if (tryRelease(arg)) {
    //释放锁成功,进行后续处理
        Node h = head;
     //如果head不为null,并且当前的head的状态不为0=》只有状态为-1的时候后面才有一个线程处理挂起,为0代表后面没有线程挂起
        if (h != null && h.waitStatus != 0)
           //说明AQS的队列中,有node 在排队,并且线程已经挂起了,具体见下方方法说明
            unparkSuccessor(h);
        return true;
    }
    //返回false,代表释放一次没有完全释放掉
    return false;
}



//优先查看的tryRelease  参数releases=1,因为是上方传来的
protected final boolean tryRelease(int releases) {
//直接获取state,并且-releases=》将state-1
    int c = getState() - releases;
    //如果释放锁的线程,不是占用锁的线程,直接抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
        //声明一个标识
    boolean free = false;
    //判断state-1后是否为0
    if (c == 0) {
    //如果为0,锁资源释放掉
        free = true;
        //架构占用互斥锁的线程标识置为null
        setExclusiveOwnerThread(null);
    }
    //锁之前重入了,一次内释放掉,将C赋值给state,等待下次再次执行是,再次判断
    setState(c);
    return free;
}



//唤醒AQS中被挂起的线程
private void unparkSuccessor(Node node) {
  //获取head的状态
    int ws = node.waitStatus;
    if (ws < 0)
    //将当前的head状态设置为0
        compareAndSetWaitStatus(node, ws, 0)
        //拿到next节点
    Node s = node.next;
    //如果下一个节点为null,或者状态为cancel,需要找到离head节点最近的有效node大于0只有是cancelled状态
    if (s == null || s.waitStatus > 0) {
        s = null;
        //从后往前找这个节点(为什么从后往前找,需要查看addWaiter的内容)
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    //找到最近的node后,直接唤醒
    if (s != null)
    //唤醒线程
        LockSupport.unpark(s.thread);
}
相关推荐
李少兄1 分钟前
解决 Spring Boot 中 `Ambiguous mapping. Cannot map ‘xxxController‘ method` 错误
java·spring boot·后端
matrixlzp1 分钟前
Java 责任链模式 减少 if else 实战案例
java·设计模式
代码小鑫13 分钟前
A031-基于SpringBoot的健身房管理系统设计与实现
java·开发语言·数据库·spring boot·后端
Json____18 分钟前
学法减分交管12123模拟练习小程序源码前端和后端和搭建教程
前端·后端·学习·小程序·uni-app·学法减分·驾考题库
monkey_meng38 分钟前
【Rust类型驱动开发 Type Driven Development】
开发语言·后端·rust
落落落sss1 小时前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
我救我自己1 小时前
UE5运行时创建slate窗口
java·服务器·ue5
2401_853275731 小时前
ArrayList 源码分析
java·开发语言
爪哇学长1 小时前
SQL 注入详解:原理、危害与防范措施
xml·java·数据库·sql·oracle
大鲤余1 小时前
Rust,删除cargo安装的可执行文件
开发语言·后端·rust