x86架构上的Java,易失性和内存障碍

时间:2016-04-23 13:22:01

标签: java multithreading volatile memory-barriers

这更像是一个理论问题。我不确定所有的概念,编译器行为等是否都是最新的并且仍在使用中,但是如果我能正确理解我试图解决的一些概念,我想确认一下。学习。

语言是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的旧值,即使它已被实际更改?

3 个答案:

答案 0 :(得分:4)

  

1)到目前为止我所写的内容是否有意义?还是我完全误解了什么?

我认为你把一切都搞定了。

  

2)为什么编译器在field1 volatile赋值后省略了StoreLoad?这是优化吗?但它有一些缺点吗?

是的,这是一项优化,但要想做到这一点非常棘手。

Doug Lea的JMM Cookbook实际上显示了两个连续volatile商店的推荐障碍示例,并且每个商店之后都有StoreLoad两个商店之间有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操作来命令给定线程上的先前操作。你的第一次易失操作没有内存障碍的原因,仅仅是因为它与你的代码是否有效无关(可能存在可能的情况,但我无法想到,让任何一个好主意的地方。