지난 시간에 자바로 멀티쓰레드를 구현하는 방법에 대해서 알아보았다.
이번엔 동시성 이슈를 처리하는 방법을 알아보겠다.
- 동시에 실행되는 것처럼 보이는 것
- 싱글 코어에서 멀티 쓰레드를 동작시키기 위한 방식으로, 멀티 태스킹을 위해
여러 개의 쓰레드가 번갈아 가면서 실행되는 성질을 말한다.
멀티쓰레드 방식은 멀티 태스킹을 하는 방식 중, 한 코어에서 여러 쓰레드를 이용해
번갈아작업을 처리하는 방식이다.
멀티 쓰레드를 이용하면 공유하는 영역이 많아 프로세스 방식보다context switching오버헤드가 작아, 메모리 리소스가 상대적으로 적다는 장점이 있다.
하지만, 쓰레드는 같은 자원을 공유하여 사용할 수 있기 때문에, 이 과정에서동시성 이슈가 발생한다.
하나의 자원을 두고경쟁상태같은 문제가 발생한다.
다음 코드를 확인하자
public class Count{
private int count = 0;
public void increase() {
count++;
}
public int getCount() {
return count;
}
}
간단해 보이는 이 코드는 thread-safe하지 않다.
쓰레드 A와 쓰레드 B가 동시에 increase()메서드를 호출하여 count를 증가시키면, A의 increase()로 count가 1이 되고, B의 increase()로 count가 2가 되어야 할 것이다.
하지만, A와 B가 동시에 increase() 메서드를 호출하면, count는 1이 되는 문제가 발생한다.
여러 쓰레드가 작동하는 환경에서도 문제 없이 동작하는 것을
Thread safe하다고 말한다.
즉, 동시성 이슈를 해결하고 일어나지 않는다면Thread safe하다고 한다.
- 기본적으로 Lock을 적용하게 되면 하나의 쓰레드가 해당 메서드를 실행하고 있을 때 다른 메서드가 해당 메서드는 실행하지 못하고 대기하게 된다.
- lock은 메서드, 변수에 각각 걸 수 있고, 해당 메서드, 변수에 진입하는 쓰레드는 단 하나만 가능하다.
- 변수에 lock을 걸기 위해선 해당 변수는 객체여야한다. int, long과 같은 기본형 타입에는 lock을 걸 수 없다.
- 단점 : 한 번에 하나의 쓰레드만 메서드를 실행시킬 수 있으므로 병렬성은 매우 낮아진다.
동시성을 해결하는 데 가장 간단하면서 쉬운 방법은 Lock을 거는 것이다.
문제의 메서드, 변수에 각각synchronized라는 키워드를 넣는 것이다.
public class Count {
private int count = 0;
public synchronized void increase() { //synchronized 추가
count++;
}
public synchronized int getCount() { //synchronized 추가
return count;
}
}
synchronized 키워드를 사용하는 암시적 Lock과는 달리 Lock 인터페이스를 이용한다. Lock객체의 lock() 메서드를 호출하여 잠그고, unlock() 메서드를 호출하여 잠금을 해제한다.
public class Count {
private int count = 0;
private Lock lock = new ReentrantLock();
public void increase() {
lock.lock(); //잠금
try {
count++;
} finally {
lock.unlock(); //잠금 해제
}
}
public int getCount() {
lock.lock(); //잠금
try{
return count;
}finally{
lock.unlock(); //잠금 해제
}
}
}
이러면 무조건 synchronized 쓰면 되잖아?라고 생각했는데, Lock의 범위를 메서드 내부에서 한정하기 어렵거나, 동시에 여러 Lock을 사용하고 싶을 때 사용한다고 한다.
volatile 키워드를 사용하면 변수가 항상 메인 메모리에서 읽고 쓰이도록 보장된다.
public class Count {
private volatile int count; //volatile 추가
public void increase() {
count++;
}
public int getCount() {
return count;
}
}
캐시에 저장되지 않고 메인 메모리에 항상 저장이 되기 때문에, 캐시 사용으로 인한 데이터 불일치를 막을 수 있다.
대표적으로 ConcurrentHashMap 클래스를 사용한다.
public class Count {
private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
public void increase() {
Integer currentValue = map.get("count");
if (currentValue == null) {//"count"라는 키가 Map에 존재하지 않으면
map.put("count", 1);
} else { //존재하면
map.put("count", currentValue + 1);
}
}
public int getCount() {
return map.get("count");
}
}
불변 객체는 상태가 변경될 수 없는 객체이기 때문에 Thread-safe하다.
public final class Count {
private final int count;
public Count(int count) {
this.count = count;
}
public Count increase(){
return new Count(count + 1); //새로운 Count 객체 생성
}
public int getCount(){
return count;
}
}
하지만, 불변 객체를 사용하면, 객체를 생성하는 비용이 굉장히 크기 때문에, 성능상 문제가 발생할 수 있다. 그렇기 때문에, 불변 객체를 재사용하는 방법을 이용한다.