Java--ReentrantLock

本文详细剖析了ReentrantLock的工作原理,包括非公平锁与公平锁的区别、内部类Sync的作用及其实现细节、lock方法的具体流程,以及如何通过AbstractQueuedSynchronizer实现线程间的同步。

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

ReentrantLock

Lock定义了锁的接口规范。
ReentrantLock实现了Lock接口。
AbstractQueuedSynchronizer中以队列的形式实现线程之间的同步。
ReentrantLock的方法都依赖于AbstractQueuedSynchronizer的实现。
ReentrantLock一共有3个内部类,Sync、NonfairSync、FairSync,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。
这里写图片描述

1、lock()方法的实现

这里写图片描述
首先,进入lock()方法是调用sync.lock()

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

sync是ReentrantLock在构造函数中实现的:当传入boolean参数是true时,实现公平锁;无参数和false时候,实现非公平锁;我们分析非公平锁代码。
非公平锁:当锁处于无线程占有的状态,此时其他线程和在队列中等待的线程都可以抢占该锁。
公平锁:当锁处于无线程占有的状态,在其他线程抢占该锁的时候,都需要先进入队列中等待。

    public ReentrantLock() {
        sync = new NonfairSync();
    }

    public ReentrantLock(boolean fair) {
        sync = (fair)? new FairSync() : new NonfairSync();
    }

NonfairSync继承自Sync,因此也继承了AbstractQueuedSynchronizer中的所有方法实现。利用CAS判断锁是否被占用,如果没占用,把状态设为1,并设置当前线程为该锁的独占线程;如果设置失败执行acquire(1);方法把线程入队。

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

    }

下面代码主要完成了同步状态获取、节点构造、加入同步队列以及在同步队列中自旋等待的相关工作,其主要逻辑是:首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。

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

  1. 第一步。尝试调用tryAcquire()去获取锁。如果尝试获取锁成功,方法直接返回。如果获得锁失败,则将当前线程加入到CLH队列中进行等待。
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 获得状态
            int c = getState();
            // 如果状态为0,则表示该锁未被其他线程占有
            if (c == 0) {
                // 此时要再次利用cas去尝试占有锁
                if (compareAndSetState(0, acquires)) {
                    // 标记当前线程为锁拥有者
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 如果当前线程已经占有了,则state + 1,记录占有次数
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                // 此时无需利用cas去赋值,因为该锁肯定被当前线程占有
                setState(nextc);
                return true;
            }
            return false;
        }

在nonfairTryAcquire()中,首先会去获得锁的状态,如果为0,则表示锁未被其他线程占有,此时会利用cas去尝试将锁的状态置位,并标记当前线程为锁拥有者;如果锁的状态大于0,则会判断锁是否被当前线程占有,如果是,则state + 1,这也是为什么lock()的次数要和unlock()次数对等;如果占有锁失败,则返回false。
在nonfairTryAcquire()返回false的情况下,会继续调用acquireQueued(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;
            }
        }
        // 尾节点为空,说明队列还未初始化,需要初始化head节点并入队新节点
        enq(node);
        return node;
    }

    private Node enq(final Node node) {
        // CAS方法有可能失败,因此要循环调用,直到当前线程的节点加入到队列中
        for (;;) {
            Node t = tail;
            
            if (t == null) { // Must initialize
                Node h = new Node(); // Dummy header,头节点为虚拟节点
                h.next = node;
                node.prev = h;
                    if (compareAndSetHead(h)) {
                    tail = node;  
                    return h;
                }
            }
            else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

addWaiter()是AbstactQueuedSynchronizer的方法,会以节点的形式来标记当前线程,并加入到尾节点中。enq()方法是在节点加入到尾节点失败的情况下,通过for(;;)循环反复调用cas方法,直到节点加入成功。接着调用acquireQueued()方法去尝试获得锁。

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true; //标记是否成功获取锁
    try {
        boolean interrupted = false; //标记线程是否被中断过
        for (;;) {
            final Node p = node.predecessor(); //获取前驱节点
            //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
            if (p == head && tryAcquire(arg)) {
                setHead(node); // 获取成功,将当前节点设置为head节点
                p.next = null; // 原head节点出队,在某个时间点被GC回收
                failed = false; //获取成功
                return interrupted; //返回是否被中断过
            }
            // 判断获取失败后是否可以挂起,若可以则挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                // 线程若被中断,设置interrupted为true
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

看下shouldParkAfterFailedAcquire和parkAndCheckInterrupt

/**
 * 判断当前线程获取锁失败之后是否需要挂起.
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //前驱节点的状态
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // 前驱节点状态为signal,返回true
        return true;
    // 前驱节点状态为CANCELLED
    if (ws > 0) {
        // 从队尾向前寻找第一个状态不为CANCELLED的节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 将前驱节点的状态设置为SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
  
/**
 * 挂起当前线程,返回线程中断状态并重置
 */
private final boolean parkAndCheckInterrupt() {
	// 调用park()使线程进入waiting状态
    LockSupport.park(this);
    // 如果被唤醒,查看自己是不是被中断的。
    return Thread.interrupted();
}

线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,它的含义是“Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!”。所以shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,若符合则返回true,然后调用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱节点是否>0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态设置为SIGNAL。
整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下看有没有机会去尝试竞争锁。

释放锁 unlock();

public void unlock() {
    sync.release(1);
}
  
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,如果释放失败那么返回false表示解锁失败。

/**
 * 释放当前线程占用的锁
 * @param releases
 * @return 是否释放成功
 */
protected final boolean tryRelease(int releases) {
    // 计算释放后state值
    int c = getState() - releases;
    // 如果不是当前线程占用锁,那么抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        // 锁被重入次数为0,表示释放成功
        free = true;
        // 清空独占线程
        setExclusiveOwnerThread(null);
    }
    // 更新state值
    setState(c);
    return free;
}

tryRelease的过程为:当前释放锁的线程若不持有锁,则抛出异常。若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,并且则清空独占线程,最后更新state值,返回free
AbstractQueuedSynchronizer中的Condition配合ReentrantLock使用,实现了wait/notify的功能。

总结一下lock方法
这里写图片描述

  • 首先用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能乖乖的去排队啦。
  • 第二步,入队。如果队列未初始化会使用自旋+CAS方式一直到入队成功。
  • 第三步,挂起。让已经入队的线程尝试获取锁,若失败则会被挂起。

个人总结
1、lock()方法调用sync.lock();
2、非公平锁,CAS判断锁状态,为占用设成1,失败执行tryAcquire方法;
3、tryAcquire方法,获取锁,成功直接返回,失败构造节点,入队;
4、addWaiter方法,构造节点,如果队列为初始化,先初始化,for死循环把节点加入队列
5、acquireQueued方法,当前节点前驱是头节点,获取锁,否则挂起;
6、selfInterrupt 中断当前线程的

独占锁和共享锁区别:
与AQS的独占功能不同,当锁被头节点获取后,独占功能是只有头节点获取锁,其余节点的线程继续沉睡,等待锁被释放后,才会唤醒下一个节点的线程,而共享功能是只要头节点获取锁成功,就在唤醒自身节点对应的线程的同时,继续唤醒AQS队列中的下一个节点的线程,每个节点在唤醒自身的同时还会唤醒下一个节点对应的线程,以实现共享状态的“向后传播”,从而实现共享功能。

共享锁:共享功能的主要实现为CountDownLatch

资料 https://blog.youkuaiyun.com/pfnie/article/details/53191892

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值