In Java, when multiple threads work together, there’s a risk that two threads may access the same shared resource (like a variable or object) at the same time — leading to data inconsistency or unexpected behavior. To prevent this, Java provides a mechanism called Locks.
In this article, we’ll explore everything about thread synchronization and locks — including how they work internally, when to use them, and how to avoid common mistakes like deadlocks.
🔹 What is a Lock in Java?
A Lock is used to ensure that only one thread can access a shared resource at a time.
Think of it like a bathroom with one key — only one person can use it at a time, and others must wait until it’s free.
🔹 Why Do We Need Locks?
When multiple threads modify the same variable or object without synchronization, race conditions can occur.
class Counter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class RaceConditionExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + counter.getCount());
}
}
Output (varies): You may not get 2000 because both threads modify count simultaneously, leading to a race condition.
🔹 Using Synchronized Keyword
The simplest way to avoid such issues is to use the synchronized keyword.
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
Now, only one thread can execute increment() at a time.
🔹 How Locks Work Internally
When a thread enters a synchronized block, it acquires the lock of the object. Other threads trying to access the same block must wait until the first thread releases the lock.
Example:
class Display {
public synchronized void wish(String name) {
for (int i = 1; i <= 3; i++) {
System.out.println("Good morning: " + name);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class SyncExample {
public static void main(String[] args) {
Display display = new Display();
Thread t1 = new Thread(() -> display.wish("Alice"));
Thread t2 = new Thread(() -> display.wish("Bob"));
t1.start();
t2.start();
}
}
Output:
All “Good morning: Alice” messages will print first, then “Good morning: Bob” — because only one thread holds the lock at a time.
🔹 ReentrantLock Example (Advanced Lock)
Java also provides a more flexible locking mechanism through the java.util.concurrent.locks package. The most commonly used lock is ReentrantLock.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // acquire lock
try {
count++;
} finally {
lock.unlock(); // release lock
}
}
public int getCount() {
return count;
}
}
public class ReentrantLockExample {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final Count: " + counter.getCount());
}
}
Output: Always 2000 ✅
🔹 Difference Between synchronized and ReentrantLock
| Feature | synchronized | ReentrantLock |
|---|---|---|
| Package | java keyword | java.util.concurrent.locks |
| Lock Release | Automatic | Must be released manually using unlock() |
| Try Lock | Not possible | Possible using tryLock() |
| Fairness Policy | No | Can be set (first-come-first-serve) |
| Condition Objects | No | Yes (using newCondition()) |
🔹 What is a Deadlock?
A deadlock occurs when two or more threads are waiting for each other’s lock indefinitely.
class A {
synchronized void methodA(B b) {
System.out.println("Thread 1 starts execution of methodA");
try { Thread.sleep(100); } catch (InterruptedException e) {}
b.last();
}
synchronized void last() {
System.out.println("Inside A.last()");
}
}
class B {
synchronized void methodB(A a) {
System.out.println("Thread 2 starts execution of methodB");
try { Thread.sleep(100); } catch (InterruptedException e) {}
a.last();
}
synchronized void last() {
System.out.println("Inside B.last()");
}
}
public class DeadlockExample extends Thread {
A a = new A();
B b = new B();
public void m1() {
this.start();
a.methodA(b);
}
public void run() {
b.methodB(a);
}
public static void main(String[] args) {
new DeadlockExample().m1();
}
}
Output:
Both threads wait for each other forever → Deadlock.
✅ Tips to Avoid Deadlocks
- Always acquire locks in a consistent order.
- Use
tryLock()with timeout instead of blocking locks. - Keep synchronized blocks as small as possible.
🔹 Summary
- Lock allows only one thread at a time to access shared data.
- Use
synchronizedfor simple cases,ReentrantLockfor advanced scenarios. - Always release the lock in the
finallyblock. - Be careful of deadlocks when multiple locks are used.
💬 Interview Questions
- What is a race condition in multithreading?
- Difference between synchronized and ReentrantLock?
- What happens if a thread forgets to release a lock?
- What is a deadlock? How can it be avoided?
- Can a thread acquire a lock it already holds?
Answer: Yes, that’s why it’s called ReentrantLock — a thread can reacquire the same lock multiple times.
🧠 Conclusion
Locks are a core part of Java’s concurrency model. They ensure thread safety and prevent race conditions, but if used carelessly, they can cause deadlocks or performance issues. Use them wisely depending on your application’s complexity and concurrency needs.
0 Comments