zoukankan      html  css  js  c++  java
  • ReentrantLock源码解析——虽众但写

    在看这篇文章时,笔者默认你已经看过AQS或者已经初步的了解AQS的内部过程。

      先简单介绍一下ReentantLock,跟synchronized相同,是可重入的重量级锁。但是其用法则相当不同,首先ReentrantLock显式的调用lock方法表示接下来的这段代码已经被当前线程锁住,其他线程需要执行时需要拿到这个锁才能执行,而当前线程在执行完之后要显式的释放锁,固定格式

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

    1.ReentrantLock的demo程序

    来通过下面这段代码简单的了解ReentrantLock是如何使用的

    	// 定义一个锁
    	private static Lock lock = new ReentrantLock();
    
        /**
         * ReentrantLock的使用例子,并且验证其一些特性
         * @param args 入参
         * @throws Exception 错误
         */
        public static void main(String[] args) throws Exception {
            // 线程池
            ThreadPoolExecutor executor = ThreadPoolUtil.getInstance();
    
            executor.execute(() -> {
                System.err.println("线程1尝试获取lock锁...");
                lock.lock();
                try {
                    System.err.println("线程1拿到锁并进入try,准备执行testForLock方法");
                    // 调用下方的方法,验证lock的可重入性
                    testForLock();
                    TimeUnit.MILLISECONDS.sleep(500);
                    System.err.println("线程1try模块全部执行完毕,准备释放lock锁");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                    System.err.println("线程1释放lock锁,线程1释放锁2次,此时才算真正释放,验证了ReentrantLock加锁多少次就要释放多少次锁");
                }
            });
    
            // 先睡他100ms,保证线程1先拿到锁
            TimeUnit.MILLISECONDS.sleep(100);
            
            executor.execute(() -> {
                System.err.println("线程2尝试获取lock锁...");
                lock.lock();
                try {
                    System.err.println("线程2拿到锁并进入try");
                } finally {
                    lock.unlock();
                    System.err.println("线程2执行完毕,释放lock锁");
                }
            });
    
        }
    
        /**
         * 验证ReentrantLock具有可重入
         */
        public static void testForLock() throws InterruptedException {
            System.err.println("线程1开始执行testForLock方法,正准备获取lock锁...");
            lock.lock();
            try {
                System.err.println("testForLock成功获取lock锁,证明了ReentrantLock具有可重入性");
                TimeUnit.MILLISECONDS.sleep(200);
            } finally {
                lock.unlock();
                System.err.println("testForLock释放lock锁,线程1释放锁一次");
            }
        }
    

    结果图:1585664568146

      从结果图中,我们得到了很多信息,比如ReentrantLock具备可重入性(testForLock方法得出),并且其释放锁的次数必须跟加锁的次数保持一致(这样才能保证正确性);此外ReentrantLock悲观锁,在某个线程获取到锁之后其他线程在其完全释放之前不得获取(线程2充分证明了这一点,其开始获取锁的时间要比线程1的执行时间快许多,但还是被阻塞住了)。

    2.获取锁的方法——lock()

      okay,那来看下其内部是如何实现的,直接点击lock()方法

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

    看到其直接调用了synclock()方法,再点击进入

    abstract static class Sync extends AbstractQueuedSynchronizer {
        // ...
        
        abstract void lock();
        
        // ...
    }
    

      可以看到Sync类是ReentrantLock的一个内部类,继承了AQS框架,也就是说ReentrantLock就是AQS框架下的一个产物,那么问题就变得简单起来了。如果还没了解过AQS的可以看下我另一篇文章——AQS框架详解,看过之后再回头看ReentrantLock,你会发现,就这?

      扯回来ReentrantLock,这边可以看到内部类Sync是一个抽象类,lock()方法也是一个抽象方法,也就意味着这个lock会根据子类的不同实现执行不同操作,点开子类发现有两个——公平锁和非公平锁

    1585667693048

    里边的具体实现先放一放,回到ReentrantLocklock方法

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

      直接调用说明sync已经被初始化过,那么在哪里进行初始化的呢?仔细翻一翻可以从ReentrantLock两个构造方法中发现猫腻

    /**
     * 构造方法1
     * 无参构造方法,直接将sync初始化为非公平锁
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    
    /**
     * 构造方法2
     * 带参构造方法,根据传进来的布尔值决定将sync初始化为公平还是非公平锁
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    

      这里顺带说一下,在AQS有一个同步队列(CLH),是一种先进先出队列。公平锁的意思就是严格按照这个队列的顺序来获取锁,非公平锁的意思就是不一定按照这个队列的顺序来。

      那现在知道sync是在创建ReentrantLock的时候就进行了初始化,我们就来看下公平和非公平锁各自做了什么吧。

    2.1 非公平锁

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
    
        final void lock() {
            // 使用CAS尝试将state改为1,如果成功了,则表示获取锁成功,设置当前线程为持有线程即可
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                // 否则的话调用AQS的acquire方法乖乖入同步队列等待去吧
                acquire(1);
        }
    
        // AQS暴露出来需要子类重写的方法
        protected final boolean tryAcquire(int acquires) {
            // 方法解释在下方
            return nonfairTryAcquire(acquires);
        }
    }
    
    // 非公平锁的tryAcquire方法,该方法是放在Sync抽象类中的,为了tryLock的时候使用
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        // 当前锁的状态
        int c = getState();
        // 如果是0则表示锁是开放状态,可以争夺
        if (c == 0) {
            // 使用CAS设置为对应的值,在ReentrantLock中acquires的值一直是1
            if (compareAndSetState(0, acquires)) {
                // 成功了设置持有线程
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        /* 
         * 如果当前线程是持有线程,那么state的值+1
         * 这里也是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;
    }
    

      非公平锁基本的流程解释在上方的代码中已经在注释写出,相信不难看懂。不过有个需要注意的点要说一下,首先要看清楚非公平锁的定义,它是不一定按照队列顺序来获取,不是不按照队列顺序获取。

      从上面的代码我们也可以看出来,非公平锁调用lock()方法的时候会先调用一次CAS来获取锁,成功了直接返回,这第一次操作没有按照队列的顺序来,但也只有这一次。如果失败了,入队之后还是乖乖的得按照CLH同步队列的顺序来拿锁,这一点要搞清楚。

    2.3 公平锁

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;
    
        // lock方法直接调用AQS的acquire方法,连一点争取的欲望都没有
        final void lock() {
            acquire(1);
        }
    
        // 公平锁的获取资源方法,该方法是在acquire方法类调用的
        protected final boolean tryAcquire(int acquires) {
            
            // 整体逻辑还是挺简单的,跟非公平有些类似
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                /*
                 * c==0表示当前锁没有被获取
                 * 如果没有前驱节点或者前驱节点是头结点,
                 * 那么使用CAS尝试获取资源
                 * 成功了设置持有线程并返回true,失败了直接返回
                 */
                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;
        }
    }
    

      公平锁的逻辑相对来说十分简单,lock方法老老实实的去排队获取锁,而获取资源方法的逻辑也在代码注释写得很清楚了,没有什么需要多讲的。

    3.锁释放

    上面的理解之后释放锁的逻辑就简单的多了,直接放代码吧:

    /*
     * 解锁方法直接调用AQS的release方法
     * 而release方法的去向又是跟tryRelease的返回值直接相关
     * tryRelease方法的实现在内部类Sync中,具体在下方
     */
    public void unlock() {
        sync.release(1);
    }
    
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;
    
        // ...
    
        // 释放资源的方法
        protected final boolean tryRelease(int releases) {
            // 拿到当前锁的加锁次数
            int c = getState() - releases;
            // 当前线程必须是锁持有线程才能操作
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 如果次数为0,表示完全释放,清空持有线程
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
    
     	// ...
    }
    

      释放锁的逻辑在注释中解释得很清楚了,看完也知道由于ReentrantLock是可重入的,所以锁的数值会逐渐增加,那么在释放的时候也要一个一个逐一释放

    主要的逻辑还是AQSrelease方法中,这里详讲的话篇幅太多,有兴趣的话可以单独看下AQS的文章,传送门:AQS

    4.ReentrantLock的可选择性

      来讲下ReentrantLockSynchonized的一大不同点之一——Condition。那么condition是什么呢,简单来说就是将等待获取资源的线程独立出来分队,什么意思呢?举个例子,现在有8个线程同时争取一个锁,我觉得太多了,就把这个8个线程平均分成4队,等我觉得哪队OK就将那一队的线程叫出来争取这个锁。在这里的condition就是队伍,4队就是4个condition

      另外说一句,condition(队伍)中的线程是不参与锁的竞争的,如果上方的8个线程我只将2个线程放入一个队,其他线程不建立队伍,那么其他线程会参与锁的竞争,而独立到队伍中的2个线程则不会,因为其被放在AQS等待队列中,等待队列是不参与资源的竞争的,我在另一篇文章——AQS框架详解写得很清楚了。还是那句话,AQS懂了再看ReentrantLock,理解难度就会低得多得多得多得多....

    okay,那来简单看下Condition如何使用

    // 线程池
    ThreadPoolExecutor executor = ThreadPoolUtil.getInstance();
    // 这里只建了一个condition起理解作用,自己有兴趣的话可以多建几个模拟多点场景
    Condition condition = lock.newCondition();
    
    executor.execute(() -> {
        System.err.println("线程1尝试获取lock锁...");
        lock.lock();
        try {
            System.err.println("线程1拿到锁并进入try");
            System.err.println("线程1准备进行condition操作");
            /*
             * 将当前线程即线程1放入指定的这个condition中,
             * 如果是其他condition则调用其他condition的await()方法
             */
            condition.await();
            System.err.println("线程1结束condition操作");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
            System.err.println("线程1执行完毕,释放lock锁");
        }
    });
    // 保证线程1获取锁并且执行完毕
    TimeUnit.MILLISECONDS.sleep(200);
    executor.execute(() -> {
        System.err.println("线程2尝试获取lock锁...");
        lock.lock();
        try {
            System.err.println("线程2拿到锁并进入try");
            // 唤醒condition的所有线程
            condition.signalAll();
            System.err.println("线程2将condition中的线程唤醒");
        } finally {
            lock.unlock();
            System.err.println("线程2执行完毕,释放lock锁");
        }
    });
    

    结果图:

    1585749193417

    可以从结果图中看到,

      当线程调用了condition.await()的时候就被放入了condition中,并且此时将持有的锁释放,将自己挂起睡觉等待其他线程唤醒。所以线程2才能在线程1没执行完的情况获取到了锁,并且线程2执行完操作之后将线程1唤醒,线程1此时其实是重新进入同步队列(队尾)争取资源的,如果队列前方还有线程在等待的话它是不会拿到的,要按照队列顺序获取,可以自己在本地创多几个线程试一下。

      通过这段简单的代码之后明显可以看到condition具有不错的灵活性,也就是说提供了更多了选择性,这也就是跟synchronized不同的地方,如果使用synchronized加锁,那么Object的唤醒方法只能唤醒全部,或者其中的一个,但是ReentrantLock不同,有了condition的帮助,可以不同的线程进行不同的分组,然后有选择的唤醒其中的一组或者其中一组的随机一个。

    5.总结

      ReentrantLock的源码如果有了AQS的基础,那么看起来是不费吹灰之力(开个玩笑,还是要比吹灰费劲的)。所以本章的篇幅也比较简单,先从一个例子说明了ReentrantLock的用法, 并且通过这个例子介绍了ReentrantLock可重入、悲观锁的几个特性;接着对其lock方法进行源码跟踪,从而了解到其内部的方法都是由继承AQS的内部类Sync来实现的,而Sync又分成了两个类,代表两种不同的锁——公平锁和非公平锁;接下来再讲到两种锁的具体实现和释放的逻辑,到这里加锁解锁的流程就完整了;最后再介绍ReentrantLock的另一种特性——Condition,这种特性允许其选择特定的线程来争夺锁,也可以选择性的唤醒锁,到这里整篇文章就告一段落。

     

    孤独的人不一定是天才,还可能是得了郁抑症。

  • 相关阅读:
    MYSQL索引使用
    事务的概念是什么,有哪些基本属性?
    springboot和springmvc的区别
    List、Map、Set的区别与联系
    MyBatis-动态SQL
    MyBatis-映射文件
    MyBatis操作数据库及全局配置文件
    Jmeter的基本使用
    MySQL索引优化
    MySQL索引
  • 原文地址:https://www.cnblogs.com/zhangweicheng/p/12616803.html
Copyright © 2011-2022 走看看