volatile特性概述
volatile总体概览
在上节中,我们已经研究完了volatile可以实现并发下共享变量的可见性,除了volatile可以保证可见性外,volatile 还具备如下一些突出的特性:
volatile的原子性问题:volatile不能保证原子性操作。禁止指令重排序:volatile可以防止指令重排序操作。
volatile不保证原子性
所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中 断,要么所有的操作都不执行。volatile不保证原子性。
问题案例演示
public class VolatileAtomicThread implements Runnable {// 定义一个int类型的遍历private int count = 0 ;@Overridepublic void run() {// 对该变量进行++操作,100次for(int x = 0 ; x < 100 ; x++) { count++ ;System.out.println("count =========>>>> " + count);}}}public class VolatileAtomicThreadDemo {public static void main(String[] args) {// 创建VolatileAtomicThread对象VolatileAtomicThread volatileAtomicThread = new VolatileAtomicThread() ;// 开启100个线程对count进行++操作for(int x = 0 ; x < 100 ; x++) {new Thread(volatileAtomicThread).start();}}}
执行结果:不保证一定是10000
问题原理说明
以上问题主要是发生在count++操作上:
count++操作包含3个步骤:
从主内存中读取数据到工作内存对工作内存中的数据进行++操作将工作内存中的数据写回到主内存
count++操作不是一个原子性操作,也就是说在某一个时刻对某一个操作的执行,有可能被其他的线程打断。
假设此时x的值是100,线程A需要对该变量进行自增1的操作,首先它需要从主内存中读取变量x的值。由于CPU的切换关系,此时CPU的执行权被切换到了B线程。A线程就处于就绪状态,B线程处于运行状态线程B也需要从主内存中读取x变量的值,由于线程A没有对x值做任何修改因此此时B读取到的数据还是100线程B工作内存中x执行了+1操作,但是未刷新之主内存中此时CPU的执行权切换到了A线程上,由于此时线程B没有将工作内存中的数据刷新到主内存,因此A线程工作内 存中的变量值还是100,没有失效。A线程对工作内存中的数据进行了+1操作5)线程B将101写入到主内存线程A将101写入到主内存
虽然计算了2次,但是只对A进行了1次修改。
volatile原子性测试
代码测试
// 定义一个int类型的变量private volatile int count = 0 ;
小结
在多线程环境下,volatile关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境 下volatile修饰的变量也是线程不安全的)。
在多线程环境下,要保证数据的安全性,我们还需要使用锁机制。
问题解决
使用锁机制
我们可以给count++操作添加锁,那么count++操作就是临界区的代码,临界区只能有一个线程去执行,所以
count++就变成了原子操作。
public class VolatileAtomicThread implements Runnable {// 定义一个int类型的变量private volatile int count = 0 ;private static final Object obj = new Object();@Overridepublic void run() {// 对该变量进行++操作,100次for(int x = 0 ; x < 100 ; x++) { synchronized (obj) {count++ ;System.out.println("count =========>>>> " + count);}}}}
原子类
AtomicInteger
原子型Integer,可以实现原子更新操作
演示基本使用。案例改造
使用AtomicInteger对案例进行改造.
public class VolatileAtomicThread implements Runnable {// 定义一个int类型的变量private AtomicInteger atomicInteger = new AtomicInteger() ;@Overridepublic void run() {// 对该变量进行++操作,100次for(int x = 0 ; x < 100 ; x++) {int i = atomicInteger.getAndIncrement(); System.out.println("count =========>>>> " + i);}}}
禁止指令重排序
概述
什么是重排序:为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。
什么是重排序:为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。
原因:一个好的内存模型实际上会放松对处理器和编译器规则的束缚,也就是说软件技术和硬件技术都为同一个目标 而进行奋斗:在不改变程序执行结果的前提下,尽可能提高执行效率。JMM对底层尽量减少约束,使其能够发挥自身 优势。因此,在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序。一般重排序可以分为如下三 种:
编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器 可以改变语句对应机器指令的执行顺序;内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
重排序的好处
重排序可以提高处理的速度。
重排序问题案例演示
引入:重排序虽然可以提高执行的效率,但是在并发执行下,JVM虚拟机底层并不能保证重排序下带来的安全性等问题,请看如下案例:
public class OutOfOrderExecution {private static int i = 0, j = 0; private static int a = 0, b = 0;public static void main(String[] args) throws InterruptedException { int count = 0; // 计 数while(true) {count++; i = 0;j = 0;a = 0;b = 0;Thread one = new Thread(new Runnable() { @Overridepublic void run() { a = 1;i = b;}});Thread two = new Thread(new Runnable() { @Overridepublic void run() { b = 1;j = a;}});two.start();one.start();one.join();two.join();String result = "第" + count + "次( i= " + i + ", j= " + j + ")"; if (i == 0 && j == 0) {System.out.println(result); break;} else {System.out.println(result);}}}}
以上程序中的4行代码的执行顺序决定了最终 i 和 j 的值,在执行的过程中可能会出现三种情况如下:
但是有一种情况大家可能是没有发现的,经过测试如下:
现象分析
按照以前的观点:代码执行的顺序是不会改变的,也就第一个线程是a=1是在i=b之前执行的,第二个线程b=1是在j=a之前执行的。
发生了重排序:在线程1和线程2内部的两行代码的实际执行顺序和代码在Java文件中的顺序是不一致的,代码指令并 不是严格按照代码顺序执行的,他们的顺序改变了,这样就是发生了重排序,这里颠倒的 是 a = 1 ,i = b 以及j=a , b=1 的顺序,从而发生了指令重排序。直接获取了i = b(0) , j = a(0)的值!显然这个值是不对的。
volatile禁止重排序
volatile修饰变量后可以实现禁止指令重排序!volatile禁止重排序案例演示:
public class OutOfOrderExecution {// 使用volatile修改变量。private volatile static int i = 0, j = 0; private volatile static int a = 0, b = 0;public static void main(String[] args) throws InterruptedException {int count = 0; // 计 数while(true) {count++; i = 0;j = 0;a = 0;b = 0;Thread one = new Thread(new Runnable() { @Overridepublic void run() { a = 1;i = b;}});Thread two = new Thread(new Runnable() { @Overridepublic void run() { b = 1;j = a;}});two.start();one.start();one.join();two.join();String result = "第" + count + "次( i= " + i + ", j= " + j + ")"; if (i == 0 && j == 0) {System.out.println(result); break;} else {System.out.println(result);}}}}
经过很长的测试都没有出现过重排序,从而实现了业务的安全性:
小结
使用volatile可以禁止指令重排序,从而修正重排序可能带来的并发安全问题。
领取专属 10元无门槛券
私享最新 技术干货