volatile关键字的作用以及原子类

146 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 15 天,点击查看活动详情

本文内容

  1. 学习volatitle的作用及使用方法。
  2. 学习Java提供的原子类。

volatile修饰符

它不能保证变量的操作是原子性。

粗略的理解,volatile关键字确保应用中的可视性。它的意思是,如果你将一个变量volatile修饰后,如果对这个域产生了写操作,那么所有的读操作就可以看到这个修改,该变量会立即写入到主存中,而读取操作就是在主存中发生的。

简单理解:synchronized保证了写操作的一致性,volatile保证了读操作的一致性。

我们通过一个代码了解一下volatile

代码分析

public class AtomicityTest implements Runnable{
    private int i = 0;
    public int getValue(){
        return i;
    }

    private synchronized void evenIncrement(){
        i++;
        i++;
    }
    @Override
    public void run() {
        while (true){
            evenIncrement();
        }
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        AtomicityTest at = new AtomicityTest();
        exec.execute(at);
        while (true){
            int val = at.getValue();
            if (val % 2 != 0){
                System.out.println(val);
                System.exit(0);
            }
        }
    }
}

此代码中任务执行的是evenIncrement方法,该方法将i每次加2,也就是说i始终是一个偶数。但是当我们执行时会有问题:int val = at.getValue();会取到奇数。这是为什么呢?

  1. 第一个问题:我们看代码,int val = at.getValue();是在主线程中调用的,也就是说明有两个方法正好构成了对同一个域,一个线程在写,一个线程在读。所以i需要用volatile去修饰。

  2. 第二个问题:此外,也需要在主线程和后台线程之间使用适当的同步机制来确保对变量的修改和读取在不同线程之间同步。所以getValue也需要用synchroized去修饰。

  3. 第三个问题:在Java中i++并不是一个原子操作,在JVM指令中,它是分两步的,先要取出来,再操作。

接下去,我们来了解一下原子类。

原子类

原子操作是指一次操作在内存中是不可分的。读取和写入是两个动作,它们在操作时会发生上下文切换,导致不同的任务可能看到不正确的结果,就像上面那个例子。

JavaSE5引入了诸如AtomicIntegerAtomicLongAttomicReference等特殊的原子性变量类,它们提供,下面形式的原子性条件更新操作:

boolean compareAndSet(expectedValue,updateValue);

我们使用AtomicIntteger来改进上面的例子:

public class AtomicIntegerTest implements Runnable{
    private AtomicInteger i = new AtomicInteger(0);
    public int getValue(){
        return i.get();
    }

    public void evenIncrement(){
        i.addAndGet(2);
    }
    @Override
    public void run() {
        while (true){
            evenIncrement();
        }
    }

    public static void main(String[] args) {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("时间到");
                System.exit(0);
            }
        },5000);
        ExecutorService exec = Executors.newCachedThreadPool();
        AtomicIntegerTest atomicIntegerTest = new AtomicIntegerTest();
        exec.execute(atomicIntegerTest);
        while (true){
            int value = atomicIntegerTest.getValue();
            if(value % 2 != 0){
                System.out.println(value);
                System.exit(0);
            }
        }
    }
}

执行后会发现,即使evenIncrement没有使用synchronized,变量域也没有使用volatile,该程序也不会出现奇数的结果,在5秒后自动的终止了。

常用原子类的构造方法,及其基本方法使用

这是一个拓展内容,不属于本篇核心内容,由于篇幅和内容特殊性,有需要的请在另一篇文章查看。

Java并发基础阅读知识:常用原子类的构造方法,以及基本方法使用 - 掘金 (juejin.cn)