Singleton Pattern Variants and Blocking Queue Implementation

Singleton pattern is a standard approach that ensures a class has only one instance, preventing multiple instantiations.

Eager Initialization (Thread-Safe): The simplest singleton implementation where the instance is created when the class is loaded, ensuring thread safety.

public class Singleton {
    private static Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

Lazy Initialization (Not Thread-Safe): The instance is created only when it is first accessed, but this version is not thread-safe.

public class LazySingleton {
    private static volatile LazySingleton instance = null;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

Lazy Initialization (Thread-Safe): This version adds synchronization and double-checking to ensure thread safety.

public class LazySingleton {
    private static volatile LazySingleton instance = null;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

Blocking queue is a thread-safe data structure with the following characteristics:

  • When the queue is empty, dequeue operations block until an element is added.
  • When the queue is full, enqueue operations block until an element is removed.

Standard library blocking queue example:

BlockingQueue<String> queue = new ArrayBlockingQueue<String>(1000);
queue.put("abc");
String elem = queue.take();

Simulating a Blocking Queue

Implementing a blocking queue using a circular array:

  1. When inserting elements, check if the queue is full and wait if necesary.
  2. When removing elements, check if the queue is empty and wait if necesssary.
  3. Ensure proper notification logic to wake up waiting threads.
  4. Use a single lock for all operations.

Code implementation:

public class MyBlockingQueue {
    private String[] elements = null;
    private int head = 0;
    private int tail = 0;
    private int size = 0;
    private Object lock = new Object();

    public MyBlockingQueue(int capacity) {
        elements = new String[capacity];
    }

    public void put(String element) throws InterruptedException {
        synchronized (lock) {
            while (size >= elements.length) {
                lock.wait();
            }
            elements[tail++] = element;
            if (tail >= elements.length) {
                tail = 0;
            }
            size++;
            lock.notify();
        }
    }

    public String take() throws InterruptedException {
        String element = null;
        synchronized (lock) {
            while (size == 0) {
                lock.wait();
            }
            element = elements[head++];
            if (head >= elements.length) {
                head = 0;
            }
            size--;
            lock.notify();
        }
        return element;
    }

    public static void main(String[] args) throws InterruptedException {
        MyBlockingQueue queue = new MyBlockingQueue(1000);
        queue.put("aaa");
        queue.put("bbb");
        queue.put("ccc");
        queue.put("ddd");

        String elem = queue.take();
        System.out.println("elem=" + elem);
        elem = queue.take();
        System.out.println("elem=" + elem);
        elem = queue.take();
        System.out.println("elem=" + elem);
        elem = queue.take();
        System.out.println("elem=" + elem);
    }
}

Implementing Producer-Consumer Model with the Simulated Blocking Queue

Modify the main method as follows:

public static void main(String[] args) {
    MyBlockingQueue queue = new MyBlockingQueue(1000);

    Thread producer = new Thread(() -> {
        int n = 1;
        while (true) {
            try {
                queue.put(n + "");
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Produced element " + n);
            n++;
        }
    });

    Thread consumer = new Thread(() -> {
        while (true) {
            try {
                String n = queue.take();
                System.out.println("Consumed element " + n);
                // Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });

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

Adding a delay after production causes one element to be produced and consumed at a time. Adding a delay after consumption without a delay after production leads to a large number of elements being produced before the first is consumed.

Tags: Singleton Pattern blocking queue producer-consumer model Thread Safety Java Concurrency

Posted on Sat, 07 Nov 2026 16:48:51 +0000 by deth4uall