Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Volatile variable visibility guarantees in Java

I have checked out some posts about volatile variables in Java. But I am still a bit confused, not to mention some of them contradict each other.

Here is an example I would like to ask about:

public class Main {

    static  ArrayList<Integer> list = new ArrayList<>();
    static volatile int listSize = 0;

    public static void main(String[] args) {
        Thread writer = new Thread(Main::write);
        Thread reader = new Thread(Main::read);
        writer.start();
        reader.start();
    }

    public static void write() {
        while (true) {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                return;
            }
            list.add(1);
            listSize++;
            System.out.println("Added an element.");

        }
    }

    public static void read() {
        int currentIndex = 0;
        while (true) {
            if(currentIndex>=listSize) {
                continue;
            }
            System.out.println("Read an element.");
            int element = list.get(currentIndex);
            currentIndex++;
            // do stuff with element...
        }
    }
}

So in this example a writer thread writes to an ArrayList and a reader thread reads the elements from that list. According to some answers on this topic even if I made the list itself volatile it would have no visibility guarantees between threads for the contents of the list, only for the list object itself. In other words if the writer thread would do this:

list = new ArrayList<>();

The reader thread would know about it. But if the writer thread is adding/removing/modifying the list, like in the example above, there are no guarantees the reader thread would ever see it.

And here is a quote from a book called Java Concurrency in Practice by Brian Goetz et al.

The visibility effects of volatile variables extend beyond the value of the volatile variable itself. When thread A writes to a volatile variable and subsequently thread B reads that same variable, the values of all variables that were visible to A prior to writing to the volatile variable become visible to B after reading the volatile variable. So from a memory visibility perspective, writing a volatile variable is like exiting a synchronized block and reading a volatile variable is like entering a synchronized block.

This quote is also the reason I made a volatile int that the writer thread increments whenever it writes to the list instead of making the list itself volatile.

So I tested this example with and without the volatile variable (replacing currentIndex >= listSize with currentIndex >= list.size() ), and without the volatile variable the reader thread blocks completely, never actually seeing the added elements to the list. With the volatile variable though the reader thread instantly sees the change to the list. Seemingly the quote from the book is correct but I would like to be sure since the example I wrote is based on something else I am writing, and it wouldn't be great if the reader thread blocks indefinitely.


Edit: The thread spin is to get a better chance at caching.

like image 424
iexav Avatar asked Nov 01 '25 19:11

iexav


1 Answers

The very next sentence of Java Concurrency in Practice (after the part you cited) is:

However, we do not recommend relying too heavily on volatile variables for visibility; code that relies on volatile variables for visibility of arbitrary state is more fragile and harder to understand than code that uses locking.

In that sense, I recommend to use a full featured synchronization mechanism (either synchronized or via advanced locks) to ensure thread safety instead of relying on sideeffects of volatile.

Apart from that, your code has a busy-wait loop - you should definitely avoid that! (Maybe that's just for the example code)

This is a valid example of your code that uses proper synchronization with conditional wait (instead of a busy-wait loop):

public class Main {
    static  ArrayList<Integer> list= new ArrayList<>();
    static volatile int listSize=0;

    public static void main(String[] args) {
        Thread writer = new Thread(Main::write);
        Thread reader = new Thread(Main::read);
        writer.start();
        reader.start();
    }

    public static void write(){
        while (true){
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                return;
            }

            // all access to shared state should be synchronized
            synchronized(this) {
                list.add(1);
                listSize++;
                notifyAll(); // notifiy waiting reader thread(s)
            }
            System.out.println("Added an element.");
        }
    }

    public static void read(){
        int currentIndex=0;
        while (true) {
            int element = 0;

            // all access to shared state should be synchronized
            synchronized(this) {
                while (currentIndex >= listSize) {
                    wait(); // wait for notifications of writer thread(s)
                }

                System.out.println("Read an element.");
                element = list.get(currentIndex);
            }
            currentIndex++;
            // do stuff with element...
        }
    }
}

(Note that in read-world code you would rather use any of the existing concurrent collection classes instead, e.g. java.util.concurrent.ConcurrentLinkedQueue)

like image 166
isnot2bad Avatar answered Nov 04 '25 09:11

isnot2bad