Java: Volatile and Synchronized
October 4, 2019•584 words
Gist: a volatile variable isn't enough synchronization.
In Java, a "volatile" field is often presented as a weaker form of synchronization: a field that's specifically indicated to the compiler and runtime as "not-to-be-reordered", which, consequently, doesn't get cached; it is guaranteed to return the most recent write on the field to any threads accessing that field.
Because "most recent write" doesn't always imply "every write", using volatile as a form of synchronization could become problematic.
Consider the code below where the starting balance (represented by volatile member variable balance) is 0; two threads (the main thread, and another created (thread) change the balance to add 100 and 51000 respectively. After execution, one'd expect the final balance to be 51100 (100+51000) for a synchronized operation; however, that isn't what happens: on my machine, the final balance takes values like 51100, 51099, 51000 etc. See the output following the code.
import java.lang.Thread;
/* Over several executions of the program,
a write to the volatile variable "balance"
either by the main thread or by the created thread
is lost i.e. overwritten*/
public class ThreadTest
{
volatile int balance = 0;
public void deposit(int val)
{
balance = balance+val;
}
public void credit(int val)
{
if(balance>0)
balance = balance-val;
}
public void debit(int val)
{
try
{
Thread internalThread = new Thread(new Runnable(){
@Override
public void run()
{
try
{
System.out.println("pre-change balance in second thread is: "+Integer.toString(balance));
deposit(val);
System.out.println("post-change balance in second thread is: "+Integer.toString(balance));
}
catch(Exception ioe)
{
System.out.println("error in debit thread run.");
}
}
});
internalThread.start();
}
catch(Exception ioe)
{
System.out.println("error in debit().");
}
}
public static void main(String[] args) throws InterruptedException
{
System.out.println("Main Thread started");
ThreadTest tt = new ThreadTest();
System.out.println("Main Thread starting balance is: "+Integer.toString(tt.balance));
// alter balance from a new thread
tt.debit(100);
// alter balance from the main thread
for(int i = 1;i<=51000; i++)
{
tt.deposit(1);
}
System.out.println("Main Thread changed balance is: "+Integer.toString(tt.balance));
}
}
[my-pc java]$ java ThreadTest
Main Thread started
Main Thread starting balance is: 0
pre-change balance in second thread is: 1420
post-change balance in second thread is: 2837
Main Thread changed balance is: 51000
[my-pc java]$ java ThreadTest
Main Thread started
Main Thread starting balance is: 0
pre-change balance in second thread is: 935
post-change balance in second thread is: 1619
Main Thread changed balance is: 51100
In the same code, simply marking the method "deposit()" as synchronized ensures that the final balance is always 51100. Of course, the choice of using a synchronized method or a synchronized block is situational.