ReentrantLock可重入锁源码原理详解

1 篇文章 0 订阅
1 篇文章 0 订阅

ReentrantLock可重入锁源码原理详解

背景介绍

AbstractQueuedSynchronizer是Doug Lea在JDK1.5的时候加入的一个同步框架,也被简称为AQS,该框架主要维护了被竞争资源的状态,和获取到资源的线程(通过AbstractOwnableSynchronizer来维护)以及未获取到资源的线程的管理,AQS主要通过volatile的内存可见性和CAS来实现。具体的竞争资源的方式(公平、非公平)由子类实现,Doug Lea在引入该框架时提供了一系列已经实现好的子类,比如:ReentrantLock、ReentrantReadWriteLock,为了对使用者透明具体实现细节降低使用门槛,这两个锁本身并不继承AQS,而是由内部类Sync继承AQS并通过组合的方式实现锁。

ReentrantLock介绍

AQS实现了共享方式和排它方式,而ReentrantLock只对外暴露出了AQS的排它方式,所以ReentrantLock也叫做排它锁,在这个基础上ReentrantLock又通过两个内部类(FairSync、NonfairSync)间接继承了AQS分别实现了公平锁、非公平锁

在这里插入图片描述

ReentrantLock与synchronized对比

1. 竞争锁标识

​ synchronized: 线程通过获取某个对象的Monitor的所有权

​ ReentrantLock: 线程通过修改AQS中的被volatile修饰的int类型的state变量

2. 抢到锁标识的线程以及未抢到锁标识的线程维护

​ synchronized: 不清楚具体实现(跟Monitor Record有关),不了解JVM是如何维护的。

​ ReentrantLock: 通过AQS的基类AbstractOwnableSynchronizer中的一个成员变量来记录获取到资源的线程,并把为获取到锁标识的线程维护在一个FIFO的队列中。

Monitor Record内部结构:

Monitor Record
Owner初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
EntryQ关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。
RcThis表示blocked或waiting在该monitor record上的所有线程的个数。
Nest用来实现重入锁的计数。
HashCode保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

3. 线程通信

synchronized: 通过某个对象的wait()、notify()、notifyAll()方法来实现。 注: 该方法需要在synchronized 语句块中调用否则会抛IllegalMonitorStateException异常。

ReentrantLock:通过await()、signal()、signalAll()方法来实现(区别于wait()、notify()、notifyAll())。 注: 该方法需要在ReentrantLock调用lock()方法后,unlock()方法前调用,否则会抛IllegalMonitorStateException异常。

4. 未抢到锁标识的线程状态

synchronized: 线程处于BLOCKED状态。

ReentrantLock: 线程处于WAITING状态。
下文证明

5.锁类型

synchronized: 只有非公平锁实现。

ReentrantLock: 既有非公平锁又有公平锁,默认为非公平锁。

ReentrantLock(非公平锁)实现源码

首先看一下ReentrantLock的构造函数:

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

1.获取锁(采用模板方法模式)

获取锁的过程主要采用模板方法模式,流程看起来感觉会有点乱。

  1. 获取锁的入口是ReentrantLock类中的lock()方法,该方法会调用内部抽象类的lock()方法

    public void lock() {
            sync.lock();    //sync为ReentrantLock的内部抽象类继承AQS
        }
    
  2. 内部抽象类Sync的lock()方法为抽象方法,该方法的具体实现由ReentrantLock的内部类NonfairSync实现

    abstract void lock();
    
  3. ReentrantLock的lock()方法最终实现是在NonfairSync的lock()方法,一进入该方法先去竞争锁标识,这里也是非公平的原因之一。

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

    ① 获取锁的时候先通过CAS竞争锁标识,如果成功把AQS中的state成员变量从0修改为1就认为自己(当前线程)成功。(方法简单不展开)

    获取到锁标识并记录到AbstractOwnableSynchronizer中的成员变量exclusiveOwnerThread中,线程继续向下执行。(方法简单不展开)

    ③ 如果①失败表示该线程未获取到锁标识则进入AQS的acquire()方法。

  4. AQS的acquire()方法

    public final void acquire(int arg) {    //该方法采用模板方法模式
            if (!tryAcquire(arg) &&         
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))        
                selfInterrupt();
        }
    

    该方法采用模板方法模式,其中tryAcquire()方法尝试获取锁标识具体实现由子类实现,如果获取锁标识成功线程继续向下执行,如果获取失败,线程将会进入等待状态(不是阻塞状态跟synchronized不同,如下图),然后将该线程构建成一个独占式的节点放到队列中进行维护。

    如图(分析两个线程通过ReentrantLock锁成功和失败的线程状态):

    示例代码:

       public static void main(String[] args) throws ExecutionException, InterruptedException {
    
            ReentrantLock reentrantLock = new ReentrantLock();
    
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
    
                    try {
                        reentrantLock.lock();
                        while (true) {
                        }
                    } finally {
                        reentrantLock.unlock();
                    }
                }
            }, "T1");
            t1.start();
    
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
    
                    try {
                        reentrantLock.lock();
                        while (true) {
                        }
                    } finally {
                        reentrantLock.unlock();
                    }
                }
            }, "T2");
            t2.start();
    
            t1.join();
            t2.join();
    
        }
    

    上述事例代码很简单,两个线程进行争抢同一把锁,然后通过jstack分析T1、T2线程所处的状态:

在这里插入图片描述

可以清晰的看到T1线程获取到了锁标识处于RUNNABLE(JVM将操作系统的READY就绪状态和RUNNING运行中状态合并为RUNNABLE状态)状态,T2线程并未获取到锁标识处于WAITING状态(并不是BLOCKED状态原因是因为底层调用LockSupport.park()方法)。

  1. 如果竞争锁标识失败将会进入addWaiter(Node.EXCLUSIVE)方法:

    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;
                }
            }
            enq(node);
            return node;
        }
    

    该方法把当前线程构建成一个独占式的Node节点放到FIFO队列中,该方法先判断队尾是否为空,如果不为空通过CAS将该线程的节点放到队尾然后返回若CAS失败则进入enq()方法,如果队尾为空则证明该队列尚未初始化,则进入enq()方法初始化队列并将该节点放入队列中,具体向下看enq()方法实现。

  2. enq()方法分析:

    private Node enq(final Node node) {
            for (;;) {
                Node t = tail;
                if (t == null) { // Must initialize
                    if (compareAndSetHead(new Node()))
                        tail = head;
                } else {
                    node.prev = t;
                    if (compareAndSetTail(t, node)) {
                        t.next = node;
                        return t;
                    }
                }
            }
        }
    

    可以看到该方法是个死循环(CAS的失败重试),用来保证该线程节点放入队尾,第一个判断用来判断该队列是否已经初始化过,若尚未初始化则先进性初始化操作,然后在通过CAS失败重试将该节点放入队尾并返回。

  3. 再继续分析将该节点放入队列中的后续操作,将会执行acquireQueued()方法:

    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())      //主要调用LockSupport.park()方法阻塞当前线程,并在当前线程醒来时重				置该线程的中断标志位
                        interrupted = true;
                }
            } finally {
                if (failed)
                    cancelAcquire(node);
            }
        }
    

① 首先该方法将会进入一个死循环,如果该节点的父节点是对头(只有对头的节点才持有锁标识),如果该节点的父节点是头结点则证明该节点可能(下文宏观获取锁流程讲解为什么是可能) 马上就可以获取到锁标识了,进行tryAcquire()尝试获取锁标识,如果获取成功,把该节点设置为头结点,并返回false(主要是返回一个暗号,不让当前线程设置中断标识[下文讲解线程中断])。

② 如果该节点的父节点不是头结点(说明下一个获取到锁的线程一定不是他),或者是头结点但是竞争锁标识失败了(下文宏观获取锁流程讲解为什么会失败),将会进入shouldParkAfterFailedAcquire()方法,该方法主要判断该节点是否可以安全的进行阻塞,还有其他处理逻辑,如果可以安全阻塞将会(触发LockSupport.park()方法进入WAITING状态)阻塞该线程。

③ 可以发现上述流程发生在一个死循环中,一般情况会等到获取到锁标识后正常返回,不过肯能存在几种情况在为获取到锁之后就返回了,不如线程取消,等待超时,将会进入cancelAcquire()方法(该方法还没来得及好好看,好像主要是针对这些未正常获取到锁就返回的线程的处理以及对存放线程Node节点的队列的一些维护操作)。

④ 正常返回的情况下会返回true或者false,如果返回false证明该线程并没有进入WAITING状态,如果返回true则说明该线程进入过WAITING状态并在苏醒时对线程的中断标志位进行置位。后续操作会根据这个返回结果对该线程的中断标志位进行相应的设置。

注:线程中断标志位好像在本流程中没用,好像在其他流程中用到了,比如tryAcquireNanos()方法,该方法好像会相应线程中断。

  1. 还有一个重要的方法就是tryAcquire()方法:

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

    该方法很重要很多地方都用到了,但是也很简单。

    ① 首先获取当前线程,并且获取锁标识的值(0标识没有线程持有锁)。

    ② 当锁标识为0的时候标识没有线程持有锁,使用CAS竞争锁标识,如果竞争成功则把当前线程记录到AbstractOwnableSynchronizer的成员变量中。

    ③ 如果锁标识不为0,则说明有线程持有该锁,然后判断持有锁的线程是否是当前线程,如果是则将锁标识位+1(这就是为什么说ReentrantLock是一把可重入锁)。

获取锁(非公平锁)的流程结束,释放锁的流程很简单,大家有兴趣可以自己去看源码(实在是不想写了)

线程中断

个人理解:线程中断是一种线程间进行通信的方式之一,他本身是线程的一个属性,用来标识其他线程(也可以是他本身)给该线程(运行中)的中断标识位设置值(好像是一个boolean值),相当于其他线程给该线程发送的一个消息。

注:该线程必须处于运行中才能收到该线程的中断消息,比如该线程处于WAITING状态无法收到中断消息。

宏观获取锁流程

首先看一下公平锁和非公平锁表现的结果:

测试代码:

public class ReentrantLockTest {

    //需要继承ReentrantLock把getQueuedThreads()方法暴露出来
    public class MyReentrantLock extends ReentrantLock{

        public MyReentrantLock(boolean fair) {
            super(fair);
        }

        @Override
        public Collection<Thread> getQueuedThreads() {
            List<Thread> list = new ArrayList<>(super.getQueuedThreads());
            //由于是逆序输出的所以进行翻转,不信可以看输入线程队列的源码
            Collections.reverse(list);
            return list;
        }
    }

    public static class Job extends Thread{

        public MyReentrantLock reentrantLock;
        public Job(MyReentrantLock reentrantLock){
            this.reentrantLock = reentrantLock;
        }
        @Override
        public void run() {

            for (int i = 0; i < 2; i++){
                reentrantLock.lock();
                List<String> collect = reentrantLock.getQueuedThreads().stream().map(e -> {
                    return e.getName();
                }).collect(Collectors.toList());

                System.out.println("当前线程:"+Thread.currentThread().getName()+",阻塞队列线程:"+collect);

                try {
                    TimeUnit.MILLISECONDS.sleep(20);
                }catch (Exception e){}
                reentrantLock.unlock();
            }
        }
    }

    @Test   //非公平锁测试
    public void testNotFair() throws InterruptedException {
        MyReentrantLock myReentrantLock = new MyReentrantLock(false);
        for (int i=0;i<5;i++){
            Job job = new Job(myReentrantLock);
            job.setName(i+"");
            job.start();
        }
        Thread.currentThread().join(2000);
    }

    @Test   //公平锁测试
    public void testFair() throws InterruptedException {
        MyReentrantLock myReentrantLock = new MyReentrantLock(true);
        for (int i=0;i<5;i++){
            Job job = new Job(myReentrantLock);
            job.setName(i+"");
            job.start();
        }
       Thread.currentThread().join(2000);
    }
}

结果:

非公平锁testNotFair():

在这里插入图片描述

公平锁testFair():

在这里插入图片描述

对比结果可以看到如下结果:

非公平锁:每当一个线程抢到锁之后,再来的其他线程将会进入到FIFO的队列中进行排队,当获取到锁的线程释放锁之后,本应该队列中的下一个节点线程获取锁,但是如果有新线程(没有在队列中的线程)来抢锁,那么新线程将会和队列中的头结点的下一个线程争抢锁标识,这就是表现出来的不公平的地方,但是仔细观察结果会发现不公平中也存在的公平,只要线程进入队列中之后,将会在队列中按照先进先出的顺序进行获取锁。

公平锁:每当一个线程抢到锁之后,再来的其他线程不会尝试抢锁,先回去判断线程队列中是否还有线程在进行排队获取锁,如果有自己将会去队列中排队获取锁,从结果来看,线程0释放锁之后将会去队列末尾排队,线程1释放锁后同样回去排队,以此类推。

这就解决了上述的问题,如果本文章有什么不对或者不合理的地方,希望大佬指正。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值