Volatile详解
内存模型概念
内存模型描述了程序中各个变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节,对象最终是存储在内存里面的,但是编译器、运行库、处理器或者系统缓存可以有特权在变量指定内存位置存储或者取出变量的值。【JMM】允许编译器和缓存以数据在处理器特定的缓存(或寄存器)和主存之间移动的次序拥有重要的特权,除非程序员使用了final或synchronized明确请求了某些可见性的保证。在Java中应为不同的目的可以将java划分为两种内存模型:gc内存模型。并发内存模型。
程序计数器
程序计数器用于记录某个线程下次执行指令位置。程序计数器也是线程私有的。
虚拟机栈/本地方法栈
在栈中,会为每一个线程创建一个栈。线程越多,栈的内存使用越大。对于每一个线程栈。当一个方法在线程中执行的时候,会在线程栈中创建一个栈帧,用于存放该方法的上下文(局部变量表、操作数栈、方法返回地址等等)。每一个方法从调用到执行完毕的过程,就是对应着一个栈帧入栈出栈的过程。
本地方法栈与虚拟机栈发挥的作用是类似的,他们之间的区别是虚拟机栈为虚拟机执行java方法服务的,而本地方法栈是为虚拟机执行native方法服务的。
堆
几乎所有的对象/数组的内存空间都在堆上(有少部分在栈上)。在gc管理中,将虚拟机堆分为永久代、老年代、新生代。通过名字我们可以知道一个对象新建一般在新生代。经过几轮的gc。还存活的对象会被移到老年代。永久代用来保存类信息、代码段等几乎不会变的数据。堆中的所有数据是线程共享的。
方法区
方法区就是在堆中称为永久代的堆区域。
并发编程的三大特性
原子性
原子操作:不可分割的操作,中间不会被其他线程打断,不需要同步操作
多个原子操作合起来就需要使用到同步
原子性指的是一次操作或者多个操作,要么全部执行,要么全部都不执行
例如:
int a = 10 ; //原子操作
a++; // 不是原子操作
int b = a; //不是原子操作
a = a+1; // 不是操作
可见性
当在一个线程中队某一共享变量进行了修改,那么另外的线程可以立即看到修改后的新值。除了volatile之外,Java还有两个关键字能实现可见性,即synchronized和final.同步块的可见性是由“对一个变量执行unlock操作前,必须先把此变量同步回主内存”这条规则获得的,而final关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把"this"的引用传递出去,那么在其他线程中就能看见final字段的值。
可序性
程序代码在执行过程中的先后顺序
单线程环境指令重排序不会影响最终的结果
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行的进入。
缓存一致性问题
每个线程都有自己的工作内存,对应cpu cache,比如有两个线程A B,执行i++操作,线程A将i放入自己工作内存,得到i的副本,线程B执行相同操作,首先线程A将i读取到自己的工作内存,没来得及加1,线程B开始执行加1操作,i=1;随后线程A中的i执行加1,i=1;由于线程执行了两次自增,而结果却为1,所以在多线程情况下,缓存就会出现缓存不一致的问题。
解决方法
解决计算机中cpu、cpu cache、计算机主存所引起的缓存一致性问题
a. 通过总线加锁解决
b. 通过缓存一致性协议 MESI协议
b1.读取操作,不会做任何处理
b2.写入操作,发出信号通过其他cpu将cpu cache中副本置为无效状态,其他cpu在进行该变量读取的时候不得不到主内存中去再次获取
Java内存模型
java内存模型中规定了所有变量都存贮到主内存(如虚拟机物理内存中的一部分)中。每一个线程都有一个自己的工作内存(如cpu中的高速缓存)。线程中的工作内存保存了该线程使用到的变量的主内存的副本拷贝。线程对变量的所有操作(读取、赋值等)必须在该线程的工作内存中进行。不同线程之间无法直接访问对方工作内存中变量。线程间变量的值传递均需要通过主内存来完成。
Volatile 内存屏障
在多线程中
volatile关键字解决类似于cpu缓存一致性问题
线程1 | 线程2
线程1的工作内存 | 线程2的工作内存
(x的副本) | (x的副本)
主内存(共享变量x)
x++
unsafe.cpp源码中,volatile修饰的变量存在一个"lock."的前缀
"lock."相当于是一个内存屏障
例:
int x = 10;
int y = 10;
volatile int z = 30;
x++;
y–;
具体保障:
1)确保指令重排序时不会将内存屏障后面的排到内存屏障之前
2)确保指令重排序时不会将内存屏障前面的排到内存屏障之后
3)确保在执行到内存屏障修饰的指令时,前面的代码全部执行完成
4)强制将线程工作内存中的的值刷新到主内存中
5)写操作时,会导致其他线程(使用共享变量)工作内存中的缓存数据会失效,下一次 就需要去主内存中再次读取。
Volatile关键字
原理及实现机制
Volatile可见性
用Volatile修饰的变量,值改变过后,副本中的变量将置为无效状态,其他线程在进行该变量读取的时候不得不到主内存中去再次获取
private volatile static int initValue = 0;
private final static int MAX = 5;
public static void main(String[] args) {
new Thread("reader") {
@Override
public void run() {
int localValue = initValue;
while (localValue < MAX) {
if (initValue != localValue) {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("The initValue is updated to " + initValue);
localValue = initValue;
}
}
}
}.start();
new Thread("updater") {
@Override
public void run() {
int localValue = initValue;
while (localValue < MAX) {
//修改initValue
System.out.println("The initValue will be change to " + (++localValue));
initValue = localValue;
//短暂休眠,目的是为了使得Reader线程及时输出变化的initValue
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
Volatile 不可保证原子性
private static CountDownLatch countDownLatch = new CountDownLatch(10);
public volatile static int i = 0;
public static void increase(){
i++;
/**
* i++分为3步骤:
* 1)从主内存获取i的值,缓存到当前线程的工作内存
* 2)在线程工作内存对i进行+1
* 3)将i的最新值写入到主内存中
*/
/** 0
* 线程A:1)-》线程切换到B -》2) -》 3) 1
* 线程B:1) -> 2) -> 线程上下文切换 -》3) 1
*/
}
public static void main(String[] args) {
for(int i=0; i<10; i++) {
new Thread() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
increase();
}
//将计数器的值减1
countDownLatch.countDown();
}
}.start();
}
//调用await方法使得该线程阻塞,直到计数器为0才会执行
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(i);
}
运行结果:
如满足原子性,运行结果为10000,而实际运行结果不为10000,所以Volatile不可保证原子性
Volatile 可序性
unsafe.cpp源码中,volatile修饰的变量存在一个"lock."的前缀
"lock."相当于是一个内存屏障,保证了Volatile 的可序性。
使用场景及作用
使用场景:
- 1)修饰boolean类型的值
- 2)状态标记利用顺序性
- 3)单例模式
作用:
- 1)防止指令重排序
- 2)解决缓存一致性问题