目录
JMM 概念和原理 、三大特性 、 Happens-Before 先行发生原则_m0_71149992的博客-优快云博客
知识前提
Juc11_Java内存模型之JMM、八大原子操作、三大特性、读写过程、happens-before_所得皆惊喜的博客-优快云博客_java11内存模型
JMM 概念和原理 、三大特性 、 Happens-Before 先行发生原则_m0_71149992的博客-优快云博客
Volatile特点
被volatile修改的变量有2大特点
①. 特点:可见性、有序性、不保证原子性
②. volatile的内存语义
1、当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
2、当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
3、所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取
volatile凭什么可以保证可见性和有序性
内存屏障
有序性
不存在数据依赖关系 可以重排序
存在数据依赖关系 禁止重排序
什么是内存屏障
①. 内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性
②. 内存屏障之前的所有写操作都要回写到主内存
内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)
③. 一句话:对一个volatile域的写, happens-before于任意后续对这个volatile域的读,也叫写后读
内存屏障分类
粗分
细分
源码分析内存屏障指令的由来
(1). IDEA工具里面找Unsafe.class
(2). Unsafe.java(3). Unsafe.cpp
(4). OrderAccess.hpp
(5). orderAccess_linux_x86.inline.hpp
所以最终细分有4分钟
LoadLoad
StoreStore
LoadStore
StoreLoad
深入内存屏障
happens-before之volatile变量规则
内存屏障策略
读屏障
读插入内存屏障的指令序列示意图
写屏障
写插入内存屏障的指令序列示意图
volatile特性
volatile特性 - 保证可见性
保证不同线程对这个变量进行操作时的可见性,即变量一旦改变所有线程立即可以看到
代码如下
public class VolatileSeeDemo
{
static boolean flag = true; //不加volatile,没有可见性
public static void main(String[] args)
{
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t"+"---come in");
while(flag)
{
}
System.out.println("t1 over");
},"t1").start();
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
flag =false;
System.out.println(Thread.currentThread().getName()+"\t 修改完成 flag: "+ flag);
}
}
测试结果
程序并没有停止
原因: 主线程 将 flag修改为 false 没有将最新的数据刷新到主内存,也没有通知其他线程flag已发生改变,让他重新去主内存拉取最新数据
修改代码
public class VolatileSeeDemo
{
static volatile boolean flag = true; //不加,没有可见性
public static void main(String[] args)
{
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t"+"---come in");
while(flag)
{
}
System.out.println("t1 over");
},"t1").start();
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
flag =false;
System.out.println(Thread.currentThread().getName()+"\t 修改完成 flag: "+ flag);
}
}
结果
上面问题 原理解释:
volatile变量读写过程
volatile特性 - 不保证原子性
测试代码如下
理想 结果 是 10000
class MyNumber
{
volatile int number = 0;
public void addPlusPlus()
{
number++;
}
}
public class VolatileNoAtomicDemo
{
public static void main(String[] args) throws InterruptedException
{
MyNumber myNumber = new MyNumber();
for (int i = 1; i <=10; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
myNumber.addPlusPlus();
}
},String.valueOf(i)).start();
}
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName() + "\t" + myNumber.number);
}
}
测试结果
每次运行结果都不一样
读取赋值一个变量的情况
不保证原子性的原因
可以在 《深入jvm虚拟机 》第12章3.3可以查看
从底层来看i++ 问题
结论
volatile特性 - 禁止指令重排
指令重排是什么
不存在数据依赖性 可以重排
存在数据依赖性 禁止重排
volatile底层实现
volatile底层是根据内存屏障实现的
volatile有关的禁止指令重排的行为
四大内存屏障插入情况
代码说明
添加 这个 StoreStore 是禁止将 这个flag =true 重新排序 ,如果没有 这个volatile 修饰 可能 会将 flag =true 会优化到第一行 ,结果有多个线程调用这个read 方法 if(flag)为true 但是 i 还没有进行赋值 ,此时 结果与预想结果有误 ,造成语义错误
StoreLoad 指令 就是禁止 flag =true 与下面的 volatile 读或者写重排序
添加 LoadLoad指令 只要有 volatile 写 此时主内存的数据已经是最新的 必须去从主内存中拿最新数据 只有我读完了 确认了在进行后面操作
添加 LoadStore 指令 只有volatile读完了 以后再 进行后面操作 从而保证语义正确性和一致性
volatile使用场景
单一赋值可以,but含复合运算赋值不可以(i++之类)
volatile int a = 10
volatile boolean flag = false
判断业务是否结束
public class UseVolatileDemo{
private volatile static boolean flag = true;
public static void main(String[] args){
new Thread(() -> {
while(flag) {
//do something......
}
},"t1").start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
flag = false;
},"t2").start();
}
}
开销较低的读,写锁策略
public class UseVolatileDemo{
/**
* 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
* 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
*/
public class Counter{
private volatile int value;
public int getValue(){
return value; //利用volatile保证读取操作的可见性
}
public synchronized int increment(){
return value++; //利用synchronized保证复合操作的原子性
}
}
}
单列模式 DCL双端锁的发布
原始代码
在单线程看待这个问题
没加volatile可能出现重排序
在多线程看待这个问题
也就是 第二步和第三步执行顺序 对调了 导致对象还没有初始化完成 就引用指向刚开辟的空间 导致其他现在得到一个null
问题解决
public class SafeDoubleCheckSingleton{
//通过volatile声明,实现线程安全的延迟初始化。
private volatile static SafeDoubleCheckSingleton singleton;
//私有化构造方法
private SafeDoubleCheckSingleton(){
}
//双重锁设计
public static SafeDoubleCheckSingleton getInstance(){
if (singleton == null){
//1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
synchronized (SafeDoubleCheckSingleton.class){
if (singleton == null){
//隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
//原理:利用volatile,禁止 "初始化对象"(2) 和 "设置singleton指向内存空间"(3) 的重排序
singleton = new SafeDoubleCheckSingleton();
}
}
}
//2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
return singleton;
}
}
有不加volatile的方法解决单例模式的懒汉线程问题
采用静态内部类的方式实现
public class SingletonDemo {
private SingletonDemo() { }
private static class SingletonDemoHandler {
private static SingletonDemo instance = new SingletonDemo();
}
public static SingletonDemo getInstance() {
return SingletonDemoHandler.instance;
}
public static void main(String[] args) {
for (int i = 0; i <10 ; i++) {
new Thread(()->{
SingletonDemo instance = getInstance();
// 可以知道这里获取到的地址都是同一个
System.out.println(instance);
},String.valueOf(i)).start();
}
}
}
采用枚举 也可以解决线程安全问题
public enum Singleton {
INSTANCE;
public void sayHello() {
System.out.println("Hello World");
}
}
总结
volatile 可见性
volatile 禁止重排
1 写指令
2 读指令
volatile 与内存屏障
内存屏障是什么
内存屏障能干啥
内存屏障四大指令