深入理解Java内存模型(Final)

与前面介绍的锁和volatile相比较,对final域的读和写更像是普通的变量访问。对于final域,编译器和处理器要遵守两个重排序规则。

  • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

下面,我们通过一些示例性的代码来分别说明这两个规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FinalExample {
int i; //普通变量
final int j; //final变量
static FinalExample obj;

public void FinalExample () { //构造函数
i = 1; //写普通域
j = 2; //写final域
}

public static void writer () { //写线程A执行
obj = new FinalExample ();
}

public static void reader () { //读线程B执行
FinalExample object = obj; //读对象引用
int a = object.i; //读普通域
int b = object.j; //读final域
}
}

这里假设一个线程A执行writer(),随后另一个线程B执行reader()。下面我们通过这两个线程的交互来说明这两个规则。

深入理解Java内存模型(锁)

锁的释放-获取建立的Happens-Before关系

锁是Java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。
下面是锁释放-获取的示例代码。

1
2
3
4
5
6
7
8
9
10
11
12
class MonitorExample {
int a = 0;

public synchronized void writer() { //1
a++; //2
} //3

public synchronized void reader() { //4
int i = a; //5
……
} //6
}

假设线程A执行writer(),随后线程B执行reader()
根据happens-before规则,这个过程包含的happens-before关系可以分为两类。

  1. 根据程序次序规则,1 happens-before 22 happens-before 34 happens-before 55 happens-before 6
  2. 根据监视器锁规则,3 happens-before 4
  3. 根据happens-before的传递性,2 happens-before 5

上述happens-before关系的图形化表现形式如下。

在上图中,每一个箭头链接的两个节点,代表了一个happens-before关系。黑色箭头表示程序顺序规则;橙色箭头表示监视器锁规则;蓝色箭头表示组合这些规则后提供的happens-before保证。
上图表示在线程A释放了锁之后,随后线程B获取同一个锁。在上图中,2 happens-before 5。因此,线程A在释放锁之前所有可见的共享变量,在线程B获取同一个锁之后,将立刻变得对B线程可见。

深入理解Java内存模型(重排序)

数据的依赖性

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分下列三种类型。

上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。
前面提到过,编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
注意,这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

深入理解Java内存模型(顺序一致性)

数据竞争与顺序一致性的保证

当程序未正确同步时,就会存在数据竞争。Java内存模型规范对数据竞争的定义如下:

  1. 在一个线程中写一个变量,
  2. 在另一个线程读同一个变量,
  3. 而且写和读没有通过同步来排序。

当代码中包含数据竞争时,程序的执行往往产生违反直觉的结果。如果一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序。
JMM对正确同步的多线程程序的内存一致性做了如下保证:
如果程序是正确同步的,程序的执行将具有顺序一致性(sequentially consistent)–即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。这里的同步是指广义上的同步,包括对常用同步原语(lockvolatilefinal)的正确使用。

深入理解Java内存模型(Volatile-Happens before)

volatile特性

当我们声明共享变量为volatile后,对这个变量的读/写将会很特别。
理解volatile特性的一个好方法,把对volatile变量的单个读/写,看成是使用同一个监视器锁对这些单个读/写操作做了同步。下面我们通过具体的示例来说明,请看下面的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class VolatileFeaturesExample {
volatile long vl = 0L; //使用volatile声明64位的long型变量

public void set(long l) {
vl = l; //单个volatile变量的写
}

public void getAndIncrement () {
vl++; //复合(多个)volatile变量的读/写
}


public long get() {
return vl; //单个volatile变量的读
}
}

假设有多个线程分别调用上面程序的三个方法,这个程序在语意上和下面程序等价:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class VolatileFeaturesExample {
long vl = 0L; // 64位的long型普通变量

public synchronized void set(long l) { //对单个的普通 变量的写用同一个监视器同步
vl = l;
}

public void getAndIncrement () { //普通方法调用
long temp = get(); //调用已同步的读方法
temp += 1L; //普通写操作
set(temp); //调用已同步的写方法
}

public synchronized long get() {
//对单个的普通变量的读用同一个监视器同步
return vl;
}
}

如上面示例程序所示,对一个volatile变量的单个读/写操作,与对一个普通变量的读/写操作使用同一个监视器锁来同步,它们之间的执行效果相同。
监视器锁的happens-before规则保证释放监视器和获取监视器的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
监视器锁的语义决定了临界区代码的执行具有原子性。这意味着即使是64位的long型和double型变量,只要它是volatile变量,对该变量的读写就将具有原子性。如果是多个volatile操作或类似于volatile++这种复合操作,这些操作整体上不具有原子性。
简而言之,volatile变量自身具有下列特性:

  • 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性。对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

volatile的简单变量如果当前值由该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作。

1
2
n=n+1 ;
n++;

只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原子级别的。

深入理解Java内存模型(基础)

并发编程模型的分类

在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步。通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信。
同步是指程序用于控制不同线程之间操作发生相对顺序的机制。在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
Java的并发采用的是共享内存模型(主内存共享),Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×