zoukankan      html  css  js  c++  java
  • 并发——深入分析ReentrantLock的实现原理

    一、前言

      之前花了点时间研究了一下并发包下的一个重要组件——抽象队列同步器AQS,在并发包中,很多的类都是基于它实现的,包括Java中常用的锁ReentrantLock。知晓了AQS的实现原理,那理解ReentrantLock的实现就非常简单了,因为它的锁功能的实现就是由AQS实现的,而它的工作仅仅是重写了一些AQS中的相关方法,并使用其中的模板方法进行加锁解锁。今天这篇博客就来从源码的角度分析一下ReentrantLock的实现。


    二、正文

    2.1 抽象队列同步器AQS

      在说ReentrantLock前,必须要先提一下AQSAQS全称抽象队列同步器(AbstractQuenedSynchronizer),它是一个可以用来实现线程同步的基础框架。当然,它不是我们理解的Spring这种框架,它是一个类,类名就是AbstractQuenedSynchronizer,如果我们想要实现一个能够完成线程同步的锁或者类似的同步组件,就可以在使用AQS来实现,因为它封装了线程同步的方式,我们在自己的类中使用它,就可以很方便的实现一个我们自己的锁。

      AQS的实现相对复杂,无法通过短短的几句话将其说清楚,我之前专门写过一篇分析AQS实现原理的博客:并发——抽象队列同步器AQS的实现原理

      在阅读下面的内容前,请一定要先学习AQS的实现原理,因为ReentrantLock的实现非常简单,完全就是依赖于AQS的,所以我以下的描述均建立在已经理解AQS的基础之上。可以阅读上面推荐博客,也可以自己去查阅相关资料。


    2.2 ReentrantLock的实现原理

      我们先简单介绍一下ReentrantLock的实现原理,这样方便我们下面阅读它的源码。前面也说过,ReentrantLock基于AQS实现,AQS模板方法acquirerelease等,已经实现了加锁和解锁的操作,而使用它的类只需要重写这些模板方法中调用的方法,比如tryAcquiretryRelease等,这些方法通过修改AQS的同步状态state来加锁解锁。AQS的同步状态state是一个int类型的值,根据不同的值,就可以判断当前锁的状态,同时修改这个值就是加锁和解锁的方式。

      使用AQS的一般方式是以内部类的形式继承AQSReentrantLock也是这么实现的,在它的内部,有三个AQS的派生类:

    1. 首先第一个派生类名字叫做Sync,这是一个抽象类,直接继承自AQS,其中定义了一些通用的方法;
    2. 第二个派生类名字叫做NonfairSync,它继承自Sync,实现的是一种非公平锁
    3. 第三个派生类名字叫FairSync,它也继承自Sync,实现的是一种公平锁

      ReentrantLock就是通过NonfairSync对象或者FairSync对象来保证进行线程同步的。而这三个类中编写的方法,实际上就是修改同步状态的方式。当state的值为0时,表示当前并没有线程获取锁,而每获取一次锁,state的值就会+1,释放一次锁,state-1。下面我们就通过这三个类的源码来具体看一看吧。


    2.3 Sync类源码解析

      我们直接来看看Sync类中的方法吧,Sync类中的方法不少,我只拿出其中比较重要的几个来讲一讲:

    abstract static class Sync extends AbstractQueuedSynchronizer {
    	/** 定义一个加锁的抽象方法,由子类实现 */
        abstract void lock();
    
        /**
         * 此方法的作用是以非公平的方式尝试获取一次锁,获取成功则返回true,否则返回false;
         * 需要注意,AQS的获取锁,实际上就是修改同步状态state的值。
         * 这里有个疑惑,既然是非公平地获取锁,那这个方法为什么不写在NonfairSync类中?
         * 因为ReentrantLock有一个方法tryLock,即尝试获取一次锁,调用tryLock方法时,
         * 无论使用的是公平锁还是非公平锁,实际上都需要尝试获取一次锁,也就是调用这个方法,
         * 所以这个方法定义在了父类Sync中
        */
        final boolean nonfairTryAcquire(int acquires) {
            // 获取当前正在运行的线程
            final Thread current = Thread.currentThread();
            // 获取同步状态state的值,state定义在父类AQS中,
            int c = getState();
            // 若当前state的值为0,表示还没有线程获取锁,于是当前线程可以尝试获取锁
            if (c == 0) {
                // compareAndSetState方法通过CAS的方式修改state的值,
                // 实际上就是让state从0变为1,因为acquires的值就是1,
                // 每次有线程获取了锁时,同步状态就+1
                if (compareAndSetState(0, acquires)) {
                    // 若compareAndSetState方法返回true,表示修改state成功,
                    // 则调用setExclusiveOwnerThread方法将当前线程记录为占用锁的线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 若以上c == 0不满足,则表示已经有线程获取锁了,
            // 于是调用getExclusiveOwnerThread方法获取当前正在占用锁的线程,
            // 然后和当前线程比较,若当前线程就是占用锁的线程,则当前线程不会被阻塞,
            // 可以再次获取锁,从这里可以看出,ReentrantLock是一个可重入锁
            else if (current == getExclusiveOwnerThread()) {
                // 计算当前线程获取锁后,state的值应该是多少,实际上就是让state + 1
                int nextc = c + acquires;
                // 如果nextc小于0,则保存,因为理论上同步状态是不可能小于0的
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                // 使用上面计算出的nextc更新state的值,这里需要注意一点
                // setState不像compareAndSetState方法,setState方法并不保证操作的原子性
                // 这里不需要保证原子性,因为这里线程已经获取了锁,所以不会有其他线程进行操作
                setState(nextc);
                // 返回true表示加锁成功
                return true;
            }
            // 若以上条件均不满足,表示有其他线程获取了锁,当前线程获取锁失败
            return false;
        }
    
        
        /**
         * 此方法是的作用是尝试释放锁,其实也就是让state的值-1
         * 这个方法是一个通用的方法,不论使用的是公平锁还是非公平锁
         * 释放锁时都是调用此方法修改同步状态
         */
        protected final boolean tryRelease(int releases) {
            // getState方法获取state的值,并与参数相减,计算释放锁后state的值
            // 在ReentrantLock中其实就是-1
            int c = getState() - releases;
    		// 判断当前线程是不是占用锁的线程,若不是则抛出异常
            // 因为只有占用了锁的线程才可以释放锁
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            
            // 变量free用来标记锁释放真正的被释放,因为ReentranLock是一个重入锁
            // 获取锁的线程可以多次获取锁,只有每一次获取都释放,锁才是真正的释放
            boolean free = false;
            // 判断c的值是否是0,只有c的值是0,也就是state的值为0时
            // 才说明当前的线程在这次释放锁后,锁真正的处于没有被使用的状态
            if (c == 0) {
                // 若满足此条件,则free标记为true,表示锁真的被释放了
                free = true;
                // 然后标记当前占用锁的线程为null,也就是没有线程占用锁
                setExclusiveOwnerThread(null);
            }
            // 将c的值更新同步状态state
            setState(c);
            return free;
        }
    	
        /** 此方法判断当前线程是不是获取锁的线程 */
        protected final boolean isHeldExclusively() {
            // getExclusiveOwnerThread方法返回当前正在占用锁的线程
            // 于当前的运行的线程进行比较
            return getExclusiveOwnerThread() == Thread.currentThread();
        }
    }
    

      以上就是Sync类的实现。其实Sync中的方法不仅仅只有上面这几个,但是剩下的那些方法都是一些零零碎碎,对我们理解ReentrantLock没有太大帮助的方法,所以这里就不一一列举了。从上面的方法实现中,我们可以知道以下信息:线程获取锁的方式实际上就是让同步状态state的值增加,而释放锁的方式就是让state的值减小;而且ReentrantLock实现的是可重入锁,已经获取锁的线程可以不受阻碍地再次获取锁,state的值可以不断增加,而释放锁时,只有state的值减小为0,锁才是真正被释放


    2.4 NonfairSync类源码解析

    下面我们再看看第二个内部类NonfairSync,它实现的是非公平锁

    /**
     * 此类继承自Sync,它实现的是非公平锁
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        
        /**
         * 在父类Sync中定义的lock方法,在子类中实现
         */
        final void lock() {
            // 调用compareAndSetState方法,企图使用CAS机制将state的值从0修改为1
            // 若state的值不为0,表示锁已经被其他线程获取了,则当前线程将获取锁失败
            // 或者state的值一开始是0,但是在当前线程修改的过程中,被其他线程修改了,
            // 也会返回false。若修改state失败,则就需要执行下面的acquire方法
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            // acquire方法是AQS定义的模板方法,这个方法会调用tryAcquire尝试获取锁,
            // 而tryAcquire方法是由子类实现的,也就是下面那个方法;
            // 若调用tryAcquire获取锁失败,则AQS会将线程封装成一个节点Node
            // 丢入AQS的同步队列中排队(这个具体实现请参考AQS的实现博客)
            // 归根到底,这个方法就是让线程获取锁,不断尝试,直到成功为止.
            // 注意这里传入的参数是1,表示加锁实际上就是让state的值+1
            else
                acquire(1);
        }
    
        
        /** 
         * 此方法tryAcquire在AQS的模板方法中被调用,它的作用就是尝试获取一次锁,
         * 也就是尝试修改一次同步状态state;
         * 不同的实现类根据不同的需求重写tryAcquire方法,就可以按自己的意愿控制加锁的方式
         * AQS就是通过这种方式来提供给其他类使用的
         */
        protected final boolean tryAcquire(int acquires) {
            // 此处直接调用了父类Sync中,非公平地获取一次锁的nonfairTryAcquire方法
            return nonfairTryAcquire(acquires);
        }
    }
    

      上面就是NonfairSync类完整的代码,并没有删减,可以看出,非常的简短。实现了Sync类中定义的lock方法,同时重写了tryAcquire方法,供AQS的模板方法acquire调用,且tryAcquire的实现仅仅是调用了Sync中的nonfairTryAcquire方法。为了有助于我们理解,我们还是来看看AQSacquire方法的代码吧:

    public final void acquire(int arg) {
        // 这里首先调用tryAcquire方法尝试获取一次锁,在AQS中这个方法没有实现,
        // 而具体实现是在子类中,也就是调用的是NonfairSync的tryAcquire方法,
        // 若方法返回true,表示成功获取到锁,于是后面代码都不会执行了,
        // 否则,将先执行addWaiter方法,这个方法的作用是将当前线程封装成为一个Node节点,
        // 加入到AQS的同步队列的尾部,同时将返回这个Node节点,并传入acquireQueued方法
        // acquireQueued方法的作用就是让当前线程阻塞,直到成功获取到锁才会从这个方法返回
        // acquireQueued会返回这个线程在等待的过程中是否被中断,若被中断,
        // 则调用selfInterrupt方法真正执行中断。
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    

      为什么说NonfairSync是非公平锁?我们可以看到,在NonfairSynclock方法中,一个线程尝试去获取锁前,并不会判断在它之前是否有线程正在等待获取锁,而是直接尝试调用compareAndSetState方法获取一次锁,若获取失败,进入acquire方法,在这个方法中又会调用tryAcquire方法获取一次锁。此时若再次获取失败,才会进行进入同步队列中排队,这个过程中插了两次队,所以NonfairSync是非公平锁。


    2.5 FairSync类源码解析

      下面我们来看看最后一个内部类FairSync,它实现的是公平锁,也就是线程按照先来后到的顺序获取锁,而不会插队:

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
    
        /** 实现父类的lock方法,加锁 */
        final void lock() {
            // 直接调用AQS的模板方法acquire进行加锁,调用这个方法后,线程若没有获取锁
            // 则会被阻塞,直到获取了锁后才会返回。这里需要注意一点,和NonfairSync中的lock不同
            // 这里直接调用acquire,而不会先调用一次compareAndSetState方法获取锁
            // 因为FairSync是公平锁,所以不会执行这种插队的操作.
            // 注意这里传入的参数是1,表示加锁实际上就是让state的值+1
            acquire(1);
        }
    
        /** 
         * 和NonfairSync一样,重写AQS的tryAcquire方法,若使用的是FairSync,
         * 则acquire中将调用此tryAcquire方法,尝试获取一次锁
         */
        protected final boolean tryAcquire(int acquires) {
            // 首先获取当前正在执行的线程
            final Thread current = Thread.currentThread();
            // 记录同步状态
            int c = getState();
            // 若state的值为0,表示现在没有线程占用了锁,于是当前线程可以尝试获取锁
            if (c == 0) {
                // 尝试获取锁前,先调用hasQueuedPredecessors方法,这个方法是判断
                // 是否有其他线程正在排队尝试获取锁,若有,方法将返回true,那为了公平性,
                // 当前线程不能获取锁,于是直接结束,否则调用compareAndSetState修改state
                // 若修改成功,调用setExclusiveOwnerThread方法将自己设置为当前占用锁的线程
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 若state不等于0,则表示当前锁已经被线程占用,那此处判断占用锁的线程是否是自己
            // 若是,则当前线程可以再次获取锁,因为ReentrantLock实现的是可重入锁,
            else if (current == getExclusiveOwnerThread()) {
                // 计算当前线程再次获取锁后,state的值将变为多少,此处实际上就是 + 1
                int nextc = c + acquires;
                // 理论上state的值不可能小于0,于是若小于0,就报错
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                // 修改state的值为上面计算的新值,此处不需要CAS操作保证原子性,
                // 因为当前线程已经获取了锁,那其他线程就不能修改state,所以这里可以放心修改
                setState(nextc);
                return true;
            }
            // 若以上条件均不满足,表示有其他线程占用了锁,则直接返回false
            return false;
        }
    }
    

      FairSync的实现也比较简单。值得注意的是,因为FairSync实现的是公平锁,所以线程获取锁前,会先判断是否有在它之前尝试获取锁的线程在排队,若有,则当前线程不能插队,也需要进行排队,并且排在那些线程之后


    2.6 ReentrantLock的成员属性与构造方法

      看完了内部类,下面就正式来看一看ReentrantLock是如何操作的吧,首先看一看它的成员属性和构造方法构造方法:

    /** 记录使用的锁对象 */
    private final Sync sync;
    
    /** 默认构造方法,初始化锁对象,默认使用非公平锁 */
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    
    /** 参数为boolean类型的构造方法,若为false,使用非公平锁,否则使用公平锁 */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    

    2.7 ReentrantLock的加锁与解锁

      下面我就来看看ReentrantLock最重要的两个操作,加锁和解锁。

    (1)获取锁的方法实现

    /** 
     * 此方法让当前线程获取锁,若获取失败,线程将阻塞,直到获取成功为止 
     * 此方法不会响应中断,也就是在没有获取锁前,将无法退出
     */
    public void lock() {
        // 直接调用锁对象的lock方法,也就是之前分析的内部类中的lock方法
        sync.lock();
    }
    
    /**
     * 此方法获取锁,和上面的方法类似,唯一的不同就是,调用这个方法获取锁时,
     * 若线程被阻塞,可以响应中断
     */
    public void lockInterruptibly() throws InterruptedException {
        // 调用sync对象的acquireInterruptibly方法,这个方法定义在AQS中,
        // 也是AQS提供给子类的一个模板方法,内部也是通过tryAcquire获取锁,
        // 若获取失败,线程将被阻塞,但是此方法会检测中断信号,
        // 若检测到中断,将通过抛出异常的方式退出阻塞
        // 关于这个方法的具体实现,可以去参考AQS的相关博客,此处就不展开描述了
        sync.acquireInterruptibly(1);
    }
    
    
    /** 
     * 调用此方法尝试获取一次锁,不论成功失败,都会直接返回
     */
    public boolean tryLock() {
        // 此处直接调用Sync类中的nonfairTryAcquire方法,
        // 这也就是为什么nonfairTryAcquire定义在父类Sync中,
        // 因为不论是使用公平锁还是非公平锁,都需要在此处调用这个方法
        return sync.nonfairTryAcquire(1);
    }
    

    (2)是否锁的方法实现

    /**
     * 此方法用来释放锁
     */
    public void unlock() {
        // 此处调用的是AQS的release方法,这个方法也是AQS提供的一个模板方法,
        // 在这个方法中,将调用子类重写的tryRelease方法尝试释放锁,若释放成功
        // 则会唤醒等待队列中的下一个线程,让它停止阻塞,开始尝试获取锁,
        // 关于这个方法的具体实现,可以参考我之前推荐的AQS源码分析博客。
        // 这里需要注意,传入的参数是1,表明释放锁实际上就是让state的值-1
        sync.release(1);
    }
    

      以上就是ReentrantLock加锁和解锁的方法,出乎意料,非常的简单,每个方法都只有一句代码,调用AQS类中提供的模板方法。这就是AQS的好处,AQS封装了线程同步的代码,我们只需要在类中使用它,就能很简单的实现一个锁。所以我前面才说,在看ReentrantLock前,一定要先学习AQS,理解了AQS,理解ReentrantLock就完全没有难度了。

      上面这些就是ReentrantLock中的关键方法,其实除了这些方法之外,还有许多其他的方法,但是那些方法并不是关键,实现也都非常简单,基本上就是一句代码,可以自己直接去阅读源码,我这里就不一一列举了。


    三、总结

      经过上面的分析,我们会发现,ReentrantLock的实现原理非常的简单,因为它是基于AQS实现的,复杂性都被封装在了AQS中,ReentrantLock仅仅是它的使用者,所以,学习ReentrantLock实际上就是学习AQSAQSJava并发中的重要组件,很多的类都是基于它实现的,比如非常常用的CountDownLatchAQS也是面试中的常考题,所以一定要好好研究。此处再次推荐我写的AQS解析博客:并发——抽象队列同步器AQS的实现原理


    四、参考

    • JDK1.8源码
  • 相关阅读:
    以色列人,印象
    周末之个人杂想(三)
    长春DotNet俱乐部会员群
    sharepoint中的ListViewWebPart和ViewToolBar的操作 Virus
    SharePoint2007中的WCM Virus
    SPWeb.ProcessBatchData Method Virus
    sharepoint中显示网页库item的webpart和显示列表库item的webpart Virus
    jquery在vs2008中智能提示的配置 Virus
    格式化sharepoint中取出来的字段值 Virus
    使用google的ajax API中的翻译小工具 Virus
  • 原文地址:https://www.cnblogs.com/tuyang1129/p/12689122.html
Copyright © 2011-2022 走看看