1. Concurrent Programming with Threads
Programs often utilize threads as a programming aid, and operating systems provide them as a feature. If you are using Windows and check the task manager, you’ll see that there are roughly 3000 running threads. In this chapter, we aim to increase this number further to execute our own threads. However, the exercises should not solely focus on creating threads; concurrency requires careful attention to ensure proper access to shared resources, necessitating coordination among the threads.
Prerequisites
be able to use
main
threadbe able to create own threads, know the difference between
Thread
andRunnable
be able to put threads to sleep
be able to interrupt threads
be able to submit tasks to the thread pool for processing
be able to use concurrent operations with returns
understand the difference between
Runnable
andCallable
synchronize threads with
Lock
objectsbe able to notify other threads with
Condition
objectsknow synchronization helpers like
Semaphore
Data types used in this chapter:
1.1. Create threads
When the JVM starts, it creates a thread named main
. This thread executes the main(…)
method, and it has executed our programs in all previous exercises. We want to change that in the next exercises. We want to create more threads and let them execute program code.
1.1.1. Create threads for waving and flag waving ⭐
There is a parade in honor of Captain CiaoCiao. He stands at the ramp of his ship, waves with one hand, and waves a flag with the other.
Exercise:
Threads always execute code of type
Runnable
in Java.Runnable
is a functional interface, and there are two ways to implement functional interfaces in Java: classes and lambda expressions. Write two implementations ofRunnable
; one using a class, one using a lambda expression.Put a loop with fifty repetitions in both
Runnable
implementations. OneRunnable
should output "wink" on the screen, the output of the other one should be "wave flag".Create a
Thread
object, and pass theRunnable
. Then start the threads. Do not start fifty threads, but only two!
Extension: The run()
method of each thread should contain the statement System.out.println(Thread.currentThread());
. What will be displayed?
Suppose Captain CiaoCiao has a few more arms to wave. How many threads can be created until the system comes to a standstill? Observe the memory usage in the Windows Task Manager (kbd: [Ctrl+Alt+Del]). Can you estimate how much a thread "costs"?
1.1.2. No more waving flags: End threads ⭐
Threads can be killed with stop()
— the method has been deprecated
for decades, but will probably never be deleted — or kindly asked to terminate itself with interrupt()
. But for this, the thread has to play along and check with isInterrupted()
whether such a termination request exists.
Captain CiaoCiao is still standing on the ship, winking and waving flags. When things get serious, he must stop this amusement for the people.
Exercise:
Write a program with two
Runnable
implementations that in principle wink and wave flags indefinitely, unless there is an interruption. Therun()
method should therefore useThread.currentThread().isInterrupted()
to test whether there was an interruption, and then exit the loop.Build a delay into the loop. Copy the following code:
try { Thread.sleep( 2000 ); } catch ( InterruptedException e ) { Thread.currentThread().interrupt(); }
The main program should respond to input with
JOptionPane.showInputDialog(String)
so that the commandsendw
stop winking andendf
stop flag-waving.
1.1.3. Parameterize Runnable ⭐⭐
A glance at the following code shows that the two Runnable
implementations are very similar, differing only in the screen output:
// Runnable 1
class Wink implements Runnable {
@Override public void run() {
for ( int i = 0; i < 50; i++ )
System.out.printf( "Wink; %s%n", Thread.currentThread() );
// ^^^^
}
}
Runnable winker = new Wink();
// Runnable 2
Runnable flagWaver = () -> {
for ( int i = 0; i < 50; i++ )
System.out.printf( "Wave flag; %s%n", Thread.currentThread() );
// ^^^^^^^^^
};
Code duplication is rarely good, though, this should be changed.
Exercise:
Figure out how to pass data to a
Runnable
.Implement a parameterized
Runnable
, so that in the above loop one canthe screen outputs and
the number of repetitions.
Rewrite the wink-and-wave program so that the parameterized
Runnable
is passed to a thread for execution.
1.2. Execute and idle
A thread can be in several states, these include running, waiting, sleeping, blocked, or terminated. In the previous exercises, we started the thread so that it is in the running state, and we ended the run()
method by completing the loop, which also ends the thread. In this section, the exercises are about the sleeping state, and in the section "Protecting Critical Sections", and "Thread Cooperation and Synchronization Helpers", there are exercises about the waiting/blocked states.
1.2.1. Delay execution by sleeping threads. ⭐⭐
The sleep program (http://man7.org/linux/man-pages/man1/sleep.1.html), known on Unix, can be invoked from the command line and then sleeps for a while, thus delaying subsequent programs in scripts.
Exercise:
Re-implement the sleep program in Java so that one can write comparable to the example on the command line:
$ *java Sleep 22
The Java program should then sleep for 22 seconds and if there are subsequent program invocations in a script, for example, they will be delayed.
The Java program should be able to be given the sleep time in various formats on the command line. If only an integer is passed, then the waiting time in seconds. Suffixes behind the integer should be allowed for different durations:
s
for seconds (default)m
for minutesh
for hoursd
for days
If more than one value is passed, they are summed up to give the total waiting time.
Various things can go wrong with the call, for example, if no number is passed or the number is too large. Check if the values, ranges, and suffixes are correct. Optional: In case of an error, exit the program with an individual exit code via
System.exit(int)
.
Example:
Valid call examples:
$ java Sleep 1m $ java Sleep 1m 2s $ java Sleep 1h 3h 999999s
Invalid calls leading to termination:
$ java Sleep $ java Sleep three $ java Sleep 1y $ java Sleep 999999999999999999
Tip: Structure the program so that the three essential parts are recognizable:
running the command line and analyzing the arguments
converting the units to seconds
the actual sleeping, for the accumulated seconds
1.2.2. Watch file changes by threads ⭐
After successful looting, all new treasures are systematically added to an inventory list. This is saved in a simple file. Bonny Brain wants to be notified when the file changes.
Exercise:
Write a class
FileChangeWatcher
with a constructorFileChangeWatcher(String filename)
.Implement
Runnable
.Output the filename, file size and
Files.getLastModifiedTime(path)
at half second intervals.Check with
getLastModifiedTime(…)
if the file has changed. Print a message if the file changes. All this should happen endlessly, every new change should be reported.Extension: We now want to react more flexibly to changes. Therefore, we want to be able to pass a
java.util.function.Consumer
object in the constructor. The consumer should be stored byFileChangeWatcher
and always call theaccept(Path)
method when something changes. This way, we can register an object that will be informed when a file change occurs.
1.2.3. Catch Exceptions ⭐
The distinction between checked exceptions and unchecked exceptions is important because if unchecked exceptions are not caught, this can escalate to the point where they end up at the executing thread, which is then terminated by the virtual machine. This is done automatically by the runtime environment, and we kindly get a message on the standard error channel, but we can’t revive the thread anymore.
On a local thread or globally for all threads, an UncaughtExceptionHandler
can be installed, which is informed when an exception terminates the thread. It can be used in four scenarios:
An
UncaughtExceptionHandler
can be set on an individual thread. Whenever this thread gets an unhandled exception, the thread is terminated and the setUncaughtExceptionHandler
is informed.An
UncaughtExceptionHandler
can be set on a thread group.An
UncaughtExceptionHandler
can be set globally for all threads.The
main
thread is special in the sense that the JVM automatically creates it and executes the main program. Of course, there can be unchecked exceptions in themain
thread as well, which can be reported by anUncaughtExceptionHandler
. However, there is an interesting feature to this: At themain(…)
method there can bethrows
, and checked exceptions can thus go back to the JVM. In case of a checked exception, a setUncaughtExceptionHandler
is also notified.
The processing takes place in a cascade: If there is an unchecked exception, the JVM first looks to see if an UncaughtExceptionHandler
is set on the individual thread. If not, it looks for an UncaughtExceptionHandler
in the thread group and then looks for a global handler to inform.
Exercise:
Run a thread that is terminated by division by
0
. Use your own globalUncaughtExceptionHandler
to document this exception.Start a second thread that has a local
UncaughtExceptionHandler
that ignores the exception, so no message appears either.If the
main(…)
method saysthrows Exception
and the body saysnew URL("captain")
, is the globalUncaughtExceptionHandler
also called?
1.3. Thread pools and results
It is not always the best way for Java developers to create threads themselves and associate these with program code; it is often more sensible to separate the program code from the physical execution. This is done in Java by an Executor
. This makes it possible to separate the program code from the current thread, and also to use the same thread several times for different program codes.
In the Java library, there are three central Executor
implementations: ThreadPoolExecutor
, ScheduledThreadPoolExecutor
, and ForkJoinPool
. The ThreadPoolExecutor
and ForkJoinPool
types implement thread pools that manage a collection of existing threads so that exercises can be passed to existing free threads.
Each code execution in the background is realized by a thread in Java, which is either created and started by yourself or indirectly used by an Executor
or internal thread pool. There are two important interfaces to encapsulate concurrent code: Runnable
and Callable
. Runnable
is passed directly to the Thread
constructor, a Callable
cannot be passed to Thread
; for Callable
you need an Executor
. A Callable
also returns a result, as does a Supplier
, but it has no parameter to pass. With a Runnable
nothing can be returned and also not passed. The run()
method does not throw an exception, call()
has throws Exception
in the method signature, so it can pass on any exceptions.
UML diagram of the Runnable
and Callable
interfaces
So far, we have always built threads ourselves and used only Runnable
. The following exercises will be about thread pools and also about Callable
.
1.3.1. Using thread pools ⭐⭐
Easter is coming up, and Bonny Brain goes to an orphanage dressed as a Wookiee with her crew members to deliver gifts.
Exercise:
Create an
ExecutorService
withExecutors.newCachedThreadPool()
, which is the thread pool.Create a string array with presents.
Bonny Brain processes each gift in the
main
thread and passes it to a crew member, which is a thread in the thread pool, at 1 to 2 second intervals.The crew members are threads in the thread pool. They execute Bonny Brain’s commands to distribute a gift. It takes them between 1 and 4 seconds to do this.
The flow is as follows: The gift distribution is implemented by a
Runnable
, the actual action. A free thread from the thread pool (the crew member) is selected and executes theRunnable
. TheRunnable
needs a way to receive the gift from Bonny Brain.
1.3.2. Get last modification of web pages ⭐⭐
The following class implements a method that returns a timestamp in which a web page was last modified (the data may not be available, in which case the time is set to 1/1/1970). The server should send in zone time UTC ± 0.
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class WebChecker {
public static void main(String[] args) throws IOException {
ZonedDateTime urlLastModified = getLastModified(new URL("http://www.tutego.de/index.html"));
System.out.println(urlLastModified);
ZonedDateTime urlLastModified2 = getLastModified(new URL("https://en.wikipedia.org/wiki/Main_Page"));
System.out.println(urlLastModified2);
}
private static ZonedDateTime getLastModified(URL url) {
try {
HttpURLConnection con = (HttpURLConnection) url.openConnection();
long dateTime = con.getLastModified();
con.disconnect();
return ZonedDateTime.ofInstant( Instant.ofEpochMilli( dateTime ), ZoneId.of( "UTC" ) );
} catch ( IOException e ) {
throw new IllegalStateException(e);
}
}
}
Exercise:
Create a record
WebResourceLastModifiedCallable
with a record componentURL url
.Let
WebResourceLastModifiedCallable
implement theCallable<ZonedDateTime>
interface. Put the implementation ofgetLastModified(URL)
from the example into thecall()
method. Doescall()
need to catch the checked exception itself?Build
WebResourceLastModifiedCallable
objects, and let the thread pool execute them.Let the
Callable
execute once with no time limit.Give the
Callable
only one microsecond to execute; what is the result?
Optional: Convert the time since how many minutes relative to the current time the web page has changed.
1.4. Protect critical sections
When multiple parts of a program run simultaneously, they may access shared resources or memory areas. To avoid faulty states, these accesses must be synchronized, allowing one thread to finish its work before another thread accesses the same resource. Failing to coordinate access to shared resources can lead to errors.
Programs must protect critical sections, so that only one other thread may be in a section. Java provides two mechanisms :
the keyword `synchronized
Lock
objects
synchronized
is a convenient keyword, but limited in capability. The Java Concurrency Utilities provide more powerful data types. For "locking" exclusively executed program parts, the interface java.util.concurrent.locks.Lock
and various implementations exist, such as ReentrantLock
, ReentrantReadWriteLock.ReadLock
, ReentrantReadWriteLock.WriteLock
.
Lock
and ReentrantLock
types.1.4.1. Writing memories into a poetry album ⭐
Once a cargo ship carrying valuable nepenthe has changed hands successfully, the pirates are chronicling their memories in a poetry album. The Captain CiaoCiao later adorns the album with stickers.
Given is the following program code in the main(…)
method of a class:
class FriendshipBook {
private final StringBuilder text = new StringBuilder();
public void appendChar( char character ) {
text.append( character );
}
public void appendDivider() {
text.append(
"\n_,.-'~'-.,__,.-'~'-.,__,.-'~'-.,__,.-'~'-.,__,.-'~'-.,_\n" );
}
@Override public String toString() {
return text.toString();
}
}
class Autor implements Runnable {
private final String text;
private final FriendshipBook book;
public Autor( String text, FriendshipBook book ) {
this.text = text;
this.book = book;
}
@Override public void run() {
for ( int i = 0; i < text.length(); i++ ) {
book.appendChar( text.charAt( i ) );
try { Thread.sleep( 1 ); }
catch ( InterruptedException e ) { /* Ignore */ }
}
book.appendDivider();
}
}
FriendshipBook book = new FriendshipBook();
String q1 = "The flowers need sunshine and " +
"I need Captain CiaoCiao to be happy";
new Thread( new Author( q1, book ) ).start();
String q2 = "When you laugh, they all laugh. " +
"When you cry, you cry alone.";
new Thread( new Author( q2, book ) ).start();
TimeUnit.SECONDS.sleep( 1 );
System.out.println( book );
Exercise:
Before running the program, figure out the expected result.
Put the code in its own class and
main(…)
method, and check the assumption.The flaw in the code is the unrestrained access to the
FriendshipBook
. Improve the program with aLock
object so that theFriendshipBook
can only be written to by one pirate at a time.
1.5. Thread cooperation and synchronization helper
It is important to synchronize program code so that two threads do not overwrite each other’s data. We’ve learned that this can be done with Lock
objects. However, Lock
objects only lock a critical area, and the runtime environment will automatically make a thread wait when a critical area is locked. An enhancement of this is to have a thread — or multiple threads — not only wait for entry into a critical section, but be informed via signals that it has something to do. Java provides different synchronization helpers with the internal states that cause other threads to wait or start executing when certain conditions are met.
Semaphore
: Whereas alock
only allows a single thread to be in a critical section, aSemaphore
allows a user-defined number of threads to be in a block. The method names are also slightly different:Lock
declares thelock()
method andSemaphore
declares theacquire()
method. Ifacquire()
reaches the maximum number, a thread must wait for access, just as with alock
. A semaphore with a maximum count of 1 is like aLock
.Condition
: With aCondition
a thread can wait and be woken up again by another thread. UsingCondition
objects, consumer-producer relationships can be programmed, but in practice, there is little need for this data type because there are Java types based on it, which are often simpler and more flexible.Condition
is an interface, and theLock
object provides factory methods that returnCondition
instances.CountDownLatch
: Objects of typeCountDownLatch
are initialized with an integer, and various threads count down thisCountDownLatch
, which puts them in a waiting state. Finally, when theCountDownLatch
reaches 0, all threads are released. ACountDownLatch
is a way to bring different threads together at a common point. Once aCountDownLatch
is consumed, it cannot be reset.CyclicBarrier
: The class is an implementation of a so-called barrier. With a barrier, several threads can meet at one point. If, e.g., work orders are parallelized and have to be rejoined later, this can be realized by a barrier. After all, threads have met this barrier, they continue to run. The constructor ofCyclicBarrier
can be passed aRunnable
, which is called at the time of the coincidence. Unlike aCountDownLatch
, aCyclicBarrier
can be reset and reused.Exchanger
: Producer-consumer relationships occur frequently in programs, and the producer transmits data to the consumer. Exactly for this case, there is the classExchanger
. This allows two threads to meet at a point and exchange data.
1.5.1. Attending the Banquet with the Captains — Semaphore ⭐⭐
Bonny Brain and Captain CiaoCiao are planning a banquet with many companions. They both sit at a table with 6 seats and receive different guests. The guests come, stay a little, tell and eat, and leave the table again.
Exercise:
Create a
Semaphore
with as many seats as there can be guests at the table with the captains at the same time.Model a guest as record
Guest
, which implementsRunnable
. All guests have a name.Guests are waiting for a seat. It does not have to be
fair
, so the guest who has been waiting for the longest is not necessarily next to the table.The program should do a screen output for a guest who would like to come to the table, for a guest who has been seated, and for a guest who is leaving the table.
1.5.2. Swearing and insulting — Condition ⭐⭐
Pirates don’t duel with cutlasses these days, they duel with curses.
Exercise:
Start two threads, each representing two pirates; give each thread a name.
A random pirate starts cursing and gets an endless insult contest going.
The curses should be random from a given collection of curses.
Before the actual curse, a pirate may take a "pause for thought" of up to one second.
1.5.3. Take pens out of paintbox — Condition ⭐⭐
In kindergarten, the little pirates regularly get together and paint pictures. Unfortunately, there is only one box with 12 pencils. When one child has taken pens from the box, another child has to wait whenever he wants more pens than are available in the box.
The scenario can be well implemented with threads.
Exercise:
Create a class
Paintbox
. This class should get a constructor and accept the maximum number of free pens.In the class
Paintbox
place a methodacquirePens(int numberOfPens)
, by which the children can request several pens. The requested number of pens may be greater than the available number, in which case the method shall block until the number of requested pens is available again.The class
Paintbox
additionally has the methodreleasePens(int numberOfPens)
for putting the pens back. This method signals that pens are available again.Create a class
Child
.Give the class
Child
a constructor so that each child has a name and can get a reference to a paintbox.The class
Child
shall implement the interfaceRunnable
. The method shall determine a random number between 1 and 10, representing the number of pencils desired. Then the child requests this number of pens from the paintbox. The child uses the pencils for between 1 and 3 seconds and then puts all the pencils back into the paintbox — no more and no less. Subsequently, the child waits between 1 and 5 seconds and starts again, claiming a random number of pencils.
Painting can be started with the following children:
public static void main( String[] args ) {
Paintbox paintbox = new Paintbox( 12 );
ExecutorService executor = Executors.newCachedThreadPool();
executor.submit( new Child( "Mirjam", paintbox ) );
executor.submit( new Child( "Susanne", paintbox ) );
executor.submit( new Child( "Serena", paintbox ) );
executor.submit( new Child( "Elm", paintbox ) );
}
1.5.4. Play Rock, Paper, Scissors — CyclicBarrier ⭐⭐⭐
Rock, Paper, Scissors is an old game that was played as early as the 17th century. After a start signal, the two players form a shape for scissors, stone, or paper with one hand. Which player wins is determined by the following rules:
Scissors cuts the paper (scissors wins).
Paper wraps the stone (paper wins).
Rock blunts the scissors (rock wins).
So, each hand sign can win or lose.
We want to write a simulation for the game and take the following enumeration for hand signs as a base:
[[source,java]
enum HandSign { SCISSORS, ROCK, PAPER; static HandSign random() { return values()[ ThreadLocalRandom.current().nextInt( 3 ) ]; } int beats( HandSign other ) { return (this == other) ? 0 : (this == HandSign.ROCK && other == HandSign.SCISSORS || this == HandSign.PAPER && other == HandSign.ROCK || this == HandSign.SCISSORS && other == HandSign.PAPER) ? +1 : -1; } }
The enum
HandSign
declares three enumeration elements for scissors, stone, paper. The static method random()
returns a random hand sign. The beats(HandSign)
method is similar to a comparator
method: it compares the current hand sign with the passed hand sign and returns 0
if both hand signs are equal, +1
if the own hand sign is higher than the passed sign, and -1
otherwise.
Exercise:
Run a starter thread that triggers a snick-snack game every second. For repeated execution, a
ScheduledExecutorService
can be used.A player is represented by a
Runnable
that chooses a random hand signal and puts the choice into a data structure of typeArrayBlockingQueue
withadd(…)
.After a player picks a hand sign, the
await()
method is to be called on a previously constructedCyclicBarrier
.The constructor of
CyclicBarrier
shall get aRunnable
which determines the winner at the end of the game. TheRunnable
takes out of theArrayBlockingQueue
the two hand signs withpoll()
, compares them, and evaluates the winner and loser. At the first position of the data structure is player 1, at the second position player 2.
1.5.5. Find the fastest runner — CountDownLatch ⭐⭐
For the next heist, Bonny Brain needs fast runners. For this, she hosts a competition and lets the best runners compete. With a starting gun, Bonny Brain stands at the track, and everyone waits for the starting gun.
Exercise:
Create 10 threads that wait for Bonny Brain’s signal. Thereafter, the threads start and take between a freely chosen number of 10 and 20 seconds to run. In the end, the threads should write their time into a common data structure, so that the name of the thread (runner name) is noted with the runtime.
Bonny Brain starts the runners in the
main
thread and at the end outputs all run times sorted in ascending order with the runner names.
If several threads are to come together in one place, a
|