🧵 Java Multithreading and Concurrency — Threads, synchronized, Executors
Master Java multithreading and concurrency — Thread vs Runnable, synchronized, volatile, deadlocks, the ExecutorService, Callable/Future and thread safety. With examples and interview Q&A.
Concurrency lets a program do many things at once. It's powerful and a top senior-interview topic — race conditions and deadlocks are the hard part.
🧵 Creating threads
// Prefer Runnable (composition over inheritance)
Runnable task = () -> System.out.println("working");
Thread t = new Thread(task);
t.start(); // start() spawns a new thread; run() would NOT🔐 synchronized & the race condition
When threads share mutable state, you get race conditions. synchronized gives mutual exclusion — only one thread holds the lock at a time:
private int count = 0;
synchronized void inc() { count++; } // atomic now📡 volatile
volatile guarantees visibility — writes by one thread are seen by others immediately. It does NOT give atomicity (use synchronized or AtomicInteger for compound actions).
⚙️ ExecutorService — the modern way
Don't manage raw threads. Use a thread pool:
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(() -> 2 + 2);
int result = f.get(); // 4
pool.shutdown();💀 Deadlock
Two threads each hold a lock the other needs → frozen forever. Avoid by always acquiring locks in the same order, or using timeouts. This is a classic system design interview topic too.
🧰 java.util.concurrent
AtomicInteger, ConcurrentHashMap, CountDownLatch, BlockingQueue — battle-tested concurrent building blocks. Prefer them over hand-rolled locking.
💻 Code Examples
Runnable + Thread
Runnable task = () -> System.out.println("hello from " + Thread.currentThread().getName());
Thread t = new Thread(task, "worker-1");
t.start();
t.join(); // wait for it to finishhello from worker-1
ExecutorService with Future
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> f = pool.submit(() -> {
Thread.sleep(50);
return 21 * 2;
});
System.out.println(f.get());
pool.shutdown();42
AtomicInteger avoids a race
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
counter.addAndGet(5);
System.out.println(counter.get());6
⚠️ Common Mistakes
- Calling run() instead of start() — run() executes on the current thread; only start() spawns a new one.
- Using volatile for compound actions like count++ — it's not atomic; use synchronized or AtomicInteger.
- Acquiring locks in inconsistent order across threads — classic deadlock.
- Managing raw Threads instead of an ExecutorService — error-prone and doesn't scale; use thread pools.
🎯 Interview Questions
Real questions asked at top product and service-based companies.
Q1.What's the difference between start() and run()?Beginner
Q2.What's the difference between synchronized and volatile?Intermediate
Q3.What is a deadlock and how do you prevent it?Intermediate
Q4.Why prefer ExecutorService over creating threads manually?Intermediate
Q5.What's the difference between Runnable and Callable?Advanced
🧠 Quick Summary
- Prefer Runnable/Callable over extending Thread; call start(), not run().
- synchronized = atomicity + visibility; volatile = visibility only.
- Deadlock: avoid by consistent lock ordering / timeouts.
- Use ExecutorService thread pools + Future, not raw threads.
- Reach for java.util.concurrent (AtomicInteger, ConcurrentHashMap).