您的位置:首页 > 编程语言 > Java开发

刨根问底Java多线程系列:线程不安全的最根本的原因是什么

2017-11-23 16:53 501 查看


一、引言

在多线程环境中,线程安全毫无疑问是最主要面对的问题。
找到线程不安全的根源,就好像找到了一把万能钥匙,解开程序中的任何线程不安全隐患。
1
2


二、分析

对于线程安全的定义,《深入理解Java虚拟机:JVM高级特性与最佳实践》(P343)认为《Java Concurrency In Practice》的作者Brian Goetz对“线程安全”的定义比较恰当:

“当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。”

我也认为这个定义是很准确的,不过有个问题:现在我们知道了什么是线程安全,但线程安全的根源在哪呢。换句话说,我们知道了“是什么”,但不知道“为什么”。

在《深入理解Java虚拟机:JVM高级特性与最佳实践》(12.3.1节“主内存与工作内存”,P319)中,给出了一个原因【原因1】:

“每条线程有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对该变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。”

线程、主内存、工作内存三者的交换关系图【图1】: 



在《Java多线程编程核心技术》(3.1.10节“等待wait的条件发生变化”,P155)的例子给出了另外一个原因【原因2】:(只看核心代码)
...
public void subtract(){
try{
synchronized(lock){
if(ValueObject.list.size() == 0){
lock.wait();
}
ValueObject.list.remove(0);
}
}catch(InterruptedException e){
e.printStackTrace();
}
...
1
2
3
4
5
6
7
8
9
10
11
12
13

该例中,即使对ValueObject的操作加锁,仍然会出现问题。问题就出在:
if(ValueObject.list.size() == 0){
lock.wait();
}
1
2
3

如果只有两个线程A、B,都因为 ValueObject.list.size() == 0 而wait,然后线程C对ValueObject.list增加了1个元素并通知所有呈wait等待状态的线程(在此就是线程A、B),假设线程A比线程B先启动拿到锁,线程A可以正确删除list中索引为0的数据,但线程B启动后则出现索引溢出的异常,因为list中仅仅添加了一个数据,也只能删除一个数据。

好了,我们有线程安全的两个原因了,请问:
它们两的共同点是什么?
1
2

结论就是: 
线程内的关于外部变量的语境,与真实外部语境不一致。

我们来解释下这句话。你看在【原因1】中各个线程都有自己的工作内存( 线程内的关于外部变量的语境),主内存就是 真实外部语境。线程安全是不是由于他们不一致产生的?再看看【原因2】,线程B被唤醒后能执行操作
ValueObject.list.remove(0); 的先决条件 if(ValueObject.list.size() == 0) 就是 线程内的关于外部变量的语境,而出现异常的原因就在于该此时先决条件已经发生变化( 与真实外部语境不一致,list.size为0)。是不是觉得有点道理呢?


三、有关方法论

不过可能有同学会说,这不很显而易见吗?我只能说,在你被线程安全问题逼到绝路时不妨拿出来看看这个结论,问问自己确实做到了吗。还有同学会说,不能因为几个例子就能下此结论,这个用数学上来说也就是个必要不充分条件。话说的一点都没错,但是我在学习过程中往往会碰到这样的情况:在很难掌握到所有论据的情况下,却要抓住已有问题的核心共同点。抓不住核心共同点,就会感觉学习的被动和茫然。怎么办呢,我的方法就是先假设,然后去反证,直到有例子能推翻假设。这在物理学里面也是司空见惯的,许多的理论往往会起步于假设。更何况即使你的假设是错的,带着目标去学习也好过盲目。


四、进一步验证

你看我下面就来验证我的假设,在《深入理解Java虚拟机:JVM高级特性与最佳实践》(12.3.3节“对volatile型变量的特殊规则”,P323)这个例子里,虽然变量race是volatile型,但是仍然出现了错误。原因就出在 race++ 上。 race++ 是要将race的值先放入该线程的操作栈顶(线程内的关于外部变量的语境),然后再进行运算。在此race就是真实外部语境,两者出现不一致了。你看我说的结论又一次胜利了。关于volatile型变量我会在后面的文章中进一步进行讨论。


五、结论

最后再次总结线程不安全的根源:


线程内的关于外部变量的语境,与真实外部语境不一致。


参考文献

《深入理解Java虚拟机:JVM高级特性与最佳实践》 

《Java多线程编程核心技术》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  线程安全 内存