在介绍volatile之前,先来说说一下几个相关的概念
一、相关概念解释
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
可见性:一个线程对共享变量值的修改,能够及时地被其他线程看到
共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量
重排序:代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能儿做的优化
1.编译器优化的重排序(编译器优化)
2.指令级并行重排序(处理器优化)
3.内存系统的重排序(处理器优化)
如:代码的顺序为
int number =1;
int result =0;
而实际执行顺序可能为:
int result = 0;
int number = 1;
Java内存模型:Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JCM中将变量存储到内存和从内存中读取变量这样的底层细节。
如图所示:共享变量x,所有的变量都存储在主内存中,每个线程都有自己独立的工作内存,里面保存该线程使用到的变量的副本(主内存中该变量的一份拷贝)
两条规定:
1)线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写
2)不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要通过主内存来完成
共享变量可见性实现的原理
线程1对共享变量的修改要被线程2及时看到,必须要经过如下2个步骤:
1.把工作内存1中更新过的共享变量刷新到主内存中
2.将主内存中最新的共享变量的值更新到工作内存2中
二、Volatile关键字
首先被volatile修饰的变量能够保证可见性,但不能保证变量复合操作的原子性。
volatile是如何实现可见性的:
深入来说:通过加入内存屏幕和禁止重排序优化来实现的
对volatile变量执行写操作时,会在写操作后加入一条store屏幕指令。对volatile变量执行对操作时,会在读操作前加入一条load屏幕指令。
通俗地讲:volatie变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样任何时刻,不同的变量总能看到该变量的最新值。
线程写volatile变量的过程:
1.改变线程工作内存中volatile变量副本的值
2.将改变后的副本的值从工作内存刷新到主内存
线程读volatile变量的过程:
1.从主内存中读取volatile变量的最新值到线程的工作内存中
2.从工作内存中读取volatile变量的副本
volatile不能保证volatile变量复核操作的原子性:
如:private int number = 0; number ++; 不是原子操作。number++其实可以分解为3个操作步骤:
1)读取number的值
2)将number的值加1
3)写入最新的number的值
package com.imooc.demo.hello;
public class VolatileDemo {
// 被volatile修饰的变量
private volatile int number = 0;
public int getNumber(){
return this.number;
}
public void increase(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.number++;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
final VolatileDemo volDemo = new VolatileDemo();
for(int i = 0 ; i < 500 ; i++){
new Thread(new Runnable() {
@Override
public void run() {
volDemo.increase();
}
}).start();
}
//如果还有子线程在运行,主线程就让出CPU资源,
//直到所有的子线程都运行完了,主线程再继续往下执行
while(Thread.activeCount() > 1){
Thread.yield();
}
System.out.println("number : " + volDemo.getNumber());
}
}
运行结果:
如果程序正常执行的话,number的值应该为500,而在某个时刻,number的值却为493。这是为什么呢?
我们不妨假设初始时number=5;现在有A、B两个线程,而实际它们的运行过程可能是这样的:
1)线程A读取number的值
2)线程B读取number的值
3)线程B执行加1操作
4)线程B写入最新的number值
5)线程A执行加1操作
6)线程A写入最新的number值
预期的话,两次number=7,而如果按照上述步骤执行,两次number++只增加了1,这也就是为什么程序执行结束后number的值为493了,可见volatile关键字并不能保证复合操作的原子性。
那么有哪些方式可以保证原子性操作呢?
1)使用synchronize关键字
2)使用ReetrantLock(java.util.concurrent.locks包下)
3)使用AtomicInterger (java.util.concurrent.atomic包下)
这并不是说volatile关键字没有用了,哪些场合适合使用volatile关键字呢?
volatile使用场合:
要在多线程中安全的使用volatile变量,必须同时满足:
1.对变量的希尔操作不依赖当前值
不满足:number++、count = count*5等
满足:boolean变量、记录温度变化的变量等
2.该变量没有包含在具有其他变量的不变式中
不满足:不变式 low<up
synchronized和volatile比较
1.volatile不需要加锁,比synchronized更轻量级,不会阻塞线程;
2.从内存可见性角度讲,volatile读相当于加锁,volatile写相当于解锁
3.synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子