Java内存模型JMM(Java Memory Model)。
Java被设计为跨平台的语言,在内存管理上,显然也要有一个统一的模型--JMM。
JMM主要是为了规定了线程和内存之间的一些关系。对Java程序员来说只需负责用synchronized同步关键字,其它诸如与线程/内存之间进行数 据交换/同步等繁琐工作均由虚拟机负责完成。
如图1所示:根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。
工作内存里的变量,在多核处理器下, 将大部分储存于处理器高速缓存中, 高速缓存在不经过内存时,也是不可见的
Java语言的内存模型由一些规则组成,这些规则确定对内存的访问如何排序,以及何时可以确保它们对线程是可见的。下面我们将分别介绍 Java 内存模型的重排序,内存可见性和 happens-before 关系。虽然JMM设计上方便了程序员,但是它增加了虚拟机的复杂程度,而且还导致某些编程技巧在Java语言中失效。
内存模型描述了程序的可能行为。具体的编译器实现可以产生任意它喜欢的代码-- 只要所有执行这些代码产生的结果,能够和内存模型预测的结果保持一致。这为编译器实现者提供了很大的自由,包括操作的重排序。
编译器生成指令的次序,可以不同于源代码所暗示的“显然”版本。重排序后的指令,对于优化执行以及成熟的全局寄存器分配算法的使用,都是大有脾益的,它使得程序在计算性能上有了很大的提升。
重排序类型包括:
- 编译器生成指令的次序,可以不同于源代码所暗示的“显然”版本。
- 处理器可以乱序或者并行的执行指令。
- 缓存会改变写入提交到主内存的变量的次序。
由于现代可共享内存的多处理器架构可能导致一个线程无法马上(甚至永远)看到另一个线程操作产生的结果。所以Java 内存模型规定了JVM 的一种最小保证:什么时候写入一个变量对其他线程可见。在JMM中, 通过并发线程修改变量值, 必须将线程变量同步回主存后, 其他线程才能访问到。
在现代可共享内存的多处理器体系结构中每个处理器都有自己的缓存,并周期性的与主内存协调一致。假设线程A 写入一个变量值V,随后另一个线程B 读取变量 V 的值,在下列情况下,线程 B 读取的值可能不是线程 A 写入的最新值:
- 执行线程 A 的处理器把变量 V 缓存到寄存器中。
- 执行线程 A 的处理器把变量 V 缓存到自己的缓存中,但还没有同步刷新到主内存中去。
- 执行线程 B 的处理器的缓存中有变量 V 的旧值。
happens-before 关系保证:如果线程 A 与线程 B 满足 happens-before 关系,则线程 A 执行动作的结果对于线程 B 是可见的。如果两个操作未按 happens-before 排序,JVM 将可以对他们任意重排序。
下面介绍几个与理解 ConcurrentHashMap 有关的 happens-before 关系法则:
- 程序次序法则:如果在程序中,所有动作 A 出现在动作 B 之前,则线程中的每动作 A 都 happens-before 于该线程中的每一个动作 B。
- 监视器锁法则:对一个监视器的解锁 happens-before 于每个后续对同一监视器的加锁。
- Volatile 变量法则:对 Volatile 域的写入操作 happens-before 于每个后续对同一 Volatile 的读操作。
- 传递性:如果 A happens-before 于 B,且 B happens-before C,则 A happens-before C。
一般情况下的示例程序:
x =0;
y =0;
i =0;
j =0;
// threadA
y =1;
x =1;
// threadB
i =x;
j = y;
在如上程序中, 如果线程A,B在无保障情况下运行, 那么i,j各会是什么值呢?
答案是, 不确定. (00,01,10,11都有可能出现),这里没有使用Java同步机制, 所以Java内存模型有序性和可见性都无法得到保障. happens-before ordering( 先行发生排序) 如何避免这种情况? 排序原则已经做到:
happens-before ordering( 先行发生排序) 如何避免这种情况? happens-before ordering原则保证:
a, 在程序顺序中, 线程中的每一个操作, 发生在当前操作后面将要出现的每一个操作之前。
b, 对象监视器的解锁发生在等待获取对象锁的线程之前。
c, 对volitile关键字修饰的变量写入操作, 发生在对该变量的读取之前。
d, 对一个线程的 Thread.start() 调用发生在启动的线程中的所有操作之前。
e, 线程中的所有操作 发生在从这个线程的 Thread.join()成功返回的所有其他线程之前。
为了实现 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)
因此,对于示例中,如果加上synchronized关键字,如果A先执行,B随后执行的情况下,执行情况如下图解释:
下面的图显示了Java内存模型
图1.java内存模型图
线程若要对某变量进行操作,必须经过一系列步骤:首先从主存复制/刷新数据到工作内存,然后执行代码,进行引用/赋值操作,最后把变量内容写回Main Memory。Java语言规范(JLS)中对线程和主存互操作定义了6个行为,分别为load,save,read,write,assign和 use,这些操作行为具有原子性,且相互依赖,有明确的调用先后顺序。
我们都知道同步时用到synchronized关键字,现在,从JMM的角度来重新审视synchronized关键字。
假设某条线程执行一个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保护的代码段,很有可能某条线程已经改动了变量 的值,但是其他线程却无法看到这个改动,依然在旧的变量值上进行运算,最终导致不可预料的运算结果。
经典j2ee设计模式Double-Checked Locking失效问题
双重检查锁定失效问题,一直是JMM无法避免的缺陷之一.了解DCL失效问题, 可以帮助我们深入JMM运行原理.
要展示DCL失效问题, 首先要理解一个重要概念- 延迟加载(lazy loading).
非单例的单线程延迟加载示例:
class Foo
{
privateResource res = null;
publicResource getResource()
{
// 普通的延迟加载
if (res== null)
res = new Resource();
returnres;
}
}
非单例的多线程延迟加载示例:
Class Foo
{
PrivateResource res = null;
Publicsynchronized Resource getResource()
{
// 获取实例操作使用同步方式, 性能不高
If (res== null) res = new Resource();
returnres;
}
}
非单例的DCL多线程延迟加载示例:
Class Foo
{
PrivateResource res = null;
PublicResource getResource()
{
If (res== null)
{
//只有在第一次初始化时,才使用同步方式.
synchronized(this)
{
if(res ==null)
{
res = newResource();
}
}
}
returnres;
}
}
Double-CheckedLocking看起来是非常完美的。但是很遗憾,根据Java的语言规范,上面的代码是不可靠的。
出现上述问题, 最重要的2个原因如下:
1, 编译器优化了程序指令, 以加快cpu处理速度.
2, 多核cpu动态调整指令顺序, 以加快并行运算能力.
问题出现的顺序:
1, 线程A, 发现对象未实例化, 准备开始实例化
2, 由于编译器优化了程序指令, 允许对象在构造函数未调用完前, 将共享变量的引用指向部分构造的对象, 虽然对象未完全实例化, 但已经不为null了.
3, 线程B, 发现部分构造的对象已不是null, 则直接返回了该对象.
不过, 一些著名的开源框架, 包括jive,lenya等也都在使用DCL模式, 且未见一些极端异常.
说明, DCL失效问题的出现率还是比较低的.
接下来就是性能与稳定之间的选择了?
DCL的替代Initialize-On-Demand:
publicclass Foo {
// 似有静态内部类, 只有当有引用时, 该类才会被装载
private static class LazyFoo {
public static Foo foo = new Foo();
}
public static Foo getInstance() {
return LazyFoo.foo;
}
}