JVM——高效并发

来源:互联网 发布:mac 关闭已打开的程序 编辑:程序博客网 时间:2024/05/20 05:58

1.Java内存模型(Java Memory Model,简称JMM)

JMM规定了所有的共享变量都存储在主内存(Main Memory)中,(这里的主内存仅仅只是虚拟机内存的一部分)。每条线程都有自己的工作内存(Working Memory),线程的工作内存中保存了变量的副本,线程对于变量的所有操作(读取、赋值等)都必须在自己的工作内存中,而不能直接读写主内存中的变量值。不过,线程与线程之间也无法直接访问对方工作内存中的变量,线程之间变量值得传递需要通过主内存来完成。

212310_82w7_200838.jpg (700×338)

从低层次来说,主内存相当于物理硬件的内存,而为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓冲区,因为程序运行时主要访问读写的是工作内存。

2.内存间的交互操作

关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存中拷贝到工作内存中、如何从工作内存同步回主内存之类的实现细节。

JMM定义了8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(对于double和long来说,可以允许有例外)。

*lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态

*unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才会被其他线程锁定

*read(读取):作用于主内存的变量,它把变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用

*load(载入):作用于工作内存的变量,它把read操作从主内存中得到的值放入到工作内存的变量副本中

*use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用该变量的字节码指令时将会执行这个操作

*assign(赋值):作用于工作内存,把一个从执行引擎接收到的值符给工作内存的变量

*store(存储):作用于工作内存变量,它把工作内存中的一个变量的值传给主内存,以便随后的wirte操作

*wirte(写入):作用于主内存的变量,它把store操作从工作内存中得到的值放入主内存的变量中

如果要把一个变量从主内存复制到工作内存,那就要顺序的执行read和load操作,如果要把变量从工作内存同步到主内存中,就要顺序的执行store和wirte操作。注意,JMM只是说顺序执行,并没有说是连续执行,也就是说read和load之间、store和wirte之间也可以插入其他指令,如:对主内存的a、b进行访问时,可能出现的顺序是read a、read b、load b、load a.除此以外,JMM还规定了在执行上述8种基本操作时必须满足如下规则:

#不允许read和load、store和wirte操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或是工作内存发起了回写但是主内存不接受的情况出现

#不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存

#不允许一个线程无原因的地(没有发生任何assign操作)把数据从工作内存同步回主内存中

#一个新的变量只能在主线程中“诞生”,就是说对一个变量的use和store操作之前必须先执行load和assign操作

#一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会解锁

#如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量之前,需要重新执行load或assign操作初始化变量的值

#如果一个变量事先没有被锁定,那么就不允许对它进行unlock操作,也不允许去unlock一个被其它线程锁定的变量

#一个变量执行unclock操作之前,必须把此变量同步回主内存中(执行store,wirte操作)

3.volatile

volatile可以说是JVM提供的最轻量级的同步机制。

当一个变量定义为volatile之后,具备两种特性:一是保证此变量对所有线程的可见性,这里的“可见性”是指一天线程改变了这个变量的值,其他线程立马可见。而普通的变量则做不到这一点,普通变量的值在线程之间的春娣需要通过主内存来完成,如:线程A修改了一个变量的值,然后向主内存进行回写,另外一条线程在A回写完之后再从主内存进行读取操作,新变量值才会对线程B可见。二是禁止指令重排序来保证有序性。

注:volatile不能保证对变量的操作是原子性的。

volatile的原理和实现机制:观察加入volatile和没有加入volatile所生成的汇编代码发现,加入volatile时,会多出来一个lock前缀指令。lock前缀指令相当于一个内存屏障(内存栅栏),它会提供以下功能:

1>确保指令重排序时不会把后面的指令拍到内存屏障之前,也不会把前面的指令排到内存屏障之后,即在执行到内存屏障这句指令时,在它前面的操作全部完成,保证代码的执行顺序与程序的顺序相同

2>强制对缓存的修改写入内存

4.对于long和double型变量的特殊规则

long和double的非原子性操作。

JMM允许JVM将没有被volatile修饰的64位的数据类型的读写操作分为两次来读写,导致可能会读取到“半个变量”的情况。

因此,需要加volatile关键字

5.原子性、可见性和有序性

原子性:JMM来直接保证的原子性操作是read、load、use、assign、store、wirte,我们大致可以认为基本数据类型的访问读写是具备原子性的(除long 、double的非原子性协定)。

如果场景中需要更大范围的原子性保证,JMM还提供了lock和unlock的操作来满足要求,尽管JVM未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式的使用这两个操作,这两个字节码的指令反应到java代码中就是同步块——synchronized关键字,因此synchronized块之间的操作也具备原子性

可见性:是指当一个线程修改了共享变量的值,其他线程能立即得知这个修改。JMM是通过变量在修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式实现可见性,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存中,以及每次使用前能立即从主内存刷新。因此说,volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。

除volatile之外,Java还有两个关键字能实现可见性,即synchronized和final。同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、wirte操作)”这条规则获得的,而final的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this的引用传递出去(this引用逃逸是一件很危险的事,其他线程有可能通过这个引用访问到“初始化可一半”的对象),那其他线程中就能看见final字段的值。如:

public static final int i;public final int j;static {       i=0;//do something}{      j=0//do something}
变量i和j都具备可见性,它们无须同步就可以被其他线程正确访问到。

有序性:java提供了volatile和synchronized来保证线程操作之间的有序性。volatile关键字本身就包含可禁止了指令重排序的语义,而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则而得,这天规则决定了持有同一个锁的两个同步块只能串行进入。

6.先行发生原则

*程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确来说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构

*管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序(只能解锁以后在加锁)

*线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作

*线程终止规则:线程中所有的操作都先行发生于对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到程序已经终止执行

*线程中断规则:对线程intrrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生

*对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始

*传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,就可以得到操作A先行发生于操作C

总结:时间的先后顺序与先行发生原则之间基本没有太大的关系,所以在衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。

0 0
原创粉丝点击