Navigation
Java Full Course: Mastering the Language Synchronization and Inter-Thread Communication
Synchronization and Inter-Thread Communication
1. Introduction
In a multithreaded environment, multiple threads often share the same resources (e.g., variables, objects, files). When threads access shared data concurrently, race conditions can occur, leading to inconsistent or corrupted states. Synchronization is the mechanism that controls access to shared resources, ensuring that only one thread can execute a critical section at a time.
Inter‑thread communication allows threads to coordinate their actions—for example, one thread waits for a condition to be satisfied before proceeding, while another thread signals that the condition is met.
Java provides built‑in synchronization using monitors (associated with every object) and methods wait(), notify(), notifyAll(). For more advanced concurrency, the java.util.concurrent package offers higher‑level constructs (locks, semaphores, queues, etc.), but understanding the low‑level primitives is essential.
Part 1: Synchronization
2. The Need for Synchronization
Consider a simple counter incremented by multiple threads without synchronization:
The count++ operation is not atomic; it consists of read, increment, and write. Without synchronization, two threads may interfere, resulting in lost updates.
3. Synchronized Methods
Java uses intrinsic locks (monitors). Every object has an intrinsic lock. A thread that enters a synchronized method acquires the lock on the object (for instance methods) or on the class (for static methods). No other thread can enter any synchronized method on the same object until the lock is released.
Now the two threads cannot interleave increment() calls because they compete for the same object lock.
Key points:
- Only one thread can execute any synchronized instance method of an object at a time.
- Other threads trying to execute any synchronized method on the same object will block until the lock is released.
- The lock is released when the method exits (normally or via exception).
4. Synchronized Blocks
Synchronized blocks allow finer control: you specify the object whose lock to acquire. This can improve performance by reducing the scope of synchronization.
Why use a separate lock object?
- Avoid exposing
thisto external synchronized code. - Allows multiple independent locks within the same class.
Static synchronized methods lock on the Class object:
5. Important Concepts
- Reentrant synchronization – A thread can re‑enter a lock it already holds (e.g., a synchronized method calling another synchronized method on the same object).
- Lock is released only after the synchronized block/method exits, not when
wait()is called (covered later). - Volatile keyword – Guarantees visibility of changes across threads, but does not guarantee atomicity. Use for simple flags or when only one thread writes and others read.
6. Common Pitfalls with Synchronization
| Pitfall | Explanation / Solution |
|---|---|
| Deadlock | Two or more threads are blocked forever, each waiting for a lock held by the other. Avoid by consistent lock ordering or using tryLock from java.util.concurrent.locks. |
| Liveness issues (starvation) | A thread never gets CPU time because other threads continuously acquire locks. Use fair locks if needed. |
| Synchronizing on a mutable object | If the lock object reference changes, threads may not synchronize on the same lock. Use final lock objects. |
| Over‑synchronization | Holding locks longer than necessary degrades performance. Synchronize only the critical section. |
Using this as lock in a class that is extended | Subclasses can inadvertently use the same lock, causing interference. Prefer private lock objects. |
Part 2: Inter‑Thread Communication
7. The wait(), notify(), notifyAll() Methods
These methods are defined in java.lang.Object and allow threads to communicate:
wait()– Causes the current thread to release the lock on the object and enter the WAITING state until another thread callsnotify()ornotifyAll()on the same object (or a timeout).notify()– Wakes up one thread that is waiting on the object’s monitor.notifyAll()– Wakes up all threads waiting on the object’s monitor.
[!IMPORTANT] Important rules:
- These methods must be called from within a synchronized context (synchronized method or block) on the same object. Otherwise,
IllegalMonitorStateExceptionis thrown. - The thread calling
wait()releases the lock and waits. When it is notified, it re‑acquires the lock before returning fromwait().
8. Producer‑Consumer Example
A classic example of inter‑thread communication: one thread produces data, another consumes it, using a shared buffer.
Key points:
- Use
whilearoundwait()to handle spurious wakeups (threads can wake up withoutnotify). - Always call
notifyAll()(ornotify()) after changing the condition. - Synchronize on the same object (here,
buffer) for both producer and consumer.
9. Why wait() Must Be in a Loop
A thread can wake up from wait() without an explicit notify (spurious wakeup). By checking the condition in a loop, you ensure that the thread only proceeds when the condition is actually satisfied.
10. notify() vs notifyAll()
notify()– Wakes up one arbitrarily chosen waiting thread. Use it when only one thread can make progress (e.g., multiple consumers but only one item). However, if the wrong thread wakes up, it may wait again.notifyAll()– Wakes all waiting threads. They will compete for the lock, and only one will proceed. This is safer and often preferred, though it may cause some overhead.
11. Modern Alternatives
The java.util.concurrent package provides higher‑level synchronization and communication utilities:
BlockingQueue(e.g.,ArrayBlockingQueue,LinkedBlockingQueue) – Handles producer‑consumer without explicitwait/notify.CountDownLatch,CyclicBarrier,Semaphore– For coordinating threads.LockandCondition– Offer more flexible locking (e.g.,ReentrantLock,Conditionwith multiple await/signal).
Example using BlockingQueue:
The put() and take() methods block automatically and handle synchronization internally.
12. Common Pitfalls
| Pitfall | Explanation / Solution |
|---|---|
Calling wait() or notify() without synchronization | Throws IllegalMonitorStateException. Always synchronize on the same object. |
Using if instead of while around wait() | Spurious wakeups or missed signals can cause incorrect behavior. Use while. |
Not using notifyAll() when multiple threads wait | notify() may wake the wrong thread, causing deadlock. Use notifyAll() unless you are certain. |
| Forgetting to re‑check condition after wait | The condition may have changed again. Loop ensures correctness. |
| Deadlock due to nested locks | Be careful with lock ordering. Use tryLock to avoid indefinite blocking. |
13. Best Practices
- Prefer
java.util.concurrentutilities over low‑levelwait/notifyfor new code. They are easier to use and less error‑prone. - Synchronize only on
finalobjects to avoid accidental reassignment of the lock. - Keep synchronized blocks as small as possible to reduce contention.
- Use
volatilefor simple flags to avoid synchronization overhead. - Name your threads to help debugging.
- Always handle
InterruptedExceptionproperly (restore interrupt status if you cannot throw it).
14. Key Points to Remember
- Synchronization prevents race conditions by controlling access to shared resources.
- Every object has an intrinsic lock; synchronized methods/blocks acquire that lock.
wait()releases the lock and puts the thread into WAITING state.notify()/notifyAll()wakes waiting threads; they must re‑acquire the lock before proceeding.- Always call
wait()in a loop to check the condition. - Modern concurrency utilities (
BlockingQueue,Lock,Semaphore, etc.) provide safer and more efficient alternatives.
