Mastering Thread‑Safe Objects in Java: From Synchronized Collections to Atomic Variables
This article explains how Java threads share objects, why immutable objects are safest, how to make mutable collections thread‑safe using synchronized wrappers or concurrent classes, handles non‑thread‑safe types like SimpleDateFormat, and demonstrates race‑condition fixes with synchronized blocks and AtomicInteger.
Thread‑Safe Objects
In Java, threads communicate by sharing references to the same objects. Concurrent reads and writes on mutable objects can lead to inconsistent state or unexpected results. The most reliable way to avoid these problems is to use immutable objects. When immutability is not feasible, the mutable objects must be made thread‑safe.
Synchronizing Collections
Standard collection classes (e.g., HashMap, ArrayList) keep internal mutable state. The simplest way to protect them is to wrap the collection with the synchronized utilities provided by java.util.Collections:
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
List<Integer> list = Collections.synchronizedList(new ArrayList<>());The wrapper synchronizes each individual method call, guaranteeing that only one thread can execute a method at a time. This prevents the collection from being left in an inconsistent state, but it also forces every operation—including reads—to acquire a lock.
Concurrent Collections for Read‑Heavy Workloads
When the workload performs many more reads than writes, the lock contention of synchronized wrappers becomes a bottleneck. Java provides lock‑free or low‑contention alternatives: CopyOnWriteArrayList – on each mutating operation a fresh copy of the underlying array is created. Reads are performed on an immutable snapshot, giving near‑zero contention. Writes are more expensive than Collections.synchronizedList, so this class is best when writes are rare. ConcurrentHashMap – internally partitions the map into segments (or uses a lock‑striping algorithm in newer JDKs) so that reads and writes on different keys can proceed concurrently. It outperforms Collections.synchronizedMap because it does not serialize all operations on a single lock.
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
ConcurrentHashMap<String, String> chm = new ConcurrentHashMap<>();Handling Non‑Thread‑Safe Types (e.g., SimpleDateFormat)
Classes such as java.text.SimpleDateFormat maintain mutable internal state and are not safe for concurrent use. Sharing a single instance across threads can produce incorrect parsing or formatting.
Safe patterns include:
Instantiate a new SimpleDateFormat each time it is needed.
Store a ThreadLocal<SimpleDateFormat> so each thread has its own instance.
Synchronize access to a shared instance using the synchronized keyword or an explicit Lock.
These approaches apply to any mutable class that lacks built‑in thread safety.
Race Conditions
Illustrative Counter Example
class Counter {
private int counter = 0;
public void increment() { counter++; }
public int getValue() { return counter; }
}The counter++ operation consists of three steps: read the current value, add one, and write the new value back. When two threads execute increment() simultaneously, the steps can interleave, causing one increment to be lost and the final value to be 1 instead of 2.
Synchronized Solution
class SynchronizedCounter {
private int counter = 0;
public synchronized void increment() { counter++; }
public synchronized int getValue() { return counter; }
}Marking the methods as synchronized forces exclusive access to the critical section, eliminating the race at the cost of lock acquisition overhead.
Lock‑Free Atomic Variables
Java’s java.util.concurrent.atomic package provides lock‑free classes that perform atomic operations using low‑level CPU instructions.
AtomicInteger atomic = new AtomicInteger(3);
int newValue = atomic.incrementAndGet(); // atomically increments and returns the new valueUsing AtomicInteger (or other atomic types) removes the need for explicit synchronization while delivering higher throughput.
Compound Operations on Collections
Problem: Non‑Atomic Sequences
Even when each collection method is synchronized, a sequence of calls is not atomic. For example:
List<String> list = Collections.synchronizedList(new ArrayList<>());
if (!list.contains("FunTester")) {
list.add("FunTester");
}Between the contains check and the add, another thread may modify the list, resulting in duplicate entries.
External Synchronization
Wrap the whole sequence in a synchronized block that locks on the collection object:
synchronized (list) {
if (!list.contains("FunTester")) {
list.add("FunTester");
}
}This guarantees that only one thread can execute the compound action at a time.
Built‑in Atomic Methods in ConcurrentHashMap
ConcurrentHashMapsupplies atomic compound operations that eliminate the need for external locking: putIfAbsent(key, value) inserts the value only if the key is not already present. computeIfAbsent(key, mappingFunction) computes a value lazily and inserts it atomically.
Map<String, String> map = new ConcurrentHashMap<>();
map.putIfAbsent("foo", "bar");
map.computeIfAbsent("foo", k -> k + "bar");These methods are part of the Map interface and provide a concise, thread‑safe way to perform conditional updates.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
