Home/Java/Synchronization in Java

Synchronization in Java

Learn how to prevent data corruption when multiple threads access shared resources

Think of synchronization like a bathroom with a lock! When someone is using the bathroom (a thread working with shared data), they lock the door so nobody else can come in at the same time. Only after they're done and unlock the door can the next person enter. Without this lock, multiple people trying to use the bathroom at once would be chaos - just like threads accessing shared data without synchronization!

The Problem Without Synchronization

Imagine a bank account shared by family members. If two people try to withdraw money at exactly the same time without proper coordination, the balance could get messed up! Let's see this race condition in code:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// Without synchronization - race condition occurs!
class BankAccount {
private int balance = 1000;
public void withdraw(int amount) {
System.out.println(Thread.currentThread().getName() +
" is trying to withdraw $" + amount);
// Check if enough balance
if (balance >= amount) {
System.out.println(Thread.currentThread().getName() +
" - Balance is sufficient");
try {
// Simulate some processing time
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Withdraw the money
balance = balance - amount;
System.out.println(Thread.currentThread().getName() +
" completed withdrawal. Remaining balance: $" + balance);
} else {
System.out.println(Thread.currentThread().getName() +
" - Insufficient balance!");
}
}
public int getBalance() {
return balance;
}
}
public class RaceConditionExample {
public static void main(String[] args) {
BankAccount account = new BankAccount();
// Two family members try to withdraw money simultaneously
Thread husband = new Thread(() -> {
account.withdraw(800);
}, "Husband");
Thread wife = new Thread(() -> {
account.withdraw(800);
}, "Wife");
husband.start();
wife.start();
try {
husband.join();
wife.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("\nFinal balance: $" + account.getBalance());
}
}
/* Output (Race Condition!):
Husband is trying to withdraw $800
Wife is trying to withdraw $800
Husband - Balance is sufficient
Wife - Balance is sufficient
Husband completed withdrawal. Remaining balance: $200
Wife completed withdrawal. Remaining balance: $-600
Final balance: $-600
PROBLEM: Both checked balance before either withdrew!
This should never happen - you can't have negative money!
*/

Synchronized Methods

The easiest way to fix this is by using synchronized methods! When you add the 'synchronized' keyword to a method, only one thread can execute it at a time. It's like putting a lock on the bathroom door - one person at a time!

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// Using synchronized method - problem solved!
class SafeBankAccount {
private int balance = 1000;
// synchronized keyword ensures only one thread at a time
public synchronized void withdraw(int amount) {
System.out.println(Thread.currentThread().getName() +
" is trying to withdraw $" + amount);
if (balance >= amount) {
System.out.println(Thread.currentThread().getName() +
" - Balance is sufficient");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance = balance - amount;
System.out.println(Thread.currentThread().getName() +
" completed withdrawal. Remaining balance: $" + balance);
} else {
System.out.println(Thread.currentThread().getName() +
" - Insufficient balance!");
}
}
public synchronized int getBalance() {
return balance;
}
}
public class SynchronizedMethodExample {
public static void main(String[] args) {
SafeBankAccount account = new SafeBankAccount();
Thread husband = new Thread(() -> {
account.withdraw(800);
}, "Husband");
Thread wife = new Thread(() -> {
account.withdraw(800);
}, "Wife");
husband.start();
wife.start();
try {
husband.join();
wife.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("\nFinal balance: $" + account.getBalance());
}
}
/* Output (Fixed!):
Husband is trying to withdraw $800
Husband - Balance is sufficient
Husband completed withdrawal. Remaining balance: $200
Wife is trying to withdraw $800
Wife - Insufficient balance!
Final balance: $200
SUCCESS: Wife waited for Husband to finish first!
Now only one person could withdraw, preventing negative balance.
*/

Synchronized Blocks

Sometimes you don't want to lock the entire method - just a specific part. That's where synchronized blocks come in! It's like having a shared kitchen but only locking the refrigerator when someone needs to use it, allowing others to use the stove at the same time.

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
// Using synchronized blocks for finer control
class Restaurant {
private int totalOrders = 0;
private final Object orderLock = new Object();
private final Object cookLock = new Object();
public void processOrder(String dish) {
// This part doesn't need synchronization
System.out.println(Thread.currentThread().getName() +
" received order for: " + dish);
try {
Thread.sleep(50); // Simulate order entry
} catch (InterruptedException e) {
e.printStackTrace();
}
// Only synchronize the critical section
synchronized (orderLock) {
totalOrders++;
System.out.println(Thread.currentThread().getName() +
" - Total orders now: " + totalOrders);
}
// Cooking can happen in parallel for different dishes
synchronized (cookLock) {
System.out.println(Thread.currentThread().getName() +
" is cooking: " + dish);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(dish + " is ready!");
}
}
public int getTotalOrders() {
synchronized (orderLock) {
return totalOrders;
}
}
}
public class SynchronizedBlockExample {
public static void main(String[] args) {
Restaurant restaurant = new Restaurant();
Thread waiter1 = new Thread(() -> {
restaurant.processOrder("Pizza");
}, "Waiter-1");
Thread waiter2 = new Thread(() -> {
restaurant.processOrder("Burger");
}, "Waiter-2");
Thread waiter3 = new Thread(() -> {
restaurant.processOrder("Pasta");
}, "Waiter-3");
waiter1.start();
waiter2.start();
waiter3.start();
try {
waiter1.join();
waiter2.join();
waiter3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("\nTotal orders processed: " +
restaurant.getTotalOrders());
}
}
/* Output:
Waiter-1 received order for: Pizza
Waiter-2 received order for: Burger
Waiter-3 received order for: Pasta
Waiter-1 - Total orders now: 1
Waiter-1 is cooking: Pizza
Waiter-2 - Total orders now: 2
Waiter-3 - Total orders now: 3
Pizza is ready!
Waiter-2 is cooking: Burger
Burger is ready!
Waiter-3 is cooking: Pasta
Pasta is ready!
Total orders processed: 3
*/

Static Synchronization

When you need to synchronize across all instances of a class (not just one object), use static synchronization. This locks the entire class, like having one master key that controls all apartments in a building, not just one apartment!

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
// Static synchronization - class-level locking
class Counter {
private static int count = 0;
// Synchronized on the class itself
public static synchronized void increment() {
count++;
System.out.println(Thread.currentThread().getName() +
" incremented count to: " + count);
}
// Alternative: synchronized block with class lock
public static void decrement() {
synchronized (Counter.class) {
count--;
System.out.println(Thread.currentThread().getName() +
" decremented count to: " + count);
}
}
public static synchronized int getCount() {
return count;
}
}
public class StaticSyncExample {
public static void main(String[] args) {
// Create multiple threads that all work on the static counter
Runnable incrementTask = () -> {
for (int i = 0; i < 5; i++) {
Counter.increment();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Runnable decrementTask = () -> {
for (int i = 0; i < 3; i++) {
Counter.decrement();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread t1 = new Thread(incrementTask, "Thread-1");
Thread t2 = new Thread(incrementTask, "Thread-2");
Thread t3 = new Thread(decrementTask, "Thread-3");
t1.start();
t2.start();
t3.start();
try {
t1.join();
t2.join();
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("\nFinal count: " + Counter.getCount());
}
}
/* Output:
Thread-1 incremented count to: 1
Thread-2 incremented count to: 2
Thread-3 decremented count to: 1
Thread-1 incremented count to: 2
Thread-2 incremented count to: 3
Thread-3 decremented count to: 2
Thread-1 incremented count to: 3
Thread-2 incremented count to: 4
Thread-3 decremented count to: 3
Thread-1 incremented count to: 4
Thread-2 incremented count to: 5
Thread-1 incremented count to: 6
Thread-2 incremented count to: 7
Final count: 7
(5 + 5 - 3 = 7) Correctly synchronized!
*/

Understanding Deadlock

Deadlock happens when two or more threads are waiting for each other to release locks, and nobody can proceed! It's like two people meeting in a narrow hallway, each waiting for the other to move first - they're stuck forever! Let's see this dangerous situation:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// Example of Deadlock - Be careful!
class Resource {
private String name;
public Resource(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class DeadlockExample {
public static void main(String[] args) {
Resource pen = new Resource("Pen");
Resource paper = new Resource("Paper");
// Thread 1: Takes pen, then wants paper
Thread student1 = new Thread(() -> {
synchronized (pen) {
System.out.println("Student-1 acquired: " + pen.getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Student-1 waiting for: " + paper.getName());
synchronized (paper) {
System.out.println("Student-1 acquired: " + paper.getName());
System.out.println("Student-1 writing...");
}
}
}, "Student-1");
// Thread 2: Takes paper, then wants pen
Thread student2 = new Thread(() -> {
synchronized (paper) {
System.out.println("Student-2 acquired: " + paper.getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Student-2 waiting for: " + pen.getName());
synchronized (pen) {
System.out.println("Student-2 acquired: " + pen.getName());
System.out.println("Student-2 writing...");
}
}
}, "Student-2");
student1.start();
student2.start();
// This program will hang forever!
}
}
/* Output (Deadlock - program hangs!):
Student-1 acquired: Pen
Student-2 acquired: Paper
Student-1 waiting for: Paper
Student-2 waiting for: Pen
(Program stuck here forever - both waiting for each other!)
SOLUTION: Always acquire locks in the same order!
If both students always get pen first, then paper, no deadlock.
*/

Real-World Example: Thread-Safe Shopping Cart

Let's build a practical shopping cart that multiple threads can safely use at the same time. This shows how to properly synchronize a real application where multiple users might add or remove items simultaneously.

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// Real-world thread-safe shopping cart
import java.util.HashMap;
import java.util.Map;
class ShoppingCart {
private Map<String, Integer> items = new HashMap<>();
private double totalPrice = 0.0;
// Synchronized method to add items
public synchronized void addItem(String itemName, int quantity, double price) {
System.out.println(Thread.currentThread().getName() +
" adding: " + quantity + "x " + itemName);
items.put(itemName, items.getOrDefault(itemName, 0) + quantity);
totalPrice += (quantity * price);
System.out.println(Thread.currentThread().getName() +
" - Cart updated. Total: $" + String.format("%.2f", totalPrice));
}
// Synchronized method to remove items
public synchronized boolean removeItem(String itemName, int quantity) {
System.out.println(Thread.currentThread().getName() +
" removing: " + quantity + "x " + itemName);
Integer currentQuantity = items.get(itemName);
if (currentQuantity == null || currentQuantity < quantity) {
System.out.println(Thread.currentThread().getName() +
" - Cannot remove. Item not in cart or insufficient quantity.");
return false;
}
if (currentQuantity == quantity) {
items.remove(itemName);
} else {
items.put(itemName, currentQuantity - quantity);
}
System.out.println(Thread.currentThread().getName() +
" - Item removed successfully");
return true;
}
// Synchronized method to get cart summary
public synchronized void printCart() {
System.out.println("\n=== Shopping Cart Summary ===");
System.out.println("Customer: " + Thread.currentThread().getName());
if (items.isEmpty()) {
System.out.println("Cart is empty");
} else {
items.forEach((item, qty) ->
System.out.println(" " + item + ": " + qty));
}
System.out.println("Total Price: $" + String.format("%.2f", totalPrice));
System.out.println("============================\n");
}
public synchronized int getItemCount() {
return items.size();
}
}
public class ShoppingCartExample {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
// Multiple users adding items simultaneously
Thread user1 = new Thread(() -> {
cart.addItem("Laptop", 1, 999.99);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
cart.addItem("Mouse", 2, 25.50);
}, "User-1");
Thread user2 = new Thread(() -> {
cart.addItem("Keyboard", 1, 79.99);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
cart.addItem("Monitor", 1, 299.99);
}, "User-2");
Thread user3 = new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
cart.removeItem("Mouse", 1);
}, "User-3");
user1.start();
user2.start();
user3.start();
try {
user1.join();
user2.join();
user3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
cart.printCart();
}
}
/* Output:
User-1 adding: 1x Laptop
User-1 - Cart updated. Total: $999.99
User-2 adding: 1x Keyboard
User-2 - Cart updated. Total: $1079.98
User-1 adding: 2x Mouse
User-1 - Cart updated. Total: $1130.98
User-2 adding: 1x Monitor
User-2 - Cart updated. Total: $1430.97
User-3 removing: 1x Mouse
User-3 - Item removed successfully
=== Shopping Cart Summary ===
Customer: main
Laptop: 1
Keyboard: 1
Mouse: 1
Monitor: 1
Total Price: $1405.47
============================
All operations thread-safe! No data corruption.
*/

Key Concepts

Race Condition

Occurs when multiple threads access shared data simultaneously and the final outcome depends on the timing of their execution. Synchronization prevents this by ensuring only one thread accesses the critical section at a time.

Monitor Lock

Every Java object has an intrinsic lock (monitor). When a thread enters a synchronized method or block, it acquires this lock. Other threads must wait until the lock is released.

Synchronized vs Blocks

Synchronized methods lock the entire method. Synchronized blocks allow finer control - you can lock only specific code sections or use different objects as locks, improving performance.

Deadlock Prevention

To prevent deadlock: always acquire locks in the same order, avoid nested locks when possible, use timeout mechanisms, and minimize the time locks are held.

Best Practices

  • Always synchronize access to shared mutable data to prevent race conditions
  • Keep synchronized blocks as small as possible - only protect the critical section
  • Avoid calling other synchronized methods from within a synchronized method to prevent deadlock
  • Always acquire multiple locks in the same order across all threads
  • Document which locks protect which data fields clearly in comments
  • Consider using higher-level concurrency utilities (java.util.concurrent) instead of basic synchronization

Common Mistakes to Avoid

  • Synchronizing on 'this' when the object is exposed to external code - use a private lock object instead
  • Making entire methods synchronized when only a small section needs protection
  • Forgetting to synchronize all access points to shared data - must synchronize both reads and writes
  • Creating deadlocks by acquiring locks in different orders in different threads
  • Using synchronized blocks with different lock objects thinking they protect the same data
  • Over-synchronizing everything - this hurts performance and may not be necessary

Interview Tips

  • 💡Know the difference between synchronized methods and synchronized blocks - when to use each
  • 💡Be able to identify and explain race conditions in code examples
  • 💡Understand what causes deadlock and how to prevent it (consistent lock ordering)
  • 💡Know that synchronization uses monitor locks - every object has one intrinsic lock
  • 💡Understand that synchronized methods lock 'this' for instance methods and the Class object for static methods
  • 💡Be prepared to write thread-safe code and explain why synchronization is needed in specific scenarios