文章目录
- 前言
- 详解
- 解决多线程下的问题
- Happens-before原则
- 总结
- as-if-serial语义
- happens-before的例子
前言
"as-if-serial"原则是Java内存模型中的一个重要概念。该规则规定:不管怎么重排序(编译期间的重排序,指令级并行的重排序,内存系统的重排序等),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
为了获取更好的性能,编译器和处理器常常会对指令做重排序,但是他们必须遵守数据依赖性,即在不改变单线程程序执行结果的前提下进行指令重排序。例如,对于以下代码:
int a = 1; //语句1
int b = 2; //语句2
int c = a + b; //语句3
语句1和语句2没有数据依赖性,可以重排序。但是语句3依赖于语句1和语句2,所以它不能被重新排序到语句1或语句2之前。
然而,这个原则只适用于单线程,对于多线程,就需要遵守happens-before原则,确保线程间操作的有序性和可见性。
详解
as-if-serial原则是说,不考虑并发编程的情况,Java程序的执行结果应该与该程序在串行化环境中的执行结果一致。简单来说,就是程序在执行过程中无论如何重新排序(例如,编译器的优化,处理器的优化),只要最终呈现出的执行结果与串行执行的结果一致,那么这样的重排序是被允许的。
例如,考虑以下代码:
int a = 1;
int b = 2;
int c = a + b;
依照as-if-serial原则,虽然在执行过程中,可能会将int b = 2;
语句移到int c = a + b;
之后执行,但是最终的执行结果(c的值)仍然与串行化执行的结果一致。
但在并发环境下,as-if-serial原则可能会导致问题。例如:
public class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
在并发环境下,如果有两个线程同时执行 increment() 方法,由于 as-if-serial 原则,编译器或处理器可能将 count++ 重排序为两个操作:先读取 count 的值,然后再写回 count+1 的值。在两个线程并发执行的情况下,可能第一个线程读取了 count 的值,然后第二个线程也读取了 count 的值,然后两个线程都将 count+1 的值写回,导致 count 的值只增加了 1,而不是预期的 2。
下面是一个更具体的例子来说明 as-if-serial 原则可能导致的问题:
```java
public class Example {
private int a = 0;
private int flag = 0;
public void writer() {
a = 1; //1
flag = 1; //2
}
public void reader() {
if (flag == 1) //3
{
int i = a; //4
}
}
}
在这个例子中,假设有两个线程,一个线程执行 writer() 方法,另一个线程执行 reader() 方法。按照 as-if-serial 原则,编译器或处理器可能会将 writer() 方法中的两行代码的顺序交换,即先执行 flag = 1,然后再执行 a = 1。如果这样的重排序发生,reader() 方法可能会在 a 被赋值之前就读取到 flag 的值为 1,然后读取到 a 的值为 0,而不是预期的 1。
解决多线程下的问题
Java通过使用volatile关键字来解决这个问题。
当一个变量被volatile修饰后,它将具备两种特性:
-
可见性(Visibility): 当一个线程修改了一个volatile变量的值,新值对于其他线程来说是可以立即得知的。
-
禁止指令重排序优化:普通的变量仅仅会满足1,而被volatile修饰过的变量由于禁止指令重排序优化,可以满足2。
这两种特性使得volatile变量在并发编程中非常有用。
例如,在上面的例子中,如果flag变量被声明为volatile,那么两个线程看到的flag永远都是最新的,如果writer线程更改了flag的值,reader线程立刻就能看到,这就解决了可见性问题。同时,对一个volatile变量的任何写操作,都会立即刷新到主存,因此在写操作后的任何读操作,都会看到这个新值。
volatile关键字还有一个额外的特性就是禁止指令重排序。编译器在执行优化时,可能会重新排序代码的执行顺序。当flag变量被volatile关键字修饰后,编译器就不会对这个变量前后的代码进行重排序,这就保证了顺序性,解决了重排序问题。
Happens-before原则
是用来判断数据是否存在竞争、线程之间的修改操作是否对其他线程可见的原则。
引入happens-before原则的原因主要有两个:
-
解决可见性问题:在并发编程中,由于线程切换、编译器优化等原因,一个线程对共享变量的修改不一定立即对其他线程可见,这就导致了可见性问题。通过Happens-before原则,我们可以清楚地知道哪些操作对其他线程可见。
-
解决有序性问题:在并发编程中,由于指令重排序,代码的执行顺序可能会与我们编写的顺序不同。通过Happens-before原则,我们可以明确的知道操作的前后顺序。
Happens-before原则包括以下几种规则:
-
程序次序规则(Program order rule):一个线程中的每个操作,happens-before于该线程中的任意后续操作。
-
监视器锁规则(Monitor lock rule):对一个锁的解锁,happens-before于随后对这个锁的加锁。
-
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volitile域的读。
-
传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
-
start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作 happens-before于线程B中的任意操作。
-
join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
例如,假设有两个线程A和B,线程A写入一个volatile变量,然后线程B读取这个变量。那么线程A的写入操作happens-before线程B的读取操作,线程B可以看到线程A的写入。
再例如,假设一个线程先解锁一个对象,然后另一个线程锁定这个对象。那么第一个线程的解锁操作happens-before第二个线程的加锁操作,第二个线程可以看到第一个线程解锁前的所有操作。
总结
as-if-serial语义
这个程序中,尽管编译器或处理器可能会重排序代码,但是从程序的行为上看,它就如同按照源代码的顺序串行执行的,这就是as-if-serial语义。
public class AsIfSerial {
private static int x = 0;
private static int y = 0;
public static void main(String[] args) {
x = 1;
y = 2;
int a = x;
int b = y;
System.out.println("a = " + a + ", b = " + b);
}
}
happens-before的例子
在这个程序中,线程A的flag = true操作happens-before线程B的if(flag)操作,因为它们之间有volatile变量规则和join规则。所以,线程B可以看到线程A将flag设置为true。
public class HappensBefore {
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
flag = true;
});
Thread threadB = new Thread(() -> {
if (flag) {
System.out.println("ThreadB sees flag = true");
}
});
threadA.start();
threadA.join();
threadB.start();
threadB.join();
}
}
as-if-serial是一种程序优化原则,它允许编译器和处理器对程序进行各种优化,包括重新排序指令等,但优化后的程序必须与按照程序源代码顺序执行的结果一致。这样可以保证程序的正确性,又可以提高程序的运行效率。
happens-before则是一种描述多线程程序中两个或多个操作之间可能存在的偏序关系。如果操作A happens-before操作B,那么A的结果对B是可见的,即B可以看到A的效果。happens-before关系可以保证多线程程序的正确同步。