Advanced Multithreading
A thread has 5 states: New, Runnable, Running, Waiting/Blocked, Terminated.
wait()/notify() let threads wait for each other's signal. ExecutorService manages a thread pool — instead of manually creating new threads, ready threads get reused.
A deadlock happens when two threads keep waiting for the resource the other one holds, and neither moves forward — like two people at a door, each waiting for the other to go first, forever.
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(() -> System.out.println("Task running!"));
pool.shutdown();- Thread lifecycle: New->Runnable->Running->Waiting->Terminated
- ExecutorService = a thread pool manager
- Deadlock = threads stuck waiting forever
A synchronized method (the whole method gets locked) is simple but locks the entire method, even if only a small part is critical — this can cause unnecessary blocking (other threads that want to do unrelated work also have to wait).
A synchronized block (synchronized(obj) { ... }) locks only the necessary part — better performance, since the rest of the code can run in parallel. Every object has its own "monitor lock" that synchronized uses.
synchronized void increment() { count++; } // poora method locked
void increment2() {
// ... non-critical code, parallel chal sakta hai
synchronized(this) {
count++; // sirf ye critical section locked
}
}These three are methods on the Object class (available on every object), and can only be used inside a synchronized block (otherwise you get an IllegalMonitorStateException). wait() "puts the current thread to sleep" (and releases the lock) until another thread calls notify().
Classic use-case: the Producer-Consumer pattern, where the consumer calls wait() until the producer produces something and calls notify(). notifyAll() wakes up all waiting threads (not just one), which is safer when multiple threads are waiting.
Every thread goes through 5 states in its "life", starting at New and ending at Terminated. Understanding this matters for debugging — if your thread seems "stuck", checking its state can tell you whether it's in Waiting or Blocked.
| State | Meaning |
|---|---|
| New | Thread object created, but start() not called |
| Runnable | start() called, waiting for/getting CPU time |
| Running | Actually running on the CPU right now |
| Waiting/Blocked | Waiting on wait(), sleep(), or a lock |
| Terminated | run() completed, thread finished |
Manually creating threads all the time is expensive (every thread costs OS resources). ExecutorService manages a thread pool: newFixedThreadPool(n) — a fixed number of reusable threads, like n workers always ready. newCachedThreadPool() — a pool that grows/shrinks as needed, idle threads get freed once work is done. newScheduledThreadPool() — for running tasks after a delay or on a repeating interval.
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> doTask());
pool.shutdown(); // naye tasks accept karna band, purane complete hongeDeadlock happens when Thread A holds Lock-1 and waits for Lock-2, while Thread B holds Lock-2 and waits for Lock-1 — both get stuck forever, each waiting for what the other one is holding.
Prevention: always acquire locks in the *same order* across all threads (like always Lock-1 first, then Lock-2, never reversed) — this makes "circular wait" impossible in the first place. Or use locks with timeouts (tryLock(timeout)), so a thread never waits forever and gives up after the timeout.
In a multi-threaded environment, each thread can have its own CPU-cached "local copy" of a variable (for performance) — so it's possible that one thread's change doesn't immediately show up for another thread.
The volatile keyword guarantees that reads/writes of a variable always go to main memory, not a cache — so one thread's change is immediately visible to other threads. This does NOT give mutual-exclusion like synchronization does (only one thread at a time), it only guarantees "visibility" — for a compound operation like count++, volatile isn't enough, you need synchronized/Atomic.