java中的Volatile关键字使用

本文深入解析Java中的Volatile关键字,探讨其在多线程环境下的作用及正确使用方法,包括volatile与happens-before原则的关系。


java中的Volatile关键字使用

在本文中,我们会介绍java中的一个关键字volatile。 volatile的中文意思是易挥发的,不稳定的。那么在java中使用是什么意思呢?

我们知道,在java中,每个线程都会有个自己的内存空间,我们称之为working memory。这个空间会缓存一些变量的信息,从而提升程序的性能。当执行完某个操作之后,thread会将更新后的变量更新到主缓存中,以供其他线程读写。

因为变量存在working memory和main memory两个地方,那么就有可能出现不一致的情况。 那么我们就可以使用Volatile关键字来强制将变量直接写到main memory,从而保证了不同线程读写到的是同一个变量。

什么时候使用volatile

那么我们什么时候使用volatile呢?当一个线程需要立刻读取到另外一个线程修改的变量值的时候,我们就可以使用volatile。我们来举个例子:

public class VolatileWithoutUsage {
    private  int count = 0;

    public void incrementCount() {
        count++;
    }
    public int getCount() {
        return count;
    }
}

这个类定义了一个incrementCount()方法,会去更新count值,我们接下来在多线程环境中去测试这个方法:

    @Test
    public void testWithoutVolatile() throws InterruptedException {
        ExecutorService service= Executors.newFixedThreadPool(3);
        VolatileWithoutUsage volatileWithoutUsage=new VolatileWithoutUsage();

        IntStream.range(0,1000).forEach(count ->service.submit(volatileWithoutUsage::incrementCount) );
        service.shutdown();
        service.awaitTermination(1000, TimeUnit.MILLISECONDS);
        assertEquals(1000,volatileWithoutUsage.getCount() );
    }

运行一下,我们会发现结果是不等于1000的。


java.lang.AssertionError: 
Expected :1000
Actual   :999

这是因为多线程去更新同一个变量,我们在上篇文章也提到了,这种情况可以通过加Synchronized关键字来解决。

那么是不是我们加上Volatile关键字后就可以解决这个问题了呢?

public class VolatileFalseUsage {
    private volatile int count = 0;

    public void incrementCount() {
        count++;
    }
    public int getCount() {
        return count;
    }

}

上面的类中,我们加上了关键字Volatile,我们再测试一下:

    @Test
    public void testWithVolatileFalseUsage() throws InterruptedException {
        ExecutorService service= Executors.newFixedThreadPool(3);
        VolatileFalseUsage volatileFalseUsage=new VolatileFalseUsage();

        IntStream.range(0,1000).forEach(count ->service.submit(volatileFalseUsage::incrementCount) );
        service.shutdown();
        service.awaitTermination(5000, TimeUnit.MILLISECONDS);
        assertEquals(1000,volatileFalseUsage.getCount() );
    }

运行一下,我们会发现结果还是错误的:

java.lang.AssertionError: 
Expected :1000
Actual   :992
~~

为什么呢? 我们先来看下count++的操作,count++可以分解为三步操作,1. 读取count的值,2.给count加1, 3.将count写回内存。添加Volatile关键词只能够保证count的变化立马可见,而不能保证1,2,3这三个步骤的总体原子性。 要实现总体的原子性还是需要用到类似Synchronized的关键字。

下面看下正确的用法:

~~~java
public class VolatileTrueUsage {
    private volatile int count = 0;

    public void setCount(int number) {
        count=number;
    }
    public int getCount() {
        return count;
    }
}
    @Test
    public void testWithVolatileTrueUsage() throws InterruptedException {
        VolatileTrueUsage volatileTrueUsage=new VolatileTrueUsage();
        Thread threadA = new Thread(()->volatileTrueUsage.setCount(10));
        threadA.start();
        Thread.sleep(100);

        Thread reader = new Thread(() -> {
            int valueReadByThread = volatileTrueUsage.getCount();
            assertEquals(10, valueReadByThread);
        });
        reader.start();
    }

Happens-Before

从java5之后,volatile提供了一个Happens-Before的功能。Happens-Before 是指当volatile进行写回主内存的操作时,会将之前的非volatile的操作一并写回主内存。

public class VolatileHappenBeforeUsage {

    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;              // 1 线程A修改共享变量
        flag = true;        // 2 线程A写volatile变量
    }
}

上面的例子中,a是一个非volatile变量,flag是一个volatile变量,但是由于happens-before的特性,a 将会表现的和volatile一样。

本文的例子可以参考:https://github.com/ddean2009/learn-java-concurrency/tree/master/volatile

更多精彩内容且看:

更多教程请参考 flydean的博客

Javavolatile关键字具有重要作用,主要体现在以下几个方面: ### 保证可见性 在Java内存模型(JMM)中,每个线程都有自己的工作内存,变量在主内存中存储。当线程操作变量时,会先将变量从主内存拷贝到工作内存,操作完成后再写回主内存。普通变量的修改对其他线程的可见性没有保障,即一个线程修改了变量的值,其他线程可能无法及时看到最新值。而使用volatile关键字修饰的变量,保证了该变量在多个线程之间的可见性。当一个线程修改了volatile变量的值,会立即将新值刷新到主内存,其他线程在使用该变量时,会从主内存重新读取最新的值,而不是使用自己工作内存中的旧值 [^1] [^2]。 ### 部分保证原子性 volatile只能保证单个读或写操作的原子性,但不能保证复合操作(如自增、复合赋值等)的原子性。例如,`count++` 这样的操作实际上包含读取、修改和写入三个步骤,volatile无法保证这三个步骤的原子性,因此在多线程环境下仍然可能导致数据不一致。只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如 `n = m + 1`。当变量的值由自身的上一个决定时,如 `n=n+1`、`n++` 等,volatile关键字将失效。所以在使用volatile关键字时要慎重,如果自己没有把握,可以使用synchronized来代替volatile [^4] [^5]。 ### 保证有序性 volatile关键字能禁止指令重排序。在Java中,为了提高程序的执行效率,编译器和处理器可能会对指令进行重排序。重排序可能会影响程序在多线程环境下的正确性。使用volatile关键字修饰的变量,会禁止编译器和处理器对其相关的指令进行重排序,保证代码的执行顺序与编写顺序一致,从而保证程序在多线程环境下的有序性 [^1]。 以下是一个简单的示例代码,展示了volatile关键字保证可见性的作用: ```java public class VolatileVisibilityExample { private volatile boolean flag = false; public void changeFlag() { flag = true; } public void checkFlag() { while (!flag) { // 等待flag变为true } System.out.println("Flag is now true!"); } public static void main(String[] args) { VolatileVisibilityExample example = new VolatileVisibilityExample(); Thread t1 = new Thread(example::checkFlag); Thread t2 = new Thread(example::changeFlag); t1.start(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); } } ``` 在上述代码中,如果 `flag` 变量没有使用 `volatile` 关键字修饰,`t1` 线程可能无法及时看到 `t2` 线程对 `flag` 变量的修改,从而导致 `t1` 线程一直处于等待状态。而使用 `volatile` 关键字后,`t1` 线程能及时看到 `flag` 变量的修改,跳出循环并输出信息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

flydean程序那些事

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

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

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

打赏作者

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

抵扣说明:

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

余额充值