Java Thread Synchronization

When multiple threads access the same object simultaneously, then there may be data inconsistency. To resolve this data inconsistency problem synchronized method or synchronized block should be used. If a method or block is synchronized, then only one thread at a time is allowed to execute that method or block on a given object. This is what thread synchronization in java

Types of Thread Synchronization in Java
  1. 1. Method level Synchronization
  2. 2. Block Level Synchronization
1. Method level Synchronization
Syntax:

    synchronized datatype method

    synchronized void print(){ // method is synchronized
            // lines of codes
    }


Example without synchronization

In the below program, class Table contains one method print() which is not synchronized. Two threads, t1 and t2 are accessing the same print() method, which print table 4 and 6, respectively. Let’s see the output we get from the program when the method is not synchronized.


    package com.java.session.thirteen;

    public class PrintTableDemo {
        public static void main(String[] args) {
            Table t = new Table();
            Thread1 t1 = new Thread1(t);
            Thread2 t2 = new Thread2(t);
            t1.start();
            t2.start();
        }
    }
    class Table {
        void print(int value) {
            for (int i=1; i<=10; i++) {
                System.out.println(value*i);

                try {
                    Thread.sleep(1000);
                } catch(InterruptedException ie) {
                    ie.printStackTrace();
                }

            }
        }
    }
    class Thread1 extends Thread {
        Table t;
        Thread1(Table t) {
            this.t = t;
        }
        public void run() {
            t.print(4);
        }
    }
    class Thread2 extends Thread {
        Table t;
        Thread2(Table t) {
            this.t = t;
        }
        public void run() {
            t.print(6);
        }
    }


In the above program, class Thread1 and Thread2 extends the Thread class. In Thread1 class, the print() method of Table class is called to print table of 4 and in the Thread2 class, the print() method of the Table class is called to print a table of 6. Both classes access the print() method simultaneously. Since the method is not synchronized, we get different outputs.

Here, by observing the outputs, we conclude that if the method is not synchronized, then we get different outputs, or a data inconsistency problem will occur. So, to resolve this issue, we have to make a synchronized method. Now let’s make the method synchronized and then see the output.


    package com.java.session.thirteen;

    public class PrintTableDemo {
        public static void main(String[] args) {
            Table t = new Table();
            Thread1 t1 = new Thread1(t);
            Thread2 t2 = new Thread2(t);
            t1.start();
            t2.start();
        }
    }
    class Table {
    synchronized void print(int value) {
            for (int i=1; i<=10; i++) {
                System.out.println(value*i);

                try {
                    Thread.sleep(1000);
                } catch(InterruptedException ie) {
                    ie.printStackTrace();
                }

            }
        }
    }
    class Thread1 extends Thread {
        Table t;
        Thread1(Table t) {
            this.t = t;
        }
        public void run() {
            t.print(4);
        }
    }
    class Thread2 extends Thread {
        Table t;
        Thread2(Table t) {
            this.t = t;
        }
        public void run() {
            t.print(6);
        }
    }


Here, the print() method is synchronized, so at a time, only one thread is allowed to execute the method. Hence, we get the same output, and thus, we solve the problem of data inconsistency that we saw in the earlier program when the method is not synchronized. The first table of 4 is printed, and then the table of 6 will print.

2. Block Level Synchronization

In block-level synchronization, the entire method is not synchronized; only the part of the method or the block of code in the method is synchronized, which allows one thread to execute that block of code at a time.

Syntax:

    synchronized(this){
    //lines of code
}


Example:

Let’s take the same example again of table printing, but instead of making the method synchronized, we make a synchronized block inside the print () method to print tables 4 and 6. We always get the same output as the thread is synchronized, and at a time, one thread enters inside the synchronized block to execute the code.


    package com.java.session.thirteen;

    public class PrintTableDemo {
        public static void main(String[] args) {
            Table t = new Table();
            Thread1 t1 = new Thread1(t);
            Thread2 t2 = new Thread2(t);
            t1.start();
            t2.start();
        }
    }
    class Table {
        void print(int value) {
            synchronized (this) {  // synchronized block
                for (int i = 1; i <= 10; i++) {
                    System.out.println(value * i);

                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ie) {
                        ie.printStackTrace();
                    }
                }
            }
        }
    }
    class Thread1 extends Thread {
        Table t;
        Thread1(Table t) {
            this.t = t;
        }
        public void run() {
            t.print(4);
        }
    }
    class Thread2 extends Thread {
        Table t;
        Thread2(Table t) {
            this.t = t;
        }
        public void run() {
            t.print(6);
        }
    }



wait(): The wait() method is used to make a thread voluntarily give up its lock on an object, allowing another thread to execute code within a synchronized block. The thread that calls wait() will enter a waiting state until another thread calls notify() or notifyAll() on the same object, allowing it to resume execution.

notify(): The notify() method wakes up one of the waiting threads on the same object. If multiple threads are waiting, it is not specified which one will be awakened. The awakened thread will then compete for the lock on the object.

notifyAll(): The notifyAll() method wakes up all waiting threads on the same object. This can be useful when multiple threads are waiting, and you want all of them to be notified simultaneously.

Example 1: Producer-Consumer Problem

The producer-consumer problem is a classic synchronization problem where multiple threads share a common buffer. Producers add items to the buffer, while consumers remove items from it. The buffer has a limited capacity, and synchronization is essential to prevent race conditions.

In this example, we use the wait() and notify() methods for synchronization between the producer and consumer threads. The Buffer class has synchronized produce and consume methods. When the buffer is full, the producer thread should wait(), and when it becomes non-empty, the producer should notify() the consumer to start consuming. Similarly, when the buffer is empty, the consumer should wait(), and the producer should notify() when it adds an item. The producer and consumer threads run concurrently and demonstrate the producer-consumer problem.


package com.java.session.thirteen;

class Buffer {
    private final int[] buffer;
    private final int size;
    private int count;

    public Buffer(int size) {
        this.size = size;
        this.buffer = new int[size];
        this.count = 0;
    }

    public synchronized void produce(int item) throws InterruptedException {
        while (count == size) {
            // Buffer is full, wait for the consumer to consume
            wait();
        }

        buffer[count] = item;
        count++;
        System.out.println("Produced: " + item);

        // Notify the consumer that an item is available
        notify();
    }

    public synchronized int consume() throws InterruptedException {
        while (count == 0) {
            // Buffer is empty, wait for the producer to produce
            wait();
        }

        int item = buffer[count - 1];
        count--;
        System.out.println("Consumed: " + item);

        // Notify the producer that space is available
        notify();

        return item;
    }
}

class Producer implements Runnable {
    private final Buffer buffer;

    public Producer(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        try {
            for (int i = 1; i <= 5; i++) {
                buffer.produce(i);
                Thread.sleep(100);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

class Consumer implements Runnable {
    private final Buffer buffer;

    public Consumer(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        try {
            for (int i = 1; i <= 5; i++) {
                int item = buffer.consume();
                Thread.sleep(100);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Buffer buffer = new Buffer(10);

        Thread producerThread = new Thread(new Producer(buffer));
        Thread consumerThread = new Thread(new Consumer(buffer));

        producerThread.start();
        consumerThread.start();

        try {
            producerThread.join();
            consumerThread.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}


Example 2: Multi-Threaded Chat Application

Imagine you are developing a chat application where multiple users can send and receive messages simultaneously. Here, notifyAll() can be useful to wake up all waiting threads (users) when a new message arrives, allowing all users to see the message simultaneously.

In this example, we have a ChatRoom class that represents the chat room, which can send and receive messages. Users are represented as separate threads, and they can send messages to the chat room using the sendMessage method. The receiveMessage method allows users to retrieve messages from the chat room.

Each user thread sends five messages to the chat room with a short delay between each message. The wait() method is used in the receiveMessage method to make user threads wait when there are no messages in the chat room. The notifyAll() method is used to wake up all waiting user threads when a new message is sent.


    package com.java.session.thirteen;

    import java.util.LinkedList;
    import java.util.Queue;

    class ChatRoom {
        private Queue<String> messages = new LinkedList<>();

        public synchronized void sendMessage(String message) {
            messages.add(message);
            System.out.println("Message sent: " + message);
            notifyAll(); // Notify all users about the new message
        }

        public synchronized String receiveMessage() throws InterruptedException {
            while (messages.isEmpty()) {
                wait(); // Wait if no messages are available
            }
            String message = messages.poll();
            System.out.println("Message received: " + message);
            return message;
        }
    }

    class User implements Runnable {
        private String name;
        private ChatRoom chatRoom;

        public User(String name, ChatRoom chatRoom) {
            this.name = name;
            this.chatRoom = chatRoom;
        }

        @Override
        public void run() {
            try {
                for (int i = 1; i <= 5; i++) {
                    String message = "Hello from " + name + " - Message " + i;
                    chatRoom.sendMessage(message);
                    Thread.sleep(100); // Simulate some work before sending the next message
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public class ChatMain {
        public static void main(String[] args) {
            ChatRoom chatRoom = new ChatRoom();

            Thread user1Thread = new Thread(new User("User 1", chatRoom));
            Thread user2Thread = new Thread(new User("User 2", chatRoom));

            user1Thread.start();
            user2Thread.start();

            try {
                user1Thread.join();
                user2Thread.join();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }