Java, volatile and memory barriers on x86 architecture

狂风中的少年 提交于 2021-01-02 07:18:52

问题


This is more of a theoretical question. I'm not sure if all concepts, compiler behaviors, etc. are uptodate and still in use, but I'd like to have confirmation if I'm correctly understanding some concepts I'm trying to learn.

Language is Java.

From what I've understood so far, on X86 architecture, StoreLoad barriers (despite the exact CPU instructions used to implement them) are put after Volatile writes, to make them visible to subsequent Volatile Reads in other threads (since x86 doesn't guarantee that newer reads always see older writes) (reference http://shipilev.net/blog/2014/on-the-fence-with-dependencies/)

Now from here (http://jpbempel.blogspot.it/2013/05/volatile-and-memory-barriers.html) I see that:

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);
    }
}

the resulting assembly has the StoreLoad only after field6 assignment, and not after field1 assignment which however is volatile as well.

My questions:

1) Does what I have written so far make sense? Or am I totally misinterpreting something?

2) Why is the compiler omitting a StoreLoad after field1 volatile assignment? Is this an optimization? But has it some drawbacks? For example, another thread kicking in after field1 assignment, might still read an old value for field1 even if it has been actually changed?


回答1:


1) Does what I have written so far make sense? Or am I totally misinterpreting something?

I think you got everything correct.

2) Why is the compiler omitting a StoreLoad after field1 volatile assignment? Is this an optimization? But has it some drawbacks?

Yes, it's an optimization, but it's a pretty tricky one to get right.

Doug Lea's JMM Cookbook actually shows an example of the recommended barriers in the case of two consecutive volatile stores, and there are StoreLoads after each one of them there's a StoreStore (x86 no-op) between the two stores and a StoreLoad only after the second one. The Cookbook however notes that the related analysis can be fairly involved.

The compiler should be able to prove that a volatile read cannot occur in the synchronization order between the write to field1 and the write to field6. I'm not sure if that's doable (by the current HotSpot JIT) if TestJIT was changed slightly so that a comparable amount of volatile loads is executed in another thread at the same time.

For example, another thread kicking in after field1 assignment, might still read an old value for field1 even if it has been actually changed?

That should not be allowed to happen, if that volatile load follows the volatile store in the synchronization order. So as mentioned above, I think that the JIT gets away with it, because it doesn't see any volatile loads being done.

Update

Changed the details around the JMM Cookbook example, as kRs pointed out that I've mistook a StoreStore for a StoreLoad. The essence of the answer was not changed at all.




回答2:


Why is the compiler omitting a StoreLoad after field1 volatile assignment?

Only the first load and the last store is required to be volatile.

Is this an optimization?

If this is happening, this is the most likely reason.

But has it some drawbacks?

Only is you rely on there being two store a barrier. i.e. you need to see field1 changed before field6 has change more often than it would happen by accident.

might still read an old value for field1 even if it has been actually changed?

yes though you will have no way of determining this has happened, but do you want to see a new value even while the other fields might not be set yet.




回答3:


To answer question (1), you are correct with everything you've said about memory barriers etc (though the explanation is incomplete. A memory barrier ensures the ordering of ALL loads/stores before it, not just volatile ones). The code example is iffy though.

The thread performing memory operations should be ordering them. Using a volatile operation at the start of your code in this way, is redundant, as it doesn't provide any worthwhile assurances about ordering (I mean, it does provide assurances, they're just extremely fragile).

Consider this example;

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);
}

No matter what we do on thread2, there are absolutely no assurances about what happens on thread1 - which is free to do pretty much whatever it wants. Even if you use volatile operations on thread1, the ordering wouldn't be seen by thread2.

To fix this, we need to order the writes on thread1 with a memory barrier (aka volatile operation);

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
   }
}

In this example, the ordering is handled by thread1 but depends on the state of flag. As such, we only need to check the state of flag, but you don't need another memory barrier on that read (aka, you need to check a volatile field, it just doesn't need a memory barrier).

So to answer your question (2): The JVM expects you to use a volatile operation to order previous operations on a given thread. The reason why there is no memory barrier on your first volatile operation, is simply because it has no bearing on whether or not your code will work (there might be situations where it could, but I can't think of any, let alone any where it would be a good idea).



来源:https://stackoverflow.com/questions/36811405/java-volatile-and-memory-barriers-on-x86-architecture

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!