Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AtomicReference to array and array element changes visibility

Does Java guarantee that updates to array elements done by thread A before storing the array reference in an AtomicReference will be always visible to thread B that obtains this reference?

In other words, what are the possible outputs of executing this:

class References {
    AtomicReference<String[]> refs = new AtomicReference<>(new String[]{"first"});

    public void add(String s) {
        refs.updateAndGet(oldRefs -> {
            String[] newRefs = new String[oldRefs.length + 1];
            System.arraycopy(oldRefs, 0, newRefs, 0, oldRefs.length);
            newRefs[oldRefs.length] = s;
            return newRefs;
        });
    }

    public static void main(String[] args) {
        References r = new References();
        new Thread(() -> r.add("second")).start();
        System.out.println(Arrays.toString(r.refs.get()));
    }
}

can the above only print either [first] or [first, second], or is it also possible to get results like [first, null] or [null, null]?

The javadoc for java.util.concurrent.atomic states:

compareAndSet and all other read-and-update operations such as getAndIncrement have the memory effects of both reading and writing volatile variables.

which seems to not make any guarantees for the non-volatile elements of the array, only the array reference itself.

like image 848
pdabro Avatar asked Jul 03 '15 08:07

pdabro


2 Answers

A volatile write to a variable will guarantee that everything that happened before it will have been happened before a subsequent volatile read of that same variable.

The relevant rules from the JLS are:

17.4.4. Synchronization Order

  • A write to a volatile variable v (§8.3.1.4) synchronizes-with all subsequent reads of v by any thread (where "subsequent" is defined according to the synchronization order).

17.4.5. Happens-before Order

Two actions can be ordered by a happens-before relationship. If one action happens-before another, then the first is visible to and ordered before the second.

If we have two actions x and y, we write hb(x, y) to indicate that x happens-before y.

  • If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).
  • If an action x synchronizes-with a following action y, then we also have hb(x, y).

  • If hb(x, y) and hb(y, z), then hb(x, z).

As AtomicReference guarantees that your array reference is only stored/loaded in a volatile manner (and once written, you don't modify an existing array), this is enough to guarantee visibility of the results of System.arrayCopy() (and the line following it) for anyone calling refs.get().

However the construct still isn't entirely watertight because anyone obtaining the array reference through refs.get() can then go on and make changes to the elements without the protection of AtomicReference.

CopyOnWriteArrayList works very similar to this (it uses the combination of a ReentrantLock and a volatile array field instead of an AtomicReference), except it is also guaranteed that no-one can obtain the underlying array and play around with it in a non-safe manner.

like image 200
biziclop Avatar answered Nov 01 '22 04:11

biziclop


An AtomicReference to an array is no different than any other - it is only the reference that is atomic and therefore has the relevant memory barriers in place. Accessing the array is just like any other object - no extra protection.

You will therefore always get [first] or [first, second] and no other option as you are creating the new array, populating it and placing it back into the old reference which is atomically protected.

Java arrays are not very good at resizing. If you want a resizeable structure you would do better to use an ArrayList. If you want concurrent access to it use a CopyOnWriteArrayList which is essentially what you are trying to implement in your code.

like image 30
OldCurmudgeon Avatar answered Nov 01 '22 02:11

OldCurmudgeon