[JUC] 通过ReentrantLock源码理解AQS的原理 (二)

四、通过ReentrantLock理解AQS原理

2. ReentrantLock加锁机制:

在上篇文章学习到了

- 创建ReentrantLock对象

- 初次加锁的逻辑

- 同一线程第二次加锁 (可重入性)的逻辑。

现在我们回到我们最初的代码。

public class ReentrantLockDemo1 {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "尝试获取锁");
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "获取锁成功,开始执行!");
        }, "Thread A");

        Thread threadB = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "尝试获取锁");
            lock.lock();
            System.out.println(Thread.currentThread().getName() + "获取锁成功,开始执行!");
        }, "Thread B");

        threadA.start();
        TimeUnit.SECONDS.sleep(2);
        threadB.start();
    }
}

当线程A第一次进入获取到了锁。这个时候线程B在A未释放 资源之前是无法获取到锁的。那么AQS又是怎么做的?

3. 不同线程锁的竞争

此时,我们关注的重点是在线程B调用ReentrantLock的lock方法,底层到底是怎么做的。

此时state是1,所以同样会走NonfairSync非公平锁的tryAcquire(1)方法。

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;
        }

此时,既不是锁持有线程,state又不是0。所以直接返回false,而返回false后。

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

因为判断第一段为true,所以首先会进入addWaiter(Node.EXCLUSIVE), arg)方法。

private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

此时终于等到我们的双向队列出场了。首先新创建了一个node,并将当前线程放入,mode为EXCLUSIVE的模式,因为传入的是EXCLUSIVE,所以为独占锁。即只允许一个线程访问。看看接下来的逻辑:

  1. 将尾部节点tail赋值给上一个节点pred(此时尾节点为空,所以pred节点也为空)
  2. (因为pred节点为空,跳过非空判断体)执行enq(node)。传入以当前想要获取锁的线程为基础的Node对象。

我们来看看enq方法内部:

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

终于第一次在相关代码中看到自旋的概念。此时enq就是执行一个自旋的过程。

  1. 第一次进入因为tail是空的,所以Node t也是空。
  2. 如果是空的,则执行compareAndSetHead原子操作将一个新的空Node存到双向队列的头部。并且将tail=head,尾部即位头部。
  3. 第二次自旋进入。t不为空,他已经是一个空的Node节点。将传入的node(带有想要获得锁线程的Node)的前一个位置prev填入这个空节点。执行原子操作当尾部等于t的时候,改尾部为传入的node。然后将t的下一位next改为传入的node。返回t这个Node,跳出自旋。

可见,只有当t节点的尾部被改为想要获得锁线程的Node的时候,自旋才会退出。

最终enq方法执行完成,返回了一个当前线程为想要获取锁的线程ThreadB,并且拥有一个空的prev节点的Node。最终addWaiter方法获取到一个node并作为参数传入到acquireQueued方法中。该方法是acquire方法第判断体第二段。

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

我们来看这个方法:

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);
        }
    }

遇见的第二个自旋就在该方法中。我们传入了一个拥有当前想要获取锁ThreadB的Node。并且1作为第二个参数。看看他的逻辑step:

  • 获取当前节点的上一个节点(上一个节点为空的头部节点)
  • 再次调用tryAcquire(1)去尝试获取锁,(因为线程A未释放,不可能获取到)
  • 将上一个节点和当前节点传入到shouldParkAfterFailedAcquire做第一个判断。

当前主要判断上一个节点状态waitStatus是否为-1(SIGNAL) 该状态为需要unpark释放。

    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 {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
  • 可惜判断到上个节点waitStatus为0,执行compareAndSetWaitStatus设置上个节点的waitStatus为-1。(上个节点本来为空节点,所以需要标记为unpark)。返回false
  • 继续回到acquireQueued中自旋。又到shouldParkAfterFailedAcquire做判断,发现上个节点的waitStatus已经是-1.直接返回true。
  • 开始执行acquireQueued判断体第二部分parkAndCheckInterrupt,非常简单粗暴。使用LockSupport的park,将当前非公平锁传入作为blocker对象。
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

至此,结束线程B的求锁之路。

如果线程A此时释放了锁,会调用unpark方法唤醒该线程。线程又返回到acquireQueued中自旋,尝试调用tryAcquire(1)方法去获得锁。如果获得成功,则将该节点设置为head节点。双向队列中等待的线程依次继续尝试获取锁,只是当前线程获得做执行了,所以队列中需要把他设置为head节点供后续节点等待。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值