Java - Immutable array thread-safety

后端 未结 4 1912
抹茶落季
抹茶落季 2021-02-08 13:30

I have a question regarding the Java Memory Model. Here is a simple class presenting the problem:

public class Immutable         


        
相关标签:
4条回答
  • 2021-02-08 13:47

    In this example, everything will be fine (hmm, let's suspend judgement a bit). Immutability is ambrosia when it comes to thread-safety - if a value cannot change, the majority of concurrency problems are immediately no longer a concern.

    Amir mentioned volatile which is generally useful - but the constructor also has similar semantics for final variables that ensure visibility. See JLS clause 17.5 for details - essentially the constructor forms a happens-before relationship between the write to the final variables and any subsequent reads.

    EDIT: So you set the values reference to the array in the constructor, it's visible across all threads at that point, and then it doesn't change. So we know all other threads will see the same array. But what about the array's contents?

    As it stands, array elements don't have any special semantics with regard to volatility, they're as if you just declared a class yourself something like:

    public class ArrayTen {
        private int _0;
        private int _1;
        // ...
        private int _9;
    
        public int get(int index) {
           if (index == 0) return _0;
           // etc.
        }
    }
    

    So - another thread will only see these variables if we can do something to establish the happens-before relationship. And if my understanding is correct this requires but a small change to your original code.

    We already know that the setting of the array reference happens-before the end of the constructor. An additional point which is always true, is that actions in one thread happen-before later actions in that same thread. So we can combine these by setting the array fields first, and then assigning the final field, so as to get this transitive guarantee of visibility. This will of course require a temporary variable:

    public class ImmutableIntArray {
    
        private final int[] array;
    
        public ImmutableIntArray() {
            int[] tmp = new int[10];
            for (int i = 0; i < 10; i++) {
                tmp[i] = i;
            }
            array = tmp;
        }
    
        // get() etc.
    }
    

    I think this is guaranteed to be safe, now that we've switched the seemingly irrelevant order of assignment and population.

    But again, there might be something else I've missed which means the concurrency guarantees aren't as robust as hoped. This question is to my mind an excellent example of why writing bulletproof multithreaded code is tricky, even when you think you're doing something very simple, and how it takes a lot of thought and caution (and then bugfixes) to get right.

    0 讨论(0)
  • 2021-02-08 13:59

    I do think that you're afforded the same semantics with an array as with a final reference to an object. The spec states

    A thread that can only see a reference to an object after that object has been completely initialized is guaranteed to see the correctly initialized values for that object's final fields.

    It also says

    It will also see versions of any object or array referenced by those final fields that are at least as up-to-date as the final fields are.

    http://java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5

    0 讨论(0)
  • 2021-02-08 14:03

    your example is not quite right. in order to get the final field assurance, you need:

    public ImmutableIntArray() {
        int tmparray = new int[10];
        for (int i = 0; i < 10; i++) {
            tmparray[i] = i;
        }
        array = tmparray;
    }
    
    0 讨论(0)
  • 2021-02-08 14:04

    I think your array changes will be visible with your ImmutableIntArray. From my reading on JLS the [freeze] action should take place when constructor exits. The use of a temp array I think is useless:

    int tmparray = new int[10];
    for (int i = 0; i < 10; i++) {
        tmparray[i] = i;
    }
    array = tmparray;
    

    To obtain final field guaranties we will need a [freeze] somewhere before constructor exit:

    int tmparray = new int[10];
    for (int i = 0; i < 10; i++) {
        tmparray[i] = i;
    }
    array = tmparray;
    [freeze]
    

    Anyway, [freeze] leaves the gates open to reorder instructions above it, so we will have the same thing:

    int tmparray = new int[10];
    array = tmparray; 
    for (int i = 0; i < 10; i++) {
        tmparray[i] = i;
    }
    [freeze]
    

    [freeze] is implemented to contain at minimum a [StoreStore]. This [StoreStore] barrier must be issued before the moment when instance constructed is published.

    From JSR-133 Cookbook:

    You cannot move stores of finals within constructors down below a store outside of the constructor that might make the object visible to other threads. (As seen below, this may also require issuing a barrier). Similarly, you cannot reorder either of the first two with the third assignment in: v.afield = 1; x.finalField = v; ... ; sharedRef = x;

    And I think this is done by (JSR-133 Cookbook):

    Issue a StoreStore barrier after all stores but before return from any constructor for any class with a final field.

    So we can't store in sharedRef before all other contructor stores are done.

    You can search by: "Transitive guarantees from final fields" in (JSR133 spec).

    0 讨论(0)
提交回复
热议问题