AQS公平锁与非公平锁之源码解析-AQS加锁逻辑

ReentrantLock.lock

    public void lock() {
        sync.acquire(1);
    }

AbstractQueuedSynchronizer#acquire

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

addWaiter就是将节点加入队列的尾部,我们先看看非公平锁NonfairSynctryAcquire

	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;
	}
  • 可以看到是尝试cas获取锁,获取到了将当前线程设置为持有锁的线程
  • 在AQS中,有一个STATE变量,当为1时表示该锁被占用,所以cas的是这个status值
  • 如果cas失败,就会回到acquire方法,继续调用acquireQueued

公平锁fairSynctryAcquire

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;
        }
  • 和非公平锁的区别:就是在tryAuquire时会先进行hasQueuedPredecessors,即判断当前是否有节点在队列里,有的话不参与cas竞争,实际上后续的解锁和唤醒操作,对于是否公平都是一样,只有这里体现了公平与非公平的区别,对于公平锁,当解锁唤醒队列中的节点时,此时新的获取锁的请求不会与队列中的节点竞争,保证队列中的节点优先唤醒,即保证了FIFO

AbstractQueuedSynchronizer#acquireQueued

![[Pasted image 20250121170956.png]]

  1. 再一次调用tryAcquire这个方法,尝试获取锁,获取成功后将该节点设置为头节点,两个结论:1. cas失败两次会进行阻塞,2. 链表中持有锁的节点就是头节点
  2. 如果失败就会进入shouldParkAfterFailedAcquire
   private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        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 {
            pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
        }
        return false;
    }

![[Pasted image 20250121171505.png]]

  • 五个状态:
状态 作用
CANCELLED 1 表示该节点已经取消等待,不再参与锁竞争
SIGNAL -1 表示后续节点需要被唤醒
CONDITION -2 该节点在等待条件队列中
PROPAGATE -3 共享模式下传播信号,让后续线程继续执行
默认值 0 节点刚入队列时的状态,当节点是队尾,状态就是0
  • 回到shouldParkAfterFailedAcquire方法,当前驱节点状态时SINGAL时,说明前驱节点将锁释放了,将唤醒当前节点(唤醒意味着该节点重新参与竞争锁,因为如果是非公平锁仍需要竞争),当前的线程不应该park,应该回到前面的for循环继续tryacquire
  • 如果状态大于0,说明前驱节点已经cancel,这个节点应该移除链表,可以看到这里的while会将其移除链表
  • 如果状态此时小于0等于了,说明这个前驱节点是正常的,将其设置为SINGAL状态,意为下次会唤醒当前节点
  1. parkAndCheckInterrupt,这里就真正进行阻塞了,所以当前驱节点唤醒当前节点时,回到这个位置,重新开始for循环,acquire尝试获取锁

park与sleep的区别:可以被另一个线程调用LockSupport.unpark()方法唤醒;线程的状态和wait调用一样,都是进入WAITING状态
park与wait的区别:wait必须在synchronized里面,且唤醒的是随机,而park是消耗许可,unpark是颁发许可,可以提前unpark,park也会一次性消耗所有许可

总结

我们举一个例子:

		ReentrantLock reentrantLock = new ReentrantLock();
        new Thread(new Runnable() {
            @Override
            public void run() {
                reentrantLock.lock();
            }
        }, "Thread-A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                reentrantLock.lock();
            }
        },"Thread-B").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                reentrantLock.lock();
            }
        },"Thread-C").start();
  • 以上是三个线程尝试加锁,当然是只有第一个线程获取锁,调试结果如下,可以看到Thread-A即持有锁的线程在sync的属性里,而链表的头节点不记录线程信息但是状态为SIGNAL,而Thread-B的状态也为SINGAL,其后继节点Thread-C的状态就是默认状态0
    ![[Pasted image 20250121175854.png]]
上一篇:物联网网关Web服务器--CGI开发实例BMI计算


下一篇:如何迅速并识别处理MDL锁阻塞问题