Synchronization in Java Multithreading

Introduction

Synchronization in Java is a powerful mechanism that allows you to control the access of multiple threads to shared resources. It ensures that only one thread can access the resource at a time, preventing data inconsistency and race conditions. This is essential in a multithreading environment where threads often share resources like variables, arrays, or objects.

Table of Contents

  1. Overview of Synchronization
  2. Synchronized Methods
  3. Synchronized Blocks
  4. Static Synchronization
  5. Example: Synchronized Method
  6. Example: Synchronized Block
  7. Reentrant Synchronization
  8. Synchronization and Performance
  9. Conclusion

1. Overview of Synchronization

In Java, the synchronized keyword can be used to synchronize access to critical sections of code. There are two main types of synchronization:

  • Synchronized Methods: Entire methods are marked as synchronized, ensuring that only one thread can execute them at a time.
  • Synchronized Blocks: Specific blocks of code are marked as synchronized, providing more granular control over the synchronization.

2. Synchronized Methods

A synchronized method ensures that only one thread can execute it at a time for a particular object. The lock is held on the object instance on which the method is called.

Syntax:

public synchronized void synchronizedMethod() {
    // synchronized code
}

3. Synchronized Blocks

A synchronized block is a block of code within a method that is synchronized on a specified object. This allows you to lock only the critical section of the code rather than the entire method.

Syntax:

public void method() {
    synchronized (this) {
        // synchronized code
    }
}

4. Static Synchronization

Static synchronization ensures that a class-level lock is acquired, so all instances of the class share the same lock.

Syntax:

public static synchronized void staticSynchronizedMethod() {
    // synchronized code
}

Or using a synchronized block:

public static void staticMethod() {
    synchronized (ClassName.class) {
        // synchronized code
    }
}

5. Example: Synchronized Method

Example:

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class SynchronizedMethodExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Final count: " + counter.getCount());
    }
}

Output:

Final count: 2000

Explanation:

  • The Counter class has a synchronized increment method.
  • Two threads are created and both execute the increment method 1000 times each.
  • The final count is 2000, demonstrating that the synchronization prevents race conditions.

6. Example: Synchronized Block

Example:

class Counter {
    private int count = 0;

    public void increment() {
        synchronized (this) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

public class SynchronizedBlockExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Final count: " + counter.getCount());
    }
}

Output:

Final count: 2000

Explanation:

  • The Counter class has an increment method with a synchronized block.
  • Two threads are created and both execute the increment method 1000 times each.
  • The final count is 2000, demonstrating that the synchronization prevents race conditions.

7. Reentrant Synchronization

In Java, the locks used in synchronized methods and blocks are reentrant. This means that if a thread already holds a lock on a synchronized method or block, it can re-enter any synchronized method or block on the same object.

Example:

class ReentrantExample {
    public synchronized void method1() {
        System.out.println("Inside method1");
        method2();
    }

    public synchronized void method2() {
        System.out.println("Inside method2");
    }

    public static void main(String[] args) {
        ReentrantExample example = new ReentrantExample();
        example.method1();
    }
}

Output:

Inside method1
Inside method2

Explanation:

  • The method1 calls method2, both of which are synchronized.
  • The thread re-enters the lock it already holds, demonstrating reentrant synchronization.

8. Synchronization and Performance

While synchronization is essential for thread safety, it can impact performance due to the overhead of acquiring and releasing locks. To reduce contention and improve performance, it's crucial to minimize the scope of synchronized blocks and methods.

9. Conclusion

Synchronization in Java is used to ensure thread safety and prevent race conditions in a multithreaded environment. By using synchronized methods, synchronized blocks, and static synchronization, you can control the access of multiple threads to shared resources. However, it's important to use synchronization judiciously to balance thread safety and performance.

Happy coding!

Comments