Understanding Threads in Java

Pamal Jayawickrama
10 min readSep 12, 2024

--

In Java, a thread is the smallest unit of execution within a process. Threads within the same process share the same memory space and resources, but they can run independently of each other. Multiple threads can execute concurrently, allowing a program to perform multiple tasks simultaneously or in parallel. This is particularly useful in improving performance for tasks like handling multiple requests or performing background operations.

Thread Lifecycle:

To work with threads in a program, it is important to identify thread state. let’s understand how to identify thread states in Java thread life cycle.

image from baeldung.com
  • NEW: When we create a thread object using Thread class, thread is born and is known to be in New state. But the start() method has not been called yet on the instance.
  • Runnable: Once the thread’s start() method is called, the thread enters the RUNNABLE state. This state means the thread is ready to run and is either currently executing or waiting to be scheduled by the operating system.
  • Running: In running state, processor gives its time to the thread for execution and executes its run method. This is the state where thread performs its actual functions. A thread can come into running state only from runnable state.
  • Blocked: A thread enters the BLOCKED state when it attempts to enter a synchronized method or block, but another thread already holds the lock on the object. The thread will remain blocked until the lock becomes available.
// code without using synchronized method
class Resource {
public void shareResource(String name) throws InterruptedException {
System.out.println("start sharing the resource :" + name);
Thread.sleep(1000);
System.out.println("end sharing the resource :" + name);
}
}

class MyThread implements Runnable {

private final Resource resource;
private final String tName;

public MyThread(Resource resource, String tName) {
this.resource = resource;
this.tName = tName;
}

@Override
public void run() {
try {
Thread.currentThread().setName(tName);
resource.shareResource(Thread.currentThread().getName());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

public class BlockState {
public static void main(String[] args) {
Resource resource = new Resource();
Thread thread1 = new Thread(new MyThread(resource, "t1"));
Thread thread2 = new Thread(new MyThread(resource, "t2"));
Thread thread3 = new Thread(new MyThread(resource, "t3"));
thread1.start();
thread2.start();
thread3.start();
}
}


----------------------
OUTPUT
start sharing the resource :t3
start sharing the resource :t1
start sharing the resource :t2
end sharing the resource :t1
end sharing the resource :t2
end sharing the resource :t3
// code using synchronized method
class Resource {
public synchronized void shareResource(String name) throws InterruptedException {
System.out.println("start sharing the resource :" + name);
Thread.sleep(1000);
System.out.println("end sharing the resource :" + name);
}
}

class MyThread implements Runnable {

private final Resource resource;
private final String tName;

public MyThread(Resource resource, String tName) {
this.resource = resource;
this.tName = tName;
}

@Override
public void run() {
try {
Thread.currentThread().setName(tName);
resource.shareResource(Thread.currentThread().getName());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

public class BlockState {
public static void main(String[] args) {
Resource resource = new Resource();
Thread thread1 = new Thread(new MyThread(resource, "t1"));
Thread thread2 = new Thread(new MyThread(resource, "t2"));
Thread thread3 = new Thread(new MyThread(resource, "t3"));
thread1.start();
thread2.start();
thread3.start();
}
}

--------------------
OUTPUT

start sharing the resource :t1
end sharing the resource :t1
start sharing the resource :t3
end sharing the resource :t3
start sharing the resource :t2
end sharing the resource :t2

Once Thread-1 (t1)completes the work inside the synchronized method and releases the lock, one of the blocked threads (Thread-2(t2) or Thread-3(t3)) will acquire the lock and proceed with its execution. The previously blocked thread will transition back to the RUNNABLE state.

  • Waiting : thread is waiting indefinitely for another thread to perform a particular action (e.g.,Thread.join() without a timeout).
  • Timed-Waiting : thread is waiting for a specific amount of time before it either proceeds or returns to the RUNNABLE state. This can happen with method like Thread.sleep().
  • Terminated (Dead): That is, a thread is terminated or dead when a thread comes out of run() method. A thread can also be dead when the stop() method is called.

Thread Scheduler in Java:

A thread scheduler is a component of an operating system responsible for managing the execution of threads. It decides which thread runs at any given time, ensuring efficient use of CPU resources. The scheduler’s main tasks include:

  • Prioritizing threads: Based on factors like priority, execution time, or fairness.
  • Context switching: Moving between threads to ensure multitasking.
  • Preemption: Temporarily halting a thread to allow another thread to run.
  • Handling synchronization: Managing thread access to shared resources without conflicts.

Schedulers may use different algorithms (e.g., round-robin, priority-based) to allocate CPU time among threads.

Creating Threads in Java:

In Java, you can create threads using two main methods:

  • Extending the Thread class: You can create a thread by extending the Thread class and overriding its run() method. The run() method contains the code that will be executed by the thread.
class MyThread extends Thread {
public void run() {
System.out.println("Thread is running.");
}
}

public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // Start the thread, which invokes the run() method
}
}j
  • Implementing the Runnable interface: A more flexible approach is to implement the Runnable interface. This is preferred in cases where you want your class to extend another class but also implement multithreading.
class MyRunnable implements Runnable {
public void run() {
System.out.println("Thread is running via Runnable.");
}
}

public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // Start the thread
}
}j

Thread Control Methods:

Java provides several methods to control threads:

  • start(): Starts the thread and invokes the run() method.
class ChildThread extends Thread {

@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("child thread: " + i);
}
}
}

public class Main {
public static void main(String[] args) {
ChildThread childThread = new ChildThread();
childThread.start(); // execute the thread

for (int i = 0; i < 100; i++) {
System.out.println("main thread: " + i);
}
}
}


-----------------------
OUTPUT

child thread: 0
child thread: 1
....
main thread: 4
main thread: 5
..
child thread: 50
main thread: 89
main thread: 90
...
child thread: 99
main thread: 100
child thread: 100

After starting childThread, the main thread continues executing the main method in parallel with the childThread.

Both threads will execute their respective code blocks concurrently. This means you will see interleaved outputs from both the main thread and the childThread.

Question: what happens when calling start() multiple time ?

public class Main {
public static void main(String[] args) {
ChildThread childThread = new ChildThread();
childThread.start();
childThread.start();// execute the thread

for (int i = 0; i < 100; i++) {
System.out.println("main thread: " + i);
}
}
}

After a thread has finished its execution (i.e., its run() method has completed), it cannot be restarted. Attempting to call start() on a thread that has already completed will result in an IllegalThreadStateException.

Each thread can only be started once. After a thread has been started, its state changes to RUNNABLE and it can no longer be restarted.

To perform a new task, you need to create a new instance of the Thread class (or a subclass thereof) and start it.

public class Main {
public static void main(String[] args) {
ChildThread childThread1 = new ChildThread();
childThread1.start();

ChildThread childThread2 = new ChildThread();
childThread2.start();

for (int i = 0; i < 100; i++) {
System.out.println("main thread: " + i);
}
}
}

Question: what happens if you override start() ?

class ChildThread extends Thread {

@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("child thread: " + i);
}
}

public void start() {
System.out.println("child thread start");
}

}

public class Main {
public static void main(String[] args) {
ChildThread childThread = new ChildThread();
childThread.start();
}
}

In the above code, the program will print "child thread start". The run() method will not be executed. The reason is that overriding start() disrupts the normal thread lifecycle.

Call super.start(), which actually starts the thread and invokes the run() method.

class ChildThread extends Thread {

@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("child thread: " + i);
}
}

public void start() {
System.out.println("child thread start");
super.start();
}

}
  • sleep(long millis): Causes the current thread to pause for a specified period.
class ChildThread extends Thread {

@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("child thread: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

public class Main {
public static void main(String[] args) {
ChildThread childThread = new ChildThread();
childThread.start();

for (int i = 0; i < 100; i++) {
System.out.println("main thread: " + i);
}
}
}

Following are the syntax of the sleep() method.

public static void sleep(long milliseconds) throws InterruptedException   
public static void sleep(long milliseconds, int nanoseconds) throws InterruptedException
  • join(): Forces one thread to wait until another thread completes execution.
class ChildThread extends Thread {

@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("child thread: " + i);
}
}
}

public class Main {
public static void main(String[] args) throws InterruptedException {
ChildThread childThread = new ChildThread();
childThread.start();
childThread.join();

for (int i = 0; i < 100; i++) {
System.out.println("main thread: " + i);
}
}
}

When childThread.join() is called, the main thread pauses its execution and waits for the child thread to complete before continuing. This ensures that the child thread finishes its work before the main thread resumes its task.

Question: Can you use a timeout with Thread.join()? How does it work?

Yes, Thread.join() can accept a timeout parameter (in milliseconds and/or nanoseconds). If a timeout is specified, the current thread will wait for the specified amount of time for the target thread to finish. If the target thread does not finish within the given time, the current thread will resume execution even if the other thread is still running.

Question: Can Thread.join() cause a deadlock? How?

Yes, Thread.join() can cause a deadlock.

public class Main {
public static void main(String[] args) throws InterruptedException {
Thread.currentThread().join();
for (int i = 0; i < 100; i++) {
System.out.println("main thread: " + i);
}
}
}

The join() method causes the current thread to wait until the thread on which it is called completes. However, here it is being called on the main thread itself.

This leads to an issue because the main thread cannot complete while it is waiting for itself to finish. This creates a situation where the main thread is essentially waiting indefinitely, resulting in a deadlock.

Question: How would you use Thread.join() to ensure sequential execution of threads?

You can use the Thread.join() method to enforce a specific order of thread execution. By calling join() on one thread before starting another, you can ensure that the previous thread completes before the next thread begins.

Thread t1 = new Thread(() -> System.out.println("Thread 1"));
Thread t2 = new Thread(() -> System.out.println("Thread 2"));
t1.start();
t1.join(); // Ensures Thread 1 completes before Thread 2 starts
t2.start();
  • yield(): When a thread calls Thread.yield(), it moves from the Running state to the Runnable state, allowing the thread scheduler to potentially switch to another thread. The current thread is still eligible to be selected for execution again if no other higher-priority or equally prioritized threads are available.
class ChildThread extends Thread {

private final String childThread;

public ChildThread(String childThread) {

this.childThread = childThread;
}

@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(childThread+": " + i);
Thread.yield();
}
}
}

public class Main {
public static void main(String[] args) throws InterruptedException {
ChildThread childThread1 = new ChildThread("childThread 1");
ChildThread childThread2 = new ChildThread("childThread 2");
childThread1.start();
childThread2.start();
}
}

----------------
OUTPUT

childThread 2: 0
childThread 1: 0
childThread 2: 1
childThread 1: 1
childThread 2: 2

....
childThread 2: 99
childThread 1: 95
childThread 1: 96
childThread 1: 97
childThread 1: 98
childThread 1: 99

In this code, both t1 and t2 are instances of ChildThread. As the yield() method is called within the loop, the currently executing thread hints to the scheduler that it’s willing to allow another thread to run. If the scheduler honors the yield() hint, you may see interleaved output from t1 and t2. However, if the operating system ignores the hint, one thread may execute entirely before the other starts.

Question: What is the difference between Thread.yield() and Thread.sleep()?

Thread.yield() is a hint to the scheduler to allow other threads to execute, but it does not guarantee that other threads will run, and it does not block the thread—it merely returns to the Runnable state.

Thread.sleep() pauses the execution of the current thread for a specified period of time, during which it moves to the Timed Waiting state. After the sleep time is over, the thread returns to the Runnable state.

Question: Does Thread.yield() guarantee that another thread will be scheduled?

Thread.yield() does not guarantee that another thread will be scheduled. It merely hints to the thread scheduler that the current thread is willing to yield its CPU time slice. The scheduler may choose to continue running the current thread, depending on the operating system's thread scheduling policy and the priorities of other threads.

Question: Can Thread.yield() be ignored by the JVM?

Thread.yield() can be ignored by the JVM. It is a platform-dependent feature, meaning the behavior of yield() can vary between different operating systems and JVM implementations. Some JVMs or operating systems may simply ignore the yield hint and continue running the current thread.

Thread Priority:

In a typical thread scheduling system, priorities are assigned using numerical values, often ranging from 1 to 10, with 1 being the lowest and 10 being the highest priority. The thread priority level dictates how the operating system’s scheduler allocates CPU time, ensuring that higher-priority tasks are executed before lower-priority ones.

Daemon threads:

  • Background Execution: Daemon threads operate in the background and do not prevent the main program from exiting. When all user (non-daemon) threads complete, the JVM or operating system will terminate daemon threads, regardless of whether they have finished their tasks.
  • Lifecycle: Daemon threads are automatically stopped when the program ends. They are not required to complete their execution before the program terminates, making them suitable for tasks that do not need to be completed for the application to exit.
  • Use Cases: Commonly used for tasks such as monitoring, periodic housekeeping, or other background activities that should not block the program from terminating.

In this article, I’ve covered most of the fundamental aspects of multithreading in Java. I hope this gives you a solid foundation to start working with threads effectively in your Java programs. In my next article, I’ll dive into a practical scenario to demonstrate how to apply these concepts in real-world applications.

Happy coding!

--

--