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.
In the following program, four clients decide to come to the store to get their coffee:
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):
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:
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:
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:
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):
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:
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.
I'm publishing my new posts on Google plus and Twitter. Follow me if you want to be updated with new content.
Labels: Concurrency, Core, Java