Hello and welcome to Bits and Stories! In today’s post, we’ll explore Java Multithreading—one of the most essential and exciting features in Java for creating robust, high-performance applications. If you’re looking to harness the power of multithreading to make your programs faster and more responsive, you’ve come to the right place. In this comprehensive guide, we’ll explain what multithreading is, how to use it, and provide copy-and-paste-ready code examples (complete with explanations!) that you can test in your IDE. Let’s dive in!


1. Introduction to Java Multithreading

Multithreading enables a program to execute multiple threads simultaneously. A thread is the smallest unit of a process that can execute independently. Java, with its built-in multithreading support, empowers developers to create efficient and responsive applications.

Advantages of Multithreading

  • Improved Performance: Tasks can run in parallel, leveraging multi-core processors for better speed and efficiency.
  • Better Resource Utilization: CPU idle time is minimized, as time is utilized effectively.
  • Ease in Modeling: Perfect for animations, background operations, or handling multiple client requests.
  • Responsive Applications: Long-running tasks can be delegated to separate threads, ensuring the application remains interactive.

Single-Threaded vs. Multithreaded Processes

  • Single-threaded: Tasks execute sequentially. Example: A simple script where statements are executed one after the other.
  • Multithreaded: Tasks execute concurrently. Example: A web server handling thousands of clients simultaneously.

2. Creating Threads in Java

There are two primary ways to create threads in Java:

a) By Extending the Thread Class

Create a subclass of Thread and override the run() method.

Example:

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

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

b) By Implementing the Runnable Interface

Separate the task logic from the thread by implementing Runnable.

Example:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable thread running: " + Thread.currentThread().getName());
    }
}

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

Comparing Approaches

  • Extending Thread: Ties logic directly to the thread, limiting flexibility.
  • Implementing Runnable: Promotes better object-oriented design, as it allows the same class to extend other classes.

3. Thread Lifecycle

Threads in Java transition through the following states:

  1. New: Created but not started (Thread t = new Thread();).
  2. Runnable: Ready to run, waiting for CPU time.
  3. Running: CPU is executing the thread’s code.
  4. Blocked/Waiting: Waiting for a resource or signal.
  5. Terminated: Execution is complete.

Thread Lifecycle Diagram:

NEW -> RUNNABLE -> RUNNING -> TERMINATED
|
WAITING

4. Thread Methods

Java provides various methods to manage thread behavior:

  • start(): Begins execution and invokes the run() method.
  • run(): Contains the thread’s logic.
  • sleep(milliseconds): Suspends the thread for the specified time.

Example:

try {
    Thread.sleep(1000); // Sleep for 1 second
} catch (InterruptedException e) {
    e.printStackTrace();
}
  • join(): Waits for a thread to complete.

Example:

Thread thread = new Thread(() -> {
    System.out.println("Thread running...");
});
thread.start();
thread.join(); // Main thread waits for this thread to finish
  • yield(): Suggests the thread scheduler that the current thread is willing to yield CPU time.
  • interrupt(): Interrupts a sleeping or waiting thread.

5. Synchronization in Java

To avoid thread interference or memory inconsistency when sharing resources, Java provides synchronization mechanisms.

Synchronized Methods

Allows only one thread to execute the method at a time.

Example:

class Counter {
    private int count = 0;

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

    public int getCount() {
        return count;
    }
}

Synchronized Blocks

Synchronize specific sections of code to minimize the lock’s scope.

Example:

synchronized (this) {
    // Critical section
}

6. Inter-Thread Communication

Java uses wait(), notify(), and notifyAll() for communication between threads.

Example:

class SharedResource {
    synchronized void waitForSignal() throws InterruptedException {
        wait(); // Wait for a signal
    }

    synchronized void sendSignal() {
        notify(); // Notify one waiting thread
    }
}

7. Thread Priorities and Scheduling

Threads have priorities ranging from 1 (MIN_PRIORITY) to 10 (MAX_PRIORITY). The default priority is 5.

Example:

Thread t1 = new Thread();
t1.setPriority(Thread.MAX_PRIORITY);
  • Thread Scheduling: Depends on the OS, using either preemptive or time-slicing mechanisms.

8. Concurrency Utilities

Java’s java.util.concurrent package provides high-level abstractions for multithreading.

a) Executors and Thread Pools

Efficiently manage thread pools.

Example:

ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> System.out.println("Task 1"));
executor.shutdown();

b) Callable and Future

Return results from threads.

Example:

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> 42);
System.out.println("Result: " + future.get());
executor.shutdown();

c) Synchronizers

  • CountDownLatch: Waits for multiple threads to finish.
  • CyclicBarrier: Synchronizes threads at a common point.

Example:

CountDownLatch latch = new CountDownLatch(3);
Runnable task = () -> {
    System.out.println("Task completed");
    latch.countDown();
};
new Thread(task).start();
latch.await();

9. Best Practices and Considerations

  • Thread Pools: Avoid creating unnecessary threads; use thread pools.
  • Shared Resources: Use immutable objects or thread-local variables to minimize synchronization.
  • Exception Handling: Handle exceptions properly to avoid unpredictable behavior.
  • Avoid Deadlocks: Manage synchronized blocks carefully to prevent circular dependencies.
  • Modern Features: Leverage virtual threads introduced by Project Loom for lightweight concurrency.

Related Reading

If you enjoyed this tutorial, be sure to check out our friendly introduction to Java Streams. Java Streams offer a modern and powerful way to process data efficiently.


By following these best practices and using Java’s latest features, you can build robust and maintainable multithreaded applications. For more insightful programming content, visit Bits and Stories.