Class 14: Synchronization with synchronized Blocks
Learn how to synchronize only a specific portion of a method instead of making the entire method synchronized.
1. A Quick Recap
In our previous class, we learned about synchronized methods.
We saw that when a thread enters a synchronized instance method, it acquires the lock associated with that object.
Another thread trying to execute a synchronized instance method using the same object must wait until the lock becomes available.
Let's look at a simple example again.
class Counter {
int count = 0;
synchronized void increment() {
count++;
}
}
Here, the entire increment() method is
synchronized.
But sometimes, we may have a method containing several operations, and only one small portion needs protection.
This is where synchronized blocks become useful.
2. What Is a synchronized Block?
A synchronized block allows us to synchronize only a specific section of code.
Instead of writing synchronized before the
method declaration, we write it around the code that
needs protection.
The basic syntax looks like this:
synchronized (lockObject) {
// Code that needs synchronization
}
The object inside the parentheses is called the lock object.
A thread must acquire that object's lock before entering the synchronized block.
If another thread already holds the same lock, it must wait.
3. Our First synchronized Block
Let's take our Counter example and use a synchronized block instead of a synchronized method.
class Counter {
int count = 0;
void increment() {
synchronized (this) {
count++;
}
}
}
Notice that the method itself is not synchronized.
Only the statement that updates the counter is inside the synchronized block.
Here, this refers to the current Counter object.
So, the block uses the lock associated with that Counter object.
If two threads call increment() on the same
Counter object, only one thread at a time can execute the
code inside this synchronized block.
4. Let's Understand It with a Complete Example
We will use the same example from our previous class. Two threads will increment a shared counter 100,000 times.
class Counter {
int count = 0;
void increment() {
synchronized (this) {
count++;
}
}
}
public class Main {
public static void main(String[] args)
throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
Let's understand the important points.
- Both threads share the same Counter object.
- Both threads call the same
increment()method. - The counter update happens inside a synchronized block.
- The block uses
thisas its lock object. - Only one thread at a time can update the counter using this block on that object.
The expected output is:
200000
The counter update is now protected against the race condition we discussed in Class 12.
5. Why Not Synchronize the Entire Method?
Let's understand the difference with a simple example.
Imagine a method that performs three operations:
- Prints a message.
- Updates a shared counter.
- Prints another message.
Suppose only the counter update needs synchronization. We do not necessarily need to make the entire method synchronized.
We can write it like this:
class Counter {
int count = 0;
void increment() {
System.out.println("Starting increment");
synchronized (this) {
count++;
}
System.out.println("Finished increment");
}
}
Here, only the counter update is protected by the lock. The print statements are outside the synchronized block.
This means another thread may execute those print statements while the first thread is inside the synchronized block.
We should keep the synchronized section limited to the code that actually needs protection, while ensuring that all relevant access to shared data follows the same locking approach.
6. Can We Use a Different Lock Object?
Yes. We do not always have to use this.
We can create a separate object and use it as a lock.
class Counter {
int count = 0;
private final Object lock = new Object();
void increment() {
synchronized (lock) {
count++;
}
}
}
Here, we created a separate object named lock.
Whenever a thread wants to execute the synchronized block, it must acquire the lock associated with this object.
The private keyword prevents other classes
from directly accessing this lock field.
The final keyword ensures that this field
continues to refer to the same lock object.
For now, remember that a synchronized block can use any suitable object as its lock.
7. synchronized Method vs. synchronized Block
Let's compare the two approaches.
| synchronized Method | synchronized Block |
|---|---|
| The entire method is synchronized. | Only a specific section of code is synchronized. |
| An instance method uses the current object's lock. | We explicitly provide the lock object. |
| Simple when the entire method needs protection. | Useful when only a portion of the method needs protection. |
Both approaches use Java's monitor locking mechanism. The main difference is how much code we choose to protect and which lock object we use.
8. A Few Things to Remember
- A synchronized block protects a specific section of code.
- We write the lock object inside the parentheses.
- A thread must acquire that object's lock before entering the block.
- Other threads trying to acquire the same lock must wait.
- We can use
thisor a separate lock object. - Keep synchronized sections focused on the operations that need protection.
- All relevant access to shared data must follow a consistent synchronization approach.
Test Your Understanding
Let's see how well you understood this class!
Question 1: What is the main purpose of a synchronized block?
Question 2: What does the following code use as its lock?
synchronized (this) {
count++;
}
Question 3: What happens if another thread tries to enter a synchronized block using a lock that is already held?
Question 4: Can we use a separate object as a lock?
Question 5: What is one advantage of a synchronized block over a synchronized method?
Class Summary
Let's quickly revise what we learned in this class.
- A synchronized block allows us to protect a specific section of code.
- We specify the lock object inside the synchronized block.
- Using
synchronized (this)means using the current object's lock. - We can also use a separate object as a lock.
- Only one thread at a time can hold a particular object's monitor lock.
- Synchronized blocks are useful when we do not need to synchronize an entire method.
Remember: A synchronized block lets us decide exactly which section of code needs protection.
You have completed 14 out of 46 classes.