一、java内存模型
1.java内存模型
程序运行过程中的临时数据是存放在主存(物理内存)中,但是现代计算机CPU的运算能力和速度非常的高效,从内存中读取和写入数据的速度跟不上CPU的处理速度,在这种情况下,CPU高速缓存应运而生。基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是出现了一个新的问题:目前计算机多为多核CPU或多处理器,每个处理器都有自己的高速缓存,但是这些处理器又共享同一主存。java内存模型主要是定义了java程序中各个变量的访问规则,这里的变量与java编程中变量不是同一个概念,包括:实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,java内存模型规定:
(1)所有的变量存储在主内存中,每个线程都有自己的工作内存,线程的工作内存保存了该线程使用到的变量,并且这些变量是从主内存中对应变量的拷贝
(2)线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量
(3)不同线程之间无法直接访问彼此的工作内存,线程间的变量值的传递需要借助主内存。
2.java工作内存与主内存的交互
lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
二、基本概念
1.可见性:java多线程之间的可见性,一个线程修改的某个对象的状态对另外一个线程是可见的。java中经常使用同步或者volatile来保证变量的可见性。
2.原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。原子代表不可分割性,例如:int a=0,这个操作是不可分割的,反之,a++这个操作是可以切割的
可以分为a=a+1,读取内存中a的值,读取a的值后进行加1操作,把结果值写入内存。
3.有序性:即程序执行的顺序按照代码的先后顺序执行。在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
4.重排序:在执行程序时为了提高性能,编译器和处理器经常会对指令进行重排序,重排序分成三种类型:
编译器优化的重排序。编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序
- 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行
- 内存系统的重排序。由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行
三、volatile
1.基本概念
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
2.基本使用
public class DoubleCheck{ private static volatile DoubleCheck instance; private DoubleCheck(){} public static DoubleCheck getInstance(){ //第一次检测
if (instance==null){
//同步
synchronized (DoubleCheck.class){
if (instance == null){
//多线程环境下可能会出现问题的地方
instance = new DoubleCheck();
}
}
}
return instance;
}
}
上述代码一个经典的单例的双重检测的代码,在单线程环境下这样写并没有什么问题,但如果在多线程环境下就可能出现线程安全问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化,而通过volatile修饰后,就能很好的解决非线程安全的问题。
3.总结
①、保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新
②、禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障
③、volatile的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
④、volatile变量,本质上是通过内存屏障来实现其可见性和禁止重排优化。内存屏障是硬件层的概念,不同的硬件平台实现内存屏障的手段并不是一样,java通过屏蔽这些差异,统一由jvm来生成内存屏障的指令。内存屏障有两个作用:阻止屏障两侧的指令重排序;强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。