⚡IntermediateConcurrency Basics

Threads and Runnable

Create and manage threads using Thread and Runnable to execute code concurrently.

Overview

A thread is an independent unit of execution within a program. Java supports multithreading natively: multiple threads share the same memory space and can run simultaneously on multi-core processors. You create a thread by either implementing the Runnable interface or extending Thread. The recommended approach is implementing Runnable (or using a lambda) because it separates the task from the thread mechanism and allows the task to extend other classes. Threads are started with start() (not run()). The JVM schedules when each thread actually executes.

Key Points

  • •Runnable: implement void run() and pass to new Thread(runnable)
  • •Thread subclass: extend Thread and override run()
  • •thread.start() creates a new thread; thread.run() calls run() on the current thread (not concurrent)
  • •thread.join() waits for the thread to finish before continuing
  • •thread.sleep(milliseconds) pauses the current thread
  • •Thread.currentThread() returns the currently executing thread
  • •Threads share heap memory; each thread has its own stack
  • •The main method runs in the main thread; spawned threads run in parallel

Examples

Creating threads with Runnable and lambda

public class Main {
    public static void main(String[] args) throws InterruptedException {
        // Using Runnable (anonymous class)
        Runnable task1 = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    System.out.println("Task1: " + i + " on " + Thread.currentThread().getName());
                }
            }
        };

        // Using lambda (cleaner)
        Runnable task2 = () -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("Task2: " + i + " on " + Thread.currentThread().getName());
            }
        };

        Thread t1 = new Thread(task1, "worker-1");
        Thread t2 = new Thread(task2, "worker-2");

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

        // Wait for both to finish
        t1.join();
        t2.join();
        System.out.println("Both threads done");
    }
}

Output order between t1 and t2 is non-deterministic: the JVM scheduler decides. Always call start() not run(). join() ensures main() waits for threads to complete before exiting.

Thread lifecycle: sleep and state

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread counter = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Count: " + i);
                try {
                    Thread.sleep(100); // pause 100ms
                } catch (InterruptedException e) {
                    System.out.println("Thread interrupted!");
                    return; // exit thread on interrupt
                }
            }
        }, "counter");

        System.out.println("State before start: " + counter.getState()); // NEW
        counter.start();
        System.out.println("State after start: " + counter.getState());  // RUNNABLE or TIMED_WAITING

        counter.join();
        System.out.println("State after join: " + counter.getState());   // TERMINATED
    }
}

Thread.sleep() throws InterruptedException if the thread is interrupted while sleeping. Always handle it: either stop the thread or restore the interrupt flag with Thread.currentThread().interrupt().

Parallel computation: splitting work across threads

public class Main {
    static long sumRange(long start, long end) {
        long sum = 0;
        for (long i = start; i <= end; i++) sum += i;
        return sum;
    }

    public static void main(String[] args) throws InterruptedException {
        long total = 100_000_000L;
        long mid = total / 2;

        long[] results = new long[2];

        Thread t1 = new Thread(() -> results[0] = sumRange(1, mid));
        Thread t2 = new Thread(() -> results[1] = sumRange(mid + 1, total));

        long startTime = System.currentTimeMillis();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        long elapsed = System.currentTimeMillis() - startTime;

        System.out.println("Sum: " + (results[0] + results[1]));
        System.out.println("Time: " + elapsed + "ms");
    }
}

Sharing an array between threads (results[0] and results[1]) is safe here because each thread writes to a different index. Concurrent writes to the same index would require synchronization.

Common Mistakes

  • Calling thread.run() instead of thread.start(). run() executes on the calling thread without creating a new thread. Only start() spawns a new thread
  • Not calling join() when you need the thread to finish before using its results. Without join(), you may read results before the thread writes them
  • Catching InterruptedException and doing nothing with it. At minimum, re-throw or restore the interrupt flag: Thread.currentThread().interrupt()

Note

For simple concurrent tasks, prefer ExecutorService (covered in the next topic) over managing Thread objects directly. ExecutorService handles thread pooling, lifecycle, and result collection more cleanly.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 4 exercises

Knowledge Check

Question 1 of 4

What is the difference between calling thread.run() and thread.start()?