본문 바로가기
Language/Effective java

동시성

by y.j 2022. 8. 22.
728x90

공유 중인 가변 데이터는 동기화해 사용하라

synchronized 키워드는 해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장한다. 

  • 한 객체가 일관된 상태를 가지고 생성되고 이 객체에 접근하는 메서드는 락을 건다. 그리고 다른 일관된 상태로 변화시킨다. 동기화를 제대로 사용한다면 이 객체의 상태가 일관되지 않은 순간을 볼 수 없다.
  • 동기화는 일관성이 깨진 상태를 볼 수 없게 하다.
  • 동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다.

 

자바에서는 스레드가 필드를 읽을 떄 항상 '수정이 완전히 반영된' 값을 얻는다고 보장하지만, 한 스레드가 저장한 값이 다른 스레드에게 '보이는가'는 보장하지 않는다. 따라서 동기화는 배타적 실행뿐 아니라 스레드 사이의 안정적인 통신이 꼭 필요하다.

 

안정적으로 동기화를 사용하는 방법

Thread.stop사용하지 말기

stopRequested처럼 필드를 두고 스레드를 멈추고자 할 때 true를 넣는 방식이다.

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

하지만, 위 코드는 끝나지 않고 계속 지속된다. OpenJDK서버 VM이 실제로 적용하는 호이스팅 기법을 통해 아래와 같이 바뀔 수 있기 때문이다.

while(!stopRequested) 
    i++;
if(!stopRequested)
    while(true)
        i++;

 

쓰기와 읽기 모두가 동기화되지 않으면 동작을 보장하지 않는다. 따라서 requestStop과 stopRequested함수 둘다 synchronized를 붙여줘야한다.

public class StopThread {
    private static boolean stopRequested;

    private static synchronized void requestStop() {
        stopRequested = true;
    }

    private static synchronized boolean stopRequested() {
        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 {
    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;
    }
}

 

이 코드는 안전 실패를 포함할 수 있다. 1번 스레드가 nextSerialNumber라는 필드를 읽고 새로운 값을 저장하려는데 그 사이에 2버 스레드가 값을 읽어가면 첫 번째 스레드와 똑같은 값을 돌려받게 된다. 또 ++연산을 2번 할 수 있기 때문에 int의 최댓값을 넘어갈 수도 있다.

private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() {
    return nextSerialNumber++;
}

 

해결방법은 메서드에 synchronized한정자를 붙이면 해결되지만 필드(nextSerialNumber)에서도 volatile를 제거해야 한다.

 

자바에서 제공하는 객체가 있다. AtomicLong은 락 없이도 스레드 안전한 프록래밍을 지원하는 클래스들이 담겨 있으며 원자성까지 지원한다.

private static final AtomicLong nextSerialNum = new AtomicLong();

public static long generateSerialNumber() {
    return nextSerialNum.getAndIncrement();
}

 

기본적으로는 가변 데이터에서는 단일 스레드만 쓰는 것이 좋다. 사용해야 된다면

  • 공유하는 부분만 동기화해도 된다. 
  • 불변객체를 정의하고  다른 스레드에 이런 객체를 건네는 행위를 안전 발행위라 한다.
  • 클래스 초기화 과정에서 공유하는 필드를 static, volatile, final 필드를 이용하여 접근한다.
728x90

'Language > Effective java' 카테고리의 다른 글

동시성  (0) 2022.08.27
동시성  (0) 2022.08.24
예외  (0) 2022.08.19
예외  (0) 2022.08.18
예외  (0) 2022.08.17

댓글