前言
- java 5之前这个关键字备受争议,java5只有volatile才得以重生
- 因为volatile和java的内存模型有关(想弄清楚volatile就必须弄清相关的内存模型)
java相关的内存模型的概念
- 程序运行时临时数据存放在主存(物理内存中)
- cpu 在告诉运作时cpu存在高速缓存
- 就存在高速缓存和内存之间的数据交互
- 问题:数据的高并发同步问题
并发编程的三个概念
-
原子性
一个操作或者多个操作,要么全部执行,要么都不执行称为一个程序的原子性 -
可见性
多个线程同时访问一个变量,当一个线程改变了这个变量的值,其他线程能够立即 看到修改的值 -
有序性
程序执行的顺序按照代码的编写顺序执行(因为在代码执行阶段,处理器会对代码进行重排,但是这并不影响代码的最终执行结果)
java 内存模型
从代码层面分析前面提的三个概念
- 分析代码的原子性操作
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。
语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。
所以上面4个语句只有语句1的操作具备原子性。
总结:
java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和lock来实现,(因为锁可以保证同一时刻只有一个线程执行该代码块,从而保证了原子性)
-
可见性
1、对于可见性,volatile关键字来保证可见性
当一个共享变量被volatile修饰时,他会保证修改的值会立即被更新到主存,当有其他线程读取时,他会去主存内从新读取
2、而普通的共享变量不能保证可见性,因为不能保证什么时候写入朱存
3、利用锁可以实现变量的可见性,因为在synchronized和lock能保证同一时刻只有一个线程获取锁执行同步代码,并且在释放锁之前会将变量的修改刷新到朱存当中来保证可见性; -
有序性
在java内存模型中,允许编译器和处理器对指令进行重排,但是重排过程不会影响单线程的执行,却会影响多线程的并发执行顺序
1、 java中 可以通过volatile关键字来保证有序性
2、当然也可通过synchronized和lock来保证有序性
3、java 先天存在happens-before原则,
剖析volatile的关键字
一旦一个变量被volatile修饰,就具备以下两种含义
1、 保证了不同线程对于这个变量进行操作时的可见性
2、禁止指令重排
注意:维度不能保障原子性,
首先先看个代码
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
很多人都会这么干,但是这个会不会出错呢?答案是会出错的,因为线程中的数据和全局的数据是一样的吗?
解决的话 就是在stop 前面添加volatile关键字,他会在线程刷新数据的时候立马改变共享内存的数据
所以说:volatile无法保证原子性
-
volatile保障原子性?
无法保障
如果想保障程序的原子性 可依靠sychronized或者lock 来实现 -
volatile保障可见性
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
这串代码就能保证最后的结果是10000吗?答案也是no
因为虽然说线程在执行的时候会立马更新主存的数据,但是数据的++本身分为两个步骤,所以无法达到理想效果
1、可采用java atomic 包下面的 atomicinteger 的增加来计算
2、利用synchronized 和 lock 来实现原子性
总结:volatile实现可见性,是一个线程(cpu)的数据改变,他会更新主存的数据改变,同时他会让其他线程(cpu)的数据失效。
- volatile保证有序性
线程中的顺序 会以volatile为分界,按顺序执行
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
volatile flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
分析:1、2是一组 谁在前谁在后不一定
但是一定会在3前,同理,3肯定在4、5的前面,
4、5谁在前谁在后不一定
volatile的应用场景
- synchronizd关键字是防止多个线程同时执行一段代码,那么他会很影响程序执行的效率,而volatile关键字在某些方面优于synchronized,但是注意volatile关键字无法替代synchronized关键字的,因为volatile无法保证程序的原子性,
- 通常使用volatile有以下2个条件:
1、对变量的改变不依赖于当前值
2、对变量的依附不依赖于其他对象
示例:
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
一般都是作为多线程标记用的
网友评论