Java Programming Concurrency Study Guide

Java Multithreading - Complete Tutorial

Master Java Multithreading: Learn Thread, Runnable, Executors, ThreadPool, Synchronization, Locks, Concurrent Collections, CompletableFuture, and best practices for high-performance applications.

What is Multithreading?

Multithreading allows concurrent execution of multiple threads within a single program. Threads share the same memory space but execute independently.

Why Use Multithreading?

  • Better CPU utilization — Keep CPU busy while one thread waits for I/O
  • Improved responsiveness — UI remains responsive during background tasks
  • Faster processing — Parallel execution on multi-core processors
  • Resource sharing — Threads share memory more efficiently than processes
Diagram of Java multithreading: process, threads, shared heap, Thread and Runnable
Threads: share heap, separate stacks; use Thread or Runnable; synchronize shared mutable state.

Thread Lifecycle

A thread can be in one of six states during its lifetime:

NEWRUNNABLERUNNINGBLOCKED / WAITING / TIMED_WAITINGTERMINATED
StateDescription
NEWThread created but not started
RUNNABLEReady to run, waiting for CPU
BLOCKEDWaiting for monitor lock
WAITINGWaiting indefinitely for another thread
TIMED_WAITINGWaiting for specified time
TERMINATEDThread execution completed

Creating Threads

Method 1: Extending Thread Class

MyThread.java
public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("Thread " + Thread.currentThread().getName() + " is running");

        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": Count " + i);
            try {
                Thread.sleep(500); // Pause for 500ms
            } catch (InterruptedException e) {
                System.out.println("Thread interrupted");
            }
        }
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.setName("Thread-A");
        t2.setName("Thread-B");

        t1.start(); // Starts thread execution
        t2.start();
    }
}

Method 2: Implementing Runnable Interface (Recommended)

MyRunnable.java
public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println("Running in " + Thread.currentThread().getName());

        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " - Iteration " + i);
        }
    }

    public static void main(String[] args) {
        // Create Runnable objects
        MyRunnable task1 = new MyRunnable();
        MyRunnable task2 = new MyRunnable();

        // Create threads with Runnable
        Thread thread1 = new Thread(task1, "Worker-1");
        Thread thread2 = new Thread(task2, "Worker-2");

        thread1.start();
        thread2.start();

        // Using lambda (Java 8+)
        Thread thread3 = new Thread(() -> {
            System.out.println("Lambda thread running");
        }, "Lambda-Thread");
        thread3.start();
    }
}

Method 3: Using Lambda Expressions (Java 8+)

LambdaThreadExample.java
public class LambdaThreadExample {
    public static void main(String[] args) {
        // Simple lambda
        Thread t1 = new Thread(() -> {
            System.out.println("Thread 1 is running");
        });

        // Lambda with parameters
        Thread t2 = new Thread(() -> {
            for (int i = 1; i <= 3; i++) {
                System.out.println("Count: " + i);
                try { Thread.sleep(100); }
                catch (InterruptedException e) {}
            }
        });

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

Method 4: Using Callable and Future (Return values)

CallableExample.java
import java.util.concurrent.*;

public class CallableExample {
    public static void main(String[] args) throws Exception {
        // Create Callable that returns a result
        Callable<Integer> task = () -> {
            int sum = 0;
            for (int i = 1; i <= 10; i++) {
                sum += i;
                Thread.sleep(100);
            }
            return sum;
        };

        // Execute Callable
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<Integer> future = executor.submit(task);

        // Do other work while task runs
        System.out.println("Waiting for result...");

        // Get result (blocks until available)
        Integer result = future.get();
        System.out.println("Sum from 1 to 10 = " + result);

        executor.shutdown();
    }
}

Thread Methods and Control

Essential Thread Methods

ThreadMethodsDemo.java
public class ThreadMethodsDemo {
    public static void main(String[] args) throws InterruptedException {

        Thread worker = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("Working: " + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    System.out.println("Thread was interrupted!");
                    return;
                }
            }
        });

        // Set thread properties
        worker.setName("WorkerThread");
        worker.setPriority(Thread.MAX_PRIORITY); // 1-10, default 5
        worker.setDaemon(true); // Daemon thread exits when main thread ends

        System.out.println("Thread state before start: " + worker.getState());

        worker.start();

        System.out.println("Thread state after start: " + worker.getState());

        // Main thread sleeps
        Thread.sleep(1200);

        // Interrupt the thread
        worker.interrupt();

        // Wait for thread to finish
        worker.join(); // Main waits for worker to die

        System.out.println("Thread state after completion: " + worker.getState());
        System.out.println("Is alive? " + worker.isAlive());
    }
}

Thread Priority Example

PriorityExample.java
public class PriorityExample {
    public static void main(String[] args) {
        Thread low = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Low priority: " + i);
            }
        });

        Thread high = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("HIGH priority: " + i);
            }
        });

        low.setPriority(Thread.MIN_PRIORITY);  // 1
        high.setPriority(Thread.MAX_PRIORITY); // 10

        low.start();
        high.start();

        // Note: Priority is a hint, not guaranteed
    }
}

Synchronization and Thread Safety

When multiple threads access shared resources, synchronization prevents data corruption.

Problem: Race Condition

RaceConditionDemo.java
class Counter {
    private int count = 0;

    public void increment() {
        count++; // Not atomic! Three operations: read, increment, write
    }

    public int getCount() {
        return count;
    }
}

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

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) counter.increment();
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) counter.increment();
        });

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

        t1.join();
        t2.join();

        System.out.println("Expected: 2000, Actual: " + counter.getCount());
        // Output might be less than 2000 due to race condition!
    }
}

Solution 1: Synchronized Method

SynchronizedCounter.java
class SynchronizedCounter {
    private int count = 0;

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

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

// Now the result will always be 2000

Solution 2: Synchronized Block

SynchronizedBlockCounter.java
class SynchronizedBlockCounter {
    private int count = 0;
    private final Object lock = new Object(); // Lock object

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

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

Solution 3: Volatile Keyword

VolatileExample.java
class VolatileExample {
    private volatile boolean running = true; // Changes visible to all threads

    public void run() {
        while (running) {
            // Do work
        }
    }

    public void stop() {
        running = false; // Immediately visible to other threads
    }
}

Practical Example: Bank Account

BankDemo.java
class BankAccount {
    private double balance;
    private final Object lock = new Object();

    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    }

    public void deposit(double amount) {
        synchronized(lock) {
            balance += amount;
            System.out.println(Thread.currentThread().getName() +
                " deposited " + amount +
                ", balance: " + balance);
        }
    }

    public boolean withdraw(double amount) {
        synchronized(lock) {
            if (balance >= amount) {
                balance -= amount;
                System.out.println(Thread.currentThread().getName() +
                    " withdrew " + amount +
                    ", balance: " + balance);
                return true;
            }
            System.out.println(Thread.currentThread().getName() +
                " insufficient funds!");
            return false;
        }
    }

    public double getBalance() {
        synchronized(lock) {
            return balance;
        }
    }
}

public class BankDemo {
    public static void main(String[] args) throws InterruptedException {
        BankAccount account = new BankAccount(1000);

        // Multiple threads accessing same account
        Thread user1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                account.deposit(100);
                try { Thread.sleep(50); } catch (InterruptedException e) {}
            }
        }, "User-1");

        Thread user2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                account.withdraw(150);
                try { Thread.sleep(50); } catch (InterruptedException e) {}
            }
        }, "User-2");

        user1.start();
        user2.start();

        user1.join();
        user2.join();

        System.out.println("Final balance: " + account.getBalance());
    }
}

Inter-thread Communication

wait(), notify(), and notifyAll() coordinate producer-consumer style communication between threads.

ProducerConsumerDemo.java
class MessageQueue {
    private String message;
    private boolean hasMessage = false;

    public synchronized void put(String message) {
        while (hasMessage) {
            try {
                wait(); // Wait for consumer to consume
            } catch (InterruptedException e) {}
        }

        this.message = message;
        hasMessage = true;
        System.out.println("Produced: " + message);
        notify(); // Notify waiting consumer
    }

    public synchronized String take() {
        while (!hasMessage) {
            try {
                wait(); // Wait for producer to produce
            } catch (InterruptedException e) {}
        }

        hasMessage = false;
        System.out.println("Consumed: " + message);
        notify(); // Notify waiting producer
        return message;
    }
}

public class ProducerConsumerDemo {
    public static void main(String[] args) {
        MessageQueue queue = new MessageQueue();

        // Producer thread
        Thread producer = new Thread(() -> {
            String[] messages = {"Hello", "World", "Java", "Multithreading", "Done"};
            for (String msg : messages) {
                queue.put(msg);
                try { Thread.sleep(500); } catch (InterruptedException e) {}
            }
        });

        // Consumer thread
        Thread consumer = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                String msg = queue.take();
                try { Thread.sleep(1000); } catch (InterruptedException e) {}
            }
        });

        producer.start();
        consumer.start();
    }
}

Thread Pools (Executor Framework)

Managing threads manually is inefficient. Use ExecutorService for thread pooling.

Fixed Thread Pool

ThreadPoolExample.java
import java.util.concurrent.*;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // Create a pool of 3 threads
        ExecutorService executor = Executors.newFixedThreadPool(3);

        // Submit tasks
        for (int i = 1; i <= 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " executed by " +
                    Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {}
            });
        }

        // Shutdown executor
        executor.shutdown();

        // Wait for all tasks to complete
        try {
            executor.awaitTermination(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {}

        System.out.println("All tasks completed");
    }
}

Different Executor Types

ExecutorTypesDemo.java
import java.util.concurrent.*;

public class ExecutorTypesDemo {
    public static void main(String[] args) {
        // 1. Fixed thread pool - Reuses fixed number of threads
        ExecutorService fixedPool = Executors.newFixedThreadPool(5);

        // 2. Cached thread pool - Creates threads as needed, reuses idle ones
        ExecutorService cachedPool = Executors.newCachedThreadPool();

        // 3. Single thread executor - One thread, tasks execute sequentially
        ExecutorService singlePool = Executors.newSingleThreadExecutor();

        // 4. Scheduled thread pool - For delayed/recurring tasks
        ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2);

        // Schedule task with delay
        scheduledPool.schedule(() -> {
            System.out.println("Delayed task executed");
        }, 3, TimeUnit.SECONDS);

        // Schedule recurring task
        scheduledPool.scheduleAtFixedRate(() -> {
            System.out.println("Recurring task");
        }, 1, 2, TimeUnit.SECONDS);

        // Shutdown after some time
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {}

        fixedPool.shutdown();
        cachedPool.shutdown();
        singlePool.shutdown();
        scheduledPool.shutdown();
    }
}

Concurrency Utilities

CountDownLatch

CountDownLatchExample.java
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3); // Wait for 3 tasks

        // Create worker threads
        for (int i = 1; i <= 3; i++) {
            final int workerId = i;
            new Thread(() -> {
                System.out.println("Worker " + workerId + " is working");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {}
                System.out.println("Worker " + workerId + " finished");
                latch.countDown(); // Decrement counter
            }).start();
        }

        System.out.println("Main thread waiting for workers...");
        latch.await(); // Wait until count reaches 0
        System.out.println("All workers completed! Main thread proceeds.");
    }
}

CyclicBarrier

CyclicBarrierExample.java
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(3, () -> {
            System.out.println("All threads reached barrier! Let's proceed.");
        });

        for (int i = 1; i <= 3; i++) {
            final int threadId = i;
            new Thread(() -> {
                System.out.println("Thread " + threadId + " is working...");
                try {
                    Thread.sleep(threadId * 1000);
                    System.out.println("Thread " + threadId + " reached barrier");
                    barrier.await(); // Wait for others
                    System.out.println("Thread " + threadId + " passed barrier");
                } catch (InterruptedException | BrokenBarrierException e) {}
            }).start();
        }
    }
}

Semaphore

SemaphoreExample.java
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(2); // Allow 2 concurrent accesses

        for (int i = 1; i <= 5; i++) {
            final int threadId = i;
            new Thread(() -> {
                try {
                    System.out.println("Thread " + threadId + " waiting for permit");
                    semaphore.acquire(); // Acquire permit
                    System.out.println("Thread " + threadId + " acquired permit");

                    // Critical section
                    Thread.sleep(2000);

                    semaphore.release(); // Release permit
                    System.out.println("Thread " + threadId + " released permit");
                } catch (InterruptedException e) {}
            }).start();
        }
    }
}

CompletableFuture (Java 8+)

CompletableFutureExample.java
import java.util.concurrent.CompletableFuture;

public class CompletableFutureExample {
    public static void main(String[] args) throws Exception {
        // Async computation
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("Computing in " + Thread.currentThread().getName());
            return "Hello";
        }).thenApply(result -> {
            return result + " World";
        }).thenApply(String::toUpperCase);

        // Chain multiple operations
        CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 10)
            .thenApply(num -> num * 2)
            .thenApply(num -> num + 5);

        // Combine two futures
        CompletableFuture<String> combined = future.thenCombine(future2,
            (str, num) -> str + " - " + num);

        System.out.println(combined.get()); // HELLO WORLD - 25

        // Handle exceptions
        CompletableFuture<Integer> errorFuture = CompletableFuture.supplyAsync(() -> {
            if (true) throw new RuntimeException("Error occurred");
            return 42;
        }).exceptionally(ex -> {
            System.out.println("Error: " + ex.getMessage());
            return 0;
        });

        System.out.println(errorFuture.get()); // 0
    }
}

Atomic Variables and Locks

Atomic Variables (java.util.concurrent.atomic)

AtomicExample.java
import java.util.concurrent.atomic.*;

public class AtomicExample {
    public static void main(String[] args) throws InterruptedException {
        // AtomicInteger - Thread-safe integer operations
        AtomicInteger counter = new AtomicInteger(0);

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.incrementAndGet(); // Atomic increment
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.incrementAndGet();
            }
        });

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

        System.out.println("Atomic counter: " + counter.get()); // Always 2000

        // Other atomic types
        AtomicBoolean flag = new AtomicBoolean(false);
        AtomicLong longValue = new AtomicLong(100L);
        AtomicReference<String> reference = new AtomicReference<>("Hello");

        // Compare and set
        reference.compareAndSet("Hello", "World");
        System.out.println(reference.get()); // World

        // Atomic operations
        int oldValue = counter.getAndSet(500);
        int updated = counter.addAndGet(100);
        System.out.println("Old: " + oldValue + ", Updated: " + updated);
    }
}

ReentrantLock

ReentrantLockDemo.java
import java.util.concurrent.locks.*;

class LockedCounter {
    private int count = 0;
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();

    public void increment() {
        lock.lock();
        try {
            count++;
            condition.signalAll(); // Notify waiting threads
        } finally {
            lock.unlock(); // Always unlock in finally block
        }
    }

    public void waitForValue(int target) throws InterruptedException {
        lock.lock();
        try {
            while (count < target) {
                condition.await(); // Wait for condition
            }
            System.out.println("Target " + target + " reached!");
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

public class ReentrantLockDemo {
    public static void main(String[] args) {
        LockedCounter counter = new LockedCounter();

        // Thread waiting for counter to reach 5
        Thread waiter = new Thread(() -> {
            try {
                counter.waitForValue(5);
            } catch (InterruptedException e) {}
        });

        // Thread incrementing counter
        Thread incrementer = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                counter.increment();
                System.out.println("Count: " + counter.getCount());
                try { Thread.sleep(500); } catch (InterruptedException e) {}
            }
        });

        waiter.start();
        incrementer.start();
    }
}

Deadlock and Prevention

What is Deadlock?

DeadlockExample.java
class DeadlockExample {
    private static final Object LOCK1 = new Object();
    private static final Object LOCK2 = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized(LOCK1) {
                System.out.println("Thread 1: Holding LOCK1");
                try { Thread.sleep(100); } catch (InterruptedException e) {}

                System.out.println("Thread 1: Waiting for LOCK2");
                synchronized(LOCK2) {
                    System.out.println("Thread 1: Acquired LOCK2");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized(LOCK2) {
                System.out.println("Thread 2: Holding LOCK2");
                try { Thread.sleep(100); } catch (InterruptedException e) {}

                System.out.println("Thread 2: Waiting for LOCK1");
                synchronized(LOCK1) {
                    System.out.println("Thread 2: Acquired LOCK1");
                }
            }
        });

        t1.start();
        t2.start();
        // DEADLOCK! Both threads waiting for each other's locks
    }
}

Preventing Deadlock

DeadlockPrevention.java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

class DeadlockPrevention {
    private static final Object LOCK1 = new Object();
    private static final Object LOCK2 = new Object();

    // Solution 1: Always acquire locks in same order
    static class Solution1 {
        public void method1() {
            synchronized(LOCK1) {
                synchronized(LOCK2) {
                    // Safe - same order as method2
                }
            }
        }

        public void method2() {
            synchronized(LOCK1) {
                synchronized(LOCK2) {
                    // Safe - same order
                }
            }
        }
    }

    // Solution 2: Use tryLock() with timeout
    static class Solution2 {
        private final ReentrantLock lock1 = new ReentrantLock();
        private final ReentrantLock lock2 = new ReentrantLock();

        public void safeMethod() {
            boolean locked1 = false;
            boolean locked2 = false;

            try {
                locked1 = lock1.tryLock(1, TimeUnit.SECONDS);
                locked2 = lock2.tryLock(1, TimeUnit.SECONDS);

                if (locked1 && locked2) {
                    // Perform operations
                }
            } catch (InterruptedException e) {
                System.out.println("Could not acquire locks");
            } finally {
                if (locked1) lock1.unlock();
                if (locked2) lock2.unlock();
            }
        }
    }
}

Command Line Arguments in Multithreading

Pass command-line arguments to configure multithreaded applications:

ThreadConfigFromArgs.java
import java.util.concurrent.*;

public class ThreadConfigFromArgs {

    static class Config {
        int numThreads;
        int taskCount;
        int delayMs;

        Config(String[] args) {
            // Parse command line arguments
            this.numThreads = args.length > 0 ? Integer.parseInt(args[0]) : 4;
            this.taskCount = args.length > 1 ? Integer.parseInt(args[1]) : 20;
            this.delayMs = args.length > 2 ? Integer.parseInt(args[2]) : 100;
        }
    }

    public static void main(String[] args) {
        Config config = new Config(args);

        System.out.println("Configuration:");
        System.out.println("Threads: " + config.numThreads);
        System.out.println("Tasks: " + config.taskCount);
        System.out.println("Delay: " + config.delayMs + "ms");

        // Create thread pool based on config
        ExecutorService executor = Executors.newFixedThreadPool(config.numThreads);

        // Submit tasks
        for (int i = 0; i < config.taskCount; i++) {
            final int taskId = i;
            executor.submit(() -> {
                try {
                    System.out.println("Task " + taskId + " started by " +
                        Thread.currentThread().getName());
                    Thread.sleep(config.delayMs);
                    System.out.println("Task " + taskId + " completed");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        executor.shutdown();
        try {
            executor.awaitTermination(1, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("All tasks completed");
    }
}

Running with arguments:

Run from terminal
java ThreadConfigFromArgs 8 100 50
# 8 threads, 100 tasks, 50ms delay per task

Real-World Examples

This tutorial includes several patterns you will see in production code:

ExampleUse CaseSection
BankDemo Thread-safe shared resource (bank account deposits and withdrawals) Synchronization
ProducerConsumerDemo Producer-consumer coordination with wait() / notify() Inter-thread Communication
ThreadConfigFromArgs Configurable thread pool driven by command-line arguments Command Line Arguments

Together, these examples show how to protect shared state, coordinate work between threads, and scale concurrency settings without changing source code.

Best Practices and Common Pitfalls

DO's

Recommended patterns
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
import java.util.*;

// 1. Always use thread-safe collections
Map<String, String> map = new ConcurrentHashMap<>();
List<String> list = new CopyOnWriteArrayList<>();
Queue<String> queue = new ConcurrentLinkedQueue<>();

// 2. Use ExecutorService instead of raw threads
ExecutorService executor = Executors.newFixedThreadPool(10);

// 3. Always release locks in finally block
Lock lock = new ReentrantLock();
lock.lock();
try {
    // critical section
} finally {
    lock.unlock();
}

// 4. Use volatile for flags
private volatile boolean running = true;

// 5. Prefer higher-level concurrency utilities
CountDownLatch latch = new CountDownLatch(3);

DON'Ts

Patterns to avoid
// 1. Don't use Thread.stop() (deprecated and unsafe)
// Thread.stop(); // DON'T DO THIS

// 2. Don't ignore InterruptedException
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    // DON'T just print - restore interrupted status
    Thread.currentThread().interrupt();
}

// 3. Don't synchronize on non-final objects
String s = "hello";
synchronized(s) { // BAD - s might change
    // ...
}

// 4. Don't call Thread.sleep() in synchronized blocks
synchronized(lock) {
    Thread.sleep(1000); // BAD - holding lock while sleeping
}

// 5. Don't use Vector or Hashtable (use concurrent collections instead)
// Vector<String> v = new Vector<>(); // Consider ArrayList with synchronization

Summary Table

ConceptWhen to UseExample
ThreadSimple background tasksnew Thread(() -> {}).start()
RunnableTask that doesn't return resultexecutor.submit(() -> {})
CallableTask that returns resultFuture<T> result = executor.submit(callable)
synchronizedSimple mutual exclusionsynchronized(this) { }
ReentrantLockAdvanced locking (tryLock, conditions)lock.lock(); try {} finally { lock.unlock(); }
ExecutorServiceManaging thread poolsExecutors.newFixedThreadPool(10)
BlockingQueueProducer-consumer patternsnew ArrayBlockingQueue<>(5)
CountDownLatchWaiting for multiple threadslatch.await(); latch.countDown()
AtomicIntegerSimple counterscounter.incrementAndGet()
CompletableFutureAsynchronous pipelinesfuture.thenApply().thenAccept()