Notes
Notes
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
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
PRODUCER CONSUMER
Producer-Consumer Zone
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()
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
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
Runnable and Callable both represent tasks that run in another thread. The key dierence is the
return type:
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-
If you change a class from Runnable to Callable, Thread no longer accepts it:
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.
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
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.
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 }
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.
ISP says
No class should be forced to depend on methods it does not use.
Wiring in main
1 TransactionQueue queue = new TransactionQueue (5) ;
2 Customer alice = new Customer ( " Alice " , account , queue ) ;
3 Teller preeti = new Teller ( " Preeti " , queue ) ;
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).
No branching needed
1 public void execute () {
2 taskType . execute ( account , amount ) ;
3 }
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.
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.
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
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
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
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
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.
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.
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.
This is correct. No data will be lost. But think about what happens at a real bank:
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.
Best for: Quick x for existing Best for: Account registry Best for: Audit log (rarely writ-
Two calls = a gap. Another thread can act between them. One call. No gap. No race condition.
✓ 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
Our bank needs a central account registry. Multiple threads open accounts while others process trans-
actions. ConcurrentHashMap is the right t:
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.
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.
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.
synchronized ( accounts ) {
for ( var entry : accounts . entrySet () ) { ... }
}
Java Concurrency Notes 20
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.
✓ Reads are instant and lock-free × Writes are expensive (full copy)
Best for: audit logs, cong lists, observer lists written once, read many times.
Transaction audit log : Use CopyOnWriteArrayList. Written once per transaction, but read
by many threads for reporting and end-of-day reconciliation.
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
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
ANALOGY
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
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.
[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.
Need a direct reference to the Thread object. Doesn't work Waits for a number of events, not specic threads. Works
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
If any line throws before countDown(), the latch stays at 1 finally guarantees countDown() runs even if the teller
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.
25 } finally {
26 latch . countDown () ; // always runs
27 }
28 return res ;
29 }
30 }
A CountDownLatch is a counter that threads can wait on. Three operations, that's it:
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.
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 ..."
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 ) ;
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 nested try-catch structure can look confusing. Here's exactly what each layer 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
A CountDownLatch is a one-time counter that threads can wait on. Three operations, that's it:
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.
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.
[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
What if a teller crashes before calling countDown()? The latch never reaches zero. await()
hangs forever. The entire program freezes.
If execute() throws an uncaught exception, countDown() is No matter what happens normal nish, caught exception,
This nesting can look confusing, so here's what each layer does:
This catches known business errors. The loop continues to the next transaction. Nothing escapes.
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.