Lesson 2 - Multithreading v Javě - Daemon, join, and synchronized
In the previous lesson, Multithreading in Java, we gave a brief introduction to the Java thread model and learned the basics of working with threads. We also talked about the main thread. Maybe it wasn't quite clear why the main thread is actually the main thread.
This thread, known as the main thread, is especially important because it starts automatically when the Java program is running. Until other threads are created and running from the main thread, the main thread is actually synonymous with the program itself. The main thread does not have to be terminated last (although we usually do so).
Consider the MyThread
class and the main()
method
from the previous lesson:
class MyThread extends Thread { public MyThread(String name) { super(name); } @Override public void run() { System.out.println("Thread " + getName() + " is running"); for(int i = 0; i < 4; ++i) { System.out.println("Thread " + getName() + ": " + i); try { Thread.sleep(500); } catch (InterruptedException ex) { System.out.println("Thread " + getName() + " suspended"); return; } } System.out.println("Thread " + getName() + " terminated"); } } public static void main(String[] args) { System.out.println("Main thread is running"); MyThread myThread = new MyThread("Thread2"); myThread.start(); for(int i = 0; i < 4; ++i) { System.out.println("Main thread: " + i); try { Thread.sleep(750); } catch (InterruptedException ex) { System.out.println("My thread suspended"); return; } } System.out.println("My thread terminated"); }
In the main()
method, we replace Thread.sleep(750)
with Thread.sleep(200)
. Now, when we run the program, we find that
the second thread continued after the main thread ended. In general, the program
is running as long as at least one thread that is not marked as a daemon is
running. No matter the thread is the main one or not. This means that the number
of running daemon threads has no effect on program termination.
Daemon threads run in the background of the program similar to the **Garbage collection **. Running these threads makes sense only in the presence of other threads - that's why they are terminated automatically. A good example is a timer. Non-daemon threads are sometimes referred to as user threads.
When created, every thread has the value of the daemon
field set
to false
. This can be changed by the
setDaemon(boolean daemon)
instance method. But be careful, this
method can only be called before the thread is started. If this rule is broken,
the IllegalThreadStateException
is thrown.
So, before calling the start()
method, we'll call the following
code on the myThread
instance in the main()
method:
myThread.setDaemon(true);
If we run the program now, only part of the second thread is executed, and the termination message is never displayed. This is, of course, because the thread is marked as a daemon, and the program ends when the main thread ends.
Inter-thread Communication
Until now, we have simplified the thread scheduling situation by using the
Thread.sleep()
method. When a thread only performs trivial
operations and then waits for a long time, it is quite easy to predict how the
thread will run. In real applications, however, threads will perform different
calculations or wait for user inputs, and we won't be able to tell exactly how
long the thread will run.
Fortunately, there are a number of sophisticated methods that allow us to do
some kind of inter-thread communication. This is a more advanced topic related
to synchronization, so its place will be further in our series. However, we'll
now show two very practical methods - join()
and
isAlive()
.
isAlive()
The isAlive()
method is an instance method of the
Thread
class that returns true
if the thread on which
it's been called is still running. Otherwise, it returns false
.
Let's design a program so that the main thread, using the isAlive()
method, "waits" for the second thread:
public static void main(String[] args) throws InterruptedException { System.out.println("Main thread is running"); MyThread myThread = new MyThread("Thread2"); myThread.start(); while(myThread.isAlive()) { Thread.sleep(1); } System.out.println("Main thread terminated"); }
The MyThread
class remains unchanged. There's probably nothing
to explain. We should see the following output:
Console application
Main thread is running
Thread Thread2 is running
Thread Thread2: 0
Thread Thread2: 1
Thread Thread2: 2
Thread Thread2: 3
Thread Thread2 terminated
Main thread terminated
Not bad, the program works as it should. But we won't be satisfied with this
solution It wouldn't be
Java if it didn't offer us a better one. It's the second of these methods - the
join()
method.
join()
The join()
method is also provided by instances of the
Thread
class, but its use is more complex. It ensures that the
thread from which it's been called is waiting for the thread on which it's
called. The previous main()
method code could thus be reduced
to:
System.out.println("Main thread is running"); MyThread myThread = new MyThread("Thread2"); myThread.start(); myThread.join(); System.out.println("Main thread terminated");
When we run the app, identical output is displayed.
Synchronization
An extensive topic of multithreading is synchronization. It's actually a way of ensuring that only one thread can access the resource at a time. Let's consider a situation where multiple threads access a complex structure - such as a collection. In this case, there must be a way to prevent the threads from "getting into each other's work". Let's suppose that one thread will go through the collection elements and print them. At the same time, however, the second thread will produce and add new elements to the collection. What exactly would happen depends on the particular type of the collection, but it's certain that the procedure would not lead to the expected result. What's worse, the result wouldn't even be the same every time we run the app, so we wouldn't be able to predict it.
Let's try to create a similar example. Several threads will write some text at the same time. Let's change our code:
public static void main(String[] args) throws InterruptedException { MyThread t1 = new MyThread("Greetings"); MyThread t2 = new MyThread("Hello world"); MyThread t3 = new MyThread("End"); t1.start(); t2.start(); t3.start(); } static class MyThread extends Thread { private final String message; public MyThread(String message) { this.message = message; } @Override public void run() { int position = 0; while(position < message.length()) { System.out.print(message.charAt(position++)); try { Thread.sleep(1); } catch (InterruptedException ex) { System.out.println("Thread with the message \"" + message + "\" terminated"); return; } } } }
In this example, several threads print specified text simultaneously by
sending one character after another to the console. In the run()
method loop, we also call Thread.sleep(1)
- which simulates a more
time-consuming operation than just printing a character. We already know that
the output will always be different and that the word list will be mixed up. For
me it looked like this:
Console application
GHKreoeelnloeetc wingorld
If you wish, try removing the line putting the thread to sleep from the
run()
method. The output will be most likely still mixed, though
not so much. This is because the thread not being put to sleep can write
multiple characters during a single context switch.
But we'll now look at how to achieve unmixed output. We won't use the
finished solution in the form of the println()
method which must be
synchronized somehow as well
If you have read carefully now, you should be able to create solutions using the
Thread.sleep()
, isAlive()
, or join()
methods. However, these solutions would probably be inefficient and
unnecessarily complex and unclear. For this reason, Java, as usual, offers us a
comprehensive solution to the problem - synchronization.
A very loose comparison
Let's imagine that our threads are first-grade kids sitting in a circle and talking about what they experienced over the weekend. Their teacher is a despot and has determined that only one who has a particular stone in their hand can speak. So one child talks and all the others are silent. When one finishes talking, they pass the stone to the child on the left (or right - it doesn't matter, but I try to be as specific as possible), they get permission to talk and so they do. This is repeated until all the children say what they want.
A multi-threaded application using synchronization works the same way with minor differences. The stone is called a monitor and can be owned by only one thread at a time. In practice, the monitor is just any other object.
The whole synchronization is then realized in two ways:
- By specifying the
synchronized
keyword in the method header declaration. Then the object with this method is the monitor. In practice, this means that only one synchronized method can be executed on the object at a time. - By creating a custom
synchronized
block and specifying an external monitor. Then the code behaves in much the same way as the synchronized method. This means that a particularsynchronized
block can only execute one thread at a time. The advantage is greater variability (we choose our own monitor, we can introduce the block anywhere), the disadvantage of the higher level of complexity.
The synchronized
block looks like this:
synchronized(monitor) { // Synchronized commands }
In both cases, among other things, we make several commands an indivisible (atomic) operation. This also means that if the thread having the monitor waits, it will delay all other threads waiting for the monitor.
If a thread encounters a synchronized
block but the monitor
isn't free, it's blocked and queued for the monitor.
If something is still unclear to you, don't mind. This is because we'll look at synchronization in more detail the next lesson, Multithreading in Java - Synchronization in practice