一、Java内存模型
Java内存模型简称JMM (Java Memory Model),是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。
主内存(Main Memory):主内存被所有的线程所共享,对于一个共享变量(比如静态变量,或是堆内存中的实例)来说,主内存当中存储了它的“本尊”。
工作内存(Working Memory):每一个线程拥有自己的工作内存,对于一个共享变量来说,工作内存当中存储了它的“副本”。
工作内存类似高速缓存、Cache,而主存类似于内存,直接操纵主内存速度太慢,所以JVM更倾向于在工作内存操纵线程。线程对共享变量的所有操作都必须在工作内存进行,不能直接读写主内存中的变量。不同线程之间也无法访问彼此的工作内存,变量值的传递只能通过主内存来进行。
JMM工作流程:定义一个静态变量 static int s = 0;线程A执行操作将s修改为3:s = 3;
1)、Thread A先进行读操作,读取主内存 s 值,此时s = 0 ,并更新工作内存s值, s = 0;
2)、在工作内存进行写操作,将s 由 0 写为 3;
3)、同步更新主内存s值,在主内存进行写操作,将s 由 0 改为 3;
通过一系列内存读写的操作指令,线程A把静态变量 s=0 从主内存读到工作内存,再把 s=3 的更新结果同步到主内存当中。从单线程的角度来看,这个过程没有任何问题。此时引入线程B,则结果将会出现两种状态:① Thread B 读出 s = 0, ②Thread B 读出 s = 3。
出现上述结果原因时在,Thread A 进行同步主内存 s = 3 写操作前 ,Thread B 进行主线程的 s 值读取操作,则此时得到结果为s = 0,而当Thread A 进行同步主内存 s = 3 写操作后,Thread B 进行主线程的 s 值读取操作,则此时得到结果为s = 3。
由于 Synchronized 锁会影响程序性能,所以轻量级锁 volatile 出现了。
二、Volatile
volatile关键字其中最重要的特性就是保证了用 volatile 修饰的变量对所有线程的可见性。可见性指当一个线程修改了变量的值,新的值会立刻同步到主内存当中。而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。volatile 关键字特性得益于java语言的先行发生原则(happens-before)。
先行发生原则:两个事件的结果之间的关系,如果一个事件发生在另一个事件之前,结果必须反映,即使这些事件实际上是乱序执行的(通常是优化程序流程)。这里的事件指各种指令操作,比如读操作、写操作、初始化操作、锁操作等等。先行发生原则作用于很多场景下,包括同步锁、线程启动、线程终止、volatile。我们这里只列举出volatile相关的规则:对于一个volatile变量的写操作先行发生于后面对这个变量的读操作。
回到上述的代码例子,如果在静态变量s之前加上volatile修饰符:volatile static int s = 0;线程A执行如下代码:s = 3;这时候我们引入线程B,执行如下代码:System.out.println("s=" + s); 当线程A先执行的时候,把 s = 3 写入主内存的事件必定会先于读取s的事件。所以线程B的输出一定是 s = 0。
volatile 只保证变量的可见性,并不能保障变量的原子性,请看一下代码:
package com.demo.sync;
public class VolatileTest {
public volatile static Integer count = 0;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e){
e.printStackTrace();
}
//每个线程将count自增100次;
for (int j = 0; j < 100; j++) {
count++;
}
}
}).start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(count);
}
}
结果:开启10个线程,每个线程当中让静态变量count自增100次。执行之后会发现,最终count的结果值未必是1000,有可能小于1000,如下图。
分析:count++这一行代码本身并不是原子性操作,在字节码层面可以拆分成如下指令:
getstatic //读取静态变量(count)
iconst_1 //定义常量1
iadd //count增加1
putstatic //把count结果同步到主内存
虽然每一次执行 getstatic 的时候,获取到的都是主内存的最新变量值,但是进行iadd的时候,由于并不是原子性操作,其他线程在这过程中很可能让 count 自增了很多次。这样一来本线程所计算更新的是一个陈旧的count值,自然无法做到线程安全:
Volatile适用场景:
1.运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
2.变量不需要与其他的状态变量共同参与不变约束。
volatile static int start = 3;
volatile static int end = 6;
//线程A执行如下代码:
while (start < end){
//do something
}
//线程B执行如下代码:
start+=3;
end+=3;
这种情况下,一旦在线程A的循环中执行了线程B,start有可能先更新成 6,造成了一瞬间 start == end,从而跳出while循环的可能性。