-
java.util.concurrent.locksJAVA 2025. 7. 17. 18:06728x90반응형
java.util.concurrent.locks 패키지는 내장된 동기화(synchronization) 및 모니터(monitor) 기능과는 구별되는 락(lock)과 조건(condition) 대기를 위한 프레임워크를 제공하는 인터페이스 및 클래스들로 구성되어 있습니다.
구문(syntax)이 다소 복잡해지는 대가로 락과 조건 변수 사용에 있어 훨씬 더 유연한 방식을 허용합니다.
Lock Interface : 유연한 동시성 제어를 위한 핵심 도구
- 주요 구현체 : ReentrantLock
Lock 인터페이스는 재진입, 공정성 등 다양한 락 제어 방식을 지원하며 synchronized처럼 코드 블록 단위로만 락을 거는 방식과 달리 필요한 시점에 직접 락을 걸고 해제할 수 있기 때문에 노드 하나씩 이동하며 락을 넘겨주는 방식(hand-over-hand locking)이나 락을 걸 순서를 바꿔야 하는 상황(lock reordering)처럼 복잡하고 유동적인 흐름에서도 유연하게 동기화를 제어할 수 있습니다.
📖 hand-over-hand locking
탐색 중 현재 노드의 락을 획득한 상태에서 다음 노드의 락을 먼저 획득한 뒤 현재 노드의 락을 해제하는 방식입니다.
이 방식은 짧은 범위의 락 점유로 경쟁을 줄이고 동시성(concurrency)을 높이는 데 도움이 됩니다.
💡 노드 란 무엇인가?
값(data)과 다른 노드를 가리키는 참조(reference, pointer)를 함께 가지는 데이터 단위를 말합니다.
가장 이해하기 쉬운 예시로는 연결 리스트(Linked List)가 있습니다.
class Node { int value; Node next; Node(int value) { this.value = value; this.next = null; } }
💡 Code로 알아보는 hand-over-hand locking 예시
아래는 연결 리스트의 각 노드에 락을 두고 핸드 오버 핸드 락킹(hand-over-hand locking) 방식으로 안전하게 값을 탐색하는 예시이며 이 방법은 연결 리스트 전체에 락을 거는 방식과 달리 짧은 범위의 락 점유로 경쟁을 줄여 동시성(concurrency)을 높이는 데 효과적입니다.
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class LockedNode { int value; LockedNode next; final Lock lock = new ReentrantLock(); LockedNode(int value) { this.value = value; this.next = null; } } public class LockedLinkedList { private LockedNode head; public LockedLinkedList() { head = null; } public void addFirst(int value) { LockedNode newNode = new LockedNode(value); if (head != null) { newNode.next = head; } head = newNode; } // ⭐ hand-over-hand locking public boolean contains(int value) { LockedNode current = head; if (current == null) return false; current.lock.lock(); try { while (true) { if (current.value == value) { return true; } LockedNode next = current.next; if (next == null) { return false; } next.lock.lock(); // 다음 노드 락 먼저 획득 current.lock.unlock(); // 현재 노드 락 해제 current = next; // 다음 노드로 이동 } } finally { ⭐ 예외가 터져도 락 해제 current.lock.unlock(); } } }
📖 lock reordering
락 획득 순서가 일관되지 않으면 교착 상태가 발생할 수 있습니다.
lock reordering은 이를 예방하기 위해 락 획득 순서를 조정하여 서로 다른 스레드가 락을 반대 순서로 획득하는 상황을 방지합니다.💡 Code로 알아보는 lock reordering 예시
public class LockReorderingExample { private final Object lockA = new Object(); private final Object lockB = new Object(); public void safeMethod() { Object firstLock; Object secondLock; // ⭐ identityHashCode를 기준으로 정렬하여 락 순서 결정 // 이 값은 JVM 실행마다 달라질 수 있지만 한 번의 실행 내에서는 lockA와 lockB 간의 상대적인 순서를 항상 명확하게 결정합니다. if (System.identityHashCode(lockA) < System.identityHashCode(lockB)) { firstLock = lockA; secondLock = lockB; } else { firstLock = lockB; secondLock = lockA; } synchronized (firstLock) { synchronized (secondLock) { // 안전한 작업 수행 } } } }
ReadWriteLock Interface : 읽기와 쓰기를 분리한 동시성 제어
- 주요 구현체 : ReentrantReadWriteLock
읽기(read) 와 쓰기(write) 작업을 분리하여 동기화를 제어할 수 있도록 설계된 인터페이스리며 synchronized 또는 ReentrantLock과 같은 상호 배제(mutex) 기반 락보다 더 좋은 동시성(concurrency) 을 제공할 수 있습니다.
읽기 작업이 빈번하고 쓰기 작업은 드문 시스템에서 성능 최적화에 매우 효과적입니다.
예를 들어 데이터가 한 번 초기화된 후 변경은 거의 없지만 자주 조회되는 경우(예: 사전, 캐시, 설정 값)입니다.
📖 구성 요소
public interface ReadWriteLock { Lock readLock(); // 다중 읽기 전용 락 Lock writeLock(); // 단일 쓰기 전용 락 }
- readLock(): 여러 스레드가 동시에 획득 가능합니다. (단, 쓰기 중일 경우 대기)
- writeLock(): 단일 스레드만 접근 가능합니다. (읽기와 쓰기 모두에 대해 배타적)
- writeLock 획득된 경우 모든 읽기 락 요청은 차단됩니다.
- writeLock 해제 후 락 정책에 따라 다음 락을 획득할 스레드(읽기 또는 쓰기)가 결정됩니다.
💡 Code로 알아보는 ReadWriteLock 사용 예시
public class ReadWriteLockTest { private int data = 0; private final ReadWriteLock rwLock = new ReentrantReadWriteLock(); private final Lock readLock = rwLock.readLock(); private final Lock writeLock = rwLock.writeLock(); // 📘 읽기 작업: 여러 스레드가 동시에 접근 가능 public int read() { readLock.lock(); try { System.out.println(" [읽기] " + Thread.currentThread().getName() + " : " + data); return data; } finally { readLock.unlock(); } } // ✏️ 쓰기 작업: 하나의 스레드만 접근 가능 public void write(int value) { writeLock.lock(); try { System.out.println(" [쓰기] " + Thread.currentThread().getName() + " : " + value); this.data = value; } finally { writeLock.unlock(); } } }
💡 메모리 가시성과 동기화 보장
ReadWriteLock은 단순히 락 충돌을 방지하는 것뿐만 아니라 메모리 동기화(visibility guarantees) 도 보장합니다.
이는 자바 메모리 모델(JMM)에 따라 락을 사용하는 스레드 간 데이터 일관성과 가시성을 유지하기 위한 중요한 규칙이기 때문에writeLock.unlock() 이전에 수행된 모든 변경사항은 이후에 readLock.lock()을 획득한 스레드에게 반드시 반영되어야 합니다.- 이 내용은 당연해 보일 수 있지만 CPU or JVM은 성능을 위해 각 스레드가 로컬 캐시에 값을 저장하고 사용하게 되는데 이때 스레드 간 불일치 문제가 발생하거나 최적화를 위한 코드 재배치로 값이 달라질 수 있다고 합니다.
data = 0 [Thread-A] writeLock.lock(); data = 1 writeLock.unlock(); -> 해당 내용은 메인 메모리에 flush [Thread-B] readLock.lock(); ← 최신 data 값을 반드시 읽을 수 있음
이러한 메모리 가시성 보장은 ReentrantReadWriteLock을 포함한 모든 ReadWriteLock 구현체에서 의무적으로 제공되어야 하는 계약(Contract)이기 때문에 읽기 락이 쓰기 락 해제 이후의 상태를 정확히 반영하지 않는다면 구현체가 메모리 모델을 위반하는 것입니다.
💡 ReentrantReadWriteLock 락 정책(Lock Policy)
ReentrantReadWriteLock (Java Platform SE 8 )
When constructed as fair, threads contend for entry using an approximately arrival-order policy. When the currently held lock is released, either the longest-waiting single writer thread will be assigned the write lock, or if there is a group of reader thr
docs.oracle.com
항목 ReentrantReadWriteLock(true) ReentrantReadWriteLock(false : default) 락 획득 순서 First In First Out 성능 우선 장점 (writeLock) 스레드 기아 문제 완화 높은 처리 단점 락 획득 / 해제 시 오버헤드 발생 (writeLock) 스레드 기아 문제 발생 이 외에 설정할 수 없지만 알고 있으면 유용한 설계는 아래와 같습니다.
- 읽기 락 승격(Read Lock Upgrading) 방지
- 이미 읽기 락을 획득한 스레드가 쓰기 락을 바로 획득하는 것을 허용하지 않습니다.
- 이는 교착 상태 방지를 위한 설계 결정입니다.
Condition Interface: 유연한 스레드 간 협력 도구
Object 클래스의 wait(), notify(), notifyAll() 메서드와 비슷한 메커니즘을 제공하지만 Lock 인터페이스와 함께 사용될 때 시너지가 좋습니다.
Object 클래스의 wait(), notify(), notifyAll() 메서드가 특정 객체의 모니터와 관련된 단 하나의 대기 집합(wait-set)만을 가지는 것과 달리 Condition은 하나의 Lock 인스턴스에 여러 개의 Condition 인스턴스를 연결하여 사용할 수 있어 특정 조건을 기다리는 스레드 그룹을 개별적으로 관리할 수 있습니다.
- 다중 조건 대기 : 하나의 Lock에 여러 Condition 객체를 연결하여 스레드 그룹을 분리 관리합니다.
- 인터럽트 가능 : await()로 대기 중인 스레드를 외부에서 중단시킬 수 있습니다.
- 시간 제한 대기 : awaitNanos(), awaitUntil() 등으로 특정 시간/시각까지 대기합니다.
- 공정성 : ReentrantLock이 공정성 모드일 때 Condition도 스레드를 공정하게 깨웁니다.
Condition (Java Platform SE 8 )
Condition factors out the Object monitor methods (wait, notify and notifyAll) into distinct objects to give the effect of having multiple wait-sets per object, by combining them with the use of arbitrary Lock implementations. Where a Lock replaces the use
docs.oracle.com
📖 주요 기능
✔️ void await() - throws InterruptedException
현재 스레드를 대기시키고 현재 락을 해제합니다.
다른 스레드가 signal() 또는 signalAll()을 호출하거나 스레드가 인터럽트될 때까지 대기합니다.
✔️ boolean await(long time, TimeUnit unit) throws InterruptedException
지정된 시간 동안 현재 스레드를 대기시킵니다.
nanos를 붙이면 나노 초 단위로 지정할 수 있습니다.
✔️ void signal()
Condition에서 대기 중인 스레드 중 하나를 랜덤하게 깨웁니다.
하지만 공정 모드인 경우 FIFO를 따릅니다.
✔️ void signalAll()
Condition에서 대기 중인 모든 스레드를 깨웁니다.
AbstractQueuedSynchronizer (AQS): 동기화 장치의 기반 프레임워크
ReentrantLock, CountDownLatch, ReentrantReadWriteLock 등 자바의 동기화 컴포넌트들이 이 AQS를 기반으로 구현되어 있으며 내부적으로 FIFO(선입선출) 큐를 사용하여 락을 획득하지 못한 스레드들의 대기열을 관리합니다.
그리고 state라는 하나의 정수형 변수를 통해 동기화 상태(예: 락이 획득되었는지, 세마포어의 허용 개수 등)를 표현 및 관리합니다.
AbstractQueuedSynchronizer (Java Platform SE 8 )
Attempts to acquire in exclusive mode. This method should query if the state of the object permits it to be acquired in the exclusive mode, and if so to acquire it. This method is always invoked by the thread performing acquire. If this method reports fail
docs.oracle.com
📖 동기화 모드
AQS는 두 가지 기본 동기화 모드를 지원합니다.
- 독점 모드 (Exclusive Mode) :: writeLock
- 한 번에 오직 하나의 스레드만 락을 획득할 수 있는 모드입니다.
- 공유 모드 (Shared Mode) :: readLock
- 여러 스레드가 동시에 락을 획득할 수 있는 모드입니다.
📖 동작 원리
AQS를 상속받는 클래스는 아래 추상 메서드를 재정의하여 동기화 로직을 구현합니다.
✔️ protected boolean tryAcquire(int arg)
독점 모드에서 락을 획득하려는 시도를 하며 성공하면 true 실패하면 false를 반환합니다.
이 메서드 내에서 state 변수를 조작하여 락 획득 여부를 결정합니다.
✔️ protected boolean tryRelease(int arg)
독점 모드에서 락을 해제하려는 시도를 하며 성공하면 true 실패하면 false를 반환합니다.
✔️ protected int tryAcquireShared(int arg)
공유 모드에서 락을 획득하려는 시도를 하며 음수 값은 실패, 0은 획득 성공 (더 이상 공유 획득 불가능), 양수 값은 획득 성공 (추가 공유 획득 가능)을 의미합니다.
✔️ protected boolean tryReleaseShared(int arg)
공유 모드에서 락을 해제하려는 시도를 하며 해제 후 추가 대기 스레드를 깨울 수 있으면 true 아니면 false를 반환합니다.
✔️ protected boolean isHeldExclusively()
현재 스레드가 독점적으로 락을 보유하고 있는지 여부를 반환합니다.
AQS를 직접 사용하여 동기화 장치를 만들 때 위의 메서드를 구현하여 state 변수와 획득/해제 로직을 정의하고 이렇게 정의된 로직을 바탕으로 스레드 큐잉(queuing), 대기(waiting), 신호(signaling) 등 복잡한 부분들을 자동으로 처리해줍니다.
728x90반응형'JAVA' 카테고리의 다른 글
java.util.concurrent.Future (0) 2025.08.07 [Java] Thread에 대해 알아보자 (3) 2025.06.26 [Java] I/O 작동 원리 HDD 구조부터 시작하기 (0) 2025.05.29 [Java] 인코딩 디코딩 다시 이해하기 (0) 2025.05.08 [Java 파먹기] 정렬 기준 : Comparable & Comparator (1) 2025.04.17