티스토리 뷰
Thread Safe 개념
Thread Safe란 멀티 스레드 프로그래밍에서
함수, 변수, 객체 등이 여러 스레드로부터 동시에 접근이 이루어져도
프로그램의 실행에 문제가 없다는 것을 뜻합니다.
하나의 함수가 한 Thread로부터 호출되어 실행 중일 때
다른 Thread가 그 함수를 호출하여 동시에 함께 실행되더라도
각 스레드에서의 함수의 수행 결과가 의도한대로 나와야합니다.
제일 좋은 방법은 자원을 공유하지 않는 코드를 작성하는 것이 좋습니다.
Thread Safe하지 않은 코드 작성하기
수열을 생성하는 SequenceGenerator를 구현합니다.
이 코드는 currentValue를 Update하는데, Thread가 동시에 접근할 경우, 의도하지 않은 결과가 나올 수 있습니다.
예를 들어, currentValue가 29일 때, 동시에 Thread가 Update하는 함수에 접근한다면 의도한 결과는 31이지만,
Thread가 동시에 접근했기 때문에 Thread 모두가 29를 30으로 Update합니다.
이를 동시에 접근했을 때 의도하지 않은 결과가 나오므로 Thread Safe하지 않은 코드라고 말할 수 있습니다.
public class SequenceGenerator {
private int currentValue = 0;
private AtomicInteger atomicInteger = new AtomicInteger(0);
public int getNextSequence() {
// 이부분은 2개의 연산으로 이루어집니다.
// currentValue + 1 연산
// currentValue + 1 결과를 대입하는 연산
currentValue = currentValue + 1;
return currentValue;
}
}
여러 Thread가 동시에 접근할 경우를 재현하기 위해서 getUniqueSequences()를 만듭니다.
private Set<Integer> getUniqueSequences(SequenceGenerator generator, int count) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(3);
// Set을 활용하여 중복된 수열을 제거
Set<Integer> uniqueSequences = new LinkedHashSet<>();
List<Future<Integer>> futures = new ArrayList<>();
for (int i = 0; i < count; i++) {
futures.add(executor.submit(generator::getNextSequence));
}
for (Future<Integer> future : futures) {
uniqueSequences.add(future.get());
}
executor.awaitTermination(1, TimeUnit.SECONDS);
executor.shutdown();
return uniqueSequences;
}
SequenceGenerator가 Thread Safe 하지 않다는 테스트 코드를 작성합니다.
Thread Safe하지 않은 코드이기 때문에 의도한 결과가 일정하게 나오지 않습니다.
간혹 의도한 결과가 나와서 테스트가 통과하지 않는 경우도 발생합니다.
@Test
public void givenUnsafeSequenceGenerator_whenRaceCondition_thenUnexpectedBehavior() throws Exception {
Set<Integer> uniqueSequences = getUniqueSequences(new SequenceGenerator(), COUNT);
assertNotEquals(COUNT, uniqueSequences.size());
}
Thread Safe한 코드 작성하기
Synchronized 함수 사용
Synchronized 함수를 사용하면 클래스로 만들어진 하나의 인스턴스를 기준으로 동기화가 이루어진다.
public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator {
@Override
public synchronized int getNextSequence() {
return super.getNextSequence();
}
}
Synchronized 블럭 사용
Synchronized 블럭을 사용하면 전달받은 객체를 기준으로 동기화가 이루어진다.
여기서는 mutex라는 Object를 기준으로 블럭 안의 코드를 한 쓰레드만이 실행할 수 있다.
public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator {
private Object mutex = new Object();
@Override
public int getNextSequence() {
synchronized (mutex) {
return super.getNextSequence();
}
}
}
ReentrantLock 사용
ReentrantLock을 사용하면 시작점과 끝점을 명백히 명시할 수 있습니다.
Synchronized는 암묵적이고 ReentrantLock는 명시적이라는 차이가 있습니다.
public class SequenceGeneratorUsingReentrantLock extends SequenceGenerator {
private ReentrantLock mutex = new ReentrantLock();
@Override
public int getNextSequence() {
try {
mutex.lock(); // 동기화 시작
return super.getNextSequence(); // 동기화 대상
} finally {
mutex.unlock(); // 동기화 끝
}
}
}
세마포어 사용
하나의 스레드만 임계구역에 들어가면 성능 이슈가 발생하는데, 세마포어는 임계구역에 여러 스레드가 들어갈 수 있는 장점이 있습니다.
공유자원이 2개 이상일 때 잘못 사용하면 서로 자원을 점유하기 위해서 대기상태에 빠지므로 DeadLock이 발생할 수 있습니다.
public class SequenceGeneratorUsingSemaphore extends SequenceGenerator {
private Semaphore mutex = new Semaphore(1);
@Override
public int getNextSequence() {
try {
mutex.acquire();
return super.getNextSequence();
} catch (InterruptedException e) {
throw new RuntimeException("Exception in critical section.", e);
} finally {
mutex.release();
}
}
}
모니터 사용
모니터는 2개의 Queue가 존재한다.
하나의 Queue는 하나의 Thread만 공유자원에 접근할 수 있게하는 역할을 한다.(상호배타)
다른 Queue는 임계구역에 진입한 wait()을 통해 Thread가 블락되면 새로운 Thread가 진입할 수 있도록 알려주는 역할을 한다.(조건동기)
그리고 새로 진입한 Thread가 notify()를 통해 블락된 Thread를 재진입할 수 있도록 하는 역할도 한다.(조건동기)
public class SequenceGeneratorUsingMonitor extends SequenceGenerator {
private Monitor mutex = new Monitor();
@Override
public int getNextSequence() {
mutex.enter();
try {
return super.getNextSequence();
} finally {
mutex.leave();
}
}
}
https://www.baeldung.com/java-mutex
https://sycho-lego.tistory.com/11
https://copycode.tistory.com/83