Multithreading in Java

    Atul Kabra3 min readUpdated

    Multithreading lets a Java program do several things at once by running multiple threads — independent paths of execution within the same program. A program can download data on one thread while keeping its interface responsive on another. Java has first-class threading support, but concurrency brings a cost: when threads share data they can interfere with each other (race conditions), so you must coordinate access with synchronization. This article shows the safe, modern way to start.

    Creating a thread with Runnable

    The cleanest approach is to define the work as a Runnable and hand it to a Thread.

    public class Demo {
        public static void main(String[] args) {
            // A Runnable describes the work to run on a thread.
            Runnable task = () -> {
                for (int i = 1; i <= 3; i++) {
                    System.out.println("Worker: " + i);
                }
            };
    
            Thread t = new Thread(task);
            t.start();   // start() runs the task on a NEW thread (do not call run())
            System.out.println("Main thread continues");
        }
    }
    

    Call start(), not run(). start() launches a new thread; run() would just execute the code on the current thread, defeating the purpose.

    The thread lifecycle

    A thread moves through states: new (created but not started), runnable (eligible to run), blocked/waiting (paused), and terminated (finished). The JVM scheduler decides which runnable thread actually runs at any moment, so output order across threads is not guaranteed.

    Thread t = new Thread(() -> System.out.println("Hello from a thread"));
    t.start();
    t.join(); // wait here until t finishes before continuing
    System.out.println("Thread done");
    

    join() makes the calling thread wait for another to complete.

    Race conditions and synchronization

    When two threads update the same data without coordination, the result can be wrong. The classic example is an unguarded counter.

    class Counter {
        private int count = 0;
    
        // 'synchronized' lets only one thread run this method at a time on this object.
        public synchronized void increment() {
            count++; // read-modify-write must be atomic to be correct
        }
        public synchronized int get() { return count; }
    }
    

    The synchronized keyword grants one thread exclusive access at a time, preventing the lost-update problem. For simple counters, classes in java.util.concurrent.atomic (such as AtomicInteger) are an easier, lock-free alternative.

    Want to learn this properly?

    Join the waitlist for our courses — beginner-friendly, project-first classes in Jalgaon.

    Browse courses

    Prefer an ExecutorService

    Manually creating threads is rarely the best approach. An ExecutorService manages a pool of threads for you, which is more efficient and easier to reason about.

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class PoolDemo {
        public static void main(String[] args) {
            ExecutorService pool = Executors.newFixedThreadPool(2);
            for (int i = 1; i <= 4; i++) {
                int taskId = i;
                pool.submit(() -> System.out.println("Task " + taskId
                    + " on " + Thread.currentThread().getName()));
            }
            pool.shutdown(); // stop accepting new tasks; let running ones finish
        }
    }
    

    The pool reuses a fixed number of threads across many tasks instead of creating a fresh thread each time.

    Common mistakes

    • Calling run() instead of start(). That runs the code on the current thread — no concurrency happens.
    • Sharing mutable data without synchronization. Leads to race conditions and intermittent, hard-to-reproduce bugs.
    • Assuming output order. Thread scheduling is non-deterministic; interleaved output is normal.
    • Forgetting to shut down an ExecutorService. The program may not exit because pool threads keep it alive. Call shutdown().
    • Over-synchronizing. Locking everything kills the benefit of threads and can cause deadlocks. Synchronize only the shared state that needs it.

    FAQ

    Is multithreading the same as parallelism? Not exactly. Concurrency is about managing many tasks; parallelism is running them literally at the same time on multiple CPU cores. Multithreading enables both.

    Are threads hard? The basics are approachable, but correct shared-state coordination is genuinely tricky. Lean on java.util.concurrent utilities rather than hand-rolling locks.

    Keep going

    Next, connect to a database with JDBC: Connecting Java to a Database, or revisit The Java Collections Framework. All tutorials are on the Java hub; for guided practice in Jalgaon, see the Java course.


    Learn safe concurrency at Infoplanet, Jalgaon. Join the waitlist for the Java course.

    Want to learn this properly?

    Join the waitlist for our courses — beginner-friendly, project-first classes in Jalgaon.

    Browse courses
    Atul Kabra

    Founder, Infoplanet

    Atul Kabra founded Infoplanet in 2001 and has spent over two decades teaching programming — C, C++, Java, databases and more — to students across Maharashtra.

    Related guides