본문 바로가기
Java & Kotlin

Java 에서의 동시성 관리 방법

by kiwi_wiki 2025. 1. 27.
728x90
반응형

Synchronized

  • 키워드를 메서드나 블록에 적용해 해당 코드 블록에 접근할 때 하나의 스레드만 접근할 수 있도록 보장
  • 락을 잡고 있는 동안 다른 스레드들은 해당 자원을 사용하지 못하기 때문에 경합이 발생하여 성능 저하가 일어날 수 있음
  • 락을 과도하게 사용하면 데드락 문제도 발생할 수 있음

ReentrantLock

  • Lock 인터페이스를 구현한 클래스. 락을 명시적으로 관리할 수 있게 함
  • 락을 획득할 때 타임아웃 설정이나 중단할 수 있는 기능을 제공함
  • synchronized보다 여러 조건을 처리하는데에 유리
  • 락을 획득하는데 실패할 경우 재시도를 하거나 특정 시간내에 락을 획득하지 못하면 다른 처리를 할 수 있음
Lock lock = new ReentrantLock();
lock.lock();
try {
    // 작업 수행
} finally {
    lock.unlock();
}

ReadWriteLock

  • 읽기와 쓰기를 분리하여 읽기 작업이 동시에 여러 스레드에서 이루어지도록 허용하는 방법. 쓰기 작업은 하나의 스레드만 접근할 수 있음
  • 읽기 작업이 많고 쓰기 작업이 적은 경우 성능 향상에 도움이 됨
ReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();
try {
    // 읽기 작업
} finally {
    lock.readLock().unlock();
}

lock.writeLock().lock();
try {
    // 쓰기 작업
} finally {
    lock.writeLock().unlock();
}

Atomic Classes

  • 원자적인 연산을 지원하는 Atomic 클래스 제공
  • AtomicInteger, AtomicLong, AtomicReference 등을 사용하면 동기화 없이 원자적인 값을 업데이트 할 수 있음
  • 내부적으로 CAS(Compare And Swap) 알고리즘을 사용하여 동기화 문제를 해결
  • 단일 변수에 대한 동기화만 제공하고 복잡한 객체나 다중 변수를 다룰때는 적합하지 않음
AtomicInteger count = new AtomicInteger();
count.incrementAndGet();  // 동기화 없이 원자적으로 값 증가

ForkJoinPool

  • 병렬 작업을 처리하는데 최적화된 Pool
  • 작업을 분할하여 병렬로 처리. 재귀적인 작업에 유리함
  • 분할-정복 전략을 사용하여 큰 작업을 작은 작업으로 나누고 그 결과를 합치는 방식으로 동시성 관리
ForkJoinPool forkJoinPool = new ForkJoinPool();
forkJoinPool.submit(() -> {
    // 분할-정복 알고리즘 예시: 배열을 분할하여 각 부분을 병렬로 처리
});
forkJoinPool.shutdown();

Executors

  • 스레드 풀을 관리하고 스레드 생성을 효율적으로 할 수 있게 도와줌
  • ExecutorService는 비동기 작업을 처리할 수 있는 API를 제공. 스레드 풀에서 동시 작업을 관리
  • 멀티스레딩 작업에 적합. 여러 독립적인 작업을 동시 처리
ExecutorService executor = Executors.newFixedThreadPool(10); // 10개의 스레드를 가진 풀 생성
executor.submit(() -> {
    System.out.println("작업 1");
});
executor.submit(() -> {
    System.out.println("작업 2");
});
executor.shutdown(); // 작업이 끝난 후 스레드 풀을 종료

CompletableFuture

  • 비동기 프로그래밍을 위한 도구. 여러 작업을 병렬로 실행하고 그 결과를 조합함
  • 비동기 작업의 결과를 기다리며 다른 작업을 수행할 수 있음
CompletableFuture.supplyAsync(() -> {
    // 비동기 작업
    return "Result";
}).thenAccept(result -> {
    // 결과 처리
});

ExecutorService vs ForkJoinPool

특성 ExecutorService ForkJoinPool

목적 일반적인 작업을 처리하는 스레드 풀 재귀적이고 병렬화 가능한 분할-정복 작업 처리
작업 처리 방식 큐에 들어온 작업을 스레드 풀에서 처리 큰 작업을 작은 작업으로 나누어 병렬 처리 후 결과 합침
작업 스케줄링 큐에서 작업을 하나씩 처리 워커 스레드가 작업을 훔쳐서 처리(워크 스티알링)
적합한 사용 사례 일반적인 멀티스레딩 작업, I/O 작업 분할-정복 알고리즘, 재귀적이고 계산 집약적인 작업
성능 최적화 다양한 스레드 풀 전략을 통해 최적화 가능 워크 스티알링을 통해 비효율적인 스레드 처리를 피함
에러 처리 작업의 결과를 쉽게 처리할 수 있는 메서드 제공 재귀적으로 분할된 작업을 처리할 때 관리가 필요함
728x90
반응형

'Java & Kotlin' 카테고리의 다른 글

JVM  (0) 2025.01.26
Garbage Collection  (0) 2025.01.26
Java & Kotlin 장단점  (0) 2025.01.25
PriorityQueue (우선순위 큐)  (0) 2023.07.07
Kotlin?  (0) 2022.08.28