Synchronization in Java is a mechanism that ensures that two or more concurrent threads do not simultaneously execute some particular program segment known as a critical section. This is crucial in a multithreaded environment to prevent thread interference and memory consistency errors.

Key Concepts

  1. Thread Interference: Occurs when multiple threads try to modify shared data simultaneously.
  2. Memory Consistency Errors: Occur when different threads have inconsistent views of what should be the same data.
  3. Critical Section: A part of the program where shared resources are accessed.
  4. Locks: Mechanisms to control access to the critical section.

Synchronized Methods

A synchronized method ensures that only one thread can execute it at a time for a given object.

Example

public class Counter {
    private int count = 0;

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

    public int getCount() {
        return count;
    }
}

public class Main {
    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());
    }
}

Explanation

  • Counter Class: Contains a synchronized method increment() which ensures that only one thread can increment the count at a time.
  • Main Class: Creates two threads that run the same task of incrementing the counter 1000 times. The join() method ensures that the main thread waits for both threads to finish before printing the final count.

Synchronized Blocks

Synchronized blocks provide more granular control over the synchronization. They can be used to synchronize only a part of the method.

Example

public class Counter {
    private int count = 0;

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

    public int getCount() {
        return count;
    }
}

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

        Runnable task = () -> {
            for (int i = 1000; i > 0; 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());
    }
}

Explanation

  • Synchronized Block: The synchronized (this) block ensures that only one thread can execute the block at a time, providing more control over which part of the method is synchronized.

Static Synchronization

Static synchronization is used to synchronize static methods. The lock is on the class object.

Example

public class Counter {
    private static int count = 0;

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

    public static int getCount() {
        return count;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        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());
    }
}

Explanation

  • Static Synchronized Method: The increment() method is synchronized and static, ensuring that only one thread can execute it at a time across all instances of the class.

Practical Exercises

Exercise 1: Synchronized Method

Task: Create a class BankAccount with a synchronized method deposit and a method getBalance. Create multiple threads to deposit money into the account and ensure the final balance is correct.

public class BankAccount {
    private int balance = 0;

    public synchronized void deposit(int amount) {
        balance += amount;
    }

    public int getBalance() {
        return balance;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        BankAccount account = new BankAccount();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                account.deposit(1);
            }
        };

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

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

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

        System.out.println("Final balance: " + account.getBalance());
    }
}

Solution

  • BankAccount Class: Contains a synchronized method deposit() to ensure thread-safe deposits.
  • Main Class: Creates two threads that deposit money into the account. The final balance should be 2000.

Exercise 2: Synchronized Block

Task: Modify the BankAccount class to use a synchronized block instead of a synchronized method.

public class BankAccount {
    private int balance = 0;

    public void deposit(int amount) {
        synchronized (this) {
            balance += amount;
        }
    }

    public int getBalance() {
        return balance;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        BankAccount account = new BankAccount();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                account.deposit(1);
            }
        };

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

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

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

        System.out.println("Final balance: " + account.getBalance());
    }
}

Solution

  • Synchronized Block: The deposit() method now uses a synchronized block to ensure thread-safe deposits.

Common Mistakes and Tips

  1. Over-Synchronization: Synchronizing more than necessary can lead to performance issues. Use synchronized blocks instead of synchronized methods when possible.
  2. Deadlocks: Be cautious of deadlocks, which occur when two or more threads are waiting for each other to release locks.
  3. Atomic Variables: For simple operations, consider using atomic variables from java.util.concurrent.atomic package.

Conclusion

Synchronization is a fundamental concept in Java for ensuring thread safety in a multithreaded environment. By using synchronized methods, synchronized blocks, and static synchronization, you can control access to critical sections and prevent thread interference and memory consistency errors. Practice with the provided exercises to reinforce your understanding and become proficient in handling synchronization in Java.

Java Programming Course

Module 1: Introduction to Java

Module 2: Control Flow

Module 3: Object-Oriented Programming

Module 4: Advanced Object-Oriented Programming

Module 5: Data Structures and Collections

Module 6: Exception Handling

Module 7: File I/O

Module 8: Multithreading and Concurrency

Module 9: Networking

Module 10: Advanced Topics

Module 11: Java Frameworks and Libraries

Module 12: Building Real-World Applications

© Copyright 2024. All rights reserved