三大性质总结:原子性、可见性、有序性
三大性质总结:原子性、可见性、有序性
1. 原子性
原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
先看看哪些是原子操作,哪些不是原子操作,有一个直观的印象:
int a = 10; //1
a++; //2
int b = a; //3
a = a + 1; //4
上面这四个语句中只有第 个语句是原子操作,将 赋值给线程工作内存的变量 a
,而语句 (a++
),实际上包含了三个操作:
- 读取变量
a
的值; - 对
a
进行加一的操作; - 将计算后的值再赋值给变量
a
而这三个操作无法构成原子操作。
对语句 , 的分析同理可得这两条语句不具备原子性。
在 java 内存模型中定义了 种操作都是原子的,不可再分的。
lock
(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态;unlock
(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;read
(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面的load
动作使用;load
(载入):作用于工作内存中的变量,它把read
操作从主内存中得到的变量值放入工作内存中的变量副本;use
(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;assign
(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;store
(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存中以便随后的write
操作使用;write
(操作):作用于主内存的变量,它把store
操作从工作内存中得到的变量的值放入主内存的变量中。
上面的这些指令操作是相当底层的,可以作为扩展知识面掌握下。
那么如何理解这些指令了?
比如,把一个变量从主内存中复制到工作内存中就需要执行 read
,load
操作,将工作内存同步到主内存中就需要执行 store
,write
操作。
注意
java 内存模型只是要求上述两个操作是顺序执行的并不是连续执行的。
也就是说 read
和 load
之间可以插入其他指令,store
和 writer
也可以插入其他指令。
比如对主内存中的 a
,b
进行访问就可以出现这样的操作顺序:read a, read b, load b, load a
。
由原子性变量操作 read,load,use,assign,store,write
,可以大致认为基本数据类型的访问读写具备原子性(例外就是 long 和 double 的非原子性协定)
synchronized
上面一共有八条原子操作,其中六条可以满足基本数据类型的访问读写具备原子性,还剩下 lock
和 unlock
两条原子操作。如果我们需要更大范围的原子性操作就可以使用 lock
和 unlock
原子操作。
尽管 jvm 没有把 lock
和 unlock
开放给我们使用,但 jvm 以更高层次的指令 monitorenter
和 monitorexit
指令开放给我们使用,反应到 java 代码中就是 synchronized 关键字,也就是说 synchronized 满足原子性。
我们先来看这样一个例子:
public class VolatileExample {
private static volatile int counter = 0;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++)
counter++;
}
});
thread.start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter);
}
}
开启 个线程,每个线程都自加 次,如果不出现线程安全的问题最终的结果应该就是:10*10000 = 100000
。可实际上,不管运行多少次都是小于 的结果,问题在于 volatile
并不能保证原子性,在前面说过 counter++
这并不是一个原子操作,包含了三个步骤:
- 读取变量
counter
的值; - 对
counter
加一; - 将新值赋值给变量
counter
。
如果线程 A 读取 counter
到工作内存后,其他线程对这个值已经做了自增操作后,那么线程 A 的这个值自然而然就是一个过期的值,因此,总结果必然会是小于 的。
如果让 volatile
保证原子性,必须符合以下两条规则:
- 运算结果并不依赖于变量的当前值,或者能够确保只有一个线程修改变量的值;
- 变量不需要与其他的状态变量共同参与不变约束。
2. 有序性
2.1 synchronized
synchronized
语义表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。
因此,synchronized
语义就要求线程在访问读写共享变量时只能 “串行” 执行,故 synchronized
具有有序性。
2.2 volatile
在 java 内存模型中说过,为了性能优化,编译器和处理器会进行指令重排序;也就是说 java 程序天然的有序性可以总结为:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的。
在单例模式的实现上有一种双重检验锁定的方式(Double-checked Locking)。代码如下:
public class Singleton {
private Singleton() {};
private volatile static Singleton instance;
public Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这里为什么要加 volatile
了?我们先来分析一下不加 volatile
的情况,有问题的语句是这条:
instance = new Singleton();
这条语句实际上包含了三个操作:
- 分配对象的内存空间;
- 初始化对象;
- 设置 instance 指向刚分配的内存地址。
但由于存在重排序的问题,可能有以下的执行顺序:
如果 和 进行了重排序的话,线程 B 进行判断 if(instance == null)
时就会为 ,而实际上这个 instance 并没有初始化成功,显而易见对线程 B 来说之后的操作就会是错的。而用 volatile
修饰的话就可以禁止 和 操作重排序,从而避免这种情况。
volatile
包含禁止指令重排序的语义,故其具有有序性。
3. 可见性
可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。
通过之前对 synchronzed
内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。从而,synchronized
具有可见性。
同样的在 volatile
分析中,会通过在指令中添加 lock
指令,以实现内存可见性。因此,volatile
具有可见性。
4. 总结
综上分析:
synchronized
: 具有原子性,有序性和可见性volatile
:具有有序性和可见性