1 原子性、可见性和有序性的基本概念
1.原子性(Atomicity)
由Java内存模型来直接保证的原子性变量操作包括read、load、use、assign、store和write六个,大致可以认为基础数据类型的访问和读写是具备原子性的。如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock与unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐匿地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块—synchronized关键字,因此在synchronized块之间的操作也具备原子性。
2.可见性(Visibility)
可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则一般保证了新值能立即同步到主内存,以及每使用前立即从内存刷新。因为我们可以说volatile保证了线程操作时变量的可见性,而普通变量则不能保证这一点。
除了volatile之外,Java还有两个关键字能实现可见性,它们是synchronized和final。同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这条规则获得的,而final关键字的可见性是指:被final修饰的字段是构造器一旦初始化完成,并且构造器没有把“this”引用传递出去(即没有造成this引用的溢出。),那么在其它线程中就能看见final字段的值。
3.有序性(Ordering)
Java内存模型中的程序天然有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存和主内存同步延迟”现象。
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则来获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。
深入可参考:
深入理解Java虚拟机笔记—原子性、可见性、有序性
Stack Overflow:Allowing the this reference to escape
2 一个并发的原子性操作问题
从一个面试的问题开始:(此章节来自:Java 原子操作与并发 ,稍作修改)
调用set1()、set2()、check(),会输出ERROR吗?
public class P1 {
private long b = 0;
public void set1() {
b = 0;
}
public void set2() {
b = 1;
}
public void check() {
System.out.println(b);
if (0 != b && 1 != b) {
System.err.println("Error");
}
}
}
咋一看,调用set1、set2,b的值只可能为0或-1,所以check里的判断永远不成立,也就是不输出“ERROR”。
其实不然,这个问题是一个并发的问题,存在两个陷阱!
下面模拟多线程环境下调用的情况:
public static void main(final String[] args) {
final P1 v = new P1();
// 线程 1:设置 b = 0
final Thread t1 = new Thread() {
public void run() {
while (true) {
v.set1();
}
};
};
t1.start();
// 线程 2:设置 b = -1
final Thread t2 = new Thread() {
public void run() {
while (true) {
v.set2();
}
};
};
t2.start();
// 线程 3:检查 0 != b && -1 != b
final Thread t3 = new Thread() {
public void run() {
while (true) {
v.check();
}
};
};
t3.start();
}
使用 3 个线程分别重复执行 set1()、set2()、check()。执行输出结果部分如下:
....
0
0
1
1
1
Error
Error
-4294967296
0
0
4294967295
....
执行环境:
Java(TM) SE Runtime Environment (build 1.6.0_31-b05)
Java HotSpot(TM) Client VM (build 20.6-b01, mixed mode, sharing), 32bit
分析:
这道题目有两个陷阱,分别考察了对并发执行的理解,以及对 JVM 基础(赋值操作)的掌握。
陷阱一:并发执行
并发执行就是多个操作一起执行,CPU 执行不同上下文(可理解为不同线程)发过来的指令。操作系统上层看上去就像是并行处理一样。并发执行就是多个操作一起执行,CPU 执行不同上下文(可理解为不同线程)发过来的指令。操作系统上层看上去就像是并行处理一样。
也就是说,在编程语言层面,一个简单的操作同样需要考虑并发问题。
在 check() 中的 if 判断上和设值是存在并发的,可能会出现这种情况:假设b=1,此时判断 0 != b 为真,再判断1!=b时,恰好 b 被赋值为 0 时判断也为真了,所以就输出了“ERROR”。
除此外,无论 JVM、操作系统、CPU 层面对指令如何优化、重排,最终都是逐一执行单一指令,唯一不同的就是不同层面可能会对执行加以限制,
比如加入原子操作,最终保证 CPU 能够完整执行一组指令。
陷阱二:JVM 赋值操作
注意有些赋值操作不是原子性的!
Java 基本类型中,long 和 double 是 64 位长的。32 位架构 CPU 的算术逻辑单元(ALU)宽度是 32 位的,在处理大于 32 位操作时需要处理两次,即读取高32位和低32位。这就说明了为什么输出了4294967295和-4294967296的原因了。
Java 已经是封装底层细节很好的语言了,但依然需要注意这些陷阱,可以使用并发处理包 java.util.concurrent.atomic 中包含了一系列无锁原子操作类型。
也可以使用 volatile 关键字保证线程间变量的可见性。当然时就使用的时候还是要注意其他问题,比如count++操作的并发问题等。
深入阅读:
聊聊并发(一)——深入分析Volatile的实现原理
Java 理论与实践: 正确使用 Volatile 变量
多核线程笔记-volatile原理与技巧
3 并发库的原子性操作
java.util.concurrent.atomic包可以对基本数据类型、数组中的基本数据类型和类中的基本数据类型等进行操作。可将 volatile 值、字段和数组元素的概念扩展到那些也提供原子条件更新操作的类。包中的类可以分成4组:
标量类(Scalar):AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
复合变量类:AtomicMarkableReference,AtomicStampedReference
内部实现原理是通过Unsafe类的方法来实现原子性操作的:(sun并没有给出Unsafe的源代码),此处不再详解。
import sun.misc.Unsafe;
...
private static final Unsafe unsafe = Unsafe.getUnsafe();
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
转载请注明出处:原子性、可见性、有序性和并发库的原子性操作