1 谈谈对 Volatile 的理解
volatile 应用于多线程环境下;
volatile 是JVM提供的轻量级的同步机制;
volatile 修饰的变量 保证可见性、不保证原子性、禁止指令重排
- 可见性:多个线程操作同一个公共资源时,其中一个线程修改了这个资源,其他线程可以第一时间就知道修改信息。
- 原子性:不可分割,即某个线程在做某个具体任务时,中间不可以被加塞或者被分割。整体要么都成功要么都失败
- 指令重排:多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致是无法确定的,结果无法预测
一个验证可见性的 Demo
class Demo {
public static void main(String[] args) {
//资源类
Date date = new Date();
new Thread(() ->{
System.out.println(Thread.currentThread().getName() + "线程开始执行");
// 线程睡眠3秒
try {
TimeUnit.SECONDS.sleep(3);
date.setNumber();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"A").start();
//模拟线程B:一直在这里等待循环,直到 number 的值不等于零
while (date.number == 0){
}
//只要变量的值被修改,就会执行下面的语句
System.out.println(Thread.currentThread().getName() + "执行结束");
}
}
class Date{
//volatile 保证可见性
volatile int number;
public void setNumber(){
number = 60;
}
}
过程解读
- 线程 a 从主内存读取 共享变量 到对应的工作内存
- 对共享变量进行更改
- 线程 b 读取共享变量的值到对应的工作内存
- 线程 a 将修改后的值刷新到主内存,失效其他线程对 共享变量的副本
- 线程 b 对共享变量进行操作时,发现已经失效,重新从主内存读取最新值,放入到对应工作内存。
一个验证不保证原子性的 Demo
public class Demo2 {
public static void main(String[] args) {
Date2 date2 = new Date2();
//开启20个线程
for(int i = 0;i < 20;i++){
new Thread(() -> {
//每个线程执行1000次++操作
for (int j = 0;j < 1000;j++){
date2.setNumberPlus();
}
},String.valueOf(i)).start();
}
//让20个线程全部执行完
while (Thread.activeCount() > 2){ //main + GC
//礼让线程
Thread.yield();
}
//查看最终结果
System.out.println(date2.number);
}
}
class Date2{
volatile int number;
public void setNumberPlus(){
//让其自增
number++;
}
}
过程解读
- 假设现在共享变量值为10,线程A 从主内存中读取数值到自己的工作内存,还没有来得及自增,CPU 调度切换到了线程B;
- 此时线程B 读取主内存中的数值,仍然是10,完成自增后,还来得及写回主内存,CPU 调度又切换回线程A ,此时线程A自增;
- 线程A 写回主内存值为11
- 线程B 写回主内存值为11
- 此时的结果就是2个线程只进行了1次修改
如何才能保证原子性
1、使用synchronized,不建议使用
2、使用AtomicInteger
代替int/Integer
,同时方法也相应的改变
public class Demo3 {
public static void main(String[] args) {
Date3 date3 = new Date3();
//开启20个线程
for(int i = 0;i < 20;i++){
new Thread(() -> {
//每个线程执行1000次++操作
for (int j = 0;j < 1000;j++){
date3.setAtomic();
}
},String.valueOf(i)).start();
}
//让20个线程全部执行完
while (Thread.activeCount() > 2){ //主线程 + GC
Thread.yield();//礼让线程
}
//查看最终结果
System.out.println(date3.number);
}
}
class Date3{
//创建一个原子 Integer 包装类,默认为0
AtomicInteger number = new AtomicInteger();
public void setAtomic(){
//相当于 atomicInter ++
number.getAndIncrement();
}
}
什么是指令重排
为了提高性能,JVM在执行代码时会经过以下过程
单线程环境里保证最终执行结果和代码顺序的结果一致。
处理器在进行指令重排时,要考虑到指令之间数据的依赖关系
在多线程环境中,由于编译器优化重排,两个线程在使用的变量能否保住一致性是无法确定的,结果无法预测 。
2 Volatile 的使用场景举例
单例模式中的DCL(双端检查机制)
public class Singleton6 {
//2.提供静态变量保存实例对象
private volatile static Singleton6 INSTANCE;
//1.私有化构造器
private Singleton6(){}
//3.提供获取对象的方法
public static Singleton6 getInstance(){
//第一重检查:针对很多个线程同时想要创建对象的情况
if(INSTANCE == null){
//同步代码块锁定
synchronized (Singleton6.class){
//第二重锁检查(针对比如A,B两个线程都为null,第一个线程创建完对象,第二个等待锁的线程拿到锁的情况)
if(INSTANCE == null){
INSTANCE = new Singleton6();
}
}
}
return INSTANCE;
}
}
为什么要在这里加上 volatile
因为创建对象分为 3 步:
- 分配内存空间;
- 初始化对象
- 设置实例执行刚分配的内存地址【正常流程走:instance ! = null】
但是,由于这 3 步不存在数据依赖关系 ,所以可能进行重排序优化,造成下列现象: - 分配内存空间
- 设置实例执行刚分配的内存地址【instance ! = null 有名无实,初始化并未完成!】
- 初始化对象
所以当另一条线程访问 instance 时 不为null,但是 instance 实例化未必已经完成,也就造成线程安全问题!
3 JMM
Java内存模型:Java Memory Model,是一种抽象概念并不真实存在,描述的是一种规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
具体的JMM 规定如下:
- 所有 共享变量 储存于 主内存 中;
- 每条线程拥有自己的工作内存,保存了被线程使用的变量的副本拷贝;
- 线程对变量的所有操作(读,写)都必须在自己的 工作内存 中完成,而不能直接读写 主内存 中的变量;
- 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存中转来完成
JMM三大特性:
- 可见性
- 原子性
- 有序性
参考阳哥教学视频Java面试_大厂高频面试题_阳哥整理