持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第23天,点击查看活动详情
Java这种托管型语言如何自动进行内存管理和回收整理?
Java中的volatile有什么用?如何正确地使用它?Java并发库大量使用volatile变量,在各种不同的体系结构下产生了很多典型的问题。
volatile语义由Java内存模型定义,先从Java内存模型聊起。
1 Java内存模型
在不同架构,缓存一致性问题不同,如x86采用TSO模型,写后写(StoreStore)和读后读(LoadLoad)完全无需程序员操心,但Arm弱内存模型要求我们在合适位置添加StoreStore barrier和LoadLoad barrier。
1.1 案例
public class MemModel {
static int x = 0;
static int y = 0;
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
x = 1;
y = 1;
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (y == 0);
if (x != 1) {
System.out.println("Error!");
}
}
});
t2.start();
t1.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在x86运行没问题,但在Arm机器就有概率打印Error:
- 第一个线程t1对变量x、y的写操作顺序不能保证顺序
- 第二个线程t2读取x、y时候也不保证顺序
为解决该问题,Java语言规范规定,即JSR 133文档规定Java内存模型,描述java编程语言在支持多线程编程中,对共享内存访问的顺序。显然,案例中的线程间变量共享的情况,就可借此解决。
JSR133文档中,这内存模型有个专门名字 - Happens-before,规定一些同步动作的先后顺序。这规范也不是一蹴而就,它也是经过几次讨论才定稿。所以,早期JVM仍存在一些弱内存相关问题,很难称其为bug,因为标准里的规定就有问题,虚拟机实现只是遵从标准。
2 Happens-before模型
Java内存模型(Java Memory Model, JMM)通过各种操作来定义的,包括对变量的读写操作、加锁和释放锁及线程的启动和等待操作。JMM为程序中的动作定义一种先后关系 - Happens-Before关系。
要想保证操作B可看到操作A的结果,A、B须满足Happens-Before,这结论与A、B是否在同一线程中执行无关。
HB模型讨论的都是同步动作,包括加锁、解锁、读写volatile变量、线程启动和线程完成等。下面操作都指同步动作:
第三条规则:写操作如果在程序里出现在读操作之前,那就不能乱序。这就是写后读屏障的规则
HB模型强调同步动作的先后关系,而对非同步动作,无任何限制。案例一的读写操作都是非同步动作,所以它在不同体系结构运行,得到不同结果。但这并不违反JMM规定。
JMM是一种理论内存模型,并非真实存在。它以具体CPU内存模型为基础的。看JSR 133的两个例子,你就理解了。
2.1 控制流依赖
包括两个线程且变量x、y初值都是0。第一个线程的代码:
// Thread1
r1 = x;
if (r1 != 0)
y = 1;
第二个线程的代码:
// Thread 2
r2 = y;
if (r2 != 0)
x = 1;
由于存在控制流依赖,这两段代码的第4行都不能提前到第2行前执行。即至今,所有主流CPU,上面两段代码都会按代码顺序执行。最终运行结果一定是r1=r2=0。
但HB是种理论模型,若线程1中,y=1先于r1=x执行,同时线程2中,x=1先于r2=y执行,最后结果,存在r1=r2=1的可能。理论上确实可能存在一种CPU,当它进行分支预测投机执行时,投机结果被其他CPU观察到。当然,实际绝对不可能出现这样CPU,这意味着厂家花费更多精力为软件开发者带来巨大麻烦,且由于核间同步通讯要求,CPU性能还会下降。
2.2 数据流依赖
假设x=y=初值0,而r1=r2=初值42。
线程1的代码:
r1 = x;
y = r1;
线程2的代码:
r2 = y;
x = r2;
因为每个线程内部的第2行和第1行之间都存在数据依赖,所以这里无法产生乱序执行,所以无论怎样顺序对这两个线程调度,都不可能r1=r2=42。
但是r1=r2=42在HB模型却是合理的,因为它没有对数据流依赖规定。
即普通的变量读写在JMM是允许乱序的,若真有人造出这愚蠢CPU,运行出这种结果却符合HB的规定的。
但这两个问题在现实并不存在。这两个案例,是因为JSR 133文档大量在介绍本不应该存在的两个问题,导致文档晦涩难懂。
理解JMM时,一定要结合具体CPU体系结构。JMM加上每一种体系结构都有的控制流依赖和数据流依赖,才是一种比较实用的内存模型。纯粹的JMM本身的实用性并不强。
JMM是一种标准规定,它并不管实现者如何实现它。具体到Java语言虚拟机的实现,当Java并发库的核心开发者Doug Lea将JMM简化之后,就变得更容易理解一些。