I was taught Thread & Concurrency five years ago and it was one of the most interesting and useful modules I have ever taken. But consider how much time has elapsed, one should be forgiven for forgetting some of the stuff he learned. In fact, it is always a good thing to forget something and then re-learn it again years later, so you could look at it from a different angles and probably gain more insight. In this blog, I aim to jot down some fundamentals about Thread & Concurrency (in Java) to remind my future self, so that he won't make stupid mistakes when writing highly concurrent programs.
1. Concurrency problems (which lead to synchronization methods) arise from the scenarios where an object O is shared by multiple working threads. As later discussed, Thread.sleep() method is only used to put the current thread to sleep, it has no synchronization semantic whatsoever, because it doesn't have anything to do with shared objects.
The analogy I use here (taken from a Java book) is the phone booth
, being the common resource shared by multiple threads (users eager to use the phone booth).
There are two properties we want regarding the shared objects:
- At most one thread can access the object at a time, to avoid concurrent reading, writing for example. In the phone booth analogy, this property means that no more than one person is using the booth at a time.
- One thread can communicate with each other about the state of the object. In particular, if the booth is broken, we want all users to wait until another thread (the maintenance officer) fixes it. Once finish, the maintenance officer announce to all waiting people that the booth is now fix and people could start making call again. We prefer this to the scenarios where everyone attempts to use the booth, one after another even though it is known that the booth is still broken. This scenario is less efficient.
2. Each object, namely O, has an implicit monitor lock
called L. In the phone booth analogy, the lock is the phone booth's door. Once one user enter and lock it, the phone booth becomes occupied and others cannot enter until it is open again and free.
3. We use the synchronized
keyword for methods implemented in O in order to serialize the access to O. For example, the following code
synchronized void methodA()
will has the following effects:
- Before entering this method, the lock L must be obtain.
- If L is not available, it will block until it is.
- Once L is obtain, the method is executed. At the end, L is release
So in the phone booth analogy, methodA() could represent a user wanting to make a call. First, he checks if the booth is available (by checking that the door is not lock). He waits until it is available, then enters, locks it, makes a calls, gets out and leaves the door open. Notice that if more than one person is waiting to use the booth, they may have to compete
(using a social protocol, for example) with each other to decide who gets the booth when it becomes available.
We can see that the access to the phone booth is serialized and no more than one person can use it at a time.
4. Java support the Lock
class, whose most commonly used sub-class is ReentranceLock
. One could attempt to get the lock and subsequently release it using
method respectively. The differences between using this and the synchronized
keyword is as follows:
- The synchronized keyword access the implicit lock associated with the object. A ReentranceLock object is explicit.
tryLock() method is non-blocking, in the sense that it returns true or false immediately depending on whether the lock is available. This is of contrast to the implicit lock, where obtaining the lock (via calling a synchronized method) will block until the lock becomes available to it.
5. The wait/notify/notifyAll
mechanisms are used to for the 2nd property of concurrency described at the beginning. In particular:
- It releases the current lock, the remaining code after the call is not executed. This means that the calling method must have obtained the lock, i.e. the wait() method must be called from inside a synchronized method. Other threads trying to get the lock can now get it.
- The current thread is put to a waiting queue Q (different from the queue of threads competing for the object's lock).
- This thread will be awaken by a notification and removed from the Q. Once removed, it enter the queue competing for the object lock. If it then gets the lock, the remaining code from after the wait() call is executed.
awakens a randomly chosen thread from Q of the current object. Notice that if more than one threads are waiting, only one random one is notified. In the phone booth analogy, the maintenance officer only tells one waiting caller that the booth is fixed, others are left waiting in vain.notifyAll():
awakens all threads in Q. As a consequence, they all wake up and compete for the lock before executing the remaining of their codes (after the wait() calls). Under normal circumstances, always use notifyAll() instead of notify()
, as with the latter, a wrong thread could be awakened. Because different threads may wait for different conditions, it is always advisable to surround the wait() method by a while loop checking for the right condition. More specifically:
//do other things
, all waiting threads wake up and compete for the lock. If the wrong one gets the lock, it will knows that its condition is still not satisfied and go back to waiting.
6. A common pitfall is using Thread.sleep()
to deal with concurrency problems. The Java specification gives no synchronization semantic to this method. This method simply puts the current thread to sleep, but nothing else. A very good example demonstrating the pitfall is as follows:
and assume that there's another thread will change this.done
at some point. The problem is that the above code could loop forever, because Java isn't required to load fresh value of this.done
from memory. It means that Java could reuse old value from its cache when checking the condition this.done
, therefore changes from other thread won't be noticed.