以下内容均由volatile关键字而引出:
JMM,Java memory model,JMM规定所有变量都需要存放在主内存中,同时每个线程又有自己的工作内存。(主要用作高速缓存。CPU有多级缓存,这个是否同一个概念,有待查证,参考简书:什么是Java内存模型)
参考简书又有一篇文章提到过,每条线程都有自己的的工作内存(这可以与CPU的高速缓存类比)。
线程的工作内存中会保存从主存中拷贝过来的副本,线程对变量的所有操作都在工作内存中进行,当程序运行结束,数据将被刷新到主存中去。
引入《深入理解Java虚拟机 第3版》中的原话:
对于SunJDK来说, 它的Windows版与Linux版都是使用一对一的线程模型实现的;
一条Java线程就映射到一条轻量级进程之中,因为Windows和Linux系统提供的线程模型就是一对一的。
所谓轻量级进程就是内核(CPU)线程的一种高级接口……线程的工作内存其实是CPU的寄存器和高速缓存的抽象。
内存可见性?一定!
当一个变量被volatile修饰的时候,任何线程对其更新操作,其他线程都可见,或者说是立即被刷新到主存中?并且强制让其他缓存了该变量的线程的工作内存中内容清空,强制从主存中重新获取。
注意不是让线程直接操作主存数据,而是立即更改后立即刷新到主存中,并且其他线程立即可见。
线程安全?不一定!
使用volatile修饰基本数据类型不能保证原子性
public class TestAtomic {
private volatile static int index = 0;
// private static AtomicInteger index = new AtomicInteger(0);
private static final CountDownLatch latch = new CountDownLatch(3);
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
// index.incrementAndGet();
index++;
}
latch.countDown();
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
// index.incrementAndGet();
index++;
}
latch.countDown();
});
Thread thread3 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
// index.incrementAndGet();
index++;
}
latch.countDown();
});
thread1.start();
thread2.start();
thread3.start();
latch.await();
// TimeUnit.MILLISECONDS.sleep(300);
System.out.println("结果,index = " + index);// 结果小于30000,uncommented AtomicInteger 则可以保证原子性达到30000.
}
}
禁止指令重排(保证有序性)
何为禁止指令重排?
singleton = new Singleton()可以分为三个过程:
1、划分内存
2、填充对象
3、分配指针
且这三个过程顺序往往是不确定的,但是在单线程中没什么问题,考虑多线程的时候:
如果不添加volatile关键字,则有可能由于JVM优化导致指令重排,变为132,并且虽然加了同步锁,但是在3执行完之后就会释放锁,此时其他线程获取同步锁判断对象仍为null,就开是创建对象咯,此时还是单例吗,就不是咯。
经典的单例DCL
public class Singleton {
// 禁止指令重新排布
private static volatile Singleton singleton;
private Singleton(){}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}