Java Multithreading – Part 1

Clean HTML study notes — Multitasking, Threads, Synchronization, Locks, Deadlock and Daemon Threads

Prepared by Srikanth Mamillapalli
Converted into a clean HTML study-notes format. Original PDF Pages are intentionally not included.

Multitasking and Multithreading

Multitasking means executing several tasks simultaneously. There are two types:

  1. Process-based multitasking — several independent programs/processes execute simultaneously.
  2. Thread-based multitasking — several independent parts of the same program/process execute simultaneously; each independent part is a thread.

Examples

Typing a Java program, listening to audio and downloading files at the same time are examples of process-based multitasking.

Thread-based multitasking is best suited to the programmatic level.

Objectives

  • Reduce response/computation time.
  • Improve application performance.

Applications

  • Multimedia and graphics
  • Animations
  • Video games
  • Web servers and application servers

Advantages of multithreading

  • Reduces computation time.
  • Improves application performance.
  • Threads share the same address space, saving memory.
  • Context switching between threads is usually less expensive than between processes.
  • Communication between threads has relatively low cost.

What is a Thread?

A thread is a single sequential flow of control within a program. It is also called an execution context or lightweight process.

  • A thread is not an independent program.
  • A thread runs within a program and uses the resources allocated to that program.
  • Each thread has its own execution stack and program counter.
  • Java provides built-in multithreading support through Thread, Runnable, ThreadGroup, and related APIs.

Two ways to create a thread

  1. Extend java.lang.Thread.
  2. Implement java.lang.Runnable.

Creating a Thread by Extending Thread

Steps:

  1. Extend the java.lang.Thread class.
  2. Override run() to define the work performed by the thread.
  3. Create an instance of the subclass.
  4. Call start() to make the thread eligible for execution.
class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("Child Thread");
        }
    }
}

public class Test {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("Main Thread");
        }
    }
}

Flow

Main Thread → creates/starts Child Thread → Thread Scheduler → both threads may execute concurrently

Main Thread and Thread Scheduler

  • The main thread executes the main() method.
  • The JVM creates the main thread.
  • If independent jobs exist, multiple threads can be used.
  • The Thread Scheduler is part of the JVM and decides which runnable thread gets processor time.
  • The exact scheduling algorithm is JVM/platform dependent.
  • Therefore, multithreaded programs generally cannot guarantee one exact execution order or output.

start() vs run()

start()run()
Creates a new thread and makes it eligible for scheduling.Does not create a new thread.
The new thread executes run().run() executes like an ordinary method call in the current thread.
Required to actually start multithreading.Calling it directly does not provide concurrent execution.

Why start() is important

start() registers the thread with the scheduler, performs required thread setup, and then invokes run(). It is therefore the key method for starting a Java thread.

start() {
    register with Thread Scheduler;
    perform required activities;
    invoke run();
}

Overloading run()

  • Overloading run() is possible.
  • start() invokes only the no-argument run().
  • Other overloaded versions must be called explicitly.
  • If run() is not overridden, Thread's empty implementation is used.

Overriding start()

Overriding start() causes the overridden method to execute as a normal method call, so a new thread will not be created. It is therefore not recommended.

Thread Lifecycle

New / Born → Ready / Runnable → Running → Dead
                     ↘ Waiting / Blocked ↗

After a thread has been started, attempting to start the same thread again results in IllegalThreadStateException.

Creating a Thread with Runnable

The Runnable approach is performed in three steps:

  1. Create a class implementing Runnable and override run().
  2. Create a Thread object by passing the Runnable object to its constructor.
  3. Call start() on the Thread object.
class RunnableDemo implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable thread");
    }
}

public class Test {
    public static void main(String[] args) {
        RunnableDemo r = new RunnableDemo();
        Thread t = new Thread(r);
        t.start();
    }
}

Important cases

  • t.start() creates a new thread and executes the appropriate run().
  • t.run() does not create a new thread.
  • A Runnable object itself does not have start() capability.

Recommended approach

Implementing Runnable is generally recommended because the class remains free to extend another class, avoiding the single-inheritance limitation of extending Thread.

Thread Constructors

Constructor
Thread()
Thread(String)
Thread(Runnable)
Thread(Runnable, String)
Thread(ThreadGroup, String)
Thread(ThreadGroup, Runnable)
Thread(ThreadGroup, Runnable, String)
Thread(ThreadGroup, Runnable, String, long)

Hybrid Thread Approach and Thread Name

A hybrid approach can combine inheritance and Runnable implementation. Every Java thread also has a name.

  • A thread may have a JVM-generated default name.
  • A programmer can provide a custom name.
  • Thread.currentThread() returns the currently executing thread.
Thread t = Thread.currentThread();
System.out.println(t.getName());
t.setName("MyThread");
System.out.println(t.getName());

Thread Priorities

Every thread has a priority. Java thread priorities range from 1 to 10.

ConstantValue
Thread.MIN_PRIORITY1
Thread.NORM_PRIORITY5
Thread.MAX_PRIORITY10
  • The scheduler may consider thread priority when allocating processor time.
  • A higher-priority thread may get preference, but exact execution order is not guaranteed.
  • For equal priorities, scheduling order is also not guaranteed.
  • Values outside 1–10 cause IllegalArgumentException.

Default priority

The main thread has default priority 5. A newly created thread normally inherits its parent's priority.

Yield

Thread.yield() causes the currently executing thread to give other runnable threads of the same priority an opportunity to execute.

  • If no suitable waiting thread exists, the current thread may continue.
  • The scheduler decides which thread runs next.
  • The yielded thread's next execution time cannot be predicted exactly.
  • Some platforms may provide limited support for the intended scheduling effect.

join()

If one thread needs to wait until another thread completes, use join().

t2.join();

If thread t1 executes t2.join(), t1 enters a waiting state until t2 completes.

Real-world example

Venue Fixing (t1) → Wedding Card Printing (t2) → Card Distribution (t3)

t2 can call t1.join(), and t3 can call t2.join().

Signatures

t2.join();
t2.join(10000);
t2.join(1000, 100);

join() throws InterruptedException, a checked exception, so it must be handled with try/catch or throws.

Deadlock examples

  • If main waits for child and child waits for main, both can wait forever.
  • Thread.currentThread().join() makes the current thread wait for itself indefinitely.

sleep()

If a thread should pause for a specified period, use sleep().

Thread.sleep(10000);

sleep() throws InterruptedException, so it must be handled.

Running → sleep() → Waiting / Timed Waiting → time expires or interrupted → Runnable

interrupt()

A thread can interrupt a sleeping or waiting thread using interrupt().

child.start();
child.interrupt();
  • If the target is sleeping or waiting, the interrupt can immediately affect it.
  • If the target is running and never enters a sleeping/waiting state, the interrupt may have no immediate visible effect.
  • An interrupt does not forcibly kill a thread; it is a cooperative interruption mechanism.

Yield vs Join vs Sleep

Propertyyield()join()sleep()
PurposeGive other runnable threads an opportunity.Wait for another thread to complete.Pause execution for a specified period.
OverloadedNoYesYes
FinalNoYesNo
Throws InterruptedExceptionNoYesYes
StaticYesNoYes

Inter-Thread Communication

Threads can communicate using wait(), notify(), and notifyAll().

  • The thread waiting for an update calls wait() and enters a waiting state.
  • The thread performing the update calls notify() or notifyAll() after updating the shared state.
  • These methods belong to Object, because a thread can wait/notify on any Java object whose monitor it owns.
  • The thread must own the object's monitor, normally by being inside a synchronized section.
  • Calling these methods without owning the monitor can cause IllegalMonitorStateException.
  • wait() releases the object's lock while waiting.
MethodReleases lock?
yield()No
join()No
sleep()No
wait()Yes
notify()Lock is not immediately released by notify itself.
notifyAll()Lock is not immediately released by notifyAll itself.

Lock and Monitor

A lock (monitor) synchronizes access to a shared resource.

  • A shared resource can be associated with a Java object's lock.
  • At most one thread can own that object monitor at a time.
  • The lock provides mutual exclusion.
  • Every Java object has an associated monitor/lock concept that can be used for synchronization.
Threads → acquire object lock/monitor → access shared resource → release lock

Producer–Consumer Problem

The producer thread produces items and puts them into a queue. The consumer thread removes items from the queue.

  • If the queue is empty, the consumer waits.
  • After producing an item, the producer notifies the waiting consumer.
  • notify() wakes one waiting thread.
  • notifyAll() notifies all waiting threads, although they still acquire the lock one at a time.
  • When multiple threads are waiting, the exact thread selected by notify() is not guaranteed.

Synchronization

Synchronization is a modifier applicable to methods and blocks, not to classes or variables.

If multiple threads operate on the same Java object simultaneously, data inconsistency can occur. The synchronized keyword provides mutual exclusion.

Advantages

  • Protects shared data.
  • Prevents data inconsistency caused by concurrent access.

Disadvantages

  • Increases thread waiting.
  • Can reduce performance when used unnecessarily.

Examples

  • Public telephone
  • Joint bank account
  • Online reservation
  • Multiple users accessing one shared resource

Internally, synchronization uses an object's lock. A thread must acquire the object's lock before executing a synchronized method/block and releases it after completion.

Object Lock and Synchronized Methods

Locking is based on the object, not simply on the method name.

  • Only one thread at a time can execute synchronized code guarded by the same object lock.
  • Other threads may still execute non-synchronized methods on the same object.
  • If multiple threads operate on the same shared object, synchronization may be required.
  • If they operate on separate independent objects, synchronization may not be necessary for that resource.
Java Object → Synchronized Area (one thread at a time) | Non-Synchronized Area (multiple threads may execute)

Class-Level Locking

Every Java class has a class-level lock. A thread executing a static synchronized method requires the class-level lock.

  • Only one thread can execute static synchronized methods of that class at a time.
  • Once the method completes, the class lock is released.
  • Other normal static methods, instance synchronized methods guarded by an object lock, and normal instance methods are governed by their respective rules and locks.

Two levels of locks

  1. Object-level lock
  2. Class-level lock

Synchronized Block

If only a small portion of a method needs synchronization, a synchronized block can be used instead of synchronizing the entire method. This narrows the critical section and can reduce unnecessary waiting.

synchronized (this) {
    // critical section
}

synchronized (obj) {
    // critical section using obj's lock
}

synchronized (MyClass.class) {
    // class-level lock
}

Synchronization locks can be based on reference types/objects and class objects, not primitive values.

Race Condition

A race condition occurs when multiple threads access and modify shared state concurrently in a way that can produce inconsistent results.

Synchronization or other concurrency-control mechanisms can be used to protect the critical section.

Synchronized Statements and Interview Questions

The statements inside a synchronized method or synchronized block are called synchronized statements.

Important interview questions

  1. What is the synchronized keyword and where can it be applied?
  2. What are the advantages of synchronization?
  3. What are the disadvantages of synchronization?
  4. What is a race condition and how can it be resolved?
  5. What is class-level locking?
  6. What is object-level locking?
  7. What is the difference between class-level and object-level locks?
  8. Can multiple threads execute synchronized methods simultaneously on the same object?
  9. What is a synchronized block?
  10. How do you synchronize using the current object?
  11. How do you obtain a class-level lock in a synchronized block?
  12. How do you synchronize using a particular object?
  13. What is the advantage of a synchronized block over a synchronized method?
  14. Can a thread acquire multiple locks simultaneously? Yes.
  15. What are synchronized statements?

Deadlock

Deadlock occurs when two or more threads wait indefinitely for locks held by one another.

Thread 1 holds Resource A → waits for Resource B
Thread 2 holds Resource B → waits for Resource A
Result: Both threads wait indefinitely

A classic example is one thread holding a printer-related resource while waiting for another resource, while a second thread holds that other resource and waits for the printer.

Deadlocks are difficult to resolve after they occur, so prevention and careful lock ordering are important.

Starvation

Starvation is prolonged waiting where a thread eventually gets a chance to execute, unlike deadlock where the waiting can continue indefinitely.

For example, a low-priority thread may repeatedly wait while higher-priority work receives processor time.

Daemon Thread

A daemon thread runs in the background to support non-daemon/user threads.

Examples include JVM background activities such as garbage collection and other service threads.

  • The main purpose is to provide background support.
  • Daemon status can be checked using isDaemon().
  • It can be changed using setDaemon(true/false).
  • Daemon status must be changed before the thread is started.
  • Changing daemon status after start() causes IllegalThreadStateException.
  • The main thread is non-daemon.
  • A newly created thread normally inherits daemon status from its parent.
  • When the last non-daemon thread terminates, the JVM can terminate remaining daemon threads.
Thread t = new Thread(() -> {
    System.out.println("Background work");
});

t.setDaemon(true);
t.start();