1. 스레드 기초
Java에서 동시성의 기본 단위는 스레드입니다. 스레드를 효과적으로 사용하면 다중 코어 프로세서의 성능을 최대한 활용할 수 있습니다.
public class BasicThreadExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Thread running: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("Main thread: " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
이 예제는 기본적인 스레드 생성과 실행을 보여줍니다. 실제 애플리케이션에서는 스레드 풀을 사용하여 스레드를 관리하는 것이 더 효율적입니다.
2. ExecutorService 활용
ExecutorService는 스레드 풀을 관리하고 비동기 작업을 쉽게 실행할 수 있게 해주는 인터페이스입니다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ExecutorServiceExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
Future future = executor.submit(() -> {
// 시간이 걸리는 작업 시뮬레이션
Thread.sleep(1000);
return 42;
});
try {
Integer result = future.get(); // 결과를 기다림
System.out.println("Result: " + result);
} catch (Exception e) {
e.printStackTrace();
}
executor.shutdown();
}
}
ExecutorService를 사용하면 스레드 생성과 관리의 복잡성을 줄이고, 작업의 실행을 더 효율적으로 제어할 수 있습니다.
3. CompletableFuture를 이용한 비동기 프로그래밍
CompletableFuture는 Java 8에서 도입된 강력한 비동기 프로그래밍 도구입니다. 복잡한 비동기 작업 흐름을 쉽게 구성할 수 있습니다.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture future1 = CompletableFuture.supplyAsync(() -> {
// 첫 번째 비동기 작업
return "Hello";
});
CompletableFuture future2 = CompletableFuture.supplyAsync(() -> {
// 두 번째 비동기 작업
return "World";
});
CompletableFuture combinedFuture = future1.thenCombine(future2, (s1, s2) -> s1 + " " + s2);
combinedFuture.thenAccept(System.out::println);
// 메인 스레드가 종료되지 않도록 대기
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
CompletableFuture를 사용하면 여러 비동기 작업을 조합하고 예외 처리를 쉽게 할 수 있어, 복잡한 비동기 로직을 간결하게 표현할 수 있습니다.
4. 동기화 기법
멀티스레드 환경에서 공유 자원에 대한 안전한 접근을 위해 동기화 기법이 필요합니다.
import java.util.concurrent.locks.ReentrantLock;
public class SynchronizationExample {
private int count = 0;
private ReentrantLock lock = new ReentrantLock();
public void incrementWithSynchronized() {
synchronized(this) {
count++;
}
}
public void incrementWithLock() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
public static void main(String[] args) throws InterruptedException {
SynchronizationExample example = new SynchronizationExample();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.incrementWithSynchronized();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
example.incrementWithLock();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + example.getCount());
}
}
이 예제는 synchronized 키워드와 ReentrantLock을 사용한 두 가지 동기화 방법을 보여줍니다. 적절한 동기화는 데이터 일관성을 유지하는 데 중요합니다.
5. 동시성 컬렉션 활용
Java는 멀티스레드 환경에서 안전하게 사용할 수 있는 다양한 동시성 컬렉션을 제공합니다.
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
public class ConcurrentCollectionsExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
CopyOnWriteArrayList list = new CopyOnWriteArrayList<>();
// ConcurrentHashMap 사용 예
map.put("key1", 1);
map.put("key2", 2);
map.forEach((k, v) -> System.out.println(k + ": " + v));
// CopyOnWriteArrayList 사용 예
list.add("item1");
list.add("item2");
for (String item : list) {
System.out.println(item);
}
}
}
동시성 컬렉션을 사용하면 명시적인 동기화 없이도 스레드 안전한 데이터 구조를 사용할 수 있습니다.
6. 동시성 프로그래밍 모범 사례
- 불변 객체 사용: 가능한 한 불변 객체를 사용하여 동시성 문제를 예방합니다.
- 락의 범위 최소화: 성능 향상을 위해 락이 적용되는 코드 블록을 최소화합니다.
- 데드락 방지: 락 획득 순서를 일관되게 유지하고, 순환 의존성을 피합니다.
- 스레드 풀 크기 최적화: 시스템 리소스와 작업 특성에 맞게 스레드 풀 크기를 조정합니다.
- 동시성 버그 디버깅: 스레드 덤프와 프로파일링 도구를 활용하여 동시성 문제를 진단합니다.
결론
Java의 동시성 프로그래밍 기술을 효과적으로 활용하면 대규모 트래픽을 처리하는 고성능 시스템을 구축할 수 있습니다. 스레드, ExecutorService, CompletableFuture, 동기화 기법, 그리고 동시성 컬렉션 등의 도구를 적절히 조합하여 사용하면 복잡한 동시성 문제를 해결하고 시스템의 확장성을 높일 수 있습니다. 동시성 프로그래밍은 복잡하지만, 지속적인 학습과 실험을 통해 마스터할 수 있는 중요한 기술입니다.