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
.
To 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.
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:
Many 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
GO TO FULL VERSION