1 JMM(Java Memory Model)
Java 内存模型(Java Memory Model, JMM)是一种抽象规范,用于定义多线程程序中共享变量的可见性和有序性规则。其核心目标是在允许编译器 / 处理器优化的同时,确保程序在多线程环境下的正确性。
- 主内存:存储所有共享变量。
- 工作内存:线程私有,存储变量副本。
- 原子性:确保操作不可中断(如 AtomicInteger)。
- 可见性:保证变量修改对其他线程立即可见(如 volatile)。
- 有序性:禁止影响正确性的指令重排序(通过 happens-before 规则)。
- 程序顺序规则、volatile 规则、锁规则等,确保操作间的可见性顺序。
- volatile:保证可见性和禁止特定重排序。
- synchronized:保证原子性、可见性和有序性。
- 原子类(如 AtomicInteger):基于 CAS 实现无锁原子操作。
- 通过插入内存屏障(如 LoadLoad、StoreLoad)禁止指令重排序,确保 JMM 语义。
2 并发三要素
2.1 原子性(Atomicity)
2.2.1 定义
原子性指的是一个操作或者一系列操作,在执行过程中不会被任何因素打断,要么全部执行完毕,要么完全不执行,不存在执行到一半的中间状态。一旦操作被中断,就可能会引发数据不一致的问题。- x = 10; //语句1:直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中
- y = x; //语句2:包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
- x++; //语句3: x++包括3个操作:读取x的值,进行加1操作,写入新的值。
- x = x + 1; //语句4: 同语句3
复制代码 上面4个语句只有语句1的操作具备原子性。也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
2.2.2 示例场景
以银行转账为例,从账户 A 向账户 B 转账 100 元,这个操作包含从账户 A 扣除 100 元以及向账户 B 增加 100 元两个步骤。原子性要求这两个步骤必须作为一个整体执行,不能出现从账户 A 扣除了 100 元,但由于某种原因向账户 B 增加 100 元的操作未执行的情况,否则就会导致数据不一致。
2.2.3 保障方式
在 Java 中,可以使用 synchronized关键字或者原子类(如 AtomicInteger)来保证操作的原子性。synchronized关键字能够将一段代码标记为同步代码块,同一时刻只允许一个线程执行该代码块;原子类则借助底层的 CAS(Compare - And - Swap)操作来保证操作的原子性。- public class AtomicExample {
- private int count = 0;
- // 非原子操作,可能会出现竞态条件
- public void increment() {
- count++; // 这一操作包含读取、增加和写入三个步骤,不具备原子性
- }
- // 使用 synchronized 保证原子性
- public synchronized void safeIncrement() {
- count++;
- }
- }
复制代码 2.2 可见性(Visibility)
2.2.1 定义
可见性描述的是当一个线程修改了共享变量的值之后,其他线程能够立即察觉到这个修改。在多处理器或者多线程的环境中,每个线程可能会将共享变量存储在自己的缓存里。如果没有恰当的同步机制,某个线程对共享变量的修改可能无法及时更新到主内存中,其他线程也就无法看到最新的值,从而导致数据不一致。
2.2.2 示例场景
假设有两个线程,线程 A 和线程 B 共享一个变量 isRunning。线程 A 负责不断检查 isRunning 的值来决定是否继续执行某个任务,线程 B 可以修改 isRunning 的值。如果没有保证可见性,线程 B 修改了 isRunning 的值后,线程 A 可能由于使用的是自己工作内存中的旧值,而无法及时停止任务。
2.2.3 保障方式
在 Java 中,volatile 关键字可以保证变量的可见性。当一个变量被声明为 volatile时,对该变量的写操作会立即刷新到主内存,读操作会从主内存中读取最新的值。另外,synchronized 块和 Lock 也能保证可见性,因为在进入和退出同步块时,会进行主内存的刷新操作。
2.3 有序性(Ordering)
2.3.1 定义
有序性意味着程序按照代码的先后顺序依次执行。但在实际情况中,为了提高性能,编译器和处理器可能会对指令进行重排序,只要重排序后的结果与程序顺序执行的结果一致即可。不过,在多线程环境下,这种重排序可能会对程序的正确性产生影响。
- public class OrderingExample {
- private int a = 0;
- private boolean flag = false;
- public void writer() {
- a = 1; // 操作 1
- flag = true; // 操作 2
- }
- public void reader() {
- if (flag) { // 操作 3
- int i = a; // 操作 4
- System.out.println(i); // 可能输出 0,因为操作 1 和操作 2 可能被重排序
- }
- }
- }
复制代码 2.3.2 示例场景
在单例模式的双重检查锁定实现中,如果没有保证有序性,可能会出现对象还未完全初始化就被其他线程使用的情况。例如,在创建对象时,可能会先分配内存空间,然后将引用指向该内存空间,最后在进行对象的初始化操作。如果发生重排序,其他线程可能在对象还未完成初始化时就拿到了引用。
2.3.3 保障方式
可以使用 volatile关键字来禁止指令重排序。volatile关键字会在指令序列中插入内存屏障,保证特定操作的执行顺序。此外,synchronized 块和 Lock 也能保证有序性,因为同步块中的代码会按照顺序执行。
3 Happens-Before 规则
3.1 定义
Happens-Before 规则是 Java 内存模型(JMM)的核心概念,用于定义多线程环境下操作间的可见性和有序性。它并不意味着操作必须按顺序执行,而是保证前一个操作的结果对后一个操作可见。
若操作 A happens-before 操作 B,则 A 的结果对 B 可见,且 A 和 B 的执行顺序不会被重排序(即便实际执行时可能重排,但结果与顺序执行一致)。
3.2 七大规则
3.2.1 程序顺序规则(Program Order Rule)
定义:同一个线程内,每个操作 happens-before 后续的操作。
作用:保证单线程程序的执行结果与顺序执行一致。:- int a = 1; // 操作 1
- int b = a + 2; // 操作 2
- // 操作 1 happens-before 操作 2(同一个线程内)
复制代码 3.2.2 监视器锁规则(Monitor Lock Rule)
定义:对一个锁的解锁操作 happens-before 后续对这个锁的加锁操作。
作用:确保锁的互斥性和可见性。- public class MonitorExample {
- private int count = 0;
- public void increment() {
- synchronized (this) { // 加锁
- count++;
- } // 解锁 happens-before 其他线程的加锁
- }
- public int getCount() {
- synchronized (this) { // 加锁
- return count;
- } // 解锁
- }
- }
复制代码 3.2.3 volatile 变量规则(Volatile Variable Rule)
定义:对一个 volatile 变量的写操作 happens-before 后续对这个变量的读操作。
作用:确保 volatile 变量的可见性和禁止特定重排序。- public class VolatileExample {
- private volatile boolean flag = false;
- public void writer() {
- flag = true; // 写操作 happens-before 读操作
- }
- public void reader() {
- if (flag) { // 读操作
- // 保证此时 flag 一定为 true
- }
- }
- }
复制代码 3.2.4 线程启动规则(Thread Start Rule)
定义:线程的 start() 方法 happens-before 该线程的任何操作。
作用:确保线程启动前的操作对新线程可见。- public class ThreadStartExample {
- private int a = 0;
- public void startThread() {
- a = 1; // 操作 1
- Thread t = new Thread(() -> {
- // 操作 2:线程启动后执行
- // 操作 1 happens-before 操作 2
- System.out.println(a); // 一定输出 1
- });
- t.start(); // 启动线程
- }
- }
复制代码 3.2.5 线程终止规则(Thread Termination Rule)
定义:线程的所有操作 happens-before 其他线程检测到该线程终止(如 join() 返回或 isAlive() 返回 false)。
作用:确保线程结束前的操作对其他线程可见。- public class ThreadTerminationExample {
- private int a = 0;
- public void test() throws InterruptedException {
- Thread t = new Thread(() -> {
- a = 1;
- });
- t.start();
- t.join(); // 等待线程终止
- // 线程 t 的所有操作 happens-before 这里
- System.out.println(a); // 一定输出 1
- }
- }
复制代码 3.2.6 中断规则(Interruption Rule)
定义:线程的 interrupt() 调用 happens-before 被中断线程检测到中断(如 isInterrupted() 返回或抛出 InterruptedException)。
作用:确保中断操作的可见性。- public class InterruptionExample {
- private Thread t;
- public void test() {
- t = new Thread(() -> {
- while (!Thread.currentThread().isInterrupted()) {
- // 循环执行
- }
- // 检测到中断,一定是因为其他线程调用了 t.interrupt()
- });
- t.start();
- t.interrupt(); // 中断调用 happens-before 检测中断
- }
- }
复制代码 3.2.7 对象终结规则(Finalizer Rule)
定义:对象的构造函数执行结束 happens-before finalize() 方法开始。
作用:确保对象初始化完成后才会被销毁。
3.3 传递性(Transitivity)
若 A happens-before B,且 B happens-before C,则 A happens-before C。- public class TransitivityExample {
- private int a = 0;
- private volatile boolean flag = false;
- public void writer() {
- a = 1; // 操作 1
- flag = true; // 操作 2(volatile 写)
- // 操作 1 happens-before 操作 2(程序顺序规则)
- }
- public void reader() {
- if (flag) { // 操作 3(volatile 读)
- int i = a; // 操作 4
- // 操作 3 happens-before 操作 4(程序顺序规则)
- // 操作 2 happens-before 操作 3(volatile 规则)
- // 通过传递性:操作 1 happens-before 操作 4 → 保证 i 一定为 1
- }
- }
- }
复制代码 3.4 Happens-Before 与指令重排序的关系
Happens-Before 规则允许指令重排序,但需保证:
- 单线程内:重排序不影响结果(as-if-serial 语义)。
- 多线程间:重排序不破坏可见性。
- int a = 1;
- int b = 2;
- // 允许重排序为:
- int b = 2;
- int a = 1;
- // 因为单线程内结果不变,且不涉及共享变量
复制代码 3.5 常见误区
- Happens-Before ≠ 时间上的先后顺序:它只保证可见性,不强制物理执行顺序。
- 未同步的操作无 Happens-Before 关系:若两个操作间无规则约束,则可能出现可见性问题。
4 volatile
volatile 是 Java 中用于处理多线程共享变量的关键字,它能保证变量的可见性和部分有序性,但不保证原子性。
4.1 核心作用
4.1.1 保证可见性
当一个变量被声明为 volatile 时:
- 写操作:会强制将变量的最新值刷新到主内存。
- 读操作:会强制从主内存读取变量值,而非使用线程工作内存中的缓存。
- public class VisibilityExample {
- private volatile boolean flag = false; // 声明为 volatile
- public void writer() {
- flag = true; // 写操作立即刷新到主内存
- }
- public void reader() {
- while (!flag) { // 读操作始终从主内存获取最新值
- // 循环等待
- }
- System.out.println("Flag is now true");
- }
- }
复制代码 若 flag 未被声明为 volatile,reader 线程可能永远看不到 writer 线程对 flag 的修改,导致无限循环。
4.1.2 禁止指令重排序
编译器和处理器为了提高性能,可能会对指令进行重排序,但 volatile 关键字会禁止特定类型的重排序:
- 写操作前:不会将后续的操作重排序到写操作之前。
- 读操作后:不会将前面的操作重排序到读操作之后。
- public class ReorderingExample {
- private int a = 0;
- private volatile boolean flag = false;
- public void writer() {
- a = 1; // 操作 1
- flag = true; // 操作 2(volatile 写)
- // 禁止重排序:操作 1 不会被重排序到操作 2 之后
- }
- public void reader() {
- if (flag) { // 操作 3(volatile 读)
- int i = a; // 操作 4
- // 禁止重排序:操作 4 不会被重排序到操作 3 之前
- // 保证 i 一定为 1
- }
- }
- }
复制代码 4.2 底层实现原理
4.2.1 内存屏障(Memory Barrier)
JVM 会在 volatile 变量的读写操作前后插入内存屏障,确保指令顺序和内存可见性:
- 写操作:插入 StoreStore 和 StoreLoad 屏障。java
- a = 1; // 普通写
- // StoreStore 屏障:确保前面的写操作先于 volatile 写执行
- flag = true; // volatile 写
- // StoreLoad 屏障:确保 volatile 写先于后续的读操作执行
复制代码
- 读操作:插入 LoadLoad 和 LoadStore 屏障。java
- if (flag) { // volatile 读
- // LoadLoad 屏障:确保 volatile 读先于后续的读操作执行
- // LoadStore 屏障:确保 volatile 读先于后续的写操作执行
- int i = a; // 普通读
- }
复制代码 4.2.2 硬件层面
- 写操作:通过总线锁或缓存一致性协议(如 MESI),确保修改立即刷新到主内存,并使其他处理器的缓存失效。
- 读操作:直接从主内存读取,绕过处理器缓存。
4.3 应用场景
4.3.1 状态标志
用于控制线程的执行流程,如终止线程:- public class ShutdownExample {
- private volatile boolean shutdown = false;
- public void shutdown() {
- shutdown = true; // 通知其他线程停止执行
- }
- public void run() {
- while (!shutdown) {
- // 执行任务
- }
- }
- }
复制代码 4.3.2 双重检查锁定(DCL)单例模式
确保单例对象的初始化安全:- public class Singleton {
- private static volatile Singleton instance; // 必须声明为 volatile
- private Singleton() {}
- public static Singleton getInstance() {
- if (instance == null) { // 第一次检查
- synchronized (Singleton.class) {
- if (instance == null) { // 第二次检查
- instance = new Singleton(); // 禁止重排序,避免其他线程看到半初始化的对象
- }
- }
- }
- return instance;
- }
- }
复制代码 若 instance 未被声明为 volatile,可能会因指令重排序导致其他线程看到半初始化的对象。
4.3.3 独立观察(Independent Observations)
多个线程独立更新同一个变量,且不依赖当前值:- public class Counter {
- private volatile int count = 0;
- public void increment() {
- count++; // 非原子操作,但每次更新不依赖当前值
- }
- public int getCount() {
- return count;
- }
- }
复制代码 注意:若更新操作依赖当前值(如 count++),volatile 无法保证原子性,需使用 AtomicInteger。
4.4 volatile vs synchronized vs Atomic
特性
| volatile
| synchronized
| Atomic
| 原子性
| ❌
| ✅
| ✅
| 可见性
| ✅
| ✅
| ✅
| 有序性
| 部分保证
| ✅
| ✅
| 性能(从高到低)
| 高
| 中
| 中高
| 适用场景
| 状态标志
| 复合操作
| 原子更新
| 4.5 注意事项
- 不保证原子性:volatile 无法保证复合操作的原子性,如 count++。
- public class NonAtomicExample {
- private volatile int count = 0;
- public void increment() {
- count++; // 非原子操作,多线程下仍会出现竞态条件
- }
- }
复制代码 需使用 AtomicInteger 替代:- private AtomicInteger count = new AtomicInteger(0);
- public void increment() {
- count.incrementAndGet(); // 原子操作
- }
复制代码
- 谨慎用于复合操作:若操作依赖变量的当前值(如读取 - 修改 - 写入),volatile 不适用。
- 性能考量:volatile 的开销低于 synchronized,但频繁访问仍可能影响性能。
4.6 双重校验锁(Double-Checked Locking)
双重校验锁是实现单例模式的高效线程安全方案,结合了synchronized的原子性和volatile的有序性,确保在多线程环境下只创建一个实例。
4.6.1 单例模式的线程安全问题
普通懒汉式单例在多线程下存在竞态条件:- public class Singleton {
- private static Singleton instance;
- private Singleton() {}
- public static Singleton getInstance() {
- if (instance == null) { // ① 第一次检查
- instance = new Singleton(); // ② 创建实例(非原子操作)
- }
- return instance;
- }
- }
复制代码 问题:步骤②包含多个指令(分配内存、初始化对象、引用赋值),可能被编译器重排序为:
- 分配内存空间
- 将引用指向内存空间(此时对象未初始化)
- 初始化对象
若线程 A 执行到重排序后的步骤 ②,线程 B 在①处判断instance != null,直接返回未初始化完成的对象,导致 NPE。
4.6.2 双重校验锁的实现
通过synchronized和volatile解决可见性和重排序问题:- public class Singleton {
- private static volatile Singleton instance; // ① 使用 volatile 禁止指令重排序
- private Singleton() {}
- public static Singleton getInstance() {
- if (instance == null) { // ② 第一次检查(无锁)
- synchronized (Singleton.class) { // ③ 加锁
- if (instance == null) { // ④ 第二次检查(确保只有一个线程创建实例)
- instance = new Singleton(); // ⑤ 安全发布对象
- }
- }
- }
- return instance;
- }
- }
复制代码 4.6.3 关键机制解析
volatile 关键字确保:
- 可见性:所有线程看到的instance引用是最新的。
- 禁止重排序:JMM 会在volatile写操作前插入StoreStore 屏障,确保初始化对象的操作(步骤 3)先于引用赋值(步骤 2)完成。
- 第一次检查(无锁):大多数情况下,实例已创建,直接返回无需加锁,提升性能。
- 第二次检查(加锁后):确保在多线程竞争时,只有第一个获取锁的线程创建实例,其他线程不会重复创建。
仅在实例未创建时加锁,避免每次调用都同步,性能优于直接同步方法:- // 性能较差的写法:
- public static synchronized Singleton getInstance() {
- if (instance == null) {
- instance = new Singleton();
- }
- return instance;
- }
复制代码 4.6.4 执行流程详解
- 线程 A 和 B 同时调用getInstance():
- 若instance为null,都进入①处的条件判断。
- 执行⑤创建实例,由于volatile禁止重排序,初始化完成后才会将引用赋值给instance。
- 发现instance已不为null(步骤④),直接返回已初始化的实例。
- 在①处判断instance不为null,直接返回,无需加锁。
4.6.5 为什么必须使用volatile?
若不使用volatile,步骤⑤的instance = new Singleton()可能被重排序为:- memory = allocate(); // 1. 分配内存
- instance = memory; // 2. 将引用指向内存(此时 instance 不为 null 但未初始化)
- ctorInstance(memory); // 3. 初始化对象
复制代码 当线程 A 执行到 2 时,线程 B 在①处判断instance != null,直接返回未初始化的对象并使用,导致崩溃。
4.6.6 其他线程安全单例实现对比
- public class Singleton {
- private static final Singleton instance = new Singleton(); // 类加载时初始化
-
- private Singleton() {}
-
- public static Singleton getInstance() {
- return instance;
- }
- }
复制代码 优点:简单、线程安全
缺点:类加载时即创建实例,可能浪费资源
- public class Singleton {
- private Singleton() {}
-
- private static class Holder {
- private static final Singleton INSTANCE = new Singleton();
- }
-
- public static Singleton getInstance() {
- return Holder.INSTANCE; // 调用时触发 Holder 类加载,初始化 INSTANCE
- }
- }
复制代码 优点:延迟加载、线程安全(由 JVM 类加载机制保证)
缺点:无法传递参数
- public enum Singleton {
- INSTANCE;
-
- public void doSomething() {
- // ...
- }
- }
复制代码 优点:线程安全、防反序列化、防反射攻击
缺点:不支持延迟加载
4.6.7 总结
双重校验锁单例的核心要点:
- volatile关键字:确保对象安全发布,禁止初始化和赋值操作的重排序。
- 双重检查:减少加锁开销,仅在实例未创建时同步。
- 锁细化:只对创建实例的代码块同步,避免锁粒度过大。
适用场景:需要延迟加载、频繁调用获取方法的单例模式。
5 CAS
5.1 定义
CAS(Compare-and-Swap)是一种无锁算法,用于实现多线程环境下的原子操作。它是 Java 并发包(java.util.concurrent)的核心技术之一,广泛应用于原子类、锁机制和并发容器中。
5.2 基本原理
CAS 操作包含三个参数:CAS(V, E, N),其中:
- V:要更新的变量(内存值)
- E:预期值(旧的预期值)
- N:新值(准备设置的新值)
操作逻辑:
- 如果 V 的值等于 E,则将 V 的值更新为 N,操作成功;
- 否则,操作失败,通常会重试或放弃。
CAS 是一条 CPU 原子指令(如 x86 的 cmpxchg),由硬件保证原子性,避免了使用锁的开销。
5.2 Java 中的 CAS 实现
Java 通过 Unsafe 类提供底层 CAS 操作,AtomicInteger 等原子类基于此实现。
5.2.1Unsafe 类
Unsafe 提供了直接操作内存和硬件的方法,如 compareAndSwapInt:- public final class Unsafe {
- // CAS 操作,原子性地比较并交换值
- public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
- // 其他 CAS 方法...
- }
复制代码
- o:包含要修改字段的对象
- offset:字段在对象中的内存偏移量
- expected:预期值
- x:新值
5.2.2 AtomicInteger 示例
- import java.util.concurrent.atomic.AtomicInteger;
- public class AtomicIntegerExample {
- private AtomicInteger count = new AtomicInteger(0);
- public void increment() {
- // 原子性地增加 1,等价于 count++
- count.incrementAndGet();
- }
- public int getCount() {
- return count.get();
- }
- }
复制代码 incrementAndGet() 的底层实现:- public final int incrementAndGet() {
- return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
- }
- // Unsafe 类的 getAndAddInt 方法
- public final int getAndAddInt(Object o, long offset, int delta) {
- int v;
- do {
- v = getIntVolatile(o, offset); // 获取当前值
- // 循环尝试 CAS 操作,直到成功
- } while (!compareAndSwapInt(o, offset, v, v + delta));
- return v;
- }
复制代码 5.3 CAS 的应用场景
5.3.1 原子类(Atomic*)
如 AtomicInteger、AtomicLong、AtomicReference 等,用于无锁原子操作:- AtomicInteger counter = new AtomicInteger(0);
- // 原子性地更新值
- counter.compareAndSet(0, 1); // 如果当前值为 0,则更新为 1
复制代码 5.3.2 非阻塞数据结构
如 ConcurrentLinkedQueue,通过 CAS 实现无锁队列:- public boolean offer(E e) {
- Node<E> n = new Node<E>(e);
- for (;;) {
- Node<E> t = tail;
- Node<E> s = t.next;
- if (t == tail) {
- if (s == null) {
- // CAS 操作设置新节点
- if (casNext(t, null, n)) {
- casTail(t, n); // 更新尾指针
- return true;
- }
- } else {
- casTail(t, s);
- }
- }
- }
- }
复制代码 5.3.3 AQS(AbstractQueuedSynchronizer)
Java 锁的基础框架,通过 CAS 实现锁状态的原子更新:- protected final boolean compareAndSetState(int expect, int update) {
- // 原子性地更新同步状态
- return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
- }
复制代码 5.3.4 乐观锁
通过版本号机制实现乐观锁,如 StampedLock:- long stamp = lock.tryOptimisticRead(); // 获取乐观读戳记
- // 读取数据
- if (!lock.validate(stamp)) { // 验证戳记是否有效
- stamp = lock.readLock(); // 升级为悲观锁
- try {
- // 重读数据
- } finally {
- lock.unlockRead(stamp);
- }
- }
复制代码 5.4 CAS 的优缺点
5.4.1 优点
- 无锁化:避免了锁的上下文切换和线程阻塞,性能更高。
- 原子性:由硬件保证原子性,比软件锁更可靠。
- 适用于竞争不激烈的场景:在低竞争下,CAS 的效率远高于 synchronized。
5.4.2 缺点
- ABA 问题:如果 V 的值先从 A 变为 B,再变回 A,CAS 会认为值未变,但实际上已经发生了变化。
解决方案:使用 AtomicStampedReference 或 AtomicMarkableReference 记录版本号或标记。
- 自旋开销:CAS 失败后通常会自旋重试,若长时间竞争激烈,会浪费 CPU 资源。
解决方案:限制重试次数,或使用 LockSupport.park() 暂停线程。
- 只能保证一个变量的原子操作:对多个变量的原子操作,CAS 无法直接保证。
解决方案:将多个变量封装成一个对象,使用 AtomicReference。
5.5 CAS 与锁的对比
特性
| CAS
| 锁(如 synchronized)
| 线程阻塞
| 无(自旋)
| 有(上下文切换)
| 适用场景
| 竞争不激烈、短操作
| 竞争激烈、长操作
| 吞吐量
| 高
| 低
| 实现复杂度
| 高(需处理 ABA、自旋等问题)
| 低
| 公平性
| 通常不保证
| 可配置公平锁
| 4 线程安全
线程安全指的是在多线程环境下,对共享资源的访问能够正确执行,不会出现数据不一致或者其他异常情况。
4.1 安全程度
不可变的对象一定是线程安全的,不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确的构建出来,永远也不会看到它在多个线程之中处于不一致的状态。
- final 关键字修饰的基本数据类型
- String
- 枚举类型
- Number 部分子类, 如 Long 和 Double 等数值包装类型,BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则是可变的。
不管运行时环境如何,调用者都不需要任何额外的同步措施。
保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施。但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
- 大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。
指对象本身并不是线程安全的,但是可以通过在调用端正确的使用同步手段来保证对象在并发环境中可以安全地使用,我们平常说一个类不是线程安全的,绝大多数时候指的是这一种情况。Java API 中大部分的类都是属于线程兼容的,如与前面的 Vector 和 HashTable 相对应的集合类 ArrayList 和 HashMap 等。
线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于 Java 语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。
4.2 互斥同步
互斥同步是指在同一时刻只允许一个线程访问共享数据。Java 提供了 synchronized 和 Lock 来保证互斥同步。互斥同步最主要的问题是性能问题,因为同一时刻只允许一个线程访问共享数据,其他线程只能等待,这样会导致线程上下文的切换和调度,降低性能,因此这种同步也叫阻塞同步。
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有其他线程等待锁释放等操作。
4.3 非阻塞同步
非阻塞同步是指一个线程访问共享数据时,另一个线程不会被阻塞。Java提供了volatile、CAS、AtomicInteger等来保证非阻塞同步。
- CAS (Compare And Swap):比较并交换。CAS是一种乐观的并发策略,总是认为不会出现问题,只有在更新共享数据时才会去检查是否有其他线程修改了共享数据。CAS是通过硬件来保证原子性的,不需要加锁,因此CAS是一种非阻塞同步。
CAS指令需要有3个操作数,分别是内存地址V、旧的预期值A和新值B。当且仅当V的值等于A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作。
- AtomicInteger:JUC包里面的整数原子类AtomicInteger,其中的compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作。
ABA 问题:如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。
4.4 无同步方案
要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无需任何同步措施去保证正确性。
- 栈封闭:多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。
- 线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |