java并发编程——java内存模型/happens-before

文章目录

    • java内存模型图
    • 重排序
    • happens-before
    • volatile 的happens-before
    • 锁的happens-before
    • final关键字的happens-before
    • happens-before之单例的实现

java内存模型图

java并发编程——java内存模型/happens-before_第1张图片

java并发编程——java内存模型/happens-before_第2张图片

第一张图从JVM角度抽象,每个线程都有一个LocalMemory,用与存储读\写变量的副本,它抽象涵盖了cpu cache memory、cpu Registers。

JMM是个抽象的概念,通过控制线程与主存之间的通信(同步),来保证程序执行的可见性。


重排序

编译器、cpu通过将指令重新排序,然后执行,以提高执行效率,

  • 编译器重排序 (javac)

  • cpu重排序 在不存在数同步问题的情况下,机器指令进行重排序

java并发编程——java内存模型/happens-before_第3张图片

从重排序看,JMM通过控制特定类型的编译器或cpu重排序,来保证程序执行的可见性。


  • 数据依赖性(单线程)

后一个操作的执行,依赖于前一个操作的执行结果,则这两个操作有数据依赖性。
读-写、写-写、写-读 这三组操作分别都有数据依赖性:
a读-写:b=a;a=1;
a写-写:a=1;a=2;
a写-读: a=1;b=a;

对有数据依赖性的操作进行重排序,会导致结果违背预期。

对于有数据依赖性的操作(happens-before),JMM禁止编译器、cpu重排序的发生。这里的两个操作仅仅指单线程中的两个操作,对于多线程的数据依赖性不被考虑(其实通过锁机制等手动来控制)。

  • as-if-serial(单线程): 无论怎样排序,结果唯一
    as-if-serial保证单线程中,有数据依赖的两个操作,总是按照预定的顺序执行(通过禁止编译器、cpu重排序实现),然而对于没有数据依赖的操作重排序并不影响执行结果,所以会看到我们的代码“似乎”总是按照从上到下的顺序来执行的

happens-before

相比于单线程中的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编程即可保证程序正确性.

volatile 的happens-before

 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

锁的内存语义(遵守happens-before),
锁释放:把该线程的对应的本地存储(在临界区中使用到的数据)刷入主存;
锁获取:把该线程 在临界区中使用到的数据 对应的本地存储置为无效,并从主存中获取;

final关键字的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

happens-before之单例的实现

由于对象的初始化会有重排序,比如,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();
		}
	}
}

你可能感兴趣的:(并发编程,深入理解java并发)