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()
)을 제공. - 재진입성이 있어 동일 스레드가 여러 번 락을 획득할 수 있다.
- 명시적으로 락을 획득하고 해제해야 하며, 공정성(fairness) 옵션과 다양한 락 획득 방식(
- 사용 사례:
- 락 획득 시 타임아웃이나 인터럽트가 필요한 경우, 조건(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: 스레드 간 협업 및 단계별 동기화가 필요한 경우.
- 저수준 스레드 제어 도구:
- LockSupport와 AbstractQueuedSynchronizer(AQS): 커스텀 동기화 도구 구현 및 내부 동기화 메커니즘 확장을 위한 기반으로 사용.
Comments