배타적 실행 : 한 스레드가 변경하는 중이라서 상태가 일관되지 않은 순간의 객체를 다른 스레드가 보지 못하게 막는다.
동기화를 제대로 사용하면 어떤 메서도도 이 객체의 상태가 일관되지 않은 순간을 볼 수 없을 것이다.
동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다.
원자적(atomic)
long과 double 외의 변수를 읽고 쓰는 동작은 원자적이다.
여러 스레드가 같은 변수를 동기화 없이 수정하는 중이라도, 항상 어떤 스레드가 정상적으로 저장한 값을 온전히 읽어롬을 보장한다는 뜻.
스레드가 필드를 읽을 때 항상 "수정이 완전히 반영된" 값을 얻는다고 보장하지만, 한 스레드가 저장한 값이 다른 스레드에게 "보이는가"는 보장하지 않는다.
메모리 모델
한 스레드가 만든 변화가 다른 스레드에게 언제 어떻게 보이는지를 규정한 자바의 메모리 모델
잘못된 코드 - 이 프로그램은 얼마나 오래 실행될까?
첫 번째 스레드 : boolean 필드를 폴링하면서 그 값이 true가 되면 멈춘다.
다른 스레드에서 이 스레드를 멈추고자 할 때 true로 변경.
import java.util.concurrent.TimeUnit;
public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while(!stopRequested) {
System.out.println("StopThread.main: " + i);
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
- 동기화하지 않으면 메인 스레드가 수정한 값을 백그라운드 스레드가 언제쯤에나 보게 될지 보증할 수 없다.
- 동기화가 빠지면 가상머신이 다음과 같은 최적화를 수행할 수도 있는 것이다.
~~~java
if(!stopRequested) {
while (true) {
i++;
}
}
적절히 동기화해 스레드가 정상 종료한다.
쓰기 메서드와 읽기 메서드 모두가 동기화 되지 않으면 동작을 보장하지 않는다.
두 쓰레드간 통신 목적으로만 사용된 코드
import java.util.concurrent.TimeUnit;
public class StopThread {
private static boolean stopRequested;
### volatile 필드를 사용해 스레드가 정상 종료한다.
- 배타적 수행과는 상관없지만 항상 가장 최근에 기록된 값을 읽게 됨을 보장한다.
- 속도가 더 빠른 대안
~~~java
import java.util.concurrent.TimeUnit;
public class StopThread {
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;
}
}
안전 실패(safety failure)
증가 연산자(++)
volatile -> synchronized
int -> long
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;;
}
#### java.util.concurrent.atomic을 이용한 lock-free 동기화(#59)
- AtomicLong : thread safe
- 통신 + 배타적 실행
- 성능도 우수
~~~~java
private static final AtomicLong nextSerialNumber = new AtomicLong();
public static long generateSerialNumber() {
return nextSerialNumber.getAndIncrement();
}
결론
가변 데이터는 단일 스레드에서만 쓰자.
불변 데이터만 공유하거나(#17) 아무것도 공유하지 말자.
문서화해 놓기
한 스레드가 데이터를 다 수정한 후 다른 스레드에 공유할 때는 해당 객체에서 공유하는 부분만 동기화해도 된다.
사실상 불편 : 그 객체를 다시 수정할 일이 생기기 전까지 다른 스레드들은 동기화 없이 자유롭게 값을 읽어 갈 수 있다.
안전 발행 : 다른 스레드에 이런 객체를 건네는 행위
객체를 안정하게 발행하는 방법
클래스 초기화 과정에서 객체를 정적 필드, volatile 필드, final 필드, 홀은 보통의 락을 통해 접근하는 필드에 저장
synchronized 키워드
원자적(atomic)
메모리 모델
잘못된 코드 - 이 프로그램은 얼마나 오래 실행될까?
public class StopThread { private static boolean stopRequested;
}
적절히 동기화해 스레드가 정상 종료한다.
public class StopThread { private static boolean stopRequested;
}
public static int generateSerialNumber() { return nextSerialNumber++;; }
결론