The java.util.concurrent package supplies tools for coordinating tasks and safely sharing selected data structures. CountDownLatch waits for a fixed number of events, Semaphore limits simultaneous access to a resource, and BlockingQueue transfers values between producers and consumers. Concurrent collections and atomic values solve other specific sharing problems. These utilities are easier to use correctly when you name the coordination requirement first instead of choosing a class merely because it sounds thread-safe.
Count down events with a latch
CountDownLatch starts with a count. Each countDown reduces it, and await waits until it reaches zero. The latch is one-use: it cannot be reset for another round. Put countDown in a finally block when task completion should count even after a task fails, or record failure separately when successful work is required. A latch establishes a completion boundary but does not by itself make every update to unrelated shared state atomic.
Limit access with permits
Semaphore maintains permits that callers acquire before entering a limited resource and release afterward. A semaphore with two permits can allow two tasks at once, unlike a monitor that excludes all but one. Release only a permit that was actually acquired, and use finally to avoid leaking it after an exception. The semaphore does not own a specific resource object; your code must keep the permit rule aligned with the resource it protects.
Transfer work through a blocking queue
BlockingQueue supports operations such as put and take that wait when a bounded queue is full or empty. It provides a natural producer-consumer boundary without manually implementing wait and notify. ArrayBlockingQueue has a fixed capacity, making buffering limits explicit. Design a termination signal, such as a documented sentinel value, or another shutdown mechanism. An empty queue does not mean the producer has permanently finished; a consumer otherwise may wait indefinitely.
Use concurrent maps for atomic map operations
ConcurrentHashMap supports safe concurrent access but ordinary multi-step code can still race. Reading a count and then writing count plus one is not made atomic simply because the map is concurrent. merge and compute offer suitable per-key atomic updates. Null keys and null values are not permitted in ConcurrentHashMap. Iteration is weakly consistent rather than a frozen snapshot, so do not use an iteration view as a transactional report of all simultaneous changes.
Coordinate phases with appropriate tools
CyclicBarrier supports reusable meetings among a fixed group of participants, while Phaser supports more flexible phase coordination. These tools are different from a latch that permanently opens at zero. A task failure can leave peers waiting unless the design handles broken coordination or cancellation. Start with the simplest primitive matching the requirement. A latch is ideal for one completion gate; a queue is clearer for a stream of work items arriving over time.
Define interruption and ownership policies
Blocking coordination operations can throw InterruptedException. Decide whether a task propagates that signal or stops and restores its interrupt status. Do not swallow it and continue indefinitely. Also decide who creates and closes the surrounding executor or worker threads. A correctly used latch can still leave an application running forever if a worker is never released or a pool is never shut down. Coordination correctness includes both data safety and eventual termination.
Practice tasks and checks
Extend QueueTransfer to send three parcels through a queue with capacity one. The program should still finish because the consumer removes items as the producer supplies them. Preserve the sentinel after all work. Next, replace the producer's protocol with a record containing a work value and a completion flag, so ordinary text cannot accidentally stop the consumer. For ConcurrentCounts, use two category keys and verify each expected total after joins. Do not require a particular map iteration order. Finally, sketch a semaphore-controlled connection pool and mark the acquire, protected operation, and finally release. Explain what happens if acquisition itself is interrupted, and ensure your cleanup does not release a permit the task never acquired.
Java Summary
- Use a latch for a one-time completion gate and a queue for transferred work.
- Concurrent collections do not make arbitrary multi-step sequences atomic.
- Define termination, permit ownership, and interruption policies.
- Continue with thread completion when these examples are clear.
Java Examples
Each example shows the code, the output it produces and what each part does.
Wait until two tasks report completion
Save this program as LatchCompletion.java. Compile it with javac LatchCompletion.java and run it with java LatchCompletion.
Expected output:
The latch begins at two, so one report alone cannot release await. Each task reduces the count once. After both reports, await returns; joins then confirm the threads have terminated as well. These boundaries differ because a real task could continue doing work after countDown. The demonstration places reporting at the task's end and prints only after both coordination requirements have been satisfied.
import java.util.concurrent.CountDownLatch;
public class LatchCompletion {
public static void main(String[] args) throws InterruptedException {
CountDownLatch ready = new CountDownLatch(2);
Runnable task = () -> ready.countDown();
Thread first = new Thread(task);
Thread second = new Thread(task);
first.start(); second.start();
ready.await();
first.join(); second.join();
System.out.println("Both reported");
}
}Both reportedPass values through a bounded queue
Save this program as QueueTransfer.java. Compile it with javac QueueTransfer.java and run it with java QueueTransfer.
Expected output:
The producer puts the work item before the sentinel, and the FIFO queue preserves that order. main takes and prints Parcel, then takes END and exits without printing the sentinel. take can wait until an item is available. END is a protocol value reserved for this example; a real domain that allows that same text as data needs a separate message type or another unambiguous termination mechanism.
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class QueueTransfer {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new ArrayBlockingQueue<>(2);
Thread producer = new Thread(() -> {
try {
queue.put("Parcel");
queue.put("END");
} catch (InterruptedException error) {
Thread.currentThread().interrupt();
}
});
producer.start();
String item;
while (!(item = queue.take()).equals("END")) {
System.out.println(item);
}
producer.join();
}
}ParcelUpdate a per-key count atomically
Save this program as ConcurrentCounts.java. Compile it with javac ConcurrentCounts.java and run it with java ConcurrentCounts.
Expected output:
For an absent key, merge stores one. For an existing key, it combines the current value with one atomically for that key. Both workers perform three hundred updates, so the final value is six hundred after both joins. Replacing merge with separate get and put calls would introduce a lost-update race even on ConcurrentHashMap. The map protects its supported atomic operations, not arbitrary sequences assembled by the caller.
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentCounts {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
Runnable task = () -> {
for (int i = 0; i < 300; i++) counts.merge("view", 1, Integer::sum);
};
Thread first = new Thread(task);
Thread second = new Thread(task);
first.start(); second.start();
first.join(); second.join();
System.out.println(counts.get("view"));
}
}600Java FAQ
Can CountDownLatch be reused?
No. Once its count reaches zero, await calls can pass and the latch stays open. Use a new latch for another one-time gate or a reusable coordination tool when participants meet repeatedly.
Does ConcurrentHashMap make every calculation atomic?
No. Its individual operations have documented guarantees. A compound get-then-put calculation can race; use merge, compute, or a wider coordination design that matches the intended invariant.
What is the difference between put and offer on a BlockingQueue?
put waits as long as necessary for space in a full bounded queue, and take waits for an element in an empty one. offer and poll return immediately with false or null instead, and their timed versions wait for at most the time you give. Choose based on whether a producer should block or give up.
Can ConcurrentHashMap store null keys or values?
No. It throws NullPointerException for a null key or value. Because several threads read the map at once, a null result from get must mean "no mapping", with no ambiguity about a stored null. Use an explicit placeholder value if you need to represent "nothing".
Can a Semaphore limit requests per second?
Not by itself. A semaphore limits how many tasks are inside a section at the same time, such as five concurrent downloads. Limiting how many start per second needs a time-based approach, such as a scheduled task that releases permits or a rate-limiter library.
Java Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Which operation waits for a latch count to reach zero?
Why is END not printed by QueueTransfer?
Java Quiz
2 questions to check what you learned.
Which update safely increments a map count per key?
- merge(key, 1, Integer::sum)
- put(key, get(key) + 1)
- get(key) alone
When should a semaphore permit be released?
- After successful acquisition, in cleanup
- Before acquisition to increase capacity
- Only when the program exits