我正在学习易变量。我知道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);
}
}
提前谢谢你!
答案 0 :(得分:8)
当你执行count++
时,这是一个读取,一个增量,然后一个写入。两个线程可以各自执行读取操作,每个线程都执行增量操作,然后每个线程执行写入操作,只产生一个增量。虽然您的读取是原子的,但您的写入是原子的,并且没有缓存任何值,这是不够的。你需要的不仅仅是 - 你需要一个原子读 - 修改 - 写操作,volatile
没有提供。
答案 1 :(得分:2)
count++
基本上是这样的:
// 1. read/load count
// 2. increment count
// 3. store count
count = count + 1;
单独地first
和third
操作是原子的。其中所有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)的增量是使用:getstatic
,iconst_1
,iadd
和putstatic
实现的。
由于这是通过四条指令完成的,并且没有锁定,因此不能期望原子性。另外值得注意的是,即使这是用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。
那么,在你的情况下,看看操作计数++,它不是原子。
答案 5 :(得分:0)
for (int i = 0; i < limit; i++) {
count++;
}
这里 count ++ 具有读取,递增然后写入的三个串行操作,线程1可能首先运行,然后线程调度程序在读取计数值后可以转移到线程2,线程2可以修改计数,在线程调度程序转移到线程1之后,它具有之前读取的先前值,因此这里存在一个竞争条件(我们必须检查并采取行动),但是该操作不是原子完成的,因此最好使用AtomicInteger类,否则将其递增通过同步作为原子单元