1.概述
在多核CPU中,处理器拥有1至多个内存缓存以提高访问速度,降低对共享内存总线的使用,这样就带来了一些新的问题。
对处理器层来说,
内存模型描述了其它处理器写操作对于当前处理器是否可见,当前处理器写对于其它处理器是否可见的充要条件。
一些处理器展示了强壮的内存模型,所有处理器都访问内存中同一个值。另一些处理器则展示了比较弱的内存模型,使用一些特殊的指令(lock或
unlock)时会引发数据的不一致性,需要刷新或使局部处理器缓存失效,这样才能看见其它处理器的写数据。lock或unlock等指令对于程序员是不
可见的。
在强壮的内存模型上编程很容易,但是处理器设计越来越倾向于弱内存模型,弱的内存一致性对于多CPU,具有更好的可扩展性和更大的内存。
像 C 和 C++ 这些语言就没有显示的内存模型 , 只是采用了处理器的内存模型,这意味着并发的 C 语言程序可以在一个处理器上,而不能在另一个处理器正确地运行。
2.Java内存模型
JVM系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。
每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。
根据JMM的设计,系统存在一个主内存(Main
Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working
Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主
存完成。
线程若要对某变量进行操作,必须经过一系列步骤:首先从主存复制/刷新数据到工作内存,然后执行代码,进行引用/赋值操作,最后把变量内容写回Main
Memory。Java语言规范(JLS)中对线程和主存互操作定义了6个行为,分别为load,save,read,write,assign和
use,这些操作行为具有原子性,且相互依赖,有明确的调用先后顺序。
3.一致性模型
JVM使用happens-before ordering(先行发生排序)保持数据一致性。
假设某条线程执行一个synchronized代码段,其间对某变量进行操作,JVM会依次执行如下动作:
(1) 获取同步对象monitor (lock)
(2) 从主存复制变量到当前工作内存 (read and load)
(3) 执行代码,改变共享变量值 (use and assign)
(4) 用工作内存数据刷新主存相关内容 (store and write)
(5) 释放同步对象锁 (unlock)
可见,synchronized的另外一个作用是保证主存内容和线程的工作内存中的数据的一致性。如果没有使用synchronized关键字,JVM不
保证第2步和第4步会严格按照上述次序立即执行。因为根据JLS中的规定,线程的工作内存和主存之间的数据交换是松耦合的,什么时候需要刷新工作内存或者
更新主内存内容,可以由具体的虚拟机实现自行决定。如果多个线程同时执行一段未经synchronized保护的代码段,很有可能某条线程已经改动了变量
的值,但是其他线程却无法看到这个改动,依然在旧的变量值上进行运算,最终导致不可预料的运算结果。
为了实现happends-before ordering原则, java及jdk提供的工具:
a, synchronized关键字
b, volatile关键字
c, final变量
d, java.util.concurrent.locks包(since jdk 1.5)
e, java.util.concurrent.atmoic包(since jdk 1.5)
4.Double-Checked Locking失效问题 双重检查锁定失效问题,一直是JMM无法避免的缺陷之一.了解DCL失效问题, 可以帮助我们深入JMM运行原理。要展示DCL失效问题, 首先要理解一个重要概念- 延迟加载(lazy loading)。
非单例的单线程延迟加载示例:
- class Foo
-
{
-
private Resource res = null;
-
public Resource getResource(){
-
// 普通的延迟加载
-
if (res == null)
-
res = new Resource();
-
return res;
-
}
-
}
在单线程环境下,一切都相安无事,但如果把上面的代码放到多线程环境下运行,那么就可能会出现问题。假设有2条线程,同时执行到了if(res == null),那么很有可能res被初始化2次,为了避免这样的竞争条件,用synchronized关键字把上面的方法同步起来。代码如下:
非单例的多线程延迟加载示例:
- Class Foo
-
{
-
Private Resource res = null;
-
Public synchronized Resource getResource(){
-
// 获取实例操作使用同步方式, 性能不高,synchronized过的方法在速度上要比未同步的方法慢上100倍
-
If (res == null)
- res = new Resource();
-
return res;
-
}
-
非单例的DCL多线程延迟加载示例:
- Class Foo{
-
Private Resource res = null;
-
Public Resource getResource(){
-
If (res == null){
-
//只有在第一次初始化时,才使用同步方式.
-
synchronized(this){
-
if(res == null){
-
res = new Resource();
- }
-
}
- }
-
return res;
-
}
-
}
Double-Checked Locking看起来是非常完美的。但是很遗憾,根据Java的语言规范,上面的代码是不可靠的。出现上述问题, 最重要的2个原因如下:
1, 编译器优化了程序指令, 以加快cpu处理速度.
2, 多核cpu动态调整指令顺序, 以加快并行运算能力.
问题出现的顺序:
1, 线程A, 发现对象未实例化, 准备开始实例化
2, 由于编译器优化了程序指令,
允许对象在构造函数未调用完前, 将共享变量的引用指向部分构造的对象, 虽然对象未完全实例化, 但已经不为null了.
3, 线程B, 发现部分构造的对象已不是null, 则直接返回了该对象.
不过, 一些著名的开源框架, 包括jive,lenya等也都在使用DCL模式, 且未见一些极端异常。说明, DCL失效问题的出现率还是比较低的。
有很多人不死心,试图想出了很多精妙的办法来解决这个问题,但最终都失败了。事实上,无论是目前的JMM还是已经作为JSR提交的JMM模型的增强,DCL都不能正常使用。在William Pugh的论文《Fixing the java Memory Model》中详细的探讨了JMM的一些硬伤,更尝试给出一个新的内存模型,有兴趣深入研究的读者可以参见文后的参考资料。
如果你设计的对象在程序中只有一个实例,即singleton的,有一种可行的解决办法来实现其LazyLoad:就是利用类加载器的LazyLoad特性。代码如下:
解决方案1:Initialize-On-Demand代替DCL方式- public class Foo {
-
// 似有静态内部类, 只有当有引用时, 该类才会被装载
-
private static class LazyFoo {
-
public static Foo foo = new Foo();
-
}
-
-
public static Foo getInstance() {
-
return LazyFoo.foo;
-
}
-
}
解决方案2:使用ThreadLocal实现DCL 由于ThreadLocal的实现效率比较低,所以这种解决办法会有较大的性能损失
参考文献1.内存模型.http://blog.csdn.net/FutureInHands/archive/2007/09/30/1808118.aspx
2.java内存模型.
阅读(764) | 评论(0) | 转发(0) |