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
- Thread Interference: Occurs when multiple threads try to modify shared data simultaneously.
- Memory Consistency Errors: Occur when different threads have inconsistent views of what should be the same data.
- Critical Section: A part of the program where shared resources are accessed.
- 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
- Over-Synchronization: Synchronizing more than necessary can lead to performance issues. Use synchronized blocks instead of synchronized methods when possible.
- Deadlocks: Be cautious of deadlocks, which occur when two or more threads are waiting for each other to release locks.
- 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
- Introduction to Java
- Setting Up the Development Environment
- Basic Syntax and Structure
- Variables and Data Types
- Operators
Module 2: Control Flow
Module 3: Object-Oriented Programming
- Introduction to OOP
- Classes and Objects
- Methods
- Constructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
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
- Introduction to Multithreading
- Creating Threads
- Thread Lifecycle
- Synchronization
- Concurrency Utilities
Module 9: Networking
- Introduction to Networking
- Sockets
- ServerSocket
- DatagramSocket and DatagramPacket
- URL and HttpURLConnection