동시성 문제
여러 스레드가 동시에 실행될 때 발생하는 문제. 교착 상태, 데드락이 대표적이다.
교착 상태란 여러 스레드가 같은 공유 자원에 동시에 접근해 각 스레드가 공유 자원의 무결성이 깨지는 상황을 의미한다.
데드락이란, 여러 스레드가 서로를 블로킹해 스레드가 영원히 실행되지 않는 상황을 의미한다.
이런 문제를 해결하기 위해서는 공유 자원에 접근하는 코드(= 임계 영역)에 접근 가능한 스레드를 제한하는 방법을 사용한다.
synchronized
이 키워드가 붙은 메서드나 블록을 모니터를 사용해 동시에 하나의 스레드만 접근할 수 있도록 한다.
public static void main(String[] args) {
Test test = new Test();
ExecutorService executorService = Executors.newFixedThreadPool(5);
executorService.execute(() -> {
test.increase();
log.info(Thread.currentThread().getName() + " " + test.number + " ");
});
executorService.execute(() -> {
test.increase();
log.info(Thread.currentThread().getName() + " " + test.number + " ");
});
executorService.execute(() -> {
test.increase();
log.info(Thread.currentThread().getName() + " " + test.number + " ");
});
}
Java
복사
class Test {
int number;
public synchronized void increase() {
log.info(Thread.currentThread().getName() + " lock 보유 " + number);
number++;
log.info(Thread.currentThread().getName() + " lock 반환 " + number);
}
}
Java
복사
위와 같이 메서드에 synchronized 키워드가 붙은 경우 이 메서드는 동시에 하나의 스레드에서만 실행될 수 있고, 다른 스레드는 그 동안 블로킹된다. 따라서 다음 결과 처럼 하나의 스레드에 대해 lock 보유와 반환이 번갈아가며 등장한다.
동시성 제어가 없는 경우
단점
특정 스레드 입장에서, 해당 스레드가 락을 보유하고있는지 아닌지 코드로 알 수 없다.
즉, 현재 해당 스레드가 공유 자원에 접근할 수 없는 상태인지 아닌지 알 수 없고, 공유 자원에 접근할 수 없으면 무조건 블록된다.
Lock
일반적인 Mutex 를 구현한 것으로, Lock 을 보유한 스레드만 해당 자원에 접근할 수 있다.
ReentrantLock
synchronized 와 똑같이, ReentrantLock.lock() 를 호출한 이후 코드는 lock 을 가진 스레드만 실행할 수 있다. 락을 해제하기 위해 ReentrantLock.unlock() 를 호출한다.
class Test {
private final ReentrantLock lock = new ReentrantLock();
int number;
public void increase() {
lock.lock();
log.info(Thread.currentThread().getName() + " lock 보유 " + number);
number++;
log.info(Thread.currentThread().getName() + " lock 반환 " + number);
lock.unlock();
}
}
Java
복사
boolean trylock() 메서드를 이용하면 lock 보유 실패 시 스레드가 블로킹되지 않고 다른 작업을 수행하도록 할 수 있다.
class Test {
private final ReentrantLock lock = new ReentrantLock();
int number;
public void increase() {
if (!lock.tryLock()) {
log.info(Thread.currentThread().getName() + " lock 보유 실패 " + number);
return;
}
log.info(Thread.currentThread().getName() + " lock 보유 " + number);
number++;
log.info(Thread.currentThread().getName() + " lock 반환 " + number);
if (lock.isLocked()) {
lock.unlock();
}
}
}
Java
복사
ReentrantReadWriteLock
읽기 작업과 쓰기 작업에 별개로 Lock을 걸어 동시에 여러 스레드에서 읽는 것은 허용하지만, 쓰는 것은 허용하지 않을 수 있다.
Semaphore
여러개의 스레드가 임계 영역에 접근하는 것을 허용할 수 있다.