본문 바로가기
java & kotlin

thread safe vs thread unsafe

by algosketch 2021. 3. 30.

 SW마에스트로 면접 준비하면서 찾아본 내용으로 java에서 ArrayList 와 Vector 의 차이가 있다. 나는 java에서 ArrayList만 써봤고 vector는 C++ 컨테이너로 사용해봤다. java의 vector는 안 써봤지만 C++에서의 개념을 생각해 보면 ArrayList와 Vector의 사용 방법은 동일할 것 같았다. 그럼 무슨 차이가 있는걸까?

 결론부터 말하자면 Vector는 thread safe 하고 ArrayList는 thread unsafe 하다. 속도는 당연히 ArrayList가 빠르기 때문에 스레드 프로그래밍을 하지 않는다면 ArrayList를 사용하는 것이 바람직해 보인다. 근데 스레드라는 게 명시적으로 thread를 만들 수도 있지만 이벤트나 네트워크, 데이터베이스 등을 쓰게 되면 내가 모르는 사이에 서브 스레드가 생길 수도 있다. 그러므로 이 점 유의해서 프로그래밍 해야할 것 같다. 

 atomic 변수나 sychronized 선언된 메서드는 thread safe하다. 물론 속도는 떨어진다. 계속해서 thread safe 와 같은 단어를 사용하는데 thread safe는 뭐고 thread unsafe는 뭘까? 일단 간단히 말하자면 thread unsafe는 다중 스레드로 작업하면 원하지 않는 결과가 나올 수도 있다는 뜻이다. 어떤 상황에서 발생하는지 확인해 보자.

 멀티 스레드에서는 멀티 프로세스와는 다르게 메모리 영역을 공유한다. 즉, View의 calledCount라는 필드는 어느 스레드에서도 같은 변수이다. 위 코드는 각각의 스레드에서 onClick() 메서드를 각각 10만번 호출하며 예상되는 결과값은 20만이다. 하지만 실제 출력 값은 185697이 나왔다. 이 값은 고정된 값이 아니라 실행할 때마다 바뀐다.
 컴퓨터는 (CPU 코어가 1개라고 가정)여러 개의 스레드를 동시에 실행하는 것이 아니라 동시에 실행하는 것처럼 보이게 만든다. 실제로는 여러 개의 스레드를 번갈아 가면서 실행하게 된다. 이 과정을 컨텍스트 스위칭이라고 하고 운영체제가 담당한다. 문제는 컨텍스트 스위칭이 발생하는 시점을 예측할 수 없고 ++calledCount도 명령어 한 줄이 아니라는 점이다.
 ++calledCount 는 메모리에서 값을 가져오고, 그 값을 증가시키고, 메모리에 저장한다. 이 세 가지 과정을 거치게 되는데 만약 메모리에서 값을 가져온 상태에서 컨텍스트 스위칭이 일어나면 문제가 생긴다. 초기 calledCount의 값이 0이라고 가정해 보자. 1번 스레드에서 0을 가져온 상태에서 컨텍스트 스위칭이 일어난 후 2번 스레드에서 ++calledCount 를 모두 실행하면 calledCount가 1이 된다. 그리고 다시 1번 스레드로 가서 값을 증가(처음에 0을 가져왔으므로 0 -> 1) 시키고 calledCount에 값을 저장하면 calledCount는 1이 된다. 실제로 onClick() 메서드는 두 번 호출되었지만 calledCount 결과는 1이다.

 다음과 같이 해결하면 된다!

 Synchronized 어노테이션을 사용하면 해당 메서드를 실행하는 동안 컨텍스트 스위칭이 일어나지 않게 된다. java는 아마 접근 제한자 옆에 적어주면 될 것이다. 지금은 큰 문제점이 안 보이지만 메서드 내에서 여러 가지 동작을 하게 되는 경우 그 동안 다른 동작을 못 하게 되므로 성능에 문제가 생긴다.

 아토믹 변수를 이용하여 해결할 수도 있다. 아토믹 변수를 수정하는 동안은 thread safe 하다. 하지만 한 메서드 안에서 thread safe 한 연산을 여러 번 하게되면 그 메서드는 thread safe 하지 않을 수도 있다.

public class Singleton {
    Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

 싱글톤 패턴을 사용할 경우 instance는 한 번만 생성 되어야 한다. instance가 초기화되지 않은 상태에서 getInstance() 를 여러 스레드에서 동시에 호출되면 instance가 여러 번 생성되는 경우도 생각해볼 수 있다.