Tutorials Logic, IN info@tutorialslogic.com
Navigation
Home About Us Contact Us Blogs FAQs
Tutorials
All Tutorials
Services
Academic Projects Resume Writing Website Development
Practice
Quiz Challenge Interview Questions Certification Practice
Tools
Online Compiler JSON Formatter Regex Tester CSS Unit Converter Color Picker
Compiler Tools

Multithreading in Java Thread, Runnable, Sync: Tutorial, Examples, FAQs & Interview Tips

Thread Lifecycle

StateDescription
NEWThread created but start() not yet called.
RUNNABLEThread is ready to run or currently running.
BLOCKEDWaiting to acquire a monitor lock.
WAITINGWaiting indefinitely for another thread (e.g., wait()).
TIMED_WAITINGWaiting for a specified time (e.g., sleep(), join(timeout)).
TERMINATEDThread has finished execution.

Creating Threads

extends Thread vs implements Runnable
// Approach 1: extend Thread
class CounterThread extends Thread {
    private String name;
    public CounterThread(String name) { this.name = name; }

    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println(name + ": " + i);
            try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
        }
    }
}

// Approach 2: implement Runnable (preferred - allows extending another class)
class PrintTask implements Runnable {
    private String message;
    public PrintTask(String message) { this.message = message; }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + message);
        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        // Approach 1
        CounterThread t1 = new CounterThread("Thread-A");
        CounterThread t2 = new CounterThread("Thread-B");
        t1.start();
        t2.start();
        t1.join();  // wait for t1 to finish
        t2.join();

        // Approach 2
        Thread t3 = new Thread(new PrintTask("Hello"), "Worker-1");
        Thread t4 = new Thread(() -> System.out.println("Lambda thread!"), "Worker-2");
        t3.start();
        t4.start();
    }
}

synchronized & wait/notify

synchronized & ExecutorService
import java.util.concurrent.*;

class Counter {
    private int count = 0;

    // synchronized - only one thread can execute this at a time
    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

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

        // Create 10 threads, each incrementing 1000 times
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) counter.increment();
            });
            threads[i].start();
        }
        for (Thread t : threads) t.join();
        System.out.println("Final count: " + counter.getCount());  // 10000

        // ExecutorService - thread pool (preferred over raw threads)
        ExecutorService executor = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 8; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " on " + Thread.currentThread().getName());
            });
        }
        executor.shutdown();
        executor.awaitTermination(5, TimeUnit.SECONDS);
    }
}
Key Takeaways
  • A thread is the smallest unit of execution. Java supports multithreading via Thread class and Runnable interface.
  • Extend Thread or implement Runnable - prefer Runnable as Java does not support multiple inheritance.
  • Use synchronized keyword to prevent race conditions when multiple threads access shared data.
  • ExecutorService manages a thread pool - more efficient than creating new threads manually.
  • volatile keyword ensures visibility of variable changes across threads - but does not ensure atomicity.
  • Use AtomicInteger, AtomicLong for thread-safe counter operations without synchronized blocks.
  • Deadlock occurs when two threads wait for each other - always acquire locks in the same order.
Common Mistakes to Avoid
WRONG new Thread(task).start() in a loop
RIGHT ExecutorService pool = Executors.newFixedThreadPool(4)
Creating new threads for every task is expensive. Use a thread pool to reuse threads and control concurrency.
WRONG int count = 0; count++ in multiple threads
RIGHT AtomicInteger count = new AtomicInteger(0); count.incrementAndGet()
count++ is not atomic - it is read-modify-write. Use AtomicInteger or synchronized for thread-safe counters.

Frequently Asked Questions

A process is an independent program with its own memory space. A thread is a lightweight unit within a process that shares memory with other threads. Threads are faster to create and communicate, but require synchronization.

A synchronized method locks the entire object (this). A synchronized block locks only a specific object for a specific section of code - more granular and better for performance.

Deadlock occurs when Thread A holds Lock 1 and waits for Lock 2, while Thread B holds Lock 2 and waits for Lock 1. Prevent it by: always acquiring locks in the same order, using tryLock() with timeout, or using higher-level concurrency utilities.

sleep() pauses the thread for a fixed time and does NOT release the lock. wait() releases the lock and waits until notify() or notifyAll() is called. wait() must be called inside a synchronized block.

Ready to Level Up Your Skills?

Explore 500+ free tutorials across 20+ languages and frameworks.