# java 中的线程安全

线程安全

  • 当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果。-- Brian Goetz
java中各操作共享的数据
  • 不可变
  • 一定是线程安全的。
  • 绝对线程安全
  • 能够完全满足 Brian Goetz 对于线程安全的定义。
  • 相对线程安全
  • 对对象单次的操作是线程安全的,对于特定顺序的连续调用可能需要额外的同步手段。
  • java 中大部分声称线程安全的类(Vector、HashTable 等)。
  • 线程兼容
  • 对象本省并不是线程安全的,可以在调用端使用同步手段保证对象在并发环境中安全使用。
  • 线程对立
  • Thread 类的 suspand ()(:中断线程)和 resume ()(:恢复线程)方法。

# 线程安全的实现方法

# 互斥同步

  • 同步:
  • 多个线程并发访问共享数据时,保证共享数据在同一时刻只被一条线程使用。
  • Java 使用 synchronized 关键字(一种块结构的同步语法)。
  • synchronized 关键字经过 Javac 编译之后, 会在同步块的前后分别形成 monitorenter 和 monitorexit 这两个字节码指令。
  • 执行 monitorenter 指令时,先去尝试获取对象的锁。
  • 如果未被锁定,或当前线程已经持有对象的锁,就将锁的计数器值加一。
  • 执行 monitorexit 指令时将锁计数器的值减一。
  • 如果获取对象锁失败,当前线程将被阻塞等待直到对象被持有的线程释放。
  • 持有锁是重量级操作。
  • java.util.concurrent 包下 java.util.concurrent.locks.Lock 接口也是一种互斥同步手段。
  • 重入锁(ReentrantLock)是 Lock 接口最常见的一种实现。相比 synchronized 增加功能:等待可中断、可实现公平锁、锁可以绑定多个条件。

# 非阻塞同步

  • 互斥同步面临线程阻塞和唤醒所带来的的性能开销(阻塞同步、悲观并发策略)。
  • 非阻塞同步是一种乐观并发策略(无锁):先进行操作,如果不存在数据共享则直接成功。产生冲突时,通过不断重试直到冲突解决。
原子性处理器指令
  • 测试并设置(Test-and-Set);
  • 获取并增加(Fetch-and-Increment);
  • 交换(Swap);
  • 比较并交换(Compare-and-Swap, 下文称 CAS);
    • 变量内存位置(V)、旧的预期值(A)、准备设置的新值(B)
    • 仅当 V 符合 A 时,处理器才会用 B 更新 V 的值,否则就执行更新。最终
  • 加载链接 / 条件储存(Load-Linked/Store-Conditional, 下文称 LL/SC)。
  • CAS 存在 “ABA” 问题漏洞。但在大部分情况下 ABA 问题不会影响程序并发的正确性。

# 无同步方案

  • 可重入代码:不依赖全局变量、存储在堆上的数据和公用的系统资源,用到的状态量都由参数中传入,不调用非可重入的方法等。
  • 线程本地存储:
    • 变量只要被某个线程独享。
    • java.lang.ThreadLocal 类实现线程本地存储的功能。K-V 值对:以 ThreadLocal.threadLocalHashCode 为键,以本地线程变量为值。

# 锁优化

# 自旋锁与自适应自旋

  • 自旋锁:锁被占用时间很短,物理机器有一个以上的处理器或处理器核心时,使用自旋锁能够提高程序运行效率(使用忙循环(自旋)让线程等待(固定时间、次数)获取锁)。
  • 自适应自旋:根据前一次在同一个锁上得自旋时间及锁的拥有者状态决定自旋时间、次数。

# 锁消除

  • 虚拟机即时编译器 JIT 运行时,对一些代码要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。
  • 主要判定依据来源于逃逸分析的数据支持。
  • 例:编译器会将 String 连接自动优化为 StringBuffer 对象的连续 append () 操作,并安全地消除掉 append 方法中的同步锁(即转为非线程安全的 StringBuilder 完成)。

# 锁粗化

  • 虚拟机探测到一连串零碎的操作都对同一对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列外部。

# 轻量级锁

  • 在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
  • 提升程序同步性能的依据:对于绝大部分的锁,在整个同步周期内都是不存在竞争的。
  • 各种状态下对象头 mark word 内容:
    markword

轻量级锁工作过程:

  • 代码即将进入同步块时,如果此对象没有被锁定,虚拟机将在当前线程栈帧中建立锁记录(Lock record)的空间,存储对象目前 MarkWord 的拷贝(Displaced Mark Word)。
  • 虚拟机使用 CAS 操作尝试把对象的 Mark Word 更新为指向 Lock Record 的指针。
  • 更新成功:该线程拥有了对象的锁,MarkWord 的锁标志位变为 “00”,轻量级锁定状态。
  • 更新失败:检查对象 MarkWord 是否指向当前线程栈帧:
  • 是:进入同步块继续执行。
  • 否:该对象已被其他线程抢占,轻量级锁失效,膨胀为重量级锁(锁标志状态值变为 “10”),此时 MarkWord 中存储的是指向重量级锁的指针。

# 偏向锁

  • 为了消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能(-XX: +UseBiased Locking)。
  • 锁会偏向于第一个获得它的线程,如果接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
  • 可以提高带有同步但无竞争的程序性能。

偏向锁工作过程

  • 当所对象第一次被线程获取时,该对象 MarkWord 中标志位设置为 “01”,偏向模式设置为 “1” 进入偏向模式。使用 CAS 操作把获取到这个锁的线程的 ID 记录在对象的 MarkWord 中。
  • CAS 成功:持有偏向锁的线程之后每次进入这个锁相关的同步块时,虚拟机可以不在进行同步操作。
  • CAS 失败:偏向模式结束。根据所对象是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为 “0”),撤销后标志位恢复到未锁定(01)或轻量级锁定(00)。