User Viacheslav
Viacheslav
Level 3
St. Petersburg

Better together: Java and the Thread class. Part VI — Fire away!

Published in the Java Developer group

Introduction

Threads are an interesting thing. In past reviews, we looked at some of the available tools for implementing multithreading. Let's see what other interesting things we can do. At this point, we know a lot. For example, from "Better together: Java and the Thread class. Part I — Threads of execution", we know that the Thread class represents a thread of execution. We know that a thread performs some task. If we want our tasks to be able to run, then we must mark the thread with Runnable. Better together: Java and the Thread class. Part VI — Fire away! - 1To remember, we can use the Tutorialspoint Online Java Compiler:

public static void main(String[] args){
	Runnable task = () -> {
 		Thread thread = Thread.currentThread();
		System.out.println("Hello from " + thread.getName());
	};
	Thread thread = new Thread(task);
	thread.start();
}
We also know that we have something called a lock. We learned about this in "Better together: Java and the Thread class. Part II — Synchronization. If one thread acquires a lock, then another thread trying to acquire the lock will be forced to wait for the lock to be released:

import java.util.concurrent.locks.*;

public class HelloWorld{
	public static void main(String []args){
		Lock lock = new ReentrantLock();
		Runnable task = () -> {
			lock.lock();
			Thread thread = Thread.currentThread();
			System.out.println("Hello from " + thread.getName());
			lock.unlock();
		};
		Thread thread = new Thread(task);
		thread.start();
	}
}
I think it's time to talk about what other interesting things we can do.

Semaphores

The simplest way to control how many threads can run simultaneously is a semaphore. It's like a railway signal. Green means proceed. Red means wait. Wait for what from the semaphore? Access. To get access, we must acquire it. And when access is no longer needed, we must give it away or release it. Let's see how this works. We need to import the java.util.concurrent.Semaphore class. Example:

public static void main(String[] args) throws InterruptedException {
	Semaphore semaphore = new Semaphore(0);
	Runnable task = () -> {
		try {
			semaphore.acquire();
			System.out.println("Finished");
			semaphore.release();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	};
	new Thread(task).start();
	Thread.sleep(5000);
	semaphore.release(1);
}
As you can see, these operations (acquire and release) help us understand how a semaphore works. The most important thing is that if we are to gain access, then the semaphore must have a positive number of permits. This count can be initialized to a negative number. And we can request (acquire) more than 1 permit.

CountDownLatch

The next mechanism is CountDownLatch. Unsurprisingly, this is a latch with a countdown. Here we need the appropriate import statement for the java.util.concurrent.CountDownLatch class. It's like a foot race, where everyone gathers at the starting line. And once everyone is ready, everyone receives the starting signal at the same time and starts simultaneously. Example:

public static void main(String[] args) {
	CountDownLatch countDownLatch = new CountDownLatch(3);
	Runnable task = () -> {
		try {
			countDownLatch.countDown();
			System.out.println("Countdown: " + countDownLatch.getCount());
			countDownLatch.await();
			System.out.println("Finished");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	};
	for (int i = 0; i < 3; i++) {
		new Thread(task).start();
 	}
}
First, we first tell the latch to countDown(). Google defines countdown as "an act of counting numerals in reverse order to zero". And then we tell the latch to await(), i.e. wait until the counter becomes zero. Interestingly, this is a one-time counter. The Java documentation says, "When threads must repeatedly count down in this way, instead use a CyclicBarrier". In other words, if you need a reusable counter, you need a different option: CyclicBarrier.

CyclicBarrier

As the name implies, CyclicBarrier is a "re-usable" barrier. We will need to import the java.util.concurrent.CyclicBarrier class. Let's look at an example:

public static void main(String[] args) throws InterruptedException {
	Runnable action = () -> System.out.println("On your mark!");
	CyclicBarrier barrier = new CyclicBarrier(3, action);
	Runnable task = () -> {
		try {
			barrier.await();
			System.out.println("Finished");
		} catch (BrokenBarrierException | InterruptedException e) {
			e.printStackTrace();
		}
	};
	System.out.println("Limit: " + barrier.getParties());
	for (int i = 0; i < 3; i++) {
		new Thread(task).start();
	}
}
As you can see, the thread runs the await method, i.e. it waits. In this case, the barrier value decreases. The barrier is considered broken (barrier.isBroken()) when the countdown reaches zero. To reset the barrier, you need to call the reset() method, which CountDownLatch does not have.

Exchanger

The next mechanism is Exchanger. In this context, an Exchange is a synchronization point where things change be exchanged or swapped. As you would expect, an Exchanger is a class that performs an exchange or swap. Let's look at the simplest example:

public static void main(String[] args) {
	Exchanger<String> exchanger = new Exchanger<>();
	Runnable task = () -> {
		try {
			Thread thread = Thread.currentThread();
			String withThreadName = exchanger.exchange(thread.getName());
			System.out.println(thread.getName() + " exchanged with " + withThreadName);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	};
	new Thread(task).start();
	new Thread(task).start();
}
Here we start two threads. Each of them runs the exchange method and waits for the other thread to also run the exchange method. In doing so, the threads exchange the passed arguments. Interesting. Doesn't it remind you of something? It's reminiscent of SynchronousQueue, which lies at the heart of CachedThreadPool. For clarity, here's an example:

public static void main(String[] args) throws InterruptedException {
	SynchronousQueue<String> queue = new SynchronousQueue<>();
	Runnable task = () -> {
		try {
			System.out.println(queue.take());
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	};
	new Thread(task).start();
	queue.put("Message");
}
The example shows that when a new thread is started, it will wait, because the queue will be empty. And then the main thread puts the "Message" string into the queue. What's more, it will also stop until this string is received from the queue. You can also read "SynchronousQueue vs Exchanger" to find more about this topic.

Phaser

We've saved the best for last — Phaser. We will need to import the java.util.concurrent.Phaser class. Let's look at a simple example:

public static void main(String[] args) throws InterruptedException {
        Phaser phaser = new Phaser();
        // By calling the register method, we register the current (main) thread as a party
        phaser.register();
        System.out.println("Phasecount is " + phaser.getPhase());
        testPhaser(phaser);
        testPhaser(phaser);
        testPhaser(phaser);
        // After 3 seconds, we arrive at the barrier and deregister. Number of arrivals = number of registrations = start
        Thread.sleep(3000);
        phaser.arriveAndDeregister();
        System.out.println("Phasecount is " + phaser.getPhase());
    }

    private static void testPhaser(final Phaser phaser) {
        // We indicate that there will be a +1 party on the Phaser
        phaser.register();
        // Start a new thread
        new Thread(() -> {
            String name = Thread.currentThread().getName();
            System.out.println(name + " arrived");
            phaser.arriveAndAwaitAdvance(); // The threads register arrival at the phaser.
            System.out.println(name + " after passing barrier");
        }).start();
    }
The example illustrates that when using Phaser, the barrier breaks when the number of registrations matches the number of arrivals at the barrier. You can get more familiar with Phaser by reading this GeeksforGeeks article. Better together: Java and the Thread class. Part VI — Fire away! - 2

Summary

As you can see from these examples, there are various ways to synchronize threads. Earlier, I tried to recollect aspects of multithreading. I hope the previous installments in this series were useful. Some people say that the path to multithreading begins with the book "Java Concurrency in Practice". Although it was released in 2006, people say that the book is quite foundational and still relevant today. For example, you can read the discussion here: Is "Java Concurrency In Practice" still valid?. It is also useful to read the links in the discussion. For example, there is a link to the book The Well-Grounded Java Developer, and we'll make particular mention of Chapter 4. Modern concurrency. There is also an entire review about this topic: Is "Java Concurrency in Practice" Still Valid in the Era of Java 8? That article also offers suggestions about what else to read to truly understand this topic. After that, you could take a look a great book like OCA/OCP Java SE 8 Programmer Practice Tests. We're interested in the second acronym: OCP (Oracle Certified Professional). You'll find tests in "Chapter 20: Java Concurrency". This book has both questions and answers with explanations. For example: Better together: Java and the Thread class. Part VI — Fire away! - 3Many people might start saying that this question is yet another example of memorization of methods. On the one hand, yes. On the other hand, you could answer this question by recalling that ExecutorService is a kind of "upgrade" of Executor. And Executor is intended to simply hide the way threads are created, but it is not the main way to execute them, that is, start a Runnable object on a new thread. That's why there is no execute(Callable) — because in ExecutorService, the Executor simply adds submit() methods that can return a Future object. Of course, we can memorize a list of methods, but it's much easier to make our answer based on our knowledge of the nature of the classes themselves. And here are some additional materials on the topic: Better together: Java and the Thread class. Part I — Threads of execution Better together: Java and the Thread class. Part II — Synchronization Better together: Java and the Thread class. Part III — Interaction Better together: Java and the Thread class. Part IV — Callable, Future, and friends Better together: Java and the Thread class. Part V — Executor, ThreadPool, Fork/Join
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION