본문 바로가기

카테고리 없음

자바의 동시성 이해하기

728x90

Effective Java의 '동시성' 챕터를 정리한 내용입니다.

 

많은 언어에서 동시성이란, 여러 작업을 동시에 처리되는 것을 의미한다. 이펙티브 자바의 '동시성'도 같은 맥락 안에서 자바에 대해서 설명한다.

가변 데이터 동기화

자바에서 동시성을 접한다면 가장 먼저 synchronized 키워드에 대해서 알게 된다. 해당 챕터를 읽기 전까지 synchronized 키워드는 독립된 두 스레드가 특정 임계 영역에서 하나만 존재하도록 하는 배타적 실행 키워드로 알기 쉽지만, '동기화' 측면에 대해서는 쉽게 접하지 못한다.

 

스레드가 필드를 읽을 때 항상 ‘수정이 완전히 반영된’ 값을 얻는다고 보장하지 만, 한 스레드가 저장한 값이 다른 스레드에게 ‘보이는가’는 보장하지 않는다. - 이펙티브 자바 p.414

 

먼저 '동기화'에 대해서 알아보자. synchronized는 '스레드 통신'을 지원한다. 즉, 스레드가 변경한 내용을 다른 스레드가 볼 수 있도록 한다. 처음 이 맥락을 이해하기 어려웠는데, 'happens-before', 'reordering'에 대해서 학습한 후 조금 더 이해할 수 있었다.

Reordering

우리는 자바 코드를 작성해 컴파일하여 바이트코드가 생성되고, 일련에 과정이 진행된 후 JVM에서 해당 바이코드를 해석하며 Machine에 적합한 명령어로 해석된다. 이 과정에서 컴파일러는 CPU를 더 효율적으로 처리하기 위해 몇가지 마법을 부리는데 그 중 하나가 reordering이다. 컴파일러는 독립적으로 실행할 수 있는 명령어를 앞으로 당겨서 CPU가 병렬적으로 처리할 수 있도록 최적화한다.

a = 1;
b = a + 10;
c = 30;

쉽게 이해할 수 있도록 우리가 익숙한 형태의 코드를 생각해보자. 두 번째 명령어는 첫 번째 명령어에 의존적이다. 따라서 첫 번째 명령어가 수행되지 못하면 두 번째 명령어를 수행될 수 없다. 하지만, 세 번째 명령어는 독립적으로 실행이 가능하다.

a = 1;
c = 30; // reordering
b = a + 10;

따라서 컴파일러는 위 코드처럼 순서를 변경한다. 책에서 언급한 호이스팅이 이를 의미한다.

Happens-before

앞 Reordering을 살펴보며 우리가 작성한 코드가 컴파일 과정에서 순서가 변경될 수 있다는 것을 알았다. 하지만 이런 변경이 기준 없이 수행되면 동시성 프로그래밍을 하는 개발자는 완전히 운에 맡겨 개발을 하게 된다. Java는 이러한 과정에서 JMM(Java memory model)을 통해 몇가지 규칙을 제안한다. 좀 더 자세한 내용인 이 곳이 곳을 참고하자.

 

즉, 간단히 이야기하자면, 두 스레드가 각각 읽기/쓰기가 동시에 수행된다면, 읽기 동작 수행 전에 쓰기가 수행되도록 보장하는 것이다. 자바의 초기 설계 단계에서 이러한 과정에서 문제가 있어 final 변수가 지연 초기화되는 등 많은 문제가 있었다고 한다.

동기화

CPU는 빠른 처리를 위해 접근 속도에 따른 저장소를 가진다. 명령어 처리를 위해 값을 저장할 용도의 레지스터, 메인 메모리에서 읽은 값을 다음에 따르게 접근하기 위한 캐시, 그리고 디스크 데이터를 빠르게 접근하기 위한 메인 메모리, 그리고 디스크 등 우리가 흔히 컴퓨터 구조에서 배웠던 내용이다.

 

책에서 언급한 동기화는 '스레드 통신'에 대한 내용으로, 스레드가 수정한 캐시의 내용을 바로 메인 메모리에 반영하도록 해 다른 스레드가 수정된 내용을 읽을 수 있도록 한다.

예제

    private static class FameExchange {

        private long framesStoredCount = 0;
        private long framesTakenCount = 0;

        private volatile boolean hasNewFrame = false;

        private Frame frame;

        private void storeFrame(final Frame frame) {
            this.hasNewFrame = true;
            this.frame = frame;
            this.framesStoredCount++;
            // this.hasNewFrame = true; // happens-before
        }

        private Frame takeFrame() {
            while (!hasNewFrame) {
                //
            }

            final var newFrame = this.frame;
            this.framesTakenCount++;
            this.hasNewFrame = false;
            return newFrame;
        }
    }

    private static class Frame {}

위 코드는 각각 write(storeFrame)와 read(takeFrame)을 의미한다. 이때 두 스레드가 각각 storeFrame, takeFrame을 동시에 실행하게 되면 `this.hasNewFrame`의 코드 라인 위치에 따라서 takeFrame은 항상 NULL을 반환하게 된다.