为什么Java中的volatile不会更新变量的值?
我已经读过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
我明白,如果我在程序中添加关键字“static”(即“private static volatile int d = 0;”),“d”将增加到4.我知道这是因为d将成为变量全class共享而非每个实例获得副本。
结果如下:
第一个线程: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?
这里有一些误解。 您似乎无法正确理解线程是什么,实例字段是什么以及静态字段是什么。
实例字段是在实例化类时分配的内存位置(即,当您使用VolatileExample v = new VolatileExample()
时,为字段d
分配内存位置)。 要从类中引用该内存位置,请执行this.d
(然后您可以写入该内存位置并从中读取)。 要从类外部引用该内存位置,它必须是可访问的(即,不是private
),然后你会做vd
。 如您所见,类的每个实例都为其自己的字段d
获取自己的内存位置。 因此,如果您有两个不同的VolatileExample
实例,则每个实例都有自己的独立字段d
。
静态字段是一个内存位置,一旦一个类被初始化就会被分配(这就忘记了使用多个ClassLoader
的可能性,只发生一次)。 因此,您可以认为静态字段是某种全局变量 。 要引用该内存位置,您将使用VolatileExample.d
(可访问性也适用(即,如果它是private
,则只能在类中完成))。
最后, 执行线程是由JVM 执行的一系列步骤。 你不能认为一个线程是一个类,或者一个Thread
类的实例,它只会让你感到困惑。 它很简单:一系列步骤。
主要的步骤顺序是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
实例。 这将分配一些内存位置:4个字节用于countdown
,4个字节用于d
。 然后你开始一个新的执行线程: 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
解决了可见性问题,但没有解决primefaces性问题: increment = read from main memory + operation on the value + write to main memory
。 正如您所看到的,2个不同的线程可以读取初始值(0),在其上操作(本地)(获得1),然后将其写入主存储器(两者最终都会写入1) – 2个增量将是被认为只有1。
要解决此问题,必须使增量成为primefaces操作。 为此,您需要使用同步机制 – 互斥锁( synchronized (...) { ... }
或显式锁定) – 或专门为此设计的类: AtomicInteger
。
volatile不会“允许共享”任何东西。 它只是阻止变量被缓存的本地线程,以便立即发生对变量值的更改。 您的d变量是一个实例变量,因此由拥有它的实例拥有。 您需要重新阅读线程教程以重新调整您的假设。
这里有一个不错的参考
volatile
可以使共享安全 (如果单个读或写操作的primefaces性足够),它不会导致共享。
请注意,如果使d
static
,则实际上未指定d
将具有什么值,因为语句d = d + 1不是primefaces的,即线程可能在读取和写入之间中断d。 同步块或AtomicInteger
是典型的解决方案。