作为一个Java小白,持续学习是不可避免的,近期看了程晓明老师的《深入理解Java内存模型》,学习了JMM的知识,所以写了此篇文章,做个知识记录,防止知识遗忘。
目录
JMM图例
先给大家画一张JMM通用模型示例图
书中指出:在Java中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程中共享。局部变量等不在线程中共享,不会存在内存可见性问题,也不受内存模型的影响。
JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,本地内存中存储了该线程用来读写的变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。JMM通过控制主内存与每个线程之间的交互,来为java程序员提供内存可见性保证。
重排序
为了提高性能,编译期和处理器有时会对指令做重排序,重排序分为三种类型
1:编译器优化的重排序:编译器在不改变单线程语义的前提下,可以重新安排语句的执行顺序。
2:指令级并行的重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行,如果不存在数据依赖性,处理器可以改变对应机器指令的执行程序。
3:内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行
处理器重排序
因为处理器的运行速度远远大于内存的操作速度,所以现代处理器使用写缓冲区来临时保存向内存写入的数据,从而保证指令流水线持续进行。同时,通过批处理的方式刷新写缓冲区,以及合并缓冲区对同一内存的多次操作,可以减少对内存总线的占用。但是缓冲区仅仅对当前处理器可见,所以就会出现处理器对一个变量进行操作了之后,其他处理器有可能拿到的还是操作前的值。也就是处理器对内存读/写的操作顺序与内存实际发生的操作顺序并不一致。
happens-before
从JDK5开始,java使用新的JSR-133内存模型,它使用了happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作间必须要存在happens-before关系,这里提到的两个操作可以是一个线程之内,也可以是不同线程。需要注意的是,两个操作具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行,happens-before仅仅要求前一个操作的执行结果对后一个操作可见,也就是处理器并不一定按照代码的顺序严格执行,但是结果一定严格按照代码的逻辑进行输出。
Happens-Before的八个规则(摘自《深入理解Java虚拟机》):
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- 管程锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;(此处后面指时间的先后)
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;(此处后面指时间的先后)
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
- 传递性:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
Synchronized
Synchronized的内存语义:
- 当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须要从主内存中获取共享变量。
- 当线程释放锁之前,JMM会把线程对应的本地内存中的共享变量刷新到主内存中。
Volatile
Volatile的内存语义:
- volatile变量读之前,必须要先从主内存进行加载。
- volatile变量写之后,必须要立马同步回主内存。
线程安全性分析
- Synchronized:获得锁的线程才能对共享变量进行读/写,所以是线程安全的。
- Volatile:并没有限制多线程针对变量的读/写,所以线程不安全。