In the previous chapters, we looked at the basic concepts of Java Multithreading like What a Thread is, the Synchronized Keyword, the volatile keyword etc. In this chapter, we are going to look at a more complex topic “Race Conditions”
When does a Race Condition Occur?
A race condition occurs when the order of execution of two or more threads may affect some variable or outcome in the program. It may turn out that all the different possible orders of thread execution have the same final effect on the program. The effect caused by the race condition may be insignificant and may not even be relevant.
The timing of the threading system may be such that the race condition never manifests itself, despite the fact that it exists in the code.
Race conditions can be considered harmless if you can prove that the result of the race condition is always the same. This is a common technique in some of Java's core classes. But in general, a race condition is a problem that is waiting to happen. Simple changes in the algorithm can cause race conditions to manifest themselves in problematic ways. Since different virtual machines have different ordering of thread execution, the developer should never let a race condition exist even if it is currently not causing a problem on the development system.
The next logical question that might arise in your mind is “Can properly Synchronizing two methods, prevent a race condition from happening?”
Well, the simple answer would be “Yes”. As explained in the previous chapter on “Synchronized keyword” synchronizing a method has the effect of serializing access to the method. This means that it is not possible to execute the same method in one thread while the method is already running in another thread. The implementation of this mechanism is done by a lock that is assigned to the object itself. The reason another thread cannot execute the same method at the same time is that the method requires the lock that is already held by the first thread. If two different synchronized methods of the same object are called, they also behave in the same fashion because they both require the lock of the same object, and it is not possible for both methods to grab the lock at the same time. In other words, even if two or more methods are involved, they are never run in parallel in separate threads.
Lets say there are two threads thread 1 and thread 2. If both these threads are attempting to acquire a lock on some object say Obj and if thread 1 acquires it first, thread 2 has to wait until thread 1 is done with all its processing before it can continue to execute.
An important point to remember here is that the lock is based on a specific instance of an object and not on any particular method or class.
Now that we know how synchronization works at a high level, the next logical question that might arise in your mind is ”How will a synchronized method behave in conjunction with a non-synchronized method?”
To answer this question, we must remember that all synchronizing does is to grab an object lock. This, in turn, provides the means of allowing only one synchronized method to run at a time, which in turn provides the data protection that solves the race condition. Simply put, a synchronized method tries to grab the object lock, and an unsynchronized method doesn't. This means that unsynchronized methods can execute at any time, by any thread, regardless of whether a synchronized method is currently running. At any given moment on any given object, any number of unsynchronized methods can be executing, but only one synchronized method can be executing.
A point to note here is that, if a non-synchronized method and a synchronized method are trying to update the same object, we may end up with unstable or inconsistent data. So, it is always a good idea to synchronize all key methods that modify important data that may be accessed by multiple threads at once.
Before we wrap up this chapter, another important question that might arise in your mind (if you are a person who has used Java for atleast a few years) is “How would Synchronization work with Static Methods?”
Ok, before we answer this question, do you understand the nuances of the static keyword and how it works when paired with methods or variables? If not, I strongly suggest you go to the topic on the “Static Keyword” and refresh your memory to understand the answer better.
Whenever we talk about threads and synchronization, we always talk about "obtaining the object lock." But, what about static methods? When a synchronized static method is called, which object are we referring to? A static method does not have a concept of the “this” object reference. It is not possible to obtain the object lock of an object that does not exist. So how does synchronization of static methods work? To answer this question, we need to understand the concept of a class lock. Just as there is an object lock that can be obtained for each instance of a class (i.e., each object), there is a lock that can be obtained for each class. We refer to this as the class lock. In terms of implementation, there is no such thing as a class lock, but it is a useful concept to help us understand how all this synchronization funda works.
When a static synchronized method is called, the program obtains the class lock before calling the method. This mechanism is identical to the case in which the method is not static; it is just a different lock. And this lock is used solely for static methods. Apart from the functional relationship between the two locks, they are not operationally related at all. These are two distinct locks. The class lock can be grabbed and released independently of the object lock.
If a nonstatic synchronized method calls a static synchronized method, it acquires both locks.
As mentioned, a class lock does not actually exist. The class lock is the object lock of the Class object that models the class. Since there is only one Class object per class, using this object achieves the synchronization for static methods.
From the perspective of a developer who creates multi-threaded enterprise applications, this whole concept can be summarized as follows:
Only one thread can execute a synchronized static method per class. Only one thread per instance of the class can execute a nonstatic synchronized method. Any number of threads can execute nonsynchronized methods, static or otherwise.
Previous: The Volatile Keyword
Next: Explicit Locking