浅谈对AQS的理解以及ReentrantLock的加锁和解锁源码分析

本文深入剖析了AQS框架中的非公平锁实现原理,包括加锁与解锁过程中的核心方法与状态变化,并详细解读了ReentrantLock的非公平锁源码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 前言:

今天有幸参与群里的AQS加锁、解锁以及同步队列的讨论。然后又撸了一遍源码,顺便写个笔记加深影响。写的不好的话还请各位客官老爷多多包涵。

2. 老生常谈的面试题:谈谈你对AQS的理解?

这个问题很多大佬都有分享自己的见解,但是不同的人表达的东西不一定适合每个人。
我觉得AQS其实就是可以让用户可以更加方便的去实现锁的一个框架。
AQS帮我们屏蔽了一些底层操作,比如:unsafe和locksupport的使用。

3. AQS的核心属性

  1. state:当前共享资源的加锁状态
  2. node:AQS内部类,用来封装加锁失败进入阻塞队列的线程
  3. 同步队列:由node组成双向链表

下面是AQS的源码结构,因为这一篇只讲普通加锁,所以我精简了一部分

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{
	
	static final class Node {
		// waitStatus的值:当前节点(线程)的后续节点处于需要被唤醒的状态
		// 也就是说当waitStatus等于SIGNAL的时候,当前节点还有后续节点
		static final int SIGNAL  = -1;
		
		// 等待状态, Node初始化的时候是0
		volatile int waitStatus;
		
		// 前节点
		volatile Node prev;
		
		// 后节点
		volatile Node next;
	}
	
	// 队列的头节点
	private transient volatile Node head;
	
	// 队列的尾节点
	private transient volatile Node tail;
	
	// 共享资源加锁状态  0:未加锁 1:加锁 大于1:重入
	private volatile int state;
}

4. ReentrantLock非公平加锁源码分析

加锁代码

// 默认构造函数创建的就是非公平锁
ReentrantLock reentrantLock = new ReentrantLock();
reentrantLock.lock();

ReetrantLock的默认构造方法

public class ReentrantLock implements Lock, java.io.Serializable {
	private final Sync sync;
    public ReentrantLock() {
        sync = new NonfairSync();
    }
 }

sync继承AQS,NonfairSync是Sync的一个子类。

4.1 ReentrantLock.lock()

public class ReentrantLock implements Lock, java.io.Serializable {
	public void lock() {
        sync.lock();
    }
}

因为这里创建的sync是 NonfairSync,所以会调用NonfairSync的lock方法

4.2 NonfairSync.lock()

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
		
        final void lock() {
        	// compareAndSetState底层调用的是 unsafe的compareAndSwapInt
        	// 尝试把state从0修改成1, 如果修改成功表明加锁成功。
        	// 并且把拥有锁的线程设置为当前线程: 解锁和判断是否是重入锁需要用到
        	// 这里也提醒了非公平性, 不管三七二十一上来先尝试加锁。
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
            	// 加锁失败
                acquire(1);
        }
		
		// 再次尝试加锁, 以及处理重入锁
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

4.3 acquire(1)

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

这个方法主要做这几件事:

  • 再次尝试加锁、以及处理重入锁
  • 初始化同步队列
  • 将当前线程封装成Node加入到同步队列

4.4 tryAcquire()

tryAcquire最终会调用 NonfairSync的 tryAcquire方法,也就是4.2中的。

        // 再次尝试加锁, 以及处理重入锁,返回false说明加锁失败。
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
		
        final boolean nonfairTryAcquire(int acquires) {
        	// 获取当前线程
            final Thread current = Thread.currentThread();
            // 获取当前共享资源的同步状态
            int c = getState();
            if (c == 0) {
            	// 通过CAS尝试加锁
                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;
        }

4.5 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

4.5.1 addWaiter(Node.EXCLUSIVE), arg)

这个方法主要是初始化队列以及将当前线程封装成node

	// 传入的mode为null
    private Node addWaiter(Node mode) {
    	// 将当前线程封装成node, 此时它的waitStatus是0
        Node node = new Node(Thread.currentThread(), mode);
        // 获取同步队列的尾节点
        Node pred = tail;
        if (pred != null) { //如果尾节点不为null, 说明之前以及初始化过同步队列
        	// 将尾节点设置为当前node的前节点
        	// 如果两个线程同时将尾节点设置为它们的前节点呢? 
        	// 其实是没有问题的, 因为下面的 compareAndSetTail只会成功一个
            node.prev = pred;
            // 将当前node设置为尾节点。如果失败,则会执行enq()去把当前node设置为尾节点
            if (compareAndSetTail(pred, node)) {
            	// 将旧的尾节点的后继节点设置为当前节点
            	// 这里不存在并发问题, 原因同上 compareAndSetTail 只有一个会成功
                pred.next = node;
                return node;
            }
        }
        // 队列未初始化 或者 compareAndSetTail失败
        enq(node);
        return node;
    }
	
	// 初始化队列以及处理并发导致的 compareAndSetTail失败的逻辑
    private Node enq(final Node node) {
        for (;;) {
        	// 获取尾节点
            Node t = tail;
            if (t == null) { // 初始化
            	// 初始化完毕之后会再次进入循环
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            	// 将当前node设置为尾节点,直到成功为止。
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
4.5.2 acquireQueued

这个方法主要是判断当前线程是否需要挂起,以及在挂起之前判断是否可以再一次进行加锁

    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);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
   
   // 判断是否需要把当前线程挂起,需要注意的是外层是自旋。
   private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
   		// 获取前节点的waitStatus, 最开始的时候waitStatus是0
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)      
            return true;
        if (ws > 0) {            
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
        	// 修改前节点的waitStatus为 -1  
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
	
	// 如果 shouldParkAfterFailedAcquire 返回true,则进入该方法
    private final boolean parkAndCheckInterrupt() {
    	// 将当前线程挂起, 等待其他线程将它唤醒。
    	// 唤醒之后, 如果线程没有被打断。将再次执行外层的自旋, 然后尝试加锁
        LockSupport.park(this);
        return Thread.interrupted();
    }

自此,加锁方法分析完毕。

5. 非公平锁解锁源码分析

release()

	// 解锁
    public final boolean release(int arg) {
    	// 尝试解锁
        if (tryRelease(arg)) {
        	// 获取头节点
            Node h = head;
            // 判断是否需要唤醒头节点的next节点
            // 当waitStatus != 0 的时候说明还有后续节点
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

tryRelease()

主要是修改state值, 需要判断是否是重入锁

        protected final boolean tryRelease(int releases) {
        	// 当 c > 0 的时候, 说明当前是重入锁
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
unparkSuccessor()

唤醒头节点的后节点,也就是队列中的第二个节点。

	// 唤醒节点
    private void unparkSuccessor(Node node) {      
        int ws = node.waitStatus;
        if (ws < 0){
            // 通过CAS设置首节点的waitStatus
        	compareAndSetWaitStatus(node, ws, 0);
        }
        // 获取下一个节点        
        Node s = node.next;
        // 当首节点的后继节点被清空,或者取消的时候
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 从队列的尾部开始往前遍历,找到可以被唤醒的线程
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            // 唤醒队列中第二个线程
            LockSupport.unpark(s.thread);
     }

唤醒之后,第二个节点的线程就会继续开始运行,我们先来看下线程是在哪里被挂起的

    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);
                    // 把旧的头节点移除出队列
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }        
                if (shouldParkAfterFailedAcquire(p, node) &&
                	// 线程是在这里被挂起的, 被唤醒之后将继续执行
                	// 因为当前是在一个自旋里, 所以又会执行上面的代码
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

自此,解锁完毕。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值