并发编程的挑战和底层原理

并发编程的目的

并发编程的目的是为了让程序运行得更快。

并发编程的挑战

  • 上下文切换
  • 死锁
  • 资源限制

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 字宽存储对象头。

Java 对象头

Mark Word

  • Mark Word 里默认存储对象的 HashCode、分代年龄和锁标记位

Mark Word

Mark Word 结构

锁的升级与对比

偏向锁

  • 为什么出现

    • 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得。为了让线程获得锁的代价更低而引入了偏向锁
  • 偏向锁的撤销和获取

    • 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 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 操作。

使用锁机制实现原子操作

  • 有偏向锁、轻量级锁和互斥锁。有意思的是,除了偏向锁,JVM 实现锁的方式都用了循环 CAS。
    • 当一个线程想进入同步块时,使用循环 CAS 的方式来获取锁。
    • 当它退出同步块时,使用循环 CAS 释放锁。