Java 多线程的 3 大特性
- Java 并发的 3 大特性,也可以说是并发编程的三大核心问题,分别是可见性、原子性和有序性
- 同步原语是操作系统或编程语言中提供的底层同步机制,用于协调多线程对共享资源的访问,确保操作的可见性、原子性和有序性
Visibility 可见性
- 指在多个线程访问同一个变量的情况下,当一个线程修改了该共享变量的值后,其他线程能够立刻读到修改后的值(能够及时被其他线程看到)
- 普通的共享变量不能保证可见性,因为该共享变量被修改后值是先写入到本地内存(线程的工作内存)缓存里的,后续写入主内存的时间是不确定的,当其他线程去读取时,此时取到的很有可能还是原来的旧值,所以无法保证可见性
- 可以通过 volatile 关键字保证变量的可见性,当一个变量被声明为 volatile 时,对该变量的读取和写入操作都直接在主内存中进行,而不是在本地缓存中,所以一个线程对 volatile 变量的修改对其他线程是立即可见的
- 可以通过 synchronized 或 Lock 保证可见性,因为 synchronized 或 Lock 可以保证同一时刻只有一个线程能访问共享资源,并在其释放锁之前将变量修改后的值刷新到主内存中
- 另外可以通过 Atomic 原子类型来保证可见性(比如 AtomicInteger、AtomicLong 等,因为 Atomic 原子类型内部的 value 使用了 volatile 关键字修饰来保证了变量的可见性)
Atomicity 原子性
- 指一个操作或者一系列操作是整体不可分割的,要么全部执行,并且在执行过程中是不能被任何因素打断,要么全部不执行(完全不发生),原子性确保了在多线程环境下对变量的操作是整体不可分割的
- Java 中对基本数据类型的变量的读取和赋值(不过赋值必须是值赋给变量,变量之间的相互赋值不是原子性操作)、引用数据类型的赋值操作本身就是原子性操作,但是如果不用 volatile 关键字修饰的话,其他线程可能不能立马看到改后的新值,而对于复合操作(例如 i++,包含了三个步骤:读取 i 值、对 i 加 1、然后将新值写回 i,这三个步骤之间可能被其他线程打断,导致结果数据不一致),所以即便加了 volatile 关键字后,整体操作也做不到原子性
- 可以通过 Atomic 原子类型保证原子性
- 可以通过 synchronized 或 Lock 保证原子性,因为 synchronized 或 Lock 能够保证同一时刻只有一个线程访问这个代码块或方法
Ordering 有序性
- 在多线程环境下,为了让机器指令能更符合 CPU 的执行特性,编译器和处理器为了优化性能,在程序编译成机器码指令时可能适当的会出现指令重排的现象(指令重排序),然而这种重排序就有可能导致多线程中出现数据不一致问题
- 可以通过 volatile 关键字来保证变量的有序性(它能禁止指令重排序,提供了变量的读写操作的有序性保证)
- 可以通过 synchronized 和 Lock 来保证有序性,很显然 synchronized 或 Lock 保证同一时刻只有一个线程访问这个代码块或方法,相当于是让线程顺序执行同步代码,自然就保证了有序性
- 不过 Atomic 原子类型无法保证有序性
复合操作(比如 i++)保证安全
private int i = 0;
public synchronized void increment() {
i++;
}
private AtomicInteger i = new AtomicInteger(0);
public void increment() {
i.incrementAndGet();
}
总结
- 可以使用 synchronized 或 Lock 来保证可见性、原子性和有序性
- 可以使用 volatile 关键字来保证可见性和有序性
- 可以使用 Atomic 原子类型来保证可见性和原子性