DEV Community

DevCorner
DevCorner

Posted on

Synchronizing Java Collections for Thread Safety: A Complete Guide

Introduction

In multi-threaded environments, collections in Java need to be synchronized to avoid race conditions and ensure thread safety. Java provides multiple ways to achieve synchronization, such as synchronized wrappers, concurrent collections, and explicit locking mechanisms.

This guide will cover all the major classes and interfaces in the Java Collection Framework (JCF) and how to make them thread-safe with code examples.


1. Synchronizing Collections Using Collections.synchronizedXXX()

Java provides utility methods in the Collections class to create synchronized versions of collections:

1.1 Synchronizing List

import java.util.*;

public class SynchronizedListExample {
    public static void main(String[] args) {
        List<String> list = Collections.synchronizedList(new ArrayList<>());

        list.add("A");
        list.add("B");
        list.add("C");

        synchronized (list) {  // Explicit synchronization required for iteration
            for (String item : list) {
                System.out.println(item);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

1.2 Synchronizing Set

Set<String> set = Collections.synchronizedSet(new HashSet<>());
Enter fullscreen mode Exit fullscreen mode

1.3 Synchronizing Map

Map<Integer, String> map = Collections.synchronizedMap(new HashMap<>());
Enter fullscreen mode Exit fullscreen mode

1.4 Synchronizing Queue

Java does not provide Collections.synchronizedQueue(), but you can use synchronizedList with LinkedList:

Queue<Integer> queue = Collections.synchronizedList(new LinkedList<>());
Enter fullscreen mode Exit fullscreen mode

1.5 Synchronizing Stack

Since Stack extends Vector, it is already synchronized. However, for better performance, consider using Deque:

Stack<Integer> stack = new Stack<>(); // Already synchronized (extends Vector)
Enter fullscreen mode Exit fullscreen mode

Alternatively, using Deque:

Deque<Integer> stack = new ArrayDeque<>();
Collections.synchronizedCollection(stack);
Enter fullscreen mode Exit fullscreen mode

1.6 Synchronizing LinkedList

While LinkedList is not synchronized by default, you can wrap it using Collections.synchronizedList():

List<Integer> linkedList = Collections.synchronizedList(new LinkedList<>());
Enter fullscreen mode Exit fullscreen mode

For better performance in concurrent scenarios, use ConcurrentLinkedQueue or ConcurrentLinkedDeque.


2. Using Concurrent Collections (Thread-Safe Implementations)

Java provides built-in thread-safe collections under the java.util.concurrent package.

2.1 Using CopyOnWriteArrayList for Thread-Safe Lists

import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListExample {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        list.add("X");
        list.add("Y");
        list.add("Z");

        for (String item : list) {
            System.out.println(item);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

2.2 Using CopyOnWriteArraySet for Thread-Safe Sets

import java.util.concurrent.CopyOnWriteArraySet;
CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
Enter fullscreen mode Exit fullscreen mode

2.3 Using ConcurrentHashMap for Thread-Safe Maps

import java.util.concurrent.ConcurrentHashMap;
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
Enter fullscreen mode Exit fullscreen mode

2.4 Using Concurrent Queues & Deques

import java.util.concurrent.*;
Queue<Integer> queue = new ConcurrentLinkedQueue<>();
Deque<Integer> deque = new ConcurrentLinkedDeque<>();
Enter fullscreen mode Exit fullscreen mode

3. Explicit Synchronization with Locks

3.1 Using synchronized block

List<String> list = new ArrayList<>();

synchronized (list) {
    list.add("Thread Safe");
}
Enter fullscreen mode Exit fullscreen mode

3.2 Using ReentrantLock for Fine-Grained Control

import java.util.*;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private static final List<String> list = new ArrayList<>();
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        lock.lock();
        try {
            list.add("Safe Element");
        } finally {
            lock.unlock();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Performance Comparison & Best Practices

Collection Type Synchronized Wrappers Concurrent Collections Explicit Locking
List Collections.synchronizedList() CopyOnWriteArrayList ReentrantLock / synchronized
Set Collections.synchronizedSet() CopyOnWriteArraySet ReentrantLock / synchronized
Map Collections.synchronizedMap() ConcurrentHashMap ReentrantLock / synchronized
Queue Collections.synchronizedList(new LinkedList<>()) ConcurrentLinkedQueue ReentrantLock / synchronized
Deque N/A ConcurrentLinkedDeque ReentrantLock / synchronized
Stack Already synchronized (Stack<>) ConcurrentLinkedDeque ReentrantLock / synchronized
LinkedList Collections.synchronizedList(new LinkedList<>()) ConcurrentLinkedQueue / ConcurrentLinkedDeque ReentrantLock / synchronized

Best Practices:

  1. Use CopyOnWriteArrayList for read-heavy operations.
  2. Use ConcurrentHashMap instead of synchronizedMap() for better performance.
  3. Use explicit locks (ReentrantLock) only when fine-grained control is required.

Conclusion

Java provides multiple ways to synchronize collections, from synchronized wrappers (Collections.synchronizedXXX()) to high-performance concurrent collections (CopyOnWriteArrayList, ConcurrentHashMap) and explicit locks (ReentrantLock). The right choice depends on your use case: performance requirements, read/write ratio, and concurrency level.

By understanding these approaches, you can ensure safe and efficient multi-threaded operations in your Java applications.

Happy Coding! 🚀

Top comments (0)