一、简介
Java 多线程中的并发性质是确保多线程程序正确性和高效性的核心概念。这些性质主要包括 原子性、可见性 和 有序性。
1.1 原子性(Atomicity)
-
定义:
- 原子性是指一个操作或一系列操作是不可分割的,在执行过程中不会被其他线程中断或干扰。要么全部执行完成,要么全部不执行。
-
问题来源:
- 在多线程环境下,如果一个操作不是原子的,可能会导致数据不一致。例如:
- i++;这个操作其实分成以下3步进行:
- 读取i的值;
- 将i的值加1;
- 将加1后的值赋给i;
- 如果多个线程同时执行 i++,可能会导致结果不符合预期。
- i++;这个操作其实分成以下3步进行:
- 示例:
public class AtomicityExample { private static int count = 0; public static void increment() { count++; // 这不是原子性操作 } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { increment(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { increment(); } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("Count: " + count); } } 在这个例子中,count++操作实际上包含了三个步骤(读取、增加、写回), 在多线程环境下不是原子性的。因此,最终的结果可能不是2000。
- 在多线程环境下,如果一个操作不是原子的,可能会导致数据不一致。例如:
-
解决方案:
- 对于基本数据类型的变量,Java内存模型(JMM)保证了读取和赋值操作的原子性。
- 对于更复杂的操作,可以使用synchronized关键字、java.util.concurrent.locks.Lock接口、以及java.util.concurrent.atomic包下的原子类(如AtomicInteger、AtomicLong等)来保证操作的原子性。
-
示例:
- 使用AtomicInteger保证操作的原子性。
import java.util.concurrent.atomic.AtomicInteger; public class AtomicityExample { private static AtomicInteger atomicCount = new AtomicInteger(0); public static void increment() { atomicCount.incrementAndGet(); // 原子性操作 } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { increment(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { increment(); } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("Atomic Count: " + atomicCount.get()); } } incrementAndGet()方法是一个原子性操作,保证了计数的正确性。
- 使用AtomicInteger保证操作的原子性。
1.2 可见性(Visibility)
-
定义:
- 可见性是指一个线程对共享变量的修改,其他线程能够立即看到修改后的值。
-
问题来源
- 为了提高CPU性能,会将数据缓存在寄存器或缓存中,而不是直接从主内存读取。这可能导致一个线程修改了共享变量的值,但其他线程仍然读取到旧值。
- 示例:
public class VisibilityExample { private static boolean flag = false; public static void write() { flag = true; } public static void read() { while (!flag) { // 等待flag变为true } System.out.println("Flag is true"); } public static void main(String[] args) { Thread thread1 = new Thread(VisibilityExample::write); Thread thread2 = new Thread(VisibilityExample::read); thread1.start(); try { Thread.sleep(100); // 等待thread1执行 } catch (InterruptedException e) { e.printStackTrace(); } thread2.start(); } } 在这个例子中,thread1将flag设置为true,但thread2可能看不到这个改变, 因为它可能一直在自己的工作内存中读取旧的flag值。
-
解决方案:
- 使用volatile关键字修饰变量,可以确保对该变量的修改能够立即被其他线程看到。
- 使用 synchronized 关键字,确保线程在释放锁之前将变量的修改刷新到主内存。
- 使用 final 关键字,确保在构造函数完成后,变量的值对所有线程可见。
-
示例:
- 使用volatile关键字保证可见性。
public class VisibilityExample { private static volatile boolean flag = false; public static void write() { flag = true; } public static void read() { while (!flag) { // 等待flag变为true } System.out.println("Flag is true"); } public static void main(String[] args) { Thread thread1 = new Thread(VisibilityExample::write); Thread thread2 = new Thread(VisibilityExample::read); thread1.start(); try { Thread.sleep(100); // 等待thread1执行 } catch (InterruptedException e) { e.printStackTrace(); } thread2.start(); } } 这里volatile关键字确保了thread1对flag的修改对thread2是可见的。
- 使用volatile关键字保证可见性。
1.3 有序性(Ordering)
-
定义:
- 有序性是指程序执行的顺序按照代码的先后顺序执行。在并发编程中,由于编译器优化和CPU指令重排序,可能会导致程序的执行顺序与代码的编写顺序不一致,从而影响程序的正确性。
-
问题来源
- 为了提高性能,编译器和处理器可能会对指令进行重排序,只要不影响单线程的执行结果。但在多线程环境下,重排序可能导致不可预期的结果。
- 示例:
int a = 0; boolean flag = false; // 线程 1 a = 1; // 语句 1 flag = true; // 语句 2 // 线程 2 if (flag) { // 语句 3 int b = a; // 语句 4 } 如果线程 1 的语句 1 和语句 2 被重排序,线程 2 可能会读取到 a 的旧值(0)。
-
解决方案:
- 使用 volatile 关键字,防止指令重排序。
- 使用 synchronized 关键字,确保同一时刻只有一个线程执行代码块。
- 使用 java.util.concurrent 包中的并发工具类(如 CountDownLatch、CyclicBarrier 等)。
-
示例:
- 使用volatile关键字或同步块。
public class OrderingExample { private static volatile int x = 0, y = 0; public static void writer() { x = 1; y = 2; } public static void reader() { int a = y; int b = x; System.out.println("x: " + b + ", y: " + a); } public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(OrderingExample::writer); Thread thread2 = new Thread(OrderingExample::reader); thread1.start(); thread1.join(); thread2.start(); thread2.join(); } } volatile关键字确保了writer线程对x和y的赋值操作不会被重排序, 从而保证了reader线程看到正确的结果。
- 使用volatile关键字或同步块。
二、Java 内存模型
Java 内存模型(Java Memory Model, JMM) 是 Java 虚拟机(JVM)规范中定义的一种抽象模型,用于描述多线程环境下,线程如何与主内存(Main Memory)以及线程的本地内存(Local Memory,如缓存或寄存器)交互。
2.1 JMM 的核心概念
- 主内存与工作内存
- 主内存(Main Memory):所有线程共享的内存区域,存储了所有的变量(实例字段、静态字段等)。
- 工作内存(Working Memory):每个线程独享的内存区域,存储了线程使用的变量的副本。线程对变量的所有操作(读取、赋值等)都发生在工作内存中,不能直接操作主内存。
- 内存间的交互
JMM 定义了线程如何将数据从主内存读取到工作内存,以及如何将修改后的数据写回主内存。具体操作包括:- read:从主内存读取变量到工作内存。
- load:将读取的值放入工作内存的变量副本中。
- use:线程使用工作内存中的变量值。
- assign:线程将新值赋给工作内存中的变量。
- store:将工作内存中的变量值写回主内存。
- write:将 store 操作的值写入主内存的变量中。
2.2 Happens-Before 规则
Happens-Before 是 JMM 的核心规则,用于定义操作之间的可见性和有序性。如果操作 A Happens-Before 操作 B,那么操作 A 的结果对操作 B 可见,且操作 A 在操作 B 之前执行。常见的 Happens-Before 规则有如下:
- 程序顺序规则:在同一个线程中,前面的操作 Happens-Before 后面的操作。
- volatile 规则:对一个 volatile 变量的写操作 Happens-Before 后续对该变量的读操作。
- 锁规则:解锁操作 Happens-Before 后续的加锁操作。
- 线程启动规则:线程的 start() 方法 Happens-Before 该线程的任何操作。
- 线程终止规则:线程中的所有操作 Happens-Before 其他线程检测到该线程已经终止(如通过 join() 或 isAlive())。
- 传递性规则:如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。
2.3 JMM 的实现机制
- volatile 关键字
- 保证变量的可见性:每次读取 volatile 变量时,都会从主内存中读取;每次写入 volatile 变量时,都会立即刷新到主内存。
- 禁止指令重排序:通过内存屏障(Memory Barrier)实现。
- synchronized 关键字
- 保证原子性:同一时刻只有一个线程可以执行同步代码块。
- 保证可见性:线程在释放锁之前,会将工作内存中的变量刷新到主内存。
- 保证有序性:通过锁机制防止指令重排序。
- final 关键字
- 保证可见性:在构造函数中完成对 final 变量的初始化后,其他线程可以看到正确的值。
- 禁止指令重排序:防止构造函数中的指令重排序。
- 内存屏障(Memory Barrier)
内存屏障是 JMM 实现有序性的底层机制,用于禁止特定类型的指令重排序。常见的屏障包括:- LoadLoad:确保前面的读操作在后面的读操作之前完成。
- StoreStore:确保前面的写操作在后面的写操作之前完成。
- LoadStore:确保前面的读操作在后面的写操作之前完成。
- StoreLoad:确保前面的写操作在后面的读操作之前完成。
三、总结
性质 | 定义 | 问题来源 | 解决方案 |
---|---|---|---|
原子性 | 操作不可分割 | 非原子操作导致数据不一致 | synchronized、原子类、锁 |
可见性 | 修改对其他线程可见 | 缓存导致读取旧值 | volatile、synchronized、final |
有序性 | 代码执行顺序符合预期 | 指令重排序导致意外结果 | volatile、synchronized |