syncronized 키워드는 특정 메서드나 코드 블록을 한번에 한 스레드만 사용하도록 보장한다.
· 동기화 없이는 한 스레드가 만든 변화를 다른 스레드에서 확인할 수 없다.
· 동기화는 스레드가 일관성이 깨진 객체를 관측할 수 없도록 할 뿐 아니라, 동기화 메서드나 동기화 블록에 진입한 스레드가 동일한 락의 보호 아래 이루어진 모든 변경의 영향을 관측할 수 있도록 보장한다.
자바 언어 명세에는 "long이나 double이 아닌 모든 변수는 원자적으로 읽고 쓸수 있다."고 명시되어 있다.
· long이나 double이 아닌 변수를 읽으면 나오는 값은 항상 어떤 스레드가 저장한 값이라는 것.
성능을 높이기 위해 "원자적 데이터를 읽거나 쓸 때 동기화를 피해야 한다.?"
· 매우 위험한 이야기이며 상호 배제성 뿐 아니라 스레드 간의 안정적 통신을 위해서도 동기화는 반드시 필요하다.
ex) 한 스레드에서 다른 스레드를 중지 시킬 수 있도록 하는 과제
· Thread.stop메서드는 안정성이 결여 된 문제가 있으므로(데이터가 망가질 수 있다.) 오래전에 폐기됨
· boolean필드를 이용하여 검사
ㄴ 해당 필드가 true면 계속 검사하며 false면 스스로 중단하는 동작
//잘못된 코드 - 얼마나 오랫동안 실행 될까?
public class StopThread{
private static boolean stopReqested;
public static void main(String[] args) throws InterruptedException{
Thread backgroundThread = new Thread(new Runnable(){
public void run(){
int i = 0;
while( !stopRequested )
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
위의 메소드는 실행한지 1초가 지나면 main스레드가 stopRequested값을 true로 바꾸고, 후면 스레드가 실행하는 순환문도 그 때 중지될 것같다. 1초뒤 종료될까?
이 프로그램은 절대 중지되지 않는다.
후면 스레드가 실행한 순환문이 계속 돌고 있다.
동기화 메커니즘을 적용하지 않은 문제로 main스레드가 변경한 stopRequested의 새로운 값을 후면 스레드가 언제 쯤 보게 될지 알 수가 없다.
동기화가 적용되지 않는다면
if(!stopRequested)
while(true)
i++;
위와 같은 방법을 최적화 끌어올리기(hoisting), HotStop서버나 VM이 하는 일이다.
위와 같은 방법으로 적용하면 생존 오류(liveness failure)가 생긴다.
· 생존 오류 : 살아있기는 하나 더 진행하지 못하는 프로그램
동기화 적용
public class StopThread{
private static boolean stopReqested;
private static syncronized void requestStop(){
stopRequested = true;
}
private static syncronized boolean stopRequested(){
return stopRequested;
}
public static void main(String[] args) throws InterruptedException{
Thread backgroundThread = new Thread(new Runnable(){
public void run(){
int i = 0;
while( !stopRequested() )
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
일기 연산과 쓰기 연산에 전부 적용하지 않으면 동기화는 아무런 효과도 없다!
순환문마다 동기화를 실행하는것이 비용을 줄이기 위해서는
위 코드에 사용된 boolean 필드를 volatile로 선언하는 것이다.
//volatile 필드를 사용
public class StopThread{
private static volatile boolean stopReqested;
public static void main(String[] args) throws InterruptedException{
Thread backgroundThread = new Thread(new Runnable(){
public void run(){
int i = 0;
while( !stopRequested )
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
volatile을 사용할 때는 주의해야 한다.
// 잘못된 예제 - 동기화가 필요하다.
private static volatile int nextSerialNumber = 0;
private static int generateSerialNumber(){
return nextSerialNumber++;
}
· 문제는 연산자 ++가 원자적이지 않다는 것에 있다.
· 먼저 값을 읽고, 새로운 값 + 1을 필드에 쓴다.
· 첫번 째 스레드가 필드의 값을 읽은 후 새 값을 미처 기록하기 전에 두번 째 스레드가 필드에서 같은 값을 읽으면 두 스레드는 같은 일련번호를 얻게 된다.
· 안전오류(Safety failure)
해결
· 메서드를 syncronized로 선언
· syncronized를 선언할 때는 volatile을 삭제해야 한다.
· int대신 long으로 사용(견고)
· nextSerialNumber가 반환할 값이 다시 0으로 돌아가 때 예외가 뜨도록 만들어라.
· 더 좋은 방법은 AtomicLong 클래스를 쓰는것
private static final AtomicLong nextSerialNum = new AtomicLong();
public static long generateSerialNumber(){
return nextSerialNum.getAndIncrement();
}
결론
· 변경 가능 데이터를 공유하지 않는 것이 가장 좋은 방법이다.
· 혹은 변경 가능 데이터는 한 스레드만 이용하도록 한다.
· 변경 가능한 데이터는 해당 데이터를 읽거나 쓰는 모든 스레드는 동기화를 수행해야 한다.
· 변경 가능 데이터를 동기화 하지 않으면 생존 오류나 안전 오류가 생긴다.
'IT > Programming' 카테고리의 다른 글
<Effective Java> RULE 64 실패 원자성 달성을 위해 노력하라 (0) | 2023.04.26 |
---|---|
<Effective Java> RULE 65 예외를 무시하지 마라 (0) | 2023.04.26 |
<Effective Java> RULE 67 과도한 동기는 피하라 (0) | 2023.04.26 |
<Effective Java> RULE 68 스레드보다는 실행자와 태스크를 이용하라 (0) | 2023.04.26 |
<Effective Java> RULE 69 wait이나 notify대신 병행성 유틸리티를 이용하라 (0) | 2023.04.26 |