Java Concurrent Programming - Overview

Java Concurrent Programming - Overview

이번 글은 Java concurrent programming의 기초 개념인 가시성과 원자성에 대해 알아본다.

프로그래밍이라는 것을 시작한 이래로 늘 무겁게 다가오는 주제가 Multi-Thread였다. 어떤 언어로 하던간에 어렵고 난해했다. 그렇기에 늘 내가 알고 하고있구나 하는 느낌 보다는 어디서 주섬 주섬 주워온 코드로 현실에 Issue들을 해결하기 급급했다. 헌데 지금은 양상이 다르다. 서버 프로그램밍을 하고 있는 시점에 "아 됐어, 돌아가!" 그리고 다음진도를 빼기에는 너무 불안하다. 사실 이 글을 쓰기전에 atomicReference와 atomicStampedReference등의 자바 Class를 이용한 기술 단편을 정리해서 글을 작성 했는데 내가 이거 뭐 알고 쓰고 있는거야 하는 자책감에 차마 공개하지 못하고 모든 진도와 계획을 멈추고 책과 자료를 찾아 뒤졌다. 자료를 하루 이틀 찾다 보니 그간 내가 정말 개념 없이 volitile, atomic을 운운 했구나 하는 생각이 들었다. 약 일주일의 시간이 긴 터널 같이 느껴질 정도로 간만에 정말 열심히 공부 했다. 더 나가기 전에 이 글을 이해하려면 적어도 Thread가 뭔지는 알아야 한다. 그렇지 못한 독자가 있다면 잠시 다른 자료를 통해 개념이 라도 이해 하고 다시 돌아오시기 바란다.

[관련 주제]

가시성(Visibility), 원자성(Atomicity)

목차

공유자원(Shared Resource)에 대한 이해

가시성(visibility)

원자성(Atomicity)

애필로그

공유자원(Shared Resource)에 대한 이해

가시성과 원자성을 다루는 이유는 복수의 스레드가 공유할 수 있는 하나의 자원을 동시에 읽거나 쓸 수 있는 경우가 발생 하기 때문이다. 프로그램을 설계 할 때 스레드 간 자원 공유를 최소화하면 사실 제일 좋다. 스레드간 상태를 공유 해야한던지 자료를 생산하고 소비사는 각각의 스레드가 공유 변수를 사용해야 하는 경우라면 어쩔 수 없다. 개발자는 가시성과 원자성을 고려하기에 앞서 스레드간 공유되는 자원이 어떤 것인지 명확히 할 필요가 있다.

repsimg_20190905

[공유 변수에 대한 스레드간 경합]

repsimg_multi-thread-isolation

[격리된 멤버변수 접근(비 경합)]

가시성(Visibility)

먼저 가시성을 이해 해보자. 가시성 그러니까 잘 보임의 대상은 무엇일까? 위에서 이미 거론 했지만 우리는 공유하는 변수에 대해 다루고 있다. 이 변수가 H/W에서는 Main-Memory에 적재 된다고 알고 있다. 맞다. 헌데 int i = 10; 이렇게 하면 그냥 메모리에 딱 하고 들어가 끝나면 좋겠는데 성능이니 뭐니 하면서 시스템은 아주 복잡한 설계를 해놨다. Thread는 동작하는 시점에 하나의 CPU(요즘에는 core라고 해야 맞게다.)를 점유하고 동작을 한다. 선언한 변수의 값이 Memory에만 존재하는 것이 아니라 CPU cache라고 하는 영역에도 존재한다. 학창시절에 풀이과정 까지 필요한 문제를 풀때 바로 답안지에 적는 것이 아니고 문제지에 풀어서 옮기는 것을 상상해보면 된다. CPU는 Memory까지 왔다 갔다 하는 시간을 아끼려 했나보다. 더 큰 문제는 CPU cache에 값이 Memory에 언제 옮겨 갈지도 모른다는 것이다. 이를 해결하는 것을 가시성이라고 한다. 이 해법에 대해서는 다음 이어질 글들에서 다루기로 한다.

repsimg_cache-by-cpu

[각 Thread가 각기 바라보는 CPU의 cashe]

repsimg_cache-use-visibility

[CUP1은 7까지 증가했으나 CPU2의 cache는 아직도 0으로 알고 있다.]

원자성(Atomicity)

가시성이라는 용어도 그렇지만 원자성이라는 이 단어 역시 상당히 은유적이다. 가시성 역시 메모리 가시성이라고 하면 좀더 쉽게 와닿을것이고 원자성 역시 연산의 원자성이라고 하면 좀더 이해가 쉽다. 관련 도서나 자료에서는 이를 연산의 단일성 혹은 단일 연산이라고 부르기도 한다. 공유되는 변수를 변경 할 때 기존의 값을 기반으로 하여 새로운 값이 결정되는 과정에서 여러 Thread가 이를 동시에 수행 할 때 생기는 이슈를 부르는 명칭이기도 하다. 다수의 책이나 자료에서 다루는 예가 i++; 연산이다. 자연어 입장에서는 하나의 문장이지만 이를 CPU가 수행 하기 위해서는 총 3가지의 instruction이 동작한다. i의 기존 값을 읽는다. i에 1을 더한다. i의 값을 변수에 할당한다. 이를 2개의 Thread가 동시에 100회 실시 한다고 했을때 만약 이 i++이 원자성을 가지고 있는 연한이라고 하면 결과가 200이어야 하겠지만 이를 프로그램으로 수행해보면 200보다 작은 값이 도출된다. 원인은 다시한번 이야기 하지만 i++이 3개의 instruction으로 구성되어 있기 때문에 Thread-1이 값을 읽어 i+1을 하기 직전에 Thread-2가 i을 읽어 i+1을 수행하고 반영하는 동작을 수행 한다면 후자의 연산은 무효가 되는 현상이 발생하는 것이다.

private int count = 0; 

@Test 
public void Test_AtomicIssue() { 
    ExecutorService es = Executors.newFixedThreadPool(2); 
    es.execute(new ForThreadTest()); 
    es.execute(new ForThreadTest()); 
    es.shutdown(); 
    try { 
        es.awaitTermination(10, TimeUnit.MINUTES); 
    } catch (InterruptedException e) { 
        e.printStackTrace(); 
    } 
    System.out.println("TEST result >>> " + count); 
} 

class ForThreadTest implements Runnable { 
    @Override public void run() { 
        for(int i = 0 ; i < 100; i++) { 
            AtomicStampedRefTest.this.count++; 
        } 
    } 
}

상기 코드는 원자성을 확보하지 못하여 기대 했던 200보다 작은 값이 도출된다. 첫 행의count변수 선언시 volatile을 지정하면 기대 했던 결과를 얻을 수도 있지만 속으면 않된다. 횟수를 늘리거나 성능이 떨어지는 PC에서 테스트 하면 기대치 보다 작은 숫자를 보게된다.

repsimg_cache-use-visibility

[가시성 문제를 해결해도 원자성이 확보되지 못하면 스레드는 여전히 불안]

애필로그

거창하게 이름 붙여진 JAVA concurrent programmming이라는 제목에서 느낄수 있겠지만 복잡하고 상당한 분량을 예고하고 있다. 이번 편에서는 병렬 프로그램의 뿌리가 되는 2가지의 용어의 개념을 이해 하자는 차원에서 그림과 간단한 코드로만 설명했다. 문제를 확실히 이해 했으면 이제 풀어나가면 된다. 모든 과학이 그러 하듯이....

참고자료

Java Volatile Keyword