In Java, multithreading is a common technique to achieve concurrency. However, when multiple threads access shared resources, such as variables, files, or databases, synchronization becomes crucial to avoid conflicts or inconsistent results. Thread synchronization in Java ensures that only one thread can access a shared resource at a time. This article will explain thread synchronization, how it works, and how to manage shared resources effectively in advanced Java.
Thread synchronization ensures that only one thread at a time can access a particular section of code, also known as a critical section. Without synchronization, threads might interfere with each other when they modify shared resources, leading to data inconsistency or unexpected behavior.
In Java, synchronization is achieved by using the synchronized
keyword, which can be applied to methods or code
blocks that need mutual exclusion.
The simplest way to synchronize access to shared resources is to declare a method as synchronized. When a method is marked synchronized, only one thread can execute that method at a time on the same object.
class Counter { private int count = 0; // Synchronized method public synchronized void increment() { count++; } public int getCount() { return count; } } public class SynchronizationExample { public static void main(String[] args) { Counter counter = new Counter(); // Create two threads that will access the increment method Thread thread1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }); // Start both threads thread1.start(); thread2.start(); // Wait for both threads to finish try { thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } // Print the final count (should be 2000) System.out.println("Final count: " + counter.getCount()); } }
In this example, the increment()
method is synchronized, meaning that only one thread can execute it at a time.
This ensures that the count
variable is not corrupted when accessed by multiple threads.
You can also synchronize specific blocks of code inside a method instead of synchronizing the entire method. This is useful when you want to minimize the performance impact of synchronization.
class Counter { private int count = 0; public void increment() { synchronized (this) { count++; } } public int getCount() { return count; } } public class SynchronizationExample { public static void main(String[] args) { Counter counter = new Counter(); // Create two threads that will access the increment method Thread thread1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }); // Start both threads thread1.start(); thread2.start(); // Wait for both threads to finish try { thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } // Print the final count (should be 2000) System.out.println("Final count: " + counter.getCount()); } }
In this example, only the critical section (the count++
operation) is synchronized, reducing the scope of synchronization.
You can synchronize an entire class to ensure that only one thread can access any of the synchronized methods of that class
at a time. This is achieved by synchronizing on the class's class object (using Class
).
class Counter { private int count = 0; // Synchronize the class-level method public static synchronized void increment(Counter counter) { counter.count++; } public int getCount() { return count; } } public class SynchronizationExample { public static void main(String[] args) { Counter counter = new Counter(); // Create two threads that will access the increment method Thread thread1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { Counter.increment(counter); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { Counter.increment(counter); } }); // Start both threads thread1.start(); thread2.start(); // Wait for both threads to finish try { thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } // Print the final count (should be 2000) System.out.println("Final count: " + counter.getCount()); } }
In this example, the increment()
method is synchronized at the class level using the static synchronized
keyword. This prevents multiple threads from executing any synchronized method of the Counter
class simultaneously.
For more advanced synchronization, Java provides the Lock
interface (part of java.util.concurrent
)
that gives more flexibility and control over thread synchronization. The ReentrantLock
class is one of the most commonly used
classes implementing the Lock
interface.
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Counter { private int count = 0; private Lock lock = new ReentrantLock(); public void increment() { lock.lock(); // Acquire the lock try { count++; } finally { lock.unlock(); // Ensure the lock is always released } } public int getCount() { return count; } } public class LockExample { public static void main(String[] args) { Counter counter = new Counter(); // Create two threads that will access the increment method Thread thread1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { counter.increment(); } }); // Start both threads thread1.start(); thread2.start(); // Wait for both threads to finish try { thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } // Print the final count (should be 2000) System.out.println("Final count: " + counter.getCount()); } }
In this example, we use a ReentrantLock
to synchronize the critical section. The lock.lock()
method is
used to acquire the lock, and the lock.unlock()
method is used to release it. Using a Lock
gives more control
over synchronization, such as the ability to interrupt a thread waiting for a lock or attempt to acquire a lock without blocking indefinitely.
To manage shared resources effectively in a multithreaded environment:
Lock
for more advanced control and flexibility over synchronization.ExecutorService
, CountDownLatch
, or Semaphore
for more complex synchronization tasks.Thread synchronization in Java is essential when multiple threads access shared resources. By using synchronization techniques such as synchronized methods, synchronized blocks, and locks, we can ensure that shared resources are accessed safely, preventing data inconsistency and race conditions. Effective synchronization is crucial for writing robust and efficient multithreaded applications in Java.