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

在这里插入图片描述


Lock的特性:

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

  • 公平性与非公平性

实现锁的核心:

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

CAS

CAS Compare and swap
能够保证不管并发有多高,都能保证这个执行的原子性

通过cas算法去加锁,这样保证加锁永远只有一个线程能够成功

CAS 工作原理

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

CAS的使用

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

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

等到线程Aunlock()的时候 唤醒线程B 这样线程B就可以接着去执行 去加锁

LockSupport.park和unpark的使用

在这里插入图片描述

ReentrantLock

ReentrantLock定义在java.util.concurrent包下

java.util.concurrent包中很多功能就是基于AQS框架的

公平锁和非公平锁实现的特性就是通过 在ReentrantLock内部定义了一个Sync的内部类,该类继承AbstractQueuedSynchronized,对 该抽象类的部分方法做了实现;
并且还定义了两个子类:
这两个类都继承自Sync,也就是间接继承了AbstractQueuedSynchronized,所以这一个 ReentrantLock同时具备公平与非公平特性。

  • FairSync 公平锁的实现
  • NonfairSync 非公平锁的实现

AQS源码分析

超类中有一个变量:记录当前获取锁的线程是谁
在这里插入图片描述

AQS类下的变量state状态器,表明当前同步器的状态
state为0表明是无锁状态,没有被任何一个线程持有
在这里插入图片描述
队列的创建:
AQS会基于Node构建一条队列,Node是AQS的内部类,
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的head属性会指向Node的头部,tail属性会指向Node的尾部

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

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


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

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

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

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

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

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

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

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

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

        • 把prev指向t即队尾指向的node
        • 用CAS的方式移动尾部指针
        • 原来尾部即t的node的next指向当前入队的node
          在这里插入图片描述
  • 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进行阻塞
          在这里插入图片描述




持有锁的这边逻辑
执行unlock()
release()

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

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章