Answers:
对于C#和Java,“ volatile”告诉编译器,永远不要缓存变量的值,因为变量的值可能会在程序本身范围之外更改。然后,如果变量“超出其控制范围”更改,编译器将避免可能导致问题的任何优化。
考虑以下示例:
int i = 5;
System.out.println(i);
编译器可以对此进行优化以仅打印5,如下所示:
System.out.println(5);
但是,如果还有另一个可以更改的线程,则i
这是错误的行为。如果另一个线程更改i
为6,则优化版本仍将打印5。
的volatile
关键字防止了这种优化和缓存,并且因此当一个变量可通过另一个线程被改变是有用的。
i
标记为的优化仍然有效volatile
。在Java中,一切都是事前发生的关系。
i
是局部变量,则其他任何线程都无法更改它。如果是字段,则除非,否则编译器无法优化调用final
。我不认为编译器可以基于假设final
未明确声明字段“看起来”的情况来进行优化。
要了解volatile对变量的作用,重要的是要了解变量不是volatile时会发生什么。
当两个线程A和B正在访问一个非易失性变量时,每个线程将在其本地缓存中维护该变量的本地副本。线程A在其本地缓存中所做的任何更改对于线程B都是不可见的。
当将变量声明为volatile时,这实质上意味着线程不应缓存此类变量,或者换句话说,线程不应信任这些变量的值,除非直接从主内存中读取它们。
那么,何时使变量可变?
当您拥有一个可由多个线程访问的变量,并且您希望每个线程都获得该变量的最新更新值时,即使该值已由程序的任何其他线程/进程/外部更新。
易读字段的读取具有语义。这意味着可以确保从volatile变量读取的内存将在随后的任何内存读取之前发生。它阻止编译器进行重新排序,并且如果硬件需要它(CPU排序不充分),它将使用特殊指令使硬件清除易失性读取之后发生但推测性启动得早的所有读取。通过防止在获取负载的问题和其收回之间发生任何投机性负载,从而防止从一开始就发布它们。
易失性字段的写入具有释放语义。这意味着可以保证对volatile变量的任何内存写操作都可以延迟,直到所有先前的内存写操作对其他处理器可见为止。
考虑以下示例:
something.foo = new Thing();
如果foo
是类中的成员变量,并且其他CPU可以访问引用的对象实例something
,则它们可能会看到值foo
更改,然后Thing
构造器中的内存写入才能全局可见!这就是“无序内存”的意思。即使编译器将所有存储都存储在构造函数中,也可以在之前存储到foo
。如果foo
是,volatile
则存储foo
将具有释放语义,并且硬件保证foo
在允许进行写入之前,写入之前的所有写入对于其他处理器都是可见的foo
。
如何对写入进行foo
如此严重的重新排序?如果高速缓存行保持foo
在高速缓存中,并且构造函数中的存储未命中高速缓存,则存储的完成时间可能比对高速缓存未命中的写入要早得多。
英特尔的(糟糕的)Itanium体系结构的内存排序较弱。原始XBox 360中使用的处理器的内存排序较弱。许多ARM处理器(包括非常流行的ARMv7-A)的内存排序都很弱。
开发人员通常看不到这些数据争用,因为诸如锁之类的事情会造成完全的内存障碍,本质上与同时获取和释放语义相同。在获取锁之前,无法推测性地执行锁内部的负载,这些负载会延迟到获取锁为止。没有存储可以在锁释放期间被延迟,释放锁的指令将被延迟,直到在锁内部完成的所有写入都全局可见为止。
一个更完整的示例是“双重检查锁定”模式。此模式的目的是避免为了懒惰初始化对象而必须始终获取锁。
摘自Wikipedia:
public class MySingleton {
private static object myLock = new object();
private static volatile MySingleton mySingleton = null;
private MySingleton() {
}
public static MySingleton GetInstance() {
if (mySingleton == null) { // 1st check
lock (myLock) {
if (mySingleton == null) { // 2nd (double) check
mySingleton = new MySingleton();
// Write-release semantics are implicitly handled by marking
// mySingleton with 'volatile', which inserts the necessary memory
// barriers between the constructor call and the write to mySingleton.
// The barriers created by the lock are not sufficient because
// the object is made visible before the lock is released.
}
}
}
// The barriers created by the lock are not sufficient because not all threads
// will acquire the lock. A fence for read-acquire semantics is needed between
// the test of mySingleton (above) and the use of its contents. This fence
// is automatically inserted because mySingleton is marked as 'volatile'.
return mySingleton;
}
}
在此示例中,MySingleton
构造器中的存储可能在到之前对其他处理器不可见mySingleton
。如果发生这种情况,窥视mySingleton的其他线程将不会获得锁定,并且它们不一定会拾取对构造函数的写入。
volatile
从不阻止缓存。它所做的是保证其他处理器“看到”的写入顺序。存储释放将延迟存储,直到所有未完成的写操作完成并且发出了总线周期,告诉其他处理器如果碰巧已经缓存了相关的行,则放弃/回写其缓存行。负载获取将刷新所有推测的读取,以确保它们不会成为过去的过时值。
head
和都tail
需要保持易变,以防止生产者假设tail
不会改变,并防止消费者假设head
不会改变。另外,head
必须可变,以确保在全局可见之前存储队列数据写入head
是全局可见的。
该volatile关键字在Java和C#不同的含义。
一个字段可以声明为volatile,在这种情况下,Java内存模型可确保所有线程看到的变量值都是一致的。
从volatile关键字的C#参考中:
volatile关键字指示可以通过诸如操作系统,硬件或同时执行的线程之类的程序在程序中修改字段。
在Java中,“易失性”用于告诉JVM变量可以同时由多个线程使用,因此某些通用优化无法应用。
值得注意的是,访问同一变量的两个线程在同一台计算机上的不同CPU上运行的情况。CPU主动缓存其保存的数据非常普遍,因为内存访问比缓存访问慢得多。这意味着,如果在CPU1中更新了数据,则必须立即通过所有高速缓存并到达主内存,而不是在高速缓存决定清除自身时,才可以使CPU2看到更新后的值(再次忽略该途中的所有高速缓存)。
当您读取非易失性数据时,执行线程可能会或可能不会始终获取更新后的值。但是,如果对象是易失性的,线程将始终获取最新的值。
易失性正在解决并发问题。使该值同步。此关键字主要在线程中使用。当多个线程更新相同的变量时。