스레드 사이에서 공유 상태는 어떤식으로 관리 될 수 있을까요 ?
먼저 문제가 있는 코드를 한번 보고, 어떤식으로 해결할 수 있을지 확인해보겠습니다.
해당 코드를 돌려보면?
성공 할 때도 있지만, 실패하는 경우도 있습니다.
while (balance.getBalance() > 0) 잔고가 0 보다 큰 경우에만 동작할 것이라 예상했지만, 우리의 예상을 깨고 마이너스 잔고가 보입니다.(여담이지만 코드만 보고 동시성 문제가 있을것이라 판단하는 것도 꽤 어려운 일이라고 생각합니다. 경우에 따라서는 제대로 동작하는 코드 처럼 보일경우도 있으니까요)
가시성, 원자성 문제를 모두 해결해야, 동시성(MultiThreading)으로 부터 자유로워질 수 있습니다.
해결에 주목적을 가지고 있기 때문에, 간단하게 짚고만 넘어가겠습니다.
가시성은 코어는 작업의 효율을 위해 캐시 공간을 가지고 있는데, 동시성 작업으로 인해 메인 메모리와 값이 일치하지 않는 경우 입니다.
이경우는 volatile 을 활용하면 해결할 수 있습니다. 캐시에 데이터를 쓰지(write)않고 메인 메모리에 바로 쓰기 때문입니다. 하지만 제약 조건이 있습니다. 하나의 스레드에서만 변경(write and modifiy) 작업을 진행하고, 나머지 스레드들에서는 읽기 작업을 진행해야 합니다. (메모리에서 읽어오는 데이터라고 해도 여러 스레드에서 공유 자원의 값을 변경 한다면 스레드 경합(race) 로 인해 데이터를 읽은 스레드에서 변경 하기도 전에 다른 스레드에서 읽어서 해당 값을 변경해버릴 수도 있습니다)
원자성은 여러 스레드에서 어떤 변수 혹은 메서드에 접근 할 때, 하나의 동작만 실행 될 수 있다는 것을 보장한다는 의미 입니다.
위의 코드는 volatile 로는 해결할 수 없는 문제입니다.
여러 스레드에서 balance 에 접근하여 값을 변경하고 있기 때문입니다.
따라서 synchronized, atomic 키워드로 해당 문제를 해결 할 수 있습니다.
첫번째 synchronized 키워드를 사용하면 꽤나 간단하게 이를 해결할 수 있습니다.
공유자원인 balance 를 다루는 withDraw 자체를 동기화(synchronized) 시켜주는 것입니다.
public synchronized void withDraw(int money) throws InterruptedException {
int balance = getBalance();
if (balance >= money) {
Thread.sleep(500); // 돈을 계산하는데 복잡한 작업이 있다고 가정
this.balance -= money;
}
}
동기화 블록으로 둘러쌓인 withDraw 는 메인 스레드가 해당 balance 를 업데이트 하고 다시 읽는 동안 다른 스레드에서는 해당 값을 건드릴 수 없습니다. 락이 걸렸다고 말할 수 있습니다.
물론 은탄환은 없습니다. 락 사용에는 대가가 따릅니다. 스레드는 잠겨있는 동안 아무 작업도 하지 않고 기다리고 있기 때문에 성능에 영향을 미칩니다. 결과적으로 필요한 국소적인 부위에 락을 사용하여 최대한 빨리 락을 해제할 수 있도록 코드를 작성해야 합니다.
이런 경우에 활용해볼 수 있는 것이 Atomic 자료 구조 입니다.
해당 클래스는 모두 원자성을 모장합니다. 즉 다른 스레드는 해당 메서드의 호출이 완료될 때까지 참조할 수 없습니다. 아토믹이 적용되야 할 부분은 balance 이고 우리는 해당 값을 읽었을 때의 시점과 비교하여 일치하면 balance 를 업데이트하고, 아니라면 업데이트 하지 않을 것 입니다.
대략적인 로직의 흐름은 getBalance 메서드를 통해 읽었을 때의 balance 값이 sleep(500) 이 지나고 확인 했을 때와 같은 값이라면 다른 스레드가 접근하지 않았기 때문에 해당 값을 balance - money 로 업데이트하여 출금합니다. 만약에 sleep(500) 이 지나고 비교했을 때 다른 값이라면 balance 를 다른 스레드에서 접근한 것이기 때문에 newValue 로 업데이트 하지 않습니다. compareAndSet 은 반환값이 boolean 이기 때문에 로깅을 이용하여 더욱 정확하게 확인해볼 수도 있습니다.
비로소 우리는 아주 간단하게 동시성을 해결할 수 있는 방법을 맛만 봤습니다.
얻어가야할 키워드가 있다면
volatile, synchronized, atomic 입니다.
해당 부분은 내용이 넓고 어려워서 호기심이 생긴다면 따로 공부하는 것을 추천드립니다.
그럼 20000 :)
'Kotlin & Java' 카테고리의 다른 글
사례로 알아보는 Generic(제네릭) (3) | 2020.03.22 |
---|---|
람다식, 익명 함수, 일급 함수, 클로저 간단하게 알아보자 (0) | 2020.03.03 |
인터페이스의 정적(static) 메서드와 기본(default) 메서드 (0) | 2020.03.02 |
타입 변환과 instanceof 연산자 Tips (0) | 2020.03.02 |
중첩 클래스 와 내부 클래스 (부제 : ViewHolder 를 이너 클래스로 만들때는 왜 static class 로 만들까?) (0) | 2020.03.01 |