小白的JUC学习11_Volatile

本文深入探讨了Java中的volatile关键字,详细分析了其保证可见性、不保证原子性以及防止指令重排序的特性。通过实例展示了volatile在多线程环境中的行为,解释了其在原子操作、指令重排序问题上的局限性,并提供了原子类作为解决方案。此外,还介绍了volatile在单例模式中的应用,以及其背后的内存屏障原理。
摘要由CSDN通过智能技术生成

Volatile


一、Volatile介绍

Volatile 是Java虚拟机提供的轻量级的同步机制

特性:

  • 保证可见性
  • 不保证原子性
  • 保证有序性

我们将围绕以上三点来探讨Volatile的作用

1.1、验证可见性


案例:启动一个线程,并使其死循环状态,通过额外线程修改共享变量使其关闭

  • 注意必须是共享变量(JMM特性)
package com.migu.jmm;

import java.util.concurrent.TimeUnit;

/**
 * 测试共享变量可见性
 */
public class Test {
    // private static boolean f = true; 未使用volatile修饰,会死循环
    private volatile static boolean f = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (f){}
        }).start();
        
        TimeUnit.SECONDS.sleep(3);
        f = false;
    }
}

结果:由于Volatile保证可见性,则使其线程正常关闭

1.2、测试不保证原子性


一、非原子操作

案例:通过多个线程对共享变量自增,看看是否自增过程中,由于由于多个线程并行执行,是否会由于原子性而保证结果正常

  • num++,并不是一个原子性操作
    • 通过对其反汇编【javac -p】,可发现一共分三步
      • 获取值、修改值、写回值
package com.migu.jmm;

/**
 * 测试Volatile不保证原子性(取自狂神例子)
 */
public class VolatileAtomicity {
    private static volatile int num = 0;
    private static void add(){
        num++; 
    }
    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000; j++) {
                    add(); // 每个线程对其自增1000次,一共10000次
                }
            }).start();
        }
        // 当其他线程未执行完毕,main线程则对其让步,每次只要进入运行态就会转到就绪态
        // GC守护线程不一定会执行
        while (Thread.activeCount() > 1) Thread.yield();
        System.out.println(num);

    }
}

输出:有9587、9352、7382.。。。。。。。

看来Volatile只保证了可见性,在某些情况下,由于跑得慢的线程,最后执行,然后此时num的值还是很小,由于可见性原因导致其他线程,num值也跟着变小,结果就变以上那样了

二、原子操作

有一种办法就是对其加锁,我们只需在以上案例中对add方法加锁即可,但不推荐,耗性能

还有一种方案使用原子类

  • 原子类位于java.util.concurrent.atomic包下:是一种支持单变量无锁线程安全编程的类的工具包

对以上案例进行改进:

package com.migu.jmm;

import java.util.concurrent.atomic.AtomicInteger;
/**
 * 测试Volatile不保证原子性(取自狂神例子)
 */
public class VolatileAtomicity {
    private static volatile AtomicInteger atomicInteger = new AtomicInteger();
    private static void add(){
        atomicInteger.getAndIncrement();
    }
    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000; j++) {
                    add(); // 每个线程对其自增1000次,一共10000次
                }
            }).start();
        }
        // 当其他线程未执行完毕,main线程则对其让步,每次只要进入运行态就会转到就绪态
        // GC守护线程不一定会执行,这边大于1即可
        while (Thread.activeCount() > 1) Thread.yield();
        System.out.println(atomicInteger.get());

    }
}

输出:10000

为什么能出现这样的效果,是由于CAS,通过unsafe类直接在内存修改,具体讲到锁时在阐述

1.3、指令重排序


一、指令重排介绍

指令重排是一种优化

编译器可能会进行重排、并行指令也可能会重排、内存系统也可能会进行重排,但是重排是有条件

不能违背happen-before原则,即数据之间的依赖性

来瞅瞅这个例子:

int x = 1;
int y = 2;
x = x + 1;
y = x + y;

其中第三条和第四条指令的执行需要依赖于第一条和第二条

也就是重排序的结果不可能为: 4–>3–>2–>1这种情况

但是在多线程环境下有可能会发生某些错误

二、产生依赖的三种情况

以下三种情况不会发生指令重排【针对同一个变量】

名称代码示例说明
写后读a = 1;b = a;写一个变量之后,再读这个位置。
写后写a = 1;a = 2;写一个变量之后,再写这个变量。
读后写a = b;b = 1;读一个变量之后,再写这个变量。

三、多线程下的指令重排

以下案例验证多线程环境下发生指令重排时,会出现什么情况

通过多次的重复读写,判断是否存在指令重排的错误

  • 下面案例中a变量和flag变量并无直接关系(则可测试)
package com.migu;


public class Test {
    private int a = 0;
    private boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        for (int i = 0; i < 500000; i++) {
        	// 两块线程启动进行测试
            new Thread(() -> {
                test.write();
            }).start();

            new Thread(() -> {
                test.read();
            }).start();
			
			// 这块循环只是为了初始化,具体无关
            while (true) {
                if (Thread.activeCount() == 1) {
                    // 每进行一次写读,则对其初始化
                    test.a = 0;
                    test.flag = false;
                    break;
                }
            }
        }
    }
	// 看看是否会先执行flag=true,再去执行a=1
    public void write() {
        a = 1;
        flag = true;
    }

    public void read() {
        if (flag && a == 0) {
            System.out.println(Thread.currentThread().getName() + "----" + a + "-------");
        }
    }
}

输出结果:(在某次读写过程中,线程A的write只执行了flag = true,便被线程B的read执行,而导致出现以下的场景)

结论:多线程环境下的指令重排序会导致结果异常

解决方案:为其中任何一个变量,加一个volatile关键字

四、Volatile原理

原因是通过Volatile修饰会为该变量的操作指令中的前后都加一个内存屏障,防止重排序,那么那些优化机制就不会对其重排序(更多细节就不懂了)

五、注意点

使用synchronized可以代替volatile关键字吗?

// synchronized同步块之间虽然是原子性操作,但是里面是非原子操作
// 所可以是可能发生指令重排的
// Synchronized虽然不能防止指令重排
// 但是至少能保证最后结果是不会出错的

六、应用场景

单例模式:双重检验懒汉式

  • 注意创建一个对象并不是一个原子操作,所以在这段创建过程是会存在指令排序,且非原子操作,所以执行过程是存在被其他线程读取并从中修改的情况
    • 1、分配内存空间
    • 2、执行构造方法,创建并初始化对象
    • 3、执行引用

在单线程情况下上面步骤是不会产生影响

package com.migu.jmm;

public class Singleton {
    public static volatile Singleton instance;
    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

单例模式更多优化可查看专门博客教学(比如什么防反射之类的)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值