java线程模型和Volatile

Volatile关键字字节码多了一个ACC_VOLATILE标志

Java的内存模型(不同于jvm内存模型)

每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值),都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

 

操作系统底层:

计算机在运行程序时,每条指令都是在 CPU 中执行的,在执行过程中势必会涉及到数据的读写。我们知道程序运行的数据是存储在主存中,这时就会有一个问题,读写主存中的数据没有 CPU 中执行指令的速度快,如果任何的交互都需要与主存打交道则会大大影响效率,所以就有了 CPU 高速缓存CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关。

有了 CPU 高速缓存虽然解决了效率问题,但是它会带来一个新的问题:数据一致性。在程序运行中,会将运行所需要的数据复制一份到 CPU 高速缓存中,在进行运算时 CPU 不再也主存打交道,而是直接从高速缓存中读写数据,只有当运行结束后,才会将数据刷新到主存中。

操作系统底层解决缓存一致性方案有两种:

  1. 通过在总线加 LOCK# 锁的方式
  2. 通过缓存一致性协议

第一种方案, 存在一个问题,它是采用一种独占的方式来实现的,即总线加 LOCK# 锁的话,只能有一个 CPU 能够运行,其他 CPU 都得阻塞,效率较为低下。

第二种方案,缓存一致性协议(MESI 协议),它确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:当某个 CPU 在写数据时,如果发现操作的变量是共享变量,则会通知其他 CPU 告知该变量的缓存行是无效的,因此其他 CPU 在读取该变量时,发现其无效会重新从主存中加载数据。

 

volatile关键字可见性:

volatile 修饰的成员变量在每次被线程访问时,都强迫从主存(共享内存)中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到主存(共享内存)。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值,这样也就保证了同步数据的可见性。

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量

可见性的一个应用点:

有一个全局的状态变量open:

这个变量用来描述对一个资源的打开关闭状态,true表示打开,false表示关闭,假设有一个线程A,在执行一些操作后将open修改为false:

线程B随时关注open的状态,当open为true的时候通过访问资源来进行一些操作:

当A把资源关闭的时候,open变量对线程B是不可见的,如果此时open变量的改动尚未同步到线程B的工作内存中,那么线程B就会用一个已经关闭了的资源去做一些操作,因此产生错误。

volatile关键字常用于一些需要全局状态的场景

 

volatile关键字原理:

如果第一个操作为volatile读,则不管第二个操作是啥,都不能重排序。这个操作确保volatile读之后的操作不会被编译器重排序到volatile读之前;

当第二个操作为volatile写是,则不管第一个操作是啥,都不能重排序。这个操作确保volatile写之前的操作不会被编译器重排序到volatile写之后;

当第一个操作volatile写,第二操作为volatile读时,不能重排序。

volatile的底层实现是通过插入内存屏障,但是对于编译器来说,发现一个最优布置来最小化插入内存屏障的总数几乎是不可能的,所以,JMM采用了保守策略。如下:

  • 在每一个volatile写操作前面插入一个StoreStore屏障,StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。
  • 在每一个volatile写操作后面插入一个StoreLoad屏障,StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。
  • 在每一个volatile读操作后面插入一个LoadLoad屏障,LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。

 

volatile去修饰数组或者对象

只会保证对象和数组引用的可见性,但数组和对象内容仍然在主存里面,所以对其中内容的修改我们好像也是可以看到的。

当不用volatile关键字时,如果在线程里面sleep一段时间,其实也能达到读到同样的值的效果。why?

JVM会尽力保证内存的可见性,即便这个变量没有加同步关键字。换句话说,只要CPU有时间,JVM会尽力去保证变量值的更新。这种与volatile关键字的不同在于,volatile关键字会强制的保证线程的可见性。而不加这个关键字,JVM也会尽力去保证可见性,但是如果CPU一直有其他的事情在处理,它也没办法。最开始的代码,一直处于死循环中,CPU处于一直占用的状态,这个时候CPU没有时间,JVM也不能强制要求CPU分点时间去取最新的变量值。而加了输出或者sleep语句之后,CPU就有可能有时间去保证内存的可见性,于是while循环可以被终止。

volatile关键字无法保证原子操作?

只能保证每次读取的是最新值,但当多个线程对共享内存的变量进行修改时,就不能保证线程安全了。

volatile防止指令重排:

正常的步骤:

1:分配对象的内存空间

2:初始化对象

3:将变量指向刚刚分配的内存地址

Instance instance=new Instance();//这就不是一个原子操作

指令重排导致:

1->3->2

一个线程先执行3,导致这个引用不为空,这时另外一个线程进入方法判断这个引用不为空,开始使用,其实这个对象并没有被初始化,就会导致使用的时候出错。

http://www.importnew.com/23535.html

happens_before原则:happens-before是JMM最核心的概念

JSR-133对其的定义:

如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果与按happens-before关系来执行的结果一致,那么这种重排序并不非法(JMM允许这种重排序)。

在设计JMM时的核心目标就是找到一个好的平衡点:

  • 一方面要为程序员提供足够强的内存可见性保证;
  • 一方面对编译器和处理器的限制要尽可能地放松。

对于不同性质的重排序:

1.对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序

2.对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种重排序)

 

java中的一些必须符合happens-before的原则:

程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读
传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作
join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回

posted @ 2019-01-19 21:20  LeeJuly  阅读(132)  评论(0编辑  收藏  举报