ReentrantLock详说

文章详细分析了Java并发包中的ReentrantLock实现,特别是公平锁的获取与释放过程。通过tryAcquire方法尝试获取锁,如果失败则会通过acquireQueued方法入队并等待。在入队过程中,会检查节点状态并可能改变waitStatus以允许被其他线程唤醒。当锁释放时,会检查头节点状态,将其重置以便唤醒等待的线程。整个过程涉及CAS操作以保证线程安全。

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

目录

尝试加锁

如果加锁不成功

重点


尝试加锁

最外层lock方法

ReentrantLock.class

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

进来发现是个抽象方法 

abstract static class Sync extends AbstractQueuedSynchronizer

abstract void lock();

底下有两个实现类,一个实现的公平锁,另一个为非公平锁 

我们看一下公平锁吧

static final class FairSync extends Sync

这里面可以看到入参为1 

final void lock() {
            acquire(1);
        }

AbstractQueuedSynchronizer

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&  // 这个是尝试去获取锁
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

我们先去看怎么去获取锁的

protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

列一下实现类

cas我理解就是通过线程的状态值和主内存中的状态值进行比较,如果相同则修改,如果不同则修改失败

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread(); // 获取当前线程
            int c = getState(); // 获取state的值
            if (c == 0) { // 为0的话,说明还没有线程持有这把锁
                // 因为你是公平锁,不能上去就去抢,要先去排队
                if (!hasQueuedPredecessors() && // 判断是否需要排队
                    compareAndSetState(0, acquires)) { // 如果不需要排队就尝试通过cas获取锁
                    setExclusiveOwnerThread(current); //获取成功,那么该锁归这个线程所有
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) { // 如果已经被加锁,但是还是这个线程想去获取锁,那么则进行+1操作,然后获取锁成功
                // 如果进入到这个if,只能只有线程进来,不存在并发,不需要通过cas进行修改
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

看一下是否需要排队的方法

  • 就是看头节点和尾节点是否相同,相同肯定就为空,还有些其他判断
public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

如果加锁不成功

就需要尝试入队了,走 && 另一个方法了

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

这边新建了一个互斥模式的节点,有互斥就会有共享模式,共享模式后面再说

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

节点内比较重要的属性:

  • waitStatus:这个是代表节点的生命状态,新建的节点这个值就为0
    • signal,cancelled,condition,propagate,0
  • pre、next:这两个就是双向链表的前后指针
  • thread:所有需要阻塞的线程肯定需要保存在某个位置等待唤醒

因为刚开始前后节点都为空,addwaiter方法里面第一个判断是不走的,走enq方法

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node())) // 这里面就是把head指向新建的node
                    // tail指向新建的node
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

这里面能看到,是个空循环,就是为了让拿不到锁的线程入队成功,开始走第一个逻辑就是为了初始化,设置一个空节点,反正这里面干了下图这个事,怎么干的我也不知道(我知道了,就是新建的节点作为head,tail = head,就是tail也是指向新建的节点)

看第二个判断就是初始化之后走这个判断,t就是新建的那个节点,初始化的节点,那个cas方法就是为了让tail指向node,最后把前驱节点进行返回,没什么用

 接着回到这个方法,里面入参那是包裹着这个线程的节点以及1        

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
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);
        }
    }

这里面可以看一下setHead方法,就是相当于将该节点置空变为前驱节点(就是之前初始化的空节点)

private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

如果不是头节点或者没有获取锁成功,那么就走下面的这个逻辑

shouldParkAfterFailedAcquire

需要去获取前驱节点的waitStatus的状态,如果为0,就需要变为-1,变为一个可以被唤醒的状态

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

然后被阻塞住

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

重点

acquireQueued(Node(currentThread), arg)
    节点阻塞之前还得再尝试一次获取锁:
    1,能够获取到,节点出队,并且把head往后挪一个节点,新的头结点就是当前节点
    2、不能获取到,阻塞等待被唤醒
        1.首先第1轮循环、修改head的状态,修改成sinal=-1标记处可以被唤醒.
        2.第2轮循环,阻塞线程,并且需要判断线程是否是有中断信号唤醒的!
        shouldParkAfterFailedAcquire(p, node)
    waitestate = 0 - > -1 head节点为什么改到-1,因为持有锁的线程T0在释放锁的时候,得判断head节点的waitestate是否!=0,如果!=0成立,会再把waitstate = -1->0,要想唤醒排队的第一个线程T1,T1被唤醒再接着走循环,去抢锁,可能会再失败(在非公平锁场景下),此时可能有线程T3持有了锁!T1可能再次被阻塞,head的节点状态需要再一次经历两轮循环:waitState = 0 -> -1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值