IT/Programming / / 2023. 4. 26. 13:25

<Effective Java> RULE 66 변경 가능 공유 데이터에 대한 접근은 동기화 하라

반응형

 

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();
}

 

결론

· 변경 가능 데이터를 공유하지 않는 것이 가장 좋은 방법이다.

· 혹은 변경 가능 데이터는 한 스레드만 이용하도록 한다.

· 변경 가능한 데이터는 해당 데이터를 읽거나 쓰는 모든 스레드는 동기화를 수행해야 한다.

· 변경 가능 데이터를 동기화 하지 않으면 생존 오류나 안전 오류가 생긴다.

 

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유