Skip to content

Expert · Multithreading and concurrency · topic 44 of 60 · 25 min

Atomic variables and concurrent collections

Explanation

java.util.concurrent offers thread-safe building blocks: AtomicInteger and AtomicLong for lock-free counters, ConcurrentHashMap for shared maps (with atomic merge and compute), CopyOnWriteArrayList for rarely-changed lists, BlockingQueue for producer-consumer pipelines and CountDownLatch to wait for several tasks. Immutable objects need no synchronisation at all.

Atomic variables

An atomic variable performs read-modify-write operations as one indivisible step using special CPU instructions (compare-and-swap), without locks. It is the simplest fix for shared counters and statistics.

java
AtomicInteger requests = new AtomicInteger();
requests.incrementAndGet();            // safe from any number of threads
requests.addAndGet(5);
int now = requests.get();
AtomicLong totalPaise = new AtomicLong();
totalPaise.addAndGet(49_900);
requests.updateAndGet(n -> Math.min(n, 1000));   // atomic update with a function

ConcurrentHashMap

A plain HashMap used by several threads can lose entries or even corrupt itself. ConcurrentHashMap is designed for many readers and writers at once. Its compound methods (merge, compute, computeIfAbsent, putIfAbsent) are atomic, so counting per key is safe.

java
ConcurrentHashMap<String, Integer> hitsByCity = new ConcurrentHashMap<>();
// called from many request threads at the same time:
hitsByCity.merge("Pune", 1, Integer::sum);              // atomic per key
hitsByCity.computeIfAbsent("Goa", k -> 0);

// WRONG with any map shared between threads (check-then-act race):
// if (!map.containsKey(k)) map.put(k, 1); else map.put(k, map.get(k) + 1);

Other concurrent collections

ClassUse when
ConcurrentHashMapA shared map with frequent reads and writes
CopyOnWriteArrayListA list read very often, changed rarely (listeners, configuration)
ArrayBlockingQueue / LinkedBlockingQueueProducer-consumer hand-off between threads
ConcurrentLinkedQueueA non-blocking shared queue
Collections.synchronizedListOld style: one lock for everything; usually slower

Producer-consumer with BlockingQueue

java
BlockingQueue<String> smsQueue = new ArrayBlockingQueue<>(100);

Thread producer = new Thread(() -> {
    for (int i = 1; i <= 5; i++) {
        try { smsQueue.put("OTP for user " + i); } catch (InterruptedException e) { return; }   // waits if full
    }
});
Thread consumer = new Thread(() -> {
    try {
        while (true) System.out.println("Sending " + smsQueue.take());   // waits if empty
    } catch (InterruptedException e) { /* stop */ }
});

CountDownLatch

java
CountDownLatch ready = new CountDownLatch(3);           // wait for 3 services
for (String svc : List.of("db", "cache", "payments")) {
    new Thread(() -> { connect(svc); ready.countDown(); }).start();
}
ready.await();                                          // main waits until the count reaches 0
System.out.println("All services connected, accepting traffic");

Immutability: the simplest thread safety

An object that never changes (final fields, no setters: records, String, LocalDate, List.of) can be shared by any number of threads with no locks at all. Designing most data as immutable and keeping mutable shared state small is the most reliable concurrency strategy.

Careful: Iterating a ConcurrentHashMap is safe but "weakly consistent": you may or may not see updates made during the loop, and size() is only an estimate under concurrent change. Do not build logic that needs an exact snapshot from a constantly changing map; take a copy or use proper synchronisation.
Think of it like this: An AtomicInteger is like a token-number machine at a bank: any number of people can press the button at the same moment, and each still gets a unique number. A BlockingQueue is like a conveyor belt between the kitchen and the delivery desk: the cook waits if the belt is full, and the packer waits if it is empty.
Tip: Before writing synchronized code, ask: can I use an atomic, a ConcurrentHashMap, a BlockingQueue, or an immutable object instead? Usually yes.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Atomic variables and concurrent collections · 1/6

Concurrent utilities

In this lesson you will use ready-made thread-safe tools from java dot util dot concurrent.

Prefer a video by a teacher? Find free videos on “java atomicinteger concurrenthashmap blockingqueue countdownlatch” on YouTube ↗

In real life: Live request counters in an API gateway

An API gateway in front of a payments service counts requests per merchant to enforce rate limits. Hundreds of threads update the counts at once. A ConcurrentHashMap<String, AtomicInteger> with computeIfAbsent gives exact, lock-free counting per merchant.

java
ConcurrentHashMap<String, AtomicInteger> perMerchant = new ConcurrentHashMap<>();
boolean allow(String merchantId) {
    int n = perMerchant.computeIfAbsent(merchantId, k -> new AtomicInteger()).incrementAndGet();
    return n <= 100;   // max 100 requests in the current window
}

Practice (0/3 done)

Task 1 · Question

Which correctly counts per key from many threads?

Task 2 · Question

BlockingQueue.take() on an empty queue…

Task 3 · Practice

Build a producer-consumer program: one producer thread puts 20 "orders" into an ArrayBlockingQueue of capacity 5, and two consumer threads take and "process" them (sleep 50 ms each), updating an AtomicInteger of processed orders. Use a CountDownLatch(20) so main can print "All 20 processed" at the end.

Quick quiz

Question 1

1.What does AtomicInteger.incrementAndGet() return?

Question 2

2.CopyOnWriteArrayList is best when…

Question 3

3.Why are immutable objects thread-safe?

Question 4

4.What does CountDownLatch.await() do?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does AtomicInteger.incrementAndGet() return?

Answer: The new value

getAndIncrement returns the old value.

CopyOnWriteArrayList is best when…

Answer: Reads are frequent and writes are rare

Every write copies the array.

Why are immutable objects thread-safe?

Answer: Their state never changes, so there is nothing to race on

No writes, no races.

What does CountDownLatch.await() do?

Answer: Waits until the count reaches zero

Other threads call countDown().

Two bank balances must be updated together by several threads. Which tool?Related

Answer: One std::mutex locked around both updates

Only a lock makes the combined change appear all at once.

From Concurrency 2: atomics, async, futures and condition variables

How do several threads send on the same channel?Related

Answer: Clone the Sender for each thread

Sender can be cloned; each clone can move into a thread.

From Message passing with channels

Which is best for a simple hit counter shared by many goroutines?Related

Answer: atomic.Int64

atomic.Int64 gives safe, fast Add and Load.

From Mutex, Once, atomic and the race detector

Many threads read a config and rarely update it. Which is a good fit?Related

Answer: RwLock

RwLock allows many readers at once.

From Shared state: Arc, Mutex, Send and Sync

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Multithreading and concurrency” before its exam: Virtual threads (Java 21).
  2. Module 9 exam: Multithreading and concurrency · 12 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, JDBC, Maven, Gradle and JUnit. 60 topics left in the course before the final exam.

← Previous: Executors, futures and CompletableFuture