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)成功,修改头结点,流程结束

并发编程之J.U.C的第一篇的更多相关文章

  1. 并发编程之J.U.C的第二篇

    并发编程之J.U.C的第二篇 3.2 StampedLock 4. Semaphore Semaphore原理 5. CountdownLatch 6. CyclicBarrier 7.线程安全集合类 ...

  2. Java并发编程之CAS第一篇-什么是CAS

    Java并发编程之CAS第一篇-什么是CAS 通过前面几篇的学习,我们对并发编程两个高频知识点了解了其中的一个—volatitl.从这一篇文章开始,我们将要学习另一个知识点—CAS.本篇是<凯哥 ...

  3. [转载]并发编程之Operation Queue和GCD

    并发编程之Operation Queue http://www.cocoachina.com/applenews/devnews/2013/1210/7506.html 随着移动设备的更新换代,移动设 ...

  4. 并发编程之 Exchanger 源码分析

    前言 JUC 包中除了 CountDownLatch, CyclicBarrier, Semaphore, 还有一个重要的工具,只不过相对而言使用的不多,什么呢? Exchange -- 交换器.用于 ...

  5. 并发编程之 Condition 源码分析

    前言 Condition 是 Lock 的伴侣,至于如何使用,我们之前也写了一些文章来说,例如 使用 ReentrantLock 和 Condition 实现一个阻塞队列,并发编程之 Java 三把锁 ...

  6. 并发编程之ThreadLocal

    并发编程之ThreadLocal 前言 当多线程访问共享可变数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要线程封闭出场了. 数据都被封闭在各自的线程之中,就不需要同步,这 ...

  7. 并发编程之:JMM

    并发编程之:JMM 大家好,我是小黑,一个在互联网苟且偷生的农民工. 上一期给大家分享了关于Java中线程相关的一些基础知识.在关于线程终止的例子中,第一个方法讲到要想终止一个线程,可以使用标志位的方 ...

  8. 并发编程之:ThreadLocal

    大家好,我是小黑,一个在互联网苟且偷生的农民工. 从前上一期[并发编程之:synchronized] 我们学到要保证在并发情况下对于共享资源的安全访问,就需要用到锁. 但是,加锁通常情况下会让运行效率 ...

  9. 并发编程之:Atomic

    大家好,我是小黑,一个在互联网苟且偷生的农民工. 在开始讲今天的内容之前,先问一个问题,使用int类型做加减操作是不是线程安全的呢?比如 i++ ,++i,i=i+1这样的操作在并发情况下是否会有问题 ...

随机推荐

  1. (转)Gamma分布,Beta分布,Multinomial多项式分布,Dirichlet狄利克雷分布

    1. Gamma函数 首先我们可以看一下Gamma函数的定义: Gamma的重要性质包括下面几条: 1. 递推公式: 2. 对于正整数n, 有 因此可以说Gamma函数是阶乘的推广. 3.  4.  ...

  2. Ubuntu 入门笔记(1)

    在阿里云上申请了一个云服务器,开始学习Linux.我选择的是Ubuntu 14.04 ,在登录时就绕了我好长时间,输入用户名是有显示的,但是输入密码就没有反应了,查找了之后才发现原来这是Ubuntu ...

  3. 惠普电脑win10关闭自动调节亮度

    自动调节亮度真的太烦人了,突然从亮的画面变暗,又从暗的亮度变量,眼睛受不了.但是试了很多种方法都不行. 方法 第一种: 有一些电脑是有在设置--->显示界面--->有一个 关闭自动调节 按 ...

  4. 解决docker容器无网络、无法连接互联网----长期更新

    众所周知,docker有三种默认的网络模式,分别是: bridge host none 然后需要注意的就是内核转发这个,一定不要忘了!配置方法如下 echo -e "net.ipv4.ip_ ...

  5. 【Java并发工具类】Lock和Condition

    前言 Java SDK并发包通过Lock和Condition两个接口来实现管程,其中Lock用于解决互斥问题,Condition用于解决同步问题.我们需要知道,Java语言本身使用synchroniz ...

  6. cpp二进制与整数之间的转换的几种方式记录

    PS: 程序为cpp代码,最重要理解操作. 方法一: n进制方法,也可以解决转换为其他进制问题. /*将整数转化为二进制的string 输出*/ string convert(int num) { s ...

  7. Java面向对象XMind

    Java面向对象的思维导图

  8. vue项目实战经验汇总

    目录 1.vue框架使用注意事项和经验 1.1 解决Vue动态路由参数变化,页面数据不更新 1.2 vue组件里定时器销毁问题 1.3 vue实现按需加载组件的两种方式 1.4 组件之间,父子组件之间 ...

  9. 《自拍教程14》Linux的常用命令

    Linux操作系统, 包括我们大家熟知的Android, Ubuntu, Centos, Red Hat, UOS等. 这些常用命令先大概了解下,当然能熟练掌握并运用到实际工作中那最好不过了. 后续技 ...

  10. Spring Bean几种注入方式——setter(常用),构造器,注入内部Bean,注入集合,接口...

    依赖注入分为三种方式: 1.1构造器注入 构造器通过构造方法实现,构造方法有无参数都可以.在大部分情况下我们都是通过类的构造器来创建对象,Spring也可以采用反射机制通过构造器完成注入,这就是构造器 ...