大多数的面试中,都会问到这个问题,基本成为了Java程序员必备的知识了。
本文带你一次性理清答题思路及扩展
目录
1. JMM(Java内存模型)
1.1 定义及规定
1.1.1 定义
JMM 本身是一种抽象的概念并不是真实存在,它描述的是一组规定或则规范,通过这组规范定义了程序中的访问方式。
1.1.2 规定
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
1.2 三大特性
1.2.1 可见性
线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存
一旦主内存中的变量发生改变,必须让所有的线程都能看见,第一时间通知,这就是可见性
1.2.2 原子性
1.2.3 有序性
2. volatile
1.1 volatile是什么?
volatile 是 Java 虚拟机提供的轻量级的同步机制
1.2 三大特性
他的特点和我们的JMM也有点类似,volatile 不保证原子性,保证可见性和禁止指令重排
1.2.1 保证可见性
线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存
如果不加 volatile 关键字,则主线程会进入死循环,加 volatile 则主线程能够退出,说明加了 volatile 关键字变量,当有一个线程修改了值,会马上被另一个线程感知到,当前值作废,从新从主内存中获取值。对其他线程可见,这就叫可见性。
class Mydata {
volatile int number = 0;
public void add() {
this.number = 60;
}
}
/*
* 1 验证volatile的可见性
* 1.1 假如 int number = 0;
* number变量之前根本没有添加volatile关键字修饰
*/
public class SeeOkValatile {
public static void main(String[] args) {
Mydata mydata = new Mydata();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
try {
TimeUnit.SECONDS.sleep(3);
} catch (Exception e) {
e.getStackTrace();
}
mydata.add();
System.out.println(Thread.currentThread().getName() + "\t update number " + mydata.number);
}, "aaa").start();
// 傻乎乎的在这转着
while (mydata.number == 0) {
}
System.out.println(Thread.currentThread().getName() + " ");
}
}
1.2.2 不保证原子性
1.2.2.1 什么是原子性?
不可分割,完整性,也就是某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败
class MydataDemo {
volatile int number = 0;
public void add() {
this.number = 60;
}
// 请注意:此时number前面是加了volatile修饰的
public void addPlus() {
number++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic() {
atomicInteger.getAndIncrement();
}
}
/*
* 验证volatile的原子性
*
* 2.3 why 对于number++这个操作 分为3步:
* 1. A = number 线程从主内存中拿到该值
* 2. B = A + 1 在自己的线程内存中加一
* 3. number = B 最后存入到主内存中
* 由于多线程的并发性,可能导致值的覆盖
*
* 2.4 怎么解决?
* 加synchronizedvoid
* 使用AtomicInteger,原子类的数据
*/
public class SeeOkValatile2 {
public static void main(String[] args) {
MydataDemo mydataDemo = new MydataDemo();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
mydataDemo.addPlus();
mydataDemo.addAtomic();
}
}, String.valueOf(i)).start();
}
while (Thread.activeCount() > 2) {
// 礼让线程
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "number is->" + mydataDemo.number);
System.out.println(Thread.currentThread().getName() + "number is->" + mydataDemo.atomicInteger);
}
}
关于count++的知识:
1.2.3 禁止指令重排
计算机在执行程序时,为了提高性能,编译器个处理器常常会对指令做重排,一般分为以下 3 种
- 编译器优化的重排
- 指令并行的重排
- 内存系统的重排
- 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
- 处理器在进行重排序时必须要考虑指令之间的数据依颍悝
- 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
class ReOrderDemo {
int a = 0;
boolean flag = false;
public void write() {
a = 1; //1
flag = true; //2
}
public void read() {
if (flag) { //3
int i = a * a; //4
}
}
}
// 单线程:1234
// 多线程:会出现混乱的错误,指令重排
1.2.3.1 禁止指令重排的实现
volatile 实现禁止指令重排序的优化,从而避免了多线程环境下程序出现乱序的现象
内存屏障(Memory Barrier)又称内存栅栏,是一个 CPU 指令,他的作用有两个:
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性(利用该特性实现 volatile 的内存可见性)
由于编译器个处理器都能执行指令重排序优化,如果在指令间插入一条 Memory Barrier 则会告诉编译器和 CPU,不管什么指令都不能个这条 Memory Barrier 指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后执行重排序优化。内存屏障另一个作用是强制刷出各种 CPU 缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本。
1.3 线程安全性获得保证
工作内存与主内存同步延迟现象导致可见性问题
- 可以使用 synchronzied 或 volatile 关键字解决,它们可以使用一个线程修改后的变量立即对其他线程可见
对于指令重排导致可见性问题和有序性问题
- 可以利用 volatile 关键字解决,因为 volatile 的另一个作用就是禁止指令重排序优化
1.4 你在哪里用到过volatile
单例模式下的DCL
正常情况
- 分配对象内存空间
- 初始化对象
- 设置instance指向刚分配的内存地址,此时instance != null
指令重排
- 分配对象内存空间
- 设置instance指向刚分配的内存地址,此时instance != null 我们的对象还没有初始化
- 初始化对象
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "我是构造方法");
}
// DCL Double Check Lock双端检锁机制
public static synchronized SingletonDemo getInstance() {
if (instance == null) {
synchronized (SingletonDemo.class) {
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
public static void main(String[] args) {
// 单线程(main线程的操作动作----)
// System.out.println(SingletonDemo.getInstance() ==
// SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance() ==
// SingletonDemo.getInstance());
// System.out.println(SingletonDemo.getInstance() ==
// SingletonDemo.getInstance());
for (int i = 0; i <= 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, String.valueOf(i)).start();
}
}
}