彻底搞懂AQS

1、AQS

  • AQS(AbstractQueuedSynchronizer)可以理解为抽象队列同步器,他定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。

  • AQS核心要点:State变量 + CLH队列 + CAS

  • AQS 使用一个volatile的int类型的成员变量来表示同步状态,通过CAS完成对State值的修改。

  • CLH 双端Node队列将要去抢占资源的线程封装成一个Node节点来实现锁的分配。

  • AQS核心思想:如果同步状态未被锁定(即state = 0),那么就将持有操作权线程指向当前线程,同步状态修改为锁定状态(即state = 1);如果同步状态为锁定状态,将暂时获取不到操作权的线程加入到CLH队列中等待唤醒。

  • 独占模式

    img

  • 共享模式

    img

CLH队列

image-20220510172124265

  • 重要方法和属性值的含义
方法和属性值含义
waitStatus当前节点在队列中的状态
thread表示处于该节点的线程
prev前驱指针
predecessor返回前驱节点,没有的话抛出 npe
next后继指针
  • waitStatus枚举值:
枚举含义
0当一个 Node 被初始化的时候的默认值
CANCELLED为 1,表示线程获取锁的请求已经取消了
CONDITION为-2,表示节点在等待队列中,节点线程等待唤醒
PROPAGATE为-3,当前线程处在 SHARED 情况下,该字段才会使用
SIGNAL为-1,表示线程已经准备好了,就等资源释放了

ReentrantLock

image-20220511093406515

  • 实现原理:volatile 变量 + CAS设置值 + AQS(状态值state+CLH双向队列)

  • 实现步骤:

    1. 如果是非公平锁,则会先通过CAS尝试获取锁资源,如果获取成功就把锁的持有者设置为当前线程。
    2. 未竞争到锁的线程将会被进入acquire方法。该方法内有三个重要方法:
      • tyAcquire(arg )尝试获取锁,该方法是AQS的方法,如果子类不重写,那么就抛出异常,因此子类必须重写;
      • addWaiter(Node.EXCLUSIVE) 将当前线程封装为Node节点添加到CLH队列中;
      • acquireQueued(node, arg) 在CLH队列中尝试获取锁;
    3. tyAcquire尝试再次获取锁,获取失败判断是否是锁持有者是否是当前线程,如果是就可重
      入。
    4. 未竞争到锁的线程将会被CAS构成一个链表结点加入队列并且被挂起。
    5. 竞争到锁的线程执行完后释放锁并且将唤醒链表中的下一个节点。
    6. 被唤醒的节点将从被挂起的地方继续执行逻辑。
  • 默认创建非公平锁

    • 公平锁和非公平锁都继承Sync内部类,Sync类继承AbstractQueuesynchronizer。
    • 唯一的区别就在于公平锁在获取锁时多了一个限制条件:hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法.

Condition

image-20220511101009094

  • 可以理解为条件等待队列
  • 当一个线程在调用了await方法以后,直到线程等待的某个条件成立才会被唤醒;
  • 为线程提供了更加简单的等待/通知模式,必须要配合锁一起使用;
  • Condition是AQS的内部类,每个Condition对象都包含一个FIFO队列,队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态;
  • 当一个线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列;
  • 节点引用更新本来就是在获取锁以后的操作,所以不需要CAS保证。同时也是线程安全的操作;
  • 调用Condition的signal()方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到同步队列中;

ReentrantReadWriteLock

  • ReentrantReadWriteLock 为 读写锁;
    ReadWriteLock管理一组锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁
  • 读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。
  • 线程进入读锁的前提条件:
    • 没有其他线程的写锁。
    • 有写请求,但调用线程和持有锁的线程是同一个。
    • 线程进入写锁的前提条件:
    • 没有任意线程的读锁
    • 没有其他线程的写锁
  • ReadLock 和 WriteLock 方法都是通过调用Sync的方法实现的;
  • AQS 的状态 **state 是32位(int 类型)的,分成两份,读锁用高16位,表示持有读锁的线程数(sharedCount),写锁低16位,表示写锁的重入次数 (exclusiveCount)。**状态值为 0 表示锁空闲,sharedCount 不为 0 表示分配了读锁,exclusiveCount 不为 0 表示分配了写锁,sharedCount和exclusiveCount 一般不会同时不为 0,只有当线程占用了写锁,该线程可以重入获取读锁。
    • 获取写状态:S & ( 1 << 16 - 1):将高16位全部抹去
    • 获取读状态:S >>> 16:无符号补0,右移16位
    • 写状态加1:S+1
    • 读状态加1:S+(1<<16)即S + 0x00010000
  • 一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。

BlockingQueue

  • 阻塞队列,特性是在任意时刻只有一个线程可以进行take或者put操作;

  • 当队列是空的,从队列中获取元素的操作将会被阻塞;

  • 当队列是满的,从队列中添加元素的操作将会被阻塞;

  • 常用于生产者和消费者的场景;

  • 优点:我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,这一切BlockingQueue都处理好了;

  • 常见的4种阻塞队列

    • ArrayBlockingQueue 由数组支持的有界队列
    • LinkedBlockingQueue 由链接节点支持的可选有界队列
    • PriorityBlockingQueue 由优先级堆支持的无界优先级队列
    • DelayQueue 由优先级堆支持的、基于时间的调度队列

CAS

  • CAS是指Compare And Swap,比较并交换,是一种基于乐观锁的很重要同步思想;
  • 如果主内存的值跟期望值一样,那么就进行修改,否则一直重试,直到一致为止。
  • CAS缺点:
    • 一直循环,开销比较大。
      • 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
    • 只能保证一个变量的原子操作,多个变量依然要加锁。
      • 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
    • ABA问题。
      • 因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了B,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际变化了。
      • 解决办法就是使用版本号,在变量前面追加版本号,每次变量更新时把版本号加1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值