public class Counter {
private int count = 0;
// 동시성 제어가 없는 경우
public void increment() {
count++; // 문제 발생 가능 지점
}
public int getCount() {
return count;
}
}
위 코드는 다음과 같은 문제를 발생시킬 수 있습니다:
1. Race Condition
2. 가시성(Visibility) 문제
3. 원자성(Atomicity) 위배
public class SafeCounter {
private int count = 0;
// 메서드 전체를 동기화
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
public class BlockSyncExample {
private final Object lockObject = new Object();
private int count = 0;
public void increment() {
synchronized(lockObject) {
count++;
}
}
public void complexOperation() {
// 동기화가 필요없는 작업
heavyComputation();
// 동기화가 필요한 부분만 블록으로 처리
synchronized(lockObject) {
count++;
}
}
}
Lock 인터페이스는 synchronized보다 더 유연하고 세밀한 동시성 제어를 제공합니다. 주요 메서드들은 다음과 같습니다:
lock(): 락을 획득할 때까지 대기unlock(): 락 해제tryLock(): 락 획득을 시도하고 즉시 결과 반환tryLock(long time, TimeUnit unit): 지정된 시간 동안만 락 획득 시도newCondition(): 현재 락과 연관된 Condition 객체 생성public class ReentrantLockCounter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock(); // 명시적 잠금
try {
count++;
} finally {
lock.unlock(); // 명시적 해제
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
ReentrantLock은 가장 일반적으로 사용되는 Lock 구현체입니다. "Reentrant"는 재진입이 가능하다는 의미로, 같은 스레드가 이미 획득한 락을 다시 획득할 수 있습니다.
공정성 (Fairness)
// 공정한 락 생성
private final ReentrantLock fairLock = new ReentrantLock(true);
// 비공정한 락 생성 (기본값)
private final ReentrantLock unfairLock = new ReentrantLock(false);
락 획득 시도 (tryLock)
public boolean transferMoney(Account from, Account to, double amount) {
boolean fromLocked = from.getLock().tryLock();
try {
if (!fromLocked) {
return false; // 락 획득 실패
}
boolean toLocked = to.getLock().tryLock();
try {
if (!toLocked) {
return false; // 락 획득 실패
}
// 실제 송금 처리
if (from.getBalance() < amount) {
throw new InsufficientFundsException();
}
from.debit(amount);
to.credit(amount);
return true;
} finally {
if (toLocked) {
to.getLock().unlock();
}
}
} finally {
if (fromLocked) {
from.getLock().unlock();
}
}
}
인터럽트 가능한 락 획득
public void lockInterruptibly() throws InterruptedException {
lock.lockInterruptibly();
try {
// 임계 영역
} finally {
lock.unlock();
}
}
락 상태 확인
ReentrantLock lock = new ReentrantLock();
// 현재 락이 획득된 상태인지 확인
boolean isLocked = lock.isLocked();
// 현재 스레드가 락을 보유하고 있는지 확인
boolean isHeldByCurrentThread = lock.isHeldByCurrentThread();
// 대기 중인 스레드 수 확인
int queueLength = lock.getQueueLength();
public class AdvancedLockExample {
private final ReentrantLock lock = new ReentrantLock(true); // 공정성 설정
public void tryLockExample() {
boolean acquired = lock.tryLock(); // 비차단 잠금 시도
if (acquired) {
try {
// 임계 영역
} finally {
lock.unlock();
}
}
}
public void timeoutLockExample() {
try {
boolean acquired = lock.tryLock(1, TimeUnit.SECONDS); // 타임아웃 설정
if (acquired) {
try {
// 임계 영역
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
ReadWriteLock은 읽기와 쓰기 작업을 분리하여 처리하는 락 인터페이스입니다. 읽기 작업은 동시에 여러 스레드가 수행할 수 있지만, 쓰기 작업은 독점적으로 수행되어야 할 때 사용합니다.
읽기 락(Read Lock)
쓰기 락(Write Lock)
public class EnhancedCache {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private final Map<String, String> cache = new HashMap<>();
private final Map<String, Long> expiryMap = new HashMap<>();
private final long defaultExpiryMs = 60000; // 1분
public String get(String key) {
readLock.lock();
try {
// 만료 체크
Long expiryTime = expiryMap.get(key);
if (expiryTime != null && System.currentTimeMillis() > expiryTime) {
readLock.unlock(); // 읽기 락 해제
// 만료된 데이터 제거를 위해 쓰기 락 획득
writeLock.lock();
try {
cache.remove(key);
expiryMap.remove(key);
return null;
} finally {
writeLock.unlock();
}
}
return cache.get(key);
} finally {
if (readLock.tryLock()) { // 위에서 이미 unlock한 경우 체크
readLock.unlock();
}
}
}
public void put(String key, String value) {
writeLock.lock();
try {
cache.put(key, value);
expiryMap.put(key, System.currentTimeMillis() + defaultExpiryMs);
} finally {
writeLock.unlock();
}
}
public int size() {
readLock.lock();
try {
return cache.size();
} finally {
readLock.unlock();
}
}
// 캐시 청소
public void cleanup() {
writeLock.lock();
try {
long now = System.currentTimeMillis();
Iterator<Map.Entry<String, Long>> it = expiryMap.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Long> entry = it.next();
if (now > entry.getValue()) {
cache.remove(entry.getKey());
it.remove();
}
}
} finally {
writeLock.unlock();
}
}
}
public class CacheWithReadWriteLock {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Lock readLock = rwLock.readLock();
private final Lock writeLock = rwLock.writeLock();
private final Map<String, String> cache = new HashMap<>();
public String get(String key) {
readLock.lock();
try {
return cache.get(key);
} finally {
readLock.unlock();
}
}
public void put(String key, String value) {
writeLock.lock();
try {
cache.put(key, value);
} finally {
writeLock.unlock();
}
}
}
StampedLock은 Java 8에서 도입된 새로운 락 메커니즘으로, ReadWriteLock의 성능을 개선한 버전입니다. 가장 큰 특징은 낙관적 읽기(Optimistic Reading) 모드를 제공한다는 점입니다.
쓰기 모드 (Write Mode)
long stamp = lock.writeLock(); // 배타적 락 획득
try {
// 쓰기 작업 수행
} finally {
lock.unlockWrite(stamp);
}
읽기 모드 (Read Mode)
long stamp = lock.readLock(); // 공유 락 획득
try {
// 읽기 작업 수행
} finally {
lock.unlockRead(stamp);
}
낙관적 읽기 모드 (Optimistic Read Mode)
long stamp = lock.tryOptimisticRead(); // 락 획득 없이 낙관적으로 읽기
// 데이터 읽기
if (!lock.validate(stamp)) { // 도중에 데이터가 변경되었는지 확인
// 일반 읽기 모드로 전환
stamp = lock.readLock();
try {
// 데이터 다시 읽기
} finally {
lock.unlockRead(stamp);
}
}
public class Point {
private final StampedLock sl = new StampedLock();
private double x, y;
// 좌표 이동 (쓰기 모드)
public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
// 현재 좌표 읽기 (읽기 모드)
public Point getPoint() {
long stamp = sl.readLock();
try {
return new Point(x, y);
} finally {
sl.unlockRead(stamp);
}
}
// 원점으로부터의 거리 계산 (낙관적 읽기 모드)
public double distanceFromOrigin() {
// 낙관적 읽기 시도
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
// 데이터가 변경되었는지 확인
if (!sl.validate(stamp)) {
// 데이터가 변경되었다면 일반 읽기 락으로 전환
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
// 특정 범위 내에 있는지 확인 (락 변환 예시)
public boolean moveIfInRange(double newX, double newY, double radius) {
// 낙관적 읽기로 시작
long stamp = sl.tryOptimisticRead();
try {
double currentX = x;
double currentY = y;
if (!sl.validate(stamp)) {
// 낙관적 읽기 실패시 읽기 락으로 전환
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
// 범위 체크
if (Math.sqrt((newX - currentX) * (newX - currentX) +
(newY - currentY) * (newY - currentY)) > radius) {
return false;
}
// 쓰기 락으로 전환
long ws = sl.writeLock();
try {
x = newX;
y = newY;
return true;
} finally {
sl.unlockWrite(ws);
}
} finally {
// 읽기 락이 남아있다면 해제
if (StampedLock.isReadLockStamp(stamp)) {
sl.unlockRead(stamp);
}
}
}
}
public class StampedLockExample {
private final StampedLock sl = new StampedLock();
private double x, y;
public void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
public double distanceFromOrigin() {
// 낙관적 읽기
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
if (!sl.validate(stamp)) {
// 낙관적 읽기 실패시 일반 읽기 락으로 전환
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
public class BoundedBuffer<T> {
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
private final T[] items;
private int putIndex, takeIndex, count;
public BoundedBuffer(int capacity) {
items = (T[]) new Object[capacity];
}
public void put(T item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await();
}
items[putIndex] = item;
putIndex = (putIndex + 1) % items.length;
count++;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public T take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();
}
T item = items[takeIndex];
takeIndex = (takeIndex + 1) % items.length;
count--;
notFull.signal();
return item;
} finally {
lock.unlock();
}
}
}
synchronized 사용이 적절한 경우
ReentrantLock 사용이 적절한 경우
ReadWriteLock 사용이 적절한 경우
StampedLock 사용이 적절한 경우
public class PerformanceExample {
// 세밀한 락 분할
private final Lock lock1 = new ReentrantLock();
private final Lock lock2 = new ReentrantLock();
public void operation1() {
lock1.lock();
try {
// 작업 1
} finally {
lock1.unlock();
}
}
public void operation2() {
lock2.lock();
try {
// 작업 2
} finally {
lock2.unlock();
}
}
}
Q: synchronized와 ReentrantLock의 차이점을 설명해주세요.
A: 주요 차이점:
기능적 측면
유연성
성능
Q: Java의 동시성 문제를 해결하기 위한 방법들을 설명해주세요.
A: 동시성 문제 해결 방법:
락 기반 해결책
격리 기반 해결책
비차단 알고리즘
Q: ReadWriteLock을 사용할 때의 장단점은 무엇인가요?
A: ReadWriteLock 특징:
장점
단점