Java Multithreaded Programming
Java provides built-in support for multithreaded programming. A thread refers to a single sequential control flow in a process, where a process can have multiple threads running concurrently, each executing different tasks in parallel.
Multithreading is a specialized form of multitasking, but it uses less resource overhead.
Here is another term related to threads defined - Process: A process includes a memory space allocated by the operating system, containing one or more threads. A thread cannot exist independently; it must be part of a process. A process continues to run until all non-daemon threads have finished running.
Multithreading allows programmers to write highly efficient programs to fully utilize the CPU.
The Life Cycle of a Thread
A thread is a dynamic execution process, with a lifecycle from creation to termination.
The diagram below shows the complete lifecycle of a thread.
- New State:
After creating a thread object using the new keyword and the Thread class or its subclass, the thread object is in the new state. It remains in this state until the program start()s this thread.
- Runnable State:
After the thread object calls the start() method, it enters the runnable state. Threads in the runnable state are in the runnable queue, waiting for the thread scheduler in the JVM to dispatch them.
- Running State:
If a runnable thread acquires CPU resources, it can execute run(), and the thread is then in the running state. The running state of a thread is the most complex, as it can transition to blocked, runnable, or dead states.
- Blocked State:
If a thread executes methods like sleep (sleep) or suspend (suspend), it loses its occupied resources and enters the blocked state from the running state. It can re-enter the runnable state after the sleep time expires or it acquires device resources. This can be divided into three types:
Waiting Blocked: A thread in the running state executes the wait() method, causing it to enter the waiting blocked state.
Synchronized Blocked: A thread fails to acquire a synchronized lock (because the lock is held by another thread).
Other Blocked: A thread enters the blocked state when it makes an I/O request by calling the sleep() or join() method of the thread. It re-enters the runnable state when the sleep() state times out, the join() waits for the thread to terminate or times out, or the I/O operation completes.
Dead State:
A thread in the running state transitions to the dead state when it completes its task or other termination conditions occur.
Thread Priority
Every Java thread has a priority that helps the operating system determine the scheduling order of threads.
The priority of a Java thread is an integer with a range of 1 (Thread.MIN_PRIORITY) to 10 (Thread.MAX_PRIORITY).
By default, every thread is assigned a priority of NORM_PRIORITY (5).
Threads with higher priority are more important to the program and should be allocated processor resources before lower-priority threads. However, thread priority does not guarantee the order of thread execution and is highly dependent on the platform.
Creating a Thread
Java provides three ways to create a thread:
By implementing the Runnable interface;
By extending the Thread class itself;
By creating a thread with Callable and Future.
Creating a Thread by Implementing the Runnable Interface
The simplest way to create a thread is to create a class that implements the Runnable interface.
To implement Runnable, a class only needs to execute the run() method, declared as follows:
public void run()
You can override this method, and it's important to understand that run() can call other methods, use other classes, and declare variables, just like the main thread.
After creating a class that implements the Runnable interface, you can instantiate a thread object within the class.
The Thread class defines several constructors, one of which we commonly use:
Thread(Runnable threadOb, String threadName);
Here, threadOb is an instance of a class that implements the Runnable interface, and threadName specifies the new thread's name.
After creating the new thread, you call its start() method for it to begin execution.
void start();
Below is an example of creating and starting a thread:
Example
class RunnableDemo implements Runnable {
private Thread t;
private String threadName;
RunnableDemo(String name) {
threadName = name;
System.out.println("Creating " + threadName);
}
public void run() {
System.out.println("Running " + threadName);
try {
for(int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// Let the thread sleep for a while
Thread.sleep(50);
}
} catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("Thread " + threadName + " exiting.");
}
public void start() {
System.out.println("Starting " + threadName);
if (t == null) {
t = new Thread(this, threadName);
t.start();
}
}
}
public class TestThread {
public static void main(String args[]) {
RunnableDemo R1 = new RunnableDemo("Thread-1");
R1.start();
RunnableDemo R2 = new RunnableDemo("Thread-2");
R2.start();
}
}
Compiling and running the above program results in the following output:
Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Thread: Thread-1, 4
Running Thread-2
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-1, 2
Thread: Thread-2, 2
Thread: Thread-1, 1
Thread: Thread-2, 1
Thread Thread-1 exiting.
Thread Thread-2 exiting.
Creating Threads by Extending Thread
The second way to create a thread is to create a new class that extends the Thread class, then create an instance of that class.
The subclass must override the run() method, which is the entry point for the new thread. It must also call start() to begin execution.
Although this method is listed as one way to implement multithreading, it essentially creates an instance that implements the Runnable interface.
Example
class ThreadDemo extends Thread {
private Thread t;
private String threadName;
ThreadDemo(String name) {
threadName = name;
System.out.println("Creating " + threadName);
}
public void run() {
System.out.println("Running " + threadName);
try {
for (int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// Let the thread sleep for a while
Thread.sleep(50);
}
} catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("Thread " + threadName + " exiting.");
}
public void start() {
System.out.println("Starting " + threadName);
if (t == null) {
t = new Thread(this, threadName);
t.start();
}
}
}
public class TestThread {
public static void main(String args[]) {
ThreadDemo T1 = new ThreadDemo("Thread-1");
T1.start();
ThreadDemo T2 = new ThreadDemo("Thread-2");
T2.start();
}
}
Compiling and running the above program results in the following output:
Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Thread: Thread-1, 4
Running Thread-2
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-1, 2
Thread: Thread-2, 2
Thread: Thread-1, 1
Thread: Thread-2, 1
Thread Thread-1 exiting.
Thread Thread-2 exiting.
Thread Methods
The following table lists some important methods of the Thread class:
No. | Method Description |
---|---|
1 | public void start() <br>Causes this thread to begin execution; the Java Virtual Machine calls the run method of this thread. |
2 | public void run() <br>If this thread was constructed using a separate Runnable run object, then that Runnable object's run method is called; otherwise, this method does nothing and returns. |
3 | public final void setName(String name) <br>Changes the name of the thread to be the same as the parameter name. |
4 | public final void setPriority(int priority) <br>Changes the priority of the thread. |
5 | public final void setDaemon(boolean on) <br>Marks this thread as either a daemon thread or a user thread. |
6 | public final void join(long millisec) <br>Waits at most millis milliseconds for this thread to die. |
7 | public void interrupt() <br>Interrupts this thread. |
8 | public final boolean isAlive() <br>Tests if this thread is alive. |
The above methods are invoked by the Thread object. The following table lists static methods of the Thread class.
No. | Method Description |
---|---|
1 | public static void yield() <br>Causes the currently executing thread object to temporarily pause and allow other threads to execute. |
2 | public static void sleep(long millisec) <br>Causes the currently executing thread to sleep (temporarily cease execution) for the specified number of milliseconds, subject to the precision and accuracy of system timers and schedulers. |
3 | public static boolean holdsLock(Object x) <br>Returns true if and only if the current thread holds the monitor lock on the specified object. |
4 | public static Thread currentThread() <br>Returns a reference to the currently executing thread object. |
5 | public static void dumpStack() <br>Prints the stack trace of the current thread to the standard error stream. |
Example
The following ThreadClassDemo program demonstrates some methods of the Thread class:
DisplayMessage.java File Code:
// File name : DisplayMessage.java
// Creating a thread by implementing the Runnable interface
public class DisplayMessage implements Runnable {
private String message;
public DisplayMessage(String message) {
this.message = message;
}
public void run() {
while(true) {
System.out.println(message);
// File name: GuessANumber.java
// Creating a thread by extending the Thread class
public class GuessANumber extends Thread {
private int number;
public GuessANumber(int number) {
this.number = number;
}
public void run() {
int counter = 0;
int guess = 0;
do {
guess = (int) (Math.random() * 100 + 1);
System.out.println(this.getName() + " guesses " + guess);
counter++;
} while(guess != number);
System.out.println("** Correct! " + this.getName() + " in " + counter + " guesses.**");
}
}
// File name: ThreadClassDemo.java
public class ThreadClassDemo {
public static void main(String [] args) {
Runnable hello = new DisplayMessage("Hello");
Thread thread1 = new Thread(hello);
thread1.setDaemon(true);
thread1.setName("hello");
System.out.println("Starting hello thread...");
thread1.start();
Runnable bye = new DisplayMessage("Goodbye");
Thread thread2 = new Thread(bye);
thread2.setPriority(Thread.MIN_PRIORITY);
thread2.setDaemon(true);
System.out.println("Starting goodbye thread...");
thread2.start();
System.out.println("Starting thread3...");
Thread thread3 = new GuessANumber(27);
thread3.start();
try {
thread3.join();
} catch(InterruptedException e) {
System.out.println("Thread interrupted.");
}
System.out.println("Starting thread4...");
Thread thread4 = new GuessANumber(75);
thread4.start();
System.out.println("main() is ending...");
}
}
The output will be as follows, and it will be different each time it runs.
Starting hello thread...
Starting goodbye thread...
Hello
Hello
Hello
Hello
Hello
Hello
Goodbye
Goodbye
Goodbye
Goodbye
Goodbye
.......
Creating a Thread with Callable and Future
-
- Create an implementation class of the Callable interface and implement the call() method, which will serve as the thread's execution body and return a value.
-
- Create an instance of the Callable implementation class, and use the FutureTask class to wrap the Callable object. The FutureTask object encapsulates the return value of the Callable object's call() method.
-
- Use the FutureTask object as the target of the Thread object to create and start a new thread.
-
- Call the get() method of the FutureTask object to obtain the return value after the child thread has finished executing.
Example
public class CallableThreadTest implements Callable<Integer> {
public static void main(String[] args) {
CallableThreadTest ctt = new CallableThreadTest();
FutureTask<Integer> ft = new FutureTask<>(ctt);
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " loop variable i value " + i);
if (i == 20) {
new Thread(ft, "Thread with return value").start();
}
}
try {
System.out.println("Child thread return value: " + ft.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception {
int i = 0;
for (; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
return i;
}
}
Comparison of Three Ways to Create Threads
-
- When creating multiple threads by implementing the Runnable or Callable interface, the thread class only implements the Runnable or Callable interface and can still inherit other classes.
-
- When creating multiple threads by extending the Thread class, the coding is simpler. If you need to access the current thread, you can directly use
this
to get the current thread without using theThread.currentThread()
method.
Main Concepts of Threads
When programming with multiple threads, you need to understand the following concepts:
Thread synchronization
Inter-thread communication
Thread deadlock
Thread control: suspend, stop, and resume
Using Multiple Threads
The key to effectively utilizing multiple threads is to understand that programs execute concurrently rather than serially. For example, if there are two subsystems in a program that need to execute concurrently, multi-threaded programming is required.
By using multiple threads, you can write very efficient programs. However, be aware that creating too many threads can actually reduce the execution efficiency of the program rather than improve it.
Remember, the overhead of context switching is also important. If you create too many threads, the CPU will spend more time on context switching than on executing the program!