베어_
TechBear
베어_
전체 방문자
오늘
어제
  • 분류 전체보기 (336)
    • Spring (33)
      • 개념 (13)
      • Security (5)
      • 실습 (1)
      • 토비 스프링 (11)
    • JPA (6)
    • 프로젝트 기록 (24)
    • DB (13)
    • JAVA (18)
    • 알고리즘 (50)
      • 유형정리 (8)
      • Baekjoon (21)
      • LeetCode (18)
    • 디자인패턴 (0)
    • 개발서적 (79)
      • Effective Java (78)
      • 객체지향의 사실과 오해 (1)
    • 독후감 (4)
    • 보안 (2)
    • 운영체제(OS) (53)
      • 공룡책 (53)
    • 컴퓨터 네트워크 (28)
      • 컴퓨터 네트워크 하향식 접근 (23)
    • 자료구조 (1)
    • DevOps (2)
    • 앱 개발 (20)
      • 안드로이드 스튜디오 (20)

블로그 메뉴

    공지사항

    인기 글

    태그

    • 이펙티브자바
    • 스프링시큐리티
    • 토비스프링
    • 자바8
    • 자바
    • jpa
    • 스프링
    • 코드업
    • BFS
    • 백준
    • C++
    • 알고리즘
    • leetcode
    • 데이터베이스
    • 함수형인터페이스
    • dfs
    • java
    • 운영체제
    • Spring
    • 스레드

    최근 댓글

    최근 글

    티스토리

    hELLO · Designed By 정상우.
    베어_

    TechBear

    개발서적/Effective Java

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

    2023. 6. 6. 07:05

    동기화

    1. synchronized 키워드는 해당 메서드나 블록을 한번에 한 스레드씩 수행하도록 보장해준다. 즉, 배타적 실행을 도와준다.
    2. 동기화는 일관성이 깨진 상태를 볼 수 없게하는 것은 물론, 동기화된 메서드나 블록에 들어간 스레드가 같은 락의 보호하에 수행된 모든 이전 수정의 최종 결과를 보게 해준다.
      • 자바 언어 명세는 스레드가 필드를 읽을 때 항상 수정이 완전히 반영된 값을 얻는다고 보장하지만, 한 스레드가 저장한 값이 다른 스레드에게 보이는가는 보장하지 않는다.

    스레드 간 통신

    공유 중인 가변 데이터를 비록 원자적으로 읽고 쓸 수 있을지라도 동기화에 실패하면 처참한 결과를 얻을 수 있다. 예를 들어 스레드를 멈추는 작업을 생각해보자.

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

    이 프로그램은 1초 후에 종료될 것 같지만 사실은 그렇지 않다. 원인은 동기화에 있는데, 동기화하지 않으면 메인 스레드가 수정한 값을 백그라운드 스레드가 언제쯤 보게 될지 보증할 수 없다. 동기화가 빠지면 가상 머신이 다음과 같은 최적화를 수행할 수도 있다.

    // 원래 코드
    while(!stopRequested) i++;
    
    // 최적화한 코드
    if(!stopRequestd)
        while (true)
            i++;

    stopRequested필드를 동기화해 접근하면 이 문제를 해결할 수 있다. 쓰기 메서드와 읽기 메서드 모두를 동기화했음에 주목하자. 쓰기와 읽기 모두가 동기화되지 않으면 동작을 보장하지 않는다.

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

    반복문에서 매번 동기화하는 비용이 크진 않지만 속도가 더 빠른 대안이 있다. stopRequested필드를 volatile으로 선언하면 동기화를 생략해도 된다.

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

    volatile을 사용할 때는 주의해서 사용해야 한다. 예를 들어 다음 일련번호를 생성하는 코드를 보자

    private static volatile int nextSerialNumber = 0;
    public static int generateSerialNumber() {
        return nextSerialNumber++;
    }

    이 메서드는 제대로 동작하지 않는다. 문제는 증가 연사자(++)에 있다. 이 연산자는 코드상으로는 하나지만 실제로는 nextSerialNumber필드에 두 번 접근한다. GenerateSerialNumber 메서드에 synchronized 한정자를 붙이면 이 문제가 해결된다. 동시에 호출해도 서로 간섭하지 않으며 이전 호출이 변경한 값을 읽게 된다. 메서드에 synchronized를 붙였다면 nextSerialNumber필드에는 volatile을 제거해야 한다.

    java.util.concurrent.atomic

    이 패키지에는 락 없이도 스레드 안전한 프로그래밍을 지원하는 크래스들이 담겨 있다. volatile은 동기화의 두 효과 중 통신 쪽만 지원하지만 이 패지키는 원자성(배타적 실행)까지 지원한다.

    private static final AtomicLong nextSerialNumber = new AtomicLong();
    public static int generateSerialNumber() {
        return nextSerialNumber.getAndIncrement();
    }

    이번 챕터에서 언급한 문제를 피하는 가장 좋은 방법은 애초에 가변 데이터를 공유하여 사용하지 않는 것이다. 불변 데이터만 공유하거나 아무것도 공유하지 말자.

    사실상 불변과 안전 발행

    한 스레드가 데이터를 다 수정한 후 다른 스레드에 공유할 때는 해당 객체에서 공유하는 부분만 동기화해도 된다. 그러면 그 객체를 다시 수정할 일이 생기기 전까지 다른 스레드들을 동기화 없이 자유롭게 값을 읽어갈 수 있다. 이를 사실상 불변이라 하고 다른 스레드에 이런 객체를 건네는 행위를 안전 발행이라 한다.

    객체를 안전하게 발행하는 방법은 다음과 같다.

    1. 클래스 초기화 과정에서 객체를 정적 필드, volatile필드, final필드, 혹은 락을 통해 접근하는 필드에 저장한다
    2. 동시성 컬렉션에 저장한다.
    저작자표시 비영리 변경금지 (새창열림)
      '개발서적/Effective Java' 카테고리의 다른 글
      • [Effective Java] 커스텀 직렬화 형태를 고려해보라
      • [Effective Java] Serializable을 구현할지는 신중히 결정하라
      • [Effective Java] 예외를 무시하지 말라
      • [Effective Java] 가능한 한 실패 원자적으로 만들라
      베어_
      베어_
      Today I learned | 문제를 해결하는 개발자

      티스토리툴바