Java 中的线程安全性

概览

什么是线程安全?

当多个线程访问某个类时,不管 runtime 采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。

什么是正确性?
某个类的行为与其预期相一致。(在编写类时,能预知的状态结果)

编写线程安全的代码,核心在于要对状态访问操作进行管理,特别是对共享的(Shared)可变的(Mutable)状态的访问。

什么时候需要线程安全?
一个对象是否需要是线程安全的,取决于该对象是否被多线程访问,即程序中访问对象的方式。要使得对象是线程安全的,要采用同步机制来协同对对象可变状态的访问。Java 常用的同步机制是 synchronizedvolatile 类型的变量,显示锁以及原子类型变量。

Java 线程安全体现

Java 线程安全在三个方面体现:

  • 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作
  • 可见性:一个线程对主内存的修改可以及时地被其他线程看到
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,happens-before 原则可以保证特定情况下有序。

原子性

  1. 提供了很多 atomic 类:AtomicInteger、AtomicLong、AtomicBoolean 等等。这些类型通过 CAS 实现原子性。

  2. synchronized 是一种同步锁,通过锁实现原子性。synchronized 作用范围内的代码,同一时刻只能由一个线程进行操作。

    JDK 提供两种锁:1. synchronized 由JVM实现;2. 代码层面的锁(lock),如:ReentrantLock。

可见性

可见性主要通过 volatile 实现,本质上是通过内存屏障和禁止重排序实现的。

  1. volatile 在写操作时,在写操作后加一条 store 屏障指令,将本地内存中的共享变量值刷新到主内存;
  2. volatile 在读操作时,在读操作前加一条 load 屏障指令,从主内存中读取共享变量;

需要注意的是,volatile 不是原子性的,进行 ++ 操作不是安全的,因此不适用于计数器。其适用场景为状态标记量:
1. 对变量的写操作不依赖于当前值
2. 该变量没有包含在具有其他变量不变的式子中

有序性

JMM(Java Memory Model)中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,会影响到多线程并发执行的正确性。

可以通过 volatile、synchronized、lock 保证部分指令的有序性。如果两个操作的执行次序无法从 happens-before 原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

happens-before 原则:

  1. 程序次序规则:在一个单独的线程中,按照程序代码书写的顺序执行。
  2. 锁定规则:一个 unlock 操作 happen—before 后面对同一个锁的 lock 操作。
  3. volatile变量规则:对一个 volatile 变量的写操作 happen—before 后面对该变量的读操作。
  4. 线程启动规则:Thread 对象的 start() 方法 happen—before 此线程的每一个动作。
  5. 线程终止规则:线程的所有操作都 happen—before 对此线程的终止检测,可以通过 Thread.join() 方法结束、Thread.isAlive() 的返回值等手段检测到线程已经终止执行。
  6. 线程中断规则:对线程 interrupt() 方法的调用 happen—before 发生于被中断线程的代码检测到中断时事件的发生。
  7. 对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before 它的 finalize() 方法的开始。
  8. 传递性:如果操作A happen—before 操作B,操作B happen—before 操作C,那么操作A happen—before 操作C。

Add a Comment

电子邮件地址不会被公开。 必填项已用*标注