Juc12_Volatile的可见性、不保证可见性、有序性、使用、内存屏障四大指令StoreStore、StoreLoad 、LoadLoad、LoadStore(四)

简介: ③. 禁止指令重排④. 在哪些地方可以使用volatile?

③. 禁止指令重排


①. 重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序(不存在数据依赖关系,可以重排序;存在数据依赖关系,禁止重排序)


②. 重排序的分类和执行流程


编译器优化的重排序:编译器在不改变单线程串行语义的前提下,可以重新调整指令的执行顺序


指令级并行的重排序:处理器使用指令级并行技术来讲多条指令重叠执行,若不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序


内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行



③. 数据依赖性:若两个操作访问同一变量,且这两个操作中有一个为写操作,此时两操作间就存在数据依赖性(存在数据依赖关系,禁止重排序===> 重排序发生,会导致程序运行结果不同)


微信图片_20220108125010.png


④. 在哪些地方可以使用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双端锁的发布


原因:


(1). DCL(双端检锁) 机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排原因在于某一个线程在执行到第一次检测,读取到的instance不为null时,instance的引用对象 可能没有完成初始化


instance=new SingletonDem(); 可以分为以下步骤(伪代码)


memory=allocate();//1.分配对象内存空间


instance(memory);//2.初始化对象


instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null


(2). 步骤2和步骤3不存在数据依赖关系.而且无论重排前还是重排后程序执行的结果在单线程中并没有改变,因此这种重排优化是允许的.


memory=allocate();//1.分配对象内存空间


instance=memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null 但对象还没有初始化完.


instance(memory);//2.初始化对象


(3). 但是指令重排只会保证串行语义的执行一致性(单线程) 并不会关心多线程间的语义一致性


所以当一条线程访问instance不为null时,由于instance实例未必完成初始化,也就造成了线程安全问题


(4). 我们使用volatile禁止instance变量被执行指令重排优化即可


private volatile static SafeDoubleCheckSingleton singleton;


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();
        }
    }
}


相关文章
|
4月前
|
编译器 C++
理解内存序,指令重排与内存模型
理解内存序,指令重排与内存模型
47 0
|
4月前
|
存储 程序员 数据处理
【汇编】mov和add指令、确定物理地址的方法、内存分段表示法
【汇编】mov和add指令、确定物理地址的方法、内存分段表示法
148 1
【汇编】mov和add指令、确定物理地址的方法、内存分段表示法
|
9月前
|
缓存 安全 Java
volatile底层的实现原理:volatile关键字的作用、内存模型、JMM规范和CPU指令
volatile底层的实现原理:volatile关键字的作用、内存模型、JMM规范和CPU指令
125 0
|
10月前
持久内存指令(PMDK)简介
持久内存指令(PMDK)简介
220 0
|
10月前
|
存储 安全
4.3 x64dbg 搜索内存可利用指令
发现漏洞的第一步则是需要寻找到可利用的反汇编指令片段,在某些时候远程缓冲区溢出需要通过类似于`jmp esp`等特定的反汇编指令实现跳转功能,并以此来执行布置好的`ShellCode`恶意代码片段,`LyScript`插件则可以很好的完成对当前进程内存中特定函数的检索工作。在远程缓冲区溢出攻击中,攻击者也可以利用汇编指令`jmp esp`来实现对攻击代码的执行。该指令允许攻击者跳转到堆栈中的任意位置,并从那里执行恶意代码。
122 0
|
12月前
|
存储 缓存 索引
通过地址和索引实现数组、CPU指令执行过程、内存概述及内存物理结构
通过地址和索引实现数组、CPU指令执行过程、内存概述及内存物理结构
68 0
|
12月前
|
前端开发 rax
实验一:查看CPU和内存,用机器指令和汇编指令编程
实验一:查看CPU和内存,用机器指令和汇编指令编程
158 0
|
存储 缓存 数据库
PPC902AE101 3BHE010751R0101 访问指令来增加CPU内存子系统的带宽
PPC902AE101 3BHE010751R0101 访问指令来增加CPU内存子系统的带宽
94 0
PPC902AE101 3BHE010751R0101 访问指令来增加CPU内存子系统的带宽
|
安全 Java 编译器
【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序
【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序
【JavaEE】并发编程(多线程)线程安全问题&内存可见性&指令重排序
|
存储 缓存 Java
关于缓存一致性协议、MESI、StoreBuffer、InvalidateQueue、内存屏障、Lock指令和JMM的那点事
关于缓存一致性协议、MESI、StoreBuffer、InvalidateQueue、内存屏障、Lock指令和JMM的那点事
291 0