한 thread가 객체를 변경하는 중이라, 상태가 일관되지 않은 순간의 객체를 다른 thread가 보지 못하게 막는다.
일관된 상태를 갖는 객체에 접근하는 메서드가 해당 객체에lock을 걸어, 객체의 상태를 확인하고 수정한다.
synchronized로 설정된 메서드 혹은 코드 블럭에 lock이 생긴다.
그 사이에 일관성이 깨지는 순간을 다른 어떤 메서드도 확인할 수 없다.
Thread간 통신(Communication)
동기화된 메서드나 블록에 들어간 Thread가 같은 lock의 보호 하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다.
말을 뭐 이렇게 어렵게 해놨나..쉽게 말해서 같은 임계 영역 바라보고 있는 애들끼리는 일관된 값을 보게 한다는 말이다. (아래 Thread 동기화 중단 To-be 코드를 보면 이해가 간다.)
✒️ Synchronization을 배타적 실행뿐 아니라 Thread 사이 안정적 통신에 꼭 필요하다
Java 명세상 long, double 외의 변수를 읽고 쓰는 동작은 원자적(Atomic)이다. 즉, 여러 Thread가 같은 변수를 동기화 없이 수정하는 중이라도, 항상 어떤 Thread가 정상적으로 저장한 값을 온전히 읽어옴을 보장한다는 것이다.
언뜻 보면, 이 정도만으로도 Thread 안정성에 문제가 없어보인다. 그래서 "성능을 높이기 위해 원자적 데이터를 읽고 쓸 때는 synchronization하지 말아야겠다"고 생각할 수 있는데, 굉장히 위험한 발상이다. 왜냐하면, Java는 한 Thread가 저장한 값이 다른 Thread에게 '보이는가'는 보장하질 않는다.
https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html
이는 Java의 Memory model의 특성이다. Thread가 변수를 읽어올 때 CPU Cache에서 읽어오게 되는데, 실제 변수 값이 변화하더라도 해당 Thread는 그 전의 값이 저장된 CPU cache에서 값을 읽어온다. 즉, "동시성 제어"는 해주지만 "가시성"에 대한 부분을 책임져주지 않는다. (메모리 가시성이라 이해하면 편하다.)
✒️ 원자적(Atomic)이란?
원자성은 연산의 원자성이라고 이해하면 쉽다. 한 줄의 프로그램 statement(문장)가 compiler에 의해 기계어로 변경되면서, 이를 기계가 순차적으로 처리하기 위한 machine instruction이 만들어져 실행되기 때문에 발생한다. 여기서 원자단위 연산이란, 다른 Thread의 CPU 개입이 있을 수 없는 최소 단위 연산이다.
✒️ long, double이 원자적(Atomic)이지 않은 이유
JVM은 32-bit memory에 값을 할당하는 연산은 원자적이다. 여기서 64-bit 연산을 수행하려면 값을 분할해야 하기 때문에 원자성이 깨진다고 한다. 만약, JVM 64-bit machine을 사용하면 long, double도 Atomic한 연산을 수행한다.
📌 동기화 : Thread 중단
❌ As-is. Thread.stop
Java11까지 와서 겨우 없어지긴 했는데, 이전 버전에서도 deprecated 됐고 쓰지 마라
해당 메서드를 사용하면 데이터가 훼손될 수 있다.
Thread.stop()을 호출하면, 해당 Thread를 바라보던 다른 Thread들에 ThreadDeathException가 전파된다.
반복을 할 때마다 동기화하는 비용(비싸진 않지만)이 아까울 때, 더 빠르게 만들 수 있다.
Java memory machine이 보장하지 않던 Visibility를 보장하게 된다.
✒️ volatile 원리
volatile로 선언된 변수는 CPU에서 연산을 하면 바로 Memory에 쓴다. (cache에서 memory로 값이 이동하는 걸 보통 flush라고 말한다.)
책에서 volatile을 쓰면 조금 더 빨라진다고 한 이유는 synchronized의 lock & unlock 과정의 context switching으로 인한 overhead를 줄여주기 때문이다. 다만, 이 volatile 또한 사용의 제약 조건이 있는데 "하나의 Thread만이 연산(modify)을 수행 해야한다."