Java内存模型描述了Java程序中各类变量(共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取这些变量的底层细节。程序员
在开始并发编程时,咱们须要思考两个关键的问题:1.线程之间如何通讯?2.线程之间如何同步?编程
在命令式编程中,线程之间有两种通讯方式:数组
同步是指程序用于控制线程发生相对顺序执行的机制。在共享内存模型里,程序员须要给代码加上制定的互斥操做来显式进行;在消息传递模型中,通讯是对程序员透明的,是隐式进行的。缓存
全部的实例域、静态域、数组元素是储存在堆中的,线程之间能够共享,能够将它们称为“共享变量”,他们可能会在并发编程时出现“可见性”问题;而局部变量、方法参数、异常处理参数不会在线程之间共享,不受内存模型的影响。bash
假如一个变量被多个线程使用到,那么这个共享变量会在多个线程的工做内存中都存在副本。多线程
当一个共享变量被一个线程修改,可以及时被其余线程看到,这叫作可见性。并发
若是两个操做访问同一个变量,并且这两个操做中有一个为写操做,那么这两个操做之间就存在了数据依赖性。数据依赖性存在如下三种状况:性能
操做 | 示例 |
---|---|
先写,后读 | a=1;b=a; |
先写,后写 | a=1;a=2; |
先读,后写 | b=a;a=1; |
不难发现,上面的三种状况,只要重排序其指令,结果都会产生变化。优化
因此编译器和处理器在进行重排序时,必须遵照数据依赖性。不能对存在数据依赖性的两个操做进行重排序。ui
看下面一段代码
if(flag){ //操做1
int num=a+b; //操做2
}
复制代码
能够看到,操做1和操做2并不存在数据依赖,可是存在控制依赖。当代码中出现控制依赖时,会影响程序的并行度。所以,编译器和处理器会采用一种“猜想执行”来克服控制依赖性对并行度的影响(并行是为了效率和性能)处理器可能提早执行操做2,将a+b计算出来,并放置到一个叫“重排序缓存”的缓存中,假如得知操做1中的flag为真,再将结果写入num中。
在单线程程序中,对存在控制依赖关系的重排序,不会影响结果;不过在多线程中,可能会影响到结果。
实际执行的代码顺序和程序员书写的顺序是不同的,编译器或处理器为了提升性能,在不影响程序结果的前提下,会进行执行顺序的优化。
经过互斥锁来实现。
可以保证volatile变量的可见性,可是不能保证volatile变量复合操做的原子性。 volatile
经过加入内存屏障和禁止指令重排序来实现可见性的。对volatile
变量执行写操做时,会在写入后加一条store
的屏障指令;对volatile
变量执行读操做时,会在读操做前加入一条load
屏障指令。
因为处理器的速度很快,为了不处理器停顿下来等待内存(内存确定跟不上处理器的速度)而产生的延迟,现代的处理器使用缓存区来临时保存处理器向内存写入的数据,而后再提供给内存,以保证接二连三地高效运行。
虽然缓存区存在诸多好处,可是它是仅对处理器可见的。这将会产生一个重要的问题:处理器堆内存的独写操做的顺序,可能与内存中实际发生读写操做顺序不一致。(由于现代的处理器大都容许使用重排序)
因此,为了保证可见性,Java编译器会在生成指令序列时,插入内存屏障来禁止特定的处理器进行重排序。
volatile
变量的过程:volatile
变量副本的值volatile
变量的过程:volatile
变量的最新的值到工做内存中。volatile
不能保证volatile
变量符合操做的原子性:举一个例子:
public class VolatileDemo{
private volatile int num=0;
public int getNumber(){
return this.num;
}
public void increase(){
this.num++;
}
public static void main(String[] args){
final VolatileDemo v=new VolatileDemo();
for(int i;i<500;i++){
new Thread(new Runnable(){
public void run(){
v.increase();
}
}).start();
}
//主线程主动让出资源让500个子线程运行。这个‘1’指的是主线程
while(Thread.activeCount()>1){
Thread.yield();
}
System.out.println(v.getNumber());
}
}
复制代码
这个程序是开启500个线程,每一个线程执行一次increase()
操做,给变量num
加一。运行这个程序屡次,发现并非每次输出结果都是500。
发生了什么问题?
由于this.num++
这条语句,实际上是三步操做,不具有原子性。假设一个运行场景:
可见,进行了两次加1操做,可是主存中的num只增长了1。怎么解决呢?咱们要保证num自增操做的原子性。