多任务处理在现代计算机操作系统中几乎已是项必备的功能了。 在许多情况下,让计算机同时去做几件事情,不仅是因为计算机的运算能力强大了,还有一个很重要的原因是计算机的运算速度与它的存储和通信子系统·速度的差距大大·,大量的时间都花费在磁盘IO、网络通信或者数据库访向上。
物理计算机中的并发问题与虚拟机的情况很相似,具有相当大的参考意义。
绝大多数计算任务都不可能只靠处理器完成。处理器至少要和内存交互(如读取数据、存储结果等等),这个IO操作时很难消除的。现代计算机系统都不得不加入了一层读写接近处理器速度的高速缓存:将运算需要使用到的数据复制到缓存
中,让运算能快速进行,当运算结束后再从缓存同步回内存
之中,这样处理器就无须等待缓慢的内存读写了。
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性 (Cache Coherence)
在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同主内存 (Main Memory).
当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?
为了了解决致性的问题, 需要各个处理器访问缓存时都遵循一些协议, 在读写时要根据协议来进行操作(协议类型很多)
可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问
的过程抽象.不同架构的物理机器可以拥有不一样的内存模型,而Java虚拟机也有自己的内存模型
为了使得处理器内部的运算单元能尽量被充分利用,处理器会对输入代码进行乱序执行(Out-Of-Order execution)优化,处理器会将乱序执行的结果重组,保证(如何保证不是我们关心的点!
)结果与顺序执行的结果时一样的;但并不保证程序中各个语句计算的先后顺序和代码中的顺序一致。
所以如果一个任务依赖另一个任务的中间结果,其顺序性不能靠代码的先后顺序来保证(应该可以靠其他方式
).JVM也有类似的指令重排序优化
java内存模型(JMM)可以屏蔽掉硬件和操作系统的内存访问差异,保证了java的可移植性。本文的JMM基于jdk1.5的。
JMM的主要目标就是定义程序中各个变量的访问规则:即jvm将变量(不包括局部变量和方法参数,这些在栈中,线程私有
)存储到内存和从内存中取出的底层细节。JVM没有限制硬件和操作系统层面的优化。
JMM定义了如下8种操作区完成变量从主内存拷贝到工作内存、以及从工作内存同步回主内存之类的实现细节。JVM实现时保证下面的操作都是原子性、不可分割的(在某些平台,如32位系统上,double和long类型的变量会有例外
)
如果要把一个变量从主内存传输到工作内存,那就要顺序的执行read和load操作,如果要把一个变量从工作内存回写到主内存,就要顺序的执行store和write操作(都是两步,不是一步!
)。
虚拟机只是要求顺序的执行,并没有要求连续的执行,所以如下也是正确的。对于两个线程,分别从主内存中读取变量a和b的值,并不一样要read a; load a; read b; load b.
关键主要是知道jvm会如何运作
,怎么实现可以先放放!!!
针对volatile
修饰的变量,会有一些特殊规定
volatile可以说是java虚拟机中提供的最轻量级的同步机制。java内存模型对volatile专门定义了一些特殊的访问规则
。当一个变量被定义位volatille之后,它将具备两种特性
保证此变量对所有的线程的可见性,即一个线程修改了该变量的值,其他线程可以立即得知。需要注意:可见性并不保证线程安全。多线程同时操作一个线程还是会出现覆盖、资源竞争等问题。
严谨的说:工作内存在某一个时刻变量可能会出现不一致,但执行引擎不会出现这种情况(因为它每次都会去主内存中读取最新值)
跟其他保证并发安全的工具相比。在某些情况
下,volatile的同步机制性能要优于锁(使用synchronized关键字或者java.util.concurrent包中的锁)。但是现在由于虚拟机对锁的不断优化和实行的许多消除动作,很难有一个量化的说快多少。
与自己相比,就可以确定一个原则:volatile变量的读操作和普通变量的读操作几乎没有差异,但是写操作会慢一些,因为要在本地代码中插入许多内存屏障指令来禁止指令重排序
,保证处理器不发生代码乱序执行行为。
指令重排序,个人感觉还是很难理解的一个知识点。特别是如果你写代码时,如果一直考虑重排序,简直就是灾难。我有几个建议:
添加volatile修饰
,这里我们不能只考虑可见性,需要注意是如何避免指令重排序的影响
普通的变量仅仅会保证在该方法执行的过程中,所有依赖赋值结果的地方都能获取到正确的结果,但不能保证变量赋值的操作顺序和程序代码的顺序一致。(在单线程环境下没有问题)
重排序有多种类型,大体归为两类,编译器级别的重排序(编译成汇编语言
)和处理器级别的重排序。那volatile对这两类的重排序到底做了什么程度的保证(我目前并不打算去研究jvm或者cpu是如何实现禁止指令重排序,我们编程更关注的是volatile的禁止指令重排序是什么意思,保证到什么程度?
)
查了很多资料,现在网上有两种主流说法:
我之前也是这么理解的,这个说法的优点在于:我们程序员在考虑指令重排序时,比较方便分析代码
)下面是JSR-133中volatile对于编译器重排序的规则表
参考:https://mp.weixin.qq.com/s/iQqJfNJxQUrcyuJtDVqyLA
我的理解(目前的个人理解)
volatile禁止指令重排序,其实是对两种类型的重排序都做了处理。
这个内存屏障可以保证屏障两边的指令不会重排序
可能有些疑问,为什么要考虑volatile的禁止指令重排序这么详细,简单理解成volatile读写两边不会重排序会有影响吗?
会有,看如下代码(这些都是我的理论场景,目前水平无法复现这些场景
)
volatile boolean success=false;
int money = -1000;//负债
test(){
success = true;
money = 1000;
}
check(){
system.out.println(money);
system.out.println(success);
}
上面时伪代码,有两个线程,线程1和线程2并发执行test和check方法。
因为两个volatile变量之间的操作肯定不会重排序
JMM就是围绕着这三个特性来实现的
由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store 和write,我们大致可以认为基本数据类型的访问读写是具备原子性的
如果应用场景需要个更大范围的原子性保证 (经常会遇到), Java内存模型还提供了lock和unlock操作来满足这种需求,尽管JVM未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块一synchronized 关键字
,因此在synchronized块之间的操作也具备原子性。
可见性是指一个线程修改了一个变量的值后,其他线程立即可以感知到这个值的修改。正如前面所说,volatile类型的变量在修改后会立即同步给主内存,在使用的时候会从主内存重新读取,是依赖主内存为中介来保证多线程下变量对其他线程的可见性的。
除了volatile,synchronized和final也可以实现可见性。synchronized关键字是通过unlock之前必须把变量同步回主内存来实现的,final则是在初始化后就不会更改,所以只要在初始化过程中没有把this指针传递出去(this引用逃逸很危险,其他线程可能看到初始化了一半的对象!!!)也能保证对其他线程的可见性。
在单线程中观察,所有的操作都是有序的;在一个线程中观察另一个线程,所有的操作都是无序的。
java本身提供了volatile和synchronized来保证有序性。其中synchronized的原理是因为被包围的代码同时只能被一个线程执行
这三种特性synchronized都能满足,但是也造成了被滥用的局面,可能会对性能照成较大的影响
如果java内存模型中所有的有序性仅仅靠volatile和synchronized来完成,那么有一些操作将会变得很烦琐,但是我们编写代码时并没有这一感觉。这是因为java语言有一个先行发生(happends-before)
的原则.
这个原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据(这个依据也是我们程序员编程时需要考虑的因素,即代码是否是安全
的).
先行发生原则是JMM中定义的两项操作之间的偏序关系。如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到
。
同一个线程内
,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。时间上
)对同一个锁的lock操作。写操作先行发生于后面(时间上)对这个变量的读操
作。时间上的先发生
不代表这个操作会是先行发生(即时间线上后面的操作不一定可见影响)必定是时间上的先发生
(比如指令重排序,解释一下:对操作int i =2;int j = 3;
而言;根据第一条规则,在同一线程内int i = 2
先行于int j= 3
,但这时可能会有指令重排序,因为这个不影响最后的结果,我理解就是观察到没有影响也是一种成功的观察;但在时间上先行的不一定先执行了)来帮助我们分析代码是否是安全的!!!暂时不用过于深究更底层的细节,否则会陷入牛角尖