在多线程编程中,理解线程的 原子性、可见性 和 有序性 是构建正确并发程序的基础。以下是它们的详细解释:
1. 原子性 (Atomicity)
定义 原子性指的是操作不可被中断,要么全部执行完成,要么完全不执行。
特性
-
原子性操作在执行时不会被其他线程干扰。
-
如果多个线程同时访问共享资源,原子性可以防止数据的不一致。
Java 中的原子性
-
原子性操作示例:
-
读取和写入基本数据类型(如
int
、float
)是原子性的。 -
对
volatile
变量的读取/写入是原子性的(不适用于复合操作)。
-
-
非原子性操作:
-
复合操作(如
counter++
或counter = counter + 1
)是非原子性的。这些操作实际上包括三步:读取变量值、修改值、写回变量。
-
解决方案
-
使用 同步机制(如
synchronized
块或方法):synchronized (lock) { counter++; }
-
使用 原子类(如
AtomicInteger
):AtomicInteger counter = new AtomicInteger(); counter.incrementAndGet();
2. 可见性 (Visibility)
定义 可见性指的是一个线程对共享变量的修改对其他线程是可见的。
特性
-
在多线程环境中,如果没有同步机制,一个线程对变量的修改可能不会立刻被其他线程看到(由于 CPU 缓存或编译优化)。
-
线程可能会一直使用自己 CPU 缓存中的值,而看不到其他线程更新后的值。
Java 中的可见性
-
存在可见性问题的场景:
private static boolean flag = false; public static void main(String[] args) { new Thread(() -> { while (!flag) { // do something } System.out.println("Thread ended."); }).start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } flag = true; // 主线程修改了 flag,但子线程可能看不到 }
-
解决方案:
-
使用 volatile 关键字:
private static volatile boolean flag = false;
-
volatile
确保变量的修改对所有线程立即可见。
-
-
使用 同步机制(如
synchronized
或显式锁Lock
),因为同步也能保证可见性。
-
3. 有序性 (Ordering)
定义 有序性指的是程序代码的执行顺序,通常来说,程序会按照代码编写的顺序执行,但编译器和处理器会为了优化性能进行 指令重排。
特性
-
在单线程环境中,指令重排不会影响程序的正确性。
-
在多线程环境中,指令重排可能导致意想不到的结果,因为线程之间的执行顺序无法预测。
指令重排的例子
int a = 1; // (1) int b = 2; // (2) int c = a + b; // (3)
在单线程中,执行顺序是 (1) -> (2) -> (3)
,但由于重排优化,CPU 可能将 (2)
和 (1)
的顺序交换。尽管结果在单线程环境中是正确的,但多线程中可能导致数据问题。
Java 中的有序性
-
可能出现问题的场景:
-
使用
volatile
可以禁止指令重排:package com.example.demopool; import org.springframework.stereotype.Component; /** * @Author: cv master * @Date: 2024/11/15 09:17 */ @Component public class B { private boolean flag = false; private int a = 0; public void writer() { a = 1; // 写变量 flag = true; // 通知其他线程 } public void reader() { if (flag) { // 读标志 System.out.println(a); // 此时 a 的值一定是 1 } } public static void main(String[] args) throws InterruptedException { B b = new B(); Thread thread1 = new Thread(b::writer); Thread thread2 = new Thread(b::reader); thread1.start(); thread2.start(); thread1.join(); thread2.join(); } }
-
在上述代码中,
volatile
保证了写操作的顺序,使得a = 1
一定发生在flag = true
之前。
-
解决方案
-
使用 volatile:确保关键变量的修改不会被指令重排。
-
使用 synchronized 或 显式锁:同步代码块可以强制线程按照指定顺序执行。
三者关系与 happens-before
原则
-
原子性 和 可见性 是独立的,但有时需要结合使用才能实现正确的多线程行为。
-
有序性 通常需要通过
volatile
或同步机制来确保。 -
happens-before
是 Java 内存模型中定义的一种原则,用于规定线程间的操作顺序:-
一个线程对变量的写操作对另一个线程的读操作可见,必须满足
happens-before
原则。 -
如:
synchronized
、volatile
、线程启动/终止等操作会建立happens-before
关系。
-
总结
特性 | 描述 | 解决方法 |
---|---|---|
原子性 | 操作不可中断,要么全部执行成功,要么完全不执行。 | 使用 synchronized 或原子类如 AtomicInteger 。 |
可见性 | 一个线程的修改对其他线程立刻可见。 | 使用 volatile 或同步机制如 synchronized 。 |
有序性 | 程序执行顺序符合预期,避免指令重排导致问题。 | 使用 volatile 、同步机制(synchronized 或锁)。 |