Lesson 1 - Multithreading in Java
In this article, we'll introduce multithreading in Java. I'll assume that you haven't used multithreading yet so I can say that all of your previous programs were running linearly. By that I mean command after command. There was only one command being executed at a time, and that command had to be completed in order to execute another. We say that only a single thread is running at a time (see further). Maybe you didn't know about the thread, but it's there and its entry point is the infamous `main () 'method. This approach is the simplest, but often not the best.
Imagine a situation where one thread is, for example, waiting for a user input. In the case of the single-threaded model, the entire program is waiting! And because users are generally slower than our program, there's an unpleasant waste of CPU time. Moreover, our program is very unstable. If anything happens to our thread (it crashes or gets blocked), the entire program will be affected again. There may also be times when we want to suspend a thread for a certain period of time. It's definitely not pleasant when we have to suspend the whole program because of that.
We can solve all these problems by... yes, you guessed it - multithreading.
Multithreading
A multithreaded program consists of two or more parts (threads), and although it may seem difficult at the start, it's quite easy to create a multithreaded application with Java's sophisticated support. We can say that a thread is a sort of self-executing sequence of commands. We can create threads freely (= define the sequence of commands) and manage them.
Creating a thread
There are practically 2 ways to create a thread:
- By inheriting from the
Thread
class - by implementing the
Runnable
interface
These two approaches are equal, and it's up to each programmer to choose between them. In this article we'll show both of them.
The Runnable Interface
The Runnable
interface is a functional interface. This is a new
syntax of Java 8 and it's nothing but an interface with one abstract method.
We'll show its benefits later. However, for the time being, the abstract
run()
method that defines this interface will be more important to
us. This method is common to both of the above principles, and it represents the
sequence of commands that the thread executes later.
The Thread Class
The Thread
class implements the Runnable
interface
and will now be very important to us because it represents the thread itself. It
defines a lot of constructors, but for now only 4 will be important:
public Thread() public Thread(String name) public Thread(Runnable target) public Thread(Runnable target, String name)
As you can see, we can define the thread's name. This is a very useful thing
that can help us debug our programs. This name can then be easily changed using
the setName(String name)
method, or retrieved by the
getName()
method.
We'll use the other two constructors to create a thread by implementing
Runnable
. First, we'll create a Runnable
object,
implement the run()
method, and pass this object in the constructor
when creating the thread. The thread will store the passed object and call the
object's run()
method automatically when its run()
method is called.
An important method of this class is the start()
method, which
is a kind of thread entry point. This method performs preparatory work and then
calls the run()
method.
Extending the Thread class
Finally, we're going to create a new thread. Let's create a new project and
name it as you want. Now we'll create a MyThread
class:
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"); } }
The class inherits from the Thread
class and overrides its
run()
method. The only new thing here is the sleep()
method:
public static void sleep(long millis) throws InterruptedException
, which is declared by the Thread
class and that suspends the
thread from which it's been called for the time specified by the
millis
argument. The time is entered in milliseconds, which is a
thousandth of a second. Now let's look at main()
:
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("Main thread suspended"); return; } } System.out.println("Main thread terminated"); }
Now when we run the program, the output will look something like this:
Console application
Main thread is running
Main thread: 0
Thread Thread2 is running
Thread Thread2: 0
Thread Thread2: 1
Main thread: 1
Thread Thread2: 2
Main thread: 2
Thread Thread2: 3
Thread Thread2 terminated
Main thread: 3
Main thread terminated
However, this output may also vary. This is, of course, because the threads do not always run the same way. One may be faster, or better said, getting more processor time than another and this may change next time we run the program. This is what makes multithreading so unpredictable - that you never know how the threads will switch between each other. This process is called context switching and is handled by the operating system itself.
Context Switching
If two or more threads share a single processor (or, more precisely, a single
core), this processor must switch between them while executing them. As I've
already mentioned, this switching is handled by the operating system.
Fortunately, we can also influence it ourselves explicitly. We use the thread
priority to decide which thread will be allowed to execute (to which the
processor time will be allocated). Each thread has an assigned priority
represented by a number from 1
to 10
. The default
value is 5
. We can set the priority using the
setPriority()
method or get it using the getPriority()
method.
We can say that a higher-priority thread is executed prior to a
lower-priority thread and it can force to suspend the lower-priority thread
anytime (the stronger one survives ). But as I said, switching is handled by the operating system and
each OS can handle threads and their priority differently. Therefore, you should
not rely solely on automatic switching and try to care about it a little bit.
For example, it's important to ensure that threads of the same priority
suspended themselves from time to time. We can cause this elegantly by the
static yield()
method on the Thread
class, which
"takes" control of the currently running thread and passes it to the waiting
thread with the highest priority.
Implementing the Runnable Interface
The second way to create a thread is to implement Runnable
. As I
said, this interface is a functional interface and therefore contains only one
abstract method. In this case, of course, this is the run()
method.
Let's go over this Java 8 syntax quickly.
Functional interfaces and lambda expressions
The functional interface is a news of Java 8 and it's an interface that has
only one abstract method. It should also be annotated with
@FunctionalInterface
for clarity.
For example, let's consider we want to create a Comparator
object. In older Java versions we'd have to proceed as if creating an abstract
class:
Comparator<String> com = new Comparator<String>() { @Override public int compare(String a, String b) { return b.compareTo(a); } };
You have to admit that it isn't very nice to write that much code for such a simple operation. Why do we actually have to specify which method we override when the interface has only one? Fortunately, this is no longer necessary. Thus, from Java 8, we can write the same thing using a lambda expression:
Comparator<String> com = (String a, String b) -> { return b.compareTo(a); };
We simply specify the parameters of the abstract method, the
->
operator, and the code block (the implementation of the
abstract method). However, if there is only one statement in this block, we can
omit both braces and return
. It's also possible to omit the
parameters' data type. The same code can look like this:
Comparator<String> com = (a, b) -> b.compareTo(a);
That's beautiful, isn't it? And if there was only one parameter in the parentheses, we could even omit those parentheses.
For those interested in more information, I have a link and one amazing and detailed article about Java 8.
So now we know what a functional interface is and we can easily create a
thread by implementing Runnable
. Remember the other two
Thread
class constructors? We'll use them here. In the
main()
method, we'll place the following code instead of creating
the MyThread
class:
Thread myThread = new Thread(() -> { 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"); }, "Thread2");
Everything should be clear here. The program should behave as before. You may find this solution a bit harsh, it'd be probably better to use the abstract class concept instead of the lambda expression. But that's up to you.
I look forward to seeing you at the next multithreading lesson in Java, Multithreading v Javě - Daemon, join, and synchronized.