volatile关键字的使用

本文详细探讨了Java中volatile关键字的作用,通过一个实例展示了在多线程环境下,不使用volatile可能导致的问题。解释了线程间变量副本导致的数据不一致,并提供了解决方案。同时提到了Atomic类在并发编程中的应用,以及volatile与Atomic类的对比。此外,还讨论了在Kotlin中如何实现类似功能,并给出了懒汉式单例模式中volatile的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

示例代码如下:

public class Main {
    static boolean run = true;
    static DateFormat format = new SimpleDateFormat("HH:mm:ss");

    public static void main(String[] args) throws Exception {
        new Thread(() -> {
            long start = System.currentTimeMillis();
            while (run) {
                if (System.currentTimeMillis() - start >= 2000) {
                    start = System.currentTimeMillis();
                    System.out.println("当前时间:" + format.format(start));
                }
            }
        }).start();

        Thread.sleep(7000);
        run = false;
        System.out.println("已经设置run = " + run  + ",当前时间:" + format.format(System.currentTimeMillis()));
    }

}

运行结果如下:

当前时间:17:26:27
当前时间:17:26:29
当前时间:17:26:31
已经设置run = false,当前时间:17:26:32
当前时间:17:26:33

代码的功能为,在子线程中是一个死循环,每两秒打印一下当前时间,在main线程中睡眠7秒,则理论上子线程可以打印3个时间,因为每两秒打印一个,6秒打印3个,第7秒的时候main线程把run变量设置为false,理论上此时while循环就结束了,但是并没有,while循环一直在转,直到第8秒的时候打印了第4个时间之后while循环才结束,也就是说在子线程中,第7秒到第8秒的时候,while语句拿到的run变量一直是true,所以才没有退出循环。

同样的代码,复制到Android项目中运行又没这个问题。神奇。

问了公司同事,说是每个线程在访问run变量时,都会拷贝一份副本到各自线程的堆栈中,所以我们在主线程中修改run变量只是修改了一个副本,而子线程中的run是另一个副本,没有得到及时的更新,所以才出现了问题,解决方案就是在变量上加入volatile修饰符,如下:

volatile static boolean run = true;

再次运行就没问题了。volatile的功能可以简单的理解为不再使用副本了,所以不会有之前的问题。

如果不加volatile 修饰符,代码稍作修改又没问题了,如下:

while (run) {
    if (System.currentTimeMillis() - start >= 2000) {
        start = System.currentTimeMillis();
        System.out.println("当前时间:" + format.format(start));
    }
    
    if (run) {
    
    }
}

代码很简单,就是在while中加入了一个if判断,其它代码都不变,但是运行是OK。或者我们把while获取时间的代码提取到一个end变量,然后运行也是没问题的,如下:

while (run) {
    long end = System.currentTimeMillis();
    if (end - start >= 2000) {
        start = System.currentTimeMillis();
        System.out.println("当前时间:" + format.format(start));
    }
}

至于这些神奇的现象,我们就不管它了,总之就记得,在使用多线程的时候,如果多个线程要访问同一个数据,这个数据就加volatile修饰,这个数据一般指8大基本数据类型,如果是对象类型,可以不用加的,因为一般对象我们创建后就是访问这个对象中的属性,很少会再创建一个新的对象。

在懒汉式单例中,也是需要加入volatile修饰的,示例如下:

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

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

如果我们没加volatile 的话,IntelliJ也会提示我们加的,如下:
在这里插入图片描述
对于基本数据类型的属性,如果需要在多线程中又读又写的话,尽量使用对应的同步对象:

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong

这样的话就不需要加volatile修饰了,因为这些对象里面包装的基本数据已经加入了volatile修饰了。如果我们查看它们的源码,会发现对应的getset方法并没有加入同步代码块,因为它包装的value已经是volatile修饰的了,所以简单的一个set赋值,其实就是一行语句的执行,没必要加入同步代码,影响效率,对于AtomicBoolean,它封装的value是用int类型的,用0表示false,1表示true。在addAndGet函数上,还看到了Unsafe的使用,据说它是sun公司留的后门,用这个类可以申请内存,且不受jvm控制,具体可百度。

使用AtomicBoolean修改前面的Demo,如下:

public class Main {
    static AtomicBoolean run = new AtomicBoolean(true);
    static DateFormat format = new SimpleDateFormat("HH:mm:ss");

    public static void main(String[] args) throws Exception {
        new Thread(() -> {
            long start = System.currentTimeMillis();
            while (run.get()) {
                if (System.currentTimeMillis() - start >= 2000) {
                    start = System.currentTimeMillis();
                    System.out.println("当前时间:" + format.format(start));
                }
            }
        }).start();

        Thread.sleep(7000);
        run.set(false);
        System.out.println("已经设置run = " + run  + ",当前时间:" + format.format(System.currentTimeMillis()));
    }

}

kotlin中,没有volatile关键字,可以在变量上加@Volatile注解实现同样的功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

android_cai_niao

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值