Multithreading in Java: Threads, Synchronisation, Wait/Notify & Concurrent Locks
This post is a comprehensive guide to Java Multithreading – including threads, synchronisation, wait/notify, concurrent locks and semaphores with practical examples.
In a Nutshell
Multithreading enables parallel execution of tasks. Synchronisation ensures consistent data access, while concurrent locks provide modern mechanisms for controlling threads.
Compact Technical Description
Multithreading is the ability of a program to execute multiple threads simultaneously. Each thread has its own stack but shares heap memory with other threads.
Basic Concepts:
Thread Lifecycle
- NEW: Thread created but not yet started
- RUNNABLE: Ready for execution (running or ready)
- BLOCKED: Waiting for monitor lock
- WAITING: Waiting indefinitely for a condition
- TIMED_WAITING: Waiting with time limit
- TERMINATED: Thread terminated
Synchronisation Mechanisms
- synchronized: Monitor-based synchronisation
- wait()/notify(): Inter-thread communication
- ReentrantLock: Flexible lock mechanism
- ReadWriteLock: Separate read/write locks
- Semaphore: Counter-based access control
- CountDownLatch: Waiting for multiple threads
- CyclicBarrier: Synchronisation point for threads
Thread Safety
- Immutable Objects: Naturally thread-safe
- Thread-Local: Thread-specific data
- Volatile: Visibility of variables
- Atomic Classes: Lock-free operations
Exam-Relevant Key Points
- Threads: Lightweight processes with own stack
- Synchronisation: Protection of shared resources
- Monitor: Object-based synchronisation mechanism
- wait/notify: Inter-thread communication
- Deadlock: Deadlock through mutual blocking
- Race Condition: Uncontrolled access to shared data
- Volatile: Guarantees visibility between threads
- IHK-relevant: Important for performant, parallel applications
Core Components
- Thread Management: Creation, control, lifecycle
- Synchronisation: synchronized, locks, semaphores
- Inter-Thread Communication: wait/notify, blocking queues
- Concurrent Collections: Thread-safe data structures
- Thread Pools: ExecutorService, ThreadPoolExecutor
- Thread Safety: Immutable, volatile, atomic classes
- Performance: Lock granularity, contention reduction
- Debugging: Thread dumps, race conditions, deadlocks
Practical Examples
1. Basic Thread Operations
public class ThreadGrundlagen {
// Thread durch Vererbung von Thread
static class MeinThread extends Thread {
private String name;
public MeinThread(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(name + " - Zählung: " + i);
try {
Thread.sleep(500); // 500ms Pause
} catch (InterruptedException e) {
System.out.println(name + " wurde unterbrochen");
return;
}
}
System.out.println(name + " beendet");
}
}
// Thread durch Implementierung von Runnable
static class MeinRunnable implements Runnable {
private String name;
public MeinRunnable(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println(name + " - Arbeit: " + i);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
System.out.println(name + " unterbrochen");
return;
}
}
System.out.println(name + " fertig");
}
}
// Lambda-Ausdruck als Runnable
static void lambdaThreadDemo() {
Thread lambdaThread = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
System.out.println("Lambda Thread - Schritt " + i);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
return;
}
}
});
lambdaThread.start();
}
public static void main(String[] args) {
System.out.println("=== Thread-Grundlagen ===");
// Thread durch Vererbung
MeinThread thread1 = new MeinThread("Thread-1");
thread1.start();
// Thread durch Runnable
Thread thread2 = new Thread(new MeinRunnable("Runnable-1"));
thread2.start();
// Lambda Thread
lambdaThreadDemo();
// Auf Threads warten
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hauptthread beendet");
}
}
2. Synchronisation with synchronized
public class SynchronisationDemo {
// Gemeinsame Ressource
static class Zaehler {
private int wert = 0;
// Synchronisierte Methode
public synchronized void erhoehen() {
wert++;
System.out.println(Thread.currentThread().getName() +
" erhöht auf: " + wert);
}
// Synchronisierter Block
public void verringern() {
synchronized(this) {
wert--;
System.out.println(Thread.currentThread().getName() +
" verringert auf: " + wert);
}
}
public synchronized int getWert() {
return wert;
}
}
// Producer-Consumer mit wait/notify
static class Warenlager {
private final int[] lager = new int[5];
private int index = 0;
public synchronized void einlagern(int ware) throws InterruptedException {
// Warten wenn Lager voll
while (index >= lager.length) {
System.out.println("Lager voll - Producer wartet");
wait();
}
lager[index] = ware;
index++;
System.out.println(Thread.currentThread().getName() +
" eingelagert: " + ware);
// Consumer benachrichtigen
notifyAll();
}
public synchronized int auslagern() throws InterruptedException {
// Warten wenn Lager leer
while (index <= 0) {
System.out.println("Lager leer - Consumer wartet");
wait();
}
index--;
int ware = lager[index];
System.out.println(Thread.currentThread().getName() +
" ausgelagert: " + ware);
// Producer benachrichtigen
notifyAll();
return ware;
}
}
static class Producer implements Runnable {
private Warenlager lager;
public Producer(Warenlager lager) {
this.lager = lager;
}
@Override
public void run() {
try {
for (int i = 1; i <= 10; i++) {
lager.einlagern(i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
static class Consumer implements Runnable {
private Warenlager lager;
public Consumer(Warenlager lager) {
this.lager = lager;
}
@Override
public void run() {
try {
for (int i = 1; i <= 10; i++) {
lager.auslagern();
Thread.sleep(150);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static void main(String[] args) {
System.out.println("=== Synchronisation Demo ===");
// Einfache Synchronisation
Zaehler zaehler = new Zaehler();
Thread[] threads = new Thread[5];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 3; j++) {
zaehler.erhoehen();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
return;
}
}
});
threads[i].setName("Thread-" + i);
}
// Threads starten
for (Thread t : threads) {
t.start();
}
// Auf Threads warten
for (Thread t : threads) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Endwert: " + zaehler.getWert());
// Producer-Consumer Demo
System.out.println("\n=== Producer-Consumer Demo ===");
Warenlager lager = new Warenlager();
Thread producer = new Thread(new Producer(lager), "Producer");
Thread consumer = new Thread(new Consumer(lager), "Consumer");
producer.start();
consumer.start();
try {
producer.join();
consumer.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3. Concurrent Locks and Modern Synchronization
import java.util.concurrent.locks.*;
import java.util.concurrent.*;
public class ConcurrentLocksDemo {
// ReentrantLock Example
static class Bankkonto {
private double kontostand;
private final ReentrantLock lock = new ReentrantLock();
public Bankkonto(double startbetrag) {
this.kontostand = startbetrag;
}
public void einzahlen(double betrag) {
lock.lock();
try {
double alterStand = kontostand;
Thread.sleep(50); // Simulate processing
kontostand = alterStand + betrag;
System.out.println(Thread.currentThread().getName() +
" deposited: " + betrag +
", new balance: " + kontostand);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
public boolean abheben(double betrag) {
lock.lock();
try {
if (kontostand >= betrag) {
double alterStand = kontostand;
Thread.sleep(50);
kontostand = alterStand - betrag;
System.out.println(Thread.currentThread().getName() +
" withdrew: " + betrag +
", new balance: " + kontostand);
return true;
} else {
System.out.println(Thread.currentThread().getName() +
" Could not withdraw: " + betrag +
" (balance: " + kontostand + ")");
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
lock.unlock();
}
}
public double getKontostand() {
lock.lock();
try {
return kontostand;
} finally {
lock.unlock();
}
}
}
// ReadWriteLock Example
static class ThreadSafeList {
private final List<String> liste = new ArrayList<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
public void add(String element) {
writeLock.lock();
try {
liste.add(element);
System.out.println(Thread.currentThread().getName() +
" added: " + element);
} finally {
writeLock.unlock();
}
}
public String get(int index) {
readLock.lock();
try {
return liste.get(index);
} finally {
readLock.unlock();
}
}
public List<String> getAll() {
readLock.lock();
try {
return new ArrayList<>(liste); // Return copy
} finally {
readLock.unlock();
}
}
public int size() {
readLock.lock();
try {
return liste.size();
} finally {
readLock.unlock();
}
}
}
// Semaphore Example
static class RessourcenPool {
private final Semaphore semaphore;
private final List<String> ressourcen;
public RessourcenPool(int maxRessourcen) {
semaphore = new Semaphore(maxRessourcen);
ressourcen = new ArrayList<>();
for (int i = 1; i <= maxRessourcen; i++) {
ressourcen.add("Ressource-" + i);
}
}
public String acquire() throws InterruptedException {
semaphore.acquire();
synchronized(ressourcen) {
if (!ressourcen.isEmpty()) {
String ressource = ressourcen.remove(0);
System.out.println(Thread.currentThread().getName() +
" acquired: " + ressource);
return ressource;
}
}
semaphore.release();
return null;
}
public void release(String ressource) {
synchronized(ressourcen) {
ressourcen.add(ressource);
System.out.println(Thread.currentThread().getName() +
" released: " + ressource);
}
semaphore.release();
}
}
// CountDownLatch Example
static class Worker implements Runnable {
private final CountDownLatch startSignal;
private final CountDownLatch doneSignal;
private final int workerId;
public Worker(CountDownLatch startSignal, CountDownLatch doneSignal, int workerId) {
this.startSignal = startSignal;
this.doneSignal = doneSignal;
this.workerId = workerId;
}
@Override
public void run() {
try {
// Wait for start signal
System.out.println("Worker " + workerId + " ready");
startSignal.await();
// Perform work
System.out.println("Worker " + workerId + " working");
Thread.sleep((long) (Math.random() * 1000));
System.out.println("Worker " + workerId + " done");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
doneSignal.countDown();
}
}
}
public static void main(String[] args) {
System.out.println("=== Concurrent Locks Demo ===");
// ReentrantLock Demo
Bankkonto konto = new Bankkonto(1000.0);
Thread[] bankThreads = new Thread[4];
for (int i = 0; i < bankThreads.length; i++) {
final int threadId = i;
bankThreads[i] = new Thread(() -> {
for (int j = 0; j < 3; j++) {
if (threadId % 2 == 0) {
konto.einzahlen(100);
} else {
konto.abheben(50);
}
}
}, "BankThread-" + i);
}
for (Thread t : bankThreads) {
t.start();
}
for (Thread t : bankThreads) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Final balance: " + konto.getKontostand());
// ReadWriteLock Demo
System.out.println("\n=== ReadWriteLock Demo ===");
ThreadSafeList liste = new ThreadSafeList();
// Writer Thread
Thread writer = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
liste.add("Element-" + i);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
return;
}
}
}, "Writer");
// Reader Threads
Thread[] readers = new Thread[3];
for (int i = 0; i < readers.length; i++) {
readers[i] = new Thread(() -> {
for (int j = 0; j < 10; j++) {
List<String> alle = liste.getAll();
System.out.println(Thread.currentThread().getName() +
" read: " + alle.size() + " elements");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
return;
}
}
}, "Reader-" + i);
}
writer.start();
for (Thread reader : readers) {
reader.start();
}
try {
writer.join();
for (Thread reader : readers) {
reader.join();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// Semaphore Demo
System.out.println("\n=== Semaphore Demo ===");
RessourcenPool pool = new RessourcenPool(2);
Thread[] poolThreads = new Thread[5];
for (int i = 0; i < poolThreads.length; i++) {
poolThreads[i] = new Thread(() -> {
try {
String ressource = pool.acquire();
if (ressource != null) {
Thread.sleep(1000); // Use resource
pool.release(ressource);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "PoolThread-" + i);
}
for (Thread t : poolThreads) {
t.start();
}
for (Thread t : poolThreads) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// CountDownLatch Demo
System.out.println("\n=== CountDownLatch Demo ===");
int workerCount = 3;
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(workerCount);
for (int i = 1; i <= workerCount; i++) {
new Thread(new Worker(startSignal, doneSignal, i)).start();
}
try {
Thread.sleep(1000);
System.out.println("All workers ready - Start signal!");
startSignal.countDown();
doneSignal.await();
System.out.println("All workers done!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4. Thread-Safe Data Structures and Atomic Classes
import java.util.concurrent.atomic.*;
import java.util.concurrent.*;
public class ThreadSafeCollections {
// Atomic Classes Demo
static class AtomicZaehler {
private final AtomicInteger zaehler = new AtomicInteger(0);
private final AtomicLong longZaehler = new AtomicLong(0);
private final AtomicBoolean flag = new AtomicBoolean(false);
private final AtomicReference<String> nachricht = new AtomicReference<>("");
public void increment() {
int alterWert = zaehler.getAndIncrement();
System.out.println(Thread.currentThread().getName() +
" increment: " + alterWert + " -> " + zaehler.get());
}
public void add(long wert) {
long alterWert = longZaehler.getAndAdd(wert);
System.out.println(Thread.currentThread().getName() +
" add: " + alterWert + " + " + wert + " -> " + longZaehler.get());
}
public void toggleFlag() {
boolean alterWert = flag.getAndSet(!flag.get());
System.out.println(Thread.currentThread().getName() +
" toggle: " + alterWert + " -> " + flag.get());
}
public void updateNachricht(String neueNachricht) {
String alteNachricht = nachricht.getAndSet(neueNachricht);
System.out.println(Thread.currentThread().getName() +
" update: '" + alteNachricht + "' -> '" + neueNachricht + "'");
}
public int getZaehler() { return zaehler.get(); }
public long getLongZaehler() { return longZaehler.get(); }
public boolean getFlag() { return flag.get(); }
public String getNachricht() { return nachricht.get(); }
}
// Concurrent Collections Demo
static class ConcurrentCollectionsDemo {
public static void hashMapDemo() {
System.out.println("=== ConcurrentHashMap Demo ===");
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Writer Threads
Thread[] writers = new Thread[3];
for (int i = 0; i < writers.length; i++) {
final int threadId = i;
writers[i] = new Thread(() -> {
for (int j = 0; j < 5; j++) {
String key = "Key-" + threadId + "-" + j;
map.put(key, threadId * 100 + j);
System.out.println(Thread.currentThread().getName() +
" put: " + key);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
return;
}
}
}, "Writer-" + i);
}
// Reader Thread
Thread reader = new Thread(() -> {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() +
" size: " + map.size());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
return;
}
}
}, "Reader");
// Start all threads
reader.start();
for (Thread writer : writers) {
writer.start();
}
// Wait for threads
try {
for (Thread writer : writers) {
writer.join();
}
reader.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final map size: " + map.size());
}
public static void blockingQueueDemo() {
System.out.println("\n=== BlockingQueue Demo ===");
BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
// Producer
Thread producer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
String item = "Item-" + i;
queue.put(item);
System.out.println("Producer put: " + item +
" (queue size: " + queue.size() + ")");
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Producer");
// Consumer
Thread consumer = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
String item = queue.take();
System.out.println("Consumer take: " + item +
" (queue size: " + queue.size() + ")");
Thread.sleep(300);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Consumer");
producer.start();
consumer.start();
try {
producer.join();
consumer.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// ThreadLocal Demo
static class ThreadLocalDemo {
private static ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 100);
private static ThreadLocal<String> threadLocalName = new ThreadLocal<>();
public static void demo() {
System.out.println("=== ThreadLocal Demo ===");
Thread[] threads = new Thread[3];
for (int i = 0; i < threads.length; i++) {
final int threadId = i;
threads[i] = new Thread(() -> {
threadLocalName.set("Thread-" + threadId);
for (int j = 0; j < 3; j++) {
int wert = threadLocalValue.get();
String name = threadLocalName.get();
System.out.println(name + " wert: " + wert);
// Change ThreadLocal value
threadLocalValue.set(wert + threadId);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
return;
}
}
// Clean up ThreadLocal
threadLocalName.remove();
threadLocalValue.remove();
}, "Thread-" + i);
}
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
// Atomic Classes Demo
System.out.println("=== Atomic Classes Demo ===");
AtomicZaehler zaehler = new AtomicZaehler();
Thread[] atomicThreads = new Thread[4];
for (int i = 0; i < atomicThreads.length; i++) {
final int threadId = i;
atomicThreads[i] = new Thread(() -> {
zaehler.increment();
zaehler.add(threadId * 10);
zaehler.toggleFlag();
zaehler.updateNachricht("Nachricht von Thread-" + threadId);
}, "AtomicThread-" + i);
}
for (Thread t : atomicThreads) {
t.start();
}
for (Thread t : atomicThreads) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Final values:");
System.out.println("Zaehler: " + zaehler.getZaehler());
System.out.println("LongZaehler: " + zaehler.getLongZaehler());
System.out.println("Flag: " + zaehler.getFlag());
System.out.println("Nachricht: " + zaehler.getNachricht());
// Concurrent Collections Demo
ConcurrentCollectionsDemo.hashMapDemo();
ConcurrentCollectionsDemo.blockingQueueDemo();
// ThreadLocal Demo
ThreadLocalDemo.demo();
}
}
Thread Pools and ExecutorService
ThreadPoolExecutor
// Fixed Thread Pool
ExecutorService fixedPool = Executors.newFixedThreadPool(4);
// Cached Thread Pool
ExecutorService cachedPool = Executors.newCachedThreadPool();
// Single Thread Executor
ExecutorService singlePool = Executors.newSingleThreadExecutor();
// Scheduled Thread Pool
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2);
// Execute tasks
Future<String> future = fixedPool.submit(() -> {
Thread.sleep(1000);
return "Result";
});
// Scheduled tasks
scheduledPool.scheduleAtFixedRate(() -> {
System.out.println("Periodic task");
}, 0, 1, TimeUnit.SECONDS);
// Shutdown pool
fixedPool.shutdown();
scheduledPool.shutdown();
Deadlock Prevention
Deadlock Conditions
- Mutual Exclusion: Resource can only be used by one thread
- Hold and Wait: Thread holds resources and waits for more
- No Preemption: Resources cannot be forcibly released
- Circular Wait: Circular waiting loop between threads
Avoidance Strategies
// Lock Ordering - always lock in the same order
public void transfer(Account from, Account to, double amount) {
// Synchronize accounts in consistent order
Account first = from.getId() < to.getId() ? from : to;
Account second = from.getId() < to.getId() ? to : from;
synchronized(first) {
synchronized(second) {
from.withdraw(amount);
to.deposit(amount);
}
}
}
// TryLock with timeout
public boolean transferWithTryLock(Account from, Account to, double amount) {
while (true) {
try {
if (from.getLock().tryLock(1, TimeUnit.SECONDS)) {
try {
if (to.getLock().tryLock(1, TimeUnit.SECONDS)) {
try {
from.withdraw(amount);
to.deposit(amount);
return true;
} finally {
to.getLock().unlock();
}
}
} finally {
from.getLock().unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
// Short pause before retry
Thread.sleep(100);
}
}
Performance Optimization
Lock Granularity
// Coarse-grained synchronization (bad)
public synchronized void addElement(Object element) {
// Entire operation locked
list.add(element);
size++;
}
// Fine-grained synchronization (better)
public void addElement(Object element) {
synchronized(list) {
list.add(element);
}
synchronized(this) {
size++;
}
}
Volatile vs Synchronized
// Volatile for simple visibility
private volatile boolean running = true;
public void stop() {
running = false; // Visible to all threads
}
public void run() {
while (running) {
// Perform work
}
}
// Synchronized for complex operations
private int counter = 0;
public synchronized void increment() {
counter++; // Atomic operation
}
Advantages and Disadvantages
Advantages of Multithreading
- Performance: Parallel execution on multi-core systems
- Responsiveness: UI remains responsive during long operations
- Resource utilization: Better utilization of system resources
- Scalability: Tasks can be distributed
Disadvantages
- Complexity: Synchronization is error-prone
- Debugging: Race conditions are hard to reproduce
- Overhead: Thread creation and context switching cost time
- Resources: More memory and CPU consumption
Common Exam Questions
-
What is the difference between wait() and sleep()? wait() releases the lock, sleep() retains the lock. wait() requires synchronized, sleep() does not.
-
Explain deadlock and how to avoid it! Deadlock is mutual blocking. Avoidance through lock ordering, tryLock with timeout.
-
When do you use volatile instead of synchronized? volatile for simple visibility of variables, synchronized for complex operations.
-
What is the difference between Runnable and Callable? Runnable has no return value, Callable returns a result and can throw exceptions.
Most Important Sources
- https://docs.oracle.com/javase/tutorial/essential/concurrency/
- https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/package-summary.html
- https://www.baeldung.com/java-concurrency