java并发编程学习(四) Java内存模型详解

Java内存模型是用来做什么的

主要目标

Java内存模型抽象结构

   ​

Java内存模型如何保证内存一致性

happens-before 原则

Java内存模型的有序性

从源代码到指令序列的重排序

重排序导致的线程安全问题

Java内存模型如何解决重排序导致的线程安全问题        

volatile 禁止 指令重排

synchronized锁控制线程串行执行

总结


Java内存模型是用来做什么的

Java虚拟机规范中试图定义一种Java内存模型来屏蔽各种硬件和操作系统的内存访问差异,以实现Java程序在各种平台下都能达到一致的内存访问效果

主要目标

Java内存模型的主要目标是定义程序中各个变量的访问细节,即将变量从内存取出和将变量写入内存的细节。

注意:变量是指实例变量,静态变量和构成数据对象的元素,不包括局部变量和方法参数,因为局部变量和方法参数是线程私有的,不会共享,就不存在竞争的问题,不用考虑内存一致性。

总的来说,Java内存模型为了实现变量的内存一致性的效果,例如在跨平台上,或者多线程环境下。 

Java内存模型抽象结构

   

Java内存模型中有两大划分,主内存和工作内存

主内存是用来存储变量的,工作内存则是每个线程都私有的,存放的主内存变量的拷贝

每个线程不能访问其他线程的工作内存,每个线程对变量的读写都是在工作内存中进行的,不能直接访问主内存

线程之间共享变量的传递必须通过主内存完成。

举个例子说明,线程A更新共享变量的值到工作内存,jmm(Java内存模型)将工作内存中更新的值,同步到主内存中,线程B的工作内存读取主内存的拷贝,获取到共享变量的修改。

Java内存模型如何保证内存一致性

java内存模型中定义了8种原子性的操作来完成主内存和工作内存的交互,并对这几种操作定义规则保证,这些规则是虚拟机层面的不方便记忆,我们只需要知道是happen-before原则保证了并发访问变量的安全性

这8种原子性的操作是: 

lock(锁定),unlock(解锁),read(读取),load(载入),use(使用),assign(赋值),store(存储),write(写入)

happens-before 原则

从jdk5开始,java使用新的jsr-133内存模型。JSR-133使用happens-before原则来阐述操作间的内存可见性。

在jmm中,如果一个操作的执行结果对另一个操作的执行结果可见,那么这两个操作间必须存在happens-before原则,这两个操作可以在一个线程中,也可以在不同线程间。

happens-before原则:

  1. 程序顺序规则:一个线程中,按照代码顺序,书写在前面的操作先行发生于线程中任意后续操作(时间上的先后)
  2. 监视器锁规则:一个锁的解锁,先行发生对这个锁的加锁(时间上的先后)
  3. volatile变量规则:对一个volatile域的写,先行发生任意后续对这个域的读
  4. 线程启动规则:线程A执行ThreadB.start() 启动线程B,那么A线程的ThreadB.start()操作happen-before线程b中的任意操作
  5. 线程终止规则:线程A执行ThreadB.join()操作成功返回,那么线程B中的操作happen-before线程A从ThreadB.join()操作成功返回
  6. 线程中断原则: 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检查到是否有中断发生。
  7. 对象终结原则: 一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
  8. 传递性原则: 如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C

验证线程中断原则

package cn.bing.mnvdemo;
class TUtils{
	public static boolean flag = true;
}
public class MyTest extends Thread{
	@Override
	public void run() {
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("----flag--- "+TUtils.flag);
	}
	public static void main(String[] args) {
		MyTest mt = new MyTest();
		mt.start();
		TUtils.flag = false;
		mt.interrupt();
	}
	
}

运行结果:

java.lang.InterruptedException: sleep interrupted
----flag--- false
	at java.lang.Thread.sleep(Native Method)
	at mnvdemo.MyTest.run(MyTest.java:9)

验证对象终结原则 

package cn.bing.mnvdemo;
class TUtils{
	public static boolean flag = true;
}
public class MyTest extends Thread{
	public MyTest() {
		TUtils.flag = false;
	}
	@Override
	public void run() {
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		System.out.println("----flag--- "+TUtils.flag);
	}
	public static void main(String[] args) {
		MyTest mt = new MyTest();
		mt = null;
		System.gc();
	}
	
}

运行结果 

----flag--- false

注意:

 两个操作间具有happens-before关系,并不意味着前一个操作在后一个操作前执行,happens-before仅仅要求前一个操作的执行结果对后一个操作的执行结果可见,且前一个操作按照顺序上排在第二个操作之前。

Java内存模型的有序性

从源代码到指令序列的重排序

在执行程序时,为了提高性能,编译器和处理器常会对指令做重排序,重排序分为3种重排序

1> 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排重排序

2> 指令级并行的重排序。现代处理器采用了指令级并行技术将多条指令重叠执行。如果不存在数据依赖性,可以改变对应机器指令的执行顺序。

3> 内存系统的重排序。 由于处理器使用缓存和读写缓存区,使得加载和存储操作看起来像是乱序执行

                        从源代码到最终执行的指令序列示意图

1属于编译器重排序,2和3属于处理器重排序,这些重排序会导致出现内存可见性问题

总的来说:

1. 对于一段程序,在虚拟机中,并不是按照代码顺序执行的,会发生重排序。

2. 对于单线程的方法,不管怎么重排序,还是满足顺序一致性原则,也就是单线程的程序的执行结果始终和程序顺序执行的结果一致。

3.  对于其他线程而言,看另一个线程的方法,因为重排序,是乱序的,这样就会导致多线程环境下的线程安全问题。

重排序导致的线程安全问题

以双重检查锁定的单例模式为例来说明,重排序导致的内存可见性问题

package cn.bing.singleton;
/**
 * 双重检查锁定延迟加载
 * @author Administrator
 *
 */
public class SingleTonDoubleLock {
	private static SingleTonDoubleLock instance;
	private SingleTonDoubleLock() {}
	public static SingleTonDoubleLock getInstance() {
		if(instance==null) {//1 第一次检查
			synchronized (SingleTonDoubleLock.class) {//2
				if(instance==null)//第二次检查
				instance = new SingleTonDoubleLock();
			}
		}
		return instance;
	}
}

1. 线程A调用getInstance方法,执行2位置,获取到synchronized锁,开始进行对象的创建,对象的创建分为下面步骤

  1. 给对象分配内存空间
  2. 对象初始化
  3. 将对象的地址赋值给引用instance

重排序将3排序到2之前,也就是对象还有初始化,就给引用赋值

2. 此时线程B执行到1位置,看到引用不为空,直接就返回了,但是对象没有初始化,这就是重排序导致的线程安全问题。

Java内存模型如何解决重排序导致的线程安全问题        
 

volatile 禁止 指令重排

上面的例子中,只需要声明instance是volatile类型的,就会禁止对象还没有初始化就对引用赋值。

synchronized锁控制线程串行执行

通过锁控制,每次只能有一个线程执行程序,那么另一个线程是不可见另一个线程的重排序,并且对于单线程而言,重排序不会影响程序的执行结果。

总结

Java内存模型是围绕原子性、可见性,有序性三个特征建立的

原子性:Java虚拟机提供了monitorenter和monitorexit两个指令来保证一段程序的原子性,体现在synchronize同步代码中,synchronize修饰的方法是原子操作。

基本类型变量读写是原子性的(Java内存模型允许对于64位的基本类型变量的读写操作,分为两个32位进行读写,也就是存在多线程读取到64位的基本类型的值是半个值,但是基本不可能发生,因为各种商用的虚拟机都是将64位的基本类型的读写作为原子操作)

可见性:Java虚拟机的可见性定义了一套规则——happen-before原则实现多线程间共享变量的可见。另外,对于final的可见性,当final修饰的变量,在构造函数中初始化,并且对象没有发生this逃逸,final的值立即对其他线程是可见的。

this逃逸: 在对象还没有初始化就已经拿到对象引用了

有序性:Java中,线程本身是有序的,但是一个线程看另一个线程是乱序的。这里的有序性,指的是通过一些方法来保证线程看另一个线程的执行时有效的。两个线程同时执行一个对于共享变量的操作的方法,容易出现线程安全问题,如this逃逸问题。Java通过volatile关键字来禁止重排序,这样另一个线程看到的也是有序的,或者使用synchronize确保每次只有一个线程执行,这样另一个线程看不到另一个线程中的重排序。

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值