All the intricacies of a multi-threaded environment arise from sharing objects across threads. As discussed
However, it is not always possible to design a threaded application without making some objects visible across threads. In this article, we will talk about some standard practices for sharing objects. These practices will help us build more resilient and robust multi-threaded systems!
When we publish an object, we make it available to code outside of its current scope. This can be useful in many situations, but there are also cases where we want our objects to remain unpublished. However, there are times when we may need to publish objects, and it’s important to ensure that this is done in a thread-safe manner using proper synchronization.
An accidentally published object is said to have been escaped. There are several ways in which an object may escape!
➥ Making objects visible globally ~ Storing references to objects such that any executing thread can access it causes objects to ‘escape’.
class Escape1 {
// ...
public static List<Integer> activeUserIds = new ArrayList<>();
// ...
}
In the example above, any thread can access activeUserIds
and it is technically escaped!
➥ Returning objects from non-private methods ~ Returning objects from non-private methods also publishes objects. This is because any executing thread can get hold of the internal states of the object.
class Escape2 {
// ...
private static List<Integer> activeUserIds = new ArrayList<>();
// ...
public List<Integer> getActiveUserIds() { return activeUserIds; }
public void addActiveUser(Integer userId) {
boolean userExists = activeUserIds.stream().anyMatch(id -> id == userId);
if(!userExists) {
activeUserIds.add(userId);
}
}
}
In the above example, the activeUserIds
variable, intended to be private to Escape2
class, has escaped its intended scope due to the accessibility of the getActiveUserIds()
method. This situation poses a risk of breaking the invariant that an ID should only appear once in the activeUserIds
list.
➥ Passing object as parameter to alien methods ~ When we pass objects as parameters to an alien method, there is a possibility of that method using the object in a way that breaks the bounded invariants. Even worse, that method may store a reference to the object and modify it later from another thread. It therefore causes objects to escape!
➥ Publishing inner class instance ~ When an object of inner class is published, an implicit reference to the outer class’ object is published. This leads to escaping!
When objects are used only from a single thread, there is no requirement of extra management to make your code thread-safe.
Utilising an object from a single thread is an implementation decision, and although the language provides mechanisms to enforce these constraints, it ultimately falls on the programmer to guarantee that such objects remain within the intended scope of a single thread.
The use of local variables helps us confine objects to a thread. This is because each thread maintains its own set of local variables on the stack. This stack is inaccessible to other threads. However, we need to take special care of not publishing references to such local variables.
Another way of confining an object to a thread is to use Java’s ThreadLocal class. Objects of this class hold separate values for separate executing threads.
public class ThreadLocalTesting {
final static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) throws InterruptedException {
// Setting threadLocal to 5 from manin thread
threadLocal.set(5);
// Creating a new thread
final Thread thread = new Thread(() -> {
System.out.println("::: Thread => " + threadLocal.get());
// Setting threadLocal to 10 from new thread
threadLocal.set(10);
System.out.println("::: Thread => " + threadLocal.get());
});
thread.start();
System.out.println("::: Main => " + threadLocal.get());
thread.join();
}
}
The need for thread-safety arises when mutable states are shared across threads. We just saw how can we ensure objects are confined to a single thread. Let us consider the other end, objects with immutable states.
Since an immutable object can always be in a single state, it is inherently thread-safe! But how do you define immutability? An immutable object —
Immutable objects can be safely accessed across threads even when synchronization is not used!
Note ~ It is considered a best practice to mark all fields as private unless they require greater visibility. Similarly, it is advisable to make all fields final unless there is a specific requirement for them to be mutable.
Sometimes, it’s necessary to share objects across threads, but it’s essential to ensure that this is done in a thread-safe manner. When dealing with immutable shared objects, no extra synchronization is required. However, when working with mutable objects, it’s crucial to ensure their safe publication to avoid concurrency issues.
To safely publish an object, we can
➯ initialize object reference from static initializer
➯ store object reference in a volatile field or AtomicReference
➯ mark the object with the final keyword
➯ use proper locking when accessing the object
After an object has been safely published and is no longer subject to modification, no additional measures are necessary to guarantee its correctness in a multi-threaded environment. However, if the object can be modified after publication, it’s crucial to either implement proper locking mechanisms or ensure that the object is inherently thread-safe to maintain thread safety.
A proper understanding of when and how to share objects in a multi-threaded environment is really necessary to ensure thread-safety of your application. Avoiding sharing objects or making them immutable wherever possible is a great rule of thumb to ensure there are fewer things to fail!
With this, we reach the end of this blog. Follow for more such blogs!
Also published here.