1. Overview
Synchronization in Java is quite helpful for getting rid of multi-threading issues. However, the principles of synchronization can cause us a lot of trouble when they’re not used thoughtfully.
In this tutorial, we’ll discuss a few bad practices associated with synchronization and the better approaches for each use case.
2. Principle of Synchronization
As a general rule, we should synchronize only on objects that we’re sure no outside code will lock.
In other words, it’s a bad practice to use pooled or reusable objects for synchronization. The reason is that a pooled/reusable object is accessible to other processes in the JVM, and any modification to such objects by outside/untrusted code can result in a deadlock and nondeterministic behavior.
Now, let’s discuss synchronization principles based on certain types like String, Boolean, Integer, and Object.
3. String Literal
3.1. Bad Practices
String literals are pooled and often reused in Java. Therefore, it’s not advised to use the String type with the synchronized keyword for synchronization:
public void stringBadPractice1() {
String stringLock = "LOCK_STRING";
synchronized (stringLock) {
// ...
}
}
Similarly, if we use the private final String literal, it’s still referenced from a constant pool:
private final String stringLock = "LOCK_STRING";
public void stringBadPractice2() {
synchronized (stringLock) {
// ...
}
}
Additionally, it’s considered bad practice to intern the String for synchronization:
private final String internedStringLock = new String("LOCK_STRING").intern();
public void stringBadPractice3() {
synchronized (internedStringLock) {
// ...
}
}
As per Javadocs, the intern method gets us the canonical representation for the String object. In other words, the intern method returns a String from the pool – and adds it explicitly to the pool, if it’s not there – that has the same contents as this String.
Therefore, the problem of synchronization on the reusable objects persists for the interned String object as well.
Note: All String literals and string-valued constant expressions are automatically interned.
3.2. Solution
The recommendation to avoid bad practices with synchronization on the String literal is to create a new instance of String using the new keyword.
Let’s fix the problem in the code we already discussed. First, we’ll create a new String object to have a unique reference (to avoid any reuse) and its own intrinsic lock, which helps synchronization.
Then, we keep the object private and final to prevent any outside/untrusted code from accessing it:
private final String stringLock = new String("LOCK_STRING");
public void stringSolution() {
synchronized (stringLock) {
// ...
}
}
4. Boolean Literal
The Boolean type with its two values, true and false, is unsuitable for locking purposes. Similar to String literals in the JVM, boolean literal values also share the unique instances of the Boolean class.
Let’s look at a bad code example synchronizing on the Boolean lock object:
private final Boolean booleanLock = Boolean.FALSE;
public void booleanBadPractice() {
synchronized (booleanLock) {
// ...
}
}
Here, a system can become unresponsive or result in a deadlock situation if any outside code also synchronizes on a Boolean literal with the same value.
Therefore, we don’t recommend using the Boolean objects as a synchronization lock.
5. Boxed Primitive
5.1. Bad Practice
Similar to the boolean literals, boxed types may reuse the instance for some values. The reason is that the JVM caches and shares the value that can be represented as a byte.
For instance, let’s write a bad code example synchronizing on the boxed type Integer:
private int count = 0;
private final Integer intLock = count;
public void boxedPrimitiveBadPractice() {
synchronized (intLock) {
count++;
// ...
}
}
5.2. Solution
However, unlike the boolean literal, the solution for synchronization on the boxed primitive is to create a new instance.
Similar to the String object, we should use the new keyword to create a unique instance of the Integer object with its own intrinsic lock and keep it private and final:
private int count = 0;
private final Integer intLock = new Integer(count);
public void boxedPrimitiveSolution() {
synchronized (intLock) {
count++;
// ...
}
}
6. Class Synchronization
The JVM uses the object itself as a monitor (its intrinsic lock) when a class implements method synchronization or block synchronization with the this keyword.
Untrusted code can obtain and indefinitely hold the intrinsic lock of an accessible class. Consequently, this can result in a deadlock situation.
6.1. Bad Practice
For example, let’s create the Animal class with a synchronized method setName and a method setOwner with a synchronized block:
public class Animal {
private String name;
private String owner;
// getters and constructors
public synchronized void setName(String name) {
this.name = name;
}
public void setOwner(String owner) {
synchronized (this) {
this.owner = owner;
}
}
}
Now, let’s write some bad code that creates an instance of the Animal class and synchronize on it:
Animal animalObj = new Animal("Tommy", "John");
synchronized (animalObj) {
while(true) {
Thread.sleep(Integer.MAX_VALUE);
}
}
Here, the untrusted code example introduces an indefinite delay, preventing the setName and setOwner method implementations from acquiring the same lock.
6.2. Solution
The solution to prevent this vulnerability is the private lock object.
The idea is to use the intrinsic lock associated with the private final instance of the Object class defined within our class in place of the intrinsic lock of the object itself.
Also, we should use block synchronization in place of method synchronization to add flexibility to keep non-synchronized code out of the block.
So, let’s make the required changes to our Animal class:
public class Animal {
// ...
private final Object objLock1 = new Object();
private final Object objLock2 = new Object();
public void setName(String name) {
synchronized (objLock1) {
this.name = name;
}
}
public void setOwner(String owner) {
synchronized (objLock2) {
this.owner = owner;
}
}
}
Here, for better concurrency, we’ve granularized the locking scheme by defining multiple private final lock objects to separate our synchronization concerns for both of the methods – setName and setOwner.
Additionally, if a method that implements the synchronized block modifies a static variable, we must synchronize by locking on the static object:
private static int staticCount = 0;
private static final Object staticObjLock = new Object();
public void staticVariableSolution() {
synchronized (staticObjLock) {
count++;
// ...
}
}
7. Conclusion
In this article, we discussed a few bad practices associated with synchronization on certain types like String, Boolean, Integer, and Object.
The most important takeaway from this article is that it’s not recommended to use pooled or reusable objects for synchronization.
Also, it’s recommended to synchronize on a private final instance of the Object class. Such an object will be inaccessible to outside/untrusted code that may otherwise interact with our public classes, thus reducing the possibility that such interactions could result in deadlock.
As usual, the source code is available over on GitHub.