1、并发编程中的三个概念
1.1、原子性
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
实现:synchronized、Lock
1.2、可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
实现:volatile、synchronized、Lock
1.3、有序性
即程序执行的顺序按照代码的先后顺序执行
实现:volatile、synchronized、Lock
也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
2、volatile关键字
2.1、volatile关键字的两层含义
共享变量(类的成员变量、类的静态成员变量)被volatile修饰后,那么就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
2.2、volatile非原子性
volatile无法保证对变量的任何操作都是原子性,因此,可使用以下方法达到效果:
- synchronized
- Lock
- AtomicInteger
2.3、volatile保证有序性
volatile关键字禁止指令重排序有两层意思:
- 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
- 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
2.4、volatile的原理和实现机制
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
---《深入理解Java虚拟机》
lock前缀指令实际上相当于一个内存屏障,内存屏障会提供3个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
3、使用volatile关键字的场景
synchronized关键字是防止多个线程同时执行一段代码,会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
使用场景:
状态标记量
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
double check
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
为什么使用double check
Java的多线程是开启线程资源,每一个线程是通过抢占CPU时间片执行的,当线程一通过判断进入到获取实例的代码中的时候还没执行赋值的时候,这时候线程二正好拿到时间片执行发现这个实例为空,于是它也进入到了创建实例的代码块中,所以创建了两个对象,从而导致创建了两个不同的对象。