Java volatile关键字未按预期工作

时间:2017-02-23 09:56:08

标签: java multithreading

我正在学习易变量。我知道volatile的作用,我为Volatile变量编写了一个示例程序但没有按预期工作。

为什么“count”的最终值有时会低于2000.我使用了volatile,因此系统不应该缓存“count”变量,值应该始终为2000.

当我使用synchronized方法时,它工作正常但不是volatile关键字。

public class Worker {

private volatile int count = 0;
private int limit = 10000;

public static void main(String[] args) {
    Worker worker = new Worker();
    worker.doWork();
}

public void doWork() {
    Thread thread1 = new Thread(new Runnable() {
        public void run() {
            for (int i = 0; i < limit; i++) {

                    count++;

            }
        }
    });
    thread1.start();
    Thread thread2 = new Thread(new Runnable() {
        public void run() {
            for (int i = 0; i < limit; i++) {

                    count++;

            }
        }
    });
    thread2.start();

    try {
        thread1.join();
        thread2.join();
    } catch (InterruptedException ignored) {}
    System.out.println("Count is: " + count);
}
}

提前谢谢你!

6 个答案:

答案 0 :(得分:8)

当你执行count++时,这是一个读取,一个增量,然后一个写入。两个线程可以各自执行读取操作,每个线程都执行增量操作,然后每个线程执行写入操作,只产生一个增量。虽然您的读取是原子的,但您的写入是原子的,并且没有缓存任何值,这是不够的。你需要的不仅仅是 - 你需要一个原子读 - 修改 - 写操作,volatile没有提供。

答案 1 :(得分:2)

count++基本上是这样的:

// 1. read/load count
// 2. increment count
// 3. store count
count = count + 1;

单独地firstthird操作是原子的。其中所有3个together都不是原子的。

答案 2 :(得分:1)

i++ is not atomic in Java。因此两个线程可以同时读取,两者都计算+1为相同的数字,并且两者都存储相同的结果。

使用javac inc.java

进行编译
public class inc {
    static int i = 0;
    public static void main(String[] args) {
        i++;
    }
}

使用javap -c inc读取字节码。我已将其修剪为仅显示函数main()

public class inc {
  static int i;

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field i:I
       3: iconst_1
       4: iadd
       5: putstatic     #2                  // Field i:I
       8: return
}

我们看到(使用静态int)的增量是使用:getstaticiconst_1iaddputstatic实现的。

由于这是通过四条指令完成的,并且没有锁定,因此不能期望原子性。另外值得注意的是,即使这是用1条指令完成的,我们也可能运气不好(引用this thread中的用户“Hot Licks”评论):

  

即使在实现“增量存储位置”指令的硬件上,也无法保证线程安全。仅仅因为一个操作可以表示为一个操作员,所以没有说明它的线程安全性。

如果你真的想解决这个问题,你可以使用AtomicInteger,它具有原子性保证:

final AtomicInteger myCoolInt = new AtomicInteger(0);
myCoolInt.incrementAndGet(1);

答案 3 :(得分:1)

当您使用synchronized方法时,它按预期工作,因为它确保如果其中一个线程执行该方法,则其他调用方线程的执行将暂停,直到当前正在执行的线程退出该方法。在这种情况下,整个读 - 增量 - 写周期都是原子的。

来自tutorial

  

首先,两次调用同步方法是不可能的   在同一个对象上进行交错。当一个线程正在执行时   对象的synchronized方法,所有其他调用的线程   同一对象块的同步方法(暂停执行)   直到第一个线程完成对象。

     

其次,当同步方法退出时,它会自动建立一个   发生在与任何后续调用a之间的关系之前   同一对象的同步方法。这保证了变化   所有线程都可以看到对象的状态。

当你使用volatile时(正如其他人所解释的那样)这个循环不是原子的,因为使用这个关键字并不能确保在get和增量之间的其他线程上没有其他写入变量这个帖子的步骤。

对于原子计数而不是synchronized关键字,您可以使用例如AtomicInteger

public class Worker {
    private AtomicInteger count = new AtomicInteger(0);
    private int limit = 10000;

    public static void main(String[] args) {
        Worker worker = new Worker();
        worker.doWork();
    }

    public void doWork() {
        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < limit; i++)
                    count.getAndIncrement();
            }
        });
        thread1.start();
        Thread thread2 = new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < limit; i++)
                    count.getAndIncrement();
            }
        });

        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException ignored) {
        }
        System.out.println("Count is: " + count);
    }
}

这里getAndIncrement()确保原子读 - 增量 - 设置周期。

答案 4 :(得分:1)

内存可见性和原子性是多线程中两个不同但常见的问题。使用synchronized关键字时,它通过获取锁来确保这两者。而volatile只解决了内存可见性问题。 在他的书Concurrency in practice中,Brain Goetz解释了何时应该使用volatile。

  1. 写入变量不依赖于其当前值,或者您 可以确保只有一个线程更新该值;
  2. 变量不参与其他状态的不变量 变量;
  3. 变量所在的任何其他原因都不需要锁定 被访问。
  4. 那么,在你的情况下,看看操作计数++,它不是原子。

答案 5 :(得分:0)

for (int i = 0; i < limit; i++) {
    count++;
}

这里 count ++ 具有读取,递增然后写入的三个串行操作,线程1可能首先运行,然后线程调度程序在读取计数值后可以转移到线程2,线程2可以修改计数,在线程调度程序转移到线程1之后,它具有之前读取的先前值,因此这里存在一个竞争条件(我们必须检查并采取行动),但是该操作不是原子完成的,因此最好使用AtomicInteger类,否则将其递增通过同步作为原子单元