Search code examples
javamultithreadingeffective-java

Effective Java Item 66: Why to synchronize both read and write methods?


In Effective Java -> Item 66, Joshua stresses on the need to synchronize both read and write operations to avoid liveness failure.

Here in this particular example, I think synchronization on write method is redundant. Even after removing synchronize on write method, program runs and terminates without any issue. Synchronize is needed to see consistent state of an object, which is achieved through synchronize on read method.

Please let me know your views on this.

import java.util.concurrent.TimeUnit;

public class StopThread {

private static boolean stopRequested;

public static void main(String[] args) throws InterruptedException {
    new Thread(new Runnable() {

        @Override
        public void run() {
            int i = 0;
            while (!isStopRequested())
                i++;
        }
    }).start();
    ;
    TimeUnit.SECONDS.sleep(1);
    setStopRequested(true);
}

private static synchronized boolean isStopRequested() {
    return stopRequested;
}

private static void setStopRequested(boolean stopRequested) {
    StopThread.stopRequested = stopRequested;
}
}

Solution

  • The example you have mentioned could be best suited to demonstrate how in absence of synchronization (or volatile) there is no guarantee about when the values from thread local memory would be flushed to main memory, but this example is certainly not best suited to demonstrate the "read-write concurrency issues".

    I think you might have mis-understood the purpose of example, the purpose was to show the effect of thread communication in absence of synchronization. Read below excerpt from same item #66:

    The actions of the synchronized methods in StopThread would be atomic even without synchronization. In other words, the synchronization on these methods is used solely for its communication effects, not for mutual exclusion.

    The reason why you think it is working is because in absence of synchronization JVM makes no "guarantee" when the values from the thread local memory would be flushed to the main memory, which means that it may not flush at all or it may flush but "when" is not guaranteed. When you run it, then values are getting flushed but it is not necessary that it will always get flushed, so that's where "guarantee" comes into picture, if you use synchronization (or volatile, depending on scenario) then JVM guarantees the "happens-before" relationship, which is nothing but guarantee that flushing of values from thread local memory to main memory would "happen before" any thread could read the values from main memory.

    A better example to check the effect of read-write related concurrency issue in absence of synchronization could be the popular bank account debit credit example, below is quick sample:

    public class AccountDebitCredit {
    
        private int accountBalance = 100;
    
        public static void main(String[] args) throws InterruptedException {
            final AccountDebitCredit accountDebitCredit = new AccountDebitCredit();
    
            Thread t1 = new Thread(new Runnable() {
    
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        // if you remove synchronization from t1 and t2, then there would be concurrency issues.
                        synchronized (accountDebitCredit) {
                            accountDebitCredit.accountBalance = accountDebitCredit.accountBalance + 100;
                        }
                    }
                }
            });
    
            Thread t2 = new Thread(new Runnable() {
    
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        // if you remove synchronization from t1 and t2, then there would be concurrency issues.
                        synchronized (accountDebitCredit) {
                            accountDebitCredit.accountBalance = accountDebitCredit.accountBalance - 100;
                        }
                    }
                }
            });
    
            System.out.println(accountDebitCredit.accountBalance);
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println(accountDebitCredit.accountBalance);
        }
    
    }