Home Python C Language C ++ HTML 5 CSS Javascript Java Kotlin SQL DJango Bootstrap React.js R C# PHP ASP.Net Numpy Dart Pandas Digital Marketing

Thread Synchronization and Managing Shared Resources


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.

Step-by-Step Guide to Thread Synchronization

Step 1: Understanding Thread Synchronization

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.

Step 2: Synchronizing Methods

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.

Step 3: Synchronizing Code Blocks

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.

Step 4: Synchronization at the Class Level

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.

Step 5: Using Locks for Advanced Synchronization

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.

Step 6: Managing Shared Resources Effectively

To manage shared resources effectively in a multithreaded environment:

Conclusion

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.



Advertisement





Q3 Schools : India


Online Complier

HTML 5

Python

java

C++

C

JavaScript

Website Development

HTML

CSS

JavaScript

Python

SQL

Campus Learning

C

C#

java