【多线程】轻松彻底搞懂ReentrantLock的底层实现原理

前言

上篇博客讲解了Sychronized的底层实现原理,它是基于jdk实现的,现在此篇博客讲解基于AQS 框架实现的另一个锁ReentrantLock

1、名词解释

为什么叫CLH队列

CLH 全称是三个人创造的,分别为Craig、Landin、Hagersten,所以以这个三个名字的首字母命名,原始的CLH锁是一种单向链表结构的自旋锁队列
每个节点仅包含:

前驱指针(myPred):指向队列中前一个节点
状态变量(locked):表示当前线程是否需要获取锁(true=需要自旋等待,false=可获取锁)
线程信息:封装当前等待线程

但是在ReentrantLock里它对原来的CLH队列进行了升级,Java 并发包中的 AQS 框架采用 CLH 锁的变体(虚拟双向队列(FIFO队列)),通过封装线程为 Node 对象实现锁分配。

2、ReentrantLock的实现

2.1 Node的数据结构

在AQS框架中,是使用队列的方式来实现同步管理的,队列中每个节点是Node节点,看下Node的数据结构,

        static final Node SHARED = new Node();
        static final Node EXCLUSIVE = null;
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;

源码如上,其实核心的结构就是

thread:一个等待获取同步状态的线程

prev:指向上一个节点的引用

next:指向下一个节点的引用

waitStatus:等待的状态:
在这里插入图片描述
1.CANCELLED:值尾1,表示在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消等待,接入CANCELLED状态后将不会再变化
2.SIGNAL:值尾-1,表示后续节点的线程处于等待状态,如果当前节点的线程释放了同步状态或者被取消便会通知后续节点,使后续节点的线程得以运行
3.CONDITION,值为-2 表示节点再条件队列中,节点等待线程再Condition上,当其它线程队Condition调用了signal()后,该节点将会从条件队列中转移到同步队列中
4.PROPAGATE:值为-3,表示下一次共享式同步状态获取将会无条件传播下去

nextWaiter:表示条件队列中的后续节点,如果当前节点是共享的,那么这个字段将是一个SHARED变量static final Node SHARED = new Node();,也就是说节点类型(独占和共享)和条件队列中的后续节点共用同一个字段EXCLUSIVE:独占模式节点

AQS实现原理

上边介绍了AQS 中的Node节点,现在说下AQS实现的原理

核心元素
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
	// Node节点
    static final class Node {...}
    // 指向头节点
    private transient volatile Node head;
    // 指向尾节点
    private transient volatile Node tail;
    // 表示资源的可用状态
    private volatile int state;
}
AQS 中的同步等待队列结构

AQS中 维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
这里volatile能够保证多线程下的可见性,当state=1则代表当前对象锁已经被占有,其他线程来加锁时则会失败,加锁失败的线程会被放入一个FIFO的等待队列中,比列会被LockSupport.park()操作挂起,等待其他获取锁的线程释放锁才能够被唤醒。另外state的操作都是通过CAS来保证其并发修改的安全性。锁的可重入性state也会有所体现,当state等于0说明没有其它线程获取锁,可以被获取,获取后会设为1并将exclusiveOwnerThread设置为获取到锁的线程,如果不是等于0则判断是否是exclusiveOwnerThread的线程,是的话则获取锁,state+1,否则进入等待队列进行排队。需要注意的是head 指向的是一个虚拟节点,该节点不关联任何线程(thread=null),仅作为队列的起始标识。这种设计简化了队列操作,避免在无竞争时频繁创建和销毁节点
在这里插入图片描述

源码分析
1、ReentrantLock下的lock方法
   public ReentrantLock() {
        sync = new NonfairSync();
    }
    public void lock() {
        sync.acquire(1);
    }
获取锁
//尝试获取锁,如果成功则直接返回,如果失败则调用 addWaiter(Node.EXCLUSIVE) 将当前线程加入等待队列,并调用 acquireQueued 进行排队等待。
//如果在排队过程中线程被中断,则调用 selfInterrupt() 恢复中断状态。
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    // 非公平锁
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

        @ReservedStackAccess
        final boolean nonfairTryAcquire(int acquires) {
           //获取当前线程
            final Thread current = Thread.currentThread();
            // 同步状态c
            int c = getState();
            //如果状态值 c 为 0(表示锁未被占用),尝试通过 CAS 操作将状态设置为 acquires,成功则设置当前线程为独占线程并返回 true
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
           // 如果状态值 c 不为 0 且当前线程已经是独占线程(自己),则更新状态值为 c + acquires,如果发生溢出则抛出异常,否则返回 true。
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

	
	   final boolean acquireQueued(final Node node, int arg) {
	        // 初始化中断标志为 false。
	        boolean interrupted = false;
	        try {
	            for (;;) {
	            	//进入无限循环,获取当前节点的前驱节点 p:
	                final Node p = node.predecessor();
	                //如果前驱节点是头节点且成功获取锁(tryAcquire(arg) 返回 true),则将当前节点设置为头节点,
	                //并清理前驱节点的引用以帮助垃圾回收,返回中断标志
	                if (p == head && tryAcquire(arg)) {
	                    setHead(node);
	                    p.next = null; // help GC
	                    return interrupted;
	                }
	                if (shouldParkAfterFailedAcquire(p, node))
	                    interrupted |= parkAndCheckInterrupt();
	            }
	        } catch (Throwable t) {
	            cancelAcquire(node);
	            if (interrupted)
	                selfInterrupt();
	            throw t;
	        }
	    }
	
	//添加等待队列节点
    private Node addWaiter(Node mode) {
   // 创建一个新节点 node,其模式由参数 mode 指定 这里mode=EXCLUSIVE 独占式
        Node node = new Node(mode);

        for (;;) {
            Node oldTail = tail;
            //如果队列尾部 tail 不为空,设置新节点的前驱为当前尾节点,并通过 CAS 操作更新尾节点。
            if (oldTail != null) {
                node.setPrevRelaxed(oldTail);
                //如果 CAS 成功,将原尾节点的 next 指向新节点并返回新节点
                if (compareAndSetTail(oldTail, node)) {
                    oldTail.next = node;
                    return node;
                }
            } else {
            //如果 tail 为空,初始化同步队列
                initializeSyncQueue();
            }
        }
    }
//使用 HEAD.compareAndSet 方法尝试将头节点从 null 设置为新创建的节点 h,
//如果设置成功,则将尾节点 tail 指向该新节点 h。
  private final void initializeSyncQueue() {
        Node h;
        if (HEAD.compareAndSet(this, null, (h = new Node())))
            tail = h;
    }

// 释放锁
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

跟据源代码生成的流程图
在这里插入图片描述

2 head节点的核心作用

(1)虚拟节点的标识与初始化
虚拟节点设计:head通常指向一个 虚拟节点(dummy node) ,该节点不关联任何线程(thread=null),仅作为队列的起始标识。这种设计简化了队列操作,避免在无竞争时频繁创建和销毁节点。
延迟初始化:队列初始化时,head和tail均为null。当第一个线程竞争资源失败时,AQS通过enq()方法创建一个虚拟节点,并设置head和tail指向它,随后将实际线程节点追加到尾部。
(2)锁持有者的标识
资源持有者标识:当线程成功获取锁(如通过tryAcquire),它会将自身节点设为新的head,并清空其thread和prev字段。此时,原head节点被移出队列,帮助GC回收资源。
唤醒逻辑的起点:释放锁时,AQS从head.next开始唤醒后续节点,确保FIFO顺序。若head.next为空(如节点被取消),则从尾部向前遍历找到有效节点。
(3)线程安全与状态管理
CAS原子操作:head的更新通过compareAndSetHead()方法保证原子性,防止多线程竞争导致队列结构破坏。
状态传递:head节点的waitStatus(如SIGNAL)用于协调后继节点的阻塞与唤醒。例如,当节点状态为SIGNAL时,表示其后继节点需要被唤醒。

3、非公平锁和公平锁的实现方式

其实非公平锁和公平锁的有两处不同

非公平锁

         */
        @ReservedStackAccess
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

公平锁

        @ReservedStackAccess
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    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;
        }
    }

非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值