For the last few posts, we’ve looked at naming things in our code. Now we’ll move on to something entirely different and look at threads and multi-threading in Java.
Multitasking is the ability of an operating system to have more than one program executing simultaneously. Unless we have a machine with multiple processors, what really happens is that the operating system allocates small blocks of time to each application in turn, at a speed that makes it appear as though all the programs are executing simultaneously.
Types of Multitasking
Multitasking can be done in various ways:
Pre-emptive multitasking
This is when the operating system interrupts applications (without consulting with them first), in order to run another application waiting in the queue for resources. The operating system then runs this process for a limited amount of time (a time slice). When the operating system determines that the process has completed its allocated time slice, it gets taken off the run queue.
There are various approaches to time slicing:
- Round robin allocates the same time slice size to all processes, regardless of priority.
- Priority-based time slicing allocates a length of time depending on the priority of the process.
- Pre-emptive time slicing also allocates a length of time depending on the priority of the process. With this approach, however, the operating system will also pre-empt a lower priority process if a higher-priority process joins the queue, i.e. becomes runnable.
Cooperative multitasking
This is when the processes are only interrupted when they are willing to yield control. This approach depends on all processes being well behaved. If not, a single badly-behaved process can hog resources and bring the entire system to a standstill.
Threads
Some applications can benefit from running several tasks at once. We might want to do this to make the application more responsive, or to do some background processing. The common mechanism for doing this is to create a number of threads that run in parallel with the main thread.
Multiple processes can run within one application. These are commonly known as threads. Other terms commonly used are threads of execution, lightweight processes, or execution contexts.
Java provides basic support for writing multithreaded programs via the Thread
class and Runnable
interface, the synchronized
and volatile
language keywords, and the wait()
, notify()
and notifyAll()
methods of the Object
class.
Potential Pitfalls
While multithreading can have many advantages, it also increases the possibility of bugs. There are several pitfalls to be aware of:
-
Safety: threads can attempt to simultaneously access or modify data. This can lead to data corruption and loss of data integrity.
-
Liveness and deadlock: it is possible that one thread can be waiting for another thread to execute, while that thread is waiting for the first thread to execute.
-
Non-determinism: if a non-threaded program is run, its behaviour will be identical every time. However, when a threaded program runs, the behaviour may be different each time. The Java Virtual Machine does not guarantee the order of execution of unsynchronized threads. This can make troubleshooting difficult.
-
Performance: it is possible for a threaded program to perform more poorly than a non-threaded program. This is due to the overhead involved in creating threads, switching context between threads, synchronizing threads, etc.
Creating Threads
How do we go about creating threads? There are two basic ways: extending the Thread
class and implementing the Runnable
interface.
The Thread
class has a run()
method which we can override to get the functionality we require. The Runnable
interface defines a run()
method which we need to implement.
We can think of the run()
method as a kind of “main” method for our thread process.
Subclassing the Thread Class
If we create a thread by extending the Thread
class, we must override the run()
method. For example:
public class MyThread extends Thread {
// Override the empty run() method of the Thread class
public void run() {
statements;
}
}
In general, we only extend classes to add extra functionality, and not merely to implement some default behaviour we need. Extending the Thread
class just to create an object that can be run as a thread isn’t good object-oriented design.
Remember that we extend a class to create a super-duper version of the class with lots of extra functionality. The Thread
class should only be extended if we want to add extra methods to our inherited Thread
classes that other classes in our application can use.
Remember too that Java only supports single inheritance. We can only extend a single class. We cannot add multithreading behaviour directly to an existing subclass by also extending Thread
.
We could, however, add multithreading behaviour to an existing class (or an extended version of it) by using anonymous inner classes:
public class MyClass extends SomeOtherClass {
private Thread thread;
public void foo() {
// Creating an anonymous inner class of type Thread
thread = new Thread() {
// Overriding run method
public void run() {
statements;
}
};
thread.start();
}
}
Implementing the Runnable Interface
The preferred way to create a thread is to implement the Runnable
interface in an existing class, and implement the run()
method:
public class MyRunnableClass implements Runnable {
// Implement the run() method of the Runnable interface
public void run() {
statements;
}
}
The advantage of this method is that we can add multithreading behaviour to any existing class.
The run() Method
We think of the run()
method as the thread’s “main()” method. After starting a thread in a program, the code in the run()
method of that particular thread is executed by a newly created sub-process, which will then execute in parallel with other threads (including the main thread of the program).
A typical implementation of a run()
method follows:
public void run() {
// stillMoreWorkToDo is a boolean variable that can be
// reset by another method in the same class to end the loop.
while (stillMoreWorkToDo) {
‹statements;›
}
// Exit and terminate thread
}
Starting Threads
To start a thread, we use the following code:
// If extending the Thread class:
Thread t = new MyThread();
t.start(); // Creates a new thread and executes run()
// If implementing the Runnable interface:
Runnable r = new MyRunnableClass();
Thread t = new Thread(r);
t.start(); // Creates a new thread and executes run()
We have to use the start()
method in the Thread
class to run a thread. We cannot simply call the run()
method when we want to run a thread. Well, we could, but it will simply execute the code in the run()
method directly in the main thread of execution, and it will not create a parallel thread.
The start()
method spawns a new operating system level thread. The code in the run()
method is passed to it to be executed in this new thread of execution. The actual low-level code of spawning a new thread is primarily implemented by operating system specific native C code. As such, we should never need to override the start()
method.
Conclusion
This is the tip of the iceberg of threads. There’s lots more to come!
In the next few posts we’ll look at the lifecycle of threads, how to control them, the basic use of the synchronized
keyword, and the wait()
and notify()
methods of the Object
class. Then we’ll look at the exciting classes in the java.util.concurrent
package which make creating, running and controlling threads vastly easier.
Please share your comments and questions.
Until then, stay safe and keep coding!