Lesson 4
Synchronized Blocks for Better Control
Synchronized Blocks for Better Control

Welcome back to your journey through Java concurrency! In the previous lesson, we discussed synchronization and data sharing between threads. You learned how to use the synchronized keyword to prevent race conditions and ensure the integrity of shared data by applying it to instance methods. Today, we will dive deeper by exploring synchronized blocks, which offer more granular control over specific parts of your code.

What You'll Learn

In this lesson, you will:

  • Understand the four types of synchronization: instance methods, static methods, synchronized blocks inside instance methods, and synchronized blocks inside static methods.
  • Learn how synchronized blocks provide more precise control over thread synchronization.
  • Compare synchronized methods to synchronized blocks.
  • Use practical examples to effectively apply these concepts.

By the end of this lesson, you will be able to use synchronized blocks to improve performance and manage concurrency issues in your multithreaded Java applications.

Types of Synchronization in Java

The synchronized keyword can be applied to four different parts of your code:

  1. Instance Methods: Synchronizing an entire instance method ensures that only one thread can call that method at a time on the given instance.
    Java
    1public synchronized void increment() { 2 count++; 3}
  2. Static Methods: Synchronizing a static method locks the entire class, preventing multiple threads from accessing that static method simultaneously.
    Java
    1public static synchronized void staticMethod() { 2 // Critical section 3}
  3. Synchronized Blocks Inside Instance Methods: Synchronizing specific parts of an instance method gives you control over which code blocks need to be locked, providing more efficiency.
    Java
    1public void increment() { 2 synchronized (this) { 3 count++; 4 } 5}
  4. Synchronized Blocks Inside Static Methods: You can also use synchronized blocks within static methods, which can lock specific parts of the static method.
    Java
    1public static void staticMethod() { 2 // Synchronize on the class itself to ensure that this block is 3 // accessed by only one thread at a time across all instances 4 synchronized (SynchronizedBlockCounter.class) { 5 // Critical section 6 } 7}

In the previous lesson, we focused on instance methods by marking the entire method with the synchronized keyword. In this lesson, we will focus on synchronized blocks, which allow more precise control by synchronizing only the critical sections of a method rather than the entire method.

Using Synchronized Blocks: A Practical Example

Synchronized blocks allow you to specify which object acts as the "monitor" or "lock." In the example below, this is used as the monitor, meaning the current instance of the class is locked while executing the critical section. This ensures that only one thread at a time can execute the synchronized block on a given instance.

Java
1public class SynchronizedBlockCounter { 2 private int count = 0; 3 4 public void increment() { 5 synchronized (this) { // Synchronize only the critical section using this as the monitor 6 count++; 7 } 8 } 9 10 public int getCount() { 11 synchronized (this) { 12 return count; 13 } 14 } 15}

In the above example, the increment method synchronizes only the critical section where the shared variable count is updated, instead of locking the entire method. This can improve performance when other parts of the method do not require synchronization.

The this keyword is used to indicate that the current instance serves as the monitor. Before a thread can enter the synchronized block, it must acquire the lock on the instance. By using a synchronized block, we ensure that updates to count are made safely, while allowing other parts of the class to remain unsynchronized, thus improving efficiency.

Similarly, the getCount method uses a synchronized block to protect access to count, ensuring it reads a consistent value without risking partially updated data.

We'll explore more advanced locking mechanisms, like explicit locks, in future lessons, which offer even more flexibility in managing thread synchronization.

Running Threads with Synchronized Blocks

Let’s see how synchronized blocks can be used to manage shared data effectively in a multithreaded context.

Java
1public class Main { 2 public static void main(String[] args) throws InterruptedException { 3 SynchronizedBlockCounter counter = new SynchronizedBlockCounter(); 4 Thread t1 = new Thread(() -> { 5 for (int i = 0; i < 1000; i++) { 6 counter.increment(); 7 } 8 }); 9 Thread t2 = new Thread(() -> { 10 for (int i = 0; i < 1000; i++) { 11 counter.increment(); 12 } 13 }); 14 15 t1.start(); 16 t2.start(); 17 t1.join(); 18 t2.join(); 19 20 System.out.println("Final count with synchronized block: " + counter.getCount()); 21 } 22}
  • We create an instance of SynchronizedBlockCounter to be accessed by multiple threads.
  • Two threads (t1 and t2) increment the counter concurrently.
  • Using synchronized(this), only the critical sections (count++) are synchronized, ensuring efficient use of locks.
  • After starting both threads, we use join() to make sure the main thread waits until both have completed before printing the final count.
  • The expected output is 2000, as each thread increments the counter 1000 times. Using synchronized blocks ensures that there is no data inconsistency due to race conditions.
Importance of Synchronized Blocks

Synchronized blocks are essential in managing concurrent threads effectively:

  • Fine-Grained Synchronization: Synchronized blocks provide more precise control over synchronization, locking only necessary sections of code.
  • Reduced Contention: Only critical sections are synchronized, which means other parts of the method can still run concurrently.
  • Flexible Locking: You can use different monitor objects, not just this, to control synchronization at a more precise level.

By understanding synchronized blocks, you will be better equipped to write more efficient and scalable multithreaded Java applications. This is particularly important for real-world applications where optimizing resource usage and ensuring thread safety are both critical.

Ready to test your understanding with some exercises? Let’s dive into practice and see how well you can apply synchronized blocks!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.