ReentrantLock(公平锁、非公平锁)可重入锁原理


基本使用

ReentrantLock,位于java.util.concurrent包,于JDK1.5引入,一种可重入互斥Lock ,其基本行为和语义与使用synchronized方法和语句访问的隐式监视器锁相同,但具有扩展功能。
ReentrantLock的使用也很简单,在源码注释中可以看到使用的推荐方式:

public void m() {
    lock.lock();  // block until condition holds
    try {
        // ... method body
    } finally {
        lock.unlock()
    }       
}

具体使用代码如下:

package com.starsray.test.lock;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;

public class TestReentrantLock {
    private final static ReentrantLock lock = new ReentrantLock();
    
    
    static int threadCount = 5;
    static int counter = 0;
    static int threshold = 1000;
    private static final CountDownLatch latch = new CountDownLatch(threadCount);

    public static void m() {
        lock.lock();
        try {
            counter++;
        } finally {
            lock.unlock();
        }
    }
    
    
    public static void main(String[] args) throws InterruptedException {
        
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                for (int j = 0; j < threshold; j++) {
                    m();
                }
                latch.countDown();
            }, "Thread-" + i).start();
        }
        latch.await();
        System.out.println("count:" + counter);
    }
}

源码分析

ReentrantLock由上次成功锁定但尚未解锁的线程拥有。当锁不被另一个线程拥有时,调用lock的线程将返回,成功获取锁。如果当前线程已经拥有锁,该方法将立即返回。可以使用方法isHeldByCurrentThreadgetHoldCount来检查。
ReentrantLock除了支持synchronized的语义之外,还支持公平锁和非公平锁的拓展,在非公平锁模式下与synchronized相同,也是其默认的锁模式。构造函数接受一个可选的公平参数,当设置为true时表示为公平锁。
ReentrantLock基于AQS实现,内部维护了一个CLH队列,所谓公平非公平,指的是线程是否按照入队的顺序进行锁的获取。需要注意的是,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的许多线程之一可能会连续多次获得它,而其他活动线程没有进展并且当前没有持有锁。另请注意,不定时的tryLock()方法不遵守公平设置。如果锁可用,即使其他线程正在等待,它也会成功。
查看ReentrantLock的类图结构,其实现了Lock接口,其中定义了关于锁的一些基本操作,包含了SyncFairSyncNonfairSync这几个内部类。从名称也可以看出FairSyncNonfairSync分别实现了公平锁和非公平锁,二者同时继承自,作为锁实现的Sync,并且Sync继承自AbstractQueuedSynchronizer
ReentrantLock.png
接下来对ReentrantLock的源码进行分析。

Sync(基础类)

ReentrantLock实现的基础依赖于Sync类,一个静态内部抽象类,继承自AbstractQueuedSynchronizer,使用AQS中的state状态值来记录持有锁的次数,为下面的公平锁和非公平锁扩展定义好了基础框架。
ReentrantLock中关于加锁根据场景有不同的实现,对应的lock方法分别在Lock接口和内部类SyncFairSyncNonfairSync中进行定义和重载,而解锁的方式统一调用的是AQS中定义的变更状态方法,得益于AQS优秀的设计。如下代码所示:

public void unlock() {
    // 此处release调用为synch子类对象指向Sync继承自AQS的父类方法
    sync.release(1);
}

public void lock() {
    // 此处lock方法为sync子类对象各自引用的调用
    sync.lock();
}

AQS中实现了同步队列阻塞以及释放锁的能力,开放了子类关于锁的添加和释放的方法,tryAcquireSharedtryAcquiretryReleasetryReleaseShared,因此ReentrantLock中也是如此,由于其为互斥锁实现,因此只用重写tryAcquiretryRelease方法的实现即可。这在Sync类中分别对应:

// 默认实现了非公平锁的加锁条件
final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 如果同步状态位为0,当前线程尝试通过CAS去修改状态位
        if (compareAndSetState(0, acquires)) {
            // 如果CAS成功,设置当前线程为独占线程,返回true
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果不为0,判断当前线程是否为独占线程
    else if (current == getExclusiveOwnerThread()) {
        // 如果再次获取锁,记录重入次数
        int nextc = c + acquires;
        if (nextc < 0) // overflow 溢出
            throw new Error("Maximum lock count exceeded");
        // 更新状态位
        setState(nextc);
        return true;
    }
    // 如果线程首次即没有获得锁,也不满足重入条件,返回false,获取锁失败
    return false;
}

// 使用模板方法设计模式,该方法在AQS的release方法中被调用,作为判断是否释放锁状态的前置条件
protected final boolean tryRelease(int releases) {
    // 每次释放锁对状态为进行递减
    int c = getState() - releases;
    // 如果当前线程不是队列首线程,抛出IllegalMonitorStateException异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 当同步状态位state==0时,返回释放锁成功
    boolean free = false;
    if (c == 0) {
        free = true;
        // 当前独占线程为null,即没有线程占用
        setExclusiveOwnerThread(null);
    }
    // 更新同步状态位为初始值
    setState(c);
    return free;
}

Sync中tryRelease对应为父类AQS中的实现,nonfairTryAcquire对应子类中的公共实现,父类AQS中tryAcquire的实现分别在FairSyncNonfairSync子类中进行实现。

FairSync(公平锁)

由于ReentrantLock基于FIFO的队列来实现线程排队等待,公平锁中的tryAcquire方法,当CPU进行调度时,每次按照队列的排队顺序进行同步状态为值的判断,尝试锁的获取,因此实现了先到先得的公平性。

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

    final void lock() {
        // 调用AQS的acquire方法,传入参数state值为1
        // 是否将当前线程加入到等待队列中,取决于tryAcquire的返回结果
        // 如果尝试获取锁或者添加到队列失败,将中断当前线程操作
        acquire(1);
    }

    // 尝试获取锁的公平锁实现,如果不是队列首线程将获取锁失败
    protected final boolean tryAcquire(int acquires) {
        // 获取当前运行线程
        final Thread current = Thread.currentThread();
        // 获取AQS的同步状态值
        int c = getState();
        if (c == 0) {
            // hasQueuedPredecessors方法的执行结果:如果当前线程之前有排队线程,则为true ,如果当前线程位于队列的头部或队列为空,则为false
            // 如果同步状态为0,并且当前队列没有线程排队,并且CAS修改状态位成功则尝试获取锁成功
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                // 设置当前线程为独占拥有线程
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            // 如果 c != 0,且当前线程为独占线程,记录当前线程重入锁的次数。
            int nextc = c + acquires;
            if (nextc < 0)
                // 冲入次数溢出了,超过了int表示的最大值
                throw new Error("Maximum lock count exceeded");
            // 更新AQS同步状态位的值
            setState(nextc);
            return true;
        }
        return false;
    }
}

NonfairSync(非公平锁)

关于非公平锁的实现也是相当巧妙,基于队列实现公平锁很简单,如何实现非公平锁呢?如下代码所示:

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

    final void lock() {
        // 在线程入队前先进性一次CAS尝试,如果成功,则将当前线程设置为独占模式的执行线程
        // 其余线程继续保持队列排队等待状态,作者也在注释中说明了锁的公平性并不能保证线程调度的公平性
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            // 如果CAS失败,则进入队列
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        // 调用基础类中的实现
        return nonfairTryAcquire(acquires);
    }
}

总结

  • ReentrantLock实现了synchronized相同的语义,并且对其进行了扩展,synchronized借助于操作系统层面互斥信号量来实现互斥锁,ReentrantLock借助于AQS抽象同步队列框架来实现锁。
  • 公平锁与非公平锁,二者的区别在于,公平锁直接依赖队列的顺序进行线程的执行与挂起,而非公平锁在CPU进行调度时直接进行一次CAS操作,如果成功则获得调度优先级,失败则进入队列等待。
  • 在锁的获取方面使用synchronized的锁往往会被一个线程多次获取,而ReentrantLock提供的锁在线程执释放锁后,如果再次获得锁就需要进入队列等待。

参考资料:

  • jdk1.8.0_311 ReentrantLock源码

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/275732.html

(0)
上一篇 2022年7月21日 00:32
下一篇 2022年7月21日 00:37

相关推荐

发表回复

登录后才能评论