2023-12-29
JAVA volatile
关键字的一些问题记录
从一个例子出发
/**
* Application
*/
public class Application {
public volatile int count;
public void add() {
count++;
}
public static void main(String[] args) {
Application application = new Application();
for (int i = 0; i < 100; i++) {
new Thread(application::add).start();
}
System.out.println(application.count);
}
}
result:
[Running] cd "c:\Users\Renz_\github\demo\" && javac Application.java && java Application
96
[Done] exited with code=0 in 1.181 seconds
[Running] cd "c:\Users\Renz_\github\demo\" && javac Application.java && java Application
99
一句话概括: volatile保证了可见性和有序性,并不能保证原子性。
原因分析
JMM(Java Memeory Model)
是个什么东西
计算机内存模型
CPU和缓存一致性
指令在cpu执行,指令执行所需要的数据保存在主存中,也就是物理内存中。为了解决内存读写速度和CPU执行速度差距过大的问题,引入了cpu高速缓存。随着cpu规格的不断提升,衍生出多级缓存,也就是常见的L1 L2 L3 Cache
。
这时候cpu指令运行时读取数据的顺序就变成 L1 -> L2 -> L3 OR Main Memeory
.
在CPU和内存之间加缓存,多线程场景下就有了缓存一致性的问题。也就是每个核心的缓存中数据存在一致性问题。
处理器优化 指令重排
为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理, 处理器优化
除了处理器会对指令做乱序处理之外,很多语言也会对指令进行指令重排优化。
并发编程的问题
对并发问题进行抽象之后,得出了并发编程的3个关键问题:
- 原子性 简单理解一个操作在cpu中不可以中断,要么执行完成,要么不完成,没有中间结果。
- 可见性 指多线程环境下,一个线程对变量的修改对其他线程是立即可见的。
- 有序性 指程序执行顺序和代码顺序一致。
对应起来就是 cpu缓存 -> 可见性。 处理器优化-> 原子性 指令重排 -> 有序性
什么是JAVA内存模型
为了解决上述的三个问题,保证共享内存的正确性,内存模型定义了共享内存系统中的多线程处理内存读写的规范。
主要的解决方式包括两种:
-
限制处理器优化
-
内存屏障(英语:Memory barrier),也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,它使得 CPU 或编译器在对内存进行操作的时候, 严格按照一定的顺序来执行, 也就是说在内存屏障之前的指令和之后的指令不会由于系统优化等原因而导致乱序。
大多数现代计算机为了提高性能而采取乱序执行,这使得内存屏障成为必须。
语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障
JAVA内存模型
java内存模型其实就是内存模型规范的一种实现。屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。PDF
JAVA 规定所有的变量都在主存中,每一个线程有自己的工作内存,工作内存是主存变量的拷贝,线程对变量的操作都在线程内存中操作,不能直接写主存。不同线程的工作内存是隔离的,变量传递需要在工作内存和主存之间通过数据同步进行。
JMM的实现
JMM定义了规范,同时也提供了很多同步原语,对系统封装之后提供给使用者。比如并发相关的实现:volatile
synchronized
final
JUC包
等。
对应上述三个抽象问题:
原子性
java通过monitors
来解决同步问题。 java的每一个对象都关联了monitor
,线程可以通过操作monitor
来进行 lock
unlock
操作。 java提供了2个字节码 monitorenter
monitorexit
来保证原子性。其对应的关键字就是synchronized
。
可见性
java线程对变量的修改发生在工作内存,然后同步到主存。在读取时从主存中刷新变量值到工作内存中。
volatile
保证其修饰的变量在被修改后会立即同步到主存中,在每次读取的时候都是从主存中刷新。以此来保证可见性。
初次之外 synchronized
final
也能保证可见性。
有序性
可以通过 volatile
synchronized
来保证有序性。具体实现机制不同。
volatile
通过禁止重排序来实现。
synchronized
通过保证单线程操作来保证有序性。