Java programming language logo

Synchronization

Threads often work with shared data objects. Modification of an object’s field by multiple threads can lead two kind of problems: thread interference and memory visibility problem.

Thread Interference

Let’s suppose there is a shared integer variable:

int x = 0;

What will be the result of x when two threads try to modify it at the same time? For example, the first thread increments it by 1, the second one decrements it by 1. That can be done by two expression: x++ and x--. Can they have any interference?

The answer is: yes. Because the single x++ expression actually stands from three steps:
1. Get the value from x.
2. Increment the value by 1.
3. Assign the new value to x.

When the x++ and x-- expression is executed at the same time by different threads, a possible order of the steps can be like these:

Thread A: Get the value from x.
Thread B: Get the value from x.
Thread A: Increment the value by 1.
Thread B: Decrement the value by 1.
Thread A: Assign the new value to x.
Thread B: Assign the new value to x.

This time the result of x is -1, but another time also can be 0 or 1. This problem can be solved by synchronization.

Memory Visibility

Memory visibility error happens when different threads modifies the same variable at different times, but they still don’t see each other’s modification. For example, one thread modifies a variable, and after that another thread on another CPU reads it and finds the old value. This can happen till the value from the cache of CPU doesn’t get written back to the main memory.

All we need to overcome this problem is the happens-before relationship. This relationship ensures that, all modifications to a variable are visible to the other threads.

Several actions trigger a happens-before relationship. For example:
- exiting from a synchronized method or block,
- writing a volatile variable,
- Thread.start and Thread.join methods.

So synchronization solves the thread interference and the memory visibility problems. Synchronization can be applied for methods or statements.

Synchronized Methods

In the next example, the Data is a shared object. It has a value field, which can be incremented and decremented by methods. Without synchronization, the increment and the decrement methods could be invoked on the same instance at the same time. To make these methods synchronized, the synchronized keyword has to be added to their declarations.


package com.programcodex.concurrency.sync.method;

public class Data {
    
    private int value;

    public synchronized void increment() {
        value++;
    }

    public synchronized void decrement() {
        value--;
    }

    public synchronized int getValue() {
        return value;
    }
}

Synchronization guarantees that two threads cannot invoke synchronized methods on the same object instance at the same time. If one thread executes a synchronized method, a second thread cannot invoke any synchronized method on that object until the first thread not finished with it. The second thread has to wait for the object to get released. Furthermore, exiting from a synchronized method has a happens-before relationship with the other threads, so every state changes of the object get visible to them.

The getValue method also synchronized because it should not read the value while other methods write it.

The ThreadInc and ThreadDec classes manipulate the same Data object instance. They get a reference to that instance in their constructor. The ThreadInc increments the value of the Data object three times, the ThreadDec decrements it three times.


package com.programcodex.concurrency.sync.method;

public class ThreadInc implements Runnable {

    private Data data;

    public ThreadInc(Data data) {
        this.data = data;
    }

    @Override
    public void run() {
        data.increment();
        data.increment();
        data.increment();
    }
}

The ThreadDec class:


package com.programcodex.concurrency.sync.method;

public class ThreadDec implements Runnable {

    private Data data;

    public ThreadDec(Data data) {
        this.data = data;
    }

    @Override
    public void run() {
        data.decrement();
        data.decrement();
        data.decrement();
    }
}

The TestSynchronizedMethod instantiates the shared object and two threads. After that, the threads are started and when they are done, the result is printed out.


package com.programcodex.concurrency.sync.method;

public class TestSynchronizedMethod {

    public static void main(String[] args) throws InterruptedException {
        Data data = new Data();
        Thread threadInc = new Thread(new ThreadInc(data));
        Thread threadDec = new Thread(new ThreadDec(data));

        threadInc.start();
        threadDec.start();

        threadInc.join();
        threadDec.join();

        System.out.println("The value is " + data.getValue());
    }
}

The output of the above class:

Synchronized Method

Without synchronization the value could be anything between -3 and 3.

Intrinsic Locks

Before getting to synchronized statements, we have to get familiar with object locking.

Java has a built-in locking mechanism for supporting synchronization, which is called intrinsic lock or monitor lock. Every object has an intrinsic lock, and when a thread needs to call a synchronized method of an object, it has to own the lock of that object instance. The thread needs to get the lock before executing the synchronized method, and after that it has to release it. If the lock is owned by another thread, the current thread suspends the execution and waits for the lock because only one thread can have the same lock at the same time. This is done by Java automatically when a synchronized method get called.

This mechanism also works with static methods despite of static methods belong to a class, not to an object instance. Static synchronized methods use the Class object for the lock.

Synchronized Statements

Synchronized method locks the whole object and maybe blocks other threads unnecessarily because this technique uses the object’s single lock. With synchronized statement, an object has to be specified that provides the intrinsic lock. This object’s lock has to be owned by the thread while the synchronized code is executed.

In the below Data class, the x and y fields are modified in a synchronized way. As we saw before, the increment and decrement methods should not interfere, but modification of x and y allowed to happen at the same time. The lockX field provides the lock object for x, and lockY for y field. So the incX and decX methods cannot be running at the same time, and this is also true for the incY and decY methods, but ...X and ...Y methods can be executed concurrently without any problem.


package com.programcodex.concurrency.sync.statement;

public class Data {

    private int x;
    private int y;

    private Object lockX = new Object();
    private Object lockY = new Object();

    public void incX() {
        synchronized (lockX) {
            x++;
        }
    }

    public void decX() {
        synchronized (lockX) {
            x--;
        }
    }
    
    public void incY() {
        synchronized (lockY) {
            y++;
        }
    }
    
    public void decY() {
        synchronized (lockY) {
            y--;
        }
    }
}

Synchronized Statement Example

The Printer class prints out messages. The slowPrint method prints out two line, but it sleeps for three seconds between the two. The fastPrint method also prints out two line, but it only sleeps for one second between the two, so this is faster. These methods use synchronized statements, but they use different locking objects: lockSlow and lockFast. This means, they can be executed on the same instance at the same time, but several invocations to the same methods can only be executed one after another.

The Printer class also has a noSyncPrint method. This doesn’t apply any synchronization, so it is executed right when it is invoked.


package com.programcodex.concurrency.sync.statement;

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

public class Printer {

    private static DateTimeFormatter dtf =
            DateTimeFormatter.ofPattern("HH:mm:ss");

    private final Object lockSlow = new Object();
    private final Object lockFast = new Object();

    public void slowPrint(String className) {
        synchronized (lockSlow) {
            print(className, 3000);
        }
    }

    public void fastPrint(String className) {
        synchronized (lockFast) {
            print(className, 1000);
        }
    }

    public void noSyncPrint() {
        String time = dtf.format(LocalTime.now());
        System.out.println(time + " - No synchronization, no blocking...");
    }

    private void print(String className, long millis) {
        String time = dtf.format(LocalTime.now());
        System.out.format("%s - %s: print begins%n", time, className);

        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        time = dtf.format(LocalTime.now());
        System.out.format("%s - %s: print ends%n", time, className);
    }
}

The SlowRunnable class receives a Printer instance in its constructor and calls this printer’s slowPrint method with the own class name (SlowRunnable).


package com.programcodex.concurrency.sync.statement;

public class SlowRunnable implements Runnable {

    private Printer printer;

    public SlowRunnable(Printer printer) {
        this.printer = printer;
    }

    @Override
    public void run() {
        printer.slowPrint(this.getClass().getSimpleName());
    }
}

The FastRunnable class similar to the SlowRunnable, but it calls the fastPrint method of the printer.


package com.programcodex.concurrency.sync.statement;

public class FastRunnable implements Runnable {

    private Printer printer;

    public FastRunnable(Printer printer) {
        this.printer = printer;
    }

    @Override
    public void run() {
        printer.fastPrint(this.getClass().getSimpleName());
    }
}

In the main method, a Printer, a SlowRunnable and a FastRunnable are instantiated. The two Runnable thread share the printer instance. After these, four threads are started. Two with the slowRunnable object, and two with fastRunnable object.

Finally, one second later, the main thread calls the noSyncPrint method of the printer.


package com.programcodex.concurrency.sync.statement;

public class TestSyncStatement {

    public static void main(String[] args) throws InterruptedException {
        Printer printer = new Printer();

        Runnable slowRunnable = new SlowRunnable(printer);
        Runnable fastRunnable = new FastRunnable(printer);

        new Thread(slowRunnable).start();
        new Thread(slowRunnable).start();
        new Thread(fastRunnable).start();
        new Thread(fastRunnable).start();

        Thread.sleep(1000);
        printer.noSyncPrint();
    }
}

The output of the above class:

Synchronized Statement

The slowPrint and the fastPrint methods can be executed concurrently by the SlowRunnable and FastRunnable threads, but several invocations to the same methods from different threads can only be executed one after another.

The noSyncPrint executed when it is called because this method not blocked by any synchronization.

Synchronized Method Example

What happens, if we replace the synchronized statements with synchronized methods in the above example? The slowPrint and fastPrint methods of the Printer class are altered below, and the lock objects got removed.


package com.programcodex.concurrency.sync.method2;

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

public class Printer {

    private static DateTimeFormatter dtf =
            DateTimeFormatter.ofPattern("HH:mm:ss");

    public synchronized void slowPrint(String className) {
        print(className, 3000);
    }

    public synchronized void fastPrint(String className) {
        print(className, 1000);
    }

    public void noSyncPrint() {
        String time = dtf.format(LocalTime.now());
        System.out.println(time + " - No synchronization, no blocking...");
    }

    private void print(String className, long millis) {
        String time = dtf.format(LocalTime.now());
        System.out.format("%s - %s: print begins%n", time, className);

        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        time = dtf.format(LocalTime.now());
        System.out.format("%s - %s: print ends%n", time, className);
    }
}

This modification to the Printer class causes a different output. The slowPrint and the fastPrint methods cannot be executed concurrently by the SlowRunnable and FastRunnable threads. They can only call the printer's methods one after another. The execution time also got longer, which means this version is less effective.

Synchronized Method