Core Java

Java Concurrency Tutorial – Locking: Intrinsic locks

In previous posts we reviewed some of the main risks of sharing data between different threads (like atomicity and visibility) and how to design classes in order to be shared safely (thread-safe designs). In many situations though, we will need to share mutable data, where some threads will write and others will act as readers. It may be the case that you only have one field, independent to others, that needs to be shared between different threads. In this case, you may go with atomic variables. For more complex situations you will need synchronization.

 
 
 

1. The coffee store example

Let’s start with a simple example like a CoffeeStore. This class implements a store where clients can buy coffee. When a client buys coffee, a counter is increased in order to keep track of the number of units sold. The store also registers who was the last client to come to the store.

public class CoffeeStore {
    private String lastClient;
    private int soldCoffees;
    
    private void someLongRunningProcess() throws InterruptedException {
        Thread.sleep(3000);
    }
    
    public void buyCoffee(String client) throws InterruptedException {
        someLongRunningProcess();
        
        lastClient = client;
        soldCoffees++;
        System.out.println(client + " bought some coffee");
    }
    
    public int countSoldCoffees() {return soldCoffees;}
    
    public String getLastClient() {return lastClient;}
}

In the following program, four clients decide to come to the store to get their coffee:

public static void main(String[] args) throws InterruptedException {
    CoffeeStore store = new CoffeeStore();
    Thread t1 = new Thread(new Client(store, "Mike"));
    Thread t2 = new Thread(new Client(store, "John"));
    Thread t3 = new Thread(new Client(store, "Anna"));
    Thread t4 = new Thread(new Client(store, "Steve"));
    
    long startTime = System.currentTimeMillis();
    t1.start();
    t2.start();
    t3.start();
    t4.start();
    
    t1.join();
    t2.join();
    t3.join();
    t4.join();
    
    long totalTime = System.currentTimeMillis() - startTime;
    System.out.println("Sold coffee: " + store.countSoldCoffees());
    System.out.println("Last client: " + store.getLastClient());
    System.out.println("Total time: " + totalTime + " ms");
}

private static class Client implements Runnable {
    private final String name;
    private final CoffeeStore store;
    
    public Client(CoffeeStore store, String name) {
        this.store = store;
        this.name = name;
    }
    
    @Override
    public void run() {
        try {
            store.buyCoffee(name);
        } catch (InterruptedException e) {
            System.out.println("interrupted sale");
        }
    }
}

The main thread will wait for all four client threads to finish, using Thread.join(). Once the clients have left, we should obviously count four coffees sold in our store, but you may get unexpected results like the one above:

Mike bought some coffee
Steve bought some coffee
Anna bought some coffee
John bought some coffee
Sold coffee: 3
Last client: Anna
Total time: 3001 ms

We lost one unit of coffee, and also the last client (John) is not the one displayed (Anna). The reason is that since our code is not synchronized, threads interleaved. Our buyCoffee operation should be made atomic.

2. How synchronization works

A synchronized block is an area of code which is guarded by a lock. When a thread enters a synchronized block, it needs to acquire its lock and once acquired, it won’t release it until exiting the block or throwing an exception. In this way, when another thread tries to enter the synchronized block, it won’t be able to acquire its lock until the owner thread releases it. This is the Java mechanism to ensure that only on thread at a given time is executing a synchronized block of code, ensuring the atomicity of all actions within that block.

Ok, so you use a lock to guard a synchronized block, but what is a lock? The answer is that any Java object can be used as a lock, which is called intrinsic lock. We will now see some examples of these locks when using synchronization.

3. Synchronized methods

Synchronized methods are guarded by two types of locks:

  • Synchronized instance methods: The implicit lock is ‘this’, which is the object used to invoke the method. Each instance of this class will use their own lock.
  • Synchronized static methods: The lock is the Class object. All instances of this class will use the same lock.

As usual, this is better seen with some code.

First, we are going to synchronize an instance method. This works as follows: We have one instance of the class shared by two threads (Thread-1 and Thread-2), and another instance used by a third thread (Thread-3):

public class InstanceMethodExample {
    private static long startTime;
    
    public void start() throws InterruptedException {
        doSomeTask();
    }
    
    public synchronized void doSomeTask() throws InterruptedException {
        long currentTime = System.currentTimeMillis() - startTime;
        System.out.println(Thread.currentThread().getName() + " | Entering method. Current Time: " + currentTime + " ms");
        Thread.sleep(3000);
        System.out.println(Thread.currentThread().getName() + " | Exiting method");
    }
    
    public static void main(String[] args) {
        InstanceMethodExample instance1 = new InstanceMethodExample();
        
        Thread t1 = new Thread(new Worker(instance1), "Thread-1");
        Thread t2 = new Thread(new Worker(instance1), "Thread-2");
        Thread t3 = new Thread(new Worker(new InstanceMethodExample()), "Thread-3");
        
        startTime = System.currentTimeMillis();
        t1.start();
        t2.start();
        t3.start();
    }
    
    private static class Worker implements Runnable {
        private final InstanceMethodExample instance;
        
        public Worker(InstanceMethodExample instance) {
            this.instance = instance;
        }
        
        @Override
        public void run() {
            try {
                instance.start();
            } catch (InterruptedException e) {
                System.out.println(Thread.currentThread().getName() + " interrupted");
            }
        }
    }
}

Since doSomeTask method is synchronized, you would expect that only one thread will execute its code at a given time. But that’s wrong, since it is an instance method; different instances will use a different lock as the output demonstrates:

Thread-1 | Entering method. Current Time: 0 ms
Thread-3 | Entering method. Current Time: 1 ms
Thread-3 | Exiting method
Thread-1 | Exiting method
Thread-2 | Entering method. Current Time: 3001 ms
Thread-2 | Exiting method

Since Thread-1 and Thread-3 use a different instance (and hence, a different lock), they both enter the block at the same time. On the other hand, Thread-2 uses the same instance (and lock) as Thread-1. Therefore, it has to wait until Thread-1 releases the lock.

Now let’s change the method signature and use a static method. StaticMethodExample has the same code except the following line:

public static synchronized void doSomeTask() throws InterruptedException {

If we execute the main method we will get the following output:

Thread-1 | Entering method. Current Time: 0 ms
Thread-1 | Exiting method
Thread-3 | Entering method. Current Time: 3001 ms
Thread-3 | Exiting method
Thread-2 | Entering method. Current Time: 6001 ms
Thread-2 | Exiting method

Since the synchronized method is static, it is guarded by the Class object lock. Despite using different instances, all threads will need to acquire the same lock. Hence, any thread will have to wait for the previous thread to release the lock.

4. Back to the coffee store example

I have now modified the Coffee Store example in order to synchronize its methods. The result is as follows:

public class SynchronizedCoffeeStore {
    private String lastClient;
    private int soldCoffees;
    
    private void someLongRunningProcess() throws InterruptedException {
        Thread.sleep(3000);
    }
    
    public synchronized void buyCoffee(String client) throws InterruptedException {
        someLongRunningProcess();
        
        lastClient = client;
        soldCoffees++;
        System.out.println(client + " bought some coffee");
    }
    
    public synchronized int countSoldCoffees() {return soldCoffees;}
    
    public synchronized String getLastClient() {return lastClient;}
}

Now, if we execute the program, we won’t lose any sale:

Mike bought some coffee
Steve bought some coffee
Anna bought some coffee
John bought some coffee
Sold coffee: 4
Last client: John
Total time: 12005 ms

Perfect! Well, it really is? Now the program’s execution time is 12 seconds.  You sure have noticed a someLongRunningProcess method executing during each sale. It can be an operation which has nothing to do with the sale, but since we synchronized the whole method, now each thread has to wait for it to execute. Could we leave this code out of the synchronized block? Sure! Have a look at synchronized blocks in the next section.

5. Synchronized blocks

The previous section showed us that we may not always need to synchronize the whole method. Since all the synchronized code forces a serialization of all thread executions, we should minimize the length of the synchronized block. In our Coffee store example, we could leave the long running process out of it. In this section’s example, we are going to use synchronized blocks:

In SynchronizedBlockCoffeeStore, we modify the buyCoffee method to exclude the long running process outside of the synchronized block:

public void buyCoffee(String client) throws InterruptedException {
    someLongRunningProcess();
    
    synchronized(this) {
        lastClient = client;
        soldCoffees++;
        System.out.println(client + " bought some coffee");
    }
}

public synchronized int countSoldCoffees() {return soldCoffees;}

public synchronized String getLastClient() {return lastClient;}

In the previous synchronized block, we use ‘this’ as its lock. It’s the same lock as in synchronized instance methods. Beware of using another lock, since we are using this lock in other methods of this class (countSoldCoffees and getLastClient).

Let’s see the result of executing the modified program:

Mike bought some coffee
John bought some coffee
Anna bought some coffee
Steve bought some coffee
Sold coffee: 4
Last client: Steve
Total time: 3015 ms

We have significantly reduced the duration of the program while keeping the code synchronized.

6. Using private locks

The previous section used a lock on the instance object, but you can use any object as its lock. In this section we are going to use a private lock and see what the risk is of using it.

In PrivateLockExample, we have a synchronized block guarded by a private lock (myLock):

public class PrivateLockExample {
    private Object myLock = new Object();
    
    public void executeTask() throws InterruptedException {
        synchronized(myLock) {
            System.out.println("executeTask - Entering...");
            Thread.sleep(3000);
            System.out.println("executeTask - Exiting...");
        }
    }
}

If one thread enters executeTask method will acquire myLock lock. Any other thread entering other methods within this class guarded by the same myLock lock, will have to wait in order to acquire it.

But now, let’s imagine that someone wants to extend this class in order to add its own methods, and these methods also need to be synchronized because need to use the same shared data. Since the lock is private in the base class, the extended class won’t have access to it. If the extended class synchronizes its methods, they will be guarded by ‘this’. In other words, it will use another lock.

MyPrivateLockExample extends the previous class and adds its own synchronized method executeAnotherTask:

public class MyPrivateLockExample extends PrivateLockExample {
    public synchronized void executeAnotherTask() throws InterruptedException {
        System.out.println("executeAnotherTask - Entering...");
        Thread.sleep(3000);
        System.out.println("executeAnotherTask - Exiting...");
    }
    
    public static void main(String[] args) {
        MyPrivateLockExample privateLock = new MyPrivateLockExample();
        
        Thread t1 = new Thread(new Worker1(privateLock));
        Thread t2 = new Thread(new Worker2(privateLock));
        
        t1.start();
        t2.start();
    }
    
    private static class Worker1 implements Runnable {
        private final MyPrivateLockExample privateLock;
        
        public Worker1(MyPrivateLockExample privateLock) {
            this.privateLock = privateLock;
        }
        
        @Override
        public void run() {
            try {
                privateLock.executeTask();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    private static class Worker2 implements Runnable {
        private final MyPrivateLockExample privateLock;
        
        public Worker2(MyPrivateLockExample privateLock) {
            this.privateLock = privateLock;
        }
        
        @Override
        public void run() {
            try {
                privateLock.executeAnotherTask();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

The program uses two worker threads that will execute executeTask and executeAnotherTask respectively. The output shows how threads are interleaved since they are not using the same lock:

executeTask - Entering...
executeAnotherTask - Entering...
executeAnotherTask - Exiting...
executeTask - Exiting...

7. Conclusion

We have reviewed the use of intrinsic locks by using Java’s built-in locking mechanism. The main concern here is that synchronized blocks that need to use shared data; have to use the same lock.

This post is part of the Java Concurrency Tutorial series. Check here to read the rest of the tutorial.

  • You can find the source code at Github.

Xavier Padro

Xavier is a software developer working in a consulting firm based in Barcelona. He is specialized in web application development with experience in both frontend and backend. He is interested in everything related to Java and the Spring framework.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button