并发编程的挑战和底层原理
并发编程的挑战和底层原理
并发编程的目的
并发编程的目的是为了让程序运行得更快。
并发编程的挑战
- 上下文切换
- 死锁
- 资源限制
Java 并发机制的底层实现原理
Java 中的并发机制依赖于 JVM 的实现和 CPU 的指令。
volatile 关键字
- volatile 是轻量级的 synchronized,它在多处理器开发中保证了共享变量的“可见性”。
实现原理
- 对 volatile 变量进行写操作时,会多出第二行汇编代码:
- 将当前处理器缓存行的数据写回到系统内存。
- 这个写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效。
volatile 的使用优化
- 将共享变量追加到 64 字节,保证每个缓存行中保留完整的一份数据,对于频繁修改的数据,可以有效提高多处理器的处理效率。
synchronized 关键字
- 锁对象
- 对于普通同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前类的 Class 对象。
- 对于同步方法块,锁是 Synchronized 括号里配置的对象。
实现原理
- JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步:
- 任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态。
- 代码块同步是使用 monitorenter 和 monitorexit 指令实现的:
- monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处,JVM 要保证每个 monitorenter 必须有对应的 monitorexit 与之配对。
- 尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。
Java 对象头
- synchronized 用的锁是存在 Java 对象头里的。
- 如果对象是数组类型,虚拟机用 3 个字宽(Word)存储对象头。
- 如果对象是非数组类型,则用 2 字宽存储对象头。
Mark Word
- Mark Word 里默认存储对象的 HashCode、分代年龄和锁标记位。
锁的升级与对比
偏向锁
为什么出现:
- 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。为了让线程获得锁的代价更低而引入了偏向锁。
偏向锁的撤销和获取:
- 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID。
- 偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
- 偏向锁的获取不需要使用 CAS 操作,线程第一次访问同步块的时候,对象头会被设置为指向这个线程,并标记为偏向锁。
- 偏向锁的撤销需要使用 CAS。
- 撤销过程:
- 首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着。
- 如果线程不处于活动状态,则将对象头设置成无锁状态。
- 如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的 Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁。
- 最后唤醒暂停的线程。
轻量级锁
轻量级锁加锁:
- 线程在执行同步块之前,JVM 会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的 Mark Word 复制到锁记录中,官方称为 Displaced Mark Word。
- 然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
轻量级锁解锁:
- 轻量级解锁时,会使用原子的 CAS 操作将 Displaced Mark Word 替换回到对象头。
- 如果成功,则表示没有竞争发生;如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
锁的优缺点对比
Java 如何实现原子操作
使用循环 CAS 实现原子操作
- CAS 实现原子操作的三大问题:
- ABA 问题:
- JDK 的 Atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。这个类的 compareAndSet 方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志。
- 循环时间长开销大。
- 只能保证一个共享变量的原子操作:
- JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行 CAS 操作。
- ABA 问题:
使用锁机制实现原子操作
- 有偏向锁、轻量级锁和互斥锁。有意思的是,除了偏向锁,JVM 实现锁的方式都用了循环 CAS。
- 当一个线程想进入同步块时,使用循环 CAS 的方式来获取锁。
- 当它退出同步块时,使用循环 CAS 释放锁。
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Comment