Java 동기화 메커니즘: 스레드 안전한 코드 작성

 

 

Java 동기화 메커니즘: 스레드 안전한 코드 작성을 위한 완벽 가이드

동기화의 필요성

멀티스레드 환경에서는 여러 스레드가 동시에 같은 자원에 접근할 때 데이터 불일치 문제가 발생할 수 있습니다. 다음 예제는 동기화되지 않은 카운터 클래스입니다:

ThreadUnsafeCounter.java
public class ThreadUnsafeCounter {
    private int count = 0;
    
    public void increment() {
        count++; // 이 연산은 원자적이지 않습니다!
    }
    
    public int getCount() {
        return count;
    }
}
주요 문제점:

  • 경쟁 상태(Race Condition): 여러 스레드가 동시에 count 변수를 읽고 변경할 때 발생
  • 가시성 문제(Visibility Problem): 한 스레드가 변경한 값이 다른 스레드에 즉시 보이지 않음
  • 명령어 재배치(Instruction Reordering): 컴파일러나 JVM의 최적화로 코드 실행 순서가 변경됨

synchronized 키워드

synchronized는 Java에서 가장 기본적인 동기화 메커니즘으로, 메소드나 코드 블록에 적용할 수 있습니다.

메소드 동기화

SynchronizedCounter.java
public class SynchronizedCounter {
    private int count = 0;
    
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
}

블록 동기화

BlockSynchronizedCounter.java
public class BlockSynchronizedCounter {
    private int count = 0;
    private final Object lock = new Object(); // 명시적 락 객체
    
    public void increment() {
        synchronized(lock) {
            count++;
        }
    }
    
    public int getCount() {
        synchronized(lock) {
            return count;
        }
    }
}
synchronized의 특징:

  • 내부 구현: 모니터(monitor)를 사용하여 상호 배제(mutual exclusion) 구현
  • 재진입성(Reentrancy): 이미 락을 획득한 스레드는 동일한 락을 다시 획득할 수 있음
  • 자동 락 해제: 예외가 발생해도 락이 자동으로 해제됨
  • 성능: Java 6 이후 적응형 락(adaptive lock) 메커니즘으로 성능 개선

volatile 키워드

volatile 키워드는 변수의 가시성(visibility) 문제를 해결하지만, 원자성(atomicity)은 보장하지 않습니다.

VolatileExample.java
public class VolatileExample {
    private volatile boolean flag = false;
    private int counter = 0;
    
    public void setFlag() {
        flag = true; // 다른 스레드에 즉시 가시적
    }
    
    public void incrementIfFlag() {
        if (flag) {
            counter++; // volatile이 이 연산의 원자성을 보장하지는 않음
        }
    }
    
    public boolean isFlag() {
        return flag;
    }
    
    public int getCounter() {
        return counter;
    }
}
volatile의 특징:

  • 메모리 가시성: 한 스레드에서 변경한 값이 다른 스레드에 즉시 보임
  • 순서 보장: volatile 변수의 읽기/쓰기 주변의 명령어 재배치 방지
  • 하드웨어 메모리 배리어 삽입: CPU 캐시와 메인 메모리 간의 동기화 보장
  • 적합한 사용 사례: 단일 스레드가 쓰고 다른 스레드들이 읽는 상황, 상태 플래그

Atomic 클래스

java.util.concurrent.atomic 패키지는 원자적 연산을 위한 특수 클래스들을 제공합니다.

AtomicCounter.java
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);
    
    public void increment() {
        count.incrementAndGet(); // 원자적 연산
    }
    
    public int getCount() {
        return count.get();
    }
    
    public void addValue(int value) {
        count.addAndGet(value); // 원자적 연산
    }
    
    public boolean compareAndSet(int expect, int update) {
        return count.compareAndSet(expect, update); // CAS 연산
    }
}
주요 Atomic 클래스:

  • AtomicInteger, AtomicLong, AtomicBoolean: 기본 타입을 위한 클래스
  • AtomicReference<V>: 객체 참조를 위한 클래스
  • AtomicIntegerArray: 배열을 위한 클래스
  • AtomicFieldUpdater: 기존 객체의 필드를 원자적으로 업데이트
특징 synchronized Atomic 클래스
구현 방식 모니터 락 Compare-And-Swap (CAS)
블로킹 블로킹 Non-blocking
성능 경쟁 시 성능 저하 경쟁이 적을 때 높은 성능
복합 연산 지원 제한적

Lock 인터페이스

java.util.concurrent.locks 패키지의 Lock 인터페이스는 synchronized보다 더 유연한 락 메커니즘을 제공합니다.

LockCounter.java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockCounter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();
    
    public void increment() {
        lock.lock(); // 락 획득
        try {
            count++;
        } finally {
            lock.unlock(); // 반드시 unlock 호출
        }
    }
    
    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
    
    public boolean tryIncrement() {
        if (lock.tryLock()) { // 락 획득 시도
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false; // 락 획득 실패
    }
}

ReentrantLock의 추가 기능

AdvancedLockExample.java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class AdvancedLockExample {
    private final ReentrantLock lock = new ReentrantLock(true); // 공정성 설정
    
    public void fairLockMethod() {
        lock.lock();
        try {
            // 공정한 락 획득
        } finally {
            lock.unlock();
        }
    }
    
    public boolean timedLockMethod() throws InterruptedException {
        // 2초 동안 락 획득 시도
        if (lock.tryLock(2, TimeUnit.SECONDS)) {
            try {
                // 락 획득 성공
                return true;
            } finally {
                lock.unlock();
            }
        }
        // 락 획득 실패
        return false;
    }
    
    public void interruptibleLockMethod() throws InterruptedException {
        // 인터럽트에 반응하는 락 획득
        lock.lockInterruptibly();
        try {
            // 락 획득 성공
        } finally {
            lock.unlock();
        }
    }
}

ReadWriteLock

ReadWriteLock은 읽기 작업이 많고 쓰기 작업이 적은 경우에 유용한 동기화 메커니즘입니다.

ReadWriteCounter.java
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteCounter {
    private int count = 0;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    public void increment() {
        lock.writeLock().lock(); // 쓰기 락 (배타적)
        try {
            count++;
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public int getCount() {
        lock.readLock().lock(); // 읽기 락 (공유 가능)
        try {
            return count;
        } finally {
            lock.readLock().unlock();
        }
    }
}
ReadWriteLock의 특징:

  • 여러 스레드가 동시에 읽기 락 획득 가능
  • 쓰기 락은 배타적(exclusive)으로 획득
  • 기본적으로 쓰기 스레드에 우선권 부여
  • 읽기 작업이 쓰기 작업보다 훨씬 많은 경우에 적합

StampedLock

Java 8에서 도입된 StampedLock은 더 유연한 락 메커니즘을 제공합니다.

StampedLockCounter.java
import java.util.concurrent.locks.StampedLock;

public class StampedLockCounter {
    private int count = 0;
    private final StampedLock lock = new StampedLock();
    
    public void increment() {
        long stamp = lock.writeLock(); // 쓰기 락 획득 및 스탬프 반환
        try {
            count++;
        } finally {
            lock.unlockWrite(stamp); // 스탬프로 락 해제
        }
    }
    
    public int getCount() {
        long stamp = lock.tryOptimisticRead(); // 낙관적 읽기 시도
        int currentCount = count; // 값 읽기
        
        if (!lock.validate(stamp)) { // 읽기 중 변경 확인
            stamp = lock.readLock(); // 읽기 락으로 전환
            try {
                currentCount = count; // 값 다시 읽기
            } finally {
                lock.unlockRead(stamp);
            }
        }
        
        return currentCount;
    }
}
StampedLock의 특징:

  • 낙관적 읽기(Optimistic Reading): 락 없이 읽기 시도, 이후 유효성 검사
  • 스탬프 기반: 모든 락 획득/해제 작업이 스탬프(long 값)를 사용
  • 락 변환: 한 모드에서 다른 모드로 락 변환 지원
  • 높은 성능: 경쟁이 적은 경우 ReadWriteLock보다 높은 성능
  • 제한 사항: 재진입(reentrant) 불가, 조건 변수(condition) 미지원

ThreadLocal

ThreadLocal은 각 스레드가 자신만의 변수 사본을 가질 수 있게 해주는 특별한 변수 타입입니다.

ThreadLocalExample.java
import java.text.SimpleDateFormat;
import java.util.Date;

public class ThreadLocalExample {
    // 각 스레드별로 독립적인 SimpleDateFormat 인스턴스 제공
    private static final ThreadLocal<SimpleDateFormat> dateFormatter = 
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
    
    public String formatDate(Date date) {
        return dateFormatter.get().format(date);
    }
    
    // ThreadLocal 변수를 초기값과 함께 생성
    private static final ThreadLocal<Integer> threadId = 
        ThreadLocal.withInitial(() -> (int) (Thread.currentThread().getId()));
    
    public Integer getThreadId() {
        return threadId.get();
    }
    
    // 사용 후 명시적 제거
    public void cleanup() {
        dateFormatter.remove();
        threadId.remove();
    }
}
ThreadLocal 사용 시 주의사항:

  • 메모리 누수: 스레드 풀에서 사용 시 remove() 호출 필요
  • 상속: 자식 스레드는 부모의 ThreadLocal 값을 상속하지 않음
  • 성능: 과도한 사용 시 성능 저하 가능

동기화 컬렉션

Java는 스레드 안전한 컬렉션 클래스들을 제공합니다.

동기화 래퍼(Synchronized Wrappers)

SynchronizedCollections.java
import java.util.*;

public class SynchronizedCollections {
    public static void main(String[] args) {
        // 동기화 맵
        Map<String, Integer> synchronizedMap = 
            Collections.synchronizedMap(new HashMap<>());
        
        // 동기화 리스트
        List<String> synchronizedList = 
            Collections.synchronizedList(new ArrayList<>());
        
        // 동기화 세트
        Set<Integer> synchronizedSet = 
            Collections.synchronizedSet(new HashSet<>());
        
        // 다중 스레드 환경에서 사용
        synchronizedMap.put("key", 1);
        synchronizedList.add("value");
        
        // 반복 시 명시적 동기화 필요
        synchronized(synchronizedList) {
            for(String value : synchronizedList) {
                System.out.println(value);
            }
        }
    }
}

동시성 컬렉션(Concurrent Collections)

ConcurrentCollections.java
import java.util.concurrent.*;

public class ConcurrentCollections {
    public static void main(String[] args) {
        // ConcurrentHashMap
        ConcurrentMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
        
        // CopyOnWriteArrayList
        CopyOnWriteArrayList<String> copyOnWriteList = new CopyOnWriteArrayList<>();
        
        // ConcurrentLinkedQueue
        Queue<String> concurrentQueue = new ConcurrentLinkedQueue<>();
        
        // BlockingQueue
        BlockingQueue<String> blockingQueue = new LinkedBlockingQueue<>();
        
        // ConcurrentSkipListMap (정렬된 동시성 맵)
        ConcurrentNavigableMap<String, Integer> skipListMap = 
            new ConcurrentSkipListMap<>();
        
        // 안전한 반복 (명시적 동기화 필요 없음)
        for(String key : concurrentMap.keySet()) {
            System.out.println(key);
        }
        
        // 원자적 조건부 연산
        concurrentMap.putIfAbsent("key", 1);
        concurrentMap.replace("key", 1, 2);
    }
}
특징 동기화 컬렉션 동시성 컬렉션
구현 Collections 유틸리티 메소드 java.util.concurrent 패키지
동기화 방식 메소드 전체에 락 적용 세밀한 락 또는 lock-free 알고리즘
반복자(Iterator) fail-fast (ConcurrentModificationException) 일관성 보장 또는 실패 없음
성능 경쟁 상황에서 성능 저하 고병렬성 지원
원자적 연산 제한적 다양한 복합 연산 지원

병렬 스트림 처리

Java 8에서 도입된 Stream API는 병렬 처리를 통해 멀티코어 프로세서의 성능을 활용할 수 있는 강력한 기능을 제공합니다.

ParallelStreamExample.java
import java.util.Arrays;
import java.util.List;

public class ParallelStreamExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
        // 순차 스트림
        long startSeq = System.nanoTime();
        int sumSeq = numbers.stream()
                           .filter(n -> n % 2 == 0)
                           .mapToInt(n -> n * 2)
                           .sum();
        long endSeq = System.nanoTime();
        
        // 병렬 스트림
        long startPar = System.nanoTime();
        int sumPar = numbers.parallelStream()
                           .filter(n -> n % 2 == 0)
                           .mapToInt(n -> n * 2)
                           .sum();
        long endPar = System.nanoTime();
        
        System.out.println("Sequential: " + sumSeq + ", time: " + (endSeq - startSeq) + " ns");
        System.out.println("Parallel: " + sumPar + ", time: " + (endPar - startPar) + " ns");
    }
}

병렬 스트림 사용 시 주의사항

1. 상태 공유와 부작용(Side Effect): 병렬 스트림에서는 공유 상태를 수정하는 작업을 피해야 합니다.
잘못된 예제
// 문제가 발생할 수 있는 코드
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> result = new ArrayList<>();

// 병렬 처리 중 ArrayList에 동시 접근으로 인한 예상치 못한 결과
numbers.parallelStream()
       .map(n -> n * 2)
       .forEach(n -> result.add(n)); // 안전하지 않음!

System.out.println(result.size()); // 10보다 작을 수 있음
올바른 해결책
// 올바른 방법
List<Integer> result = numbers.parallelStream()
                             .map(n -> n * 2)
                             .collect(Collectors.toList()); // 스레드 안전

병렬 스트림의 커스텀 스레드 풀

CustomThreadPoolExample.java
import java.util.concurrent.ForkJoinPool;

public class CustomThreadPoolExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        
        // 커스텀 스레드 풀 생성 (4개 스레드)
        ForkJoinPool customPool = new ForkJoinPool(4);
        
        try {
            // 커스텀 풀에서 병렬 작업 실행
            int sum = customPool.submit(() ->
                numbers.parallelStream()
                       .filter(n -> n % 2 == 0)
                       .mapToInt(n -> {
                           System.out.println("Processing " + n + 
                               " in thread " + Thread.currentThread().getName());
                           return n * 2;
                       })
                       .sum()
            ).get();
            
            System.out.println("Sum: " + sum);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            customPool.shutdown();
        }
    }
}
병렬 스트림 사용 권장사항:

  • 독립적인 작업(데이터 간 종속성 없음)
  • 계산 비용이 높은 작업
  • 충분히 큰 데이터셋
  • 분할이 용이한 자료구조 사용(ArrayList, 배열)
  • 상태를 변경하는 연산 피하기

이름 없는 패턴(Unnamed Patterns) – Java 21

Java 21에서 도입된 이름 없는 패턴(Unnamed Patterns)은 사용하지 않는 변수를 명시적으로 처리할 수 있게 해주는 기능입니다.

기본 사용법
// Java 21 이전 - 사용하지 않는 변수에 대한 경고
public class PatternExample {
    public void processPoint(Object obj) {
        if (obj instanceof Point(int x, int y)) {
            // y 좌표는 사용하지 않지만 선언해야 함
            System.out.println("X coordinate: " + x);
        }
    }
}

// Java 21 - 이름 없는 패턴 사용
public class UnnamedPatternExample {
    public void processPoint(Object obj) {
        if (obj instanceof Point(int x, _)) {
            // y 좌표는 사용하지 않음을 명시적으로 표현
            System.out.println("X coordinate: " + x);
        }
    }
}

switch 표현식에서의 이름 없는 패턴

SwitchUnnamedPatterns.java
public class SwitchUnnamedPatterns {
    // Record 타입 정의
    public record Point(int x, int y) {}
    public record Color(int r, int g, int b) {}
    
    public void demonstrateSwitchPatterns(Object obj) {
        // 이름 없는 패턴을 사용한 switch 표현식
        String result = switch (obj) {
            case Point(int x, _) when x > 0 -> 
                "Positive X point: " + x;
            case Point(_, int y) when y > 0 -> 
                "Positive Y point: " + y;
            case Point(_, _) -> 
                "Point at origin or negative coordinates";
            case Color(int r, _, _) when r > 128 -> 
                "High red component: " + r;
            case Color(_, _, _) -> 
                "Color with low red component";
            case null -> 
                "Null object";
            default -> 
                "Unknown object type";
        };
        
        System.out.println(result);
    }
}

try-catch에서의 이름 없는 패턴

UnnamedExceptionHandling.java
import java.io.IOException;

public class UnnamedExceptionHandling {
    public void demonstrateUnnamedCatch() {
        try {
            performRiskyOperation();
        } catch (IOException _) {
            // IOException은 사용하지 않음
            System.err.println("IO operation failed");
        } catch (SecurityException _) {
            // SecurityException은 사용하지 않음
            System.err.println("Security check failed");
        } catch (Exception e) {
            // 다른 예외는 사용할 수 있음
            System.err.println("Unexpected error: " + e.getMessage());
        }
    }
}
이름 없는 패턴의 장점:

  • 코드 명확성: 사용하지 않는 변수를 명시적으로 표현
  • 컴파일러 최적화: 미사용 변수에 대한 경고 제거
  • 의도 표현: 코드 작성자의 의도를 더 명확히 표현
  • 유지보수성: 어떤 부분이 중요한지 쉽게 파악

실제 사례와 성능 비교

계좌 이체 시스템 예제

BankTransferExample.java
public class BankTransferExample {
    public static void main(String[] args) {
        final Account account1 = new Account(1, 1000);
        final Account account2 = new Account(2, 1000);
        
        // 동시에 여러 이체 작업 실행
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
                    transfer(account1, account2, 10);
                    transfer(account2, account1, 10);
                }
            }).start();
        }
    }
    
    // 데드락을 방지하는 계좌 이체 메소드
    private static void transfer(Account from, Account to, int amount) {
        // 항상 낮은 ID의 계좌부터 락 획득으로 데드락 방지
        Account first = from.id < to.id ? from : to;
        Account second = from.id < to.id ? to : from;
        
        synchronized(first) {
            synchronized(second) {
                if (from.balance >= amount) {
                    from.balance -= amount;
                    to.balance += amount;
                }
            }
        }
    }
    
    static class Account {
        final int id;
        int balance;
        
        Account(int id, int balance) {
            this.id = id;
            this.balance = balance;
        }
    }
}

성능 비교 벤치마크

SynchronizationBenchmark.java
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class SynchronizationBenchmark {
    private static final int THREADS = 10;
    private static final int ITERATIONS = 1_000_000;
    
    public static void main(String[] args) throws InterruptedException {
        // 다양한 카운터 구현 테스트
        testCounter("Unsynchronized", new UnsynchronizedCounter());
        testCounter("Synchronized", new SynchronizedCounter());
        testCounter("LockBased", new LockBasedCounter());
        testCounter("Atomic", new AtomicCounter());
    }
    
    private static void testCounter(String type, Counter counter) throws InterruptedException {
        Thread[] threads = new Thread[THREADS];
        
        long start = System.nanoTime();
        
        // 스레드 생성 및 시작
        for (int i = 0; i < THREADS; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < ITERATIONS; j++) {
                    counter.increment();
                }
            });
            threads[i].start();
        }
        
        // 모든 스레드 완료 대기
        for (Thread thread : threads) {
            thread.join();
        }
        
        long end = System.nanoTime();
        long duration = (end - start) / 1_000_000; // 밀리초
        
        System.out.printf("%s Counter: count=%d, time=%d ms%n", 
                          type, counter.getCount(), duration);
    }
}

동기화 관련 일반적인 문제점과 해결책

1. 데드락(Deadlock)

두 개 이상의 스레드가 서로 상대방이 점유한 자원을 기다리며 무한정 대기하는 상태입니다.

데드락 예제
public class DeadlockExample {
    private final Object resource1 = new Object();
    private final Object resource2 = new Object();
    
    public void method1() {
        synchronized(resource1) {
            System.out.println("Thread 1: Holding resource 1...");
            
            try { Thread.sleep(100); } 
            catch (InterruptedException e) {}
            
            System.out.println("Thread 1: Waiting for resource 2...");
            synchronized(resource2) {
                System.out.println("Thread 1: Holding resource 1 & 2...");
            }
        }
    }
    
    public void method2() {
        synchronized(resource2) { // 여기서 순서가 다름 (데드락 원인)
            System.out.println("Thread 2: Holding resource 2...");
            
            try { Thread.sleep(100); } 
            catch (InterruptedException e) {}
            
            System.out.println("Thread 2: Waiting for resource 1...");
            synchronized(resource1) {
                System.out.println("Thread 2: Holding resource 1 & 2...");
            }
        }
    }
}
데드락 해결 방법:

  1. 락 순서 정하기: 항상 동일한 순서로 락 획득
  2. 락 타임아웃 사용: tryLock()에 타임아웃 설정
  3. 데드락 감지 및 복구: 순환 대기 감지 후 락 해제

2. 라이브락(Livelock)

스레드가 계속 활성화되어 있으나, 작업을 진행하지 못하는 상태입니다.

라이브락 해결 방법:

  • 무작위 지연(Random Delay) 추가
  • 우선순위 기반 접근
  • 백오프(backoff) 전략 사용

3. 기아 상태(Starvation)

특정 스레드가 필요한 자원을 계속 얻지 못하는 상태입니다.

기아 상태 해결 방법:

  • 공정한 락(Fair Lock) 사용: new ReentrantLock(true)
  • 자원 사용 시간 제한
  • 우선순위 기반 스케줄링

4. 성능 최적화 기법

락 분할(Lock Splitting)

LockSplittingExample.java
// 분할 전 - 하나의 락 사용
class SingleLockCache {
    private final Object lock = new Object();
    private Map<String, Object> map1 = new HashMap<>();
    private Map<String, Object> map2 = new HashMap<>();
    
    public Object getFromMap1(String key) {
        synchronized(lock) {
            return map1.get(key);
        }
    }
    
    public Object getFromMap2(String key) {
        synchronized(lock) {
            return map2.get(key);
        }
    }
}

// 분할 후 - 각 맵마다 별도의 락 사용
class SplitLockCache {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();
    private Map<String, Object> map1 = new HashMap<>();
    private Map<String, Object> map2 = new HashMap<>();
    
    public Object getFromMap1(String key) {
        synchronized(lock1) {
            return map1.get(key);
        }
    }
    
    public Object getFromMap2(String key) {
        synchronized(lock2) {
            return map2.get(key);
        }
    }
}

락 스트라이핑(Lock Striping)

StripedMap.java
public class StripedMap<K, V> {
    private static final int NUM_LOCKS = 16;
    private final Object[] locks;
    private final List<Map<K, V>> maps;
    
    public StripedMap() {
        locks = new Object[NUM_LOCKS];
        maps = new ArrayList<>(NUM_LOCKS);
        
        for (int i = 0; i < NUM_LOCKS; i++) {
            locks[i] = new Object();
            maps.add(new HashMap<>());
        }
    }
    
    private int hash(Object key) {
        return Math.abs(key.hashCode() % NUM_LOCKS);
    }
    
    public V get(K key) {
        int hash = hash(key);
        synchronized (locks[hash]) {
            return maps.get(hash).get(key);
        }
    }
    
    public V put(K key, V value) {
        int hash = hash(key);
        synchronized (locks[hash]) {
            return maps.get(hash).put(key, value);
        }
    }
}

요약 및 결론

Java의 동기화 메커니즘은 다양한 상황에 맞게 선택할 수 있는 풍부한 옵션을 제공합니다. 적절한 동기화 메커니즘을 선택하는 것은 애플리케이션의 성능과 안정성에 큰 영향을 미칩니다.

동기화 메커니즘 선택 가이드

상황 권장 메커니즘 이유
단순한 동기화 synchronized 간단하고 안전함
가시성만 보장 volatile 성능 우수, 단순 변수에 적합
단일 변수의 원자적 연산 Atomic 클래스 Non-blocking, 높은 성능
세밀한 제어 필요 Lock 인터페이스 타임아웃, 인터럽트 지원
읽기 작업이 많음 ReadWriteLock 동시 읽기 허용
최대 성능 필요 StampedLock 낙관적 읽기로 성능 최적화
스레드별 데이터 ThreadLocal 동기화 불필요
고성능 컬렉션 java.util.concurrent 세밀한 동기화

모범 사례

  1. 공유 상태 최소화: 가능한 한 공유 상태를 최소화하세요
  2. 불변 객체 사용: 상태가 변경되지 않는 불변 객체를 활용하세요
  3. 락 범위 최소화: 필요한 코드 부분만 동기화하세요
  4. 데드락 방지: 일관된 락 획득 순서를 유지하세요
  5. 세밀한 락 사용: 단일 락 대신 여러 개의 세밀한 락을 고려하세요
  6. 적절한 도구 선택: 상황에 맞는 동기화 메커니즘을 선택하세요
  7. 성능 테스트: 중요한 코드는 반드시 성능 테스트를 수행하세요
  8. 요구사항 분석: 정확히 어떤 종류의 스레드 안전성이 필요한지 파악하세요

이 가이드를 통해 Java의 동기화 메커니즘을 마스터하고 멀티스레드 프로그래밍에서 발생할 수 있는 다양한 문제들을 해결하는 데 도움이 되길 바랍니다.

멀티스레드 프로그래밍은 복잡하지만, 적절한 동기화 메커니즘을 사용하면 안전하고 효율적인 애플리케이션을 개발할 수 있습니다. 각 메커니즘의 특성을 이해하고 상황에 맞게 적용하는 것이 성공적인 동시성 프로그래밍의 열쇠입니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다