Skip to main content
    Courses/Java/Concurrency Utilities

    Lesson 27 • Advanced

    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.

    Before You Start

    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.

    What You'll Learn in This Lesson

    • Use AtomicInteger to count across threads without locks
    • Pick ConcurrentHashMap and CopyOnWriteArrayList over unsafe collections
    • Pass work safely between threads with a BlockingQueue
    • Wait for tasks with CountDownLatch and sync phases with CyclicBarrier
    • Limit concurrent access with a Semaphore
    • Use ReentrantLock and ReadWriteLock the right way (unlock in finally)

    🏢 Real-World Analogy: An Office Building

    Imagine threads as employees sharing one building, and the concurrency utilities as the building's systems that keep everyone safe and orderly:

    • AtomicInteger is the turnstile counter at the door — every entry clicks it up by one, and two people pushing through at once can never make it lose a count.
    • ConcurrentHashMap / CopyOnWriteArrayList are filing cabinets built so many clerks can use them at once without jamming or tearing pages.
    • BlockingQueue is the mail room conveyor — senders drop parcels on, receivers take them off, and each side simply waits if the belt is full or empty.
    • Semaphore is the car park barrier — only N cars allowed in; arrival N+1 waits for someone to leave.
    • CountDownLatch is the launch gate that opens once three setup crews each report "done."
    • CyclicBarrier is a meeting room where everyone waits for the whole team before each phase — and it is reused every phase.
    • ReentrantLock / ReadWriteLock are keycards for a room: one writer gets exclusive access, while many readers can browse together.

    1️⃣ Atomics — Lock-Free Counters

    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.

    Worked Example: AtomicInteger vs a plain int
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class Main {
        // A plain int is NOT thread-safe: count++ is really read, add, write —
        // three steps. Two threads can read the same value and one update is lost.
        static int unsafeCount = 0;
    
        // AtomicInteger does the whole "read-modify-write" as ONE atomic step,
        // with no lock needed. incrementAndGet() can't be interrupted halfway.
        static AtomicInteger safeCount = new AtomicInteger(0);
    
        public static void main(String[] args) throws InterruptedException {
            Runnable bumpUnsafe = () -> { for (int i = 0; i < 100_000; i++) unsafeCount++; };
            Runnable bumpSafe   = () -> { for (int i = 0; i < 100_000; i++) safeCount.incrementAndGet(); };
    
            Thread t1 = new Thread(bumpUnsafe), t2 = new Thread(bumpUnsafe);
            Thread t3 = new Thread(bumpSafe),   t4 = new Thread(bumpSafe);
            t1.start(); t2.start(); t3.start(); t4.start();
            t1.join();  t2.join();  t3.join();  t4.join();   // wait for all four
    
            // Expected total for each = 2 threads * 100,000 = 200,000.
            System.out.println("Unsafe int total : " + unsafeCount + " (usually < 200000 — lost updates!)");
            System.out.println("Atomic total     : " + safeCount.get() + " (always 200000)");
    
            // A few other atomic operations:
            AtomicInteger a = new AtomicInteger(10);
            System.out.println("getAndAdd(5)      : returns " + a.getAndAdd(5) + ", now " + a.get());
            System.out.println("compareAndSet     : " + a.compareAndSet(15, 99) + ", now " + a.get());
        }
    }
    Output
    Unsafe int total : 137204 (usually < 200000 — lost updates!)
    Atomic total     : 200000 (always 200000)
    getAndAdd(5)      : returns 10, now 15
    compareAndSet     : true, now 99
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    2️⃣ Concurrent Collections

    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.

    • ConcurrentHashMap — the default shared map. It locks only small slices internally, so many threads read and write different keys simultaneously. Use merge() or compute() to update a key atomically.
    • CopyOnWriteArrayList — every write makes a fresh copy of the backing array. That is expensive for frequent writes but perfect when reads vastly outnumber writes (event listeners, config snapshots) because iteration never needs a lock and never throws.
    Worked Example: ConcurrentHashMap & CopyOnWriteArrayList
    import java.util.Map;
    import java.util.List;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.concurrent.CopyOnWriteArrayList;
    
    public class Main {
        public static void main(String[] args) throws InterruptedException {
            System.out.println("=== Thread-safe collections ===\n");
    
            // 1. ConcurrentHashMap — many threads can read/write at once safely.
            //    A plain HashMap can corrupt or throw under concurrent writes.
            ConcurrentHashMap<String, Integer> votes = new ConcurrentHashMap<>();
            Runnable voter = () -> {
                for (int i = 0; i < 1000; i++) {
                    // merge() does the whole "get + add + put" atomically per key.
                    votes.merge("apples", 1, Integer::sum);
                }
            };
            Thread v1 = new Thread(voter), v2 = new Thread(voter);
            v1.start(); v2.start(); v1.join(); v2.join();
            System.out.println("ConcurrentHashMap votes for apples: " + votes.get("apples"));
    
            // 2. CopyOnWriteArrayList — every write copies the array.
            //    Great when reads vastly outnumber writes (e.g. listeners).
            List<String> listeners = new CopyOnWriteArrayList<>();
            listeners.add("logger");
            listeners.add("metrics");
            // Safe to iterate even if another thread adds during the loop —
            // the iterator sees a stable snapshot, never ConcurrentModificationException.
            for (String name : listeners) {
                System.out.println("Notifying listener: " + name);
            }
            System.out.println("Listener count: " + listeners.size());
        }
    }
    Output
    === Thread-safe collections ===
    
    ConcurrentHashMap votes for apples: 2000
    Notifying listener: logger
    Notifying listener: metrics
    Listener count: 2
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    3️⃣ BlockingQueue — Producer / Consumer

    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.

    Worked Example: ArrayBlockingQueue
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.BlockingQueue;
    
    public class Main {
        public static void main(String[] args) throws InterruptedException {
            System.out.println("=== Producer / Consumer with BlockingQueue ===\n");
    
            // A BlockingQueue hands work safely from producers to consumers.
            // put() blocks if the queue is full; take() blocks if it is empty —
            // so you never busy-wait and never need manual locks.
            BlockingQueue<String> queue = new ArrayBlockingQueue<>(2);  // capacity 2
    
            Thread producer = new Thread(() -> {
                String[] orders = {"Pizza", "Burger", "Salad", "DONE"};
                for (String order : orders) {
                    try {
                        queue.put(order);                 // waits if queue is full
                        System.out.println("Produced: " + order);
                    } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
                }
            });
    
            Thread consumer = new Thread(() -> {
                try {
                    String order;
                    while (!(order = queue.take()).equals("DONE")) {   // waits if empty
                        System.out.println("  Consumed: " + order);
                    }
                    System.out.println("  Got DONE — kitchen closing.");
                } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
            });
    
            producer.start();
            consumer.start();
            producer.join();
            consumer.join();
            System.out.println("All orders handled.");
        }
    }
    Output
    === Producer / Consumer with BlockingQueue ===
    
    Produced: Pizza
    Produced: Burger
      Consumed: Pizza
      Consumed: Burger
    Produced: Salad
    Produced: DONE
      Consumed: Salad
      Got DONE — kitchen closing.
    All orders handled.
    Real Java from java.util.concurrent. Run it free at onecompiler.com/java. Because the producer and consumer interleave, the exact ordering of "Produced"/"Consumed" lines can vary slightly between runs.

    🎯 Your Turn #1 — Thread-Safe Page Counter

    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 // 👉.

    Fill in the blanks (___)
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class Main {
        // 🎯 YOUR TURN — fill in the blanks marked with ___
    
        public static void main(String[] args) throws InterruptedException {
            // 1) Create a shared counter that is safe across threads.
            AtomicInteger hits = new ___;        // 👉 new AtomicInteger(0)
    
            // 2) Two threads each add 50,000 page hits.
            Runnable visit = () -> {
                for (int i = 0; i < 50_000; i++) {
                    hits.___;                    // 👉 incrementAndGet()  (atomic +1)
                }
            };
    
            Thread a = new Thread(visit), b = new Thread(visit);
            a.start(); b.start();
            a.join();  b.join();                  // wait for both before reading
    
            // 3) Read the final value with get().
            System.out.println("Total hits: " + hits.___);   // 👉 get()
    
            // ✅ Expected output:  Total hits: 100000
        }
    }
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    4️⃣ Coordinating Threads: Semaphore, CountDownLatch, CyclicBarrier

    These three tools let threads agree on timing without sharing data:

    • Semaphore hands out a fixed number of permits. acquire() takes one (blocking if none are free), release() returns one. Perfect for connection pools and rate limiting. tryAcquire() returns false instead of blocking.
    • CountDownLatch is a one-shot gate. You set a count, threads call countDown(), and whoever called await() stays blocked until the count hits zero. Once open it cannot be reset.
    • CyclicBarrier is a reusable rendezvous. A fixed number of threads each call await(); when the last one arrives they are all released together, then the barrier resets for the next phase.
    Worked Example: Semaphore + CountDownLatch + CyclicBarrier
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.CyclicBarrier;
    import java.util.concurrent.Semaphore;
    
    public class Main {
        public static void main(String[] args) throws InterruptedException {
            System.out.println("=== Coordinating threads ===\n");
    
            // 1. Semaphore — a pool of 3 permits (e.g. DB connections).
            //    tryAcquire() returns false instead of blocking when none are free.
            System.out.println("1. SEMAPHORE (pool of 3):");
            Semaphore pool = new Semaphore(3);
            for (String c : new String[]{"A", "B", "C", "D"}) {
                boolean got = pool.tryAcquire();
                System.out.println("  Conn-" + c + ": " + (got ? "acquired" : "WAIT (full)")
                    + " — free: " + pool.availablePermits());
            }
            pool.release();                                 // give one permit back
            System.out.println("  Released one — free: " + pool.availablePermits());
    
            // 2. CountDownLatch — ONE-SHOT gate. Main waits for 3 startup tasks.
            System.out.println("\n2. COUNTDOWNLATCH (server startup):");
            CountDownLatch latch = new CountDownLatch(3);
            for (String task : new String[]{"Database", "Cache", "Config"}) {
                new Thread(() -> {
                    System.out.println("  " + task + " ready");
                    latch.countDown();                      // -1 from the count
                }).start();
            }
            latch.await();                                  // blocks until count hits 0
            System.out.println("  All ready — server up! (count=" + latch.getCount() + ")");
    
            // 3. CyclicBarrier — REUSABLE. All 3 workers meet, then the barrier resets.
            System.out.println("\n3. CYCLICBARRIER (3 workers, 2 phases):");
            CyclicBarrier barrier = new CyclicBarrier(3,
                () -> System.out.println("  -- all arrived, phase complete --"));
            for (int w = 1; w <= 3; w++) {
                int id = w;
                new Thread(() -> {
                    try {
                        System.out.println("  Worker " + id + " phase 1 done");
                        barrier.await();                    // wait for the other two
                        System.out.println("  Worker " + id + " phase 2 done");
                        barrier.await();                    // barrier reused — that's "cyclic"
                    } catch (Exception e) { Thread.currentThread().interrupt(); }
                }).start();
            }
            Thread.sleep(200);   // give the demo threads time to finish printing
        }
    }
    Output
    === Coordinating threads ===
    
    1. SEMAPHORE (pool of 3):
      Conn-A: acquired — free: 2
      Conn-B: acquired — free: 1
      Conn-C: acquired — free: 0
      Conn-D: WAIT (full) — free: 0
      Released one — free: 1
    
    2. COUNTDOWNLATCH (server startup):
      Database ready
      Cache ready
      Config ready
      All ready — server up! (count=0)
    
    3. CYCLICBARRIER (3 workers, 2 phases):
      Worker 1 phase 1 done
      Worker 2 phase 1 done
      Worker 3 phase 1 done
      -- all arrived, phase complete --
      Worker 3 phase 2 done
      Worker 1 phase 2 done
      Worker 2 phase 2 done
      -- all arrived, phase complete --
    Real Java from java.util.concurrent. Run it free at onecompiler.com/java. The Semaphore section is deterministic; the latch and barrier sections run on separate threads, so lines within each phase may print in any order.

    5️⃣ ReentrantLock & ReadWriteLock

    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.

    Worked Example: ReentrantLock & ReadWriteLock
    import java.util.concurrent.locks.ReentrantLock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    public class Main {
        public static void main(String[] args) {
            System.out.println("=== Locks ===\n");
    
            // 1. ReentrantLock — the SAME thread can lock again (reentrant).
            //    getHoldCount() tracks how many times it has locked.
            System.out.println("1. REENTRANTLOCK:");
            ReentrantLock lock = new ReentrantLock();
            lock.lock();                                   // acquire
            try {
                System.out.println("  Locked (hold count: " + lock.getHoldCount() + ")");
                lock.lock();                               // same thread re-enters
                try {
                    System.out.println("  Re-entered (hold count: " + lock.getHoldCount() + ")");
                } finally {
                    lock.unlock();                         // each lock needs an unlock
                    System.out.println("  Inner unlock (hold count: " + lock.getHoldCount() + ")");
                }
            } finally {
                lock.unlock();                             // ALWAYS unlock in finally
                System.out.println("  Outer unlock (held by us? " + lock.isHeldByCurrentThread() + ")");
            }
    
            // 2. ReadWriteLock — MANY readers OR ONE writer (never both).
            System.out.println("\n2. READWRITELOCK:");
            ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
            rw.readLock().lock();
            rw.readLock().lock();                          // a second concurrent read is allowed
            System.out.println("  Read locks held: " + rw.getReadLockCount());
            System.out.println("  Write locked? " + rw.isWriteLocked() + " (writers wait for readers)");
            rw.readLock().unlock();
            rw.readLock().unlock();
    
            rw.writeLock().lock();                         // exclusive — no readers now
            try {
                System.out.println("  Write lock acquired? " + rw.isWriteLocked());
            } finally {
                rw.writeLock().unlock();
            }
        }
    }
    Output
    === Locks ===
    
    1. REENTRANTLOCK:
      Locked (hold count: 1)
      Re-entered (hold count: 2)
      Inner unlock (hold count: 1)
      Outer unlock (held by us? false)
    
    2. READWRITELOCK:
      Read locks held: 2
      Write locked? false (writers wait for readers)
      Write lock acquired? true
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    🎯 Your Turn #2 — Guard a Bank Balance

    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.

    Fill in the blanks (___)
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Main {
        // 🎯 YOUR TURN — protect a shared balance with a lock
    
        static int balance = 100;
        static ReentrantLock lock = new ReentrantLock();
    
        static void withdraw(int amount) {
            // 1) Acquire the lock before touching shared state.
            lock.___;                            // 👉 lock()
            try {
                if (balance >= amount) {
                    balance -= amount;
                    System.out.println("Withdrew " + amount + ", balance: " + balance);
                }
            } finally {
                // 2) ALWAYS release in finally, even if an exception is thrown.
                lock.___;                        // 👉 unlock()
            }
        }
    
        public static void main(String[] args) {
            withdraw(30);
            withdraw(50);
    
            // ✅ Expected output:
            // Withdrew 30, balance: 70
            // Withdrew 50, balance: 20
        }
    }
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    🧗 Mini-Challenge — Wait for the Crew

    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().

    Mini-Challenge (write the body yourself)
    import java.util.concurrent.CountDownLatch;
    
    public class Main {
        public static void main(String[] args) throws InterruptedException {
            // 🎯 MINI-CHALLENGE: Wait for a worker crew to finish
            // 1. Create a CountDownLatch starting at 3.
            // 2. Start 3 threads. Each prints "Worker N finished" then counts down.
            // 3. After latch.await(), print "All workers done!"
            //
            // ✅ Expected (order of "Worker" lines may vary):
            //   Worker 1 finished
            //   Worker 2 finished
            //   Worker 3 finished
            //   All workers done!
    
            // your code here
        }
    }
    This is real code — run it for free atonecompiler.com/javaor in your own editor.

    Common Errors (and the Fix)

    • Sharing a non-thread-safe collection: using a plain HashMap from many threads can throw ConcurrentModificationException or silently corrupt data. Fix: use ConcurrentHashMap (or CopyOnWriteArrayList for read-heavy lists).
    • Lock not released in finally: calling lock.lock() and then throwing before unlock() leaves the lock held forever — other threads hang. Fix: always lock(), then try { ... } finally { lock.unlock(); }.
    • Atomic vs synchronized confusion: an AtomicInteger makes one variable safe, not a whole block. Reading two atomics and combining them is still racy. Fix: use a lock or synchronized when several updates must happen together as one unit.
    • Calling unlock() without holding the lock: an extra unlock() throws IllegalMonitorStateException. Fix: pair exactly one unlock() with each lock() (re-entrant locks count holds).
    • Reusing a CountDownLatch: once its count hits zero it stays open and cannot be reset, so a second phase never blocks. Fix: use a CyclicBarrier for repeated sync points.

    Pro Tips

    💡 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().

    📋 Quick Reference

    UtilityKey APIUse Case
    AtomicIntegerincrementAndGet() / get()Lock-free shared counter
    ConcurrentHashMapmerge() / compute()Shared map, many writers
    CopyOnWriteArrayListadd() / iterateRead-heavy list (listeners)
    BlockingQueueput() / take()Producer / consumer handoff
    Semaphoreacquire() / release()Limit concurrent access
    CountDownLatchcountDown() / await()Wait for N completions (once)
    CyclicBarrierawait()Reusable phase rendezvous
    ReentrantLocklock() / unlock()Flexible mutual exclusion
    ReadWriteLockreadLock() / writeLock()Read-heavy concurrency

    Frequently Asked Questions

    When should I use AtomicInteger instead of synchronized?

    Use an atomic class when all you need is a single counter or flag updated by many threads. An AtomicInteger turns read-modify-write (like count++) into one uninterruptible CPU instruction with no lock, so it is faster and simpler than a synchronized block. Reach for synchronized or a ReentrantLock only when you must update several fields together as one consistent unit.

    What is the difference between ConcurrentHashMap and Collections.synchronizedMap?

    Collections.synchronizedMap wraps a HashMap and locks the whole map on every operation, so only one thread works at a time. ConcurrentHashMap splits the map internally so many threads can read and write different keys at once, giving far better throughput. ConcurrentHashMap is the modern default for shared maps.

    Why must I always unlock() in a finally block?

    Unlike synchronized, a ReentrantLock does not release automatically when a method exits or throws. If an exception fires between lock() and unlock() and you did not put unlock() in finally, the lock is never released and every other thread waiting on it hangs forever. The finally block guarantees release on every path out of the method.

    What is the difference between CountDownLatch and CyclicBarrier?

    A CountDownLatch is one-shot: once its count reaches zero it stays open and cannot be reset, so it is ideal for waiting until N startup tasks finish once. A CyclicBarrier is reusable: it releases all waiting threads when they all arrive, then automatically resets for the next phase, which is why it suits repeated multi-phase computations.

    What does it mean that ReentrantLock is reentrant?

    Reentrant means the thread that already holds the lock can acquire it again without deadlocking itself. The lock keeps a hold count that goes up on each lock() and down on each unlock(); the lock is only truly released when the count returns to zero. This lets a synchronized method safely call another method that locks the same object.

    🎉 Lesson Complete!

    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.

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service