我们知道,操作系统主要由CPU、I/O设备,主存等组成。但是由于CPU的处理速度与内存的读取速度相差好几个量级,而且CPU是非常宝贵的资源。因此在内存与CPU之间引入了高速缓存,内存将待处理的数据放入高速缓存,cpu从高速缓存读取数据,借以充分利用CPU的资源。
CPU的高速缓存分为L1 cache/L2 cache/L3 cache。它们的存储大小L1
L2>L3。
正是由于引入了高速缓存,在多线程环境下,由于是每一个cpu核心有一个自己的高速缓存区,可能会出现从内存中读到的数据不是最新数据的情况
举例:
多个线程 对Integer i=0 进行+1操作,如果这里我们有三个cpu核心,那么理想情况下 返回的数据是3
但是多线程 是会存在并发的,假如说cpu0后的变量 未被及时刷新到内存中,cpu1或cpu2直接把初始的变量加载进来了,从而导致最终计算的结果不准确。
我们把这类问题称之为缓存一致性问题,解决缓存已知悉的方案之一就是使用volatile
volatile
关键字是Java提供的一种轻量级同步机制。它能够保证可见性和有序性,但是不能保证原子性
即某个线程对共享变量的修改会被立即刷新到主内存中,并让其他线程强制加载最新的主内存变量数据。
举例说明
//内存不可见性举例
public class VolatileDemo {
private Integer num = 0;
public static void main(String[] args) {
VolatileDemo demo = new VolatileDemo();
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
try {
Thread.sleep(1 * 1000);
demo.setNum();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t update number value: " + demo.num);
}, "MyThread" + i).start();
}
//主线程在此判断,如果拿到了修改后的值 则循环结束
while (demo.num == 0) {
}
System.out.println("num=" + demo.num);
}
public void setNum() {
this.num = 10;
}
}
输出结果如下:
MyThread0 come in
MyThread1 come in
MyThread2 come in
MyThread1 update number value: 10
MyThread2 update number value: 10
MyThread0 update number value: 10
会发现程序被阻塞在while循环处。
起初main线程与三个子线程从内存中拿到的num值都是0,而后来三个子线程分别把num值都进行了修改,但是似乎main线程中拿到的num数据还未被更改(程序阻塞在while循环处就是证明)。
这也就证明了该案例不具备内存可见性
我们为num变量添加volatile修饰之后,再来看下
public class VolatileDemo {
private volatile Integer num = 0;
public static void main(String[] args) {
VolatileDemo demo = new VolatileDemo();
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + "\t come in");
Thread.sleep(1 * 1000);
demo.setNum();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t update number value: " + demo.num);
}, "MyThread" + i).start();
}
while (demo.num == 0) {
}
System.out.println("num=" + demo.num);
}
public void setNum() {
this.num = 10;
}
}
结果:
MyThread0 come in
MyThread1 come in
MyThread2 come in
MyThread1 update number value: 10
MyThread0 update number value: 10
MyThread2 update number value: 10
num=10
可见,此时某个线程对num的修改会立刻反映到主内存上
volatile并不能保证操作的原子性。
举例说明:1000个线程对num进行加1操作
public class VolatileDemo {
private volatile Integer num = 0;
public static void main(String[] args) {
VolatileDemo demo = new VolatileDemo();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
demo.add();
}, "MyThread" + i).start();
}
System.out.println("num=" + demo.num);
}
public void add() {
this.num = this.num + 1;
}
}
按照预期的话 结果应该是1000,但是实际执行的结果总是<=1000
为什么实际结果会<=1000呢?
首先num=num+1并不是一个原子操作,而是三个操作。
当一个线程A在自己工作内存执行完+1操作后(假如结果是2),要写入主内存时,可能线程B抢占了cpu,然后线程B 执行完+1操作(比如结果是3),写回了主内存,而线程A此时得到了CPU的执行权,它接着从之前未执行完的地方执行(把2写入主内存)。这个时候它就出现了写覆盖,将2覆盖了现在主内存的3。然后其他线程从主内存基于这个数字再运算,因此得到的结果可能比实际小。
解决方案:对add方法
加锁或者使用原子类(后面说)
public class VolatileDemo {
private volatile Integer num = 0;
public static void main(String[] args) {
VolatileDemo demo = new VolatileDemo();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
demo.add();
}, "MyThread" + i).start();
}
System.out.println("num=" + demo.num);
}
public synchronized void add() {
this.num = this.num + 1;
}
}
这样就能保证计算结果的正确性了。
volatile可以保证有序性,也就是防止指令重排序。所谓指令重排序,就是出于优化考虑,CPU执行指令的顺序会与我们程序的顺序不一致,规则是CPU将前后没有依赖关系的指令进行重排,以期达到优化的目的。
举例:
int x = 11; //语句1
int y = 12; //语句2
x = x + 5; //语句3
y = x * x; //语句4
实际的CPU执行顺序会有多种,如1234、2134等。但不会将1与3的指令执行顺序颠倒,因为语句3是依赖语句1的。
DCL 双重检查锁 即用到了volatile
public class DCL {
private volatile static DCL dcl = null;
private DCL() {
}
//Double Check Lock 双重检查锁机制:在加锁前后都进行判断
public static DCL getInstance() {
if (Objects.isNull(dcl)) {
synchronized (dcl) {
if (Objects.isNull(dcl)) {
dcl= new DCL();
}
}
}
return dcl;
}
}
之所以会用到volatile关键字,主要是因为dcl=new DCL()
不是原子性的,是多个指令的集合。
memory = allocate(); //1.分配内存
instance(memory); //2.初始化对象
instance = memory; //3.设置引用地址
CPU可能会使2、3语句发生指令重排。如果发生,此时内存已经分配,instance = memory
不为null,两次Objects.isNull(dcl)
都会跳过,最后返回了对象。但是实际上对象还没初始化。因此程序会出现问题。