Volatile可见性、主内存简介

主内存和工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。**此处Java内存模型的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。**为了获得较好的执行效能,Java内存模型并没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。
java类的成员变量有俩种:一种是被static关键字修饰的变量,叫类变量或者静态变量;另一种没有static修饰,为实例变量。
在语法定义上的区别:静态变量前要加static关键字,而实例变量前则不加。
在程序运行时的区别:实例变量属于某个对象的属性,必须创建了实例对象,其中的实例变量才会被分配空间,才能使用这个实例变量。静态变量不属于某个实例对象,而是属于类,所以也称为类变量,只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。总之,实例变量必须创建对象后才可以通过这个对象来使用,静态变量则可以直接使用类名来引用。
Java内存模型的实例字段:对应的是Java实例化对象的成员变量
Java内存模型的静态字段:对应的是Java类成员变量
Java内存模型和JVM内存模型是不一样的:参考资料 https://juejin.im/post/6844903941805703181
Java内存模型规定了所有的变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件时的主内存名字一样,两者也可以互相类比,但此处仅是虚拟机内存的一部分)。每条线程还有自己的工作内存(Working Memory,可与前面讲的处理器高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如图所示。(参考资料:https://blog.****.net/qq_41154882/article/details/102638753)
Volatile可见性、主内存简介
注:

  1. “拷贝副本”,如“假设线程中访问一个10MB的对象,也会把这10MB的内存复制一份拷贝出来吗?”,事实上并不会如此,这个对象的引用、对象中某个在线程访问到的字段是有可能存在拷贝的,但不会有虚拟机实现成把整个对象拷贝A一次。线程的工作内存拷贝了对象的引用、和正在访问的对象中的某个字段(对象中其它没有访问到的字段不拷贝)

1. volatile的作用

参考资料:https://blog.****.net/changqijihua/article/details/97510035
1 可见性。volatile修饰的变量,线程访问此变量会从主内存中去读写,而不是线程自己的缓存中。

2 非阻塞。不会阻塞访问线程。

3 不具有原子性。对于原子性操作,volatile修饰的变量可以保证原子性,但是非原子性操作不能保证原子性。例如,volatile修饰一个变量,开启20个线程,每个线程循环10000次i++自加这个变量,最后的结果小于200000。因为读取i后,肯能某个线程刚好修改了i的值。(i++其实是三个操作,读,自加,写入)。即eg:temp=i;temp=i+1;i=temp;
执行GETFIELD拿到主内存中的原始值i。temp=i
执行IADD进行加1操作,此时加的结果有一个中间态temp=i+1
执行PUTFIELD把工作内存中的值写回主内存中i=temp;
4. 防止指令重排序,双层锁校验
Volatile可见性、主内存简介

我们来看看这个场景:假设线程一执行到instance = new SingletonKerriganD()这句,这里看起来是一句话,但实际上它并不是一个原子操作(原子操作的意思就是这条语句要么就被执行完,要么就没有被执行过,不能出现执行了一半这种情形)。事实上高级语言里面非原子操作有很多,我们只要看看这句话被编译后在JVM执行的对应汇编代码就发现,这句话被编译成8条汇编指令,大致做了3件事情:

1.给Kerrigan的实例分配内存。

2.初始化Kerrigan的构造器

3.将instance对象指向分配的内存空间(注意到这步instance就非null了)。
第2和第3步顺序会打乱,修改方式:
private volatile static SingletonKerriganD instance = null; 防止第2步和第3步顺序错乱,严格按照1、2、3步骤顺序执行

2. volatile修饰数组

volatile HashMap testMap = new HashMap();
1 重新赋值。
上面的 new HashMap() 对象,存在于堆中;testMap 存在于栈中,指向上文的new HashMap()对象。
testMap 被volatile修饰,表示多线程访问时,每次都会从主内存中读取testMap 指向的地址。
所以每次给testMap 重新赋值,是符合线程间可见性原则的。
例如:
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
如果testMap = map1 ;然后又testMap = map2 ;那么获取testMap中的值,确实是map2 所指向的对象的值。
2 但是引用类型,其里面属性值并不保证可见性
这个结论,从CopyOnWriteArrayList源码中也可以看出来,调用get方法取值时没有加锁:
private transient volatile Object[] elements;
Volatile可见性、主内存简介

Volatile可见性、主内存简介