Multithreading: Synchronization in Java


In multithreading, synchronization is used to control the access of multiple threads to shared resources. Without synchronization, inconsistent data can occur when two or more threads access shared resources simultaneously. This tutorial explains how to use synchronization in Java with examples.

Step 1: The Problem Without Synchronization

Consider an example where multiple threads are incrementing a shared counter without synchronization:

    class Counter {
        private int count = 0;
    
        public void increment() {
            count++;
        }
    
        public int getCount() {
            return count;
        }
    }
    
    public class NoSynchronizationExample {
        public static void main(String[] args) {
            Counter counter = new Counter();
    
            Runnable task = () -> {
                for (int i = 0; i < 1000; i++) {
                    counter.increment();
                }
            };
    
            Thread t1 = new Thread(task);
            Thread t2 = new Thread(task);
    
            t1.start();
            t2.start();
    
            try {
                t1.join();
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("Final count: " + counter.getCount()); // May produce incorrect result
        }
    }
        

Step 2: Using Synchronized Methods

You can synchronize a method by using the synchronized keyword. Below is the corrected version:

    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) {
            Counter counter = new Counter();
    
            Runnable task = () -> {
                for (int i = 0; i < 1000; i++) {
                    counter.increment();
                }
            };
    
            Thread t1 = new Thread(task);
            Thread t2 = new Thread(task);
    
            t1.start();
            t2.start();
    
            try {
                t1.join();
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("Final count: " + counter.getCount()); // Correct result
        }
    }
        

Step 3: Using Synchronized Blocks

Instead of synchronizing the entire method, you can synchronize only the critical section of code:

    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) {
            Counter counter = new Counter();
    
            Runnable task = () -> {
                for (int i = 0; i < 1000; i++) {
                    counter.increment();
                }
            };
    
            Thread t1 = new Thread(task);
            Thread t2 = new Thread(task);
    
            t1.start();
            t2.start();
    
            try {
                t1.join();
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("Final count: " + counter.getCount());
        }
    }
        

Step 4: Synchronizing Static Methods

If a method is static, you can synchronize it using the class object:

    class StaticCounter {
        private static int count = 0;
    
        public static synchronized void increment() {
            count++;
        }
    
        public static int getCount() {
            return count;
        }
    }
    
    public class SynchronizedStaticMethodExample {
        public static void main(String[] args) {
            Runnable task = () -> {
                for (int i = 0; i < 1000; i++) {
                    StaticCounter.increment();
                }
            };
    
            Thread t1 = new Thread(task);
            Thread t2 = new Thread(task);
    
            t1.start();
            t2.start();
    
            try {
                t1.join();
                t2.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("Final count: " + StaticCounter.getCount());
        }
    }
        

Summary

In this tutorial, you learned:

  • The problem with shared resources in multithreading
  • How to use synchronized methods to prevent data inconsistency
  • How to use synchronized blocks for finer control
  • How to synchronize static methods

Synchronization is essential for ensuring thread safety when working with shared resources in Java.





Advertisement