쓰레드 동기화 (Synchronized)
여러 쓰레드가 공유 리소스를 사용하는 경우, 서로 상호 배제를 하여 문제가 발생하지 않도록 하는 기법입니다.
왜 동기화가 필요할까?
하나의 프로세스에서 A쓰레드와 B쓰레드가 있다고 생각해보자,
여기서 A쓰레드가 c변수를 읽는동안 A쓰레드의 시간이 다 되어 B쓰레드로 CPU의 통제권이 넘어갔습니다.
이때 B쓰레드 또한 C변수를 읽고 변경하게 후 A쓰레드로 다시 통제권을 넘겨준다면, C변수의 상태가 변경되었기 때문에 잘못된 접근을 할 수 있습니다.
따라서 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 막는 것이 동기화입니다.
이를 위해 임계 영역(Critical Section), 뮤텍스(Mutex), 세마포어(Semaphore) 등의 동기화 기법을 사용합니다.
임계 영역과 Lock
공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정하여, 단 하나의 쓰레드만을 이 영역 내의 코드를 수정할 수 있게 Lock을 주는 것
임계 영역 사용방법
[메서드 전체를 임계 영역으로 지정]
public synchronized void test() {
}
해당 메서드가 끝나면 Lock 반환
[특정 부분 임계 여역으로 지정]
public void test2() {
synchronized (this) {
}
}
임계 영역은 프로그램의 성능을 좌우하기 때문에 최소화하고 사용해야 효율적인 프로그램을 할 수 있다.
단점
오랫동안 임계 영역이 끝나지 않는다면 다른 쓰레드에서 해당 객체를 접근할 수 없습니다.
Wait & Notify
특정 쓰레드가 특정 객체에 대한 Lock을 가진 상태로 오래 반환하지 않을 때 다른 쓰레드의 객체를 기다리기 위한 지연을 줄이기 위한 작업, 즉 기아(starvation) 현상이라한다.
Wait 호출
임계 영역의 코드를 수행하다 더 이상 진행할 상황이 아닐때 호출
Notify 호출
중단했던 쓰레드를 다시 락을 얻어 작업을 진행하기 위해 호출
하지만 notify는 모든 쓰레드들 중에서 임의의 쓰레드 하나를 가져온 후 lock을 반환해 주는데, 특정 쓰레드를 못가져오는 경우 반환을 하지 못한다. 따라서 notifyAll을 사용해야 한다.
Volatile
멀티 쓰레드 환경에서 변수의 동기화를 유지하기 위해 사용하는 방법
- 캐시 : 멀티 코어 프로세서에서는 코어마다 별도의 캐시가 존재
Volatile란?
기존에는 메모리에서 읽어온 값을 캐시에 저장 후 캐시에서 값을 읽어서 작업하는데,
값이 변경되어도 캐시에 저장된 값이 변경이 안되어 있으므로 메모리에 저장된 값은 다르기에 잘못된 동작 발생
이때 volatile는 메모리에서 데이터를 읽어오기 때문이 이러한 문제 해결 가능
Volatile의 또 다른기능 원자화
JVM은 데이터를 4byte단위로 처리를 하는데 Long과 Double의 경우 8Bytes이므로 도중에 중단되고 다른 쓰레드로 넘어가면 데이터 읽는 것이 끊길 수 있다.
따라서 여기서 필요한 것이 원자화이다. 원자화를 통해 나눌 수 없고 한 번에 처리하게 한다.
Volatile이외에도 Synchronized또한 원자화를 할 수 있다.