回答
所谓原子性,就是一个操作或者多个操作,要么完全执行,要么完全不执行,在执行过程中是不能被其他因素打断或者插入。
volatile 我们知道它能保证可见性和有序性的,但是对于原子性,它无法保证。
详细分析
原子性
原子性的核心概念就两次:完整、不可分割。也就是某个线程在正在做某个业务时,要么它这个业务完整执行完,要么完全不执行,不允许处于一个中间状态。
我们最最经典的原子操作是银行账户转账:从账户 A 向账户 B 转入 10000 元,它只允许存在两种情况:
- 转账成功:从账户 A 扣掉 10000 元,给账户 B 增加 10000 元。
- 转账失败:账户 A 、账户 B 均没有变化。
不可能存在一个所谓的中间状态,如从账户 A 扣掉 10000 元,但是账户 B 不增加 10000 元。
我们看下面这段代码:
i = 1; // 1
j = i; // 2
i++; // 3
i = j + 1; // 4
这四句代码中哪些是原子操作,哪些不是,首先在单线程环境下,我们可以认为上面四个全部都是原子操作,但是多线程,这四句代码情况如下:
i = 1;
:是原子操作。赋值操作通常是原子的,因为它只涉及单个步骤,即将一个值写入到一个变量中。j = i;
:不是原子操作。因为这段可以分为两个步骤,读取 i,赋值 j。i++;
:不是原子操作,它包含了三个步骤:读取 i,执行 i + 1,新值写入 i。i = j + 1
:与 i++ 一样,包含了三个步骤。
其实在多线程环境下,Java 只保证基本类型的赋值操作是原子性,但这也仅限于单个变量且没有涉及到 64 位的 long
和 double
类型。
volatile 能保证原子性吗?
我们先看一个简单的例子:
public class SkTest {
private int num = 0;
public void addNum() {
num++;
}
public int getNum() {
return num;
}
public static void main(String[] args) {
SkTest test = new SkTest();
for (int i = 0; i < 20; i++) {
new Thread(() ->{
for (int j = 0; j < 10000; j++) {
test.addNum();
}
}).start();
}
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("num 的值为:" + test.getNum());
}
}
num 是普通变量,num++ 又不是原子操作,那么在多线程环境下,肯定不是线程安全的,我们执行5次,结果分别为:
num 的值为:108691
num 的值为:46885
num 的值为:135318
num 的值为:45002
num 的值为:34058
那在 num 前面加上 volatile 呢?
public class SkTest {
private volatile int num = 0;
// 省略其他代码
}
执行 5 次结果:
num 的值为:65806
num 的值为:115472
num 的值为:73228
num 的值为:109038
num 的值为:114220
为什么会出现这种情况呢?还是 volatile 的语句决定的,volatile 语义如下:
当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。 当读一个 volatile变量时,JMM 会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
即保证可见性,但不保证原子性。大明哥用一张图来解释下上图示例你就清晰了:
JMM 对原子性问题的保证
- 在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,但是 long 和 double 是非原子性协定,在某些场景下,它并不是原子性的。
- 锁的原子性保证:在Java中,锁(
synchronized
或者lock
锁)可以保证在同一时刻只有一个线程能执行一个同步块的代码。它保证了在同步块内的操作是原子性的,对共享变量的所有读取和写入都是原子性的。 - Atomic类的原子操作:Java 提供了一套原子类,如
AtomicInteger
、AtomicLong
等,它们利用了CAS 操作来实现变量操作的原子性。