ReentrantLock原理分析

本文深入探讨了Java ReentrantLock的工作原理,包括公平锁和非公平锁的实现方式。非公平锁在首次尝试获取锁时可能会进行两次CAS操作,而公平锁会检查是否有前驱线程,确保线程的公平竞争。尽管非公平锁在初次获取时存在不公平现象,但后续进入阻塞队列的线程在被唤醒后,它们之间的竞争是公平的。这种设计兼顾了性能与避免线程饿死的问题。

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


概述

可重入的自选锁,支持公平和非公平两种模式

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

Sync的实现有NonfairSync 和 FairSync
各自实现 lock 和 tryAcquire来实现不同的加锁机制

加锁原理

非公平锁

使用无参构造的默认就是非公平锁

public ReentrantLock() {
        sync = new NonfairSync();
    }
 static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        @ReservedStackAccess
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
 @ReservedStackAccess
        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;
        }

先看最外层的lock,其实if中的cas代码和nonfairTryAcquire中的开头是一样的。也就是说一个线程首次尝试获取非公平锁的时候其实是尝试了两次。并非一次失败后就阻塞。

nonfairTryAcquire中忽略了CLH队列的状态,直接通过CAS尝试获取锁,因此是非公平的。

公平锁

 static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

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

        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        @ReservedStackAccess
        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;
        }
    }

和公平锁最大的区别在于hasQueuedPredecessors,
这个方法限制了如果当前线程不是排队的首位就直接失败,继续进入阻塞队列

非公平锁真的不公平吗

先说结论:线程的首次获取锁是非公平的,但对于进入阻塞队列的线程来说,他们之间又是公平的。
事实上,每次争抢锁的参赛者是所有首次获取锁的线程(准确的说是首两次,原因上文有说)和唯一的一个被唤醒的线程。

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

无论是公平锁还是非公平锁,在尝试获取锁失败后,最终都会调用acquireQueued方法。

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

acquireQueued的方法大致分为以下几步
1.如果当前线程是头节点后的节点(可叫做预备节点),则尝试加锁
2. 如果1中加锁失败,则阻塞,等待被唤醒

而唤醒的逻辑在unlock中,每次只能唤醒一个线程

private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        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);
    }

因此,即使是非公平锁,一旦被阻塞,那么阻塞中的那些线程互相之间的竞争关系也是公平的。
关于这样设计的原因猜想:
设计者想在保证非公平锁的性能同时尽量减少线程饿死的场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值