美文网首页
Volatile关键字与三大特征的关系

Volatile关键字与三大特征的关系

作者: CoderInsight | 来源:发表于2023-10-13 09:36 被阅读0次

    在Java虚拟机中通过Volatile关键字提供了轻量级的同步机制:保证可见性、不保证原子性、可禁止指令重排

    1. 当一个变量被Volatitle修饰了之后,JMM会把该线程的本地内存的共享变量刷新到主内存;同时会将本地内存置为无效,线程接下来会从主内存中读取共享变量。 ==> 也就说使用主内存保证了可见性。
    2. 对于指令重排我们知道是在不影响单线程程序执行结果的前提下JMM优化的一种结果。那么可以使用Volatitle 禁止指令重排,从而实现有序性。
    3. 而有序性的详细实现是基于 Volatilehappens-befor 关系:其实就是一个传递性的连接关系,A > B, B > C => A > C,这种连接关系就是 happens-befor 关系。(补充数学原理:偏序关系 -> 偏序关系是集合中元素之间的一种关系,它满足反身性(对于任意元素a,a和自己存在关系)、反对称性(如果a和b存在关系,且b和a存在关系,则a和b必须相等)和传递性(如果a和b存在关系,b和c存在关系,则a和c也必须存在关系)。

    1),应用场景:状态变量标记

    对于某个状态变量被多个线程共享的时候,其他的线程需要能及时感知到这个变量的变化情况,则可以通过volatile关键字修饰变量,以此可以保证修改对其他线程立刻可见(实现的原理:将变量直接放在主内存中,并且不会被线程缓存)。另外这种方式对于读写操作都是无锁的,它无法完全替代 synchronized、Lock,因为它并没有提供原子性,但是基于多线程共享层面来讲,由于不存在加锁和释放锁的动作,所以其成本是低于synchronized、Lock 的。

    2),应用场景:单例模式

    典型双重检查锁定(DCL),如下代码(创建对象)分析:instance= new Singleton(); 这段代码其实是分为三步执行:

    1. instance 分配内存空间
    2. 初始化 instance
    3. instance 指向分配的内存地址

    但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getInstance() 后发现 instance 不为空,因此返回 instance,但此时 instance 还未被初始化。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行

    class Singleton{
        // 通过volatile禁止指令重排
        private volatile static Singleton instance = null;
        private Singleton() {
        }
        // 通过静态方法将创建对象的方法提供给外部调用
        public static Singleton getInstance() {
            //第一次加锁是为了性能考虑,因为线程安全发生在对象初始化,如果第一次不判断相当于全局控制,造成浪费。
            if(instance == null) {
                synchronized (Singleton.class) {
                    // 双检锁:即通过两次判断来避免两个线程同时创建对象,这样当第一个创建对象完成,第二个会直接结束,不再创建
                    if(instance==null)
                    {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    3),Volatile为啥不能保证原子性?

    因为Volatile关键字是通过将变量保存到主内存中实现的可见性,即便多个线程对于该变量的值是可见的,但是因为多线程竞争阻塞(比如计算i++),可能会出现线程A、B同时读到了变量的值0,但是A阻塞了,B获取将值加1,改变了变量的值,但是对于线程A来说,也早就读取到了变量的值为0此时它也会加1,那么并不会出现结果2,而是由1到1,即最终仍然是1的情况。

    public class DemoTest {  
    // 如果是使用volatile修改的i++,是无法保证原子性的,每次输出的结果都是不一致的
    //    static volatile int  i = 0;  
        // 使用并发安全的集合,此时计算结果是正确的100000
        static AtomicInteger atomicInteger = new AtomicInteger();  
        public static void main(String[] args) throws InterruptedException {  
            for (int j = 0; j < 2; j++) {  
                new Thread(  
                        () -> {  
                            for (int k = 0; k < 50000; k++) {  
    //                            i++;  
                                atomicInteger.incrementAndGet();  
                            }  
                            System.out.println("线程执行完毕");  
                        }  
                ).start();  
            }  
            // 当前等待是必然要有的,不然计算结果始终为0
            Thread.sleep(1000);  
    //        System.out.println("最终计算结果:" + i);  
            System.out.println("最终计算结果:" + atomicInteger.get());  
        }  
    }
    

    相关文章

      网友评论

          本文标题:Volatile关键字与三大特征的关系

          本文链接:https://www.haomeiwen.com/subject/drxabdtx.html