Java可重入锁:深入探究原理与源码分析

多线程环境中,线程安全问题是一个非常重要的话题。在多线程环境中,一个线程在执行期间可能需要访问共享资源,如果没有同步机制来保证线程间的协作,那么就会出现数据不一致的问题。Java中提供了多种同步机制,如synchronized关键字和ReentrantLock。本文主要介绍ReentrantLock,它是一种可重入锁,支持嵌套加锁。

可重入锁的原理

可重入锁的实现需要考虑两个方面:锁的获取和释放、锁的状态维护。

锁的获取和释放

在ReentrantLock的实现中,通过lock()方法获取锁,通过unlock()方法释放锁。在可重入锁的实现中,需要考虑锁的嵌套问题。如果同一个线程嵌套调用了lock()方法多次,那么也要相应地调用多次unlock()方法,才能完全释放锁。

锁的状态维护

在可重入锁的实现中,需要维护锁的状态,例如:是否已经被某个线程占用,占用线程的数量等。

ReentrantLock的源码分析

ReentrantLock内部维护了一个Sync对象,它是一个抽象类,定义了锁的获取和释放等方法,具体的实现由子类完成。在ReentrantLock的构造方法中,根据是否是公平锁,创建了不同的Sync子类对象。

具体来说,NonfairSync类实现了非公平锁,而FairSync类实现了公平锁。在NonfairSync的lock()方法中,通过CAS操作获取锁,如果当前线程已经持有锁,则将状态值加1,而FairSync的lock()方法则直接将当前线程加入等待队列,不会立即获取锁。

需要注意的是,ReentrantLock是通过继承AbstractQueuedSynchronizer实现的可重入锁,因此,在分析源码时需要对其内部实现进行深入研究。

锁的状态

AbstractQueuedSynchronizer维护了一个state变量,表示锁的状态。如果state为0,表示锁没有被占用,否则表示锁已经被占用。对于可重入锁,state变量还需要维护占用锁的线程数。在每次占用锁时,state变量加1,释放锁时,state变量减1。

锁的等待队列

AbstractQueuedSynchronizer内部维护了一个双向链表作为等待队列,等待队列中的节点是线程。当一个线程请求锁的时候,如果锁已经被占用,那么线程会被加入到等待队列中,并被阻塞。

等待队列中的节点维护了一个prev和next指针,用于构建双向链表,以及一个thread变量,用于表示线程。除此之外,等待队列中的节点还维护了一个waitStatus变量,用于表示节点的状态。等待队列中的节点状态主要有以下几种:

  • CANCELLED(1):表示当前节点已经取消了等待。
  • SIGNAL(-1):表示当前节点的后继节点需要被唤醒。
  • CONDITION(-2):表示当前节点在条件队列中等待。
  • PROPAGATE(-3):表示当前节点需要向后继节点传播唤醒信号。

获取锁

在ReentrantLock的实现中,获取锁的方法是lock()。这个方法会尝试获取锁,如果锁已经被其他线程占用,则当前线程会被阻塞,直到锁被释放。在可重入锁的实现中,需要维护当前线程持有锁的数量,以及持有锁的线程。

在AbstractQueuedSynchronizer中,获取锁的方法是acquire(int arg)。这个方法会尝试获取锁,如果锁已经被其他线程占用,则当前线程会被阻塞,直到锁被释放。在可重入锁的实现中,acquire(int arg)方法需要维护当前线程持有锁的数量,以及持有锁的线程。具体来说,acquire(int arg)方法主要包含以下几个步骤:

  1. 调用tryAcquire(int arg)方法,尝试获取锁。如果获取成功,则直接返回。
  2. 如果获取失败,则将当前线程加入到等待队列中,被阻塞。
  3. 当锁被释放时,唤醒等待队列中的第一个节点,并将其从等待队列中删除。
  4. 被唤醒的节点再次尝试获取锁。如果获取成功,则继续执行,否则被阻塞。

释放锁

在ReentrantLock的实现中,释放锁的方法是unlock()。这个方法会将锁的持有数减1,如果持有数为0,则释放锁。如果当前线程没有持有锁,则抛出异常。

在AbstractQueuedSynchronizer中,释放锁的方法是release(int arg)。这个方法会将锁的状态设置为0,表示锁已经被释放。如果存在等待队列中的线程,则唤醒等待队列中的第一个节点,并将其从等待队列中删除。

在AbstractQueuedSynchronizer中,释放锁的方法是比较简单的,主要是通过compareAndSetState(int expect, int update)方法来设置锁的状态。这个方法会先获取当前锁的状态,如果状态等于expect,则将状态设置为update。

compareAndSetState(int expect, int update)方法使用了CAS(Compare And Swap)操作,保证了并发时的线程安全性。

Condition条件变量

Condition条件变量是在AbstractQueuedSynchronizer基础上构建的,用于实现等待/通知机制。Condition提供了三个方法:

  • await():将当前线程加入到等待队列中,并释放锁。
  • signal():唤醒等待队列中的第一个节点,如果存在的话。
  • signalAll():唤醒等待队列中的所有节点。

在ReentrantLock的实现中,条件变量可以通过newCondition()方法创建,这个方法会返回一个与该锁相关的条件变量。当调用await()方法时,当前线程会被加入到条件变量的等待队列中,并释放锁。当调用signal()方法时,等待队列中的第一个节点会被唤醒,并尝试获取锁。如果获取成功,则继续执行,否则被阻塞。

在AbstractQueuedSynchronizer的实现中,条件变量主要是通过条件队列来实现的。条件队列是一个双向链表,其中的节点是线程。当一个线程调用await()方法时,会将当前线程加入到条件队列中,并将其从等待队列中删除。当调用signal()方法时,会唤醒条件队列中的第一个节点,并将其加入到等待队列中。在唤醒节点的过程中,需要将节点的状态设置为SIGNAL,以便后继节点可以正确地被唤醒。

源码分析

在AbstractQueuedSynchronizer的源码中,锁的获取和释放操作是由acquire(int arg)和release(int arg)方法实现的。这两个方法是ReentrantLock实现中的lock()和unlock()方法的底层实现。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    // 阻塞当前线程并尝试获取锁
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    // 释放锁
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false}

// ...

// 添加一个排他节点到等待队列中
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

// 将节点加入到等待队列中
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // 如果队列为空,则需要初始化头节点和尾节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

// ...

// 唤醒等待队列中的第一个节点,并将其从等待队列中删除
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        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);
}

// ...

// 将节点从等待队列中移除
private void unlinkCancelledWaiters() {
    Node t = tail;
    Node p = t;
    while (p != null && p != head && p.waitStatus != Node.EXCLUSIVE) {
        Node next = p.next;
        if (next != null && next.waitStatus == Node.CANCELLED)
            p.next = next.next;
        else
            p = next;
    }
    if (t != p) {
        compareAndSetTail(t, p);
        p.next = null;
    }
}

// ...
}

从源码中可以看出,AbstractQueuedSynchronizer是通过一个双向链表来维护等待队列和条件队列的。在获取锁时,线程会被加入到等待队列中。在释放锁时,如果存在等待队列中的线程,则会将其中的一个线程从等待队列中唤醒,然后将其加入到条件队列中。

ReentrantLock的实现中,锁的获取和释放是通过调用ReentrantLock.SynctryAcquire(int acquires)tryRelease(int releases)方法实现的。ReentrantLock.SyncAbstractQueuedSynchronizer的子类,其中的tryAcquire(int acquires)tryRelease(int releases)方法是需要由ReentrantLock实现的。
ReentrantLock.Sync的源码如下所示:

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = -5179523762034025860L;

    abstract void lock();

    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)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

    protected final boolean tryRelease(int releases) {
        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;
    }

    protected final boolean isHeldExclusively() {
        return getExclusiveOwnerThread() == Thread.currentThread();
    }

    final ConditionObject newCondition() {
        return new ConditionObject();
    }

    final Thread getOwner() {
        return getState() == 0 ? null : getExclusiveOwnerThread();
    }

    final int getHoldCount() {
        return isHeldExclusively() ? getState() : 0;
    }

    final boolean isLocked() {
        return getState() != 0;
    }

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        setState(0); 
    }
}

在ReentrantLock中,锁的实现是通过继承AbstractQueuedSynchronizer(AQS)来实现的。ReentrantLock.Sync继承自AQS,并重写了几个方法。

  • lock()方法:由具体的子类实现,负责获取锁。
  • nonfairTryAcquire(int acquires)方法:尝试获取非公平锁。如果当前锁没有被占用,则直接获取锁并将当前线程设置为持有锁的线程。否则,如果当前线程已经持有锁,则将锁的获取次数加1。如果当前线程没有持有锁,则获取锁失败。
  • tryRelease(int releases)方法:释放锁。如果当前线程没有持有锁,则抛出IllegalMonitorStateException异常。如果当前线程持有锁,则将锁的持有线程和获取次数恢复到先前的状态,并将等待队列中的线程唤醒。
  • isHeldExclusively()方法:判断当前线程是否持有锁。
  • newCondition()方法:返回一个与当前锁绑定的条件对象。
  • getOwner()方法:获取当前持有锁的线程。
  • getHoldCount()方法:获取当前线程持有锁的次数。
  • isLocked()方法:判断当前锁是否被持有。

从源码中可以看出,在tryAcquire(int acquires)方法中,当获取锁成功时,会将当前线程设置为持有锁的线程,并将锁的获取次数加1。当释放锁时,tryRelease(int releases)方法会将锁的持有线程和获取次数恢复到先前的状态,并将等待队列中的线程唤醒。

需要注意的是,ReentrantLock是可重入锁,也就是说,同一个线程在持有锁的情况下可以多次获取锁,而且释放锁的次数也需要和获取锁的次数相同。在ReentrantLock中,这个状态信息被存储在Sync的ThreadLocalHoldCounter中。

总结

可重入锁是多线程编程中常用的一种同步工具,可以有效地防止死锁的发生,并提高程序的并发性能。在 Java 中,ReentrantLock是实现可重入锁的一种方式,其内部通过AbstractQueuedSynchronizer来实现锁的等待和唤醒操作。通过对ReentrantLock的源码分析,我们可以更深入地理解可重入锁的原理和实现。同时,了解可重入锁的原理和实现也有助于我们更好地设计和调试多线程程序。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值