1

I was running an experiment on different approaches to deal with race condition in multi-threaded java applications . Strategies like atomic variables, synchronize worked well , but I dont see the issue being solved when using volatile variables. Here the code and output for reference.

Can you please guide on what could be the reason for the volatile variable to still lead to a race condition?

package com.shyam.concurrency;


public class main {
    public static void main(String[] args) {

   demoClass dm1 = new demoClass();
        Thread th1 = new Thread(()->{
        int i =0;
        do {
            i++;
            dm1.setCounter();
            dm1.setAtomicCounter();
            dm1.setSyncCounter();
            dm1.setVolatileCounter();
        } while (i < 100000);
        });

        Thread th2 = new Thread(()->{
            int i =0;
            do {
                i++;
                dm1.setCounter();
                dm1.setAtomicCounter();
                dm1.setSyncCounter();
                dm1.setVolatileCounter();
            } while (i < 100000);

        });

        th1.start();
        th2.start();

        try {
            th1.join();
            th2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Normal counter(Race condition) : " + dm1.getCounter() );
        System.out.println("Synchronized counter is :" + dm1.getSyncCounter());
        System.out.println("Atomic counter is :" + dm1.getAtomicCounter());
        System.out.println("Volatile counter is :" + dm1.getVolatileCounter());

The code that has the increment logic is here:

package com.shyam.concurrency;

import java.util.concurrent.atomic.AtomicInteger;

public class demoClass {
    private  int counter ;
    private int syncCounter;
    private volatile  int volatileCounter = 0;
    private AtomicInteger  atomicCounter  = new AtomicInteger() ;


 
    public int getAtomicCounter() {
        return atomicCounter.intValue();
    }

    public void setAtomicCounter() {
        this.atomicCounter.addAndGet(1);
    }

    public int getCounter() {
        return counter;
    }

    public void setCounter() {
        this.counter++;
    }
    public synchronized int getSyncCounter() {
        return syncCounter;
    }

    public synchronized void setSyncCounter() {
        this.syncCounter++;
    }

    public int getVolatileCounter() {
        return volatileCounter;
    }

    public void setVolatileCounter() {
        this.volatileCounter++;
    }
}

And here's the output i get:

Normal counter(Race condition) : 197971
Synchronized counter is :200000
Atomic counter is :200000
Volatile counter is :199601

  • 2
    Does this answer your question? [Difference between volatile and synchronized in Java](https://stackoverflow.com/questions/3519664/difference-between-volatile-and-synchronized-in-java) – Piotr Żmudzin Jun 21 '21 at 17:27
  • 3
    Well, I haven't read your code yet, but the problem is likely atomicity. `volatile` doesn't provide it, while the other two methods you mention do. So it's just a matter of choosing the correct approach for your problem. – markspace Jun 21 '21 at 17:27
  • 1
    Also https://stackoverflow.com/q/7805192/438992 – Dave Newton Jun 21 '21 at 17:28
  • 2
    Now I've read your code and the problem is definitely atomicity. `volatile` well known to not help with that sort of access. It doesn't work because the spec says it won't. – markspace Jun 21 '21 at 17:30
  • 1
    A read/modify/write operation on a `volatile` variable is not an atomic operation. Two threads can interleave such that one is reading while the other is incrementing or other ways. If you need atomicity, you can't choose an operation that only guarantees visibility. – David Schwartz Jun 21 '21 at 17:34
  • @markspace: Volatile does provide atomicity on the load and atomicity on the store; only it doesn't provide atomicity on the load in combination with the store. – pveentjer Jun 22 '21 at 04:13
  • @shyam: the normal counter is suffering from a data-race and race-condition because read/write don't have happens before edge + non atomic read-modify-write. The volatile counter is suffering only from a race-condition because of non atomic read-modify-write. The synchronized counter and the AtomicInteger.increment are both correct since they don't suffer from the data-race and race-condition. – pveentjer Jun 22 '21 at 04:17

1 Answers1

2

Visibility versus Atomicity

The volatile solves only the problem of visibility. This means that every thread sees the current value of a variable rather than possibly seeing a stale cached value.

Your line:

this.volatileCounter++;

… is performing multiple operations:

  • Fetch the current value of that variable
  • Increment that value
  • Store the new value in that variable

This group of operations is not atomic.

While one thread has fetched the value but not yet incremented and stored the new value, a second thread may access the same current value. Both threads increment the same initial value, so both threads produce and save the same redundant new value.

For example, two or more threads might access a value of 42. All of those threads would then increment to 43, and each thread would store 43. That number 43 would be stored over and over again. Some other thread might have even seen one of those 43 writes, and then incremented & stored a 44. One of the remaining threads yet to write its 43 would end up overriding the write of 44. So not only might you waste some attempts to increment and thereby fail to move the number forward, you might actually see the number move backward (effectively decrement).

If you want to use volatile you must protect the code to make atomic the multiple operations. The synchronized keyword is one such solution.

Personally, I prefer using the AtomicInteger approach instead. If you instantiate an AtomicInteger before any access attempt, and never replace that instance, then visibility of the reference variable for that AtomicInteger is a non-issue. Having no opportunity for stale cached values mean no visibility problem. And regarding racy access to its payload, the methods of the AtomicInteger provide atomicity for the simple manipulations (hence the name, obviously),

To learn more about visibility issues, study the Java Memory Model. And read the excellent book, Java Concurrency In Practice by Brian Goetz, et al.

Abdullah Khan
  • 12,010
  • 6
  • 65
  • 78
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • @SolomonSlow I’ll add a clarification. Thanks. – Basil Bourque Jun 21 '21 at 20:13
  • Only the read-modify-write can't be made atomic with just a volatile read and write. But the actual load and actual store are atomic. – pveentjer Jun 22 '21 at 04:19
  • 1
    This explanation is not fully correct. Volatile deals with 3 aspects: visibility, atomicity and ordering; not only visibility. If you do e.g. a write (release) and later on you will do a read (acquire) of that same volatile variable, then all loads/stores before the release are ordered before the release. And for the acquire, it will order all loads and stores after the acquire. Volatile also guarantees atomicity; in this particular case you can't run into a torn read or torn write. – pveentjer Jun 22 '21 at 04:21