目录
3.1什么是可见性?---- 一个线程对共享变量的修改,能够及时的被其他线程看到
3.5除了volaitle还有什么方法可以保证可见性(思考?):
1.什么是volatile?
是JVM提供的轻量级的同步机制(相当于轻量级版本的synchronized)。
特点:
1.保证可见性 2. 不保证原子性, 3.禁止指令重排序
要想解释为什么volatile会有这样的特性,那么不得不说JMM内存模型,下面就先深入了解一下java内存模型
2.JMM内存模型
Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。在Java中提供了一系列和并发处理相关的关键字,比如volatile、synchronized、final、concurren包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字。
2.1JMM是什么
首先,JMM内存模型,是一种不是真实存在的实体,是一种我们约定成俗的一种描述内存的方式。
在计算机内存中,读写速度: 硬盘<内存<CPU, 由于CPU的读写速度非常的快,但是由内存读入CPU,然后在由CPU写入内存,这种操作会导致速度变慢,所以产生了内存和CPU之间的高速缓存(现在一般有3级缓存,他们的关系是,先读一级缓存,没有读二级缓存,没有读三级缓存)。借鉴计算机的内存模型,jvm也有自己的JMM内存模型。
主内存就是我们的物理硬盘,是共享的,所有线程都可以访问,所有变量都放在主内存。多线程1,2,3如果想访问主内存当中的共享变量X,那么需要将其拷贝到直接的工作内存中,线程的工作内存一般是对应在高速缓存里面(阿里面试考过)。并且不同线程之间不能互相通讯,只有这用可以通讯:
(1)把工作内存1中更新过的共享变量刷新到主内存中
(2)将主内存中最新的共享变量的值更新到工作内存2中
2.2JMM中封装的原子操作
将这些原子操作映射到对应的图中,如下图所示:
首先,线程1在主内存中通过read操作,读取initFlag=true, 然后通过load操作,加载到自己的工作内存,然后,通过use操作,使用initFlag变量,然后通过assign操作,讲计算好的值赋值到工作内存。然后通过store操作,将工作内存的数据写入主内存,通过write操作,将store过来的变量赋值给主内存中的变量。
3.volatile如何保证可见性
3.1什么是可见性?---- 一个线程对共享变量的修改,能够及时的被其他线程看到
3.2为什么会存在可见性? -----源于计算机缓存机制
3.3volatile如何保证可见性
volatile低层实现原理:
底层实现主要是通过汇编lock前缀指令实现,它会锁定这块内存区域的缓存并回写到主内存,此操作被称为“缓存锁定”,MESI缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存值通过总线回写到内存会导致其他处理器相应的缓存失效。
3.4 代码验证可见性:
一个线程改number的值,main线程有个判断number==0,这样就可以看到是否,线程改变number的值后,是否通知了main线程。
//一个线程改number的值,main线程有个判断number==0,这样就可以看到是否,线程改变number的值后,是
//否通知了main线程。
class Mydata {
volatile int number = 0;
public void change() {
this.number = 60;
}
//
public void addPlusPlus(){
number++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic(){
atomicInteger.getAndIncrement();
}
}
public class MyVolatile {
public static void main(String args[]) {
Mydata mydata = new Mydata();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in ");
try { TimeUnit.SECONDS.sleep(3); ;
} catch (InterruptedException ie){
ie.printStackTrace();
}
mydata.change();
System.out.println(Thread.currentThread().getName() + "\t update number value: " + mydata.number);
}).start();
while (mydata.number == 0) {
}
System.out.println(Thread.currentThread().getName() + "\t mission is over" + mydata.number);
}
}
3.5除了volaitle还有什么方法可以保证可见性(思考?):
Java中的synchronized和final两个关键字也可以实现可见性。只不过实现方式不同
4.禁止指令重排序
4.1什么是指令重拍?
计算机在执行程序的时候,为了提高性能,编译器和处理器常常会对指令(代码)做重排序,
一般情况是 源代码 ----> 编译器优化的重排 ---->指令并行的重排 ----> 内存系统的重排 ------> 最终执行的指令
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致性,处理器在进行重排序时必须要考虑指令之间的数据依赖性。多线程环境中线程交替执行,由于编译器优化重排存在,两个线程中使用的变量能否保证一致性是无法确定的,结果也无法预测。
4.2volatile如何禁止指令重拍
加入volatile关键字时,会多出一个lock前缀指令” lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
5.不保证原子性
5.1什么是原子性?
即不可再分了,不能分为多步操作。比如赋值或者return。比如"a = 1;"和 "return a;"这样的操作都具有原子性。类似"a += b"这样的操作不具有原子性,在某些JVM中"a += b"可能要经过这样三个步骤:
① 取出a和b
② 计算a+b
③ 将计算结果写入内存
一般情况,程序的执行顺序是①②③,但是由于指令重排序,有可能是①③②
5.2 NUM++不是原子操作
Num++;//Num不是原子操作
Num不是原子操作,因为其可以分为:读取Num的值,将Num的值+1,写入最新的Num的值。
对于Num++;操作,线程1和线程2都执行一次,最后输出Num的值可能是:1或者2
【解释】输出结果1的解释:当线程1执行Num++;语句时,先是读入Num的值为0,倘若此时让出CPU执行权,线程获得执行,线程2会重新从主内存中,读入Num的值还是0,然后线程2执行+1操作,最后把Num=1刷新到主内存中; 线程2执行完后,线程1由开始执行,但之前已经读取的Num的值0,所以它还是在0的基础上执行+1操作,也就是还是等于1,并刷新到主内存中。所以最终的结果是1
一般在多线程中使用volatile变量,为了安全,对变量的写入操作不能依赖当前变量的值:如Num++或者Num=Num*5这些操作。
num++反编译后的字节码:
getfield #2 // Field i:I
iconst_1
iadd
putfield #2 // Field i:I
5.3 如何解决原子性问题?
1.自旋CAS
2. synchronized
3. 原子类(AtomicInteger)
CAS、原子类和synchronized具体的知识,之后会写详细的博客介绍。