• Java基础 --- 线程同步 volatile关键字


    volatile keyword

    • Volatile是Java虚拟机提供的轻量级的同步机制
    • Volatile可以保证可见性, 禁止指令重排, 但是不保证原子性
    • volatile利用MESI协议和snooping保证可见性
    • volatile不保证原子性, 比如 num++ 这个操作实际上分为三步, 拿到num值, 对num值加一, 放回num值.
    • volatile关键字保证了拿到number的值是正确的,但是在执行对num值加一, 放回num值这些指令的时候,其他线程可能已经把number的值改变了,而操作栈顶的值就变成了过期的数据,所以就可能把较小的number值同步回主内存之中.
    • 如果要实现原子性, 可以使用synchronized关键字, 或者使用 Java并发包(JUC)中的AtomicInterger等类

    使用volatile保证可见性

    import java.util.List;
    import java.util.ArrayList;
    import java.util.Arrays;
    
    public class Solution {
    	
    	static class ShareData {
    		int number = 0;
    		
    		public void setNumberTo100() {
    			this.number = 100;
    		}
    	}
    	
    	public static void main(String[] args) {
            // 资源类
            ShareData shareData = new ShareData();
     
            // 子线程 实现了Runnable接口的,lambda表达式
            new Thread(() -> {
     
                System.out.println(Thread.currentThread().getName() + "\t come in");
     
                // 线程睡眠3秒,假设在进行运算
                try {
                    Thread.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 修改number的值
                shareData.setNumberTo100();
     
                // 输出修改后的值
                System.out.println(Thread.currentThread().getName() + "\t update number value:" + shareData.number);
     
            }, "child Thread: ").start();
     
            while(shareData.number == 0) {
            	//System.out.println(Thread.currentThread().getName() + "等待number更新为100");
            }
     
            //这句话输出不出来, 因为子线程更改number值后, main线程没有感知到
            System.out.println(Thread.currentThread().getName() + "\t 主线程感知到了 number 不等于 0");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 最后线程没有停止,没有输出"主线程知道了 number 不等于0"这句话,说明没有用volatile修饰的变量,变量的更新是不可见的
      在这里插入图片描述
    • 将number 声明为 volatile: volatile int number = 0;
      在这里插入图片描述

    使用volatile禁止指令重排

    • 计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。
    • 有三种指令重排:
    • .编译器优化重排:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
    • .指令级的并行重排:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
    • .内存系统的重排:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行

    Example: 双重检测锁定的单例模式

    package com.jackson0714.passjava.threads;
    /**
     演示volatile 单例模式应用(双边检测)
     * @author: 悟空聊架构
     * @create: 2020-08-17
     */
     
    class VolatileSingleton {
        private static VolatileSingleton instance = null;
        private VolatileSingleton() {
            System.out.println(Thread.currentThread().getName() + "\t 我是构造方法SingletonDemo");
        }
        public static VolatileSingleton getInstance() {
            // 第一重检测
            if(instance == null) {
                // 锁定代码块
                synchronized (VolatileSingleton.class) {
                    // 第二重检测
                    if(instance == null) {
                        // 实例化对象
                        instance = new VolatileSingleton();
                    }
                }
            }
            return instance;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    }

    • 代码看起来没有问题,但是 instance = new VolatileSingleton();其实可以看作三条伪代码:
    memory = allocate(); // 1、分配对象内存空间
    instance(memory); // 2、初始化对象
    instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null
    
    • 1
    • 2
    • 3
    • 步骤2 和 步骤3之间不存在 数据依赖关系,而且无论重排前 还是重排后,程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
    memory = allocate(); // 1、分配对象内存空间
    instance = memory; // 3、设置instance指向刚刚分配的内存地址,此时instance != null,但是对象还没有初始化完成
    instance(memory); // 2、初始化对象
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    可以使用volatile: private static volatile VolatileSingleton instance = null;

    注意:当且仅当满足以下所有条件时,才应该用volatile变量

    • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
    • 该变量不会与其他的状态一起纳入不变性条件中。
    • 在访问变量时不需要加锁。
  • 相关阅读:
    黑五网一来袭,卖家该如何做好旺季备货
    Linux彻底卸载MySQL
    嵌入式开发--赛普拉斯cypress的铁电存储器FM25CL64B
    还记得高中生物书上的莫斯密码吗?利用Python破解摩斯密码的代码示例!
    springboot+vue网上购物商城
    开发常用指令
    Vue.js入门教程(三)
    Java基础(二)
    以太网基础——DoIP报文类型
    我要写整个中文互联网界最牛逼的JVM系列教程 | 「JVM与Java体系架构」章节:为什么要学习JVM?
  • 原文地址:https://blog.csdn.net/weixin_38803409/article/details/126205072