【Java 并发编程】volatile 、synchronized 与 lock 可见性、有序性、原子性大杀器

volatile 、synchronized 与 lock 可见性、有序性、原子性大杀器

Java 内存模型 & 并发问题本质 一文中我们已经了解到所有的并发问题的本质在于:

  1. 多核 CPU 时代的数据可见性问题
  2. 编译优化带来的有序性问题
  3. 线程切换带来的原子性问题

在实际开发中该如何解决上述问题呢?

使用 volatile 解决可见性和有序性问题

想要解决可见性、有序性问题最简单最直接的方法就是禁用 CPU 缓存 以及 禁用编译优化

Java 为我们提供了解决方案就是使用 volatile 修饰共享变量

volatile 解决可见性问题

两种方法:

  1. lock 锁总线,底层就是互斥加锁,锁了总线那只有一个 CPU 可以访问该区域的内存地址
  2. MESI 缓存一致性协议,这个需要 CPU 支持该功能,简单说就是提供了一个缓存同步的机制,通过总线嗅探来广播通知其他 CPU 改动废弃当前缓存同步最新缓存

hotspot x86 平台上的内存屏障的实现依赖于 lock 指令,而 Intel 的 lock 指令的实现依赖于缓存一致性协议(例如 MESI)。

不同的 CPU 实现不同,但是都提供 lock 锁总线的功能,我理解 hotspot 为了省事直接使用 lock ,而不是根据 CPU 型号不同进行不同实现

volatile 解决有序性问题

happen-before 原则 + 内存屏障

在 JVM 层级,JMM 规范规定

虚拟机需要实现四种逻辑上的屏障 (为什么说逻辑上?是因为在硬件上不一定是这么实现的,要看各个虚拟机的具体实现逻辑)

LoadLoad 屏障 、StoreStore 屏障、LoadStore 屏障、StoreLoad 屏障

CPU 指令级别的实现(不同 CPU 支持不一样)

X86 CPU 可以支持 sfence 读屏障、 lfence 写屏障、mfence 全屏障

intel lock 锁总线,直接锁总线也可以实现重排的问题 (也就是说 lock 锁总线即解决了 有序性,也解决了可见性问题)

在这里插入图片描述

volatile 无法解决线程切换带来的原子性问题,这时需要搬出大杀器 : 锁

synchronized 锁解决原子性问题

Java 为我们提供了 synchronized 关键字来解决原子性问题,同时加锁也解决了 有序性、可见性的问题。

加锁的本质就是将并行程序串行化。

每个对象都可以成为 锁

Java 对象由: 对象头、实例数据、对齐填充组成

当一个对象成为锁时,锁的信息就存储在对象头中。

synchronized 原理

synchronized 锁为了优化性能分为几个阶段,会存在一个锁升级的过程且不可逆

  1. 偏向锁:当只有一个线程执行时是偏向锁
  2. 轻量级锁:当锁竞争不大时,即每个线程耗时极短可以交替执行,此时是使用自旋等待的方式(认为可以很快获取锁所以可以自旋占用 CPU 资源来等待获取)
  3. 重量级锁:竞争非常激烈,此时底层是操作系统的 信号量机制,等待线程会被挂起,本质其实是一个监视器、计数器、以及维护的线程等待队列等。

synchronized 支持可重入。

Java 为什么还需要 Lock

synchronized 有他的局限性:

  1. 直接阻塞,无法 tryLock ,加锁失败直接返回
  2. 无法实现公平锁
  3. 无法进行读写锁的分离
  4. 无法响应中断

为了进行更加灵活的控制 Doug Lea 开发了 JUC --> java.util.concurrent

并发包下有很多应用工具类,本篇主要聊的是锁,即 ReentranLock

ReentranLock 是基于状态机的锁,维持 private volatile int state; 变量用于锁的争抢和重入。

ReentranLock 继承自 AbstratQueuedSynchronizer 抽象队列同步器 AQS,我们的锁同步都是基于它来进行扩展实现的。

ReentranLock 原理

锁的竞争

  1. CAS state 值
    1. CAS 是 Unsafe 中的原子方法,比较与交换,比较预计值和目标值是否一样,如果一样则改为新值 (存在 aba 问题,即一个线程从 a 改到 b ,b 再改到 a,其他线程就可能修改成功,一个解决方法时增加版本号)
    2. 只有一个线程可以更改 state 的值 0 -> 1 重入后继续 +1 ,解锁 -1 减为 0 时锁被释放其他线程可以获取锁
  2. 自旋
    1. 循环获取锁即尝试 CAS
    2. 多线程竞争情况下再循序中进行阻塞让出 CPU 使用权 LockSupport.part
  3. 阻塞队列
    1. Node 内部类实现的双向列表用于存储对应的等待线程
    2. 公平锁解锁时唤醒队头线程,非公平锁唤醒队头线程与后续还未入队线程进行争抢

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dying 搁浅

两杯酒,一杯敬你余生多欢喜。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值