반응형
자바의 동시성과 병렬 처리를 고급지게 해보자.
1. Java 동시성의 함정 피하기: 데드락과 레이스 컨디션
동시성 프로그래밍에서 가장 흔히 마주치는 문제 중 두 가지는 데드락(Deadlock)과 레이스 컨디션(Race Condition)이다.
이러한 문제는 프로그램의 안정성과 성능에 심각한 영향을 미칠 수 있으므로, 이를 이해하고 적절히 대처하는 것이 중요하다.
데드락(Deadlock)
- 데드락은 두 개 이상의 스레드가 서로가 보유한 자원을 기다리며 무한히 대기하는 상태를 말한다. 예를 들어, 스레드 A가 자원 1을 점유한 상태에서 자원 2를 요청하고, 동시에 스레드 B가 자원 2를 점유한 상태에서 자원 1을 요청하면 두 스레드는 서로를 기다리며 영원히 대기하게 된다.
예시 코드: 데드락 발생 상황
public class DeadlockExample {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void operation1() {
synchronized (lock1) {
System.out.println("Operation 1: Acquired lock1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println("Operation 1: Acquired lock2");
}
}
}
public void operation2() {
synchronized (lock2) {
System.out.println("Operation 2: Acquired lock2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println("Operation 2: Acquired lock1");
}
}
}
public static void main(String[] args) {
DeadlockExample example = new DeadlockExample();
Thread t1 = new Thread(example::operation1);
Thread t2 = new Thread(example::operation2);
t1.start();
t2.start();
}
}
해결 방법
- 자원 획득 순서 일관성 유지: 모든 스레드가 자원을 동일한 순서로 획득하도록 설계한다.
- 타임아웃 설정: 자원 획득 시 타임아웃을 설정하여 데드락을 방지한다.
레이스 컨디션(Race Condition)
- 레이스 컨디션은 여러 스레드가 동시에 공유 자원을 변경하려고 할 때 발생하는 문제다. 이로 인해 데이터 무결성이 깨지거나 예기치 않은 동작이 발생할 수 있다.
예시 코드: 레이스 컨디션 발생 상황
public class RaceConditionExample {
private int sharedResource = 0;
public void increment() {
int temp = sharedResource;
temp = temp + 1;
sharedResource = temp;
}
public static void main(String[] args) {
RaceConditionExample example = new RaceConditionExample();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) example.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) example.increment();
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final value of sharedResource: " + example.sharedResource);
}
}
해결 방법
- 동기화 메커니즘 사용: synchronized 키워드나 Lock 인터페이스를 사용하여 공유 자원에 대한 접근을 제어한다.
- 원자적 연산 활용: AtomicInteger와 같은 원자적 클래스를 사용하여 안전하게 값을 변경한다.
2. 고급 동시성 패턴: Executor Framework와 CompletableFuture
Executor Framework
- Executor Framework는 스레드의 생성, 관리, 실행을 추상화하여 제공하는 프레임워크다. 이를 통해 개발자는 스레드 풀을 쉽게 구성하고 작업을 효율적으로 분배할 수 있다.
예시 코드: Executor Framework 사용
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
int taskId = i;
executor.submit(() -> {
System.out.println("Executing task " + taskId + " by " + Thread.currentThread().getName());
try { Thread.sleep(500); } catch (InterruptedException e) {}
});
}
executor.shutdown();
}
}
활용 시나리오
- 비동기 API 호출: 여러 API를 비동기적으로 호출하고 그 결과를 조합하여 처리한다.
- 복잡한 비동기 작업 흐름 관리: 데이터베이스 접근, 파일 I/O, 네트워크 요청 등을 비동기적으로 처리하고 결과를 통합한다.
3. 동시성 제어 메커니즘: 뮤텍스와 세마포어
뮤텍스(Mutex)
- 뮤텍스는 상호 배제를 통해 공유 자원에 대한 동시 접근을 방지한다. Java에서는 synchronized 키워드나 Lock 인터페이스를 사용하여 뮤텍스를 구현할 수 있다.
예시 코드: Lock 인터페이스를 이용한 뮤텍스 구현
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MutexExample {
private int resource = 0;
private final Lock lock = new ReentrantLock();
public void useResource() {
lock.lock();
try {
resource++;
System.out.println(Thread.currentThread().getName() + " is using resource. Count: " + resource);
} finally {
resource--;
lock.unlock();
}
}
public static void main(String[] args) {
MutexExample example = new MutexExample();
Runnable task = example::useResource;
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
t1.start();
t2.start();
}
}
세마포어(Semaphore)
- 세마포어는 특정 자원에 동시에 접근할 수 있는 스레드의 수를 제한한다. Java의 Semaphore 클래스를 사용하여 구현할 수 있으며, 주로 제한된 자원의 접근을 제어할 때 유용하다.
예시 코드: Semaphore 사용
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
private final Semaphore semaphore = new Semaphore(3); // 최대 3개의 스레드 허용
public void parkCar() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " parked a car. Permits left: " + semaphore.availablePermits());
Thread.sleep(1000); // 주차 시간 시뮬레이션
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
System.out.println(Thread.currentThread().getName() + " left the parking. Permits left: " + semaphore.availablePermits());
}
}
public static void main(String[] args) {
SemaphoreExample example = new SemaphoreExample();
Runnable task = example::parkCar;
for (int i = 1; i <= 4; i++) {
new Thread(task, "Car-" + i).start();
}
}
}
활용 시나리오
- 주차 시스템: 제한된 주차 공간에 동시에 접근하는 차량 수를 제어한다.
- 웹 서버 요청 제한: 동시에 처리할 수 있는 클라이언트 요청 수를 제한하여 서버 과부하를 방지한다.
4. 성능 최적화 전략: Java Memory Model과 튜닝 방법
Java Memory Model (JMM)의 이해
- JMM은 Java의 멀티스레딩 환경에서 변수의 가시성, 원자성, 순서를 보장하는 규칙을 정의합니다. 주요 개념은 다음과 같다.
- Happens-Before 규칙: 특정 연산 A가 다른 연산 B보다 먼저 실행됨을 보장하여 데이터 일관성을 유지합니다.
- 원자성(Atomicity): 연산이 중간 단계 없이 한 번에 완료됨을 보장하여 데이터의 일관성을 유지합니다.
- 가시성(Visibility): 하나의 스레드에서 변경한 변수의 값이 다른 스레드에서도 즉시 보이도록 보장합니다.
- 순서(Ordering): 명령어의 실행 순서를 제어하여 예기치 않은 동작을 방지합니다.
성능 튜닝 방법
- 효과적인 성능 튜닝을 위해서는 정확한 성능 측정과 함께 적절한 최적화 전략을 적용해야 합니다.
성능 측정
- JMH(Java Microbenchmarking Harness): Java 애플리케이션의 성능을 정밀하게 측정할 수 있는 벤치마킹 도구다.
- 지표 분석: 처리량(Throughput), 지연 시간(Latency), CPU 사용률 등을 분석하여 병목 지점을 식별한다.
튜닝 전략
- 스레드 풀 크기 조정: 적절한 스레드 풀 크기를 설정하여 컨텍스트 스위칭 오버헤드를 최소화하고, 리소스를 효율적으로 사용한다.
- 락 경쟁 최소화: 락의 범위를 축소하거나 락 없이 동작하는 데이터 구조를 사용하여 락 경쟁을 줄인다.
- 효율적인 데이터 구조 사용: 데이터 접근과 수정이 빠른 데이터 구조를 선택하여 알고리즘의 성능을 향상시킨다.
예시: 스레드 풀 크기 조정
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTuning {
public static void main(String[] args) {
// 시스템의 가용 CPU 코어 수에 맞춰 스레드 풀 크기 설정
int availableProcessors = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(availableProcessors * 2);
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
// CPU-bound 작업
performTask();
});
}
executor.shutdown();
}
private static void performTask() {
// 실제 작업 로직
}
}
반응형
'JAVA' 카테고리의 다른 글
[Java] 추상화란 무엇인가? (1) | 2023.11.02 |
---|---|
Java I/O: BufferedReader, BufferedWriter, Buffer 사용법 (0) | 2023.11.01 |
[Java] 동시성과 병렬 처리 part1 (1) | 2023.10.31 |
[Java] Stream: mapToInt 함수로 점수 합산하기 (0) | 2023.09.27 |
[Java] 예제로 이해하는 자바 스트림(stream) (0) | 2023.09.27 |