Advanced⏱️ 13 min📘 Topic 20 of 22

🧵 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 finish
Output:
hello 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();
Output:
42

AtomicInteger avoids a race

AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();
counter.addAndGet(5);
System.out.println(counter.get());
Output:
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
start() creates a new thread and the JVM calls run() on it concurrently. Calling run() directly just executes the code on the current thread — no new thread is created.
Q2.What's the difference between synchronized and volatile?Intermediate
synchronized provides mutual exclusion (atomicity) AND visibility via a lock. volatile provides only visibility — guaranteeing reads see the latest write — but not atomicity for compound operations like count++.
Q3.What is a deadlock and how do you prevent it?Intermediate
A deadlock occurs when two or more threads each hold a lock the other needs, so none can proceed. Prevent it by acquiring locks in a consistent global order, using tryLock with timeouts, or minimizing lock scope.
Q4.Why prefer ExecutorService over creating threads manually?Intermediate
Thread pools reuse threads (avoiding creation overhead), bound concurrency, queue tasks, and provide lifecycle management (shutdown), Future results, and scheduling — far safer and more scalable than raw Thread objects.
Q5.What's the difference between Runnable and Callable?Advanced
Runnable.run() returns void and can't throw checked exceptions. Callable.call() returns a value and can throw checked exceptions. Submit a Callable to an ExecutorService to get a Future holding the result.

🧠 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).