1、volatile概念:
volatile是Java虚拟机提供的轻量级的同步机制
2、volatile三大特性
2.1 保证可见性
从可见性分析需要引入JMM内存模型概念。
JMM(Java Memory Model)本身是一种抽象的概念,并不真实存在,他描述的时一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM 关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回 主内存
- 线程加锁前,必须读取主内存中的最新值到自己的 工作内存
- 加锁解锁是同一把锁
JMM 三大特性:
可见性、原子性、有序性
由于JVM 运行程序的实体是线程,而每个线程创建时JVM 都会为其创建一个栈空间,即**工作内存** 。工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在 主内存 ,主内存是共享内存区域,所有线程均可访问,但是线程对变量的操作(读写等)必须在工作内存中进行(首先将变量从主内存拷贝到自己的工作空间,然后对其进行操作,操作完成后再将变量写回主内存),不能直接操作主内存中的变量,各个线程的工作内存中存放着主内存中变量的拷贝副本,因此不同的线程间无法访问到对方的工作内存,线程之间的通信(传值)必须通过主内存来完成。
由于共享变量是储存在主内存中的,不同线程操作变量需要将其拷贝到自己的工作内存,某一线程在操作完成后再将结果重新写入到主内存,当共享变量发生改变时及时通知其他线程的这一现象就是 可见性 ,也就是线程之间要进行及时通知。
下面通过代码验证volatile的可见性:
package com.xzr.volatileDemo;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @ClassName: VolatileDemo1
* @Auther: 戏中人
* @Description: 验证volatile的可见性示例
*
* 1.验证可见性
* 1.1 变量n不添加volatile关键字
* 运行结果:main线程会一直处于死循环
* 1.2 变量n添加volatile关键字
* 运行结果:main线程可以立马知道n的改变,从而跳出死循环
*/
public class VolatileDemo1 {
public static void main(String[] args) {
Demo demo = new Demo();
List list = new ArrayList();
//线程A
new Thread(()->{
System.out.println(Thread.currentThread().getName() + "线程开始运行并等待5秒");
//暂停5秒线程A
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
demo.changeToTen();
System.out.println(Thread.currentThread().getName() + "线程更新n的值为:" + demo.n);
},"Thread-A").start();
//main线程
while (demo.n == 0) {
//如果n的值一直为0,那么main线程就是一直处于死循环
}
//只有当main线程知道了变量n的改变,才能跳出死循环并正常结束线程
System.out.println(Thread.currentThread().getName() + "线程结束并获取n的值为" + demo.n);
}
}
class Demo{
volatile int n = 0;
public void changeToTen() {
this.n = 10;
}
}
2.2 不保证原子性
synchronized可以保证原子性。
通过下面的程序进行验证volatile不保证原子性:
public class DemoClass {
volatile int n = 0;
public void add() {
n++;
}
}
package com.xzr.volatileDemo;
/**
* @ClassName: VolatileDemo2
* @Auther: 戏中人
* @Description: 验证volatile不保证原子性
*
* 1 什么是原子性?
* 即不可分割。在某个线程执行某个业务时,中间不允许被其他线程加塞中断或分割,需要完整性
*
* 2 volatile不保证原子性
* 30个线程分别对n=0进行1000次加1,最终结果会出现不是30000的情况,也就是不保证原子性
*/
public class VolatileDemo2 {
public static void main(String[] args) {
DemoClass demo = new DemoClass();
//这里的线程尽量大于20个,这样更容易看出原子性问题导致的错误的计算结果
for (int i = 0; i < 30; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
demo.add();
}
},String.valueOf(i+1)).start();
}
//等待这10个线程全部计算完成后,再通过main线程获取最终计算结果
//大于2是因为main线程和后台GC线程
while (Thread.activeCount() > 2) {
Thread.yield(); //暂停当前正在执行的线程对象,并执行其他线程。
}
System.out.println(Thread.currentThread().getName() + "最终结果:" + demo.n);
}
}
产生原因:
从代码来看,我们的预期结果是30000,但是实际运行时会出现小于30000的情况(当然也会出现恰好是30000的结果),这是因为 n++
这一操作是线程不安全的。为什么不安全?
从JMM内存模型可知,线程操作共享变量都是拷贝到自己的内存中进行计算后再写入主内存。现假设一种情况,线程A和线程B同时访问到主内存中n的值为0,并各自对工作内存中 n=0
进行加一操作后准备 将计算后的n值写回到主内存 ,这时A线程由于线程之间的竞争关系被挂起,并没有把n=1
这一结果写到主内存;而B线程成功把 n=1
写到主内存,这个时候由JMM内存模型通知其他线程 变量n更新后的值,可能就在这一瞬间,被挂起的线程A继续执行,但是还没有收到n值更新的通知就已经把 n=1
这一结果再次写入主内存,这样尽管线程A和线程B就都对n进行了加一操作,但实际上出现了某个线程的写操作无效 。
我们可以通过javap命令查看方法中 n++
的底层代码:
可以看出被分解为3个指令,getfield
获得原始的n值,执行 iadd
进行加1操作,执行 putfield
把累加后的值写回主内存。
解决原子性问题的办法:
-
第一种(不推荐):方法添加synchronized关键字
添加synchronized关键字。虽然原子性可以保证,但是性能上却有所降低,对于n++
这种操作,更推荐采用第二种方法 -
第二种:采用原子包装类AtomicInteger
public class DemoClass { volatile int n = 0; AtomicInteger atomicInteger = new AtomicInteger(); public void add() { n++; } public void atomicAdd() { atomicInteger.getAndIncrement(); //加一操作 } }
public class VolatileDemo2 { public static void main(String[] args) { DemoClass demo = new DemoClass(); //这里的线程尽量大于20个,这样更容易看出原子性问题导致的错误的计算结果 for (int i = 0; i < 30; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { demo.add(); //不保证原子性 demo.atomicAdd(); //保证原子性 } },String.valueOf(i+1)).start(); } //等待这10个线程全部计算完成后,再通过main线程获取最终计算结果 //大于2是因为main线程和后台GC线程 while (Thread.activeCount() > 2) { Thread.yield(); //暂停当前正在执行的线程对象,并执行其他线程。 } System.out.println(Thread.currentThread().getName() + "最终结果:" + demo.n); System.out.println(Thread.currentThread().getName() + "原子操作最终结果:" + demo.atomicInteger); } }
2.3 禁止指令重排
对应JMM模型的有序性。
计算机在执程序时,为了提高性能,编译器和处理器常常会对指令 做重排 ,一般分为以下三种:
源代码 ==> 编译器优化的重排 ==> 指令并行的重排 ==>内存系统的重排 ==> 最终执行的指令
单线程环境下确保程序最终执行结果和代码顺序执行结果一致即可;
处理器在进行重排时必须要考虑指令之间的 数据依赖性 问题;
多线程环境下线程是交替执行了,由于编译器优化重排的存在,两个线程中使用变量能否保证一致性是无法确定的,导致结果无法预测。
volatile实现 禁止指令重排优化 ,从而避免多线程环境下程序出现乱序执行的现象。
3、volatile使用举例:
单例模式中的volatile就是为了禁止指令重排,避免在多线程下出现线程不安全问题。