Java SynchronousQueue with Example

Java SynchronousQueue is a specific type of BlockingQueue with no internal capacity and is primarily used in exchanging data between two threads similar to the producer-consumer technique. This article discusses the basics of SynchronousQueue and its main features with practical examples.

1. Introduction

As we know, a Queue is a linear data structure that stores elements in FIFO (First In, First Out) order. Each Queue has two ends: front and rear. New elements are added to the rear and removed from the front. Java Queue interface and its implementation classes, such as TransferQueue, BlockingQueue, LinkedList and PriorityQueue, use the queue data structure.

SynchronousQueue is a special kind of BlockingQueue.

  • It has zero or no internal capacity and will block until another Thread is ready to take the element on the other end.
  • It is mainly used in exchanging data between threads in a thread-safe manner. It is synchronous in the way that it passes the data synchronously to other threads and waits for the threads to take the data instead of just putting the data in the queue and returning what other queues do. This means that each insert operation must wait for another thread’s corresponding removal operation and vice versa.
  • It won’t allow NULL elements; adding a null value will raise NullPointerException.
  • We cannot iterate over a SynchronousQueue as there is nothing to iterate.
  • We cannot peek at a SynchronousQueue because the element is only present when we try to remove it and hence peek() always returns null.

Since SynchronousQueue has no capacity, it acts like an empty collection and other queue methods will show special behavior for SynchronousQueue.

  • isEmpty(): always returns true.
  • iterator(): returns an empty iterator in which hasNext() always returns false.
  • peek(): always returns null.
  • size(): always return zero.

We can create SynchronousQueue object by using the following constructors:

  • SynchronousQueue(): creates an instance with a non-fair access policy i.e. if multiple threads are waiting, they will be granted access in an unspecified order.
  • SynchronousQueue(boolean fair): creates an instance with a fairness access policy i.e. the waiting threads will be granted access in FIFO (First-In First-Out) order.
BlockingQueue<String> syncQueue = new SynchronousQueue<>();

BlockingQueue<String> syncQueue = new SynchronousQueue<>(true);

2. Working with SynchronousQueue

SynchronousQueue supports two primary operations put() and take(), and both of these are blocking.

  • put(): To add an element to the queue, but the Thread calling this method will block or wait for another Thread to receive this element by using take() method.
try {
    syncQueue.put("Element");
} catch (InterruptedException ie) {
    ie.printStackTrace();
}
  • take(): To retrieve and remove an element from the queue. The Thread calling this method will wait for another Thread to insert an element.
try {
    String element = syncQueue.take();
} catch (InterruptedException ie) {
    ie.printStackTrace();
}

3. Solving Producer-Consumer Problem using SynchronousQueue 

SynchronousQueue is designed to transfer objects from producer threads to consumer threads synchronously.

  • When the producer thread puts an object in the queue, it must wait for a consumer thread to take it.
  • When the consumer thread wants to take an element from the queue, it must wait for a put from the producer thread.

One possible solution is to solve the producer-consumer problem using BlockingQueue which provides a buffer to store data and controls the flow by blocking the producer thread if the buffer is full and blocking the consumer thread if the buffer is empty.

But with SynchronousQueue, elements are transferred from the producer to the consumers without being held in the queue, unlike BlockingQueue in which the elements are put, waiting to be taken by consumer threads. So if we want producer-consumer to work synchronously instead of asynchronous communication, then we can use SynchronousQueue to solve this particular use case.

Let’s now create a producer thread publishing an event to the queue and a consumer thread consuming that event from the queue.

BlockingQueue<String> syncQueue = new SynchronousQueue<>();

Runnable producer = () -> {
			try {
				String event = "Event-1";
				syncQueue.put(event);
				System.out.println("Event Published : " + event);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		};

Runnable consumer = () -> {
			try {
				String event = syncQueue.take();
				System.out.println("Event Consumed : " + event);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		};

Start both threads and watch out for the program output.

// Producer Thread creation
Thread producerThread = new Thread(producer);

// Consumer Thread creation
Thread consumerThread = new Thread(consumer);

// Starting Producer & Consumer Threads
producerThread.start();
consumerThread.start();

The program output.

Event Published : Event-1
Event Consumed : Event-1

If we notice the program output, we won’t see any difference from a traditional producer-consumer problem. In order to understand how SynchronousQueue works, we need to do the following actions,

  • Just start the producer thread by commenting line consumerThread.start(); If the consumer thread is not running then the producer will block at the syncQueue.put(event); call, and we won’t see ‘Event Published : Event-1′ message on the console.
  • Similarly, just start the consumer thread by commenting line producerThread.start(); and see the consumer thread blocks at take() method and won’t consume anything from the queue.

This happens because of the special behavior of SynchronousQueue, which guarantees that the thread inserting data will block until there is a thread to remove that data or vice-versa.

4. When to Use?

SynchronousQueue is similar to rendezvous channels and is best suited for hand-off designs, in which an object running in one thread must sync up with an object running in another thread in order to hand out some information, event, or task.

  • If we want to safely share a variable between two threads, we can put that variable in SynchronousQueue and let the other thread take it from the queue.
  • SynchronousQueue is also used in CachedThreadPool to achieve an effect of unlimited (Integer.MAX) threads creation as tasks arrive. CachedThreadPool will have the coreSize as 0 and maxPoolSize as Integer.MAX with SynchronousQueue.
  • In producer-consumer-based scenarios as well, SynchronousQueue is a choice when there are enough consumer threads, so the producer thread doesn’t have to enqueue elements.

5. Conclusion

We have learned about Java SynchronousQueue, its main features and its use with practical examples. We have also seen how to solve a Producer-Consumer problem using it, and the various scenarios where we can use it in our code.

Happy Learning !!

Comments

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.