synchronized 키워드는 해당 메소드나 블록을 한 번에 하나의 스레드만 실행할 수 있도록 보장한다.
동기화된 메소드를 사용하면, 한 스레드가 객체의 상태를 변경하는 도중에 다른 스레드가 해당 객체의 상태를 읽거나 변경하는 것을 방지할 수 있다.
즉, 동기화를 통해 객체의 상태가 항상 일관된 상태를 유지하도록 보장한다.
2. 스레드간 통신
메모리 가시성 : 한 스레드에서 변경한 데이터의 값을 다른 스레드에서 정확히 볼 수 있는 특성
자바에서는 스레드가 변수를 캐시에 저장하고 사용할 수 있기 때문에, 동기화를 하지 않으면 한 스레드에서 변경한 값이 다른 스레드에게 즉시 보이지 않을 수 있다.
그러나 동기화를 사용하면 한 스레드가 변경한 값이 메인 메모리에 즉시 반영되고, 이 변경사항은 동기화된 다른 스레드에게도 보장된다.
예시
자바는 long과 double을 제외한 모든 변수의 읽기와 쓰기는 원자적이다.(배타적 실행 보장)
원자성이 변수의 읽기와 쓰기 연산의 안전성을 보장하더라도, 다중 스레드 환경에서 한 스레드가 변수에 저장한 값을 다른 스레드가 언제 보게 될지는 보장되지 않는다.
동기화를 사용하면 메모리에 쓰여진 모든 변경 사항이 다른 스레드에게도 보이게 된다.
따라서 변수에 대한 접근이 원자적이라고 동기화의 필요성이 사라지는 것은 아니다.
원자성은 일부 연산의 안정성만 보장하는 반면, 동기화를 메모리 가시성과 배타적 실행 모두를 보장한다.
기본형 타입이 원자적인 이유
i) 4바이트 이하의 기본형 타입 : 하나의 명령어로 처리되기 때문에, 한 스레드로만 처리된다.
ii) 8바이트의 기본형 타입 : 여러 스레드가 개입될 여지가 생겨 원자적이라 할 수 없다.
동기화는 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다.
🍑 본론
동기화에서 발생할 수 있는 문제와 해결
Thread.stop 메소드는 사용하지 말자.
문제점
Thread.stop 작업은 스레드를 강제로 멈추게 한다.
스레드가 실행 중인 작업을 안전하게 완료하지 않고 중단될 수 있기 때문에, 데이터의 무결성을 손상시킬 위험이 있다.
따라서 사용 자제 API로 지정되었다.
해결방법(flag polling)
첫 번째 스레드는 자신의 boolean 필드를 주기적으로 확인(polling)하면서 그 값이 true가 되면 멈춘다.
다른 스레드가 첫 번째 스레드를 멈추고자 할 때, boolean 필드의 값을 true로 변경하여 첫 번째 스레드에게 멈추라고 신호를 보낸다.
코드 예시
아래 코드는 1초 후 종료되지 않는다.
public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested)
i++;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
메모리 가시성
stopRequested를 true로 변경했을 때, 그 변경사항이 백그라운드 스레드에게 즉시 보이지 않을 수 있다.
JVM의 최적화 : hoisting 최적화 기법
JVM은 코드를 실행할 때 성능 최적화를 위해 여러 변환을 수행한다.
그 중 하나로, 빈번한 체크하는 조건을 최적화하여 반복문을 더 효율적으로 만들려고 할 수 있다.
while(!stopRequested) 구문에서 stopRequested가 변하지 않는다고 판단된다면, 이를 반복문 밖으로 빼내어 성능을 최적화하려고 할 수 있다.
// while (!stopRequested)
// i++;
// 최적화된 코드
if (!stopRequested)
while(true)
i++;
이로 인해 응답 불가(liveness failure) 상태가 되게 된다.
동기화를 통해 해결
public class StopThread {
private static boolean stopRequested;
private static synchronized void requestStop() { // true로 설정하는 쓰기 연산을 수행
stopRequested = true;
}
private static synchronized boolean stopRequested() { // flag의 현재 값을 반환하는 읽기 연산을 수행
return stopRequested;
}
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested())
i++;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
위와 같이 쓰기와 읽기 메서드 모두 동기화해야 한다.
두 메서드는 단순해서 동기화 없이도 원자적으로 동작하기 때문에 통신 목적으로만 사용되었다.
volatile 사용
속도가 더 빠른 대안 volatile
로컬 캐시가 아닌 메인 메모리에서 직접 읽고 쓴다.
배타적 수행과는 상관없지만 항상 가장 최근 기록된 값을 읽게 됨을 보장한다.
public class StopThread {
// volatile로 선언하면 동기화를 생략해도 된다.
private static volatile boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested)
i++;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
주의점
private static volatile int nextSerialNumber = 0;
public static int getNextSerialNumber() {
return nextSerialNumber++;
}
매번 고유한 값을 반환할 의도로 만들었고 가능해 보이지만 동기화 없이 바르게 동작하지 않는다.
증가 연산자(++)가 원인이다.
실제로 nextSerialNumber 필드에 두 번 접근한다.(값 읽기, 1 증가)
만약 두 번째 스레드가 두 번 접근하는 사이 값을 읽어가면 첫 번째 스레드와 같은 값을 돌려 받는다.
이런 오류를 안전 실패라고 한다.
해결법
synchronized
동시에 호출해도 서로 간섭하지 않으며 이전 호출이 변경한 값을 읽게 된다.
synchronized를 붙이고 volatile을 제거한다.
더 견고하게 하려면 int 대신 long을 사용하거나 nextSerialNumber가 최댓값에 도달하면 예외를 던지게 한다.
AtomicLong
java.util.concurrent.atomic
이 패키지에는 락 없이도 스레드 안전한 프로그래밍을 지원하는 클래스들이 있다.
volatile은 동기화의 두 효과 중 통신 쪽만 지원하지만 이 패키지는 배타적 실행까지 지원한다.
private static final AtomicLong nextSerialNumber = new AtomicLong();
public static long getNextSerialNumber() {
return nextSerialNumber.getAndIncrement();
}
🍑 결론
가장 좋은 방법은 가변 데이터를 공유하지 않는 것이다.
불변 데이터만 공유하거나 아무것도 공유하지 말자. 가변 데이터는 단일 스레드에서만 쓰도록 하자
한 스레드가 데이터를 다 수정한 후 다른 스레드에 공유할 때는 해당 객체에서 공유하는 부분만 동기화해도 된다. 그러면 그 객체를 다시 수정할 일이 생기기 전까지 다른 스레드들은 동기화 없이 자유롭게 값을 읽어나갈 수 있다.
이런 객체를 effectively immutable(사실상 불변) 이라 하고 다른 스레드에 이런 객체를 건네는 행위를 safe publication(안전 발행)이라고 한다.
객체를 안전하게 발행하는 방법은 클래스 초기화 과정에서 객체를 정적, volatile, final, 혹은 보통의 락을 통해 접근하는 필드에 저장해도 된다. 동시성 컬렉션(아이템 81)에 저장하는 방법도 있다.
Chapter : 11. 동시성
Item : 78. 공유 중인 가변 데이터는 동기화해 사용하라
Assignee : heon118
🍑 서론
동기화의 용도
1. 배타적 실행
2. 스레드간 통신
예시
동기화는 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신에 꼭 필요하다.
🍑 본론
동기화에서 발생할 수 있는 문제와 해결
Thread.stop 메소드는 사용하지 말자.
문제점
해결방법(flag polling)
코드 예시
아래 코드는 1초 후 종료되지 않는다.
동기화를 통해 해결
volatile 사용
속도가 더 빠른 대안 volatile
로컬 캐시가 아닌 메인 메모리에서 직접 읽고 쓴다.
배타적 수행과는 상관없지만 항상 가장 최근 기록된 값을 읽게 됨을 보장한다.
주의점
매번 고유한 값을 반환할 의도로 만들었고 가능해 보이지만 동기화 없이 바르게 동작하지 않는다.
증가 연산자(++)가 원인이다.
해결법
synchronized
AtomicLong
🍑 결론
가장 좋은 방법은 가변 데이터를 공유하지 않는 것이다.
불변 데이터만 공유하거나 아무것도 공유하지 말자.
가변 데이터는 단일 스레드에서만 쓰도록 하자
한 스레드가 데이터를 다 수정한 후 다른 스레드에 공유할 때는 해당 객체에서 공유하는 부분만 동기화해도 된다. 그러면 그 객체를 다시 수정할 일이 생기기 전까지 다른 스레드들은 동기화 없이 자유롭게 값을 읽어나갈 수 있다.
이런 객체를 effectively immutable(사실상 불변) 이라 하고 다른 스레드에 이런 객체를 건네는 행위를 safe publication(안전 발행)이라고 한다.
객체를 안전하게 발행하는 방법은 클래스 초기화 과정에서 객체를 정적, volatile, final, 혹은 보통의 락을 통해 접근하는 필드에 저장해도 된다. 동시성 컬렉션(아이템 81)에 저장하는 방법도 있다.
Referenced by