같은 count++ 를 (1) 무방비 (2) synchronized (3) AtomicInteger 로 각각 실행해 결과를 비교합니다.
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class RaceConditionDemo {
static int unsafeCount = 0;
static int syncCount = 0;
static final AtomicInteger atomicCount = new AtomicInteger();
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
int threads = 8, perThread = 100_000;
Runnable work = () -> {
for (int i = 0; i < perThread; i++) {
unsafeCount++; // 경쟁 조건
synchronized (lock) { syncCount++; } // 상호 배제
atomicCount.incrementAndGet(); // CAS
}
};
Thread[] ts = new Thread[threads];
for (int i = 0; i < threads; i++) { ts[i] = new Thread(work); ts[i].start(); }
for (Thread t : ts) t.join(); // join 후 읽기는 happens-before 로 안전
int expected = threads * perThread;
System.out.println("expected = " + expected);
System.out.println("unsafe = " + unsafeCount + (unsafeCount == expected ? "" : " <-- 갱신 손실"));
System.out.println("sync = " + syncCount);
System.out.println("atomic = " + atomicCount.get());
}
}
// 출력 (unsafe 값은 실행마다 다름):
// expected = 800000
// unsafe = 613482 <-- 갱신 손실
// sync = 800000
// atomic = 800000두 계좌 사이를 양방향으로 동시에 이체하면 락 순서가 엇갈려 데드락이 납니다. 계좌 ID 순으로 락을 잡아 순환 대기를 끊습니다.
import java.util.concurrent.*;
public class BankAccount {
private final int id;
private long balance;
BankAccount(int id, long balance) { this.id = id; this.balance = balance; }
long balance() { synchronized (this) { return balance; } }
static void transfer(BankAccount from, BankAccount to, long amount) {
// 순환 대기 차단: 항상 id 가 작은 계좌부터 잠근다
BankAccount first = from.id < to.id ? from : to;
BankAccount second = from.id < to.id ? to : from;
synchronized (first) {
synchronized (second) {
if (from.balance < amount) throw new IllegalStateException("잔액 부족");
from.balance -= amount;
to.balance += amount;
}
}
}
public static void main(String[] args) throws Exception {
BankAccount a = new BankAccount(1, 1_000_000);
BankAccount b = new BankAccount(2, 1_000_000);
try (ExecutorService pool = Executors.newFixedThreadPool(4)) {
for (int i = 0; i < 10_000; i++) {
pool.submit(() -> transfer(a, b, 10)); // a → b
pool.submit(() -> transfer(b, a, 10)); // b → a (역방향, 데드락 후보)
}
} // close(): shutdown + awaitTermination
System.out.println("a = " + a.balance() + ", b = " + b.balance());
System.out.println("total = " + (a.balance() + b.balance())); // 불변식: 합계 보존
}
}
// 출력:
// a = 1000000, b = 1000000
// total = 2000000일부러 락 순서를 엇갈리게 잡아 데드락을 만들고, ThreadMXBean 으로 감지한 뒤, tryLock 버전으로 같은 작업이 끝나는 것을 보여줍니다.
import java.lang.management.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
public class DeadlockDemo {
public static void main(String[] args) throws Exception {
ReentrantLock l1 = new ReentrantLock(), l2 = new ReentrantLock();
// 1) 데드락 재현: 서로 반대 순서
Thread t1 = new Thread(() -> grabBoth(l1, l2), "T1");
Thread t2 = new Thread(() -> grabBoth(l2, l1), "T2");
t1.start(); t2.start();
t1.join(500); t2.join(500); // 타임아웃으로 안전 종료
long[] dead = ManagementFactory.getThreadMXBean().findDeadlockedThreads();
System.out.println("deadlocked threads = " + (dead == null ? 0 : dead.length));
t1.interrupt(); t2.interrupt(); // lockInterruptibly 대기 중이므로 깨어남
t1.join(); t2.join();
// 2) tryLock: 못 잡으면 포기하고 재시도 → 데드락 없음
ReentrantLock a = new ReentrantLock(), b = new ReentrantLock();
Thread t3 = new Thread(() -> tryBoth(a, b), "T3");
Thread t4 = new Thread(() -> tryBoth(b, a), "T4");
t3.start(); t4.start(); t3.join(); t4.join();
System.out.println("tryLock version finished");
}
static void grabBoth(ReentrantLock first, ReentrantLock second) {
try {
first.lockInterruptibly();
try {
Thread.sleep(100); // 상대가 다른 락을 잡을 시간
second.lockInterruptibly(); // 여기서 영원히 대기 → 데드락
second.unlock();
} finally { first.unlock(); }
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " interrupted");
}
}
static void tryBoth(ReentrantLock first, ReentrantLock second) {
while (true) {
if (first.tryLock()) {
try {
if (second.tryLock()) {
try { return; } finally { second.unlock(); }
}
} finally { first.unlock(); } // 두 번째 실패 → 첫 번째도 놓고 재시도 (점유와 대기 깨기)
}
Thread.onSpinWait();
}
}
}
// 출력:
// deadlocked threads = 2
// T1 interrupted
// T2 interrupted
// tryLock version finished주문 이벤트를 생산자가 큐에 넣고 소비자 2개가 꺼내 처리합니다. BlockingQueue 가 락과 대기를 내부에서 처리하므로 wait/notify 를 직접 쓸 필요가 없습니다. 종료는 "독약(poison pill)" 방식입니다.
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ProducerConsumer {
record OrderEvent(int id, long amount) {}
static final OrderEvent POISON = new OrderEvent(-1, 0);
public static void main(String[] args) throws InterruptedException {
BlockingQueue<OrderEvent> queue = new ArrayBlockingQueue<>(10); // 용량 10: 가득 차면 put 이 대기
AtomicInteger processed = new AtomicInteger();
int consumers = 2, total = 50;
Runnable consumer = () -> {
try {
while (true) {
OrderEvent e = queue.take(); // 비어 있으면 대기
if (e == POISON) break;
processed.incrementAndGet();
}
} catch (InterruptedException ex) { Thread.currentThread().interrupt(); }
};
Thread[] cs = new Thread[consumers];
for (int i = 0; i < consumers; i++) { cs[i] = new Thread(consumer, "consumer-" + i); cs[i].start(); }
for (int i = 1; i <= total; i++) queue.put(new OrderEvent(i, i * 100L)); // 생산
for (int i = 0; i < consumers; i++) queue.put(POISON); // 소비자 수만큼 독약
for (Thread c : cs) c.join();
System.out.println("processed = " + processed.get());
System.out.println("queue empty = " + queue.isEmpty());
}
}
// 출력:
// processed = 50
// queue empty = true파일 100개를 읽어 줄 수를 세는 작업을 submit + Future 로 처리하고, 같은 작업을 가상 스레드로 돌립니다. 실패 작업의 예외가 Future.get() 에서 나오는 것도 확인합니다.
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
public class ParallelFiles {
public static void main(String[] args) throws Exception {
Path dir = Files.createTempDirectory("logs");
for (int i = 0; i < 100; i++)
Files.writeString(dir.resolve("app-" + i + ".log"), "line\n".repeat(i + 1));
long t0 = System.nanoTime();
long total = countLines(dir, Executors.newFixedThreadPool(8));
System.out.println("fixed pool total lines = " + total);
long total2 = countLines(dir, Executors.newVirtualThreadPerTaskExecutor());
System.out.println("virtual thread total lines = " + total2);
System.out.println("both done in < 1s: " + ((System.nanoTime() - t0) / 1_000_000_000 < 1));
}
static long countLines(Path dir, ExecutorService pool) throws Exception {
List<Future<Long>> futures = new ArrayList<>();
try (pool; var files = Files.list(dir)) {
for (Path p : files.toList())
futures.add(pool.submit(() -> { try (var s = Files.lines(p)) { return s.count(); } }));
}
long sum = 0;
for (Future<Long> f : futures) {
try { sum += f.get(); }
catch (ExecutionException e) { System.out.println("failed: " + e.getCause()); } // 작업 안 예외는 여기서
}
return sum;
}
}
// 출력:
// fixed pool total lines = 5050
// virtual thread total lines = 5050
// both done in < 1s: true