Advanced Java August 05 ,2025

Java Multithreading Basics – Part 1

Mastering Java Multithreading: Concepts, Creation, and Core Mechanics

1. Introduction to Multithreading

What is a Thread?

A thread is a lightweight sub-process or the smallest unit of a process. It is a path of execution within a program. Every Java application has at least one thread — the main thread.

What is Multithreading?

Multithreading is a Java feature that allows the concurrent execution of two or more parts of a program for maximum CPU utilization. Each part of such a program is called a thread, and each thread defines a separate path of execution.

Benefits of Multithreading

  • Improved performance by doing multiple tasks simultaneously.
  • Better resource utilization.
  • Useful in real-time applications like games, video processing, or servers.

Processes vs Threads

FeatureProcessThread
DefinitionIndependent program in executionSub-part of a process
MemoryHas its own memory spaceShares memory with other threads
CommunicationComplexEasy (same address space)
OverheadHighLow

When and Why to Use Multithreading

Use multithreading when:

  • Tasks are independent and can run simultaneously.
  • There is I/O wait (e.g., file reading, database access).
  • You want to improve responsiveness in UI applications.
  • Performing parallel processing like image/video/audio rendering or web crawling.

2. The Life Cycle of a Thread

Java threads have the following states defined in java.lang.Thread.State:

  1. New – Thread object is created, but start() hasn’t been called.
  2. Runnable – After start(), thread is ready to run, waiting for CPU.
  3. Running – The thread is executing.
  4. Blocked – Waiting to access a synchronized resource.
  5. Waiting – Thread waits indefinitely for another thread to notify.
  6. Timed Waiting – Waits for a specified period (e.g., sleep, join(timeout)).
  7. Terminated – Thread has finished execution.

Life Cycle Diagram

Java - Thread Life Cycle

Code Example: Demonstrating Thread States

public class ThreadLifeCycleDemo extends Thread {
    public void run() {
        System.out.println("Thread is running");
    }

    public static void main(String[] args) {
        ThreadLifeCycleDemo t1 = new ThreadLifeCycleDemo();
        System.out.println("State before start(): " + t1.getState()); // NEW

        t1.start();
        System.out.println("State after start(): " + t1.getState()); // RUNNABLE

        try {
            Thread.sleep(100);
            System.out.println("State after sleep(): " + t1.getState()); // TIMED_WAITING or TERMINATED
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Output (may vary slightly):

State before start(): NEW
Thread is running
State after start(): RUNNABLE
State after sleep(): TERMINATED

3. Creating Threads in Java

There are two primary ways to create threads in Java:

Method 1: By Extending the Thread Class

class MyThread extends Thread {
    public void run() {
        System.out.println("Thread is running...");
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start(); // Never use run() directly
    }
}

Output:

Thread is running...

Method 2: By Implementing the Runnable Interface

class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Runnable thread running...");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        t1.start();
    }
}

Output:

Runnable thread running...

Comparison of Both Approaches

CriteriaExtending ThreadImplementing Runnable
InheritanceCannot extend another classCan extend any other class
Code ReusabilityLimitedBetter
FlexibilityLessMore

Bonus: Using Lambda with Runnable

public class LambdaThread {
    public static void main(String[] args) {
        Runnable r = () -> System.out.println("Thread using lambda...");
        Thread t = new Thread(r);
        t.start();
    }
}

 

4. Thread Class and Important Methods

Introduction

The Thread class in Java is part of the java.lang package and is used to create and control threads. A thread is a lightweight subprocess, the smallest unit of processing. Java supports multithreading, allowing multiple tasks to run simultaneously within a program, and the Thread class provides all the methods required to handle them.

Definition

public class Thread extends Object implements Runnable
  • Thread extends Object → meaning every thread is still an object in Java.
  • Thread implements Runnable → meaning it can be executed as a thread since it provides the run() method.

Creating a Thread Using the Thread Class

There are two main ways to create a thread:

  1. By extending the Thread class

    • Create a class that extends Thread.
    • Override the run() method with the code you want the thread to execute.
    • Create an object of your class and call start().

    Example:

    class MyThread extends Thread {
        public void run() {
            System.out.println("Thread is running...");
        }
    }
    
    public class Demo {
        public static void main(String[] args) {
            MyThread t1 = new MyThread();  
            t1.start();  // starts a new thread
        }
    }
    

    Here, start() creates a new call stack for the thread and internally calls the run() method.

Important Constructors of Thread Class

The Thread class provides several constructors:

  • Thread() → Creates a new thread object.
  • Thread(String name) → Creates a thread with a given name.
  • Thread(Runnable target) → Creates a thread to run the Runnable target.
  • Thread(Runnable target, String name) → Creates a thread with a Runnable target and a custom name.

Example:

Thread t1 = new Thread("MyThread");  
System.out.println("Thread name: " + t1.getName());

Important Methods of Thread Class

  1. start() → Starts the execution of the thread (calls run()).
  2. run() → Contains the code that will run inside the thread.
  3. sleep(milliseconds) → Puts the thread into sleep state for a given time.
  4. join() → Waits for the thread to finish execution.
  5. setPriority(int) → Sets thread priority (1 to 10).
  6. getPriority() → Returns the thread’s priority.
  7. getName() / setName(String) → Gets or sets the name of the thread.
  8. isAlive() → Checks if the thread is still running.
  9. yield() → Temporarily pauses the current thread to let other threads execute.
  10. currentThread() → Returns the currently executing thread object.

Thread Lifecycle (Using Thread Class)

A thread passes through the following states:

  1. New → Thread object created but not started.
  2. Runnable → After start() is called, thread is ready to run but waiting for CPU.
  3. Running → Thread is currently executing.
  4. Waiting / Timed Waiting (sleep/join) → Temporarily inactive.
  5. Terminated (Dead) → Thread has finished execution.

Example Demonstrating Multiple Threads

class MyThread extends Thread {
    public void run() {
        for(int i=1; i<=5; i++) {
            System.out.println(Thread.currentThread().getName() + " - " + i);
            try {
                Thread.sleep(500); // pause for 0.5s
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        
        t1.setName("Thread-1");
        t2.setName("Thread-2");
        
        t1.start();
        t2.start();
    }
}

Output (order may vary due to scheduling):

Thread-1 - 1
Thread-2 - 1
Thread-1 - 2
Thread-2 - 2
..

Important Methods in Detail

The Thread class (in java.lang package) provides several methods to control the behavior of threads. Below is a detailed explanation of the most important methods.

1. start()

Definition:

public void start()

Theory:

  • The start() method is used to begin the execution of a new thread.
  • Internally, it creates a new call stack for the thread and then calls the run() method.
  • If you call run() directly, it will not create a new thread; it will just execute like a normal method in the main thread.

Example:

class MyThread extends Thread {
    public void run() {
        System.out.println("Thread is running...");
    }
}

public class Demo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();  // starts the thread
    }
}

Output:

Thread is running...

2. run()

Definition:

public void run()

Theory:

  • The run() method contains the logic that the thread will execute.
  • It must be overridden in the subclass of Thread.
  • Execution of run() begins only when the start() method is called.

Example:

class MyThread extends Thread {
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println("Running: " + i);
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();
    }
}

Output:

Running: 1
Running: 2
Running: 3

3. currentThread()

Definition:

public static Thread currentThread()

Theory:

  • Returns a reference to the thread that is currently executing.
  • Often used for debugging or displaying the thread name.

Example:

class MyThread extends Thread {
    public void run() {
        System.out.println("Executing thread: " + Thread.currentThread().getName());
    }
}

public class Demo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.setName("Worker-1");
        t1.start();

        System.out.println("Main thread: " + Thread.currentThread().getName());
    }
}

Possible Output:

Main thread: main
Executing thread: Worker-1

4. setName() and getName()

Definition:

public final void setName(String name)
public final String getName()

Theory:

  • setName() assigns a custom name to a thread.
  • getName() retrieves the current thread’s name.
  • By default, JVM assigns names like Thread-0, Thread-1, etc.

Example:

class MyThread extends Thread {
    public void run() {
        System.out.println("Thread running: " + getName());
    }
}

public class Demo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.setName("FirstThread");
        t1.start();
    }
}

Output:

Thread running: FirstThread

5. setPriority() and getPriority()

Definition:

public final void setPriority(int priority)
public final int getPriority()

Theory:

  • Priorities range from 1 to 10.
    • MIN_PRIORITY = 1
    • NORM_PRIORITY = 5 (default)
    • MAX_PRIORITY = 10
  • Higher priority suggests preference to the thread scheduler, but scheduling behavior depends on the JVM and OS.

Example:

class MyThread extends Thread {
    public void run() {
        System.out.println(getName() + " Priority: " + getPriority());
    }
}

public class Demo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.setName("Low");
        t1.setPriority(Thread.MIN_PRIORITY);

        MyThread t2 = new MyThread();
        t2.setName("High");
        t2.setPriority(Thread.MAX_PRIORITY);

        t1.start();
        t2.start();
    }
}

Possible Output (order may vary):

Low Priority: 1
High Priority: 10

6. sleep()

Definition:

public static void sleep(long milliseconds) throws InterruptedException

Theory:

  • The sleep() method is a static method of the Thread class.
  • It pauses the currently executing thread for a specified time (in milliseconds).
  • During this sleep period, the thread does not lose ownership of any locks/monitors it holds.
  • After the specified time expires, the thread does not directly go to Running. Instead, it first moves to the Runnable state, waiting for the CPU scheduler to pick it again.
  • If another thread calls interrupt() on a sleeping thread, an InterruptedException is thrown.
  • Commonly used to introduce delays in execution, simulate real-time waiting, or control the flow of multithreaded programs.

Example:

class MyThread extends Thread {
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println(getName() + " - " + i);
            try {
                Thread.sleep(1000); // pause for 1 sec
            } catch (InterruptedException e) {
                System.out.println(e);
            }
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.setName("Worker");
        t1.start();
    }
}

Output:

Worker - 1
Worker - 2
Worker - 3

(with a 1 second delay between each line)

7. join()

Definition:

public final void join() throws InterruptedException

Theory:

  • Waits for a thread to finish execution before the next statement executes.
  • Useful for controlling thread execution order.

Example:

class MyThread extends Thread {
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println("Child Thread: " + i);
        }
    }
}

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        t1.start();
        t1.join(); // main waits for child to finish
        System.out.println("Main thread finished after child.");
    }
}

Output:

Child Thread: 1
Child Thread: 2
Child Thread: 3
Main thread finished after child.

8. yield()

Definition:

public static void yield()

Theory:

  • Temporarily pauses the currently executing thread, giving a chance for other threads of equal priority to execute.
  • Not guaranteed, depends on thread scheduler.

Example:

class MyThread extends Thread {
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println(getName() + " - " + i);
            Thread.yield();
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        t1.setName("First");
        t2.setName("Second");
        t1.start();
        t2.start();
    }
}

Possible Output (order may vary):

First - 1
Second - 1
First - 2
Second - 2
First - 3
Second - 3

9. isAlive()

Definition:

public final boolean isAlive()

Theory:

  • Checks if a thread is still running.
  • Returns true if the thread has been started and has not yet terminated.

Example:

class MyThread extends Thread {
    public void run() {
        System.out.println("Thread running...");
    }
}

public class Demo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        System.out.println(t1.isAlive()); // false
        t1.start();
        System.out.println(t1.isAlive()); // true (most likely, depends on timing)
    }
}

Output:

false
true

10. interrupt()

Definition:

public void interrupt()

Theory:

  • Interrupts a thread that is sleeping or waiting, causing it to throw InterruptedException.
  • Does not stop the thread forcefully; only signals it to stop.

Example:

class MyThread extends Thread {
    public void run() {
        try {
            Thread.sleep(5000);
            System.out.println("Completed task");
        } catch (InterruptedException e) {
            System.out.println("Thread interrupted before completion");
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start();
        t1.interrupt();
    }
}

Output:

Thread interrupted before completion

 

5. Thread Priorities

What are Thread Priorities in Java?

Each thread in Java is assigned a priority, which helps the thread scheduler decide the order in which threads are executed. Though thread priorities don't guarantee order of execution, they can influence the execution when multiple threads are competing for CPU time.

Java Thread Priority

Priority Levels in Java

Java defines three constants in the Thread class to set thread priorities:

ConstantValueMeaning
Thread.MIN_PRIORITY1Lowest priority
Thread.NORM_PRIORITY5Default priority
Thread.MAX_PRIORITY10Highest priority

You can also set priorities manually using any integer from 1 to 10.

How to Set and Get Priority

Thread t1 = new Thread();
t1.setPriority(8); // Set priority to 8
int p = t1.getPriority(); // Returns 8

Thread Scheduling Behavior

  • Threads with higher priority are generally given preference by the thread scheduler.
  • However, thread scheduling is platform-dependent, and there's no guarantee that higher priority threads will always run first.
  • Most modern systems use preemptive or time-sliced scheduling, making the outcome less predictable.

Code Example: Demonstrating Thread Priority

class PriorityThread extends Thread {
    public void run() {
        System.out.println(Thread.currentThread().getName() + " with priority " + Thread.currentThread().getPriority() + " is running.");
    }

    public static void main(String[] args) {
        PriorityThread t1 = new PriorityThread();
        PriorityThread t2 = new PriorityThread();
        PriorityThread t3 = new PriorityThread();

        t1.setPriority(Thread.MIN_PRIORITY);  // 1
        t2.setPriority(Thread.NORM_PRIORITY); // 5 (default)
        t3.setPriority(Thread.MAX_PRIORITY);  // 10

        t1.start();
        t2.start();
        t3.start();
    }
}

Sample Output:

Thread-0 with priority 1 is running.
Thread-2 with priority 10 is running.
Thread-1 with priority 5 is running.

⚠️ The actual order may vary depending on the JVM and OS.

Important Notes:

  • You can only set priority before starting the thread. Setting it after calling start() has no effect.
  • Use thread priorities carefully; relying on them for critical logic may lead to non-portable code.
  • For better control, use Executors and thread pools, which we'll cover in the next blog.

6. Synchronization in Java

What is Synchronization?

Synchronization is the process of controlling access to shared resources by multiple threads. In Java, synchronization is achieved using the synchronized keyword.

It ensures:

  • Mutual exclusion – only one thread can execute the synchronized code at a time.
  • Memory visibility – changes made by one thread are visible to others.

Why is Synchronization Needed?

In a multithreaded environment, if multiple threads access shared resources (like variables, arrays, or files) simultaneously, it may lead to data inconsistency or corruption. This situation is known as thread interference.

Example: The Problem Without Synchronization

class Counter extends Thread {
    static int count = 0;

    public void run() {
        for (int i = 1; i <= 1000; i++) {
            count++; // Shared resource
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter t1 = new Counter();
        Counter t2 = new Counter();

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Final Count: " + count);
    }
}

Expected Output:

Final Count: 2000

Possible Actual Output:

Final Count: 1782

⚠️ This happens because both threads modify the same variable at the same time.

Ways to Synchronize Code

1. Synchronized Method

When a method is declared synchronized, the thread holds the lock for that object before executing it.

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

class SyncMethodExample extends Thread {
    Counter counter;

    SyncMethodExample(Counter c) {
        this.counter = c;
    }

    public void run() {
        for (int i = 1; i <= 1000; i++) {
            counter.increment();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counter c = new Counter();
        SyncMethodExample t1 = new SyncMethodExample(c);
        SyncMethodExample t2 = new SyncMethodExample(c);

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Final Count: " + c.getCount());
    }
}

Output:

Final Count: 2000

2. Synchronized Block

Used to lock only a specific part of the code, improving performance by reducing the locked area.

public void increment() {
    synchronized (this) {
        count++;
    }
}

Use this when:

  • Only a portion of the method needs synchronization.
  • You want finer control over the lock object.

3. Static Synchronization

When using static methods, synchronization applies to the class level, not object level.

public synchronized static void printData() {
    // Code here is synchronized at the class level
}

Locks on Class.class object rather than the instance.

4. Synchronization Locking Mechanism

In multithreaded programming, synchronization is required to prevent race conditions—situations where multiple threads try to access and modify shared resources at the same time.

To handle this, Java provides locking mechanisms. Locks ensure that only one thread at a time can access critical code or shared data.

a. Intrinsic Locks (Monitor Locks)

Definition:

  • Every Java object has a built-in lock called an intrinsic lock (or monitor).
  • When a thread enters a synchronized method or block, it acquires the intrinsic lock of the object.
  • Other threads attempting to enter the same synchronized block/method are blocked until the lock is released.
  • Lock is automatically released when the thread exits the synchronized block (normal exit or due to exception).

Key Points:

  • Used with synchronized keyword.
  • Implicit locking (handled by JVM).
  • Works at the object level for instance methods and at the class level for static synchronized methods.

Example (Intrinsic Lock):

class Counter {
    private int count = 0;

    public synchronized void increment() { // intrinsic lock on "this"
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Counter c = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) c.increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) c.increment();
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Final Count: " + c.getCount());
    }
}

Output:

Final Count: 2000

(Without synchronization, result would vary and often be less than 2000 due to race condition.)

b. Extrinsic Locks (Explicit Locks)

Definition:

  • Provided by the java.util.concurrent.locks package.
  • Unlike intrinsic locks, extrinsic locks must be explicitly acquired and released by the programmer.
  • The most common implementation is the ReentrantLock class.

Key Points:

  • More flexible than intrinsic locks.
  • Can try acquiring lock without blocking (tryLock()).
  • Supports fairness policies (queue threads).
  • Must manually release lock in a finally block to avoid deadlocks.

Example (Extrinsic Lock with ReentrantLock):

import java.util.concurrent.locks.ReentrantLock;

class Counter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();

    public void increment() {
        lock.lock();  // acquire lock
        try {
            count++;
        } finally {
            lock.unlock(); // release lock
        }
    }

    public int getCount() {
        return count;
    }
}

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Counter c = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) c.increment();
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) c.increment();
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Final Count: " + c.getCount());
    }
}

Output:

Final Count: 2000

c. Intrinsic vs Extrinsic Locks

FeatureIntrinsic LockExtrinsic Lock
ImplementationImplicit (via synchronized)Explicit (via ReentrantLock, etc.)
Lock AcquisitionAutomatic when entering synchronized block/methodMust be manually acquired with lock()
Lock ReleaseAutomatic when exiting block/methodMust be manually released with unlock()
FairnessNo control over fairnessCan create fair locks (FIFO queue)
FeaturesBasic synchronization onlyAdvanced features: tryLock(), timed lock, interruptible lock
RiskNo risk of forgetting to release (handled by JVM)Risk of deadlock if unlock() is missed

d. Additional Notes on Locking

  • Reentrant Nature: Both intrinsic and ReentrantLock are reentrant, meaning the same thread can acquire the same lock multiple times without deadlocking itself.
  • Deadlocks: Poor lock ordering or failure to release locks can cause deadlocks.
  • Performance: Intrinsic locks are simpler and good for most use cases; extrinsic locks are better for complex synchronization scenarios.
  • Alternatives: Java also provides higher-level concurrency utilities (Semaphore, ReadWriteLock, CountDownLatch, etc.).

What Happens Internally?

Behind the scenes, synchronized blocks translate to monitorenter and monitorexit bytecode instructions that handle locking/unlocking.

Limitations of Synchronized Keyword

  • Can lead to performance bottlenecks.
  • Doesn't support interruptibility (like ReentrantLock does).
  • Cannot specify timeout.

For more advanced control, Java provides java.util.concurrent.locks, which we will cover in Blog 2.

Summary of Blog 1 Topics

In this blog, we explored the foundational concepts of multithreading in Java. We began by understanding what threads are and how multithreading enables efficient CPU utilization by allowing multiple tasks to run concurrently. We examined the life cycle of a thread, from creation to termination, and learned how to create threads using both the Thread class and the Runnable interface. Key thread operations such as start(), run(), sleep(), join(), yield(), and interrupt() were explained with practical examples. We also discussed how thread priorities can influence thread scheduling, although not guaranteed. Finally, we looked into the critical concept of synchronization and how it helps prevent issues like thread interference and race conditions when threads access shared resources.

To be continued in Part 2: Advanced Java Multithreading
(where we’ll cover inter-thread communication, deadlocks, concurrency utilities, and best practices)

 

Next Blog- Java Multithreading Basics – Part 2

Sanjiv
0

You must logged in to post comments.

Get In Touch

123 Street, New York, USA

+012 345 67890

techiefreak87@gmail.com

© Design & Developed by HW Infotech