0% found this document useful (0 votes)
2 views30 pages

Notes

The document provides a comprehensive overview of Java concurrency, covering key concepts such as ExecutorService, Callable, Future, and design principles like Interface Segregation and Open/Closed Principles. It includes practical applications in a banking system, illustrating how concurrency can be effectively managed with synchronized collections and thread-safe practices. Additionally, it discusses the differences between Runnable and Callable, and the importance of handling exceptions in concurrent programming.

Uploaded by

Abhinav
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views30 pages

Notes

The document provides a comprehensive overview of Java concurrency, covering key concepts such as ExecutorService, Callable, Future, and design principles like Interface Segregation and Open/Closed Principles. It includes practical applications in a banking system, illustrating how concurrency can be effectively managed with synchronized collections and thread-safe practices. Additionally, it discusses the differences between Runnable and Callable, and the importance of handling exceptions in concurrent programming.

Uploaded by

Abhinav
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Java Concurrency

Quick Reference Notes

March 17, 2026

Contents
1 System Architecture  What We Have Built 3
2 ExecutorService and AutoCloseable 5
3 Callable, Future, and Why Thread Cannot Run a Callable 6
3.1 Runnable vs Callable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3.2 Thread Only Accepts Runnable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3.3 ExecutorService Handles Both . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3.4 Future  the Receipt . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
3.5 Applied to Our Banking System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

4 Interface Segregation Principle (ISP) 9


4.1 The Problem  One Big Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
4.2 The Fix  Two Small Interfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
4.3 How It Wires Up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10

5 Open/Closed Principle (OCP) 11


5.1 The Problem  if/else Chains That Keep Growing . . . . . . . . . . . . . . . . . . . . 11
5.2 Fix 1  Enums with Abstract Methods . . . . . . . . . . . . . . . . . . . . . . . . . . 11
5.3 Understanding Enum Subclasses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
5.4 Fix 2  Strategy Pattern via Interface . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
5.5 Comparison  Enum vs Strategy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14

6 Synchronized Collections  Thread-Safe Storage 15


6.1 The Problem  Safe Objects in an Unsafe Container . . . . . . . . . . . . . . . . . . . 15
6.2 Three Levels of Solutions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
6.3 Why Can't I Just Lock It Myself ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
6.4 Comparison  Which One When? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
6.5 Common Pitfall  Check-Then-Act Race Conditions . . . . . . . . . . . . . . . . . . . 17
6.6 Applied to Our Banking System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Java Concurrency Notes 2

7 Synchronized Collections 19
7.1 The Problem  Safe Objects in an Unsafe Container . . . . . . . . . . . . . . . . . . . 19
7.2 Three Levels of Solutions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
7.3 Which One for Our Banking System? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
7.4 Comparison at a Glance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

8 CountDownLatch  Waiting for Everyone to Finish 22


8.1 What It Does . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
8.2 The Banking Scenario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
8.3 CountDownLatch vs join() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
8.4 Critical Pattern  countDown in nally . . . . . . . . . . . . . . . . . . . . . . . . . . 22
8.5 Applied to Our Banking System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

9 CountDownLatch  Waiting for Everyone to Finish 25


9.1 What It Does . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
9.2 CountDownLatch vs join() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
9.3 Applied to Our Banking System . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
9.4 Critical Pattern  countDown() in nally . . . . . . . . . . . . . . . . . . . . . . . . . 26
9.5 Why Two try Blocks?  Understanding Exception Flow . . . . . . . . . . . . . . . . . 26

10 CountDownLatch  Waiting for Everyone to Finish 28


10.1 What It Is . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
10.2 Banking Scenario  End-of-Day Report . . . . . . . . . . . . . . . . . . . . . . . . . . 28
10.3 CountDownLatch vs join() vs [Link]() . . . . . . . . . . . . . . . . . . . . . . . . . 28
10.4 Critical Pattern  countDown in nally . . . . . . . . . . . . . . . . . . . . . . . . . . 29
10.5 Why Two Try-Catch Levels? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Java Concurrency Notes 3

1 System Architecture  What We Have Built


The diagram below shows every class, interface, and thread in the banking system.

LEGEND Thread Interface Queue Model Executor

PRODUCER CONSUMER
Producer-Consumer Zone

Customer Teller Future⟨List⟩


Runnable Callable⟨List⟨String⟩⟩

uses uses

⟨⟨ interface ⟩⟩ ⟨⟨ interface ⟩⟩
Submittable Takeable

creates

implements
TransactionQueue implements
implements Submittable, Takeable

synchronized · wait/notifyAll

holds

Transaction
executor + account + amount

delegates targets

Account
⟨⟨ interface ⟩⟩
synchronized
TransactionExecutor deposit() / withdraw()

impl impl

DepositExecutor WithdrawalExecutor

Strategy Pattern

calls deposit()
calls withdraw()

Data Flow  How a Transaction Moves Through the System


[Link] Transaction
creates a with a specic TransactionExecutor (e.g.
DepositExecutor) and an Account.
2. Customer calls [Link](txn) via the Submittable interface. If the queue is full, the
customer thread wait()s.
3. Teller (a Callable) calls [Link]() via the Takeable interface. If the queue is empty,
the teller thread wait()s.

4. Teller calls [Link](), which delegates to the executor, which calls the synchronized
method on Account.

5. Teller returns a List<String> report via Future. Main thread calls [Link]() to retrieve
results.
Java Concurrency Notes 4

Design Principles Applied


ˆ ISP  Customer sees only Submittable, Teller sees only Takeable. Neither has access to
methods they don't need.

ˆ OCP  Adding a new transaction type means creating a new TransactionExecutor imple-
mentation. Existing code stays untouched.

ˆ SRP  Queue handles buering, Transaction holds data, Executors hold logic, Account holds
state.

ˆ Thread safety  Account uses synchronized methods. Queue uses wait/notify for coor-
dination.
Java Concurrency Notes 5

2 ExecutorService and AutoCloseable


ExecutorService does not implement AutoCloseable before Java 19. Try-with-resources won't work:

Will NOT compile before Java 19


1 // Compilation error on Java < 19
2 try ( ExecutorService pool = Executors . newCachedThreadPool () ) {
3 // ...
4 }

Fix: Use a nally block


Always shut down in finally to guarantee cleanup:

Compatible with all Java versions


1 ExecutorService pool = Executors . newCachedThreadPool () ;
2 try {
3 pool . submit (() -> { /* task */ }) ;
4 Future <? > future = pool . submit (() -> { /* task */ }) ;
5 future . get () ; // blocks until done
6 } catch ( InterruptedException e ) {
7 Thread . currentThread () . interrupt () ;
8 } catch ( ExecutionException e ) {
9 Throwable cause = e . getCause () ;
10 cause . printStackTrace () ;
11 } finally {
12 pool . shutdown () ;
13 }

Key Exceptions to Handle


ˆ InterruptedException  thrown by [Link]() or [Link]() if the waiting thread
is interrupted. Always restore the ag with [Link]().interrupt().
ˆ ExecutionException  wraps any exception thrown inside a submitted task. Use getCause()
to unwrap.
Java Concurrency Notes 6

3 Callable, Future, and Why Thread Cannot Run a Callable


3.1 Runnable vs Callable

Runnable and Callable both represent tasks that run in another thread. The key dierence is the
return type:

Runnable  re and forget Callable  returns a result


public interface Runnable { public interface Callable <V > {
void run () ; V call () throws Exception ;
// returns nothing // returns a value of type V
// cannot throw checked exceptions // can throw checked exceptions
} }

Use when you don't care about the result. Do this and I'll Use when you need a result. Do this and tell me what hap-

move on. pened.

3.2 Thread Only Accepts Runnable

The Thread class constructor only takes a Runnable:


Thread works with Runnable
1 Runnable task = () -> System . out . println ( " I 'm running " ) ;
2 Thread t = new Thread ( task ) ; // works fine
3 t . start () ;

If you change a class from Runnable to Callable, Thread no longer accepts it:

Thread CANNOT work with Callable


1 Callable < List < String > > teller = new Teller ( " Pinky " , queue , 10) ;
2 Thread t = new Thread ( teller ) ; // COMPILE ERROR !
3 // Cannot resolve constructor ' Thread ( Teller ) '

Why does this happen?


Thread was designed in Java 1.0. Callable was added in Java 5. The Thread class was never
updated to accept Callable because by then ExecutorService existed as the proper way to run
tasks.

Think of it this way: Thread is a manual screwdriver. It does one simple thing  run a Runnable.
ExecutorService is a power drill  it handles both Runnable and Callable, manages thread
pools, and gives you Futures back.

3.3 ExecutorService Handles Both

ExecutorService accepts both Runnable and Callable. When you submit a Callable, you get a
Future back  a receipt that you redeem later for the result:
ExecutorService with Runnable and Callable
1 ExecutorService pool = Executors . newFixedThreadPool (5) ;
2
3 // Runnable -- fire and forget
Java Concurrency Notes 7

4 pool . submit ( customer ) ; // returns Future <? > ( ignored )


5
6 // Callable -- get a result back
7 Future < List < String > > report = pool . submit ( teller ) ;
8
9 // Later : block until the result is ready
10 List < String > results = report . get () ;
11 results . forEach ( System . out :: println ) ;

3.4 Future  the Receipt

A Future<V> is a placeholder for a result that doesn't exist yet. Like dropping o clothes at a dry
cleaner and getting a ticket  you come back later with the ticket to pick up your clothes.

Future Methods
ˆ get()  blocks until the result is ready, then returns it. If the task threw an exception, get()
wraps it in an ExecutionException.
ˆ get(timeout, unit)  same, but gives up after the timeout and throws TimeoutException.
ˆ isDone()  checks if the result is ready without blocking.

ˆ cancel(mayInterrupt)  attempts to cancel the task.

3.5 Applied to Our Banking System

The Teller was originally a Runnable  it processed transactions but reported nothing. By changing
it to Callable<List<String, each teller now returns a full report of every transaction it processed:
Teller as Callable
1 public class Teller implements Callable < List < String > > {
2 private Takeable queue ;
3 private int transactionsToProcess ;
4
5 public List < String > call () throws Exception {
6 List < String > res = new ArrayList < >() ;
7 for ( int i = 0; i < transactionsToProcess ; i ++) {
8 Transaction t = queue . take () ;
9 try {
10 t . execute () ;
11 res . add ( " SUCCESS : " + t . getAmount () ) ;
12 } catch ( IllegalArgumentException e ) {
13 res . add ( " FAILED : " + e . getMessage () ) ;
14 }
15 }
16 return res ; // returned via Future
17 }
18 }

Getting the report in main


1 try ( ExecutorService pool = Executors . newFixedThreadPool (5) ) {
2 pool . submit ( customer1 ) ;
Java Concurrency Notes 8

3 pool . submit ( customer2 );


4
5 Future < List < String > > report1 = pool . submit ( teller1 ) ;
6 Future < List < String > > report2 = pool . submit ( teller2 ) ;
7
8 // After pool shuts down , retrieve results
9 report1 . get () . forEach ( System . out :: println ) ;
10 report2 . get () . forEach ( System . out :: println ) ;
11 }

Exception Handling with Future


If [Link]() throws InterruptedException inside call(), it propagates up since call()
declares throws Exception. The Future wraps it in ExecutionException:
try {
List < String > results = future . get () ;
} catch ( ExecutionException e) {
// e. getCause () is the original InterruptedException
} catch ( InterruptedException e) {
Thread . currentThread () . interrupt () ;
}
Java Concurrency Notes 9

4 Interface Segregation Principle (ISP)


4.1 The Problem  One Big Interface

A TransactionQueue has submit() and take(). But:

ˆ A Customer only needs submit(). They should never pick up someone else's transaction.

ˆ A Teller
only needs take(). They should never submit on behalf of customers.

If both get the full object, nothing prevents misuse.

ISP says
No class should be forced to depend on methods it does not use.

4.2 The Fix  Two Small Interfaces

Two focused interfaces


1 public interface Submittable {
2 void submit ( Transaction t ) throws InterruptedException ;
3 }
4
5 public interface Takeable {
6 Transaction take () throws InterruptedException ;
7 }

The queue implements both :

TransactionQueue implements both


1 public class TransactionQueue implements Submittable , Takeable {
2 private int maxSz ;
3 private Deque < Transaction > queue = new ArrayDeque < >() ;
4
5 public synchronized void submit ( Transaction t )
6 throws InterruptedException {
7 while ( queue . size () == maxSz ) { wait () ; }
8 queue . addLast ( t ) ;
9 notifyAll () ;
10 }
11
12 public synchronized Transaction take ()
13 throws InterruptedException {
14 while ( queue . size () == 0) { wait () ; }
15 Transaction t = queue . removeFirst () ;
16 notifyAll () ;
17 return t ;
18 }
19 }
Java Concurrency Notes 10

4.3 How It Wires Up

Customer sees only Submittable


1 public class Customer implements Runnable {
2 private Submittable queue ; // can ONLY call submit ()
3
4 public void run () {
5 queue . submit ( t ) ; // allowed
6 // queue . take () ; // compile error !
7 }
8 }

Teller sees only Takeable


1 public class Teller implements Runnable {
2 private Takeable queue ; // can ONLY call take ()
3
4 public void run () {
5 Transaction t = queue . take () ; // allowed
6 // queue . submit (...) ; // compile error !
7 }
8 }

One object, two views:

Wiring in main
1 TransactionQueue queue = new TransactionQueue (5) ;
2 Customer alice = new Customer ( " Alice " , account , queue ) ;
3 Teller preeti = new Teller ( " Preeti " , queue ) ;

Why This Matters for Concurrency


If a teller thread accidentally calls submit(), you get a subtle deadlock that shows up once in a
thousand runs. ISP turns that runtime bug into a compile-time error.
Rule of thumb: Read-only? Read-only interface. Write-only? Write-only interface. Narrower
view = fewer bugs.
Java Concurrency Notes 11

5 Open/Closed Principle (OCP)


5.1 The Problem  if/else Chains That Keep Growing

OCP violation  grows with every new type


1 public void execute () {
2 if ( task_type == TransactionType . DEPOSIT ) {
3 account . deposit ( amount ) ;
4 } else if ( task_type == TransactionType . WITHDRAWAL ) {
5 account . withdraw ( amount ) ;
6 }
7 // TRANSFER ? Another else - if . LOAN ? Another .
8 }

Every new type forces you to edit this method. You risk breaking existing logic.

OCP says
Open for extension (easy to add) but closed for modication (don't touch working code).

5.2 Fix 1  Enums with Abstract Methods

Smart enum  each constant has behavior


1 public enum TransactionType {
2 DEPOSIT {
3 public void execute ( Account account , double amount ) {
4 account . deposit ( amount ) ;
5 }
6 },
7 WITHDRAWAL {
8 public void execute ( Account account , double amount ) {
9 account . withdraw ( amount ) ;
10 }
11 };
12 public abstract void execute ( Account account , double amount ) ;
13 }

Transaction delegates in one line:

No branching needed
1 public void execute () {
2 taskType . execute ( account , amount ) ;
3 }

5.3 Understanding Enum Subclasses

Enum constants with behavior are anonymous subclasses . Same idea, two syntaxes:
Java Concurrency Notes 12

Traditional Inheritance
class Animal {
public String speak () { Enum Version
return " ... ";
} enum Animal {
} DOG {
public String speak () {
class Dog extends Animal { return " Woof " ;
public String speak () { }
return " Woof " ; },
} CAT {
} public String speak () {
return " Meow " ;
class Cat extends Animal { }
public String speak () { };
return " Meow " ;
} public abstract String speak () ;
} }

3 les. Each subclass overrides speak(). 1 le. Each constant is a tiny subclass.

How it works
{ } class Dog extends Animal { }.
polymorphism
The after each constant dene a class body  same as
When you call [Link](), Java calls DOG's version. This is .

The abstract keyword is the contract: every constant must provide speak(). Miss one →
compiler error, not a 3 AM production bug.

5.4 Fix 2  Strategy Pattern via Interface

Each type becomes a separate class . No existing le is ever touched.

Step 1: Strategy interface


1 public interface TransactionExecutor {
2 void execute ( Account account , double amount ) ;
3 }

Step 2: One class per type


1 public class DepositExecutor implements TransactionExecutor {
2 public void execute ( Account account , double amount ) {
3 account . deposit ( amount ) ;
4 }
5 }
6
7 public class WithdrawalExecutor implements TransactionExecutor {
8 public void execute ( Account account , double amount ) {
9 account . withdraw ( amount ) ;
10 }
11 }
Java Concurrency Notes 13

Step 3: Transaction holds the strategy


1 public class Transaction {
2 private TransactionExecutor executor ;
3 private Account account ;
4 private double amount ;
5
6 Transaction ( TransactionExecutor executor ,
7 Account account , double amount ) {
8 this . executor = executor ;
9 this . account = account ;
10 this . amount = amount ;
11 }
12
13 public void execute () {
14 executor . execute ( account , amount ) ;
15 }
16 }

Step 4: Caller picks the strategy


1 TransactionExecutor ex ;
2 if ( i % 2 == 0) {
3 ex = new DepositExecutor () ;
4 } else {
5 ex = new WithdrawalExecutor () ;
6 }
7 queue . submit ( new Transaction ( ex , account , amount )) ;

Adding TRANSFER = create [Link]. Nothing else changes.


Java Concurrency Notes 14

5.5 Comparison  Enum vs Strategy

Enum + Abstract Method Strategy via Interface


WHERE BEHAVIOR LIVES

All behavior in one le . Each constant has Each type in its own .java le. Like sepa-
its own code block, like chapters in a book. rate recipe cards in a binder.

ADDING A NEW TYPE

No existing le
is opened or edited.
Add a constant to the existing le. Old con- Create a brand new le.
stants untouched, but the le is edited.

NUMBER OF FILES

1 le for all types. Compact, easy to scan.


1 le per type. More les, each small and
focused.

COMPILE-TIME SAFETY

✓ abstract forces every constant to imple- ✓ Interface forces every class to implement.
ment. Forget → compiler error. Same safety level.

RUNTIME FLEXIBILITY

× Types xed at compile time. No runtime ✓ New executors can be injected at runtime
additions. (cong, plugins).

BEST WHEN. . .

Small, stable set of types (38) that rarely Many types, frequent changes, or external
change. code needs to add types.

REAL-WORLD ANALOGY

A menu painted on the wall. Adding a


A recipe binder. Adding a dish = insert-
dish = editing the wall. Each recipe is self-
contained. ing a card. Old cards never touched.

Bottom Line
Both kill the if/else chain. Enum is simpler for small projects. Strategy scales to large systems.
Pick based on how many types you expect and how often they change.
Java Concurrency Notes 15

6 Synchronized Collections  Thread-Safe Storage


6.1 The Problem  Safe Objects in an Unsafe Container

Think of a regular HashMap as a notebook lying on an open table . If one person is writing a name
on page 5 and another person is simultaneously writing a name on the same page, they scribble over
each other. One name gets lost.
That's exactly what happens with threads. Two threads calling [Link]() at the same time can
corrupt each other's work.

Now, our Account class is safe  we added synchronized, which is like giving each account its

But the map that stores all accounts has no lock.


own padlock. Only one person can touch that account at a time.
Imagine this at a real bank:

ˆ Thread A (front desk): I'm opening a new account for Ramesh → puts Ramesh into the map

ˆ Thread B (front desk): I'm opening a new account for Suresh → puts Suresh into the map

ˆ Thread C →
(teller): I need to look up Amit's account reads from the map

All three hit the map at the same instant. The individual accounts are safe, but the registry itself
isn't.

The Analogy
It's like having locks on every locker but no security at the building entrance . People
crash into each other in the hallway. The lockers (accounts) are safe, but the building (map) isn't.

6.2 Three Levels of Solutions

Java gives you three ways to make a collection thread-safe, each with dierent trade-os:

Level 1: [Link]()
Wraps a regular HashMap single giant lock
and adds a around every method call. Simple but
slow  only one thread can do anything at a time, even just reading.

Analogy: A bank with one door. Everyone  customers, tellers, cleaners  must queue at
this one door. Even someone who just wants to peek at the board has to wait.

Map < String , Account > accounts =


Collections . synchronizedMap ( new HashMap < >() );

Level 2: ConcurrentHashMap (recommended)


Built from the ground up for threads. Multiple threads can read simultaneously , and writes
only lock a small segment of the map, not the whole thing. Much faster.

Analogy: A bank with many doors, one per department. The person checking savings doesn't
block the person checking loans. Writers only lock their own department's door.

Map < String , Account > accounts = new ConcurrentHashMap < >() ;
Java Concurrency Notes 16

Level 3: CopyOnWriteArrayList
Every time youwrite , it copies the entire list. Reads are lock-free and instant. Great when you
read a lot and write rarely.

Analogy: A notice board behind glass. To add a notice, you take down the entire board,
photocopy it with the new notice added, and put the copy back up. Expensive to write, but
readers never wait and never see a half-updated board.

List < String > auditLog = new CopyOnWriteArrayList < >() ;

6.3 Why Can't I Just Lock It Myself ?

You can. And it works:


Manual locking  correct but slow
1 Map < String , Account > accounts = new HashMap < >() ;
2
3 // Anywhere you touch the map :
4 synchronized ( accounts ) {
5 accounts . put ( id , new Account ( id , name , balance ) ) ;
6 }
7
8 synchronized ( accounts ) {
9 Account a = accounts . get ( id ) ;
10 }

This is correct. No data will be lost. But think about what happens at a real bank:

Manual lock / synchronizedMap


One lock = one door. ConcurrentHashMap
Teller-1 looks up Ramesh's account. Teller-
Many locks = many doors.
wait
2 wants to look up Suresh  completely The map is split into segments internally.

no waiting
unrelated. But Teller-2 must because Teller-1 locks only Ramesh's segment. Teller-
they're both using the same lock. 2 hits Suresh's segment  .
Reads don't block at all.
Everyone queues at one door, even if they're
going to dierent rooms. Each department has its own door. Unre-
lated work never interferes.
With 3 accounts: no problem.

With 10,000 accounts and 100 threads: bottleneck. Java already built the smart locking for you. Use it.

The Interview Answer


Yes, manual locking works and is correct. But it creates a single point of contention  every
thread competes for one lock, even for unrelated keys. ConcurrentHashMap uses segment-level
locking so unrelated operations proceed in parallel. For read-heavy workloads, reads don't block
at all. It's the same correctness with much better throughput.
Java Concurrency Notes 17

6.4 Comparison  Which One When?

synchronizedMap ConcurrentHashMap CopyOnWriteArrayList


Speed: Slow Speed: Fast Speed: Reads fast, writes slow

Lock: Entire map Lock: Per segment Lock: Copies on write

Reads block: × Yes Reads block: ✓ No Reads block: ✓ No

Best for: Quick x for existing Best for: Account registry Best for: Audit log (rarely writ-

HashMap code (many reads + writes) ten, often read)

6.5 Common Pitfall  Check-Then-Act Race Conditions

two separate calls


one atomic call
Even with a thread-safe map, you can introduce bugs if you use where you should
use .

× Broken: containsKey then get ✓ Correct: single get


// RIGHT -- one atomic call
// WRONG -- race condition ! Account a = map . get ( id );
if ( map . containsKey ( id ) ) { if (a == null ) {
// Another thread could REMOVE throw new IllegalArgumentException (
// the account RIGHT HERE " Not found : " + id );
return map . get ( id ) ; // null ! }
} return a;

Two calls = a gap. Another thread can act between them. One call. No gap. No race condition.

The same principle applies when adding to the map:

✓ Correct: putIfAbsent
× Broken: containsKey then put // RIGHT -- atomic check - and - insert
Account existing =
// WRONG -- two threads could both map . putIfAbsent (id , account );
// pass the check and both put ! if ( existing != null ) {
if (! map . containsKey ( id )) { throw new IllegalArgumentException (
map . put (id , new Account (...) ); " Already exists : " + id );
} }

Two threads check at the same time, both see not found, One atomic operation. Check and insert happen together 

both insert  one overwrites the other. no gap.

The Rule  Never Check Then Act Separately


Any time you see this pattern with a shared collection:

1. Check something (containsKey, isEmpty, size)


2. Act based on the result (get, put, remove)
. . . ask yourself: Can another thread change the answer between step 1 and step 2? If
yes, nd a single atomic method that does both: get (returns null if absent), putIfAbsent,
computeIfAbsent, replace.
ConcurrentHashMap provides all of these as atomic operations. synchronizedMap does not 
putIfAbsent on a synchronizedMap is still two internal steps.
Java Concurrency Notes 18

6.6 Applied to Our Banking System

Our bank needs a central account registry. Multiple threads open accounts while others process trans-
actions. ConcurrentHashMap is the right t:

Bank class with ConcurrentHashMap


1 public class Bank {
2 private final String bankName ;
3 // Thread - safe : many threads can read / write simultaneously
4 private final Map < String , Account > accounts
5 = new ConcurrentHashMap < >() ;
6
7 public Account openAccount ( String id , String name ,
8 double initialDeposit ) {
9 Account account = new Account ( id , name , initialDeposit ) ;
10 accounts . put ( id , account ) ;
11 return account ;
12 }
13
14 public Account getAccount ( String id ) {
15 Account account = accounts . get ( id ) ;
16 if ( account == null )
17 throw new IllegalArgumentException (
18 " Account not found : " + id ) ;
19 return account ;
20 }
21
22 public void printAllBalances () {
23 // Safe to iterate -- no ConcurrentModificationException
24 accounts . values () . forEach ( System . out :: println ) ;
25 }
26 }

Why not synchronizedMap here?


With synchronizedMap, when a teller looks up an account (get), it locks the entire map. No
other teller can even read a dierent account until the rst one is done. With ConcurrentHashMap,
all tellers read simultaneously  only writes to the same segment block each other. In a bank
with thousands of accounts and many tellers, this dierence is massive.

When to Use What  Rule of Thumb


ˆ map
Need a thread-safe with frequent reads and writes? → ConcurrentHashMap
ˆ list
Need a thread-safe that is read often but written rarely? → CopyOnWriteArrayList
ˆ Have an existing HashMap and just need a quick thread-safe wrapper? →
[Link]()
ˆ Building a producer-consumer queue? → BlockingQueue (Phase 9)
Java Concurrency Notes 19

7 Synchronized Collections
7.1 The Problem  Safe Objects in an Unsafe Container

Think of a regular HashMap as a notebook lying on a table . If one person is writing a name on
page 5 and another person simultaneously writes a name on the same page, they scribble over each
other. One name gets lost.

Your Account class is safe  you added synchronized, which is like giving each account its own
lock. Only one person can touch that account at a time.

But the map that stores all accounts has no lock. Imagine this at a real bank:

ˆ Thread A (front desk): Opening a new account for Ramesh → puts Ramesh into the map

ˆ Thread B (front desk): Opening a new account for Suresh → puts Suresh into the map

ˆ Thread C →
(teller): Looking up Amit's account reads from the map

All three hit the map at the same instant. The individual accounts are safe, but the registry itself
is not. It's like having locks on every locker but no security at the building entrance  people crash
into each other in the hallway.

The Core Issue


synchronized Account data inside
collection that holds
on your methods protects the each account. But nothing
protects the the accounts. You need thread safety at both levels.

7.2 Three Levels of Solutions

Java gives you three ways to make a collection thread-safe, each with a dierent trade-o:

Level 1: [Link]()
Wraps a regular HashMap and adds a single lock around every method call.

Map < String , Account > accounts =


Collections . synchronizedMap ( new HashMap < >() );

Analogy: A single security guard at the building entrance. Only one person can enter or leave
at a time. Everyone else waits in line  even people just reading the directory board.

✓ Simple, one-line x × Slow  only one thread can do anything at a time

× Iteration still needs manual synchronization:

synchronized ( accounts ) {
for ( var entry : accounts . entrySet () ) { ... }
}
Java Concurrency Notes 20

Level 2: ConcurrentHashMap (recommended)


Built from the ground up for threads. Multiple threads can read simultaneously , and writes
only lock a small segment of the map, not the whole thing.

Map < String , Account > accounts = new ConcurrentHashMap < >() ;

Analogy: The building has multiple entrances , one per oor. People going to dierent oors
don't block each other. Only people going to the same oor wait for each other.

✓ Fast  readers never block each other ✓ No manual sync for iteration
✓ Writers only lock a small segment ✓ The default choice for concurrent maps

Level 3: CopyOnWriteArrayList
Every time you write , it copies the entire list. Reads are lock-free and instant. Great when you
read a lot and write rarely.

List < String > auditLog = new CopyOnWriteArrayList < >() ;

Analogy: printed notice board


A . Everyone can read it freely anytime. But to change
anything, the manager takes the board down, makes a whole new copy with the change, and
puts the new one up.

✓ Reads are instant and lock-free × Writes are expensive (full copy)
Best for: audit logs, cong lists, observer lists  written once, read many times.

7.3 Which One for Our Banking System?

Choosing the Right Collection


ˆ Account registry (accountId → Account): Use ConcurrentHashMap. Accounts are looked
up constantly (every transaction), and new accounts may be opened while transactions are
processing. Reads and writes happen all the time.

ˆ Transaction audit log : Use CopyOnWriteArrayList. Written once per transaction, but read
by many threads for reporting and end-of-day reconciliation.

7.4 Comparison at a Glance

synchronizedMap ConcurrentHashMap CopyOnWriteArrayList

HOW IT LOCKS

Segment-level locking.
One big lock on the entire Readers never blocked.
map. Only 1 thread at a Writers lock only their No lock for reads. Writes

time. bucket. create a full copy of the list.

PERFORMANCE

× Slowest. Every operation ✓ Fast reads and writes. ✓ Fastest reads. × Slowest
waits for the single lock. Best all-around performer. writes (full copy each time).
Java Concurrency Notes 21

BEST FOR

Quick retrot of existing Account registries, caches, Audit logs, cong, observer

HashMap code. Low thread any map with frequent lists. Written rarely, read

count. reads and writes. constantly.

ANALOGY

Multiple entrances, one per Printed notice board. Read


One security guard at the oor. People on dierent freely, but changes require
door. Everyone queues up. oors don't wait. reprinting the whole board.

Bottom Line
Use ConcurrentHashMap as your default for concurrent maps. Use CopyOnWriteArrayList only
for read-heavy, write-rare lists. Use synchronizedMap only as a quick temporary x.
Java Concurrency Notes 22

8 CountDownLatch  Waiting for Everyone to Finish


8.1 What It Does

A CountDownLatch is a one-time gate. You set a number, threads count it down, and any thread
waiting on it wakes up when it hits zero.

Three Operations  That's It


[Link] new CountDownLatch(3)
with a count:  counter starts at 3

[Link] down [Link]()


:  decrements by 1 (3→2→1→0)

[Link] [Link]()
:  blocks until counter hits 0

The counter only goes down, never up. Once zero, every thread waiting on it wakes up. You can't
reset it  it's a one-time gate.

8.2 The Banking Scenario

after all tellers have nished


Every day at closing time, the bank generates a daily report. But the report can only be generated
. You don't know which teller nishes rst  you just wait until all
are done.

Meeting Room Analogy


We can't start the meeting until all 5 people arrive. Each person who walks in decrements the
counter. Once it hits zero, the meeting begins. If someone gets stuck in trac (crashes), and
never arrives  the meeting never starts. That's why the finally block matters.

8.3 CountDownLatch vs join()

join()  waits for a specic thread CountDownLatch  waits for a count


CountDownLatch latch =
Thread t1 = new Thread ( task ); new CountDownLatch (2) ;
t1 . start () ; latch . await () ; // wait for count =0
t1 . join () ; // wait for THIS thread // any thread can call countDown ()

Need a direct reference to the Thread object. Doesn't work Waits for a number of events, not specic threads. Works

with ExecutorService. with ExecutorService.

8.4 Critical Pattern  countDown in nally

If a teller crashes before calling countDown(), the latch never reaches zero. await() hangs forever.
The entire bank freezes because of one failed teller.
Java Concurrency Notes 23

✓ Correct: countDown in nally


× Broken: countDown at the end public List < String > call ()
throws Exception {
List < String > res = new ArrayList < >() ;
public List < String > call () try {
throws Exception { for (...) {
List < String > res = new ArrayList < >() ; Transaction t = queue . take () ;
for (...) { t. execute () ;
Transaction t = queue . take () ; res . add (" SUCCESS " );
t. execute () ; // CRASHES HERE ? }
res . add (" SUCCESS " ); } finally {
} // ALWAYS runs , crash or not
// Never reached if exception above ! latch . countDown () ;
latch . countDown () ; }
return res ; return res ;
} }

If any line throws before countDown(), the latch stays at 1 finally guarantees countDown() runs even if the teller

forever. await() never returns. crashes. The bank never freezes.

The Rule  Always countDown in nally


Any time you use CountDownLatch,
countDown() in a finally block. This is the same
put
principle as closing a le or a database connection in finally  cleanup must happen regardless
of success or failure.

Interview phrasing: I put countDown in nally to guarantee the latch decrements even if the
task throws an exception, preventing the awaiting thread from hanging indenitely.

8.5 Applied to Our Banking System

Teller with CountDownLatch


1 public class Teller implements Callable < List < String > > {
2 private Takeable queue ;
3 private int transactionsToProcess ;
4 private CountDownLatch latch ;
5
6 Teller ( String name , Takeable queue ,
7 int transactionsToProcess , CountDownLatch latch ) {
8 this . queue = queue ;
9 this . transactionsToProcess = transactionsToProcess ;
10 this . latch = latch ;
11 }
12
13 public List < String > call () throws Exception {
14 List < String > res = new ArrayList < >() ;
15 try {
16 for ( int i = 0; i < transactionsToProcess ; i ++) {
17 Transaction t = queue . take () ;
18 try {
19 t . execute () ;
20 res . add ( " SUCCESS : " + t . getAmount () ) ;
21 } catch ( IllegalArgumentException e ) {
22 res . add ( " FAILED : " + e . getMessage () ) ;
23 }
24 }
Java Concurrency Notes 24

25 } finally {
26 latch . countDown () ; // always runs
27 }
28 return res ;
29 }
30 }

Main method with latch


1 CountDownLatch latch = new CountDownLatch (2) ; // 2 tellers
2
3 Teller tel1 = new Teller ( " Pinky " , queue , 8 , latch );
4 Teller tel2 = new Teller ( " Kishan " , queue , 4 , latch ) ;
5
6 Future < List < String > > fut1 = pool . submit ( tel1 ) ;
7 Future < List < String > > fut2 = pool . submit ( tel2 ) ;
8
9 latch . await () ; // blocks until both tellers finish
10
11 System . out . println ( " === End of Day Report === " ) ;
12 bank . printAllBalances () ;
13
14 // Now safe to collect individual reports
15 fut1 . get () . forEach ( System . out :: println ) ;
16 fut2 . get () . forEach ( System . out :: println ) ;
Java Concurrency Notes 25

9 CountDownLatch  Waiting for Everyone to Finish


9.1 What It Does

A CountDownLatch is a counter that threads can wait on. Three operations, that's it:

1. Create new CountDownLatch(3)


with a number:  counter starts at 3

2. Count down [Link]()


:  decrements by 1 (3→2→1→0)

3. Wait [Link]()
:  blocks until counter hits 0

The counter only goes down, never up. Once it hits zero, await() returns and every thread waiting
on it wakes up. You can't reset it  it's a one-time gate.

Analogy
A meeting room: We can't start the meeting until all 5 people arrive. Each person who walks
in decrements the counter. Once it hits zero, the meeting begins.

9.2 CountDownLatch vs join()

With join(), you wait for a specic thread . With CountDownLatch, you wait for a count of events ,
regardless of which threads trigger them. And it works with ExecutorService where you don't have
direct access to thread objects.

How it ows
1 Main thread : latch = new CountDownLatch (2) ;
2 latch . await () ; // blocks here ...
3
4 Teller 1: // processes transactions ... done !
5 latch . countDown () ; // counter : 2 -> 1
6 // main still blocked
7
8 Teller 2: // processes transactions ... done !
9 latch . countDown () ; // counter : 1 -> 0
10 // main wakes up !
11
12 Main thread : // " Both done ! Printing daily report ..."

9.3 Applied to Our Banking System

At end of day, the bank generates a report  but only after all tellers nish processing. The latch
ensures the main thread waits:
Main method with CountDownLatch
1 CountDownLatch latch = new CountDownLatch (2) ; // 2 tellers
2
3 Teller tel_1 = new Teller ( " Pinky " , queue , 8 , latch );
4 Teller tel_2 = new Teller ( " Kishan " , queue , 4 , latch ) ;
5
6 Future < List < String > > fut_1 = ex . submit ( tel_1 ) ;
7 Future < List < String > > fut_2 = ex . submit ( tel_2 ) ;
8
9 latch . await () ; // blocks until both tellers call countDown ()
Java Concurrency Notes 26

10
11 // SAFE : both tellers are done
12 System . out . println ( " === End of Day Report === " ) ;
13 bank . printAllBalances () ;
14
15 // Collect individual reports
16 fut_1 . get () . forEach ( System . out :: println ) ;
17 fut_2 . get () . forEach ( System . out :: println ) ;

9.4 Critical Pattern  countDown() in nally

What happens if a teller crashes halfway through? If countDown() never runs, the latch never reaches
zero, and await() hangs forever. The entire program freezes because of one failed teller.

The x: put countDown() in a finally block so it always runs:

[Link]() with nally


1 public List < String > call () throws Exception {
2 List < String > res = new ArrayList < >() ;
3 try {
4 for ( int i = 0; i < transactionsToProcess ; i ++) {
5 Transaction t = queue . take () ;
6 try {
7 t . execute () ;
8 res . add ( " SUCCESS : ... " ) ;
9 } catch ( IllegalArgumentException e ) {
10 res . add ( " FAILED : ... " ) ;
11 }
12 }
13 } finally {
14 latch . countDown () ; // ALWAYS runs , even if something crashes
15 }
16 return res ;
17 }

9.5 Why Two try Blocks?  Understanding Exception Flow

The nested try-catch structure can look confusing. Here's exactly what each layer does:

Inner try-catch Outer try-nally


Handles expected failures. Guarantees cleanup no matter what.

IllegalArgumentException from a bad transaction InterruptedException from [Link](), a


amount. Caught, logged as FAILED, loop contin- NullPointerException you didn't anticipate, any
ues to the next transaction. other runtime error  these y past the inner catch
and would kill call().
Once caught, the exception is consumed. It does

not propagate to the outer block. finally ensures countDown() runs regardless.
Java Concurrency Notes 27

The Rule
Critical cleanup code always goes in finally:
ˆ [Link]()  so other threads aren't stuck waiting

ˆ [Link]()  so threads don't leak

ˆ [Link]()  so DB connections don't leak

ˆ [Link]()  so les don't leak

Same pattern, same reason: This must happen, no matter what.


Java Concurrency Notes 28

10 CountDownLatch  Waiting for Everyone to Finish


10.1 What It Is

A CountDownLatch is a one-time counter that threads can wait on. Three operations, that's it:

1. Create new CountDownLatch(3)


with a number:  counter starts at 3

2. Count down [Link]()


:  decrements by 1 (3→2→1→0)

3. Wait [Link]()
:  blocks until counter hits 0

The counter only goes down, never up. Once it hits zero, every thread waiting on it wakes up. You
cannot reset it  it's a one-time gate.

Analogy
A meeting room: We can't start the meeting until all 5 people arrive. Each person who walks
in decrements the counter. Once it hits zero, the meeting begins.

10.2 Banking Scenario  End-of-Day Report

The bank needs to print a daily report, but only after all tellers have nished processing . You
don't know which teller nishes rst  you just need to wait until all of them are done.

How CountDownLatch ows in our system


1 // Main thread : " Wait until both tellers finish "
2 CountDownLatch latch = new CountDownLatch (2) ;
3
4 // Pass latch to tellers
5 Teller tel1 = new Teller ( " Pinky " , queue , 8 , latch );
6 Teller tel2 = new Teller ( " Kishan " , queue , 4 , latch ) ;
7
8 // Submit to executor
9 Future < List < String > > fut1 = pool . submit ( tel1 ) ;
10 Future < List < String > > fut2 = pool . submit ( tel2 ) ;
11
12 // Block until both tellers call countDown ()
13 latch . await () ;
14
15 // NOW it 's safe to print the report
16 System . out . println ( " === End of Day Report === " ) ;
17 bank . printAllBalances () ;

10.3 CountDownLatch vs join() vs [Link]()

ˆ [Link]()  waits for a specic thread. Doesn't work with ExecutorService because you
don't own the thread objects.

ˆ [Link]()  waits for a specic task's result. Blocks on one task at a time.

ˆ [Link]()  waits for a count of events. Doesn't care which threads trigger them. One call
waits for all.
Java Concurrency Notes 29

10.4 Critical Pattern  countDown in nally

What if a teller crashes before calling countDown()? The latch never reaches zero. await()
hangs forever. The entire program freezes.

✓ Safe: countDown in nally


public List < String > call ()
throws Exception {
List < String > res = new ArrayList < >() ;
try {
for ( int i = 0; i < n; i ++) {
× Dangerous: countDown at the end Transaction t = queue . take () ;
try {
t. execute () ;
public List < String > call () res . add (" SUCCESS " );
throws Exception { } catch (
List < String > res = new ArrayList < >() ; IllegalArgumentException
for ( int i = 0; i < n ; i ++) { e) {
Transaction t = queue . take () ; res . add (" FAILED ") ;
t. execute () ; // CRASH here ? }
res . add (" SUCCESS " ); }
} } finally {
// Never reached if crash above ! latch . countDown () ; // ALWAYS runs
latch . countDown () ; }
return res ; return res ;
} }

If execute() throws an uncaught exception, countDown() is No matter what happens  normal nish, caught exception,

skipped. await() hangs forever. uncaught crash  countDown() always executes.

10.5 Why Two Try-Catch Levels?

This nesting can look confusing, so here's what each layer does:

Inner try-catch  handles expected failures


try {
t. execute () ;
res . add (" SUCCESS " );
} catch ( IllegalArgumentException e) {
res . add (" FAILED ") ; // bad amount , insufficient funds , etc .
}

This catches known business errors. The loop continues to the next transaction. Nothing escapes.

Outer try-nally  guarantees cleanup


try {
// ... the entire loop ...
} finally {
latch . countDown () ; // runs no matter what
}

This catches everything the inner block doesn't : InterruptedException from [Link](),
unexpected NullPointerException, any other runtime crash. It doesn't handle them  it just
ensures countDown() runs before the exception propagates further.
Java Concurrency Notes 30

The Rule
Any time you have a resource or counter that must be released/decremented regardless of success
or failure, put it in finally. This applies to: countDown(), [Link](), [Link](),
[Link]().
Same reason you put [Link]() in a finally block in Section 2.

You might also like