멀티 스레드에서 동시성을 제어하기 위한 방법을 알아보도록 하자.
public class SharedObject {
public volatile int counter = 0;
}
단, 여러 개의 스레드가 읽을 수 있지만 write 할 수 있는 스레드는 하나만 존재해야 한다.
volatile 키워드는 각 스레드가 가진 CPU cache가 아닌 공유하는 Main Memory에서 Read / Write하는 방법이다.
A 클래스에서 count 라는 변수가 있고 스레드 2개가 해당 count 변수로 작업을 한다고 가정해보자. volatile 키워드 없이는 각 스레드의 CPU Cache에서 count 를 따로 가지고 있으므로 공유가 불가능하다.
그래서 volatile 키워드로 변수를 선언해서 여러 개의 스레드에서 공유할 수 있도록 한다.
volatile은 write를 하나의 스레드에서만 할 때 유용하고 여러 스레드에서 write를 한다면 부적절하다. 여러개의 변수가 동시에 읽고 그 시점을 기준으로 데이터를 변경할 때 혼란이 생기기 때문이다.
ex) 예시를 보자.
이처럼 하나의 Thread가 아닌 여러 Thread가 write 하는 상황에서는 적합하지 않다. 여러 Thread가 write 하는 상황이라면 synchronized를 통해 변수 read&write의 원자성(atomic)을 보장해야 한다.
synchronized를 사용하면 block에 스레드가 참조할 동안에는 절대로 다른 스레드가 해당 block을 참조하지 못하도록 막는다.
private static long number = 0;
public static synchronized void increase() {
number++;
System.out.println(number);
}
가장 안전한 방법이지만, synchronized를 남발한다면 퍼포먼스 저하가 발생할 수 있다.
atomic variable
(AtomicInteger, AtomicLong, AtomicBoolean 등)을 사용하면 CAS(Compare-And-Swap)
알고리즘을 이용해서 synchronized보다 효율적으로 동시성을 보장한다. 멀티 스레드에서 write도 가능하다.
CAS 알고리즘은 스레드가 가지고 있던 원래 값이 현재의 값과 같은지 비교하고 같으면 그냥 사용하고, 다르면 현재의 값을 받아온다.
그래서 synchronized보다 훨씬 작은 범위에 Lock을 걸수 있게 되고 volatile의 문제를 해결할 수 있다.
private static AtomicLong number = new AtomicLong(0);
public static void increase() {
System.out.println(number.incrementAndGet());
}
뮤텍스는 상호배제의 줄임말로, 공유 자원에 접근하는 코드인 임계 영역에 동시에 접근하는 것을 허용하지 않도록 하는 동기화 기법이다.
일종의 Locking 매커니즘으로 Lock을 소유한 객체만이 접근할 수 있다.
val mutex = Mutex()
var counter = 0
fun main() = runBlocking {
withContext(Dispatchers.Default) {
massiveRun {
mutex.withLock {
counter++
}
}
}
println("Counter = $counter")
}
withLock으로 수정이 가능하다. 여러 스레드 중 하나의 스레드만이 진입하여 수정할 수 있고, 다른 스레드는 Lock을 얻은 스레드의 작업이 완료될 때까지 기다린 다음 counter에 접근할 수 있다.