Unsynchronized CodeTake the simple BankAccount class below. It holds state in the form of a balance, which can be increased and decreased using the credit and debit methods respectively. When used in a single threaded context this object will behave as expected, crediting and debiting the amounts specified.
However, when multiple threads call the credit and debit methods simultaneously, we can end up with unpredictable behaviour and an incorrect balance. To see this in action the test method below uses an ExecutorService to submit 100 account debits and 100 account credits, each for £100.
After all debits and credits have run we'd expect the account to have a balance of zero. However, what we end up with is a balance of -100 as shown below.
So what's the problem?The credit method takes the current balance, adds a specified amount to it and then assigns the new value back to balance. The problem is that adding a value to balance and then assigning that result back to balance is not an atomic operation. This means that it does not happen as one distinct unit of work. Instead a thread may add the specified amount to the current balance and before it has a chance to assign the new value back to balance, the balance variable is updated by another thread. At this point the balance value held by the first thread is stale and the resulting variable assignment is incorrect.
To resolve the issue we need to ensure that balance = balance + amount becomes an atomic operation, performed in isolation by one thread at a time.
Synchronized MethodsThe simplest way to achieve this is by marking a method as synchronised. A synchronized method uses an implicit lock to ensure that only 1 thread at a time can enter the method. As a result, any shared state referenced inside the method is no longer vulnerable to being manipulated by multiple threads at the same time. In this instance the shared state we're protecting is of course the account balance.
Side Note: Using synchronized on an instance method will use the current objects intrinsic lock for controlling access to the method. Using synchronized on a static method however, uses the intrinsic lock associated with the class, not the object. This is an important distinction, as it is possible for one thread to hold an objects intrinsic lock, while another thread at the same time holds of the class.
Synchronized BlocksWhile synchronizing an entire method can be useful, it is sometimes preferable to only synchronize a portion of a method instead. If you think about it, synchronizing a method creates a bottleneck by allowing only 1 thread into the method at a time. This bottle neck, known as contention, is a result of multiple threads competing to acquire a single lock. Contention can have an adverse affect on performance, so it can be preferable to synchronize only vulnerable code rather than an entire method. Thankfully the synchronized block shown below allows us to do exactly that.
When defining a synchronized block you must specify an object on which to lock . The intrinsic lock of the specified object is used to control access to the synchronized block. A typical approach is to use the current object by specifying this (shown above).
A final thread safe version of the original BankAccount class is shown below.
Granular Control with the Lock InterfaceWhile synchronized methods and blocks are sufficient in most instances, there are times when greater control is required. The Lock interface defines a set of locking operations that provide developers with more granular control when writing thread safe code. ReentrantLock is a common implementation of the Lock interface and one we're going to discuss in next few sections.
ReentrantLock - what is reentrance?The term reentrant refers to a locks ability to be acquired multiple times by the same thread. Implicit locking implemented via a synchronized method or block is reentrant. This means that a thread in a syncronized method may call into another synchronized method without blocking itself. While a reentrant lock may be acquired many times by the same thread, it must also be released the same number of times before the lock can be acquired by another thread.
Creating a ReentrantLockThe code snippet below creates a ReentrantLock using its no argument constructor.
A second constructor takes a boolean to indicate whether or not the lock should apply a fairness policy. If set to true, a fairness policy is implemented that ensures that the longest waiting thread will acquire the lock when it becomes available. This avoids high priority threads monopolising CPU time, while lower priority threads are left to wait for long periods.
Locking and UnlockingThe code snippet below is an updated version of the BankAccount credit method we looked at earlier. Instead of using the synchronized keyword I've used a ReentrantLock to control access to the vulnerable code. When a thread enters the method it will call lock in an attempt to acquire mutually exclusive access to the ReentrantLock. If the lock hasn't already been acquired by another thread, it will be acquired by the current thread, which will then be allowed to proceed.
If the lock has already been acquired by another thread when lock is called, the current thread will block until the lock becomes available again.
After the balance has been updated the unlock method is called to signal that the current thread is finished and the lock is released. At this point the lock can be acquired by a waiting thread.
Note that unlock should always be called inside a finally block. This ensures that the lock is always released, even if an exception is thrown after the lock is acquired. Its imperative that the lock is released, as failing to do so will result in other threads being blocked indefinitely.
More Flexible Locking and UnlockingThe lock method we looked at above attempts to acquire a lock, waiting indefinitely if it is not available. This is the same behaviour as a synchronized method or block. There are however, times when we may want to limit the amount of time we're willing to wait for a lock, especially if we have a large number of threads competing for the same lock. Rather than have many threads blocked waiting for access, we could take some other course of action when a lock isn't available. Luckily ReentrantLock provides this flexibility via two flavours of the tryLock method.
Acquiring a lock with tryLock()The tryLock method checks for the availability of the lock, returning true if the lock is available. When tryLock returns true the current thread acquires the lock and is free to execute whatever vulnerable code is being protected. If the lock is held by another thread and not immediately available tryLock will return false, allowing the application to immediately take some other course of action.
Acquiring a lock with tryLock(long time, TimeUnit unit)An overloaded version of tryLock takes a time and time unit that determines how long the current thread should wait for the lock to become available. If the lock is available when tryLock is called, the current thread will acquire the lock and the method will return true immediately. At this point the current thread is free to execute whatever vulnerable code is being protected.
If on the other hand the lock has already been acquired by another thread, the current thread will wait for the lock to be released. If the lock is released within the specified period of time, the current thread will acquire the lock and tryLock will return true. If the time period elapsed and the lock still hasn't been released, tryLock will return false. At this point the current thread has not acquired the lock and an alternative course of action can be taken.
The debit method below shows tryLock in action. On line 3 the current thread waits up to two seconds for the lock to become available. If the lock is available or becomes available within two seconds, the current thread is free to execute whatever vulnerable code is being protected. If the lock doesn't become available after two seconds the debit amount is added to a queue for processing later.
ReentrantReadWriteLockAnother lock implementation worth looking at is the ReentrantReadWriteLock. As the names suggests the ReentrantReadWriteLock encapsulates both a read and write lock inside a single lock implementation.
Why would I need a Read & Write LockConsider a situation where you have a resource such as a HashMap that is being read from and written to by multiple threads. To synchronize access to a HashMap you could use any of the approaches we've already looked at. You could use a synchronized method, a synchronized block or even a ReentrantLock. While these approaches will work just fine, they do not always offer the most efficient solution.
When a thread attempts to read from or write to a HashMap using any of the above approaches, it will obtain mutually exclusive access to the lock, blocking all other threads from reading or writing at the same time. While this is the desired behaviour when writing to a HashMap, there is no reason why we should stop multiple threads reading from the HashMap concurrently. Reads do not manipulate the HashMap in any way so there is no reason to limit reads to only 1 thread at a time.
Ideally we'd like a means of synchronizing access to the HashMap when its being updated, but allow multiple threads to read from the HashMap when its not being updated. Thankfully this is exactly what the ReentrantReadWriteLock does.
Creating a ReentrantReadWriteLockLike ReentrantLock the ReentrantReadWriteLock can be instantiated with an optional boolean to indicate whether or not a fairness policy should be established. If a fairness policy is chosen, threads will acquire the lock in the order they've requested it.
Acquiring a Write LockThe method below demonstrates how a write lock is acquired by first calling the writeLock method, followed by an appropriate locking method. The example below chains the lock method with writeLock but both tryLock and tryLock(time, TimeUnit) are both available. As mentioned previously, the lock should be released inside a finally block.
When the write lock is acquired by a thread, that thread has mutually exclusive access to the ReentrantReadWriteLock. At this point no other thread can obtain a read or write lock, meaning that the HashMap can not be read from or written to by any other thread.
Acquiring a Read LockThe method below demonstrates how a read lock is acquired by first calling readLock followed by an appropriate locking method. Although the example below chains the lock method with readLock, both tryLock and tryLock(time, TimeUnit) are also available.
Unlike the write lock, the read lock can be held by multiple threads. In our sample code this allows multiple threads to read from the Map concurrently.
While the example above serves to demonstrate the fundamentals of ReentrantReadWriteLock, in reality we wouldn't use it to synchronize access to a HashMap. Instead we'd make use of the Collections class which has a method that takes a Map and returns a thread safe equivalent, as shown below.