19Java多线程之并发编程三大特性
原子性
原子是世界上的最小单位,具有不可分割性。
比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。
非原子操作都会存在线程安全问题,需要我们使用同步技术(synchronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。
如果把一个事务看作是一个程序,要么全部执行,要么全不执行。这种特性就叫原子性。
在 Java 中 synchronized 和在 lock、unlock 中的操作保证了原子性。
concurrent 包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。
扩展链接:
[Redis的单个操作是原子性的,多个操作支持事务](https://www.runoob.com/redis/redis-intro.html#:~:text=原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。)
可见性
可见性与Java的内存模型有关,模型采用缓存与主存的方式对变量进行操作,也就是说,每个线程都有自己的缓存空间,对变量的操作都是在缓存中进行的,之后再将修改后的值返回到主存中,这就带来了问题,有可能一个线程修改共享变量后,还没有来的及将缓存中的变量刷新到主存,另外一个线程就对共享变量进行修改,那么这个线程拿到的值是主存中未被修改的值,这就是可见性的问题。
可见性,指线程间内存的可见性,一个线程的修改,对另一个线程立马可见。
在 Java 中 volatile、synchronized 实现可见性。
可见性问题示例:
1 | public class Test { |
有序性
指令重排 (Instruction Reordering):编译器 / JVM / CPU 出于性能优化目的调整执行顺序(打破有序性的一种手段)。
Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性。
volatile
volatile
变量在写入和读取时,会插入特定的内存屏障(memory barrier),从而:
- 禁止特定类型的指令重排序(写前不能有写,读后不能有写);
- 保证对变量的可见性(一个线程写入,另一个线程马上能看到);
- ⚠️ 不保证原子性(这点常被忽视)。
具体表现:
- 写入
volatile
:JVM 会在后面插入 StoreStore + StoreLoad 屏障; - 读取
volatile
:JVM 会在前面插入 LoadLoad + LoadStore 屏障; - 这些屏障会让前后的操作 不能重排,因此达成“部分顺序性保证”。
synchronized
synchronized
的并发控制来自 Java 的“互斥锁语义”:同一时刻,最多只有一个线程持有某个对象的锁。
synchronized
也隐式地具备内存屏障语义:
- 进入同步块(lock acquire)时:会清空当前线程的工作内存,重新从主内存读取;
- 退出同步块(lock release)时:会把本地工作内存刷新到主内存;
- 这两步确保了同步块之间对同一锁的操作是 顺序可见的(强 happen-before 关系)。