Java Lock

기본 동기화 메커니즘

내장 동기화 (Intrinsic Synchronization)

  • synchronized
    • 특징:
      • 자바 언어에 내장된 키워드로, 특정 메서드나 코드 블록에 대해 모니터 락(내부 락)을 사용하여 동기화를 수행.
      • 자동 락 해제: 블록 종료 시 자동으로 해제.
      • 재진입 가능(Reentrant): 동일 스레드가 여러 번 진입할 수 있다.
    • 사용 사례:
      • 간단한 동기화가 필요할 때, 별도의 라이브러리 없이 손쉽게 사용하고자 할 때.
public class SynchronizedExample {
    private int count = 0;

    // 메서드 전체를 synchronized로 선언
    public synchronized void increment() {
        count++;
    }

    // 블록 단위로 synchronized 사용
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }

    public int getCount() {
        return count;
    }
}

명시적 락 (Explicit Lock)

1. 표준 명시적 락

  • ReentrantLock
    • 특징:
      • 명시적으로 락을 획득하고 해제해야 하며, 공정성(fairness) 옵션과 다양한 락 획득 방식(tryLock(), lockInterruptibly())을 제공.
      • 재진입성이 있어 동일 스레드가 여러 번 락을 획득할 수 있다.
    • 사용 사례:
      • 락 획득 시 타임아웃이나 인터럽트가 필요한 경우, 조건(Condition)을 이용한 복잡한 동기화 시나리오.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}
  • StampedLock
    • 특징:
      • 읽기와 쓰기 락을 보다 유연하게 사용할 수 있도록 낙관적 읽기(optimistic read)를 지원.
      • 락 획득 시 반환된 스탬프를 기반으로 락 해제 및 상태 검증을 수행하며, 비재진입성.
    • 사용 사례:
      • 읽기 작업이 매우 많고, 데이터 일관성을 유지하면서 성능 최적화가 필요한 경우.
import java.util.concurrent.locks.StampedLock;

public class StampedLockExample {
    private int count = 0;
    private final StampedLock stampedLock = new StampedLock();

    // 쓰기 작업
    public void increment() {
        long stamp = stampedLock.writeLock();
        try {
            count++;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
    }

    // 낙관적 읽기 작업
    public int getCount() {
        long stamp = stampedLock.tryOptimisticRead();
        int currentCount = count;
        // 낙관적 락이 유효한지 검사
        if (!stampedLock.validate(stamp)) {
            // 유효하지 않다면 읽기 락 획득 후 읽기
            stamp = stampedLock.readLock();
            try {
                currentCount = count;
            } finally {
                stampedLock.unlockRead(stamp);
            }
        }
        return currentCount;
    }
}

2. 읽기/쓰기 분리 락

  • ReentrantReadWriteLock
    • 특징:
      • 읽기와 쓰기 작업을 별도로 관리하여, 여러 스레드가 동시에 읽기 작업을 수행할 수 있도록 하면서, 쓰기 작업은 배타적 접근을 보장.
      • 재진입성이 제공된다.
    • 사용 사례:
      • 읽기 작업이 빈번하고, 쓰기 작업이 상대적으로 적은 상황에서 성능 향상과 데이터 일관성을 동시에 달성하고자 할 때.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private int count = 0;
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();

    // 쓰기 작업
    public void increment() {
        writeLock.lock();
        try {
            count++;
        } finally {
            writeLock.unlock();
        }
    }

    // 읽기 작업
    public int getCount() {
        readLock.lock();
        try {
            return count;
        } finally {
            readLock.unlock();
        }
    }
}

3. 자원 접근 제한 도구

  • Semaphore
    • 특징:
      • 지정한 개수의 “permit(허가)”을 관리하며, 동시에 접근할 수 있는 스레드의 수를 제한.
      • 여러 스레드가 동시에 접근할 수 있도록 허용할 수 있어, 전통적인 배타적 락과는 다른 방식의 동시성 제어를 제공.
    • 사용 사례:
      • 제한된 자원(예: 데이터베이스 커넥션, 네트워크 연결)에 동시에 접근 가능한 최대 스레드 수를 조절할 때.
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    // 동시에 3개의 스레드만 허용
    private final Semaphore semaphore = new Semaphore(3);

    public void accessResource() {
        try {
            semaphore.acquire();
            // 임계 영역: 제한된 자원에 접근
            System.out.println(Thread.currentThread().getName() + " is accessing the resource.");
            Thread.sleep(1000); // 자원 사용 중
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            semaphore.release();
        }
    }
}

동기화 보조 도구 및 저수준 제어

1. 스레드 동기화 보조 도구

  • CountDownLatch
    • 특징:
      • 특정 카운트가 0이 될 때까지 하나 이상의 스레드를 대기.
    • 사용 사례:
      • 여러 스레드의 작업이 모두 완료될 때까지 기다린 후, 다음 단계의 작업을 시작할 때.
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        int numberOfWorkers = 3;
        CountDownLatch latch = new CountDownLatch(numberOfWorkers);

        for (int i = 0; i < numberOfWorkers; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " is working.");
                // 작업 수행
                latch.countDown(); // 작업 완료 후 카운트 감소
            }).start();
        }

        // 모든 작업이 완료될 때까지 대기
        latch.await();
        System.out.println("All workers have finished.");
    }
}
  • CyclicBarrier
    • 특징:
      • 여러 스레드가 지정한 지점에 도달할 때까지 서로 대기시키며, 모두 도착하면 동시에 다음 단계로 진행할 수 있다.
    • 사용 사례:
      • 병렬 처리 작업에서 각 단계가 완료된 후 동기화가 필요한 경우.
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
    public static void main(String[] args) {
        int numberOfParties = 3;
        CyclicBarrier barrier = new CyclicBarrier(numberOfParties, () -> System.out.println("All parties have arrived. Let's proceed!"));

        for (int i = 0; i < numberOfParties; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " reached the barrier.");
                    barrier.await();
                    // Barrier 이후 작업 수행
                    System.out.println(Thread.currentThread().getName() + " is proceeding.");
                } catch (InterruptedException | BrokenBarrierException e) {
                    Thread.currentThread().interrupt();
                }
            }).start();
        }
    }
}
  • Phaser
    • 특징:
      • CountDownLatch와 CyclicBarrier의 기능을 결합하여, 단계별 동기화를 보다 유연하게 지원.
      • 동적으로 참가자를 추가하거나 제거할 수 있다.
    • 사용 사례:
      • 복잡한 단계별 동기화 및 동적 스레드 참가자 관리가 필요한 경우.
import java.util.concurrent.Phaser;

public class PhaserExample {
    public static void main(String[] args) {
        Phaser phaser = new Phaser(1); // 메인 스레드 등록

        // 3개의 작업 스레드 등록
        int numberOfParties = 3;
        for (int i = 0; i < numberOfParties; i++) {
            phaser.register();
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + " is performing phase 1.");
                phaser.arriveAndAwaitAdvance(); // 1단계 완료
                System.out.println(Thread.currentThread().getName() + " is performing phase 2.");
                phaser.arriveAndDeregister(); // 완료 후 등록 해제
            }).start();
        }

        // 메인 스레드도 1단계 참여
        phaser.arriveAndAwaitAdvance();
        System.out.println("All parties have completed phase 1.");
    }
}

2. 저수준 스레드 제어 도구

  • LockSupport
    • 특징:
      • 스레드를 블록하거나 깨우기 위한 저수준 API를 제공.
    • 사용 사례:
      • 자신만의 동기화 도구나 커스텀 락을 구현할 때 기반 기술로 활용.
import java.util.concurrent.locks.LockSupport;

public class LockSupportExample {
    public static void main(String[] args) {
        Thread parkedThread = new Thread(() -> {
            System.out.println("Thread is going to park.");
            // 스레드를 일시 정지
            LockSupport.park();
            System.out.println("Thread is unparked.");
        });

        parkedThread.start();

        try {
            Thread.sleep(2000); // 2초 후
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 스레드를 깨운다.
        LockSupport.unpark(parkedThread);
    }
}
  • AbstractQueuedSynchronizer (AQS)
    • 특징:
      • 자바 동시성 라이브러리에서 많은 명시적 락 및 동기화 보조 도구(ReentrantLock, Semaphore 등)의 내부 기반으로 사용되는 추상 클래스.
    • 사용 사례:
      • 새로운 동기화 도구를 직접 구현할 때, AQS의 기능을 확장하여 활용할 수 있다.
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class SimpleAqsLock {
    private final Sync sync = new Sync();

    private static class Sync extends AbstractQueuedSynchronizer {
        // 0: unlock, 1: lock
        @Override
        protected boolean tryAcquire(int acquires) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int releases) {
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }

    public void lock() {
        sync.acquire(1);
    }

    public void unlock() {
        sync.release(1);
    }
    
    // 간단한 사용 예
    public static void main(String[] args) {
        SimpleAqsLock lock = new SimpleAqsLock();
        Runnable task = () -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " acquired the lock.");
                Thread.sleep(500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + " released the lock.");
            }
        };

        new Thread(task).start();
        new Thread(task).start();
    }
}

정리

자바의 동시성 제어 도구는 크게 기본 동기화 메커니즘명시적 락 및 동기화 보조 도구로 나뉜다.

기본 동기화 메커니즘

  • synchronized: - 내장된 동기화 방법으로 간단한 동기화 요구에 적합.

명시적 락 (Explicit Lock)

  • 표준 명시적 락:
    • ReentrantLock: 복잡한 동기화 옵션(타임아웃, 인터럽트, 조건 변수 등)이 필요한 경우.
    • StampedLock: 읽기 작업이 많은 환경에서 낙관적 락을 통한 성능 최적화를 도모할 때.
  • 읽기/쓰기 분리 락:
    • ReentrantReadWriteLock: 읽기와 쓰기 작업을 분리하여 동시성 및 성능 개선이 필요한 경우.
  • 자원 접근 제한 도구:
    • Semaphore: 제한된 자원에 대해 동시 접근 가능한 스레드 수를 조절할 때.

동기화 보조 도구 및 저수준 제어

  • 스레드 동기화 보조 도구:
    • CountDownLatch, CyclicBarrier, Phaser: 스레드 간 협업 및 단계별 동기화가 필요한 경우.
  • 저수준 스레드 제어 도구:
    • LockSupportAbstractQueuedSynchronizer(AQS): 커스텀 동기화 도구 구현 및 내부 동기화 메커니즘 확장을 위한 기반으로 사용.

Categories:

Updated:

Comments