• Home
  • LLMs
  • Docker
  • Kubernetes
  • Java
  • All
  • About
Java | Threads
  1. Introduction
  2. Defining a Thread
    1. Extending the Thread Class
    2. Implementing the Runnable Interface
  3. Instantiating a Thread
  4. Starting a Thread
  5. Thread States at Runtime

  1. Introduction
    In a single thread of execution, program instructions are executed sequentially in the order in which they were written.

    The thread uses a call stack to sequence the execution of program instructions.
    It is not possible to execute program instructions in parallel within a single thread.
    The only way to run programs in parallel is to create new threads.
    Each thread will have its own call stack and will execute the task assigned to it concurrently with other threads.

    In Java, each application executed by the JVM gets a default thread called the main thread.

    It's important to distinguish two different concepts when talking about threads:
    • the code to execute (the task),
    • and the thread (an instance of the Thread class) that will execute this code.

    Java offers two primary ways to define a new thread of execution:
    • by extending the Thread class,
    • or by implementing the Runnable interface.

    In both cases, you must provide an implementation of the run() method, which contains the code to be executed by the thread.

    Note that the Thread class implements the Runnable interface, which defines the run() method.
  2. Defining a Thread
    1. Extending the Thread Class
      class MyThread extends Thread {
          @Override
          public void run() {
              System.out.println(
                      "Thread: " + Thread.currentThread().getName() + " (ID: " + Thread.currentThread().threadId() + ")");
          }
      }
    2. Implementing the Runnable Interface
      class MyRunnable implements Runnable {
          @Override
          public void run() {
              System.out.println(
                      "Runnable: " + Thread.currentThread().getName() + " (ID: " + Thread.currentThread().threadId() + ")");
          }
      }
      It is strongly recommended to implement the Runnable interface rather than extend the Thread class, because it provides a clear separation of concerns between the thread (the execution context) and the task (the code to be executed).

      Using Runnable allows your class to extend another class (since Java doesn't support multiple inheritance). It's also easier to use with thread pools and executor services.

      Generally, extending the Thread class is reserved for cases where you want to modify or enhance the thread's behavior itself.
  3. Instantiating a Thread
    Instantiating a thread means creating an instance of the Thread class (or a subclass of it).
    This instance is associated with the code it will execute (via the run() method).
    At this stage, only the Thread object is created; the actual thread of execution has not yet started.

    Java provides several constructors to instantiate a thread:
    • Instantiating a thread using a subclass of the Thread class:
      public class MainClass {
          public static void main(String[] args) {
              // Thread is created but not started yet
              Thread myThread = new MyThread();
      
          }
      }
    • Instantiating a thread using a class that implements the Runnable interface:
      public class MainClass {
          public static void main(String[] args) {
              Runnable myRunnable = new MyRunnable();
      
              Thread myThread1 = new Thread(myRunnable);
      
              // Giving a custom name to the thread
              Thread myThread2 = new Thread(myRunnable, "MyCustomThreadName");
          }
      }
    You can use an anonymous class or lambda expression to implement the Runnable interface when instantiating the thread:
    public class MainClass {
        public static void main(String[] args) {
            // Using anonymous class
            Thread myThread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("Anonymous Runnable: " + Thread.currentThread().getName() + " (ID: "
                            + Thread.currentThread().threadId() + ")");
                }
            });
    
            // Using lambda expression
            Thread myThread2 = new Thread(() -> {
                System.out.println("Lambda Runnable: " + Thread.currentThread().getName() + " (ID: "
                        + Thread.currentThread().threadId() + ")");
            });
        }
    }
  4. Starting a Thread
    Starting a thread means beginning its execution by creating a new thread of execution and assigning it a call stack, so the JVM can start executing the code inside the thread's run() method.

    To do this, you must call the start() method on the thread instance.
    public class MainClass {
        public static void main(String[] args) {
            // Using anonymous class
            Runnable myRunnable1 = new Runnable() {
                @Override
                public void run() {
                    System.out.println("Anonymous Runnable: " + Thread.currentThread().getName() + " ("
                            + Thread.currentThread().threadId() + ")");
                }
            };
    
            // Using lambda expression
            Runnable myRunnable2 = () -> {
                System.out.println("Lambda Runnable: " + Thread.currentThread().getName() + " ("
                        + Thread.currentThread().threadId() + ")");
            };
    
            Thread myThread1 = new Thread(myRunnable1, "AnonymousRunnable");
            Thread myThread2 = new Thread(myRunnable2, "LambdaRunnable");
    
            myThread1.start(); // This starts the new thread of execution
            myThread2.start(); // This starts the new thread of execution
        }
    }
    Notes:
    • Calling run() directly vs. start():
      A thread instance is a regular object, and you can invoke its methods, including the run() method. However, calling run() directly will execute the code in the current thread, not in a new thread of execution.
      public class MainClass {
          public static void main(String[] args) {
              Runnable myRunnable = () -> {
                  System.out.println("Runnable: " + Thread.currentThread().getName() + " (ID: "
                          + Thread.currentThread().threadId() + ")");
              };
      
              Thread myThread = new Thread(myRunnable, "myFirstThread");
      
              myThread.start(); // Creates new thread, executes run() in that thread
              myThread.run(); // Executes run() in current thread (main thread)
          }
      }
      Typical output:
      Runnable: myFirstThread (ID: 19)
      Runnable: main (ID: 1)
      As you can see, the run() method was executed by the current thread (main thread with ID 1).
      In contrast, calling start() creates a new thread of execution, which internally calls the run() method.

    • The start() method can only be called once:
      Only calling the start() method can launch a new thread of execution.
      Be aware that this method can only be called once per thread instance.
      If it is called again, the JVM will throw an IllegalThreadStateException.
      public class MainClass {
          public static void main(String[] args) {
              Runnable myRunnable = () -> {
                  System.out.println("Runnable: " + Thread.currentThread().getName() + " (ID: "
                          + Thread.currentThread().threadId() + ")");
              };
      
              Thread myThread = new Thread(myRunnable, "myFirstThread");
      
              myThread.start(); // First call - works fine
              myThread.start(); // Second call - throws exception
          }
      }
      Output:
      Runnable: myFirstThread (9)
      Exception in thread "main" java.lang.IllegalThreadStateException
          at java.base/java.lang.Thread.start(Thread.java:790)
          at MainClass.main(MainClass.java:12)
    • Multiple threads with the same task:
      If the same code needs to be executed multiple times by different threads, you must create multiple thread instances and call start() on each of them.
      public class MainClass {
          public static void main(String[] args) {
              Runnable myRunnable = () -> {
                  System.out.println("Runnable: " + Thread.currentThread().getName() + " (ID: "
                          + Thread.currentThread().threadId() + ")");
              };
      
              Thread myThread1 = new Thread(myRunnable, "myFirstThread");
              Thread myThread2 = new Thread(myRunnable, "mySecondThread");
              Thread myThread3 = new Thread(myRunnable, "myThirdThread");
      
              myThread1.start();
              myThread2.start();
              myThread3.start();
          }
      }
      Typical output:
      Runnable: myThirdThread (ID: 21)
      Runnable: myFirstThread (ID: 19)
      Runnable: mySecondThread (ID: 20)
    • Thread execution order is not guaranteed:
      The execution order of threads is not deterministic and depends on the thread scheduler.
      Java provides mechanisms to suggest priorities to the JVM via setPriority(), but the actual scheduling behavior is platform-dependent.

      Java also allows creating dependencies between threads using methods like join(), so one thread will wait for another to complete before continuing execution.
  5. Thread States at Runtime
    A thread can be in one of several states during its lifecycle. The Thread.State enum defines these states:
    • NEW:
      The thread instance has been created, but start() has not yet been called.

    • RUNNABLE:
      The start() method has been called, and the thread is either running or ready to run.
      This state encompasses both "ready to run" and "currently running" states.

    • BLOCKED:
      The thread is blocked waiting for a monitor lock (synchronized block/method).

    • WAITING:
      The thread is waiting indefinitely for another thread to perform a particular action (e.g., Object.wait(), Thread.join()).

    • TIMED_WAITING:
      The thread is waiting for a specified period of time (e.g., Thread.sleep(), Object.wait(timeout)).

    • TERMINATED:
      The run() method has finished executing, either normally or due to an exception.

    Here's an example demonstrating different thread states:
    public class MainClass {
        public static void main(String[] args) {
            Runnable myRunnable = () -> {
                System.out.println("(1) Inside run(): " + Thread.currentThread().getState()); // RUNNABLE
    
                try {
                    Thread.sleep(1000); // Thread will be in TIMED_WAITING state
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // Restore interrupt status
                    System.err.println("Thread was interrupted");
                }
            };
    
            Thread myThread = new Thread(myRunnable, "myTestThread");
    
            System.out.println("(2) After creation: " + myThread.getState()); // NEW
    
            myThread.start();
    
            System.out.println("(3) After start(): " + myThread.getState()); // RUNNABLE
    
            try {
                Thread.sleep(500); // Let the other thread start sleeping
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
    
            System.out.println("(4) While sleeping: " + myThread.getState()); // TIMED_WAITING
    
            try {
                myThread.join(); // Wait for the thread to complete
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
    
            System.out.println("(5) After completion: " + myThread.getState()); // TERMINATED
        }
    }
    Typical output:
    (2) After creation: NEW
    (1) Inside run(): RUNNABLE
    (3) After start(): RUNNABLE
    (4) While sleeping: TIMED_WAITING
    (5) After completion: TERMINATED
    Note: The exact output order may vary due to the concurrent nature of threads, but the states will follow the expected lifecycle.
© 2025  mtitek