Java Thread Synchronization

Introduction

Thread synchronization in Java is a mechanism that ensures that two or more concurrent threads do not simultaneously execute some particular program segment known as the critical section. Synchronization is essential for preventing thread interference and consistency problems when multiple threads access shared resources.

Key Points:

  • Critical Section: A segment of code that accesses shared resources and must not be executed by more than one thread at a time.
  • Thread Interference: Occurs when multiple threads modify shared data simultaneously, leading to inconsistent results.
  • Synchronization Mechanisms: Java provides various mechanisms for synchronizing threads, including synchronized methods, synchronized blocks, and higher-level concurrency utilities.

Table of Contents

  1. Synchronized Methods
  2. Synchronized Blocks
  3. Static Synchronization
  4. Lock Interface
  5. ReentrantLock
  6. Inter-Thread Communication
  7. Example: Comprehensive Usage of Synchronization
  8. Best Practices
  9. Conclusion

1. Synchronized() Methods

A synchronized method in Java is a method that can be accessed by only one thread at a time for a particular instance.

Example:

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();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + counter.getCount());  // Expected output: 2000
    }
}

2. Synchronized Blocks

A synchronized block is a block of code that is synchronized on a particular object. It allows more fine-grained control over synchronization compared to synchronized methods.

Example:

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();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + counter.getCount());  // Expected output: 2000
    }
}

3. Static Synchronization

Static synchronization is used to synchronize static methods. When a static method is synchronized, the lock is on the class’s Class object, not on an instance of the class.

Example:

class Counter {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }

    public static int getCount() {
        return count;
    }
}

public class StaticSynchronizationExample {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                Counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                Counter.increment();
            }
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + Counter.getCount());  // Expected output: 2000
    }
}

4. Lock Interface

The Lock interface provides more extensive locking operations than can be obtained using synchronized methods and statements. It provides the ability to lock, try to lock, and unlock.

Example:

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();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

public class LockExample {
    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + counter.getCount());  // Expected output: 2000
    }
}

5. ReentrantLock

ReentrantLock is a concrete implementation of the Lock interface. It offers the same basic functionality as the implicit locks used by synchronized methods and statements but with extended capabilities.

Example:

import java.util.concurrent.locks.ReentrantLock;

class Counter {
    private int count = 0;
    private ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

public class ReentrantLockExample {
    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + counter.getCount());  // Expected output: 2000
    }
}

6. Inter-Thread Communication

Inter-thread communication in Java is facilitated through methods like wait(), notify(), and notifyAll().

Example:

class SharedResource {
    private int value = 0;
    private boolean available = false;

    public synchronized void produce(int value) throws InterruptedException {
        while (available) {
            wait();
        }
        this.value = value;
        available = true;
        notify();
    }

    public synchronized int consume() throws InterruptedException {
        while (!available) {
            wait();
        }
        available = false;
        notify();
        return value;
    }
}

public class InterThreadCommunicationExample {
    public static void main(String[] args) {
        SharedResource sharedResource = new SharedResource();

        Thread producer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    sharedResource.produce(i);
                    System.out.println("Produced: " + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread consumer = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    int value = sharedResource.consume();
                    System.out.println("Consumed: " + value);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        producer.start();
        consumer.start();
    }
}

7. Example: Comprehensive Usage of Synchronization

Example: Multi-Threaded Counter with Synchronization

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();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}

public class ComprehensiveSynchronizationExample {
    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Count: " + counter.getCount());  // Expected output: 2000
    }
}

Output:

Count: 2000

8. Best Practices

  1. Minimize Synchronized Blocks: Keep synchronized blocks as short as possible to reduce contention and improve performance.
  2. Use Atomic Variables: For simple atomic operations, use classes from java.util.concurrent.atomic (e.g., AtomicInteger, AtomicBoolean).
  3. Prefer High-Level Concurrency Utilities: Use high-level concurrency utilities from the java.util.concurrent package for complex synchronization needs.
  4. Avoid Nested Locks: Avoid nested locks to prevent deadlock situations.
  5. Use try-finally for Locking: Always use try-finally blocks when working with explicit locks to ensure that locks are released properly.
  6. Understand the Cost of Synchronization: Synchronization adds overhead, so use it judiciously to balance correctness and performance.
  7. Document Synchronization Policies: Clearly document your synchronization policies to make the code easier to understand and maintain.

9. Conclusion

Thread synchronization in Java is crucial for preventing thread interference and ensuring consistency when multiple threads access shared resources. Java provides several synchronization mechanisms, including synchronized methods, synchronized blocks, the Lock interface, and inter-thread communication methods. By following best practices and leveraging high-level concurrency utilities, you can write efficient, thread-safe, and maintainable multithreaded applications.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top