Java Concurrency Fundamentals: Core Concepts and Mechanisms
Concurrency Theory
Why Multithreading is Necessary
There are significant speed differences between CPU, memory, and I/O devices. To effectively utilize CPU performance and balance these speed disparities, computer architecture, operating systems, and compilers contribute through:
- CPU caching to balance memory speed differences (causes visibility issues)
- Operating system processes and threads for CPU time-sharing to balance CPU and I/O speed differences (causes atomicity issues)
- Compiler optimization of instruction execution order for better cache utilization (causes ordering issues)
Root Causes of Concurrency Problems: Three Core Issues
Visibility: CPU Cache Issues
Visibility means modifications to shared variables by one thread are immediately visible to other threads.
// Thread A execution
int counter = 0;
counter = 10;
// Thread B execution
int result = counter;
If Thread A runs on CPU1 and Thread B on CPU2, when Thread A executes counter=10, it loads the initial value into CPU1's cache, assigns 10, but may not immediately write to main memory. Thread B executing result=counter reads from main memory where counter might still be 0, resulting in result being 0 instead of 10.
Atomicity: Time-sharing Issues
Atomicity means operations either execute completely without interrpution or don't execute at all.
int value = 1;
// Thread A
value += 1;
// Thread B
value += 1;
The operation value += 1 requires three CPU instructions:
- Read variable from memory to CPU register
- Execute value + 1 in register
- Write result to memory (may go to CPU cache due to caching)
Due to thread switching, Thread A might execute the first instruction then switch to Thread B, which executes all three instructions. When switching back to Thread A, the final value written to memory could be 2 instead of 3.
Ordering: Instruction Reordering Issues
Ordering means program execution follows code sequence order.
int index = 0;
boolean ready = false;
index = 1; // statement 1
ready = true; // statement 2
Compilers and processors may reorder instructions to improve performance. The three types of reordering are:
- Compiler optimization reordering
- Instruction-level parallelism reordering
- Memory system reordering
Java Memory Model (JMM) uses memory barriers to restrict certain types of reordering.
Thread Fundamentals
Process and Thread Concepts
Process
A process is a program in execution with independent memory space. Processes are resource allocation units with significant context switching overhead but offer stability and security.
Thread
A thread is a process entity and basic CPU scheduling unit. Threads share process resources with minimal own resources (program counter, registers, stack). Thread communication uses shared memory with fast context switching but less stability.
Coroutine
Coroutines are user-mode lightweight threads with user-controlled scheduling. They maintain register context and stack with minimal kernel switching overhead.
Thread State Transitions
- NEW: Created but not started
- RUNNABLE: May be running or waiting for CPU time slice
- BLOCKED: Waiting for exclusive lock
- WAITING: Waiting for explicit wake-up by other threads
- TIMED_WAITING: Waiting with time limit
- TERMINATED: Completed execution or terminated due to exception
Thread Implementation Methods
Runnable Interface
public class TaskRunner implements Runnable {
private String message;
public TaskRunner(String msg) {
this.message = msg;
}
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Processing: " + this.message);
}
public static void main(String[] args) {
Runnable task = new TaskRunner("test task");
new Thread(task).start();
}
}
Callable Interface
public class ComputeTask implements Callable<integer> {
@Override
public Integer call() throws Exception {
return 456;
}
}
public static void main(String[] args) throws Exception {
ComputeTask task = new ComputeTask();
FutureTask<integer> future = new FutureTask<>(task);
Thread worker = new Thread(future);
worker.start();
System.out.println(future.get());
}
</integer></integer>
Thread Class Inheritance
class WorkerThread extends Thread {
@Override
public void run() {
// task implementation
}
}
WorkerThread worker = new WorkerThread();
worker.start();
Basic Thread Mechanisms
Executor Framework
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executor.execute(new TaskRunner("task-" + i));
}
executor.shutdown();
}
Daemon Threads
public static void main(String[] args) {
Thread daemon = new Thread(new TaskRunner("daemon"));
daemon.setDaemon(true);
daemon.start();
}
Thread Sleep
public void execute() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Thread Yield
public class YieldExample {
public static void main(String[] args) {
new Thread(() -> {
int count = 0;
while (true) {
count++;
System.out.println(Thread.currentThread().getName() + " count: " + count);
}
}).start();
new Thread(() -> {
int count = 0;
while (true) {
Thread.yield();
count++;
System.out.println(Thread.currentThread().getName() + " count: " + count);
}
}).start();
}
}
Thread Interruption
InterruptedException Handling
public class InterruptHandler {
private static class SleepThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println("Thread completed");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread sleeper = new SleepThread();
sleeper.start();
sleeper.interrupt();
}
}
Interruption Status Checking
public class InterruptCheck {
public static void main(String[] args) {
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// perform work
}
System.out.println("Thread terminated via interruption");
});
worker.start();
worker.interrupt();
}
}
Thread Coordination
Wait and Notify
public class CoordinationExample {
public synchronized void signal() {
System.out.println("signaling");
notifyAll();
}
public synchronized void waitForSignal() {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("received signal");
}
}
Thread Join
public class JoinDemo {
private class First extends Thread {
@Override
public void run() {
System.out.println("First thread");
}
}
private class Second extends Thread {
private First first;
Second(First f) { this.first = f; }
@Override
public void run() {
try {
first.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Second thread");
}
}
public void demo() {
First f = new First();
Second s = new Second(f);
s.start();
f.start();
}
}
Thread Synchronization
Synchronized Blocks
public class SyncExample {
public void syncMethod() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
ReentrantLock
public class LockDemo {
private Lock lock = new ReentrantLock();
public void lockedMethod() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock();
}
}
}
Advanced Thread Coordination
Condition Variables
public class ConditionExample {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void prepare() {
lock.lock();
try {
System.out.println("preparation complete");
condition.signalAll();
} finally {
lock.unlock();
}
}
public void await() {
lock.lock();
try {
condition.await();
System.out.println("proceeding after signal");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}