【并发】5、抽象队列同步器AQS应用ReentrantLock

AQS

Java并发编程核心在于java.concurrent.util包而juc当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。

AQS的继承关系

AQS的继承关系如下图:

  • ReentrantLock就是定义了内部类Sync,这个内部类继承承AbstractQueuedSynchronized.对该抽象类的部分方法做了实现;并且还定义了两个子类:FairSync和NonfairSync
  • 除了Lock外,Java.concurrent.util当中同步器的实现如Latch,Barrier,BlockingQueue等,
    都是基于AQS框架实现
    • 一般通过定义内部类Sync继承AQS
    • 将同步器所有调用都映射到Sync对应的方法
      在这里插入图片描述

AQS具备特性

  • List item
  • 阻塞等待队列
  • 共享/独占
  • 公平/非公平
  • 可重入
  • 允许中断
    可中断特性就是因为阻塞用的是LockSupport.park(),这种阻塞可以通过interrupt唤醒

AbstractQueuedSynchronized类

重要属性

重要属性

  • 超类的属性exclusiveOwnerThread:记录当前获取锁的线程是谁
    AbstractQueuedSynchronized继承的AbstractOwnableSynchronizer的属性
    在这里插入图片描述

  • AQS类下的变量state状态器,表明当前同步器的状态
    state表示资源的可用状态,state为0表明是无锁状态,没有被任何一个线程持有

    volatile int state (32)
    

    state的三种访问方式:

    • getState()
    • setState()
    • compareAndSetState()在这里插入图片描述
  • AQS的head属性会指向Node的头部,tail属性会指向Node的尾部,形成同步等待队列CLH队列
    在这里插入图片描述

    形成的双向队列:
    在这里插入图片描述

内部类:Node

在这里插入图片描述

AQS会基于Node构建双向队列
Node的重要属性:

  • Node的prev和next属性用来形成双向链表。

    在这里插入图片描述

  • Node的thread属性用来保持对线程的引用

  • Node的SHARED属性表示锁是共享锁,Semaphore锁是共享的
    在这里插入图片描述

  • Node的EXCLUSIVE属性表示锁是互斥的,ReentrantLock需要锁是互斥的

  • Node的waitestate属性表示当前结点的生命状态(信号量)

    • SIGNAL=-1 可被唤醒
    • CANCELLED=1 代表出现异常,中断引起的,需要废弃结束
    • CONDITION=-2 条件等待
    • PROPAGATE=-3 传播
    • 0 是初始状态Init状态

AQS定义两种资源共享方式

  • Exclusive-独占,只有一个线程能执行,如ReentrantLock Share-共享
  • 多个线程可以同时执行,如Semaphore/CountDownLatch

AQS定义两种队列

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共 享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/ 唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去 实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回 false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回 false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成 功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续 等待结点返回true,否则返回false。
同步等待队列 CLH

AQS当中的同步等待队列也称CLH队列,CLH队列是Craig、Landin、Hagersten三人 发明的一种基于双向链表数据结构的队列,是FIFO先入先出线程等待队列,Java中的CLH 队列是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。
在这里插入图片描述

条件等待队列

Condition是一个多线程间协调通信的工具类,使得某个,或者某些线程一起等待某个 条件(Condition),只有当该条件具备时,这些等待线程才会被唤醒,从而重新争夺锁

在这里插入图片描述

ReentrantLock

ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。而且它具有比 synchronized更多的特性,比如它支持手动加锁与解锁,支持加锁的公平性。

//使用ReentrantLock进行同步
ReentrantLock lock = new ReentrantLock(false);//false为非公平锁,true为公平锁
lock.lock() //加锁
lock.unlock() //解锁

Lock的特性:

  • 可重入
    这个特性就是加了几次锁也要释放几次锁
    synchronized也有可重入性
  • 公平性与非公平性

ReentrantLock如何实现synchronized不具备的公平与非公平性呢?

在ReentrantLock内部定义了一个Sync的内部类,该类继承AbstractQueuedSynchronized,对该抽象类的部分方法做了实现;并且还定义了两个子类:

  1. FairSync 公平锁的实现
  2. NonfairSync 非公平锁的实现

这两个类都继承自Sync,也就是间接继承了AbstractQueuedSynchronized,所以这一个ReentrantLock同时具备公平与非公平特性。

上面主要涉及的设计模式:模板模式-子类根据需要做具体业务实现


实现锁的核心:

  • CAS
    保证加锁永远只有一个线程能够成功
  • LockSupport
    对线程阻塞和唤醒
  • 自旋
    cas加锁失败,则这些线程就要自旋,阻塞住 就不占用cpu资源
  • queue
    放阻塞的那些线程,用队列是因为队列的FIFO可以保证公平性

CAS

CAS: Compare and swap
CAS能够保证不管并发有多高,都能保证这个执行的原子性。通过cas算法去加锁,这样保证加锁永远只有一个线程能够成功。

CAS 工作原理

主内存中有一个expect=0,如果两个线程都要去修改expect,则CAS就会让这两个线程都复制一份到自己线程中,然后再用另一个变量比如是refresh存修改后的值, 线程A和线程B就都去和主内存比较 如果expect的值相等,则将refresh中的值修改主内存的值,如果expect值不等,则不改主内存的值。
在这里插入图片描述

CAS的使用

Unsafe类中提供了三个关于CAS的方法:
在这里插入图片描述

LockSupport

线程阻塞就不会占用cpu的资源
通过java的LockSupport.part() 就可以阻塞线程

LockSupport.part() 有参和无参

LockSupport的part有两种:

  • 无参 LockSupport.part()
    如果阻塞的线程被interrupt 唤醒,则永远不再会阻塞这个线程
    在这里插入图片描述
    所以结果会一直打印那两句:
    在这里插入图片描述

  • 有参
    被中断只会唤醒一次,下一次继续阻塞
    在这里插入图片描述

LockSupport.park和unpark的使用

在这里插入图片描述

AQS源码分析

源码解析Lock()

公平锁:
FairSync的lock()方法调用acquire()方法
在这里插入图片描述
acquire()方法:
在这里插入图片描述

tryAcquire 尝试去获取锁

  • tryAcquire 尝试去获取锁:
    在这里插入图片描述

    • 通过Thread.currentThread()获取当前线程的引用
    • getState() 获取同步器的状态
      • c==0 无锁状态
        • 对于公平锁,首先要判断是否有线程在排队
          通过判断队列的队头队尾是否一样在这里插入图片描述

        • 没有线程排队,才用CAS去加锁,加锁其实就是把改同步器的状态为1

        • 把当前线程的引用赋给exclusiveOwnerThread

      • c!=0
        • 第一种情况,这个锁是被当前线程持有的,则再对state++
          Lock的可重入性就是通过这部分逻辑做到的
        • 第二种情况,这个锁是被其他线程持有的,则返回false表示加锁失败

addWaiter 线程入队

  • addWaiter 线程入队
    前面的tryAquire没有获取到锁,所以要添加当前线程到队列
    返回队尾的node
    在这里插入图片描述

    • 创建Node结点
      入参是当前线程引用和mode是EXCLUSIVE互斥锁,这时默认waiteState即为0

    • enq(node) 将node入队
      在这里插入图片描述

      • t==null 则给队列初始化
        构建队列要先给队列做初始化,即创建一个空结点,thread为null,队头head队尾tail同时指向这个创建好的空结点
        在这里插入图片描述

      • t!=null 进行入队操作
        入队也存在竞争,为了保证所有阻塞线程对象能够被唤醒即都能入队,所以要用CAS保证入队的原子性

        • 把prev指向t即队尾指向的node
        • 用CAS的方式移动尾部指针
        • 原来尾部即t的node的next指向当前入队的node
          在这里插入图片描述

acquireQueued 阻塞函数

  • acquireQueued 阻塞
    **在这里插入图片描述**

    • 如果当前node是队列的第一个 则再通过tryAcquire尝试获取锁,尽可能避免线程被阻塞。
      • 获取到锁了 节点就出队。 并且把head往后挪一个节点
        通过setHead 把head指向当前node,并把当前node的thread、pred置位null,也就是变成了一个空结点在这里插入图片描述
      • 如果没有抢到锁 就阻塞
        第一轮循环,通过shouldParkAfterFailedAcquire修改head的状态为-1即SIGNAL
        第二轮循环,阻塞线程。
        • shouldParkAfterFailedAcquire
          在这里插入图片描述

          取出前驱结点的状态waitStatus,当前结点能否被唤醒取决于前驱结点的状态

          • 如果前驱结点的状态是signal ws==Node.SIGNAL 直接返回true代表是当前结点可唤醒的
          • ws>0 代表前驱节点 出现异常要被cancelled
          • ws是0或propagate,我们就通过CAS方式将前驱节点设置为可唤醒状态SIGNAL,即将ws设置为-1.
            head的waitState设置为-1的原因:因为持有锁的线程T0在释放锁的时候,会去唤醒队列中排队的第一个线程T1,要判断head的waitState是否!=0。成立的话会把waitState改为0,然后把把T1被唤醒;T1接着走循环去抢锁,可能会再失败(在非公平锁场景下),就会再次被阻塞,head的节点就又经历两轮循环 waitState从0又变成-1.
        • parkAndCheckInterrupt 阻塞线程,并且需要判断线程是否是由中断信号唤醒的

          • 调用LockSupport.park进行阻塞,唤醒park阻塞的线程有两种方式:

            • unlock 调用LockSupport.unpark() 唤醒
            • 给线程发一个中断信号(业务逻辑掉当前线程的.interrupt),线程就会从阻塞状态唤醒
          • Thread.interrupted() 获取当前线程中断的状态
            这里的Thread.interrupted() 为true就说明他是通过中断信号方式被唤醒的
            在这里插入图片描述

selfInterrupt() 打上中断标记

  • selfInterrupt()
    当没有获取到锁且acquireQueued返回true的时候就会执行selfInterrupt(),Thread.currentThread.interrupt() 就是给当前线程打上一个中断的标记
    因为在parkAndCheckInterrupt return 的是Thread.interrupted() 是true说明当前线程是通过中断的方式唤醒的,但是调用了Thread.interrupted()后那个中断标记就被清除了,所以我们这里要把中断标记打上,这样外围程序员自己定义的代码就能识别到中断信号,就知道是被中断唤醒的

在这里插入图片描述

源码解析unLock()

持有锁的这边逻辑,执行unlock(),unlock()中调用release()
在这里插入图片描述

release()

public final boolean release(int arg) {
//尝试解锁,因为重入锁,所以要stack=0 才可以完全解锁
    if (tryRelease(arg)) {
    //如果解锁成功,队列不为空,而且真实首节点状态要为-1
        Node h = head;
        if (h != null && h.waitStatus != 0)
        //解除阻塞状态
            unparkSuccessor(h);
        return true;
    }
    return false;
}

unparkSuccessor() 唤醒

这里把-1又改成0的原因是,如果在非公平锁的情况下,当前线程被唤醒后有可能还是会抢不到锁,那这样就要保持是0的状态继续去执行阻塞的逻辑

在这里插入图片描述

源码解析 lockInteruptibly()

当用的是lockInteruptibly() 会调用doAcquireInterruptibly()

doAcquireInterruptibly()和acquireQueued 的区别在于 如果获取锁失败且是通过中断唤醒的就往外抛异常,抛异常之前会执行cancelAcquire()去把当前结点移出队列

  • doAcquireInterruptibly()
    在这里插入图片描述

  • cancelAcquire()
    在这里插入图片描述
    在这里插入图片描述

    • 将当前结点的thread=null
    • 将前驱结点的waitStatus为CANCELLED的都忽略掉
    • 将当前结点的waitstatus修改为CANCELLED
    • 将当前结点从队列中移除
      • 当前结点是尾结点
      • 当前结点是头结点,就唤醒下一个结点
      • 是其它结点
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值