Overview of Java Thread-Safe Classes
| Class Type | Characteristics | Description |
|---|---|---|
| Legacy Thread-Safe Classes | Uses synchronized for thread safety | Deprecated classes like Stack, Vector, Hashtable |
| Collections-Decorated Classes | Wrapper classes using synchronized | Thread-safe versions of standard collections |
| JUC Classes | Uses CAS and multiple locks | Recommended for concurrent applications |
Legacy Thread-Safe Collections
Classes like Vector (Stack) and Hashtable use synchronized methods to ensure thread safety.
public synchronized V get(Object key) {
Entry,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
Colletcions-Decorated Thread-Safe Collections
These are wrapper classes that accept the original collection and add synchronized protection.
Collections.synchronizedCollection
Collections.synchronizedList
Collections.synchronizedMap
Collections.synchronizedSet
Collections.synchronizedNavigableMap
Collections.synchronizedNavigableSet
Collections.synchronizedSortedMap
Collections.synchronizedSortedSet
The implementation uses a mutex object to synchronize all method calls:
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
private static class SynchronizedMap<K,V> implements Map<K,V>, Serializable {
private final Map<K,V> m;
final Object mutex;
SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
mutex = this;
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
// Other methods follow the same pattern
}
JUC Thread-Safe Classes
JUC provides three categories of thread-safe classes:
- Blocking Classes: Use locks (usually ReentrantLock) and provide blocking methods
- CopyOnWrite Classes: Use copy-on-write mechanism for thread safety
- Concurrent Classes: High-performance concurrent containers using CAS and multiple locks
Concurrent classes have weak consistency:
- Iterators are weakly consistent (fail-safe)
- Size operations may not be 100% accurate
- Read operations may see stale data
ConcurrentHashMap Usage
Multi-threaded Counting Example
Scenario: Count letter occurrences across 26 files using multiple threads.
import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
class FileProcessor {
static <V> void processFiles(Supplier<Map<String, V>> mapSupplier,
BiConsumer<Map<String, V>, List<String>> processor) {
Map<String, V> frequencyMap = mapSupplier.get();
List<Thread> threads = new ArrayList<>();
for (int i = 1; i <= 26; i++) {
int fileIndex = i;
Thread thread = new Thread(() -> {
List<String> characters = readFileContent(fileIndex);
processor.accept(frequencyMap, characters);
});
threads.add(thread);
}
threads.forEach(Thread::start);
threads.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
System.out.println(frequencyMap);
}
public static List<String> readFileContent(int fileNumber) {
List<String> characters = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new FileInputStream("./data/" + fileNumber + ".txt")))) {
String line;
while ((line = reader.readLine()) != null) {
characters.add(line);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return characters;
}
}
public class Main {
public static void main(String[] args) {
FileProcessor.processFiles(
() -> new HashMap<String, Integer>(),
(map, characters) -> {
for (String ch : characters) {
Integer count = map.get(ch);
int newValue = (count == null) ? 1 : count + 1;
map.put(ch, newValue);
}
}
);
}
}
Result with HashMap (incorrect due to thread safety issues):
{a=199, b=200, c=197, d=196, e=198, f=199, g=198, h=197, i=199, j=198, k=198, l=198, m=199, n=197, o=195, p=199, q=197, r=199, s=200, t=198, u=200, v=200, w=199, x=200, y=196, z=199}
Incorrect Improvement (HashMap to ConcurrentHashMap)
FileProcessor.processFiles(
() -> new ConcurrentHashMap<String, Integer>(),
(map, characters) -> {
for (String ch : characters) {
Integer count = map.get(ch);
int newValue = (count == null) ? 1 : count + 1;
map.put(ch, newValue);
}
}
);
Result (still incorrect because ConcurrentHashMap only guarantees atomicity of individual operations):
{a=198, b=200, c=200, d=200, e=198, f=199, g=199, h=198, i=200, j=200, k=198, l=200, m=200, n=200, o=199, p=198, q=199, r=194, s=198, t=199, u=199, v=200, w=200, x=199, y=200, z=199}
Correct Improvement
FileProcessor.processFiles(
() -> new ConcurrentHashMap<String, LongAdder>(),
(map, characters) -> {
for (String ch : characters) {
LongAdder counter = map.computeIfAbsent(ch, key -> new LongAdder());
counter.increment();
}
}
);
HashMap Fundamentals
Basic Structure
- JDK 7: Array + Linked List
- JDK 8: Array + Linked List + Red-Black Tree
Key HashMap Properties
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
transient Node<K,V>[] table;
transient int size;
transient int modCount;
int threshold;
final float loadFactor;
}
JDK 7 HashMap Deadolck Issue
In JDK 7, HashMap could create circular linked lists during concurrent resizing due to head insertion method.
void transfer(Node[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Node<K,V> e : table) {
while (null != e) {
Node<K,V> next = e.next;
if (rehash) {
e.hash = (e.key == null) ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
Scenario demonstrating deadlock:
Original chain: [1] (1,35)->(35,16)->(16,null)
Thread A executes to position 1, e = (1,35), next = (35,16), then suspends
Thread B performs resizing:
- Rehashes key 16 to position 17
- Rebuilds chain at position 1: (35,1)->(1,null)
Thread A resumes:
- e = (1,null), next = (35,1)
- First iteration: [1] (1,null)
- Second iteration: [1] (35,1)->(1,null)
- Third iteration: e = (1,null), e.next becomes 35, creating cycle
- Result: [1] (1,35)->(35,1)->(1,35)
JDK 8 resolved this by using tail insertion and maintaining head/tail references, but concurrent modifications can still cause data loss.