我已经读过Java中的“volatile”允许不同的线程访问同一个字段并查看其他线程对该字段所做的更改。如果是这种情况,我预测当第一个和第二个线程完全运行时,“d”的值将增加到4.但是,每个线程将“d”增加到值2。
public class VolatileExample extends Thread {
private int countDown = 2;
private volatile int d = 0;
public VolatileExample(String name) {
super(name);
start();
}
public String toString() {
return super.getName() + ": countDown " + countDown;
}
public void run() {
while(true) {
d = d + 1;
System.out.println(this + ". Value of d is " + d);
if(--countDown == 0) return;
}
}
public static void main(String[] args) {
new VolatileExample("first thread");
new VolatileExample("second thread");
}
}
运行此程序的结果是:
第一个帖子:countDown 2. d的值是1
第二个帖子:countDown 2. d的值是1
第一个帖子:countDown 1. d的值是2
第二个帖子:countDown 1. d的值是2
据我所知,如果我添加关键字“静态”程序, (即“private static volatile int d = 0;”),“d”将增加到4。 而且我知道这是因为d将成为整个类共享的变量,而不是每个实例都获得副本。
结果如下:
第一个帖子:countDown 2. d的值是1
第一个帖子:countDown 1. d的值是3
第二个帖子:countDown 2. d的值是2
第二个帖子:countDown 1. d的值是4
我的问题是,为什么“private volatile int d = 0;”如果volatile应该允许在两个线程之间共享“d”,那么会产生类似的结果?也就是说,如果第一个线程将d的值更新为1,那么为什么第二个线程不会将d的值作为1而不是0来获取?
答案 0 :(得分:8)
volatile不会“允许共享”任何内容。它只是阻止变量被缓存的本地线程,以便立即发生对变量值的更改。您的d变量是一个实例变量,因此由拥有它的实例拥有。您需要重新阅读线程教程以重新调整您的假设。
一个不错的参考是here
答案 1 :(得分:3)
这里有一些误解。您似乎无法正确理解线程是什么,实例字段是什么以及静态字段是什么。
实例字段是一个在实例化类时分配的内存位置(即,当您d
时,为字段VolatileExample v = new VolatileExample()
分配内存位置。要从类中引用该内存位置,请执行this.d
(然后您可以写入和读取该内存位置)。要从类外部引用该内存位置,它必须是可访问的(即,不是private
),然后您执行v.d
。如您所见,类的每个实例都为其自己的字段d
获取自己的内存位置。因此,如果您有两个VolatileExample
的不同实例,则每个实例都有自己的独立字段d
。
静态字段是一个内存位置,在初始化类时会被分配(忘记使用多个ClassLoader
的可能性,恰好发生一次)。因此,您可以认为静态字段是某种全局变量。要引用该内存位置,您可以使用VolatileExample.d
(也可以使用辅助功能(例如,如果它是private
,则只能在课程内完成)。
最后,执行线程是由JVM执行的一系列步骤。您不能将线程视为类或类Thread
的实例,它只会让您感到困惑。它很简单:一系列步骤。
main 步骤序列是main(...)
方法中定义的内容。这是您启动程序时JVM将开始执行的一系列步骤。
如果要启动一个新的执行线程同时运行(即,您希望同时运行一个单独的步骤序列),在Java中,您可以通过创建类Thread
的实例来执行此操作。调用其start()
方法。
让我们稍微修改你的代码,以便更容易理解发生了什么:
public class VolatileExample extends Thread {
private int countDown = 2;
private volatile int d = 0;
public VolatileExample(String name) {
super(name);
}
public String toString() {
return super.getName() + ": countDown " + countDown;
}
public void run() {
while(true) {
d = d + 1;
System.out.println(this + ". Value of d is " + d);
if(--countDown == 0) return;
}
}
public static void main(String[] args) {
VolatileExample ve1 = new VolatileExample("first thread");
ve1.start();
VolatileExample ve2 = new VolatileExample("second thread");
ve2.start();
}
}
第VolatileExample ve1 = new VolatileExample("first thread");
行创建VolatileExample
的实例。这将分配一些内存位置:countdown
为4个字节,d
为4个字节。然后启动一个新的执行线程:ve1.start();
。该执行线程将访问(读取和写入)本段前面描述的存储器位置。
下一行VolatileExample ve2 = new VolatileExample("second thread");
创建另一个VolatileExample
实例,它将分配2个新的内存位置:ve2的countdown
为4个字节,ve2的d
为4个字节。然后,您启动一个执行线程,它将访问这些新的内存位置,而不是之前段落中描述的位置。
现在,无论是否有volatile
,您都会看到您有两个不同的字段d
:每个线程都在不同的字段上运行。 因此,您认为d
会增加到4是不合理的,因为没有单d
。
如果使d
为静态字段,则只有两个线程(假设)在同一个内存位置上运行。只有这样才会发挥volatile
的作用,因为只有这样你才能在不同的线程之间共享一个内存位置。
如果你创建一个字段volatile
,你可以保证写入直接进入主存储器并且读取直接来自主存储器(即,它们不会被高速缓存 - 非常快 - 处理器本地缓存,操作需要更长时间,但保证其他线程可见。)
但是,它不会保证您会在d
上看到值4。那是因为volatile
解决了可见性问题,而不是原子性问题:increment = read from main memory + operation on the value + write to main memory
。正如您所看到的,2个不同的线程可以读取初始值(0),在其上操作(本地)(获得1),然后将其写入主存储器(两者最终都会写入1) - 2个增量将是被认为只有1。
要解决此问题,必须使增量成为原子操作。为此,您需要使用同步机制 - 互斥锁(synchronized (...) { ... }
或显式锁定) - 或专门为此设计的类:AtomicInteger
。
答案 2 :(得分:1)
volatile
可以共享安全(如果单个读取或写入操作的原子性足够),它不会导致共享。
请注意,如果您创建d
static
,则实际上未指定值d
将具有什么值,因为语句d = d + 1不是原子的,即线程可能是在阅读和写作之间中断d。同步块或AtomicInteger
是典型的解决方案。