volatile关键字,熟悉的朋友应该能总结到该关键字的特征如下:
1.保证可见性、但不保证原子性
2.禁止指令重排
在这里有一些理论的概念需要理解清楚,什么是可见性、原子性?
这一部分的内容其实涉及到Java内存模型(JMM),什么是JMM?本质上可以理解为JVM规范了如何提供按需禁用缓存和编译优化的方法。
在这之前我们知道并发编程bug的源头有:CPU缓存带来的可见性问题、线程切换带来的原子性问题和编译优化带来的有序性问题。
背景:随着科技的不断发展,我们的cpu、内存、I/O设备都在不断迭代,变得越来越快,但是有一个核心的矛盾存在,就是这三者的速度差异。从速度上来说,cpu > 内存 > I/O,但是根据木桶原理(一只水桶能装多少水是取决于最短的那块木板),程序整体的性能是取决于最慢的操作-比如读写I/O设备。为了合理利用CPU的高性能,平衡这三者的速度差异,计算机体系机构、操作系统、编译程序都作出了贡献,主要表现为:
- CPU增加了缓存,以均衡与内存的速度差异
- 操作系统增加了进程、线程,以分时复用CPU,进而均衡了CPU与I/O设备的速度差异
- 编译程序优化指令执行次序,使得缓存能够得到更加合理的利用
可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到,称之为可见性。
image原子性:可以理解为一个操作要么全部完成,否则全部失败。
举个案例:代码中常见的cont + 1操作,其实实现起来分为三部,
指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器;
指令 2:之后,在寄存器中执行 +1 操作;
指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。
在多线程的情况下,线程A可能执行了步骤1、cpu时间片切换到了线程B开始执行,导致count=1写入到内存中,线程A再此执行count+1=1,将线程内缓存写入到主内存中,结果被覆盖了
image有序性:编译器了优化性能,有时候会改变程序中语句的先后顺序。
例如程序中”a = 6;b =7”,可能会变成”b = 7;a =6” 这样可能会导致意想不到的BUG。
一个经典的java案例就是利用双重检查创建单例对象:
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
在多线程调用时会存在问题,问题出在new操作上。
- a. 给singleton分配内存
- b. 在内存中初始化singleton对象
- c. 将内存地址赋给singleton变量
目前多采用静态内部类无锁的单例模式
public class Singleton {
private Singleton(){};
static class innerHolder{
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return innerHolder.instance;
}
}
特点:静态内部类实现单例模式,既能保证延迟加载,又能保证线程安全,只创建一个实例对象。
原理解析:Singleton类初始化的时候不会加载所有的类,只有第一次访问静态内部类的时候才会进行初始化操作。
虚拟机会保证一个类的 clinit() 方法在多线程环境中被正确的加锁、同步,如果多个线程同事去初始化一个类,那么只有一个线程去执行这个类的 clinit() 方法,其他线程都需要阻塞等待,直到活动线程执行 clinit() 方法执行完毕。具体可以参考《JVM-类加载机制》,这里不再深入。
网友评论