• 带你看看Java的锁(一)-ReentrantLock


    前言

    AQS一共花了5篇文章,对里面实现的核心源码都做了注解 也和大家详细描述了下,后面的几篇文字我将和大家聊聊一下AQS的实际使用,主要会聊几张锁,第一篇我会和大家聊下ReentrantLock 重入锁,虽然在讲AQS中也穿插了讲了一下,但是我还是深入的聊下

    PS:前面5篇写在了CSDN里面 懒得转过来来了,有兴趣的移步去看下吧

    1. Java-AQS同步器 源码解读1-独占锁加锁
    2. Java-AQS同步器 源码解读2-独占锁解锁
    3. Java-AQS同步器 源码解读3-共享锁
    4. Java-AQS同步器 源码解读4-条件队列上
    5. Java-AQS同步器 源码解读5-条件队列下

    ReentrantLock简介

    ReentrantLock中文翻译:重入锁。那具体重入是什么意思呢,如果看过前面几篇文章的人,应该了解一下,简答的说就是AQS同步器里面的State相当于一个计数器,如果某一个线程获取锁了以后再再次去获取锁,这个计算器State就会+1.后面的代码中会详细的描述到。
    还有一个重要的点 就是lock和Condition的联合使用,ReentrantLock可以创建一个Condition,这个我在条件队列的文章中详细描述过。

    Synchronized对比

    ReentrantLock是一个排他锁,也就是同一个时刻只会有一个线程能获取到锁,这个主要利用的就是AQS的独占模式实现的。ReentrantLock能保证在多线程的情况下的线程执行安全,那就会想到Synchronized的关键字。Synchronized是JVM实现的,ReentrantLock是由JDK实现的,ReentrantLock相对于比较灵活,可以设置时间等待,线程中断,锁投票等,但是一定用完记得要在finally手动释放,Synchronized是JVM做自动释放锁的操作。

    用法

    /**
     * @ClassName ReentrantLockDemo
     * @Auther burgxun
     * @Description: 重入锁的Demo
     * @Date 2020/4/5 14:21
     **/
    public class ReentrantLockDemo {
    
        private static ReentrantLock reentrantLock = new ReentrantLock();
    
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                int finalI = i;
                new Thread(() -> {
                    reentrantLock.lock();
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("获取锁的线程是:" + finalI);
                    reentrantLock.unlock();
                }).start();
            }
        }
    }
    

    执行结果是:

    org.example.ReentrantLockDemo
    获取锁的线程是:1-开始执行
    获取锁的线程是:1-执行结束
    获取锁的线程是:2-开始执行
    获取锁的线程是:2-执行结束
    获取锁的线程是:4-开始执行
    获取锁的线程是:4-执行结束
    获取锁的线程是:3-开始执行
    获取锁的线程是:3-执行结束
    获取锁的线程是:5-开始执行
    获取锁的线程是:5-执行结束
    
    Process finished with exit code 0
    

    从执行结果上 我们可以看到 一个线程执行完成释放锁后才能执行另外一个线程

    源码分析

    看完了上面的简介和用法,我们进入源码去分析看下 是怎么实现的

    代码结构

    在这里插入图片描述

    方法分析

    从UML类图上面我们可以看到 ReentrantLock有3个内部类,一个是抽象的静态类Sync还有2个实现了Sync的类一个是非公平锁的实现NonfairSync,还有一个是公平锁的实现FairSync

    Sync

    首先我们看下Sync这个抽象类

    abstract static class Sync extends AbstractQueuedSynchronizer {
            abstract void lock();
    
            /**
             * 非公平锁锁的tryAcquire的实现 AQS中tryAcquire方法的需要子类重写
             */
            final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();//获取当前线程
                int c = getState();//获取CAS的State值
                if (c == 0) {//如果等于0 说明可以获取锁
                    if (compareAndSetState(0, acquires)) {//CAS的方式 修改State 状态
                        setExclusiveOwnerThread(current);//CAS修改成功后 设置当前线程为锁的持有者
                        return true;
                    }
                } else if (current == getExclusiveOwnerThread()) {//判断当前线程 是否是锁的持有者线程
                    int nextc = c + acquires;//新增重入次数
                    if (nextc < 0) // overflow  //如果小于0 说明是出问题了 抛出异常
                        throw new Error("Maximum lock count exceeded");
                    setState(nextc);
                    return true;
                }
                return false;
            }
    
            /**
             * AQS 中方法的重写 释放资源
             */
            protected final boolean tryRelease(int releases) {
                int c = getState() - releases;// 算下 当前同步器中state的差值
                //确保释放和加锁线程是同一个  上一篇中我也提到过
                if (Thread.currentThread() != getExclusiveOwnerThread())
                    throw new IllegalMonitorStateException();
                boolean free = false;//是否完全了释放资源
                if (c == 0) {//如果C的值是0 说明没有线程拥有锁了
                    free = true;
                    /*
                     * 设置拥有锁的线程为null 因为现在锁是没线程暂用的  如果不修改 下次别的线程去获取锁的会有这个判断
                     */
                    setExclusiveOwnerThread(null);
                }
                setState(c);//修改 同步器的State 状态
                return free;
            }
    
            /**
             * 判断当前线程 是否是拥有锁的线程一致  重写了AQS的方法
             */
            protected final boolean isHeldExclusively() {
                // While we must in general read state before owner,
                // we don't need to do so to check if current thread is owner
                return getExclusiveOwnerThread() == Thread.currentThread();
            }
    
            final ConditionObject newCondition() {
                return new ConditionObject();
            }
    
            /**
             * 获取当前重入锁的拥有者
             */
            final Thread getOwner() {
                return getState() == 0 ? null : getExclusiveOwnerThread();
            }
    
            /**
             * 当前锁占用的State 数量 也就是重入了几次
             */
            final int getHoldCount() {
                return isHeldExclusively() ? getState() : 0;
            }
    
            /**
             * 重入锁 是否可以被占用  State等于0 别的线程才能来抢占到锁
             */
            final boolean isLocked() {
                return getState() != 0;
            }
        }
    

    上面是我写了注解的Sync的整个类,一会儿我们挨个分析下,从继承关系 我们可以看到Sync是继承于我们的AQS的,所以里面很多底层的方法都是用的AQS里面的实现,所以说呀 理解了AQS 那整个JUC下的锁和各种线程安全的集合什么的 看源码都会轻松很多~

    NonfairSync

    //非公平锁
        static final class NonfairSync extends Sync {
    
            @Override
            void lock() {
                if (compareAndSetState(0, 1))//如果CAS能设置成功 说明能获取到锁 就设置当前线程为锁的owner  不公平的体现
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    acquire(1);
            }
            
            /**
             * 尝试获取资源,复写的AQS类里面方法
             */
            protected final boolean tryAcquire(int acquires) {
                return nonfairTryAcquire(acquires);
            }
        }
    
    

    FairSync

      //公平锁
        static final class FairSync extends Sync {
    
            @Override
            void lock() {
                acquire(1);
            }
    
            /**
             * Fair version of tryAcquire.  Don't grant access unless
             * recursive call or no waiters or is first.
             */
            protected final boolean tryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                int c = getState();
                if (c == 0) {
                    /*
                     * hasQueuedPredecessors 返回是否Sync中是否有在当前线程前面等待的线程 false没有
                     * 如果false 那就CAS 修改State 成功后更新当前线程是锁的拥有者线程
                     * */
                    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;
            }
        }
    

    非公平锁VS公平锁

    什么是公平非公平

    看了上面的代码 有人可能不明白什么叫做TMD公平!!!哈哈
    具体的代码下文描述,我先举个小例子,让读者带入下:

    话说大家在火车站排队买票,小明和小强放寒假了,都去火车站提前买票准备回家,他们俩到了车站一看,售票窗口就开了一个,而且前面有3个人在排队,那小明和小强没办法就只能排除在了队伍里面,等了5分钟终于排到了小强,小强刚买完票,突然看到同个宿舍的小张也来买票了,立马和小张说,来来来 这边 我这边可以买票,小张立马插队到了小强后面,强行去买了票,小明心里嘀咕说居然插队,素质真差,看你人高马大的 就算了吧!哈哈~

    其实上面的类子中 公平和非公平的体现就是 卖票的窗口只有一个,就像获取独占锁,当有一个线程占有了锁,那其余的线程就必须在后面排队等待,就像买票一样,非公平的锁的实现就相当于插队,管你后面有没有人 我都要去尝试下买票

    从上面的分析我们也能知道公平和非公平是指的是获取资源时候的行为。

    ReentrantLock

    ReentrantLock的构造函数

    /**
        * 默认实现非公平锁
        */
       public ReentrantLock() {
           sync = new NonfairSync();
       }
    
       /**
        * 带参数的构造函数
        */
       public ReentrantLock(boolean fair) {
           sync = fair ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync();
       }
    

    从上面可以看到ReentrantLock默认使用的是一个非公平锁的实现

    lock加锁方法

        /**
         * 加锁
         */
        @Override
        public void lock() {
            sync.lock();
        }
    
        /**
         * 加锁 响应中断
         */
        @Override
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }
    
        @Override
        /**
         * 只做一次获取锁的尝试  不会阻塞线程
         */
        public boolean tryLock() {
            return sync.nonfairTryAcquire(1);
        }
    
        @Override
        /**
         * 只做一次获取锁的尝试  不会阻塞线程
         */
        public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(timeout));
        }
    

    非公平的加锁

    非公平的加锁

    从代码中 我们看到lock方法调用的是Sync的lock方法,但是Sync中的lock方法是一个抽象方式是子类实现的
    那我从NonfairSync类中找到了lock的具体实现,入下:

     void lock() {
             //如果CAS能设置成功 说明能获取到锁 就设置当前线程为锁的owner  不公平的体现
                if (compareAndSetState(0, 1))
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    acquire(1);
            }
    

    首先方法一开始先执行一个CAS修改State的操作,如果能够执行成功,说明当前的AQS同步器中的状态值是0,那么线程就可以占有锁,然后设置当前线程为锁的Owner线程.setExclusiveOwnerThread这个方法我在第一篇文章中也描述过,这个方法是在AQS的父类AbstractOwnableSynchronizer方法里面的!
    如果执行失败,那么方法就执行acquire方法:

     /**
         * 此方法位于AQS中
         * 获取资源
         */
        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
        
        /**
         * 此方法位于NonfairSync中
         * 尝试获取资源,复写的AQS类里面方法
         */
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
            
            
         /**
         * 此方法位于Sync中
         * 非公平锁锁的tryAcquire的实现 AQS中tryAcquire方法的需要子类重写
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//获取当前线程
            int c = getState();//获取CAS的State值
            if (c == 0) {//如果等于0 说明可以获取锁
                if (compareAndSetState(0, acquires)) {//CAS的方式 修改State 状态
                    setExclusiveOwnerThread(current);//CAS修改成功后 设置当前线程为锁的持有者
                    return true;
                }
            } else if (current == getExclusiveOwnerThread()) {//判断当前线程 是否是锁的持有者线程
                int nextc = c + acquires;//新增重入次数
                if (nextc < 0) // overflow  //如果小于0 说明是出问题了 抛出异常
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    

    acquire方法位于AQS中 此方法会首先调用tryAcquire方法去尝试获取下锁,因为虽然lock方法一开始获取锁失败了,可能这边锁又被别的线程释放了,所以要再次尝试获取下锁,具体tryAcquire怎么做的 上面的代码中我已经描述的很清楚了~

    公平的加锁

    公平的加锁

    先看下代码:

         /**
         * 公平锁版本的加锁
         */
        void lock() {
            acquire(1);
        }
    
        /**
         * 此方法位于AQS中
         * 获取资源
         */
        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
            
    
        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                /*
                 * hasQueuedPredecessors 返回是否Sync中是否有在当前线程前面等待的线程 false没有
                 * 如果false 那就CAS 修改State 成功后更新当前线程是锁的拥有者线程
                 * */
                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的时候,直接执行了acquire方法,而非和NonfairSync一样,有个CAS的尝试操作,这也是体现公平的一部分,和非公锁的方法一样的是这边的acquire方法也是在AQS上中的,但是调用的tryAcquire方法 是自己fairSync类自己实现的,而非调用的Sync的默认实现,这边唯一有区别的就是hasQueuedPredecessors 这个方法,hasQueuedPredecessors方法是获取当前Sync队列中是否还有别的等待线程,如果有 就算当前的状态State是满足条件的,也是要加入Sync等待队列中的,这个是acquireQueued方法里面做的事情,不清楚这个acquireQueued方法的,看看前面几篇文章,我就不再赘述了~

    unlock解锁

    @Override
        public void unlock() {
            sync.release(1);//调用的AQS里面的方法
        }
        
         /**
         * 此方法在AQS中
         * 释放当前资源
         * 唤醒等待线程去获取资源
         */
        public final boolean release(int arg) {
            if (tryRelease(arg)) {//说明释放资源成功
                Node h = head;//当前队列里面的head节点
                if (h != null && h.waitStatus != 0)
                    unparkSuccessor(h);//通知阻塞队列里面的head的next节点去获取锁
                return true;
            }
            return false;
        }
        
        
         /**
         * 此方法在Sync中
         * AQS 中方法的重写 释放资源  
         */
        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;// 算下 当前同步器中state的差值
            //确保释放和加锁线程是同一个  上一篇中我也提到过
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;//是否完全了释放资源
            if (c == 0) {//如果C的值是0 说明没有线程拥有锁了
                free = true;
                /*
                 * 设置拥有锁的线程为null 因为现在锁是没线程暂用的  如果不修改 下次别的线程去获取锁的会有这个判断
                 */
                setExclusiveOwnerThread(null);
            }
            setState(c);//修改 同步器的State 状态
            return free;
        }
        
    

    从源码上看 解锁的方法只有一个 因为公平锁和非公共锁 只是描述的是加锁的行为,解锁的行为其实都是一致的,都是释放当前线程占用State值,然后唤醒SyncQueue的头部Head节点的下一个节点去尝试获取锁!

    有些没帖子上面的方法描述 请在前面AQS中的几篇文章中看下 都详细描述过~

    总结

    公平锁 VS 非公平锁

    首先2者并没有好坏之分,是要根据对应的场景选择对应的锁技术

    公平锁 则重的是公平性

    非公平锁 则重的是并发性

    非公平锁 是抢占式的,忽略了SyncQueue重其他的等待线程,线程在进入等待队列之前会进行2次尝试获取锁,这大大增加了获取锁的机会,这种好处体现在2个方面:

    • 线程不必加入等待队列就可以获取锁,不仅免去了构造节点并加入队列的繁琐操作,同时也节省了线程阻塞的唤醒开销,线程阻塞和唤醒涉及到线程上下文的切换和操作系统的系统调用,是非常耗时的。在高并发的情况下,如果线程持有锁的时间非常短,短到线程入队阻塞的过程超过了线程持有并释放锁的时间的开销,那么这种抢占式的特性对并发的性能的提高会很明显
    • 减少CAS竞争,如果线程必须要加入阻塞队列才能去获取锁,那么入队时的CAS竞争将变得异常的激烈,CAS操作虽然不会导致线程失败而挂起,但不断的失败重试导致对CPU的浪费是不能忽略的

    Synchronized VS ReentrantLock

    从整个文章的分析来看,ReentrantLock是比Synchronized更加的灵活的,

    • ReentrantLock提供了更多 更全面的API 可以设置等待时间,可以中断方法等,还提供了Trylock等非阻塞的方法
    • ReentrantLock还可以配和Condition一起使用,使得线程等待的时候更加灵活,可以设置不同的条件等待 等等
  • 相关阅读:
    【2021-01-26】保持学习,也是一种科学的健康保养方式
    【2021-01-25】正常市场化的企业该有的现象
    day 42 mysql 数据库(2)
    day 41 数据库初学习
    在排序数组中查找元素的第一个和最后一个位置
    飞机游戏
    两个数组的交集 II
    重复N次的元素
    单词规律
    存在重复元素2
  • 原文地址:https://www.cnblogs.com/burg-xun/p/12638786.html
Copyright © 2020-2023  润新知