Java Volatile 变量仍然导致竞争条件

问题描述

我正在对处理多线程 Java 应用程序中的竞争条件的不同方法进行实验。诸如原子变量、同步之类的策略效果很好,但我认为使用 volatile 变量时没有解决问题。这里的代码输出供参考。

您能否就 volatile 变量仍然导致竞争条件的原因进行指导?

package com.shyam.concurrency;


public class main {
    public static void main(String[] args) {

   demoClass dm1 = new demoClass();
        Thread th1 = new Thread(()->{
        int i =0;
        do {
            i++;
            dm1.setCounter();
            dm1.setAtomicCounter();
            dm1.setSyncCounter();
            dm1.setVolatileCounter();
        } while (i < 100000);
        });

        Thread th2 = new Thread(()->{
            int i =0;
            do {
                i++;
                dm1.setCounter();
                dm1.setAtomicCounter();
                dm1.setSyncCounter();
                dm1.setVolatileCounter();
            } while (i < 100000);

        });

        th1.start();
        th2.start();

        try {
            th1.join();
            th2.join();
        } catch (InterruptedException e) {
            e.printstacktrace();
        }
        System.out.println("normal counter(Race condition) : " + dm1.getCounter() );
        System.out.println("Synchronized counter is :" + dm1.getSyncCounter());
        System.out.println("Atomic counter is :" + dm1.getAtomicCounter());
        System.out.println("Volatile counter is :" + dm1.getVolatileCounter());

具有递增逻辑的代码在这里

package com.shyam.concurrency;

import java.util.concurrent.atomic.AtomicInteger;

public class demoClass {
    private  int counter ;
    private int syncCounter;
    private volatile  int volatileCounter = 0;
    private AtomicInteger  atomicCounter  = new AtomicInteger() ;


 
    public int getAtomicCounter() {
        return atomicCounter.intValue();
    }

    public void setAtomicCounter() {
        this.atomicCounter.addAndGet(1);
    }

    public int getCounter() {
        return counter;
    }

    public void setCounter() {
        this.counter++;
    }
    public synchronized int getSyncCounter() {
        return syncCounter;
    }

    public synchronized void setSyncCounter() {
        this.syncCounter++;
    }

    public int getVolatileCounter() {
        return volatileCounter;
    }

    public void setVolatileCounter() {
        this.volatileCounter++;
    }
}

这是我得到的输出

normal counter(Race condition) : 197971
Synchronized counter is :200000
Atomic counter is :200000
Volatile counter is :199601

解决方法

可见性与原子性

volatile 只解决了可见性的问题。这意味着每个线程都会看到变量的当前值,而不是可能看到过时的缓存值。

您的线路:

this.volatileCounter++;

…正在执行多个操作:

  • 获取该变量的当前值
  • 增加该值
  • 将新值存储在该变量中

这组操作不是原子的

当一个线程已获取该值但尚未递增并存储新值时,第二个线程可能会访问相同的当前值。两个线程都增加相同的初始值,因此两个线程产生并保存相同的冗余新值。

例如,两个或多个线程可能访问值 42。然后所有这些线程将增加到 43,每个线程将存储 43。这个数字 43 将一遍又一遍地存储。其他一些线程甚至可能已经看到了这 43 次写入中的一个,然后递增并存储了 44。尚未写入其 43 的剩余线程之一将最终覆盖 44 的写入。因此,您不仅可能会浪费一些增加的尝试从而无法向前移动数字,您实际上可能会看到数字向后移动(实际上是递减)。

如果您想使用 volatile,您必须保护代码以使多个操作原子化。 synchronized 关键字就是这样一种解决方案。

就我个人而言,我更喜欢使用 AtomicInteger 方法。如果您在任何访问尝试之前实例化 AtomicInteger,并且从不替换该实例,则该 AtomicInteger 的引用变量的可见性不是问题。没有机会获得陈旧的缓存值意味着没有可见性问题。关于对其有效载荷的无情访问,AtomicInteger 的方法为简单操作提供了原子性(显然,因此得名),

要详细了解可见性问题,请研究 Java Memory Model。并阅读 Brian Goetz 等人撰写的优秀书籍 Java Concurrency In Practice