• 并发编程之J.U.C的第一篇


    AQS 原理

    全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架
    特点 :

    • 用state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
      • getState - 获取 state 状态
      • setState - 设置 state 状态
      • compareAndSetState - cas 乐观锁机制设置 state 状态
      • 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
    • 提供了基于FIFO的等待队列,类似于 Monitor的EntryList
    • 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor的WaitSet

    子类主要实现这样一些方法 (默认抛出UnsupportedOperationException)

    • tryAcquire
    • tryRelease
    • tryAcquireShard
    • tryReleaseShard
    • isHeldExclusively
      获取锁的优势
      在这里插入图片描述
      释放锁的姿势
      在这里插入图片描述

    ReentrantLock 原理

    在这里插入图片描述

    1. 非公平锁实现原理

    加锁解锁流程
    先从构造器开始看,默认为非公平锁实现
    在这里插入图片描述
    NonfairSync 继承自 AQS
    没有竞争时
    在这里插入图片描述
    第一个竞争出现时 :
    在这里插入图片描述
    Thread-1 执行了

    1. CAS尝试将state 由0 改为 1,结果失败
    2. 进入 tryAcquire 逻辑,这时state 已经是1,结果任然失败
    3. 接下来进入 addWaiter逻辑,构造Node队列
      • 图中黄色三角表示该Node的waitStatus状态,其中0为默认正常状态
      • Node的创建时懒得的
      • 其中第一个 Node称为 Dummy (哑元)或哨兵,用来占位,并不关联线程
        在这里插入图片描述
        当前线程进入 acquireQueued 逻辑
      1. acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
      2. 如果自己是紧邻着 head (排第二位),那么再次 tryAcquire 尝试获取锁,当然时 state 仍为1,失败
      3. 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的waitStatus改为 -1,这次返回false
        再次多个线程经历上述过程竞争失败,变成这个样子
        在这里插入图片描述
        Thread-0 释放锁,进入tryRelease 流程,如果成功
    • 设置exclusiveOwnerThread为null
    • state = 0
      在这里插入图片描述
      当队列不为null,并且head的waitStatus = -1,进入unparkSuccessor 流程找到队列中离head最近的一个Node(没取消的),unpark恢复其运行,本例中即为Thread-1
      回到 Thread-1 的acquireQueued 流程
      在这里插入图片描述
      回到 Thread - 1的 acquireQueued 流程
      在这里插入图片描述
      如果加锁成功(没有竞争),会设置
    • exclusiveOwnerThread 为 Thread - 1,state = 1
    • head 指向刚刚 Thread - 1 所在的Node,该Node清空Thread
    • 原本的head因为从链表断开,而可被垃圾回收
      如果这时候有其它线程来竞争(非公平的体现),例如这时有Thread - 4来了
      在这里插入图片描述
      如果不巧又被 Thread - 4 占了先
    • Thread - 4被设置为 exclusiveOwnerThread,state = 1
    • Thread - 1再次进入 acquireQueued 流程,获取锁失败,重新进入 park阻塞
      加锁源码

    2)可重入原理

    在这里插入图片描述
    在这里插入图片描述

    3. 可打断原理

    不可打断模式
    在此模式下,即使它被打断,仍会驻留在AQS队列中,等获得锁后方能继续运行(是继续运行!只是打断标记被设置为true)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    可打断模式
    在这里插入图片描述
    在这里插入图片描述

    5) 条件变量实现原理

    每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject
    await 流程
    开始 Thread - 0 持有锁,调用await,进入ConditionObject 的addConditionWaiter流程创建新的Node状态为 -2 (Node.CONDITION),关联Thread - 0,加入等待队列尾部
    在这里插入图片描述
    接下来进入AQS的fullyRelease流程,释放同步器上的锁
    在这里插入图片描述
    unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么Thread - 1竞争成功
    在这里插入图片描述
    park 阻塞 Thread - 0
    在这里插入图片描述
    signal
    假设Thread - 1 要来唤醒 Thread - 0
    在这里插入图片描述
    进入 ConditionObject 的doSignal流程,取得等待队列中第一个 Node,即Thread - 0所在Node
    在这里插入图片描述
    执行transferForSignal 流程,将该Node 加入AQS队列尾部,将Thread - 0
    的waitStatus改为0,Thread - 3的waitStatus改为 - 1
    在这里插入图片描述
    Thread - 1 释放锁,进入unlock流程。

    3. 读写锁

    3.1 ReentrantReadWriteLock

    当读操作远远高于写操作时,这时候使用读写锁让 读-读可以并发,提高性能。
    类似于数据库中的select 。。。from 。。。lock in share mode
    提供一个数据容器类内部分别使用读锁保护数据的read()方法,写锁保护数据的write()方法
    注意事项

    • 读锁不支持条件变量
    • 重入时升级不支持 :即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
      在这里插入图片描述
    • 重入时降级支持 :即持有写锁的情况下去获取读锁
      在这里插入图片描述

    缓存更新策略

    更新时,是先清缓存还是先更新数据库
    先清缓存
    在这里插入图片描述
    先更新数据库
    在这里插入图片描述

    读写锁原理

    1. 图解流程
      读写锁同的是同一个Sycn同步器,因此等待队列、state等也是同一个
      t1 w.lock,t2 r.lock
      1. t1成功上锁,流程与ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了state的低16位,而读锁使用的是state的高16位
        在这里插入图片描述
        2)t2 执行 t.lock,这时进入读锁的 sync.acquireShared(1)流程,首先会进入tryAcquireShard流程。如果有写锁占据,那么tryAcquireShared返回-1 表示失败
        tryAcquireShared 返回值表示
        • -1 表示失败
        • 0 表示成功,但后继节点继续唤醒
        • 正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回1
          在这里插入图片描述
          3)这时会进入 sync.doAcquireShared(1)流程,首先也是调用 addWaiter添加节点,不同之处在于节点被设置为 Node.SHARED 模式而非 Node.
          EXCLUSIVE模式,注意此时t2仍处于活跃状态
          在这里插入图片描述
          4)t2会看看自己的节点是不是老二,如果是,还会再次调用tryAcquireShared(1)来尝试获取锁
          5)如果没有成功,在doAcquireShared 内 for (;;)循环一次,把前驱节点的waitStatus改为 -1,再for(;;)循环一次尝试tryAcquireShared(1)如果还不成功,那么在parkAndCheckInterrupt()处park
          在这里插入图片描述
          t3 r.lock, t4 w.lock
          这种状态下,假设又有t3 加读锁和t4加写锁,这期间t1任然持有锁,就编程了下面样子
          在这里插入图片描述
          t1 w.unlock
          这时会走到写锁的 sync.release(1)流程,调用sync.tryRelease(1)成功,变成下面的样子
          在这里插入图片描述
          接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在doAcquireShared 内 parkAndCheckInterrupt()处恢复运行
          这回再来一次 for(;;)执行 tryAcquireShared成功则让读锁计数加一
          在这里插入图片描述
          这时t2 已经恢复运行,接下来t2调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
          在这里插入图片描述
          事情还没完,在setHeadAndPropagate 方法内还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared 将head的状态从 -1 改为 0并唤醒老二,这时t3在 doAcquireShared 内 parkAncCheckInterrupt() 会恢复运行
          在这里插入图片描述
          这回再来一次for(;;)执行tryAcquireShared 成功则让读锁计数加一
          在这里插入图片描述
          这时t3 已经恢复运行,接下来t3 调用 setHeadAndPropagate(node,1),它原本所在节点被置为头节点
          在这里插入图片描述
          下一个节点不是shared了,因此不会继续唤醒t4所在节点
          t2 r.unlock, t3 r.unlock
          t2 进入 sync.releaseShared(1)中,调用 tryReleaseShared(1)让计数减一,但由于计数为零
          在这里插入图片描述
          t3 进入 sync.releaseShared(1)中,调用tryReleaseShared(1)让计数减一,这回计数为零了,进入doReleaseShared()将头节点从-1改为0并唤醒老二,即
          在这里插入图片描述
          之后 t4 在acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次for(;;)这次自己是老二,并且没有其他竞争,tryAcquire(1)成功,修改头结点,流程结束
          在这里插入图片描述
  • 相关阅读:
    MySQL语句进行分组后的含有字段拼接方法
    架构基础
    自动化测试框架比较
    分布式系统中的概念--第一篇 一致性协议、一致性模型、拜占庭问题、租约
    如果两个对象具有相同的哈希码,但是不相等的,它们可以在HashMap中同时存在吗?
    分布式系统常用思想和技术总结(转)
    常用的Hash算法
    Sonar + Jacoco,强悍的UT, IT 双覆盖率统计(转)
    AWK处理日志入门(转)
    内存管理
  • 原文地址:https://www.cnblogs.com/haizai/p/12345633.html
Copyright © 2020-2023  润新知