What is Thread-Safety & How to ensure it?
Thread-safety is about managing access to state, in particular to shared, mutable state. An object’s state is its data stored in state variables (instance or static fields). Any data that can affect its externally visible behavior. Shared means it can be accessed by multiple threads and mutable means it can be changed during its lifetime.
Thread-safety means protecting data from uncontrolled concurrent access. If multiple threads access the same mutable state variable without appropriate synchronization, your program is broken.
How to fix:
- Don’t share the state variable across threads
- Make the state variable immutable
- Use synchronization whenever accessing the state variable
It’s easier to design a class to be thread-safe then to retrofit it for thread-safety later. When designing thread-safe classes, good object oriented techniques — encapsulation, immutability, and clear specification of invariants — are your best friends.
The heart of thread-safety is the concept of correctness. Which means, a class conforms to its specification. A class is thread-safe when it continues to behave correctly when accessed from multiple thread, regardless of scheduling or interleaving of the execution of those threads by the run-time environment, and with no additional synchronization or other coordination on the part of the calling code. No set of operations performed sequentially or concurrently on instances of a thread-safe class can cause an instance to be in an invalid state. Thread-safe classes encapsulate any needed synchronization so that clients need not provide their own.
Stateless objects are always thread-safe.
Race condition: occurs when the correctness of a computation depends on the relative timing or interleaving of multiple threads by the run-time (when getting the right answer relies on lucky timing). common race condition occurs in:
- Check-then-act operation (lazy initialization)
- Read-modify-write operation (increment counter)
How to ensure Thread-safety?
To preserve state consistency, update related state variables in a single atomic operation. atomic operation or atomicty is a group of statement appear to execute as single, indivisible unit.
- Using java.util.concurrent.atomic package.
- Using intrinsic lock / monitor lock with synchronized block.
Java provides a built-in locking mechanism for enforcing atomicity with synchronized block. a synchronized block has 2 elements, a reference to an object that serves as a lock and the block of code which is guarded by this lock. The lock acts as mutex i.e. at most one thread may own the lock.
Every java object can act as a lock.
Intrinsic locks are re-entrant. locks are acquired on per-thread rather than per-invocation basis. Therefore, if a thread tries to acquire a lock which it already holds, the request succeeds. These locks are implemented by associating with each lock an acquisition count and an owning thread. When the count is zero, the lock is considered un-held. When a thread acquires a previously un-held lock, the JVM records the owner and sets the acquisition count to one. If that same thread acquires the lock again, the count is incremented, and when the owning thread exists the synchronized block, the count is decremented. When the count reaches zero, the lock is considered released.
For each mutable state variable that may be accessed by more than one thread, all access to that variable must be performed with same lock held. Then we say that the variable is guarded by the that lock. And every shared, mutable variable should be guarded by exactly one lock. For every invariant that involves more than one variable, all the variables involved in that invariant must be guarded by the same lock.
While synchronized block can make individual operation atomic, additional locking is required to when multiple operations are combined into a compound action.
Avoid holding locks during lengthy computations or operations at risk of not completing quickly such as network or console I/O.