awakeBird Back-end Dev Engineer

Java并发编程 线程同步机制

2019-01-10

对Java在并发环境下线程安全的实现方式做点笔记。

基础知识

先介绍一些Java线程相关的基础概念

线程生命周期

Java中线程也是一个对象,由Thread类创建,其状态STATE有:

  • NEW 已创建未启动
  • RUNABLE:已获得CPU资源(RUNNING)和可被CPU调度(READY)状态
  • BLOCKED:阻塞状态,进行I/O调用和请求被其他线程占有的锁
  • WAITING:等待其他线程特定的操作
  • TIMED_WATING:并非无限制等待,到达一定时间后变为RUNABLE状态
  • TERMINATED:终止

线程生命周期如下:

竞态

相同输入得不到相同输入,计算结果与时间有关的状态即为竞态。

竞态产生的原因一般是多个线程在不加控制的条件下并发更新、读取了同一个共享变量,引发竞态的模式主要有两种:

  • read-and-modify
  • check-then-action

线程安全

一个类在单线程下运行正常,在多线程环境下也能运行正常,就称这个类是线程安全的。如果一个类是线程安全的,就不会引发竞态。标准库中一些常见的类都不是线程安全的,如ArrayListHashMapSimpleDateFormat

线程安全需要保证三个条件:原子性、可见性和有序性。

原子性 Atomic

线程读写某个变量的操作,从其他线程看不存在中间状态,要么执行发生,要么未发生,就称其满足原子性。

Java原子性的方式主要是:

  • 锁(指代码中的锁,也叫软件锁)
  • CAS(硬件锁)

Java中对基本类型的写操作,除了long和double外都是原子性的。

可见性 Visibility

线程对共享变量的更新对其他线程可见,即其他线程在线程更新之后立即可以读到最新值,就称其满足可见性。

下面两个问题会影响可见性:

  • 编译优化,如将while循环内的判断优化至循环外。
  • 计算机存储结构,处理器会缓存一些变量的更新。

CPU有自己的缓存单元,包括寄存器 Registerr,高速缓存 Cache,写缓冲器 Store Buffer,无效化队列 Invalidate Queue

Java的一些同步机制会使CPU缓存同步,即使CPU可以读到另一个CPU上的缓存:

  • 写线程冲刷处理器缓存:更新一个变量后使处理器对变量的修改写入Cache或RAM中,而不是停留在Store Cache
  • 读线程刷新处理器缓存:读取一个变量的值时,如果其他处理器已经更新,需要进行缓存同步
有序性 Odering

线程执行的内存访问操作在其他线程看来没有发生变化,就称其满足有序性。重排序会影响有序性。

上下文切换

一个线程被暂停,另一个线程获取CPU控制权的过程,称作上下文切换。切换过程包括线程切出 Switch out线程切入 Switch in

线程在进行上下文切换时需要在内存中读取或保存自己的运行状态,包括通用寄存器的内容和程序计数器的内容。

上下文切换可分为两种方式:

  • 自发性上下文切换:线程主动让出CPU控制权,如调用wait/sleep/yield/join/park、进行I/O操作或请求其他线程持有的锁。
  • 非自发性上下文切换:线程的CPU时间片耗尽或被其他高优先级线程(如GC)夺取CPU控制权。

上下文切换时有开销的:

  • 直接开销:CPU调度的开销和保存/恢复上下文的开销。
  • 间接开销:高速缓存重新加载和高速缓存被冲刷(Flush到二级缓冲)。

Java的线程同步机制

每种同步机制需要分析其是否满足原子性、可见性和有序性,是否存在上下文切换开销。

锁 Lock

Java中锁的实现有两种

  • 内部锁:synchronize关键字
  • 显式锁:java.concurrent.locks.Lock接口的实现类

用锁实现线程同步可以满足原子性、可见性、有序性,存在上下文开销

  • 原子性:被锁同步的代码块称作临界区,同一时间只有一个线程可以访问临界区。
  • 可见性:Java中获得锁(Acquire)刷新处理器缓存,释放锁(Release)会冲刷处理器缓存。
  • 有序性:临界区内的指令不会被重排序到临界区外。
  • 上下文切换开销:请求被其他线程持有的锁会发生上下文切换。

volatile

volatile修饰的变量满足可见性、有序性和写操作的原子性,但不具有计算的原子性,不存在上下文切换。

  • 可见性:变量不会被缓存在Store Cache中,即写操作之后会冲刷处理器缓存,读操作之前会刷新处理器缓存。
  • 有序性:编译器不会对该变量上的操作与其他内存操作进行重排序。
  • 原子性:不具有自增等计算的原子性

CAS

java.util.concurrent.atomic中的原子类借助CAS实现了可见性和原子性,它的计算实现基于一种乐观锁的思想:执行某种操作,如果不成功就一直执行直到成功为止。这里借助一段源码加强理解。

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

CAS的问题是ABA问题,可以借助AtomicStampedReference类解决。

安全发布

安全发布一个对象有以下几种方式:

  • 在静态初始化函数中初始化一个对象的引用static
  • 将某个对象的引用保存在volatile类型的域或AtomicReference对象中
  • 将对象的引用保存在正确构造对象的final类型的域中
  • 将对象的引用保存在由锁保护的域中

(End)


Similar Posts

Comments