暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

AbstractQueuedSynchronizer

Java Miraculous 2021-01-19
184
上一篇文章较深入的分析了synchronized关键字实现同步的原理,我们也知道了synchronized在JDK1.6之后做了很大的优化,由原来直接上重量级锁到后来有了锁膨胀升级的过程,那么我们是不是用synchronized就可以完美解决线程安全问题了?答案是no,synchronized可以解决线程安全问题,但并不是完美解决,那么为什么不能完美解决?有完美的方案吗?今天就来聊聊~_~。
一、synchronized为什么不是完美的?
1、synchronized是JVM的内置锁,内置锁的意思就是加锁和解锁操作都是JVM控制的,在底层对应了monitorenter和monitorexit两个命令,那么这就导致了一个问题,我们不能跨方法加锁和解锁,有人也许说怎么不可以跨方法?在一个synchronized方法里调用另外一个方法不就行了?我说的不是这样的,我说的是在方法1中加锁,在方法2中解锁,你有办法吗?其实是有的,但是不推荐使用,有兴趣的可以看下我这篇文章:Unsafe魔术类。你可以调用Unsafe类的方法进行加解锁,但是这样直接跨过了JVM虚拟机,是不推荐的。

2、synchronized的锁膨胀升级的过程是不可逆的,这里假如有一个场景,拿外卖来说,外卖的高峰期一般是中午和晚上,因为中午订单量大,线程间的竞争就大,如果系统半夜刚启动,这时候是无锁甚至是偏向锁,到早上就算是轻量级锁,但到了中午就成了重量级锁了,到了重量级锁后,系统性能会下降不少,那我到晚上了订单量增大,系统性能会越来越慢,除非重启,这时候你还会觉得用synchronized合适吗?
根据以上两点,其实你会发现synchronized并不是完美的,而且不知道你发现没有,实际开发中用到多线程你会用synchronized吗?我是没怎么用过,倒是ReentrantLock、CountDownLatch我没少用。
二、上面所提到的ReentrantLock、CountDownLatch其实是基于一个线程同步框架实现的,它就是AbstractQueuedSynchronizer,以下简称AQS,那么什么是AQS?Java里并发编程的核心在于java.util.concurrent包,以下简称juc,而juc中大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于AQS,AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。你在juc包下面随便找几个类点进去看,会发现作者都是Doug Lea这么一个人,java编程界流传着这么一句话:生平不识Doug Lea,学懂并发也枉然,从这句话可以看出,老爷子明显在另一个次元,上张帅照(老爷子竟然不是“绝顶”高手)。

通过上面的介绍可知,AQS其实是一个抽象,那有没有基于AQS的实际体现呢?咱就以ReentrantLock为例说下,那么ReentrantLock是怎么基于AQS做一系列操作的,看下:

其实它是定义了一个静态的内部类去继承AQS这个抽象类,然后再基于AQS中的方法去定义自己的方法。

ReentrantLock是悲观锁,也是可重入的,悲观锁就是同一时刻只能有一个线程获得锁,可重入就是一把锁可以被同一个线程多次获得,有两个重要的属性,一个是AQS中的state,记录锁重入的次数,另外一个是AQS父类AbstractOwnableSynchronizer中的exclusiveOwnerThread属性,叫当前拥有锁的线程,AQS中的Q代表queue,队列,如果同一时刻有多个线程过来抢占锁,最后肯定是只有一个线程获得锁,其它线程就会到队列里等待,这个队列就是基于AQS里的Node类构建的:

AQS中使用的队列是CLH队列的一个变种,因为CLH队列里的线程不是阻塞等待而是自旋等,这个队列里的节点有几个重要的属性:

    /**
    * 共享模式节点
    * */
    static final Node SHARED = new Node();
    /**
    * 独占模式节点
    */
    static final Node EXCLUSIVE = null;


    /**
    * 如果在同步队列中的线程等待超时或者被中断,
    * 在同步队列中等待的线程等待超时或者被中断,需要从同步队列中取消等待
    * */
    static final int CANCELLED = 1;
    /**
    * 后继节点的线程处于等待状态,而当前的节点如果释放了同步状态或者被取消,
    * 将会通知后继节点,使后继节点的线程得以运行。
    */
    static final int SIGNAL = -1;
    /**
    * 节点在等待队列中,节点的线程等待在Condition上,当其他线程对Condition调用了signal()方法后,
    * 该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中
    */
    static final int CONDITION = -2;
    /**
    * 表示下一次共享式同步状态获取将会被无条件地传播下去
    */
    static final int PROPAGATE = -3;


    /**
    * 标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态
    * 使用CAS更改状态,volatile保证线程可见性,高并发场景下,
    * 即被一个线程修改后,状态会立马让其他线程可见。
    */
    volatile int waitStatus;

    构建什么样的队列,以及队列中的节点目前处于什么状态吗,都是通过修改waitStatus属性为以上五个值来实现的。AQS中的队列分为同步队列和条件队列。Node中哪些属性用于同步队列,哪些属性又用于条件队列呢?

    上面红框的属性都是各自队列独有的,本篇文章只介绍同步队列,由于条件队列需要特定知识做铺垫,就放到后续章节中说。

    下面通过一个小Demo分析下ReentrantLock加锁和解锁以及其中同步队列的原理:

      package com.ayo.dubbo.consumer;


      import java.util.concurrent.locks.ReentrantLock;


      /**
      * 1、分析ReentrantLock中的可重入锁
      * 2、分析ReentrantLock中的同步队列
      *
      * @Authror ayo
      * @Date 2021/1/19 14:32
      */
      public class LockDemo {


      /**
      * 定义一把杀猪刀
      */
      private ReentrantLock knife = new ReentrantLock(true);


      /**
      * 杀猪
      * @param peopleName
      */
      public void KillThePig(String peopleName){
      System.out.println(peopleName + "准备杀猪");


      //要杀猪得先拿起刀
      knife.lock();
      System.out.println(peopleName + "杀第一头猪");


      //又拿起刀(很多人等着用刀),但是必须杀够两头后才会把刀给别人
      knife.lock();
      System.out.println(peopleName + "杀第二头猪");


      /**
      * 加几次锁,就得释放几次,这样才能完全释放掉
      */
      knife.unlock();
      knife.unlock();
      System.out.println("归还杀猪刀");
      System.out.println("---------------------");
      }


      public static void main(String[] args) {
      final LockDemo lockDemo = new LockDemo();
      new Thread(new Runnable() {
      @Override
      public void run() {
      String name = Thread.currentThread().getName();
      lockDemo.KillThePig(name);
      }
      }, "老王").start();


      /*new Thread(new Runnable() {
      @Override
      public void run() {
      String name = Thread.currentThread().getName();
      lockDemo.KillThePig(name);
      }
      }, "老王媳妇").start();*/
      }
      }

      (一)、首先看第一条,可重入锁,在killThPig方法的第一行打个断点,然后debug方式运行main方法,一步一步观察knife中的属性变化:

      可以看到state的变化,加一次锁,state的值就+1,释放一次锁,state的值就-1,exclusiveOwnerThread如上所说,就是当前持有锁的线程。那么它加锁的原理是什么,这就得看看源码了,一步步点进去lock方法:

      最后会发现是一个抽象方法,看下它的实现:

      会发现有两个,那应该看哪一个呢?看下刚才我写的demo,类的第一行的:

        /**
        * 定义一把杀猪刀
        */
        private ReentrantLock knife = new ReentrantLock(true);

        构造ReentrantLock的时候传入的是true,点进去看你会发现:

        这里说下,其实这个参数的含义表示是否公平,true代表是一把公平锁,false代表是一把非公平的锁,举个例子:下班高峰期你去坐地铁,人超级多,这时候但凡是个有素质的人就会在那排队进站,这就是公平锁,如果遇到个没素质的人不排队直接去前面进站,这就是非公平锁

        1、先看公平锁的实现

          public final void acquire(int arg) {
          //尝试获取锁
          if (!tryAcquire(arg) &&
                      /*如果获取失败就尝试进入队列,注意进入队列的方式是独占,
                      ReentrantLock是一个独占锁,独占的意思是同一时刻只有一个
                      线程能持有锁,相反共享就是这把锁可以同时被多个线程持有*/
          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();
            //获取state的值
            int c = getState();
                        //判断如果state的值是0,标识锁没有被任何线程持有
            if (c == 0) {
                            /*为毛说是公平的,就在于这里,它先去判断了队列里有没有排队
                            的,没有的话再去修改state的值*/
            if (!hasQueuedPredecessors() &&
                                //修改state的值为1,标识抢锁
            compareAndSetState(0, acquires)) {
                                //将自己的线程设置为当前持有锁的线程
            setExclusiveOwnerThread(current);
            return true;
            }
            }
                        //否则如果当前持有锁的线程是自己
            else if (current == getExclusiveOwnerThread()) {
            //那就把state的值+1
            int nextc = c + acquires;
                            //如果state的值+1后小于0,就抛出异常
            if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
                            //将state的值+1
            setState(nextc);
            return true;
            }
            return false;
            }


            2、非公平锁的实现

              final void lock() {
                         //一上来我不管3721我先修改state的值为1,也就是抢锁
              if (compareAndSetState(0, 1))
                              //如果抢锁成功就把独占线程设置为自己
              setExclusiveOwnerThread(Thread.currentThread());
              else
                              //没抢成功,就去按公平锁的方式获取
              acquire(1);
              }

              可以看到,非公平锁其实就是一上来先去抢下试试,赌的就是锁整好被别的线程释放,这时候有可能抢到的,为什么说有可能?因为如果同步队列里有别的线程的话,持有锁的线程释放锁之后,会通知队列里的线程去抢锁,这就行成一定的竞争。

              源码里用到了大量的CAS算法去修改值,你点进去之后会发现其实用的都是unsafe类直接操作内存实现的:

              包括变量都是用volatile修饰,用来防止并发操作带来脏数据。

              (二)、分析ReentrantLock中的同步队列

              接下来把上面demo中的老王媳妇注释放开,再debug,这时候观察sync的属性:

              这时候head和tail是不是有值了?其实本质上这个同步队列是一个由AQS中的Node节点组成的双向链表,链表中的每个节点通过waitStatus来标识当前节点所处的状态。

              关于具体是怎么入队列,以及节点之间的指针是怎么连接的,主要是AQS中的这段源码:

              有兴趣的童鞋可以研究下,这里不再赘述,然后看下解锁方法:

                protected final boolean tryRelease(int releases) {
                //将state的值减1
                int c = getState() - releases;
                            //如果当前线程不是持有锁的线程,抛出异常,因为你本来就没持有锁,搁这释放个毛线啊
                if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
                boolean free = false;
                            //判断如果state减1后的值为0
                if (c == 0) {
                //释放锁成功
                free = true;
                                //将持有锁的线程置为空
                setExclusiveOwnerThread(null);
                }
                            //修改state的值
                setState(c);
                return free;
                }

                关于AQS和ReentrantLock就说到这,没看过源码的可能觉得有点陌生,多看看就好了,我以前也是见源码就排斥,但是看多了,每次看到不懂的都想点进去。

                文章转载自Java Miraculous,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                评论