November Black Friday C# week
Black friday is here! Get up to 80 % extra points for free! More info
Only this week up to 80 % off on C# courses. More info

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 :D 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:

  1. 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.
  2. 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 particular synchronized 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 :)


 

 

Activities (1)

 

 

Comments

To maintain the quality of discussion, we only allow registered members to comment. Sign in. If you're new, Sign up, it's free.

No one has commented yet - be the first!