공유 중인 가변 데이터는 동기화하자 & 과도한 동기는 피하는게 좋다.

공유 중인 가변 데이터는 동기화하자

synchronized는 해당 메소드, 블록을 한 번에 한 쓰레드씩 수행하도록 보장한다. 즉, 객체를 하나의 일관된 상태에서 다른 일관된 상태로 변경시킨다. 동기화를 제대로 사용하면 어떤 메소드도 이 객체의 상태가 일관되지 않은 순간을 볼 수 없다.

동기화 이외에 다른 쓰레드가 만든 변화를 체크하기 위한 용도로도 사용할 수 있다. 동기화는 일관성이 꺠진 상태를 볼 수 없게 하는 것은 물론, 동기화된 메소드나 블록에 들어간 쓰레드가 같은 락의 보호 하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다.

자바 명세는 쓰레드가 필드를 읽을 때 ‘수정이 완전히 반영된’ 값을 보장한다고 하지만, 쓰레드 간에도 보장하는가 하면 아니다. 따라서 동기화는 배타적 실행뿐만 아니라 쓰레드 사이의 안정적인 통신에 꼭 필요하다. 이는 쓰레드가 만든 변화가 다른 쓰레드에 언제 어떻게 보이는지를 규정한 자바의 메모리 모델 때문이다.

while( !stopRequested ) i ++;

위와 같은 코드가 있다고 하자 쓰레드 동기화를 사용하지 않으면, jvm이 코드 최적화를 할 때

if(!stopRequested) {
    while (true) i ++;
}

로 바뀐다. 이는 hoisting 때문이다. 이런 잘못된 최적화로 synchronized를 사용하지 않으면 원하는대로 코드가 작동하지 않을 수 있다. 혹은 위 stopRequested 변수를 get, set으로 바꾸고 둘 다 synchronized를 붙이는 방법도 있다.


private static synchronized void requestStop() { stopRequested = true; }
private static synchronized boolean requestedStop() { return this.stopRequested; }

if(!stopRequested()) {
    while (true) i ++;
}

읽기 쓰기 모두 동기화해야 한다. 변경도, 읽기도 모두 동기화 해야 상태를 보장할 수 있다. 혹은 volatile을 사용하는 방법도 있다. volatile은 해당 변수를 메인 메모리에 저장하도록 하는 예약어다. 물론 붙인다고 능사가 아니다. set 연산을 동기화해야 한다. 왜냐하면 쓰레드가 수정하고 반영하기 전 다른 쓰레드가 비집고 들어와 첫 번쨰와 같은 값을 반환할 수 있기 떄문이다. 이런 경우를 안전 실패(Safety failure)라고 한다.

다른 방법으로 java.util.concurrent.atomic을 사용해서 원자성을 지킬 수 있다. 이 패키지에는 락 없이도 thread-safe한 클래스들이 담겨 있다.

과도한 동기는 피하는게 좋다.

과도한 동기화는 당연히 성능 저하를 일으키고 심지어 교착상태에 빠뜨리기도 하고 결국 예측할 수 없는 동작을 낳기도 한다. 응답 불가, 안전 실패를 막으려면 동기화 메소드나 동기화 블록 안에서는 제어를 절대로 클라이언트에 넘기면 안된다. 즉 재정의 메소드를 넘겨도 안되고, 함수 객체도 실행하면 안된다. 이런 alien 메소드는 예외를 일으키거나, 교착상태에 빠지게 하거나 데이터를 훼손할 수도 있다.

차라리 동기화 영역 바깥에서 alien 메소드를 호출(open call)하는게 좋다. 외부 메소드가 얼마나 오래 실행할지 모르기에 이는 현명한 선택이다. 즉, 실패 방지, 동시성 효율 제고를 해준다. 물론 동기화를 최소한으로 하는게 최고다. 동기화는 락을 거는게 문제가 아니라 Thread 간 경쟁으로 낭비(병렬성을 잃고 모든 코어가 메모리 일관성을 위한 지연 시간) 때문에 문제가 된다. 또한 JVM 최적화를 제한하기도 한다는 점이 큰 문제다. 그래서 차라리

  1. 동기화를 외부에서 하게 하거나
  2. 동기화를 아예 숨겨서 내부에서 하기

라는 방법이 있다. 2번은 차선책이다. 더 심화하면 lock-splitting, lock striping, nonblocking concurrency control 등 다양한 기법을 동원해서 동시성을 높여줄 수 있다.

  1. lock-splitting : 하나의 클래스에서 기능적으로 락을 분할( 읽기/ 쓰기락 )
  2. lock striping : 자료 구조 관점에서 일부에만 락을 거는 것
  3. nonblocking concurrency control :