java内存模型
前言:并发处理的应用,使得amdahl定律代替摩尔定律成为计算机性能发展的原动力。也就是说如今的计算机并发处理的核心是 amdahl定律,也是压榨计算机运算能力的核心。
amdahl定律:通过系统中的并行化和串行化的比重来描述多处理系统能获得的计算能力。
摩尔定律:用于描述处理器的晶体管数量和运行效率之间的关系。
描述
多任务和高并发是衡量一台计算机处理器的能力重要指标之一。
一般衡量一个服务器性能的高低好坏,使用每秒事务处理数(Transactions Per Second,TPS)这个指标比较能说明问题,它代表着一秒内服务器平均能响应的请求数,而TPS值与程序的并发能力有着非常密切的关系。
计算机硬件效率与一致性
由于计算机的 存储设备(简单理解成硬盘) 与 处理器(简单理解成CPU) 的 运算能力 之间有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的 高速缓存(cache) 来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中没这样处理器就无需等待缓慢的内存读写了。
高速缓存的引入导致了,缓存数据之间的关系不一致问题,所以进而引入了 缓存一致性协议。在多处理器系统中,每个处理器都有自己的高速缓存,而他们又共享同一主内存。
java 内存模型
个人理解:在C或C++中, 利用不同操作平台下的内存模型来编写并发程序;Java利用了自身虚拟机的优势, 使内存模型不束缚于具体的处理器架构,真正实现了跨平台。
Java内存模型的主要目标:是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到主内存和从内存中取出变量这样底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。
java 内存模型规定:
- 所有变量都存储在主内存中
- 每条线程还包含工作内存(从内存,类比高速缓存),工作内存包含该线程使用到的变量的主内存副本拷贝,以及拷贝变量的操作(读、写等),不同线程之间无法直接访问对方工作内存的拷贝变量,但是可以间接的通过主内存完成访问
内存模型抽象图:
内存交互
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节
Java内存模型八种操作:
[x] 1. lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
[x] 2. unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
[x] 3. read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用。
[x] 4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
[x] 5. use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
[x] 6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
[x] 7. store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
[x] 8. write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
八种操作串连理解: lock -> read -> load -> use -> assign -> store -> write -> unlock
八种操作遵守的规则:
- [ ] 1. 不允许read和load、store和write操作之一单独出现。
- [ ] 2. 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
- [ ] 3. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
- [ ] 4. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
- [ ] 5. 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现。
- [ ] 6. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值。
- [ ] 7. 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
- [ ] 8. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)
java内存模型的特性:原子性、可见性和有序性
java内存模型是围绕着并发过程中,如何处理原子性、可见性和有序性这3种特性建立的模型。
- #### 原子性
java虚拟机中保证原子性的操作包括 read、load、assign、use、store和write
大致可以认为基本数据类型访问具有原子性(long和double非原子性协议除外)
满足原子性应用场景:
更大范围的原子性保证,还是使用synchronized
- #### 可见性
一个线程修改了公共变量,其他线程能够立即得知该变量的修改。
能够实现可见性的3个关键字:
volatile 变量:可以在多线程保证变量的可见性
synchronized :同步块的可见性,表现在执行unlock之前,必须把变量数据保存(store和write)在主内存中。
final :final可见性,表现在被构造器初始化完成,并且构造器没有把“this”的引用传递出去。
(个人理解:this的引用如果传递出去,会导致其他线程可以通过this随意访问该线程的所有变量,所以不能将this引用传递)
- #### 有序性
java程序具有天然的有序性 (java 的先行发生原则 happens-before) ,在本线程中,所有操作都是有序的。一个线程观察另外一个线程,所有操作都是无序的。
个人理解:线程内的有序,是因为java模型本身的设计有序。线程之间的无序,是因为计算机的执行任务的方式,当多线程的时候,计算机是根据比重一会儿执行A,一会儿执行B,交替执行的。
volatile :关键字本身禁止指令重新排序,来保证有序性
synchronized : 使得变量同一时间只能被一个线程执行“lock”操作。这条规则就决定了持有同一个锁的多个同步块只能串行执行。(保证了有序性)
先行发生原则(happens-before)
java内存模型的天然先行发生关系如下: - [x] 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构 - [x] 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。(lock之前必须有一个unlock操作) - [x] volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。(也可以理解为:如果对volatile变量有一个写操作,那么后面的读操作一定会等这个写操作完成,详见volatile的happen-before操作) - [x] 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。 - [x] 线程中断规则(Thread Interruption Rule): 对于线程执行interrupt()方法调用先行发生于被中断的线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生。(interrupt()调用优先于Thread.interrupted())。 - [x] 对象终结规则(Finalizer Rule):个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。 - [x] 传递性(Transivility):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。 “时间上的先后顺序”和“先行发生”的区别:主要是判断数据是否存在竞争,是线程是否安全的判断依据。
private int a = 0;
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
public static void main(String[] args) {
/**
* 多线程操作“时间上的先后顺序”和“先行发生”的区别
* 打印100次的结果:有0有1
说明多线程中的,这种操作不是线程安全的
*/
for (int i = 0; i < 100; i++) {
final Test t = new Test();
//a线程
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
t.setA(1);
}
});
thread1.start();
//b线程
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("a:" + t.getA());
}
});
thread2.start();
}
}
重排序
在执行程序中为了提高性能,编译器和处理常常会对指令进行重排序操作。
重排序类型:
- 编译器优化重排序。
编译器在不改变单线程程序语义的前提下,重新安排语句的排序顺序。
- 指令级并行的重排序。
现代处理器(多核处理器)采用的是指令并行技术,来将多条指令重叠执行。
如果不存在数据之间的依赖,处理器可以改变对应机器的指令执行顺序。
- 内存系统的重排序。
由于处理器使用了缓存和读/写缓冲区,使得加载和保存操作看上去是乱序执行,所以需要对内存进行重排序。
java源码编译到执行重排序图:
1 是属于编译器重排序
2、3 是数据处理器重排序
JMM 是属于语言级别的重排序,确保在不同平台不同编译器之上,
通过禁止特定类型的编译器重排序和处理器重排序,提供一致性的内存可见性。
(换句话说,等同于处理跨平台)
参考:《深入理解Java虚拟机:JVM高级特性与最佳实践》
本人自己的学习整理,如有错误,请留言或者联系作者QQ:1293208049,共同学习,欢迎吐槽!
上一篇: JAVA内存模型的秘密
下一篇: 基于matlab的自适应LMS算法实现