摘要
在了解volatile关键字之前,我们要首先了解下JMM内存模型,这个和JVM内存模型是不一样的哦,这个是硬件方面的内存架构。
图片来源于网络:
在我们现在的日常开发中,一般来说电脑都是多核多线程,有自己的缓存的存在的。
主内存:是所有CPU的共享内存。所有CPU都可以访问。
工作内存:是每个工作内存独有的,每个工作内存不影响。
CPU寄存器:每个工作内存都包含一系列的寄存器,它们是CPU内内存的基础。
高速缓存:由于计算机的存储设备与处理器的运算速度之间有着几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度还要慢一点。每个CPU可能有一个CPU缓存层,一些CPU还有多层缓存。在某一时刻,一个或者多个缓存行(cache lines)可能被读到缓存,一个或者多个缓存行可能再被刷新回主存。
volatile的三大特性
可见性,禁止重排序,不能保证原子性。
可见性:在多线程中,如果某一个工作副本的值被改变后,他刷新到主内存中后,如何保证被其他工作内存知道,共享内存的值已经发生改变呢,我们可以在共享内存的值加上关键字volatile,保证共享变量的值修改后,其他工作内存可以及时的通过电脑自带的MESI协议或者总线锁监测到之后来通知其他工作内存,我的共享内存的值已经发生改变。然后重新把新的共享变量值刷新到工作副本中。
在下面的代码块中,大家可以尝试一下变量加上volatile的关键字和不加的区别,注意电脑一定是多核多线程的哦,
public class Test003 extends Thread {
public static boolean flag = false;
@Override
public void run() {
while (flag) {
System.out.println(currentThread().getName() + "我是子线程");
}
}
public static void main(String[] args) {
new Test003().start();
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
flag = true;
System.out.println("主线程停止");
}
}
}
禁止重排序:在我们的java程序中,我们的程序如果想执行的话都是通过汇编来实现的,在汇编中程序的代码是有可能被重排序的,在单线程中,如果重排序,他的执行结果不会发生变化,但是在多线程中就会影响,底层是使用了内存屏障,例如:双重检验锁。
对象实例化的过程:
1.给对象分配内存空间
2.调用构造函数初始化
3.将对象赋值给变量
重排序之后就可能变成1,3,2
假如在多线程中,对象实例化的过程 被重排序了,有一个线程拿到了锁之后, 已经将对象赋值给了变量,但是还没有初始化,然后释放锁了,然后第二个线程此时判断当前对象不为空,然后就拿着当前对象去使用肯定会发生错误。加上volatile关键之后,会禁止重排序,也就不会发生错误了。
public class Singleton {
public static volatile Singleton singleton;
public static Singleton getInstance(){
if(singleton == null){
synchronized (Singleton.class){
if (singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
public static void main(String[] args){
Singleton instance = Singleton.getInstance();
Singleton instance1 = Singleton.getInstance();
System.out.println(instance == instance1);
}
}
不保证原子性:volatile关键字是不能保证原子性的,在多线程中会发生确少数据的情况。例如在多线程中,我们两个工作内存同事对共享变量+1,但是当其中一个工作内存的值刷新到共享内存中后,另一个工作内存与共享变量的值比较发现相等,然后就不会修改共享变量的值,此时就相当于丢失了一部分数据,缺少了+1的操作;
volatiel的伪共享问题:工作内存读取的时候是以块读取的,一般为64kb读取,但有时候我们修改的值小于64kb,但是我们工作内存读取共享内存的时候都是读取块的模式,会读取其他没有修改到的变量,假如另一个cpu也同时修改了一个变量,但是这个变量正好被上一个工作内存读取的时候读取到了,我们这个工作内存必须等上一个工作内存修改后才能读取,这个就是伪共享,也会导致性能变差。那么解决伪共享的方法有什么呢,我们可以给修改的变量填充使他等于64kb,
jdk7中,写个类单独继承方
public final static class VolatileLong extends AbstractPaddingObject {
public volatile long value = 0L;
}
public class AbstractPaddingObject {
public long p1, p2, p3, p4, p5, p6;
}
jdk8中,使用注解@sun.misc.Contended,启动的时候需要加上该参数-XX:-RestrictContended
ConcurrentHashMap中就使用了此注解
@sun.misc.Contended static final class CounterCell {}