Skip to content

并发 ​

闲言少叙,并发的目的在于充分利用计算资源(CPU),其难点在于如何处理共享资源

JVM 线程模型 ​

JVM 是运行在 OS 上的,JVM 的线程与操作系统的线程之间存在某种映射关系,这种规范和协议就是 JVM 线程模型

Linux 内核的线程概念 ​

Linux 线程在内核 2.6 之前是轻量级进程,自 2.6 之后换成了更符合 POSIX 标准的 NPTL(Native POSIX Thread Library)
进程与轻量级进程的差异: 进程拥有自己独立的地址空间,而轻量级进程没有,只能共享同 1 个轻量级进程组下的地址空间

主流的线程模型 ​

  • 1 用户线程对 1 操作系统线程
    优点: 线程之间独立
    缺点: 用户线程调度会直接影响内核线程,降低性能

    JVM 属于此类

  • 多用户线程对 1 操作系统线程
    优点: 线程调度可以在用户空间完成,减少状态切换
    缺点: 1 个用户线程阻塞,影响其他线程

    JVM 早期采用

  • 多用户线程对多操作系统线程
    优点: 综合上述 2 者优点
    缺点: 实现复杂

    Golang 的 GMP 线程模型

悲观锁 ​

java 的设计是将锁放在对象头中,也就是将对象视为共享资源

synchronized ​

synchronized 会编译成 2 个字节码指令 monitorenter 和 monitorexit,用它们控制线程进出锁定区域。
底层依赖于操作系统的 Mutex Lock

对象头中的锁标志 ​

顺带提一下,对象在内存中的结构:

  • 对象头
    • mark word
    • 类型指针
  • 实例数据
  • 对齐填充

Mark Word: 32bit

锁升级 ​

源码: https://github.com/openjdk/jdk8u/blob/master/hotspot/src/share/vm/runtime/synchronizer.cpp

无锁状态(不可偏向状态) ​

这种场景是计算过 hashcode,位置用来存 hashcode 了,就不能上偏向锁了。


偏向锁状态 ​

偏向锁已经在 JDK15 弃用 https://openjdk.org/jeps/374,因为偏向锁在出现竞争时有昂贵的撤销操作

如果对象是可偏向模式,偏向锁标志位是 1;如果标志位是 0,则该对象不可偏向。
偏向的概念: 1 个线程 CAS 加锁后,如果没有其他线程竞争,该线程可以直接获取锁,无需任何 CAS 或互斥操作

当出现线程竞争偏向锁时,偏向锁可能会撤销(撤销后不能再偏向),撤销需要在 Safe Point 暂停线程 A,检查 A 是否执行完同步代码。

  • 如果锁无效,变成无锁状态,可以重偏向
  • 如果锁有效,则会升级为轻量级锁

获得偏向锁的线程其栈中也有 Lock Record,作为执行monitorexit命令时弹出栈帧的载体,也要作为重入计数,还要在检查点作为线程 A 执行完毕的依据。


轻量级锁状态 ​

  • 线程的视角:1 个线程发现该对象的锁状态标志为轻量级锁,会在自己的栈中开辟一块空间 "Lock Record",存放 markword 副本和 owner 指针(指向该对象锁)。
  • 作为锁的对象视角:markword 中存放指向线程的指针。

在线程栈中存储主要是为了将 markword 恢复

抢锁和释放锁都有 CAS 操作:

  • 如果有其他线程想要获取轻量级锁,会在用户态 CAS 自旋等待,CAS 失败则锁膨胀为重量级锁。

    自旋有适应性自旋,如果该线程在同一个锁上刚刚成功获得过锁,虚拟机允许更长的自旋时间

  • 如果释放锁 CAS 失败了,也就是说锁标识位是 10,重量级锁,也意味着有其他线程尝试获取过锁,那么除了要释放锁,还要唤醒其他线程。

设计的初衷是当没有锁竞争的情况下,减少使用操作系统互斥量的开销。如果有 2 个及以上线程竞争,会升级为重量级锁。


重量级锁状态 ​

重量级锁指的是 mark word 中指向的 ObjectMonitor 对象,其中包括(但不只有) entry set 和 wait set 区域。


乐观锁 ​

悲观锁在操作系统的用户态和内核态之间切换,其开销是很大的,乐观锁或许是另一种选择

CAS 的原子性 ​

想要使用 CAS 当作锁(某一时刻只有 1 个线程对共享资源修改),必须保证 CAS 是原子操作。
不同架构的 CPU 都提供了指令级的 CAS 原子操作,如 x86 架构下的 cmpxchg 指令,ARM 架构下的 LL/SC 指令。

在虚拟机中,对 native 方法compareAndSwapInt()的实现:https://github.com/openjdk/jdk8u/blob/master/hotspot/src/share/vm/runtime/atomic.cpp

cpp
jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte* dest, jbyte compare_value) {
  assert(sizeof(jbyte) == 1, "assumption.");
  uintptr_t dest_addr = (uintptr_t)dest;
  uintptr_t offset = dest_addr % sizeof(jint);
  volatile jint* dest_int = (volatile jint*)(dest_addr - offset);
  jint cur = *dest_int;
  jbyte* cur_as_bytes = (jbyte*)(&cur);
  jint new_val = cur;
  jbyte* new_val_as_bytes = (jbyte*)(&new_val);
  new_val_as_bytes[offset] = exchange_value;
  while (cur_as_bytes[offset] == compare_value) {
    jint res = cmpxchg(new_val, dest_int, cur);
    if (res == cur) break;
    cur = res;
    new_val = cur;
    new_val_as_bytes[offset] = exchange_value;
  }
  return cur_as_bytes[offset];
}

在更上层, Unsafe 类对底层实现做了封装,并暴露了 native 方法。

并发的应用层封装 ​

JUC,java.util.concurrent 包是 Java 语言提供的官方并发 sdk,其中的 原子类、并发队列、同步工具、并发集合、并发锁、线程池等都是非常常用的。接下来分别介绍它们。

Atomic 原子类 ​

原子类有 5 类: 基本类型,Array,Reference,Updater,Striped64(条纹)

利用 CAS,volatile 和 native 方法来保证原子操作
拿期望的值和原本的一个值作比较,如果相同则更新成新的值

UnSafe 类的 objectFieldOffset() 方法是一个 native 方法,这个方法用来拿到"原来的值"的内存地址,返回值是 valueOffset.
另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证总能拿到该变量的最新值

基本类型 ​

使用原子的方式更新基本类型

  • AtomicInteger:整形原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类

数组类型 ​

使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整形数组原子类
  • AtomicReferenceArray:引用类型数组原子类

引用类型 ​

  • AtomicReference:引用类型原子类

  • AtomicStampedReference:原子更新带有版本号的引用类型

    可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题

  • AtomicMarkableReference :原子更新带有标记位的引用类型

    标记位能标识是否修改过

对象的属性修改器 ​

更新对象的属性必须 volatile 修饰

  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器
  • AtomicLongFieldUpdater:原子更新长整形字段的更新器
  • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器

分段计数器 ​

  • Striped64
    abstract class, 其名字的含义是"条纹", 通过 base+分段的设计细分锁粒度, 提高并发度

  • LongAdder

  • DoubleAdder

  • LongAccumulator: LongAdder 的增强版

    提供了自定义二元操作的能力, LongAdder 等效于相加操作 new LongAccumulator((x, y) -> x + y, 0L).

  • DoubleAccumulator


并发基石 AQS ​

设计理念 ​

要设计一个基于 CAS 原语的并发框架, 封装对共享资源的访问, 并提供通用的扩展能力, 要怎么设计?
首先要明确要解决哪些问题:

  1. 如何标识访问共享资源的状态? 怎样处理共享和独占?
  2. 如果出现竞争, 要怎么处理?

一个一个说, 要标识访问共享资源的状态, 用一个 status 字段就能搞定, 访问共享资源前, 先读 status;

java
private volatile int state;

如果出现竞争呢? 让所有线程都 CAS 吗? 这样很浪费资源. 那可以用一个阻塞队列, 存放竞争资源的线程;
如果竞争资源失败, 怎么处理? 可以自旋重试, 也可以放弃, 在这方面可以提供多种策略.

并发队列 ​

同步工具 ​

并发锁 ​

并发集合 ​

线程池 ​


原始比例: