这更像是一个理论问题。我不确定所有的概念,编译器行为等是否都是最新的并且仍在使用中,但是如果我能正确理解我试图解决的一些概念,我想确认一下。学习。
语言是Java。
从目前为止我所理解的,在X86架构上,StoreLoad障碍(尽管用于实现它们的确切CPU指令)是在Volatile写入之后放置的,以使其在其他线程中的后续易失性读取中可见(从x86不保证较新的读取总是看到较旧的写入)(参考http://shipilev.net/blog/2014/on-the-fence-with-dependencies/)
现在从这里(http://jpbempel.blogspot.it/2013/05/volatile-and-memory-barriers.html)我看到了:
public class TestJIT
{
private volatile static int field1;
private static int field2;
private static int field3;
private static int field4;
private static int field5;
private volatile static int field6;
private static void assign(int i)
{
field1 = i << 1; // volatile
field2 = i << 2;
field3 = i << 3;
field4 = i << 4;
field5 = i << 5;
field6 = i << 6; // volatile.
}
public static void main(String[] args) throws Exception
{
for (int i = 0; i < 10000; i++)
{
assign(i);
}
Thread.sleep(1000);
}
}
生成的程序集仅在field6赋值后才具有StoreLoad,而不是在field1赋值之后,但也是不稳定的。
我的问题:
1)到目前为止我所写的内容是否有意义?还是我完全误解了什么?
2)为什么编译器在field1 volatile赋值后省略了StoreLoad?这是优化吗?但它有一些缺点吗?例如,在field1赋值后踢入的另一个线程可能仍然会读取field1的旧值,即使它已被实际更改?
答案 0 :(得分:4)
1)到目前为止我所写的内容是否有意义?还是我完全误解了什么?
我认为你把一切都搞定了。
2)为什么编译器在field1 volatile赋值后省略了StoreLoad?这是优化吗?但它有一些缺点吗?
是的,这是一项优化,但要想做到这一点非常棘手。
Doug Lea的JMM Cookbook实际上显示了两个连续volatile
商店的推荐障碍示例,并且每个商店之后都有StoreLoad
个} s>两个商店之间有StoreStore
(x86无操作),第二个商店之后只有StoreLoad
。然而,Cookbook指出,相关的分析可以非常复杂。
编译器应该能够证明在写入volatile
和写入field1
之间的同步顺序中不能发生field6
读取。我不确定(当前的HotSpot JIT)是否可行(如果TestJIT
稍微改变,以便在同一时间在另一个线程中执行相当数量的volatile
加载。
例如,在field1赋值后启动的另一个线程可能仍然会读取field1的旧值,即使它已被实际更改?
如果volatile
加载在同步顺序中的volatile
存储之后,则不应允许这种情况发生。所以如上所述,我认为JIT可以逃脱它,因为它没有看到任何volatile
加载。
更新
更改了JMM Cookbook示例的详细信息,因为 kRs 指出我错误地将StoreStore
误认为是StoreLoad
。答案的本质根本没有改变。
答案 1 :(得分:0)
为什么编译器在field1 volatile赋值后省略了StoreLoad?
只需要第一个加载和最后一个商店是易变的。
这是优化吗?
如果发生这种情况,这是最可能的原因。
但它有一些缺点吗?
只有你依靠有两个商店的障碍。也就是说,您需要在field1
更改之前更改field6
,而不是偶然发生更改。
可能仍会读取field1的旧值,即使它实际已被更改了吗?
是的,虽然您无法确定是否已经发生这种情况,但是您是否希望看到新值,即使其他字段可能尚未设置。
答案 2 :(得分:0)
要回答问题(1),你对所有关于内存障碍等的说法是正确的(尽管解释是不完整的。内存屏障确保了所有加载/存储的排序,而不仅仅是易失性的)。代码示例虽然如此。
执行内存操作的线程应该对它们进行排序。以这种方式在代码开头使用易失性操作是多余的,因为它没有提供有关订购的任何有价值的保证(我的意思是,它确实提供了保证,它们只是非常脆弱)。 / p>
考虑这个例子;
public void thread1()
{
//no assurances about ordering
counter1 = someVal; //some non-volatile store
counter2 = someVal; //some non-volatile store
}
public void thread2()
{
flag += 1; //some volatile operation
System.out.println(counter1);
System.out.println(counter2);
}
无论我们在thread2上做什么,都绝对不能保证thread1上会发生什么 - 它可以随心所欲地做任何事情。即使你在thread1上使用volatile操作,thread2也不会看到这个顺序。
要解决这个问题,我们需要在thread1上使用内存屏障(也称为volatile操作)来命令写入;
public void thread1()
{
counter1 = someVal; //some non-volatile store
counter2 = someVal; //some non-volatile store
//now we use a volatile write
//this ensures the order of our writes
flag = true; //volatile operation
}
public void thread2()
{
//thread1 has already ordered the memory operations (behind the flag)
//therefore we don't actually need another memory barrier here
if (flag)
{
//both counters have the right value now
}
}
在此示例中,排序由thread1处理,但取决于标志的状态。因此,我们只需要检查flag
的状态,但是您不需要另外的内存屏障(也就是说,您需要检查一个易失性字段,它不需要记忆障碍。)
所以回答你的问题(2):JVM希望你使用volatile操作来命令给定线程上的先前操作。你的第一次易失操作没有内存障碍的原因,仅仅是因为它与你的代码是否有效无关(可能存在可能的情况,但我无法想到,让任何一个好主意的地方。