内存模型的相关概念
执行程序的大概过程:
计算机在执行程序的时候,相关指令在CPU中执行,而相关指令的执行,会先将内存中的核心数据加载到CPU,然后CPU通过指令进行相关的运算之后再重新写入内存。 (硬盘->内存->缓存(L1,L2,L3)->CPU) 由于程序在运行过程中,临时数据都是存储在主存中(物理内存),但是由于CPU运行速度(执行指令)远比从内存中读取和写入数据时的速度快,如果这个时候每次执行指令之前都要去内存中读取数据,就会大大降低CPU的执行效率。因此这个时候科学家们就想到在CPU和内存之间加入高速缓存(Cache),而为了尽可能的提升CPU的执行效率,会加入多级缓存。缓存的空间是大于CPU小于内存的,而在缓存中读取数据的速度是大于在内存中读取数据的速度但是还是小于CPU的执行速度。但是已经相比之前的在内存中读取数据有了很大的提升。
Java内存模型
Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。注意:工作内存不是真正的内存,可以对比缓存+寄存器理解。 线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
Volatile关键字
当使用volatile去申明一个变量时,就等于告诉了虚拟机这个变量极有可能会被某些程序或者线程修改。为了确保这个变量被修改后,应用程序中的其他的线程都可以看到这个改动,虚拟机就必须采用一些特殊的手段,保证这个变量的可见性等特点。详细过程就是,volatile修饰的一些变量,这些变量在被某些程序或者线程修改后,要进行写操作时,JVM就会向处理器发送一条Lock前缀指令,将这个变量所在的缓存行的数据写回系统内存,但是就算写回到内存,如果其他处理器缓存的数据还是旧的,在执行计算操作得到的运算结果就会有问题。在多处理器的情况下,为了保证各个处理器缓存是一致的,就会实现缓存一致性协议。每个处理器通过嗅探在总线上传播的数据来检查自己的缓存值是不是过期了。当处理器发现自己的缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据再次进行修改时,就会重新从系统内存中把数据读到处理器的缓存里。
例如:i=10;i++;i++;
1.对于单线程来说: 并不会出现问题,因为哪怕编译器优化,对于一次i++之后,寄存器中的i=11,直接读取寄存器中的值,再次执行i++,i更新为12,是我们想要得到的结果。
2.对于多线程来说: 就会出现下面的结果,线程1和线程2最开始从内存中读取i=10记录在工作内存中(也就是缓存或者寄存器),然后线程1执行一次i++,得到i=11,然后将i=11写回内存,更新内存中i变量的值,但是此时线程2读取数据还是在工作内存中(也就是缓存或者寄存器),读取的之前最开始为更新内存的i=10,再执行i++,得到i=11,然后写入内存,最终结果是i=11。但是这并不是我们想得到的结果。
这就是著名的缓存一致性问题。通常称这种被多个线程访问的变量为共享变量。
Volatile的总结
1.volatile保证了内存的可见性:
当一个变量被volatile修饰,保证了可见性,某一线程去修改了这个变量时,能保证修改后的值能立即被写回主存更新值,当其它线程想要去读取变量时,就要去主存中读取更新后的变量值。而普通变量不能保证可见性,因为普通变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
2.volatile能够禁止指令重排序:
例如:new 一个实例
new操作的本质也是分成三个顺序:
1.申请内存,得到内存首地址;
2.调用构造方法初始化实例;
3.把内存首地址赋值给对象引用;
站在单线程角度是,2,3是可以互换顺序的,执行顺序2,3谁前谁后都无所谓。但是如果是多线线程情况,线程A先执行1,3,未执行2也就是还没有初始化实例,只有一个空对象。此时如果线程B执行又去执行一个方法,要对这个对象进行判断,此时虽然对象内无数据,但是有对象引用的,后续如果对这个对象进行解引用操作就危险了。
3.volatile不能保证原子性:
对于一些非原子性的操作,还是需要加锁来保证线程的安全,类似多线程里的count++等操作。