ReentrantLock 核心原理

本文详细介绍了Java中的公平锁与非公平锁的概念和原理,特别是在ReentrantLock中的实现。公平锁确保线程按等待顺序获取锁,而非公平锁则允许线程尝试插队。ReentrantLock通过构造函数可以选择公平或非公平模式。此外,文章还讨论了可中断锁与不可中断锁的区别,以及可重入锁的实现机制,强调了ReentrantLock的可重入性和线程安全特性。
摘要由CSDN通过智能技术生成


Lock 接口源码如下:

public interface Lock {

	// 加锁,没有获得锁的线程会到同步队列中阻塞排队
    void lock();
    
    // 加锁(可中断):当获得锁的线程被中断时,中断异常将会抛出,同时锁会被释放
    void lockInterruptibly() throws InterruptedException;
    
    // 尝试获得锁,如果锁没有被占用,则返回true,否则返回false
    boolean tryLock();

	// 带有超时等待时间的锁,如果超时时间到了,依旧没有获得锁的话,返回 false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    
    // 解锁
    void unlock();
	
	// 得到新的Condition
    Condition newCondition();
}

1. 公平锁与非公平锁

1.1 公平锁原理

公平锁的核心就是对争抢锁的所有线程都是公平的,在多线程并发环境中,每个线程在抢占锁的过程中,都会首先检查锁维护的等待队列。如果等待队列为空,或者当前线程为等待队列中的第一个线程,则当前线程会获取到锁,否则,当前线程会加入等待队列的尾部,然后队列中的线程会按照先进先出的规则按顺序获取锁资源。
image.png

1.2 ReentrantLock 中的公平锁

ReentrantLock 叫做可重入互斥锁。
可重入的意思是同一个线程可以对同一个共享资源重复的加锁或释放锁,互斥就是 AQS 中的排它锁的意思,只允许一个线程获得锁。
ReentrantLock 类本身实现了 Lock 接口。

ReentrantLock 实现了公平锁的机制,在类中,提供了一个带有 boolean 类型参数的构造方法:

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

**在这个构造方法中,如果传入的参数为 true,则会创建一个 FairSync 对象并复制给成员变量 sync,此时线程获取的锁就是公平锁。**FairSync 是 ReentrantLock 类中提供的一个表示公平锁的静态内部类。源码如下所示:

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

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

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

FairSync 类中的 lock 方法会调用 AQS 的 acquire 方法,AQS 的 acquire 方法又会调用 tryAcquire 方法,而 AQS 中的 tryAcquire 方法实际上是基于子类实现的,因此,此时调用的还是 FairSync 类的方法。原因是 FairSync 类继承了 Sync 类,而 Sync 类直接继承了 AQS。(这个地方有些绕,建议大家自己打开 IDE,具体看一下方法之间是如何进行调用的)
Sync 类是 ReentrantLock 类中的一个静态抽象内部类,源码如下:

    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) // overflow
                    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); // reset to unlocked state
        }
    }

回到 FairSync 类中,FairSync 类的 tryAcquire 方法会通过 hasQueuedPredecessors 方法判断队列中是否存在后继节点,如果队列中存在后继节点,并且当前线程未占用锁资源,则 tryAcquire 方法会返回 false,当前线程会进入等待队列的尾部进行排队。流程如下:
image.png

1.3 非公平锁原理

非公平锁的核心就是对抢占锁的所有线程都是不公平的,在多线程并发环境中,每个线程在抢占锁的过程中都会先直接尝试抢占锁,如果抢占锁成功,就继续执行程序的业务逻辑,如果抢占失败,就会进入等待队列中排队。
公平锁与非公平锁的核心区别就在于排队的处理上,非公平锁在队列的队首位置可以进行一次插队操作,插队成功就可以获取到锁,插队失败就会像公平锁一样进入等待队列排队。在非公平锁模式下,可能出现某个线程在队列中等待时间过长而一直无法获取到锁的现象,这种现象叫作饥饿效应。
虽然非公平锁会产生饥饿效应,但是非公平锁比公平锁性能更优。
image.png

1.4 ReentrantLock 中的非公平锁

ReentrantLock 中默认实现的就是非公平锁,调用无参构造方法创建的对象就是非公平锁:

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

也可以调用有参构造,传入 false 参数创建非公平锁:

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

无论使用上面哪两种方式,都会创建一个 NonfairSync 类的对象并赋值给 ReentrantLock 类的成员变量 sync,此时创建的就是非公平锁。
NonfairSync 类也是一个静态内部类,源码如下:

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

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

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

从上面的代码,我们可以看到,在非公平锁的加锁逻辑中,并没有直接将线程放入等待队列的尾部,而是先尝试将当前线程插入等待队列的头部,也就是先尝试获取锁资源。如果获取锁资源成功,则继续执行程序的业务逻辑。如果获取锁资源失败,则调用 AQS 的 acquire 方法获取锁资源。
同样,NofairSync 类继承了 Sync 类,而 Sync 类继承了 AQS ,所以,NonfairSync 类在加锁流程上的本质上,还是调用了 AQS 的入队出队操作。
AQS 的 acquire 方法会回调 NonfairSync 类中的 tryAcquire 方法,而在 NonfairSync 类的 tryAcquire 方法中,又会调用 Sync 类中的 nonfairTryAcquire 方法尝试获取锁,nonfairTryAcquire 方法源码如下:

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

同样,这块的逻辑,还是建议大家 IDE 一步一步看。
ReentrantLock 中非公平锁的加锁流程中方法调用的逻辑如图所示:
image.png

2. 可中断锁与不可中断锁

可中断锁是指多个线程抢占的过程中可以被中断的锁,不可中断锁指在多个线程抢占的过程中不可以被中断的锁。

2.1 可中断锁原理

ReentrantLock 就是可中断锁,支持在抢占锁的过程中中断锁。
在 Java 提供的 Lock 接口中,有两个方法抛出了 InterruptedException 异常,如下所示:

void lockInterruptibly() throws InterruptedException;

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

这两个方法在加锁的过程中可以中断锁。具体的中断逻辑如下:

  1. lockInterruptibly 方法的中断逻辑:在抢占锁的过程中会处理由 Thread 类中的 interrupt 方法发出的中断信号,如果当前线程在抢占锁的过程中被中断,就会抛出异常并终止抢占锁的过程。
  2. tryLock(long time, TimeUnit unit) 方法中的中断逻辑:尝试在指定的时间内阻塞式的抢占锁,在抢占锁的过程中会处理由 Thread 类中的 interrupt 方法发出的中断信号,如果当前线程在抢占锁的过程中被中断,就会抛出异常并终止抢占锁的过程。

2.2 不可中断锁原理

不可中断锁指线程在抢占锁的过程中不能被中断。也就是说,线程在抢占不可中断锁时,如果抢占成功,则继续执行业务逻辑;如果抢占失败,则阻塞挂起。线程在阻塞挂起的过程中,不能被中断。synchronized 锁就是不可中断锁。

3. 可重入锁

可重入锁指一个线程可以反复对相同的资源加锁。
可重入锁表示一个线程能够对相同的资源重复加锁。也就是说,同一个线程能够多次进入使用同一个修饰锁的代码块,但需要注意的是,释放锁的次数需要与加锁的次数相同,才能保证真正释放了锁,伪代码如下:

        try {
            lock.lock();
            lock.lock();
            lock.lock();
            // ....
        } finally {
            lock.unlock();
            lock.unlock();
            lock.unlock();
        }

ReentrantLock 的内部类 Sync 的 nonfairTryAcquire 方法中,如下代码是 ReentrantLock 实现可重入锁的关键代码:

        	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;

在上述代码中,在当前线程已经占有锁时,会判断当前线程是否是已经获取过锁的线程,则增加内部的状态计数,以此实现锁的可重入性。
当使用 ReentrantLock 对象解锁时,会先调用 AQS 的 relese 方法,而 AQS 的 release 方法又会调用 ReentrantLock 的内部类 Sync 的 tryRelease 方法,ReentrantLock 的内部类 Sync 的 tryRelease 源码如下:

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

在 ReentrantLock 的内部类 Sync 的 tryRelease 方法中,首先对状态计数减去传入的值,判断当前线程是否是已经获取到锁的线程,如果当前线程不是已经获取到锁的线程,则直接抛出 IllegalMonitorStateException 异常。然后定义一个是否成功释放锁的变量 free,默认值为 false。接下来,判断 state 状态计数的值是否减为 0。如果 state 状态计数的值已经减为 0,则说明当前线程已经完全释放锁,此时的锁处于空闲状态,将是否成功释放锁的变量 free 设置为 true,并将当前拥有锁的线程设置为 null。最后设置锁的状态标识,返回 free,结果会返回 true。
如果 state 状态计数的值没有减为 0,则说明当前线程并没有完全释放锁,此时的 free 变量为 false,返回 free,结果返回 false。
所以,在 ReentrantLock 中,可重入锁的加锁操作会累加状态计数,解锁操作会累减状态计数。Java 中的 synchronized 和 ReentrantLock 锁都实现了可重入性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SuZhan7710

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值