美文网首页
AQS(AbstractQueuedSynchronizer )

AQS(AbstractQueuedSynchronizer )

作者: 仕明同学 | 来源:发表于2020-06-24 20:46 被阅读0次

    AbstractQueuedSynchronizer ?

    队列同步器AbstractQueuedSynchronizer(以下简称同步器或AQS),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。并发包的大师(Doug Lea)期望它能够成为实现大部分同步需求的基础。


    image.png
    • 可以看到ReentranLock和ReentrantReadWriteLock实现的原理底层
      Timer在底层的实现就是使用的ReentranLock

      image.png
    • ReentrantReadWriteLock是个读写锁,主要是用来一边读,一边写这样的功能

        private final ReadWriteLock lock = new ReentrantReadWriteLock();
        private final Lock getLock = lock.readLock(); // 读锁
        private final Lock setLock = lock.writeLock(); // 写锁
    
    

    ReentrantLock是一个互斥锁,也是一个可重入锁(Reentrant就是再次进入的意思)。ReentrantLock锁在同一个时间点只能被一个线程锁持有,但是它可以被单个线程多次获取,每获取一次AQS的state就加1,每释放一次state就减1

    AQS使用方式和其中的设计模式

    • AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,在AQS里由一个int型的state来代表这个状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的
    image.png
    • 这就是CAS的运用
      image.png
      private static final Unsafe unsafe = Unsafe.getUnsafe();

    AQS模板方法模式

    同步器的设计基于模板方法模式。模板方法模式的意图是,定义一个操作中的算法的骨架,而将一些步骤的实现延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤

    模板方法模型是一种行为设计模型。模板方法是一个定义在父类别的方法,在模板方法中会呼叫多个定义在父类别的其他方法,而这些方法有可能只是抽象方法并没有实作,模板方法仅决定这些抽象方法的执行顺序,这些抽象方法的实作由子类别负责,并且子类别不允许覆写模板方法。

    image.png

    在安卓中 onDraw、onMeasure、onLayout 其实就是模板方法 ,还有Activity的生命周期也是

    • 安卓中view中onDraw的步骤


      image.png
    • 绘制遍历执行几个绘制步骤,这些步骤必须执行
      • 以适当的顺序:
      • 1.绘制背景
      • 2.如有必要,请保存画布的图层以准备褪色
      • 3.绘制视图的内容,去onDraw(canvas);
      • 4.画孩子,去 dispatchDraw(canvas);
      • 5.如有必要,绘制褪色边缘并恢复图层
      • 6.绘制装饰(例如滚动条) onDrawForeground(canvas)
      • 7.draw the default focus highlight =绘制默认焦点高光

    访问或修改同步状态的方法

    重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。

    • getState():获取当前同步状态。
    • setState(int newState):设置当前同步状态。
    • compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

    CLH队列锁

    • CLH队列锁即Craig, Landin, and Hagersten (CLH) locks。
    • CLH队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。


      image.png

    当一个线程需要获取锁时:

    • 1.创建一个的QNode,将其中的locked设置为true表示需要获取锁,myPred表示对其前驱结点的引用


      image.png
    • 2.线程A对tail域调用getAndSet方法,使自己成为队列的尾部,同时获取一个指向其前驱结点的引用myPred


      image.png
    • 3.线程B需要获得锁,同样的流程再来一遍


      image.png
    • 4.线程就在前驱结点的locked字段上旋转,直到前驱结点释放锁(前驱节点的锁值 locked == false)
    • 5.当一个线程需要释放锁时,将当前结点的locked域设置为false,同时回收前驱结点


      image.png

    前驱结点释放锁,线程A的myPred所指向的前驱结点的locked字段变为false,线程A就可以获取到锁。
    CLH队列锁的优点是空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail)。CLH队列锁常用在SMP体系结构下。
    Java中的AQS是CLH队列锁的一种变体实现。

    Lock &ReentrantLock&ReentrantReadWriteLock

    image.png
    • Lock是一个接口,ReentrantLock和ReentrantReadWriteLock实现了它


      image.png

    ReentrantLock的使用,如果是使用 synchronize 来做同步处理时,锁的获取和释放都是隐式的,实现的原理是通过编译后加上不同的机器指令来实现。而 ReentrantLock 就是一个普通的类,它是基于AQS(AbstractQueuedSynchronizer)来实现的。

    ReentrantLock是一个重入锁:一个线程获得了锁之后仍然可以反复的加锁,不会出现自己阻塞自己的情况。


    image.png
    image.png

    ReentrantLock 分为公平锁和非公平锁,可以通过构造方法来指定具体类型

    //默认非公平锁
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    //公平锁
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    

    非公平锁和公平锁不同的地方是:非公平锁,lock的时候,直接开始CAS,但是公平锁会先查询一下:查询是否有任何线程在等待获取更长的时间比当前线程。默认一般使用非公平锁,它的效率和吞吐量都比公平锁高的多
    nonfairTryAcquire方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。同步状态表示锁被一个线程重复获取的次数。

    image.png

    nonfairTryAcquire(int acquires)方法,对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同。tryAcquire方法,该方法与nonfairTryAcquire(int acquires)比较,唯一不同的位置为判断条件多了hasQueuedPredecessors()方法,即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

    image.png

    公平锁与非公平锁的差异主要在获取锁:
    公平锁就相当于买票,后来的人需要排到队尾依次买票,不能插队。
    而非公平锁则没有这些规则,是抢占模式,每来一个人不会去管队列如何,直接尝试获取锁。

    公平锁与非公平锁,来之美团技术的理解,可以说非常的形象了

    • 公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

    假设有一口水井,有管理员看守,管理员有一把锁,只有拿到锁的人才能够打水,打完水要把锁还给管理员。每个过来打水的人都要管理员的允许并拿到锁之后才能去打水,如果前面有人正在打水,那么这个想要打水的人就必须排队。管理员会查看下一个要去打水的人是不是队伍里排最前面的人,如果是的话,才会给你锁让你去打水;如果你不是排第一的人,就必须去队尾排队,这就是公平锁。


    image.png

    对于非公平锁,管理员对打水的人没有要求。即使等待队伍里有排队等待的人,但如果在上一个人刚打完水把锁还给管理员而且管理员还没有允许等待队伍里下一个人去打水时,刚好来了一个插队的人,这个插队的人是可以直接从管理员那里拿到锁去打水,不需要排队,原本排队等待的人只能继续等待


    image.png

    ReentrantLock里面有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),添加锁和释放锁的大部分操作实际上都是在Sync中实现的。它有公平锁FairSync和非公平锁NonfairSync两个子类。ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁。

    看一下公平锁与非公平锁的加锁方法的源码:

    image.png
    通过上图中的源代码对比,我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors() image.png
    • 再进入hasQueuedPredecessors(),可以看到该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。

    综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。


    ReentrantReadWriteLock 是读写锁,一般我们使用在读和写的场景,比如说实现一个图片的缓存,ImageCache Java中的源码实现的思路

    private ReadWriteLock lock = new ReentrantReadWriteLock();

    • getImage就是读取,从内存中读出来


      image.png
    • setImage 就是写,写入内存中


      image.png
    • Demo


      image.png

    • 其实在很多安卓的源码都可以看到读写锁,比如在BluetoothAdapter
      image.png
      image.png
      image.png

    所以说你在牛逼,安卓如果想脱离Java生态,根本不现实,哈哈!但是也可以说写源码的真牛逼

    • 最后说明
    • 何时使用 ReentrantLock和synchronize关键字?个人理解,当你需要在运行过程中需要拿到锁,就去使用 ReentrantLock,其余的情况都去使用synchronize,因为synchronize在Java5.0进行了很大的优化,性能会很高------> 具体是无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。
    • 同理 ReentrantReadWriteLock ,有这种业务场景,就去使用
    • 我看过最好的文章,美团技术出品 ReentrantLock的实现看AQS的原理及应用
    • 不可不说的Java“锁”事
    • 以上都是自己的理解,难免有差错

    相关文章

      网友评论

          本文标题:AQS(AbstractQueuedSynchronizer )

          本文链接:https://www.haomeiwen.com/subject/bdfvzhtx.html