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:
- When inserting elements, check if the queue is full and wait if necesary.
- When removing elements, check if the queue is empty and wait if necesssary.
- Ensure proper notification logic to wake up waiting threads.
- 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.