싱글 스레드 기반의 프로그램에서는 하나의 스레드가 하나의 객체를 사용하면 되기 때문에 동기화에 대한 걱정을 하지 않아도 되지만, 멀티 스레드 기반의 환경에서는 여러개의 스레드가 하나의 객체를 공유해서 사용하는 경우가 있다. 하나의 객체를 공유하며 사용하는 경우 불변 객체에 대해서는 동기화를 걱정할 필요가 없지만, 스레드가 메서드를 실행하면서 변수의 데이터를 변경하는 경우 다른 스레드에서 동기화되지 않은 데이터를 읽을 수 있다. 이런 경우에는 프로그래머가 기대한 결과와는 다른 결과를 초래할 수 있기 때문에 주의해야 한다.
동기화란?
동기화(Syncronized)란 멀티스레드 환경에서 하나의 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장하는 것을 의미한다.
동기화의 특징
한 객체가 일관된 상태를 가지고 생성되었을 때, 이 객체에 접근하는 메서드는 그 객체에 Lock을 건다. (다른 스레드가 메서드를 실행할 때 실행되지 못하도록 Lock을 건다)
Lock을 건 메서드는 객체의 상태를 확인하거나 필요하면 수정한다.
즉 일관된 하나의 상태 -> 다른 일관된 하나의 상태로 변화한다.
메서드 실행이 끝나면 Lock을 해제한다.
동기화를 제대로 사용하면 어떤 메서드도 이 객체의 상태가 일관되지 않은 순간을 볼 수 없다.
동기화 없이는 한 스레드가 만든 변화를 다른 스레드에서 확인하지 못할 수 있다. (동기화가 없다면, 너도나도 접근하는데 시점에 따라 일관된 상태가 아닐 수도 있기 때문)
언어 명세상 long과 double 외의 변수를 읽고 쓰는 동작은 원자적(atomic)이다. (여러 스레드가 하나의 변수에 동기화 없이 접근해도 항상 어떤 스레드가 정상적으로 저장한 값을 온전히 읽어옴을 보장)
성능을 높이려면 원자적 데이터를 읽고 쓸 때는 동기화 하지 말아야겠다 (위험한 발상) (필드를 읽을 때 항상 수정이 완전히 반영된 값 을 얻지만, 한 스레드가 저장한 값이 다른 스레드에도 보이는가? 는 보장하지 않음)
이 메서드는 호출 될 때 마다 1씩 증가하여 스레드에서 고유한 값을 반환할 의도로 만들어 졌다.
겉보기에는 int이기 때문에 원자적으로 접근할 수 있을 것 같다
Volatile 키워드가 쓰여져있기 때문에 최신 값을 읽을 수 있을 것 같지만 제대로 된 고유한 값이 나오지 않는다.
원인은 nextSerialNumber++
원인은 nextSerialNumber++에 있었다. 실제 이 코드는 1줄이지만 풀어쓰면 nextSerialNumber = nextSerialNumber + 1; 와 같은 형태이다. 결국 nextSerialNumber 값을 한번 읽어와 +1 한다음에 다시 nextSerialNumber 변수에 저장하는 형태이다. 만약 두번째 스레드가 nextSerialNumber + 1 연산이 이루어지는 시점을 비집고 들어온다면 1이 두번 리턴되는 형국이다. 이런 오류를 안전 실패(safety failure) 이라고 한다.
위와 같이 generateSerialNumber 메서드에 synchronized만 붙여주면 문제는 해결된다. 동시에 호출해도 배타적으로 실행 (한번에 한 스레드만 실행) 되기 때문이다. 만약 위 처럼 generateSerialNumber 메서드에 synchronized를 붙였다면 nextSerialNumber 변수에는 volatile을 제거해야 한다. 만약 메서드를 더 견고하게 하려면 int 대신 long을 사용하는게 더 많은 수를 사용할 수 있다.
long, double을 사용할 때는 더욱 더 주의하자
Java.util.concurrent.atomic 패키지의 AtomicLong, AtomicDouble을 사용하는 것이 좋다.
이 패키지는 락 없이도(lock-free) 스레드 안전한 프로그래밍을 지원하는 클래스들이 담겨 있다.
volatile은 동기화 속성 중 통신에 대해서만 보장
Java.util.concurrent.atomic 패키지는 원자성(배타적 실행) 까지 지원한다.