Concurrency Utilities
The java.util.concurrent toolkit lets many threads share data safely without you hand-writing locks. Learn atomics, concurrent collections, blocking queues, and the coordination tools (latches, barriers, semaphores, locks) the pros reach for.
Learn Concurrency Utilities in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
You should already understand Multithreading — creating threads, synchronized , and race conditions. This lesson gives you sharper, ready-made tools from java.util.concurrent so you rarely have to write low-level locking by hand.
Imagine threads as employees sharing one building, and the concurrency utilities as the building's systems that keep everyone safe and orderly:
The classic concurrency bug is count++ . It looks like one step but it is really three: read the value, add one, write it back. If two threads read the same value before either writes, one update is silently lost.
An atomic class fixes this. AtomicInteger.incrementAndGet() performs the whole read-modify-write as a single, uninterruptible operation — no lock, no lost updates. There are matching AtomicLong , AtomicBoolean , and AtomicReference types too.
Run the worked example below. The plain int almost always finishes below 200,000 because updates collide; the AtomicInteger always lands exactly on 200,000.
A plain HashMap or ArrayList is not thread-safe . Used from several threads at once it can corrupt its internal structure, lose data, or throw ConcurrentModificationException . The java.util.concurrent package gives you drop-in safe replacements.
A BlockingQueue is the cleanest way to hand work from one thread to another. put() blocks the producer when the queue is full; take() blocks the consumer when it is empty. That built-in waiting means you never busy-loop and never write your own wait() / notify() .
ArrayBlockingQueue has a fixed capacity (good for back-pressure); LinkedBlockingQueue can be unbounded. A common pattern is to send a special "poison pill" value (here, "DONE" ) to tell the consumer to stop.
Fill in the three blanks so two threads can count page hits without losing any. Each thread adds 50,000, so the total must be exactly 100,000. Hints are on each line after 👉 .
These three tools let threads agree on timing without sharing data:
synchronized is fine for simple cases, but a ReentrantLock gives you more control: a timeout with tryLock() , the ability to interrupt a waiting thread, and fairness options. "Reentrant" means the same thread can lock it again — the lock counts holds and only frees when the count returns to zero.
The golden rule: a ReentrantLock does not release itself. You must call unlock() , and it must live in a finally block so it runs even if an exception is thrown.
A ReadWriteLock splits locking into two: many threads can hold the read lock at once, but the write lock is exclusive. That is a big win for data read far more often than it is written.
Fill in the two blanks so the withdraw method locks before touching the shared balance and always unlocks afterwards. Notice the unlock() belongs in the finally block.
Now write it yourself. You only get a comment outline — no filled-in logic. Use a CountDownLatch of 3, start three worker threads that each print and count down, then print a final message after await() .
💡 tryLock() with a timeout avoids deadlocks: if (lock.tryLock(5, TimeUnit.SECONDS)) {' ... '} gives up rather than waiting forever.
💡 StampedLock (Java 8+) can beat ReadWriteLock by allowing optimistic reads that take no lock at all.
💡 LongAdder outperforms AtomicLong under very high contention because it spreads the count across cells.
💡 Prefer the high-level tools. A BlockingQueue or ExecutorService is almost always clearer and safer than hand-rolled wait() / notify() .
You can now reach for the right tool from java.util.concurrent : atomics for lock-free counters, concurrent collections instead of unsafe ones, a BlockingQueue to pass work between threads, and latches, barriers, semaphores, and locks to coordinate timing — always unlocking in a finally .
Next up: CompletableFuture & async programming — compose non-blocking tasks and chain results without raw threads.
Practice quiz
Why is a plain int count++ unsafe across threads?
- int is too small to hold the value
- the JVM forbids sharing primitives
- count++ is really read-modify-write, so concurrent updates can be lost
- ++ is not defined for fields
Answer: count++ is really read-modify-write, so concurrent updates can be lost. count++ is three steps (read, add, write); two threads can read the same value and one update is silently lost.
After new AtomicInteger(10), what does getAndAdd(5) return and what is the new value?
- returns 10, value 15
- returns 15, value 15
- returns 15, value 10
- returns 10, value 10
Answer: returns 10, value 15. getAndAdd returns the previous value (10) then adds, leaving 15. Verified by running it on Java 21.
What does ConcurrentHashMap.merge("apples", 1, Integer::sum) do under concurrent access?
- Replaces the value with 1
- Throws ConcurrentModificationException
- Locks the entire map for every call
- Atomically does get + add + put for that key
Answer: Atomically does get + add + put for that key. merge performs the read-add-write for a key atomically, so two threads each merging 1000 times yield 2000.
When is CopyOnWriteArrayList the right choice?
- When writes vastly outnumber reads
- When reads vastly outnumber writes (e.g. listeners)
- When you need sorted order
- When the list must be fixed size
Answer: When reads vastly outnumber writes (e.g. listeners). Every write copies the backing array (expensive), but iteration never locks or throws, which suits read-heavy data like listener lists.
In a BlockingQueue, what does put() do when the queue is full?
- blocks until space is free
- throws an exception
- returns false
- overwrites the oldest element
Answer: blocks until space is free. put() blocks the producer when full and take() blocks the consumer when empty, giving built-in back-pressure with no busy-waiting.
Why must a ReentrantLock be unlocked in a finally block?
- finally runs faster
- It is required to compile
- It does not release automatically, so an exception before unlock() would leave it held forever
- finally re-enters the lock
Answer: It does not release automatically, so an exception before unlock() would leave it held forever. Unlike synchronized, a ReentrantLock isn't released on method exit; a finally block guarantees unlock() runs on every path out.
What is the key difference between CountDownLatch and CyclicBarrier?
- A latch is reusable; a barrier is one-shot
- A latch is one-shot and cannot reset; a barrier is reusable across phases
- They are identical
- Only a barrier blocks threads
Answer: A latch is one-shot and cannot reset; a barrier is reusable across phases. Once a CountDownLatch hits zero it stays open; a CyclicBarrier releases all waiters when they arrive and then resets for the next phase.
What does Semaphore.tryAcquire() do when no permits are available?
- blocks until one is free
- throws InterruptedException
- creates a new permit
- returns false instead of blocking
Answer: returns false instead of blocking. tryAcquire() returns false immediately when no permit is free, unlike acquire() which blocks.
What does it mean that ReentrantLock is reentrant?
- Any thread can unlock it
- The thread already holding it can acquire it again without deadlocking
- It can only be locked once
- It automatically retries on failure
Answer: The thread already holding it can acquire it again without deadlocking. The holding thread can lock again; a hold count rises on each lock() and the lock frees only when the count returns to zero.
With a ReadWriteLock, which combination is allowed at the same time?
- Many readers AND many writers
- One reader and one writer together
- Many readers OR one writer (never both)
- Only one reader at a time
Answer: Many readers OR one writer (never both). A ReadWriteLock permits multiple concurrent readers or a single exclusive writer, but never readers and a writer simultaneously.