第一张图从JVM角度抽象,每个线程都有一个LocalMemory,用与存储读\写变量的副本,它抽象涵盖了cpu cache memory、cpu Registers。
JMM是个抽象的概念,通过控制线程与主存之间的通信(同步),来保证程序执行的可见性。
编译器、cpu通过将指令重新排序,然后执行,以提高执行效率,
编译器重排序 (javac)
cpu重排序 在不存在数同步问题的情况下,机器指令进行重排序
从重排序看,JMM通过控制特定类型的编译器或cpu重排序,来保证程序执行的可见性。
后一个操作的执行,依赖于前一个操作的执行结果,则这两个操作有数据依赖性。
读-写、写-写、写-读 这三组操作分别都有数据依赖性:
a读-写:b=a;a=1;
a写-写:a=1;a=2;
a写-读: a=1;b=a;
对有数据依赖性的操作进行重排序,会导致结果违背预期。
对于有数据依赖性的操作(happens-before),JMM禁止编译器、cpu重排序的发生。这里的两个操作仅仅指单线程中的两个操作,对于多线程的数据依赖性不被考虑(其实通过锁机制等手动来控制)。
相比于单线程中的as-if-serial,happens-before更上一层楼,囊括了单线程、多线程,从更抽象更广义的角度,定义规则,禁止编译器、cpu在某些情况下的重排序,保证内存可见性,程序的正确执行。
JSR-133中提出了happens-before用来保证操作之间的可见性(JDK1.5+).
如果一个操作A的结果必须 对另一个操作B的执行 可见,那么 A happens before B. 注:A、B操作可以是一个线程的两个操作(详见上文数据依赖性),也可以分别在两个独立的线程中(这种情况我们通常使用锁机制).
JSR-133对happens-before规则的定义:
程序顺序规则:一个线程中的任何操作,happens-before这个操作之后的操作(看起来是废话,就是as-if-serial嘛,其实就是给程序员承诺,“你可以认为你的代码是顺序执行的”,同时我们看到 as-if-serial是happens-before的一部分)
monitor锁规则:锁的释放happens-before锁的获取
volatile变量规则:对于一个volatile变量的写,happens-before这个变量的读(换句话说,最后一次volatile变量的写,总是对之后一个volatile变量读操作内存可见)
传递性:操作A happens-before 操作B, 操作B happens-before 操作C,则
操作A happens-before 操作C
Thread.start()规则,一个线程的start()操作,happens-before, 这个线程中的任意操作.(也就是说,先start了线程,才有线程中的操作)
Thread.join()规则: 线程中的所有操作 happens-before 这个线程join的执行
JMM对两种不同类型的重排序,不同对待:
单线程或正确同步的多线程中,对于会改变执行结果的重排序,JMM禁止(编译器、cpu),遵循happens-before。
单线程或正确同步的多线程中,对于不会改变执行结果的重排序,JMM允许
JSR-133中,对happens-before关系的定义:
定义1.如果操作A happens-before 操作B,那么操作A的执行结果对操作B可见,而且操作A,操作B按先后顺序执行
定义2. 两个操作(以A,B为例),操作A操作B之间有happens-before关系,并不保证JVM会按照happens-before指定的关系执行(先A后B),如果重排序后未改变执行的结果,这种重排序并不非法!
定义1是JMM对程序员编程的指导承诺;定义2是JMM对编译器,cpu重排序的约束原则.对于程序员,不关心底层重排序细节,只要按照定义1编程即可保证程序正确性.
public class VolatileSemantic {
long l = 0;// 64位
// long\double变量,如果非volatile,无法保证写的原子性(JDK1.5+,定义了任意读操作必须具有原子性)
volatile long lAtomic = 0;
// 与l不同,lAtoci的读写语义发生了变化,实际语义如下:
// public synchronized long get() {
// //volatile变量-读 内存语义:JMM会把该线程对于这个volatile变量的本地存储置为无效,同时从主存中获取。
// }
//
// public synchronized void set(long lAtomic) {
// //volatile变量-写 内存语义:写入本地存储并同时刷入主存
// }
}
volatile变量-读 内存语义:对于这个volatile变量的本地存储已经被置为无效, 从主存中获取。
volatile变量-写 内存语义:写入本地存储并同时刷入主存
关于volatile详见
http://blog.csdn.net/lemon89/article/details/50734562
happens保证了volatile的任何写操作的结果对总是对于之后的读操作可见.volatile变量的写-读与锁的释放-获取有相同的效果。
总之,volatile变量,在编译后,通过在volaitle变量操作前后插入内存屏障,禁止cpu的重排序,到达内存可见性,保证了程序的正确执行。
锁的内存语义(遵守happens-before),
锁释放:把该线程的对应的本地存储(在临界区中使用到的数据)刷入主存;
锁获取:把该线程 在临界区中使用到的数据 对应的本地存储置为无效,并从主存中获取;
final数据的初始化(在构造函数中赋值的),为了保证正确初始化之后对其他线程可见,在 final数据初始化后 与构造函数结束前加入内存凭证,禁止重排序所导致的final数据初始在构造器之外。
比如:
class FinalFieldExample {
final int x;
int y;
public FinalFieldExample() {
x = 3;
y = 4;
}
实际执行(y=4因为不是final类型,可能重排序)
class FinalFieldExample {
final int x;
int y;
public FinalFieldExample() {
x = 3;
}
.....
y = 4;//被重排序
2.读取final数据时,保证这个数据已经被正确初始化.
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
static void reader() {
f = new FinalFieldExample();
int i = f.x;
int j = f.y;
}
重排序后可能出现:
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
int j = f.y;//被重排序
static void reader() {
f = new FinalFieldExample();
int i = f.x;
}
关于final更深入内容请看
https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.5
由于对象的初始化会有重排序,比如,Resource r=new Resource,会有这三个动作:Step1.内存分配;Step2.对象初始化;Step3.对象指向引用;Step2、Step3可能会重排序为:Step1.内存分配;Step3.对象指向引用;Step2.对象初始化;
在单线程中,并不影响执行结果(as-if-serial),所以没问题!但是多线程中会有问题,具体代码分析:
关于类的初始化请查阅
http://blog.csdn.net/lemon89/article/details/47363127
//线程不安全.
public class SingletonUnSafe {
private static Resource resource;
public static Resource getResourceSingleton() {
if (null == resource) {
// 单线程时,创建对象:Step1.内存分配;Step2.对象初始化;Step3.对象指向引用;
// Step2,Step3步
// 可能会重排序为Step1->Step3->Step2,单线程时并无问题,
// 但是多线程时,可能导致第二个线程读resource时,第一个线程在做Step3,Step2还没进行(也就是对象未初始化),导致同时多个线程初始化对象。
resource = new Resource();
}
return resource;
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
public void run() {
System.out.println(SingletonUnSafe.getResourceSingleton());
}
}).start();
}
}
}
// output:并不是只创建一个单例的对象,如下看到多个对象产生
/**
* Resource@5f186fab
* Resource@41d5550d
* Resource@41d5550d
* Resource@24c21495
* Resource@24c21495
* Resource@3d4b7453
* Resource@41d5550d
* Resource@41d5550d
*/
那怎么样实现线程安全,保证同步呢?1.禁止重排序;2.重排序线程与其他线程互斥(也就是保证对象初始化的原子性),具体代码:
方式一:
//1 class加载同步性保证线程安全,也就是说,当resource = new Resource()时(首次调用getResourceSingleton),有且只有一个线程进入
//2 没有做到延迟加载,一旦SingletonSafe初始化,则resource被创建
public class SingletonSafe {
private static Resource resource = new Resource();
public static Resource getResourceSingleton() {
// 程序顺序执行,先完成这个类的初始化(也就是Class的加载,包括了完成static
// 变量的初始化),然后调用方法.所以这个判空是多余的.
// if (null == resource) {
// resource = new Resource();
// }
return resource;
}
}
方式二:
// 1 class加载同步性保证线程安全,也就是说,当resource = new
// Resource()时(首次调用getResourceSingleton),有且只有一个线程进入
// 2 做到延迟加载,一旦SingletonSafeWithLazy初始化, resource并未被初始化, 直到调用getResourceSingleton
public class SingletonSafeWithLazy {
private static class ResourceHolder {
private static Resource resource = new Resource();
}
public static Resource getResourceSingleton() {
return ResourceHolder.resource;
}
}
在第一种非线程安全的实现中,如果我们给变量加上volatile修饰呢??
同样可以看到创建多个对象,具体原因见代码注释部分。
//线程不安全.
public class SingletonUnSafe {
private volatile static Resource resource;
public static Resource getResourceSingleton() {
if (null == resource) { // 每个线程读取volatile变量均是互斥的.但是,举例来说,时间点0
// ThreadA 执行到if判空并返回true,进入.
// 时间点1,ThreadB执行到if判空并返回true,进入.
// 时间点3,ThreadA、ThreadB都还未执行resource = new
// Resource(),代码,但是可以看到这种方式会创建多个对象
resource = new Resource();
}
return resource;
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
public void run() {
System.out.println(SingletonUnSafe.getResourceSingleton());
}
}).start();
}
}
}