我所知道的 易失写 之前发生 易读,所以我们总会看到volatile变量中最新鲜的数据。我的问题主要涉及这个词 之前发生 它在哪里发生?我写了一段代码来澄清我的问题。
class Test {
volatile int a;
public static void main(String ... args) {
final Test t = new Test();
new Thread(new Runnable(){
@Override
public void run() {
Thread.sleep(3000);
t.a = 10;
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
System.out.println("Value " + t.a);
}
}).start();
}
}
(为清楚起见,省略了try catch块)
在这种情况下,我总是看到要在控制台上打印的值0。没有 Thread.sleep(3000);
我总是看到值10.这是一个发生在关系之前的情况还是打印'值10',因为线程1开始早一点线程2?
很高兴看到每个程序启动时带有和不带volatile变量的代码行为都不同的例子,因为上面代码的结果仅取决于(至少在我的情况下)线程顺序和线程休眠。
您会看到值0,因为在写入之前执行了读取操作。并且您看到值10,因为写入在读取之前执行。
如果您希望测试具有更多不可预测的输出,则应该让两个线程等待CountDownLatch,以使它们同时启动:
final CountDownLatch latch = new CountDownLatch(1);
new Thread(new Runnable(){
@Override
public void run() {
try {
latch.await();
t.a = 10;
}
catch (InterruptedException e) {
// end the thread
}
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
try {
latch.await();
System.out.println("Value " + t.a);
}
catch (InterruptedException e) {
// end the thread
}
}
}).start();
Thread.sleep(321); // go
latch.countDown();
发生之前真正与写入发生在任何后续读取之前。如果写没有发生,那真的没有关系。由于写线程处于休眠状态,因此在写入发生之前执行读取。
要观察行动中的关系,你可以有两个变量,一个是volatile,另一个是volatile。根据JMM,它表示在易失性写入发生之前,在易失性读取之前写入非易失性变量。
例如
volatile int a = 0;
int b = 0;
线程1:
b = 10;
a = 1;
线程2:
while(a != 1);
if(b != 10)
throw new IllegalStateException();
Java内存模型说明了这一点 b
应始终等于10,因为非易失性存储发生在volatile存储之前。并且所有写入都发生在易失性存储发生之前的一个线程中 - 在所有后续的易失性加载之前。
我已经重新措辞(粗体字的变化)你问题的第一句中提到的先前发生的规则,如下所示,以便更好地理解 -
“写 volatile变量的值对主存储器的影响 之前发生 随后从主存中读取该变量”。
- 另外值得注意的是 易失性写/读总是
发生在/从主存储器发生 不 往返任何本地记忆
寄存器,处理器缓存等资源
上述事实的实际意义发生在规则之前 共享volatile变量的所有线程将始终看到一致的值 那个变量。没有两个线程在任何给定的时间点看到该变量的不同值。
反之, 共享非易失性变量的所有线程在任何给定的时间点都可能看到不同的值 除非它没有被任何其他类型的同步机制同步,例如synchronized块/方法,最终关键字等。
现在回到你关于这个发生前的规则的问题,我想你有点误解了这条规则。该规则并未规定写代码应始终在读取代码之前发生(执行)。相反,它规定如果在另一个线程中的读取代码之前在一个线程中执行写入代码(volatile variable write),那么写入代码的效果应该具有 发生 在主存中 之前 执行读取代码,以便读取代码可以看到最新值。
在没有volatile(或任何其他同步机制)的情况下,这种情况发生 - 之前不是强制性的,因此读者线程可能会看到非易失性变量的陈旧值,即使它最近由不同的编写器线程编写。因为writer线程可以将值存储在其本地副本中,并且不需要将值刷新到主内存。
希望以上解释清楚:)
不要坚持“发生在之前”这个词。它是事件之间的关系,由jvm在R / W操作调度期间使用。在这个阶段,它不会帮助你了解不稳定性。关键是:jvm命令所有R / W操作。 jvm可以订购但它想要(当然服从所有同步,锁定,等待等)。
现在:如果变量是易失性的,那么任何读操作都将看到最新写操作的结果。如果变量不是volatile,那么就不能保证(在不同的线程中)。就这样
piotrek是对的,这是测试:
class Test {
volatile int a = 0;
public static void main(String ... args) {
final Test t = new Test();
new Thread(new Runnable(){
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (Exception e) {}
t.a = 10;
System.out.println("now t.a == 10");
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
while(t.a == 0) {}
System.out.println("Loop done: " + t.a);
}
}).start();
}
}
与volatile:它将永远结束
没有不稳定:永远不会结束
来自维基:
特别是在Java中,先发生关系是对语句A写入的内存对语句B可见的保证,即语句A在语句B开始读取之前完成其写入。
因此,如果线程A写入值为10的t.a而线程B稍后尝试读取t.a,则发生之前的关系保证线程B必须读取线程A写入的值10,而不是任何其他值。这很自然,就像爱丽丝买牛奶然后放入冰箱一样,然后鲍勃打开冰箱,看到牛奶。但是,当计算机运行时,内存访问通常不会直接访问内存,这太慢了。相反,软件从寄存器或缓存中获取数据以节省时间。它仅在发生缓存未命中时才从内存加载数据。问题发生了。
让我们看看问题中的代码:
class Test {
volatile int a;
public static void main(String ... args) {
final Test t = new Test();
new Thread(new Runnable(){ //thread A
@Override
public void run() {
Thread.sleep(3000);
t.a = 10;
}
}).start();
new Thread(new Runnable(){ //thread B
@Override
public void run() {
System.out.println("Value " + t.a);
}
}).start();
}
}
线程A将10写入值t.a,线程B尝试将其读出。假设线程A在线程B读取之前写入,那么当线程B读取它时将从内存加载该值,因为它不会将值缓存在寄存器或缓存中,因此它总是由线程A写入10。如果线程A写入之后线程B读取,线程B读取初始值(0)。所以这个例子没有显示易变性的工作原理和差异。但是如果我们改变这样的代码:
class Test {
volatile int a;
public static void main(String ... args) {
final Test t = new Test();
new Thread(new Runnable(){ //thread A
@Override
public void run() {
Thread.sleep(3000);
t.a = 10;
}
}).start();
new Thread(new Runnable(){ //thread B
@Override
public void run() {
while (1) {
System.out.println("Value " + t.a);
}
}
}).start();
}
}
没有 挥发物,打印值应始终为初始值(0),即使在线程A将10写入t.a之后发生某些读取,这违反了先发生关系。原因是编译器优化代码并将t.a保存到寄存器中,每次使用寄存器值而不是从高速缓冲存储器读取时,当然要快得多。但它也会导致之前发生关系违规问题,因为在其他人更新之后,线程B无法获得正确的值。
在上面的例子中, 易失性写入发生在易失性读取之前 意味着 挥发物 在线程A更新后,线程B将获得正确的t.a值。编译器将保证每次线程B读取t.a时,它必须从缓存或内存中读取而不是仅使用寄存器的陈旧值。