深入理解Volatile关键字
在仔细讲解Java的volatile关键字之前优先回顾一下并发的特征,并发性的特征一共有三个:原子性,可见性,有序性。
-原子性
如果一个操作是不可分割的,那这就是一个原子操作。相反,如果一个操作是可以分割的,那么他就是非原子操作,
eg:a++是线程安全的操作吗?
答:不是。当有多个线程同时操作a++这个过程时,a++的操作可以分割成int a;a=0;a+1;这三步操作,在每一次操作被调用的时候都会发生错误,所以它是非线程安全操作
-可见性
一个变量被多个线程共享,如果一个线程修改了这个变量的值,其他的线程会立马得知这个修改,我们称这个操作具有可见性
-有序性
有序性两方面表现:
1、在一个线程内存观察,所有的操作都是有序来的,所有的执行指令按照”串行“(as-if-serial)
2、在线程间观察,从一个线程观察其他线程,则线程的执行时交替执行,是正序的
紧接着,让我们来了解一下什么是Java内存模型。
Java内存模型
线程局部变量表
在Java内存模型中,虚拟机栈(本地方法栈)是线程私有,存储的是局部变量表、动态链接、方法的出口.dll 动态链接文件 ,虚拟机栈中存在一个个帧栈(对象存在一个个方法)
对象是存在于堆中
局部变量表:主要存储的数据:
基本的数据类型:存储在堆中的数据会拷贝一份到局部变量中 Integer
对象的引用:局部变量表中不存储对象,对象存在堆中,在局部变量中只存在对对象的引用地址
内存模型
堆内存中的对象和基本数据类型的备份,称为主内存(main memory),把上面所说的栈内存中用于存储变量的部分内存,称为本地内存(local memory)(或叫工作内存)
1、Java线程对于变量的操作,都是在自己的工作内存中进行的,线程不会直接读写主内存的变量
2、不同的线程无法访问对方线程工作内存中的变量
3、线程间变量的传递,需要主内存来完成的
主内存存在变量count = 10;
线程A和线程B来操作count,都会拷贝副本到本地内存?
当前线程A 执行count= 14;将结果写到主内存,此时线程B已经读取了count ,此时修无法读取最新值,会存在问题。
Java内存模型中对数据的操作存在8中操作:
Volatile关键字
示例:
private static boolean flag = true;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int count = 0;
while (flag) {
count++;
}
System.out.println("count :" + count);
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//1秒钟后将标志位修改为false
flag = false;
System.out.println("1秒钟结束");
}
});
thread.start();
thread1.start();
在不加volatile关键时,变量flag修改无法实时同步给另一个线程?
线程堆栈中保存线程运行时的变量值的副本,**当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,**然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,也就是 在读取登录之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样
volatile特征
禁止指令的重排序
代码重排序的问题?
代码在执行过程中,为了提高代码的执行效率,会对代码做优化,编译、字节码,机器码、汇编都会对代码进行优化
优化前:
~~int i ;~~
//do something
~~i = 10;~~
优化后:
int i=10;
//do something
//代码优化的原则是优化前后是不会来影响执行结果
Java内存模型不会对volatile指令进行重排序,从而保证对volatile变量的执行顺序,永远是按照其出现顺序执行的。重排序的依据是happens-before法则
保证内存的可见性
volatile通过一定的机制保证主内存和工作内存中的数据具有实时感知最新的变化
volatile工作原理
在volatile关键字所修饰的变量时,在汇编层代码上,会添加一个lock前缀的指令
Lock前缀指令相当于添加了一个内存屏障,内存屏障提供的功能:
1、它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2、它会强制将对缓存的修改操作立即写入主存;
3、如果是写操作,它会导致其他CPU中对应的缓存行无效。
注意:volatile保证共享数据的可见性,有序性,却无法保证数据的原子性
public class VolatileTest {
public volatile static int count = 0;
public static void main(String [] args){
//开启10个线程
for(int i = 0;i < 10; i++){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} 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= " + count);
}
}
这段代码是什么意思呢?
很简单,开启10个线程,每个线程当中让静态变量count自增100次。执行之后会发现,最终count的结果值未必是1000,有可能小于1000。
使用volatile修饰的变量,为什么并发自增的时候会出现这样的问题呢?
这是因为count++这一行代码本身并不是原子性操作,在字节码层面可以拆分成如下指令:
getstatic //读取静态变量(count)
iconst_1 //定义常量1
iadd //count增加1
putstatic //把count结果同步到主内存
虽然每一次执行 getstatic 的时候,获取到的都是主内存的最新变量值,但是进行iadd的时候,由于并不是原子性操作,其他线程在这过程中很可能让count自增了很多次。
这样一来本线程所计算更新的是一个陈旧的count值,自然无法做到线程安全。
volatile的应用场景
1、一般用来修饰Boolean类型的共享状态标志位
eg:统计一秒钟内,count++的次数时,用volatile来修饰线程1 和线程2的共享标志位flag。
private static volatile boolean flag=true;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
int count=0;
@Override
public void run() {
while (flag){
count++;
System.out.println(count+" 次数");
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag=false;
}
});
thread1.start();;
thread.start();
}
2、单例模式下的双重校验锁
eg:防止指令重排!
private DlcLzayMan(){
}
private volatile static DlcLzayMan dlcLzayMan;
public static DlcLzayMan getInstance(){
if (dlcLzayMan==null){
synchronized (DlcLzayMan.class){
if (dlcLzayMan==null){
dlcLzayMan = new DlcLzayMan();//不是原子性操作
/**
* 1.分配内存空间
* 2.执行构造方法,初始化对象
* 3.把这个对象指向这个空间
* 有可能发生指令重排现象
*/
}
}
}
return dlcLzayMan;
}
3、修饰单个的变量
修饰变量,
vaoltile String str = new String(“Hello”);
volatile修饰对象的引用时,只对引用地址能够实时感知变化,无法保证对象的可见性
注意点:
volatile对于基本数据类型(值直接从主内存向工作内存copy)才有用。
但是对于对象来说,似乎没有用,因为volatile只是保证对象引用的可见性,而对对象内部的字段,它保证不了任何事。即便是在使用ThreadLocal时,每个线程都有一份变量副本,这些副本本身也是存储在堆中的,线程栈桢中保存的仍然是基本数据类型和变量副本的引用。
所以,千万不要指望有了volatile修饰对象,对象就会像基本数据类型一样整体呈现原子性的工作了。
事实上,如果一个对象被volatile修饰,那么就表示它的引用具有了可见性。从而使得对于变量引用的任何变更,都在线程间可见。