이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/thread-signaling.html



쓰레드 시그널링의 목적은 쓰레드들이 서로 신호를 교환하도록 하기 위함이다. 쓰레드 시그널링은 쓰레드에게 다른 쓰레드로부터의 신호를 기다리도록 할 수 있다. 예를 들어, Thread B 는 Thread A 의 신호 -처리될 데이터가 준비되었다는- 가 올 때 까지 대기할 수 있다.



공유 객체를 이용한 시그널링


쓰레드들이 서로 신호를 보내도록 하는 간단한 방법은 공유 객체 변수에 신호값을 세팅하는 것이다. Thread A 는 동기화블록 안에서 boolean 타입 멤버변수인 hasDataToProcess 에 true 를 세팅하고, Thread B 는 동기화블록 안에서 이 변수의 값을 읽는다. 여기 시그널 변수를 가진 한 객체가 이 변수에 값을 세팅하고 확인하는 메소드를 제공하는 예제가 있다:


public class MySignal{

  protected boolean hasDataToProcess = false;

  public synchronized boolean hasDataToProcess(){
    return this.hasDataToProcess;
  }

  public synchronized void setHasDataToProcess(boolean hasData){
    this.hasDataToProcess = hasData;  
  }

}


Thread A 와 B 는 시그널링을 위해 MySignal 의 인스턴스를 공유해야 한다. 만일 이 두 쓰레드가 서로 다른 MySignal 인스턴스를 참조한다면, 이들은 서로의 신호를 감지할 수 없을 것이다. 시그널링을 위한 데이터는 MySignal 인스턴스로부터 분리된 공유 버퍼에 위치할 수 있다.



Busy Wait


데이터를 처리하는 쓰레드인 Thread B 는 데이터가 접근 가능해질 때까지 기다린다. 다시 말해서, 이 쓰레드는 Thread A 가 hasDataToProcess() 메소드에서 true 를 반환할 때까지 기다린다. Thread B 돌고 있는 루프는 다음과 같이 신호를 기다린다:


protected MySignal sharedSignal = ...

...

while(!sharedSignal.hasDataToProcess()){
  //do nothing... busy waiting
}


루프는 hasDatToProcess() 메소드가 true 를 호출할 때까지 그저 기다린다. 이를 busy waiting 이라 부른다. 이 쓰레드는 기다리기 바쁘다(The thread id busy while waiting).



wait(), notify() and notifyAll()


Busy waiting 은 대기(waiting)하는 쓰레드를 돌리기 위한 CPU의 활용으로써 그다지 효율적인 활용은 아니다. 평균 대기 시간이 아주 짧을 경우에는 괜찮겠지만, 그렇지 않다면 차라리 대기중인 쓰레드로 하여금 - 기다리는 신호가 도착할 때까지 - sleep 상태나 inactive 하도록 만드는 편이 현명하다.


자바에는 신호를 기다리는 동안 쓰레드의 활동을 중단하도록 하는 대기 메카니즘이 내장되어 있다. java.lang.Object 클래스에는 이를 위한 세 가지 메소드, wait(), notify() 그리고 notifyAll() 이 정의되어 있다.


wait() 메소드는 java.lang.Object 클래스에 정의되어 있기 때문에 어느 객체에든 이 메소드를 호출할 수 있는데, 어느 객체의 wait() 메소드를 호출하든, 이를 호출하는 쓰레드는 다른 쓰레드가 notify() 를 호출할 때까지 활동을 중단한다. wait() 이나 waiting 을 깨우는 메소드를 호출하기 위해서는 쓰레드는 먼저 반드시 해당 객체의 lock 을 획득하고 있어야 한다. 다시말해, wait() 이나 notify() 메소드 호출은 반드시 synchronized 블록 안에서 이루어져야 한다는 것이다. 


다음은 MySignal 클래스가 이를 활용하도록 수정한 버전이다:


public class MonitorObject{
}

public class MyWaitNotify{

  MonitorObject myMonitorObject = new MonitorObject();

  public void doWait(){
    synchronized(myMonitorObject){
      try{
        myMonitorObject.wait();
      } catch(InterruptedException e){...}
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      myMonitorObject.notify();
    }
  }
}


waiting 쓰레드는 doWait() 을 호출하고, notifying 쓰레드는 doNotify() 를 호출할 것이다. 한 쓰레드가 한 객체의 notify() 를 호출하면, 이 객체의 wait() 메소드 호출을 통해 대기중인 쓰레드들 중 한 쓰레드가 대기 상태에서 깨어나고 실행 가능한 상태가 된다. notify() 대신 notifyAll() 메소드를 호출하면 이 객체를 두고 대기중인 모든 쓰레드가 깨어난다.


보다시피 wait() 와 notify() 메소드 호출은 synchronized 블록 안에서 호출된다. 이것은 필수이다. 쓰레드는 wait(), notify() notifyAll() 메소드들이 호출되는 객체에 대한 lock 을 가지고 있지 않은 상태에서 이 메소드들을 호출할 수 없다. lock 을 가지지 않고 이 메소드들을 호출하면 IllegalMonitorStateexception 이 던져진다.


그런데, 어떻게 이게 가능할까? waiting 쓰레드는 synchronized 블록 안의 코드를 실행하는 한, 모니터 객체(myMonitorObject)에 대한 lock 을 유지하고 있지 않은가? waiting 쓰레드는 notifying 쓰레드가 doNotify() 를 호출하며 synchronized 블록 안으로 진입하는 것을 막지 않는 것인가? 대답은 no 다. 쓰레드는 wait() 을 호출하면 자신이 가지고 있던 lock 을 풀어두게 된다. 그리고 이로 인해 다른 쓰레드들이 또 lock 을 획득하고, wait() 이나 notify() 메소드를 호출할 수 있게 되는 것이다. 


한 번 wait() 메소드 호출로 잠들었다가 notify() 호출에 의해 깨어난 쓰레드는 notify() 메소드를 호출한 쓰레드가 풀어놓은 lock 을 다시 획득해야만 wait() 호출을 벗어날 수 있다. 다시말해: wait() 호출은 synchronized 블록 안에서 이루어지기 때문에, notify() 에 의해 깨어난 쓰레드는 wait() 호출을 벗어나기 위해 lock을 재획득해야만 한다. notifyAll() 호출이 잠들어있던 다수의 쓰레드를 깨우면, 이 잠들어있던 쓰레드들은 차례대로 lock 을 재획득하기 때문에, 오직 한 시점에 한 쓰레드만이 wait() 메소드를 벗어날 수 있는 것이다.



Missed Signals


wait() 를 호출한 쓰레드가 없는 상태에서 notify(), notifyAll() 메소드를 호출하면, notify(), notifyAll() 의 신호는 그대로 분실된다. 이는 문제가 될 수도, 안 될 수도 있는데, 때때로 이런 상황은 영영 깨어나지 못하고 계속 잠들어 있는 쓰레드를 만들어 내기도 한다. 이 쓰레드를 꺠우기 위한 신호가 분실됐기 때문이다.


이렇게 신호를 분실하는 상황을 피하기 위해, 신호를 내부에 저장하는 클래스를 만들어야 한다. 위 예제의 MyWaitNotify 는 notify 신호를 MyWaitNotify 인스턴스의 멤버변수로 저장할 필요가 있다. 아래 예제는 그것을 적용한 버전이다:


public class MyWaitNotify2{

  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      if(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}


doNotify() 메소드가 notify() 를 호출하기 전에 wasSignalled 를 true 로 세팅하는 부분과, doWait() 메소드가 wait() 를 호출하기 전에 wasSignalled 를 체크하는 부분을 잘 봐야한다. wait() 메소드는 doNotify() 가 호출되지 않은 상태에서만 호출된다.



Spurious wakeups


알 수 없는 이유로, notify() 나 notifyAll() 이 호출되지 않았음에도 불구하고 쓰레드가 깨어나는 현상이 있다. 이는 spurious wakeups 라 불리는데, 어떤 분명한 이유없이 쓰레드가 깨어난다.


만일 MyWaitNotify2 클래스의 doWait() 메소드에 spurious wakeup 이 발생한다면, 필요할 때 doNotify() 메소드 호출을 통해 깨어났어야 할 waiting 쓰레드가 제멋대로 깨어나 동작을 재개하는 현상이 발생할 수 있고, 이는 어플리케이션에 심각한 문제가 될 수 있다.


spurious wakeups 현상을 막기 위해서 신호를 저장하는 멤버변수 체크를 if 대신 while 루프에서 수행하는 방법이 있다. 이 역할을 하는 루프를 spin lock 이라 부른다. spin lock 안의 조건이 false 가 될 때가지 쓰레드는 루프를 계속 돈다.


MyWaitNotify2 에 spin lock 을 적용한 예제는 다음과 같다:


public class MyWaitNotify3{

  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      while(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}


wait() 호출은 if 가 아닌 while 루프 안에 있다. 이제 doNotify() 메소드가 호출되지 않는 한 wasSignalled 변수는 false 를 유지하게 되기 때문에, waiting 쓰레드가 깨어나도 wasSignalled 를 반복 체크하여 다시 wai() 메소드를 호출하고 대기 상태로 돌아갈 것이다.



같은 신호를 기다리는 멀티플 쓰레드


만약 다수의 waiting 쓰레드들이 있고, 이 쓰레드들이 모두 notifyAll() 호출을 통해 깨워지지만, 이들 중 한 쓰레드만 작업이 허용되는 상황에서라면, while 루프는 좋은 해결책이 된다. 


한 시점에 오직 한 쓰레드만 lock 을 획득할 것이고, 이는 이 한 쓰레드만 wait() 호출을 벗어나 wasSignalled 플레그를 초기화 할 수 있다는 것을 의미한다. 이 쓰레드가 wait() 호출을 벗어나고 synchronized 블록을 벗어나면, 대기중이던 다른 쓰레드들도 lock 을 획득하여 wait() 호출을 벗어나고 while 루프의 wasSignalled 변수를 체크한다. 그런데 여기서 이 wasSignalled 플레그 변수는 첫 번째로 루프를 빠져나간 쓰레드에 의해 false 로 초기화되었기 때문에, 나머지 다른 쓰레드들은 다시 루프를 반복하여 대기상태로 놓이게 된다.



글로벌 객체나 문자열 상수에 wait() 을 호출하지 말 것


이 글의 예전 버전에서는 MyWaitNotify 예제 클래스의 수정판이 있었는데, 이 클래스는 상수 문자열 ("") 을 모니터 객체로 사용했었다:


public class MyWaitNotify{

  String myMonitorObject = "";
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      while(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}


빈 문자열이나 다른 문자열 상수에 대해 wait() 과 notify() 를 사용할 때의 문제점은, JVM/컴파일러는 내부적으로 상수 문자열을 같은 객체로 변환한다는 것이다. 같은 문자로 구성된 문자열 상수는 하나의 인스턴스가 되어 참조되는데, 때문에 빈 문자열을 모니터 객체로 사용하게 되면, MyWaitNotify 의 인스턴스가 여러개일 때, 모두 같은 하나의 인스턴스 - 문자열 인스턴스, ("") - 를 모니터 객체로 공유하게 되는 것이다.


이 상황을 다이어그램으로 나타내자면 다음과 같다:



MyWaitNotify1 인스턴스과 MyWaitNotify2 인스턴스는 같은 문자열 인스턴스를 모니터 객체로 공유한다. doWait(), doNotify() 의 신호 변수는 각각의 인스턴스에 개별적으로 저장되어 있는데, 같은 모니터 객체 하나("")를 공유해서 사용하기 때문에 이럴 경우 MyWaitNotify1 의 doNotify() 호출이 MyWaitNotify2 를 깨우고, 그 신호는 MyWaitNotify1 에 저장하는 문제가 발생할 수 있다.


이 현상은 큰 문제가 될 수 있는데, 일단 MyWaitNotify2 인스턴스에 대한 doNotify() 호출이 Thread A 와 Thread B 를 의도치 않게 깨울 수 있다. 이렇게 깨어난 쓰레드들은 while 루프에서 자신의 신호(signal) 변수를 체크하고 다시 대기상태로 돌아갈 것이다. 왜냐하면 doNotify() 는 Thread A/B 가 대기중인 MyWaitNotify 인스턴스에 대해 호출된 것이 아니기 때문이다. 이 상황은 spurious wakeup 이 발생한 것과 같은 상황이다. Thread A 나 B 는 의도치 않게 깨워졌다. 하지만 신호 변수가 바뀌지 않았기에 이 쓰레드들은 다시 대기상태로 돌아가고, 아직까진 괜찮은 상황이다.


문제는 doNotify() 메소드는 notifyAll() 이 아닌, notify() 를 호출한다는 것인데, 같은 모니터 객체("") 를 두고 4개의 쓰레드가 대기중에 있지만 오직 한 쓰레드만 깨어난다. 때문에, 만약 원래 깨우려고 했던 쓰레드가 C 나 D 였지만 Thread A 나 B 중 하나가 깨어난 상황이 발생하면, 이 깨어난 쓰레드는 자신의 신호 변수를 체크하고는 자신에게 주어진 신호가 없음을 알게된다. 그리고 다시 대기상태로 돌아간다. Thread C 나 D 중 어느쪽도 - 깨워지지 못했기 때문에 - 자신에게 주어진 신호를 체크하지 않는다. 신호가 분실된 것이다. 이 상황은 위에서 설명된, 신호 분실(missed signals) 문제와 같다. Thread c 와 D 는 신호를 받았지만, 응답에는 실패했다.


doNotify() 메소드가 notify() 가 아닌 notifyAll() 을 호출한다면, 대기중이던 모든 쓰레드들이 깨어나서 차례대로 자신들의 신호 변수를 체크할 것이다. 그리고 Thread A 와 B 는 자신들에게 온 신호가 없기에 다시 대기상태로 돌아가고, C 와 D 중 하나는 신호를 체크하고 doWait() 메소드 호출을 벗어나게 된다.

이 설명을 읽은 당신은 어쩌면 언제나 notify() 대신 notifyAll() 을 호출하려 할 수도 있다. 하지만 이는 성능을 고려할 때 바람직하지 못하다. 신호를 받고 응답하는 쓰레드가 단 하나뿐인데 모든 쓰레드를 깨울 이유는 없다.


결론: wait()/notify() 를 글로벌한 객체나, 문자열 상수같은 곳에 사용하지 말아야 한다. 유니크하게 사용될 수 있는 객체를 모니터로 사용하자. 예를 들자면 위 예제들 중 MyWaitNotify3 인스턴스가 자신의 MonitorObject 인스턴스를 가지고 있는 것처럼.



'Java > Concurrency' 카테고리의 다른 글

데드락 방지(Deadlock Prevention)  (0) 2017.04.09
데드락(Deadlock)  (0) 2017.04.09
자바 쓰레드로컬(ThreadLocal)  (1) 2017.04.09
자바 volatile 키워드  (2) 2017.04.09
자바 동기화 블록(Java Synchronized Blocks)  (0) 2017.04.09

이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/threadlocal.html



자바 ThreadLocal 클래스는 오직 한 쓰레드에 의해 읽고 쓰여질 수 있는 변수를 생성할 수 있도록 한다. 때문에 만일 두 쓰레드가 같은 코드를 실행하고 이 코드가 하나의 ThreadLocal 변수를 참조하더라도, 이 두 쓰레드는 서로의 ThreadLocal 변수를 볼 수 없다. 글자 그대로 쓰레드의 지역변수이다.



쓰레드로컬 생성


다음은 ThreadLocal 을 생성하는 간단한 예제이다:


private ThreadLocal myThreadLocal = new ThreadLocal();


보다시피, 새로운 ThreadLocal 객체를 생성했다. 이 코드는 오직 한 쓰레드 당 한 번의 실행을 허용하여, 다수의 쓰레드에 의한 실행에도 각 쓰레드는 자신들 각자의 ThreadLocal 인스턴스를 가지게 된다. 두 쓰레드가 각자의 변수를 ThreadLocal 객체에 세팅했다면, 쓰레드들은 서로 다른 ThreadLocal 변수값을 가지게 되며, 오직 자신의 변수만 볼 수 있게 된다.



쓰레드로컬 다루기


쓰레드로컬을 생성하면 다음과 같은 방법으로 쓰레드로컬에 값을 저장할 수 있다:


myThreadLocal.set("A thread local value");


다음으로 쓰레드로컬의 값을 읽는 방법은 다음과 같다:


String threadLocalValue = (String) myThreadLocal.get();


get() 메소드는 Object 타입 객체를 반환하고, set() 메소드 역시 Object 타입을 파라미터로 받는다.



쓰레드로컬 제너릭


쓰레드로컬은 변수 타입을 다루기 쉽도록, 제너릭으로 생성 가능하다:


private ThreadLocal myThreadLocal = new ThreadLocal<String>();
myThreadLocal.set("Hello ThreadLocal");

String threadLocalValue = myThreadLocal.get();



쓰레드로컬 변수 초기화


쓰레드로컬 객체에 세팅한 값은 오직 값을 세팅한 쓰레드만 접근할 수 있기 때문에, 모든 쓰레드가 사용할 수 있는 쓰레드로컬 초기값(디폴트)은 없다. 대신, 쓰레드로컬을 서브클래싱하고 initialValue() 메소드를 오버라이딩하는 방법으로 쓰레드로컬의 초기값을 설정할 수 있다:


private ThreadLocal myThreadLocal = new ThreadLocal<String>() {
    @Override protected String initialValue() {
        return "This is the initial value";
    }
};  


이제 이 코드를 이용하는 모든 쓰레드들은 set() 메소드로 값을 세팅하지 않아도 get() 메소드를 호출하면 이 초기값을 볼 수 있을 것이다.



쓰레드로컬 예제


다음은 쓰레드로컬 예제이다:


public class ThreadLocalExample {


    public static class MyRunnable implements Runnable {

        private ThreadLocal<Integer> threadLocal =
               new ThreadLocal<Integer>();

        @Override
        public void run() {
            threadLocal.set( (int) (Math.random() * 100D) );
    
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
    
            System.out.println(threadLocal.get());
        }
    }


    public static void main(String[] args) {
        MyRunnable sharedRunnableInstance = new MyRunnable();

        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);

        thread1.start();
        thread2.start();

        thread1.join(); //wait for thread 1 to terminate
        thread2.join(); //wait for thread 2 to terminate
    }

}


이 예제는 Runnable 인스턴스인 MyRunnable 클래스의 인스턴스를 1개 생성하여 두 개의 다른 쓰레드에게 전달한다. 각 쓰레드는 run() 메소드를 호출하고, ThreadLocal 인스턴스에 각자 다른 값을 세팅한다. 만약 여기서 ThreadLocal 객체가 없고 set() 메소드 호출부가 동기화(synchronized)되어 있다면, 두 번째 쓰레드의 값이 첫 번째 쓰레드가 세팅한 값을 덮어쓸 것이다.


하지만 여기선 ThreadLocal 객체가 있고, 두 쓰레드는 서로의 값에 접근하지 않은 채로 각자의 값을 가진다.



InheritableThreadLocal


InheritableThreadLocal 클래스는 ThreadLocal 클래스의 서브클래스이다. ThreadLocal 은 모든 쓰레드가 각자의 값을 가지게 하지만, InheritableThreadLocal 은 자신에게 세팅된 값을, 값을 세팅한 쓰레드는 물론이고 이 쓰레드에 의해 생성된 자식 쓰레드들도 볼 수 있도록 한다.



'Java > Concurrency' 카테고리의 다른 글

데드락(Deadlock)  (0) 2017.04.09
쓰레드 시그널링  (0) 2017.04.09
자바 volatile 키워드  (2) 2017.04.09
자바 동기화 블록(Java Synchronized Blocks)  (0) 2017.04.09
자바 메모리 모델  (2) 2017.04.09

이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/volatile.html



 자바 volatile 키워드는 자바 코드의 변수를 '메인 메모리에 저장' 할 것을 명시하기 위해 쓰인다. 정확히 말해서, 모든 volatile 변수는 컴퓨터의 메인 메모리로부터 읽히고, volatile 변수에 대한 쓰기 작업은 메인 메모리로 직접 이루어진다. - CPU 캐시가 쓰이지 않는다.


실제로는 자바 5 부터 volatile 키워드는 메인 메모리로부터 읽고 쓰는 작업 이상의 것을 보장한다. 다음 섹션에서 이에 대해 설명한다.



volatile 의 가시성 보장


volatile 키워드는 쓰레드들에 대한 변수의 변경의 가시성을 보장한다. 이는 다소 추상적으로 들릴 수 있는데, 상세히 들어가보자.


멀티쓰레드 어플리케이션에서의 non-volatile 변수에 대한 작업은 성능상의 이유로 CPU 캐시를 이용한다. 둘 이상의 CPU가 탑제된 컴퓨터에서 어플리케이션을 실행한다면, 각 쓰레드는 변수를 각 CPU의 캐시로 복사하여 읽어들인다. 



non-volatile 변수에 대한 작업은 JVM 이 메인 메모리로부터 CPU 캐시로 변수를 읽어들이거나, CPU 캐시로부터 메인 메모리에 데이터를 쓰거나 할 때에 대한 어떠한 보장도 하지 않는다. 이는 몇가지 문제를 야기할 수 있는데, 다음 섹션을 보자.


둘 이상의 쓰레드가 다음과 같은 공유 객체로 접근하는 경우를 생각해보자.


public class SharedObject {

    public int counter = 0;

}


Thread1 은 counter 변수를 증가시키고, Thread1 과 Thread2 가 때에 따라서 counter 변수를 읽는다.


만일 counter 변수에 volatile 키워드가 없다면, counter 변수가 언제 CPU 캐시에서 메인 메모리로 쓰일지(written) 보장할 수 없다. CPU 캐시의 counter 변수와 메인 메모리의 counter 변수가 다른 값을 가질 수 있다는 것이다.



쓰레드가 변경한 값이 메인 메모리에 저장되지 않아서 다른 쓰레드가 이 값을 볼 수 없는 상황을 '가시성' 문제라 한다. 한 쓰레드의 변경(update)이 다른 쓰레드에게 보이지 않는다.


counter 변수에 volatile 키워드를 선언한다면 이 변수에 대한 쓰기 작업은 즉각 메인 메모리로 이루어질 것이고, 읽기 작업 또한 메인 메모리로부터 다이렉트로 이루어질 것이다.


public class SharedObject {

    public volatile int counter = 0;

}


volatile 선언은 다른 쓰레드의 쓰기 작업에 대한 가시성을 보장한다.



volatile 키워드의 Happens-Before 보장


자바 5 에서부터 volatile 키워드는 변수에 대한 읽기/쓰기 작업의 메인 메모리 사용을 보장하는 것 이상의 것을 보장한다고 했는데, 이는 다음과 같다.


  • It Thread A writes to a volatile variable and Thread B subsequently reads the same volatile variable, then all variables visible to Thread A before writing the volatile variable, will also be visible to Thread B after it has read the volatile variable.

  • The reading and writing instructions of volatile variables cannot be reordered by the JVM (the JVM may reorder instructions for performance reasons as long as the JVM detects no change in program behaviour from the reordering). Instructions before and after can be reordered, but the volatile read or write cannot be mixed with these instructions. Whatever instructions follow a read or write of a volatile variable are guaranteed to happen after the read or write.


이 문장에 대해서는 더 깊은 설명이 필요하다.


한 쓰레드가 volatile 변수를 수정할 때, 단지 이 volatile 변수만이 메인 메모리로 저장되는 것이 아니라, 이 쓰레드가 volatile 변수를 수정하기 전에 수정한 모든 변수들이 함께 메인 메모리에 저장(flushed)된다. 그리고 쓰레드가 volatile 변수를 메인 메모리에서 읽어들일 때, volatile 변수를 수정하면서 메인 메모리로 함께 저장된 다른 모든 변수들도 메인 메모리로부터 함께 읽어들여진다.


예제를 보자:


Thread A:
    sharedObject.nonVolatile = 123;
    sharedObject.counter     = sharedObject.counter + 1;

Thread B:
    int counter     = sharedObject.counter;
    int nonVolatile = sharedObject.nonVolatile;


Thread A 가 non-volatile 변수인 sharedObject.nonVolatile 을, volatile 변수인 sharedObject.counter 를 읽기 전에 수정하기 때문에, 두 변수 sharedObject.nonVolatile 과 sharedObject.counter 는 Thread A 가 sharedObject.counter(volatile 변수) 를 수정할 때 메인 메모리로 함께 저장된다.


Thread B 는 sharedObject.counter 를 읽으면서 시작하는데, 이 때 sharedObject.nonVolatile 도 메인 메모리에서 CPU 캐시로 읽어온다. Thread B 가 sharedObject.nonVolatile 변수를 읽었을 때 이 변수의 값은 Thread A 에 의해 수정된 값이 읽혀진다.


개발자들은 쓰레드간의 변수의 가시성의 최적화를 보장하기 위해 이 확장된 가시성을 활용할 수 있다. 모든 변수에 volatile 을 선언하는 대신, 단 하나, 혹은 몇몇의 변수에만 volatile 을 선언하면 된다. 여기 Exchanger 클래스를 보자:


public class Exchanger {

    private Object   object       = null;
    private volatile boolean hasNewObject = false;

    public void put(Object newObject) {
        while(hasNewObject) {
            //wait - do not overwrite existing new object
        }
        object = newObject;
        hasNewObject = true; //volatile write
    }

    public Object take(){
        while(!hasNewObject){ //volatile read
            //wait - don't take old object (or null)
        }
        Object obj = object;
        hasNewObject = false; //volatile write
        return obj;
    }
}


Thread A 는 시간에 따라 put() 을 호출하여 객체를 대입하고, Thread B 는 take() 를 호출하여 객체를 읽어 반환한다. 두 쓰레드가 맡은대로 Thread A 는 put() 만을, Thread B 는 take() 만을 호출하는 한, 이 Exchanger 클래스는 synchronized 블록 없이도 volatile 변수를 이용해 문제 없이 작동할 수 있다.


하지만 이 코드에서 만일 JVM 이 코드 실행 동작을 바꾸지 않으면서 코드를 재정리할 수 있다면, JVM 은 성능 향상을 위해 코드를 재정리하려 할 것이다. JVM 이 put() 과 take() 의 읽기와 쓰기 작업의 순서를 바꾼다면 어떻게 될까? 


put() 의 실행 코드가 다음과 같이 바뀔 수(재정리) 있다고 생각해보자.


while(hasNewObject) {
    //wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;


volatile 변수 hasNewObject 로의 쓰기 작업이 object 가 세팅되기 전으로 바뀌었다. JVM 의 시각으로 이것은 완전히 유효한 코드이다. 두 쓰기 작업은 서로에게 의존하지 않는다.


그러나 이 재정리는 object 변수의 가시성에 손상을 줄 수 있다. 먼저, Thread B 는 Thread A 가 실제로 object 에 newObject 를 세팅하기도 전에 hasNewObject 값을 true 로 읽을 수가 있다. 둘째로, 새 객체가 세팅된 object 변수가 어느 시점에 메인 메모리로 저장될지에 대한 보장이 없다.


이러한 상황을 피하기 위해, volatile 키워드는 "happends before guarantee" 성질을 갖는데, 이것은 volatile 변수에 대한 읽기/쓰기 명령은 JVM 에 의해 재정리되지 않음을 보장한다는 의미이다. volatile 변수에 대한 읽기/쓰기 명령을 기준으로, 이 변수 전에 존재하는 다른 명령들은 자기들끼리 얼마든지 재정리 될 수 있다. 그리고 이 변수 뒤에 존재하는 다른 명령들 또한 자기들끼리 재정리 될 수 있다. 다만, volatile 변수에 대한 명령 이전/이후에 존재한다는 그 전제는 반드시 지켜진다.


혼동될 수 있으니 다음 예제와 설명을 자세히 보자:


sharedObject.nonVolatile1 = 123;
sharedObject.nonVolatile2 = 456;
sharedObject.nonVolatile3 = 789;

sharedObject.volatile     = true; //a volatile variable

int someValue1 = sharedObject.nonVolatile4;
int someValue2 = sharedObject.nonVolatile5;
int someValue3 = sharedObject.nonVolatile6;


JVM 은 -volatile 변수에 쓰기 명령이 실행되기 전에 실행되는- 처음 3개의 명령을 재정리할 수 있다. 다만 이 명령들은 반드시 volatile 변수에 쓰기 명령이 실행되기 전에 실행되어야 한다.


같은 맥락으로, JVM 은 volatile 변수에 쓰기 명령이 실행 먼저 실행되기만 한다면, 마지막 3개의 명령을 재정리할 수 있다. 다만 이 명령들은 반드시 volatile 변수에 쓰기 명령이 실행된 후에 실행되어야 한다.


이 성질을 Java volatile happends before guarantee 라 한다.



volatile 은 만병통치약이 아니다


volatile 선언이 변수의 읽기/쓰기 명령을 메인 메모리로부터 수행한다는 것을 보장한다고 할지라도, volatile 선언으로 해결할 수 없는 상황들은 여전히 남아있다.


위의 예제들 중, 공유 변수인 counter 가 있고 Thread1 만이 이 변수를 수정하고, Thread2 만이 이 변수를 읽는 이런 상황에서라면 volatile 선언이 변수의 가시성을 보장해준다.


멀티쓰레드 환경에서, volatile 공유 변수에 세팅된 새로운 값이 이 변수가 가지고 있던 이전의 값에 의존적이지 않는다면, 다수의 쓰레드들이 volatile 공유 변수를 수정하면서도 메인 메모리에 존재하는 정확한 값을 읽을 수 있다. 달리 말하자면, 만일 volatile 공유 변수를 수정하는 한 쓰레드가 이 변수의 다음 값을 알아내기 위해 이전의 값을 필요로 하지 않는다면 말이다.


쓰레드가 volatile 변수의 초기값을 필요로 할 때, 그리고 volatile 변수의 새 값이 이 초기값을 근거로 할 때, volatile 선언은 더이상 정확한 가시성을 보장하기 못한다. volatile 변수를 읽고, 새 값을 쓰는 사이의 짧은 갭은 경합 조건을 일으킨다 - 다수의 쓰레드가 volatile 변수의 값을 똑같이 읽고, 새 값을 생성하여 이를 메인 메모리로 저장하는 동안, 서로의 값을 덮어쓸 수 있다.


다수의 쓰레드가 같은 counter 값을 증가시키는 상황이 바로 volatile 변수가 불완전해지는 상황이다. 다음 섹션에서 이에 대해 자세히 설명한다.


상황을 가정해보자. Thread1 이 공유 변수인 counter 의 값 0 을 자신의 CPU 캐시에 읽어들이고, 이 값을 1 증가시키고, 아직 메인 메모리로 저장하지 않았다. 그리고 Thread2 는 같은 counter 변수를 메인 메모리에서 자신의 CPU 캐시로 읽어들였고, 이 때의 값은 여전히 0 이다. 그리고 Thread2 도 이 값을 1 증가시키고 메인 메모리로 저장하지는 않았다. 다음은 이 상황을 묘사한 다이어그램이다.



Thread1 과 Thread2 는 사실상 동기화에서 완전히 멀어진 상태이다. counter 변수의 실제 값은 2 가 되어야 하지만, 두 쓰레드는 각자의 값, 1 을 자신들의 캐시에 가지고 있다. 그리고 메인 메모리의 값은 아직 0 이다. 이 상황에서 쓰레드들이 캐시에 가진 변수의 값은 메인 메모리에 저장한다고 해도, counter 의 값은 1 이 된다. 잘못된 상황이다.



volatile 을 사용하기 적절한 때는?


앞서 설명했듯, 두 쓰레드가 공유 변수에 읽기/쓰기 를 실행할 때, volatile 선언은 충분치 않다. 이런 상황에서는 변수값의 읽기/쓰기 명령의 원자성을 보장하기 위해 synchronized 를 써야한다. 변수를 읽고 쓸 때 volatile 선언은 변수에 접근하는 쓰레드들을 블록시키지 않는다. 이런 임계 영역에는 synchronized 키워드가 필요하다.


synchronized 블록을 대체하는 다른 것을 찾는다면, java.util.concurrent 패키지의 많은 원자성 데이터 타입들을 사용할 수도 있다. 에를 들자면 AtomicLong 이나 AtomicReference 와 같은 것들이다.


한 변수를 두고 오직 한 쓰레드만 이 변수에 읽기/쓰기 작업을 하고, 다른 쓰레드들은 읽기 작업만 하는 상황에서라면 이 때는 volatile 선언이 유효하다. 읽기 작업을 수행하는 쓰레드들은 언제나 이 변수의 가장 최근 수정된 값을 봐야하고, volatile 은 이를 보장해준다.


그리고 volatile 은 32비트와 64비트 변수에서 효과를 볼 수 있다.



volatile 의 성능에 대한 고찰


volatile 변수의 읽기/쓰기는 메인 메모리를 이용한다. 메인 메모리로부터 데이터를 읽고 쓰는 작업은 CPU 캐시를 이용하는 것 보다 많은 비용이 요구된다. 또한 volatile 선언은 JVM 의 성능 향상을 위한 기술인, 코드 재정리를 막기도 한다. 그러므로 volatile 키워드는 변수의 가시성 보장이 반드시 필요한 경우에만 사용되어야 한다.


이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/synchronized.html



자바 동기화 블록은 메소드나 블록 코드에 동기화 영역을 표시하며 자바에서 경합 조건을 피하기 위한 방법으로 쓰인다.



자바 synchronized 키워드


 자바 코드에서 동기화 영역은 synchronizred 키워드로 표시된다. 동기화는 객체에 대한 동기화로 이루어지는데(synchronized on some object), 같은 객체에 대한 모든 동기화 블록은 한 시점에 오직 한 쓰레드만이 블록 안으로 접근하도록 - 실행하도록 - 한다. 블록에 접근을 시도하는 다른 쓰레드들은 블록 안의 쓰레드가 실행을 마치고 블록을 벗어날 때까지 블록(blocked) 상태가 된다.


synchronized 키워드는 다음 네 가지 유형의 블록에 쓰인다.


  1. 인스턴스 메소드
  2. 스태틱 메소드
  3. 인스턴스 메소드 코드블록
  4. 스태틱 메소드 코드블록


어떤 동기화 블록이 필요한지는 구체적인 상황에 따라 달라진다.



인스턴스 메소드 동기화


다음은 동기화 처리된 인스턴스 메소드이다.


public synchronized void add(int value){
      this.count += value;
  }


메소드 선언문의 synchronized 키워드를 보자. 이 키워드의 존재가 이 메소드의 동기화를 의미한다.


인스턴스 메소드의 동기화는 이 메소드를 가진 인스턴스(객체)를 기준으로 이루어진다. 그러므로, 한 클래스가 동기화된 인스턴스 메소드를 가진다면, 여기서 동기화는 이 클래스의 한 인스턴스를 기준으로 이루어진다. 그리고 한 시점에 오직 하나의 쓰레드만이 동기화된 인스턴스 메소드를 실행할 수 있다. 결국, 만일 둘 이상의 인스턴스가 있다면, 한 시점에, 한 인스턴스에, 한 쓰레드만 이 메소드를 실행할 수 있다. 


인스턴스 당 한 쓰레드이다. 



스태틱 메소드 동기화


스태틱 메소드의 동기화는 인스턴스 메소드와 같은 방식으로 이루어진다.


  public static synchronized void add(int value){
      count += value;
  }

역시 선언문의 synchronized 키워드가 이 메소드의 동기화를 의미한다.


스태틱 메소드 동기화는 이 메소드를 가진 클래스의 클래스 객체를 기준으로 이루어진다. JVM 안에 클래스 객체는 클래스 당 하나만 존재할 수 있으므로, 같은 클래스에 대해서는 오직 한 쓰레드만 동기화된 스태틱 메소드를 실행할 수 있다.


만일 동기화된 스태틱 메소드가 다른 클래스에 각각 존재한다면, 쓰레드는 각 클래스의 메소드를 실행할 수 있다.


클래스 -쓰레드가 어떤 스태틱 메소드를 실행했든 상관없이- 당 한 쓰레드이다.



인스턴스 메소드 안의 동기화 블록


동기화가 반드시 메소드 전체에 대해 이루어져야 하는 것은 아니다. 종종 메소드의 특정 부분에 대해서만 동기화하는 편이 효율적인 경우가 있다. 이럴 때는 메소드 안에 동기화 블록을 만들 수 있다.


  public void add(int value){

    synchronized(this){
       this.count += value;   
    }
  }


이렇게 메소드 안에 동기화 블록을 따로 작성할 수 있다. 메소드 안에서도 이 블록 안의 코드만 동기화하지만, 이 예제에서는 메소드 안의 동기화 블록 밖에 어떤 다른 코드가 존재하지 않으므로, 동기화 블록은 메소드 선언부에 synchronized 를 사용한 것과 같은 기능을 한다.


동기화 블록이 괄호 안에 한 객체를 전달받고 있음에 주목하자. 예제에서는 'this' 가 사용되었다. 이는 이 add() 메소드가 호출된 객체를 의미한다. 이 동기화 블록 안에 전달된 객체를 모니터 객체(a monitor object) 라 한다. 이 코드는 이 모니터 객체를 기준으로 동기화가 이루어짐을 나타내고 있다. 동기화된 인스턴스 메소드는 자신(메소드)을 내부에 가지고 있는 객체를 모니터 객체로 사용한다.


같은 모니터 객체를 기준으로 동기화된 블록 안의 코드를 오직 한 쓰레드만이 실행할 수 있다.


다음 예제의 동기화는 동일한 기능을 수행한다.


  public class MyClass {
  
    public synchronized void log1(String msg1, String msg2){
       log.writeln(msg1);
       log.writeln(msg2);
    }

  
    public void log2(String msg1, String msg2){
       synchronized(this){
          log.writeln(msg1);
          log.writeln(msg2);
       }
    }
  }


한 쓰레드는 한 시점에 두 동기화된 코드 중 하나만을 실행할 수 있다. 여기서 두 번째 동기화 블록의 괄호에 this 대신 다른 객체를 전달한다면, 쓰레드는 한 시점에 각 메소드를 실행할 수 있다. -동기화 기준이 달라지므로.



스태틱 메소드 안의 동기화 블록


다음 예제는 스태틱 메소드에 대한 것이다. 두 메소드는 각 메소드를 가지고 있는 클래스 객체를 동기화 기준으로 잡는다.


  public class MyClass {

    public static synchronized void log1(String msg1, String msg2){
       log.writeln(msg1);
       log.writeln(msg2);
    }

  
    public static void log2(String msg1, String msg2){
       synchronized(MyClass.class){
          log.writeln(msg1);
          log.writeln(msg2);  
       }
    }
  }


같은 시점에 오직 한 쓰레드만 이 두 메소드 중 어느 쪽이든 실행 가능하다. 두 번째 동기화 블록의 괄호에 MyClass.class 가 아닌 다른 객체를 전달한다면, 쓰레드는 동시에 각 메소드를 실행할 수 있다.



자바 동기화 예제


두 쓰레드가 실행되고 이 쓰레드들은 같은 Counter 인스턴스의 add() 메소드를 호출한다. 한 시점에 오직 한 쓰레드만 add() 메소드를 호출할 수 있을 것이다 - 같은 인스턴스를 동기화 기준으로 잡았기 때문에. 왜냐하면 add() 메소드는 자신을 가진 클래스의 인스턴스를 기준으로 동기화되고 있다.


  public class Counter{
     
     long count = 0;
    
     public synchronized void add(long value){
       this.count += value;
     }
  }
  public class CounterThread extends Thread{

     protected Counter counter = null;

     public CounterThread(Counter counter){
        this.counter = counter;
     }

     public void run() {
	for(int i=0; i<10; i++){
           counter.add(i);
        }
     }
  }
  public class Example {

    public static void main(String[] args){
      Counter counter = new Counter();
      Thread  threadA = new CounterThread(counter);
      Thread  threadB = new CounterThread(counter);

      threadA.start();
      threadB.start(); 
    }
  }


두 쓰레드가 생성되었고, 같은 Counter 인스턴스가 각 쓰레드의 생성자로 전달되었다. Counter.add() 메소드는 인스턴스 메소드이기 때문에, Counter.add() 메소드는 생성자로 전달된 Counter 인스턴스를 기준으로 동기화된다. 이로써 한 시점에 두 쓰레드 중 한 쓰레드만이 add() 메소드를 호출할 수 있게 되었다. 한 쓰레드가 add() 메소드를 실행하는 동안 다른 쓰레드는 이 실행이 끝나고 실행 쓰레드가 동기화 블록을 빠져나갈 때까지 기다리게 된다.


여기서 만일 두 쓰레드가 서로 다른 Counter 인스턴스를 전달받았다면, add() 메소드는 동시에 호출될 수 있을 것이다. add() 메소드의 호출은 서로 다른 객체에 의해 이루어지고, 당연히 동기화의 기준이 달라진다. 여기에 쓰레드가 블록 상태에 놓이는 일은 없다.


  public class Example {

    public static void main(String[] args){
      Counter counterA = new Counter();
      Counter counterB = new Counter();
      Thread  threadA = new CounterThread(counterA);
      Thread  threadB = new CounterThread(counterB);

      threadA.start();
      threadB.start(); 
    }
  }


threadA, threadB 는 더이상 같은 인스턴스를 참조하지 않는다. add() 메소드는 각자의 인스턴스를 기준으로 동기화된다. counterA 에 대한 add() 메소드 호출은 counterB 의 add() 를 블록시키지 않는다.



자바 컨커런시 유틸리티


synchronized 매카니즘은 다수의 쓰레드에게 공유되는 객체로의 접근에 대한 자바의 첫 번째 동기화 매카니즘이었다. 이 매카니즘이 아주 훌륭하지는 못했기 때문에, 이보다 한층 나은 동시성 컨트롤을 위해 자바 5 에서는 컨커런시 유틸리티 클래스들이 출현하게 된다.



이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/java-memory-model.html


 자바 메모리 모델은 컴퓨터의 메모리를(RAM)을 통해 JVM이 어떻게 작동되는지 보여준다. JVM은 그 자체로 하나의 컴퓨터 모델이고, 때문에 이 모델은 내부적으로 메모리 모델을 포함한다 - AKA the Java memory model.


제대로 작동하는 컨커런트 프로그램을 구상할 때 자바 메모리 모델을 이해하는 일은 매우 중요하다. 자바 메모리 모델은 어떻게, 그리고 언제 쓰레드들이 공유 변수의 값 - 다른 쓰레드들에 의해 쓰여진 - 을 볼 수 있는지 명시한다. 그리고, 공유 변수로의 접근을 어떻게 동기화하는지 알려준다. (The Java memory model specifies how and when different threads can see values written to shared variables by other threads, and how to synchronize access to shared variables when necessary.)


오리지널 자바 메모리 모델은 완전하지 못했기 때문에 자바 메모리 모델은 자바 1.5 에서 수정되었고, 이 버전의 자바 메모리 모델은 자바 8 에서도 여전히 사용된다.



자바 메모리 모델의 내부구조


 JVM에서 내부적으로 사용되는 자바 메모리 모델은 메모리를 쓰레드 스택(들)과 힙(heap)으로 나눈다. 아래의 다이어그램은 자바 메모리 모델을 논리적인 관점에서 보여준다.



JVM에서 돌아가는 각 쓰레드들은 자신들만의 쓰레드 스택을 가진다. 이 쓰레드 스택에는 쓰레드가 호출한 메소드들의 현재 실행되는 지점(코드)을 보여주기 위한 정보가 들어있다. 이것을 '콜 스택(call stack)' 이라 부를 것이다. 이 콜 스택은 쓰레드가 코드를 실행함에 따라 변화한다.


쓰레드 스택은 쓰레드에 의해 실행되는 메소드(콜 스택에 존재하는 모든 메소드)가 가진 지역변수 또한 가지고 있다. 하나의 쓰레드는 오직 자신의 쓰레드 스택에만 접근할 수 있다. 쓰레드에 의해 생성된 지역변수는 자신을 생성한 쓰레드 외에 다른 쓰레드들에게는 보이지 않는다. 만일 두 쓰레드가 완벽하게 같은 코드를 실행한다 하더라도, 이 두 쓰레드는 각자의 쓰레드 스택에 코드의 지역변수를 저장한다. 이로 인해 각 쓰레드는 자신이 실행하는 코드의 지역변수에 대한 자기만의 버전을 갖는다.


모든 기본형(레퍼(wrapper) 타입:boolean, byte, short, char, int ,long, float, double) 지역변수는 쓰레드 스택에 저장되며, 변수를 저장한 쓰레드가 아닌 다른 쓰레드는 여기에 접근할 수 없다. 쓰레드는 기본형 변수의 복사본을 다른 쓰레드에게 전달할 수 있지만, 기본형 지역변수의 원본을 공유하는 일은 불가능하다.


힙에는 자바 어플리케이션이 생성한 모든 객체들이 들어있다. 여기서 어떤 객체가 어떤 쓰레드에 의해 생성되었는가는 아무런 상관이 없다. 여기에는 기본형 타입의 객체(Byte, Integer, Long 기타 등등)도 포함되며, 객체가 지역변수로 할당되었든, 아니면 다른 객체의 멤버 변수로 할당되었든, 객체는 여전히 힙에 저장된다.


다음 다이어그램은 쓰레드 스택에 저장된 지역변수들과 콜 스택, 그리고 힙에 저장된 객체들을 보여준다.


기본형 지역변수는 완전하게 쓰레드 스택에 저장되고, 객체를 참조하는 지역변수의 경우는 변수는 쓰레드 스택에 오지만 객체는 힙 영역에 저장된다.

객체는 메소드를 가질 수 있고 이 메소드는 지역변수를 가질 수 있는데, 이런 지역변수도 -자신이 소속된 메소드를 가진 객체가 힙 영역에 있다 하더라도- 역시 다른 지역변수와 마찬가지로 쓰레드 스택에 저장된다.


객체의 멤버변수는 그 타입이 기본형이든 참조형이든 상관없이 객체와 함께 힙에 저장된다.


스태틱 클래스 변수 또한 클래스 정의와 함께 힙에 저장된다.


힙에 위치한 객체는 이 객체로의 참조를 지닌 모든 쓰레드들이 접근 가능하다. 한 쓰레드가 어떤 객체로 접근할 때, 이 쓰레드는 이 객체와 함께 이 객체가 내부에 지닌 멤버변수로의 접근 또한 가능해진다. 만일 두 쓰레드가 동시에 같은 객체로 접근한다면 이 쓰레드들은 이 한 객체의 멤버변수로 접근하게 되지만, 각 쓰레드는 자신의 쓰레드 스택에 지역변수로 이 객체의 본사본을 가지게 된다.


다음 다이어그램을 보자.



두 쓰레드는 지역변수들을 가지고 있고, 여기서 한 지역변수(Local variable 2)는 힙에 위치한 한 객체를 가리킨다(Object 3). 두 쓰레드는 같은 하나의 객체를 가리키는 서로 다른 지역변수를 가지게 된다.


그런데 여기서 공유된 객체(Object 3)는 다른 객체(Object 2, Object 4)로의 참조를 가지고 있다(객체가 힙 영역에서 다른 객체를 참조하고 있으니 이 참조를 지닌 변수는 당연히 멤버변수이다).


다이어그램에는 각각 다른 객체를 참조하고 있는(Local variable 1 -> Object 1/5) 또다른 변수(Local variable 1 in methodTwo())들도 존재한다. 이들 변수가 참조하고 있는 객체는 서로 다른 객체이다. 이론적으로는 이 두 쓰레드가 Object 1/5 에 다른 참조만 가지고 있다면 양쪽 모두 접근 가능하지만, 다이어그램상으로 두 쓰레드는 각각 한 객체의 참조만을 가지고 있다.


위의 다이어그램을 재현할 수 있는 자바 코드는 어떤 것일까?


public class MyRunnable implements Runnable() {

    public void run() {
        methodOne();
    }

    public void methodOne() {
        int localVariable1 = 45;

        MySharedObject localVariable2 =
            MySharedObject.sharedInstance;

        //... do more with local variables.

        methodTwo();
    }

    public void methodTwo() {
        Integer localVariable1 = new Integer(99);

        //... do more with local variable.
    }
}
public class MySharedObject {

    //static variable pointing to instance of MySharedObject

    public static final MySharedObject sharedInstance =
        new MySharedObject();


    //member variables pointing to two objects on the heap

    public Integer object2 = new Integer(22);
    public Integer object4 = new Integer(44);

    public long member1 = 12345;
    public long member1 = 67890;
}

두 개의 쓰레드가 run() 메소드를 실행한다면, 위의 다이어그램이 재현될 것이다. run() 메소드는 methodOne() 메소드를 호출하고, methodOne() 은 methodTwo() 메소드를 호출한다.


methodOne() 은 기본형 지역변수(int localVariable1)와 참조형 지역변수(MySharedObject localVariable2)를 함께 선언했다.

 

methodOne() 을 호출하는 각 쓰레드는 localVariable1, localVariable2의 본사본을 각각 자신의 쓰레드 스택에 생성한다. 기본형 변수인 localVariable1은 각 쓰레드 스택에 완전히 개별적으로 위치하게 되고, 이 때 한 쓰레드에서 이 변수의 값을 변경하여도 이 변경은 이 쓰레드 자신만이 볼 수 있으며, 다른 쓰레드는 이를 볼 수 없게 된다 - 다른 쓰레드 스택에서 발생한 일이기에.


그런데 localVariable2의 경우는 다르다. 이 변수는 각각 다른 쓰레드 스택에 존재하지만, 힙에 위치한 한 같은 객체를 참조한다. MySharedObject 인스턴스는 스태틱 변수에 의해 참조되고, localVariable2 를 이 스태틱 변수를 통해 이 인스턴스를 참조한다. 스태틱 변수는 JVM의 힙 영역에 오직 하나만 존재할 수 있고, 결국 두 쓰레드가 자신들의 스택에 각각 가진 localVariable2는 같은 인스턴스를 참조하게 되는 것이다. 이 MySharedObject가 다이어그램의 Object 3 에 해당된다.


MySharedObject 클래스가 두 멤버변수를 어떤식으로 포함하고 있는지도 보자. 두 멤버변수는 MySharedObject 를 따라서 힙 영역에 저장되어 있다. 이 변수들이 참조하는 Integer 타입 객체들은 각각 다이어그램의 Object 2, Object 4 에 해당된다.


methodTwo() 메소드가 지역변수 localVariable1을 생성하는 부분도 지켜볼만한 부분이다. 이 지역변수는 Integer 타입 참조변수이고, methodTwo() 는 이 변수에 Integer 인스턴스를 세팅한다. localVariable1 의 참조는 쓰레드가 methodTwo() 를 호출할 때마다 새로 생성될 것이다. 앞의 Object 2, Object 4는 각각 힙에 저장되지만, methodTwo() 의 localVariable1 은 메소드가 호출될 때마다 새로 생성되기 때문에 두 쓰레드가 메소드를 실행할 때마다 서로 개별적인 Integer 인스턴스가 생성될 것이다. methodTwo() 의 localVariable1 은 다이어그램의 Object 1, Object 5 에 해당된다.


MySharedObject 의 나머지 변수인 long member1 을 보자. 이 변수는 멤버변수이기 때문에 MySharedObject 객체를 따라서 힙에 저장된다. 결국 쓰레드 스택에 저장되는 변수는 오직 지역변수 뿐이다.



하드웨어 메모리 아키텍처


요즘의 하드웨어 메모리 아키텍처는 자바 메모리 모델의 내부구조와 다소 차이가 있다. 자바 메모리 모델이 하드웨어와 어떻게 작동하는지 이해하기 위해, 하드웨어 메모리 아키텍처를 이해하는 일 또한 중요하다. 이 섹션에서는 일반적인 하드웨어 메모리 아키텍처에 대해 기술하고, 다음 섹션에서는 자바 메모리 모델이 하드웨어 메모리 아키텍처와 어떻게 작동하는지에 대해 다룰 것이다.


근래의 하드웨어 메모리 아키텍처를 단순한 다이어그램으로 표현해보면 다음과 같다.


근래의 컴퓨터에는 흔히 2개 이상의 CPU 가 탑제되어 있다. 그리고 이런 CPU는 멀티코어로 작동하기도 한다. 여기서 중요한 점은, 오늘날의 컴퓨터는 2개 이상의 쓰레드를 동시에 돌리는 일이 가능하다는 점이다. 각 CPU는 언제든 주어진 시간에 한 쓰레드를 돌릴 수 있다. 이는 당신의 자바 어플리케이션이 멀티쓰레드를 지원한다면, 어플리케이션 안에서 CPU당 하나의 쓰레드가 동시에 작동될 수 있다는 것을 의미한다.


CPU는 CPU 메모리에 기본적으로 존재하는 레지스터들을 포함하고 있다. CPU는 이 레지스터들을 통해 메인 메모리 안의 데이터를 작업할 때보다 훨씬 빠른 속도로 명령을 수행할 수 있다. 이것이 가능한 이유는 CPU가 레지스터에 접근하는 속도가 메인 메모리에 접근하는 속도보다 훨씬 빠른 데에 있다.


CPU는 또한 케시 메모리 영역을 가지고 있는데, 요즘 대부분의 CPU는 어느 정도 크기의 케시 메모리 영역을 가지고 있다. CPU는 케시 메모리 영역을 통해 명령을 수행하는 일이 가능한데, 이 속도 또한 메인 메모리에 접근하는 속도보다 훨씬 빠르다. 하지만 이 속도는 일반적으로 앞서 언급된, CPU가 내부에 가진 레지스터만큼 빠르지는 않다. 즉, CPU 캐시 메모리는 내부 레지스터와 메인 메모리의 중간쯤에 위치해 있다. 어떤 CPU는 이 캐시 메모리 영역을 Level1, Level2 방식으로 멀티플하게 가지기도 하는데, 이는 자바 메모리 모델과의 상호작용을 이해하는 데에 그다지 중요하지 않다. 여기서 기억해야 할 중요한 사실은 CPU는 어느 정도의 캐시 메모리 영역을 가진다는 점이다.


컴퓨터는 메인 메모리 영역(RAM)을 가지고 있다. 모든 CPU는 이 메인 메모리에 접근 가능하며, 일반적으로 이 메모리 영역은 CPU의 캐시 메모리보다 훨씬 크다.


보통, CPU가 메인 메모리로의 접근을 필요로 할 때, CPU는 메인 메모리의 일부분을 CPU 캐시로 읽어들일다(RAM -> Cache). 그리고 이 캐시의 일부분을 자신의 내부 레지스터로 다시 읽어들이고(Cache -> CPU Registers), 이 읽어들인 데이터로 명령을 수행한다. 후에 이 데이터를 다시 메인 메모리에 저장(writing)하기 위해서는 데이터를 읽어들일 때의 과정을 역순으로 밟는다. 작업 결과를 레지스터에서 캐시로 보내고, 적절한 시점에 캐시에서 메인 메모리로 보낸다.


캐시가 데이터를 메인 메모리로 보내는 적절한 시점이란, CPU가 캐시 메모리에 다른 데이터를 저장해야 할 때이다. CPU 캐시는 자신의 메모리에 데이터를 한 번 저장할 때(at a time), 메모리의 일부에 데이터를 저장해둘 수 있고, 또 일부분만을 보내는(flush) 일도 가능하다. 즉 캐시가 데이터를 읽거나 쓸 때, 반드시 한번에 캐시 메모리의 모든 데이터를 처리하지 않아도 된다는 말이다. 보통 캐시는 '캐시 라인(cache lines)' 이라고 불리는 작은 메모리 블록에 데이터를 갱신한다. 캐시 메모리에는 한 줄 이상의 캐시 라인을 읽어들일 수 있고, 반대로 한 줄 이상의 캐시 라인을 메인 메모리로 보낼(저장할) 수도 있다.



자바 메모리 모델과 하드웨어 메모리 아키텍처의 연결


 이미 언급된 바와 같이, 자바 메모리 모델과 하드웨어 메모리 아키텍처에는 차이가 있다. 하드웨어 메모리 아키텍처는 쓰레드 스택과 힙을 구분하지 않는다. 하드웨어에서 쓰레드 스택과 힙은 모두 메인 메모리에 위치한다. 그리고 쓰레드 스택과 힙의 일부분은 종종 CPU 캐시와 CPU 레지스터에도 나타날 수 있다. 다음 다이어그램이 이런 현상을 보여준다.



객체와 변수들은 컴퓨터의 다양한 메모리 영역에 존재할 수 있으며, 여기서 어떤 문제점들이 발생할 수 있다. 두가지 주요한 문제점은 다음과 같다.


  • 공유 변수에 대한 쓰레드 업데이트(쓰기 작업)의 가시성(visibility)
  • 공유 변수에의 읽기, 확인(checking), 쓰기(writing) 작업의 경합 조건


다음 섹션에서 이 두 문제점에 대해 설명한다.



공유 객체의 가시성(visibility)


 둘 이상의 쓰레드가 volatile 선언이나 synchronization을 사용하지 않은 상태로 한 객체를 공유한다면, 이 공유 객체로의 업데이트(쓰기) 작업은 작업을 실행하는 쓰레드 외에 다른 쓰레드에게는 이 객체의 변경이 보이지 않는 상황이 발생할 수 있다.


한 상황을 상상해보자. 이 공유 객체가 메인 메모리에 생성되었다. 그리고 한 CPU에서 실행되는 한 쓰레드가 이 객체를 CPU 캐시로 읽어들이고 객체의 어떤 값을 변경하였다. 이 CPU 캐시가 변경된 객체 정보를 메인 메모리로 보내지 않는 한, 공유 객체의 변경된 정보는 다른 CPU에서 실행되는 쓰레드는 볼 수 없다. 각 CPU는 자신들의 캐시에 한 공유 객체의 다른 버전 - 한쪽은 데이터가 변경된, 다른 한쪽은 변경되기 이전의 - 의 복사본을 가지게 된다.


다음 다이어그램은 이 상황을 그림으로 나타낸 것이다. 왼쪽의 CPU는 객체를 자신의 캐시에 읽어들인 후 객체의 변수값을 변경하였고, 아직 이 변경된 정보를 메인 메모리에 보내지 않은 상태이기 때문에, 오른쪽의 CPU에서는 이 변경 정보를 볼 수 없다.


이 문제에 대한 해결책으로 자바의 volatile 키워드를 사용할 수 있다. volatile 키워드는 특정 변수에 대한 읽기 작업은 반드시 메인 메모리로부터 수행하고, 쓰기 작업은 항상 메인 메모리에 즉각 반영하도록 강제할 수 있다.



경합 조건


 둘 이상의 쓰레드가 한 객체를 공유하는 상황에서 한 쓰레드가 이 객체의 변수값을 변경했을 때 경합 조건이 발생할 수 있다.


여기서도 어떤 상황을 상상해보자. 쓰레드 A 가 한 공유 객체의 변수, count 를 자신의 캐시에 읽어들였다. 그리고 다른 CPU에서 실행되는 쓰레드 B 역시 이 변수를 자신의 캐시에 읽어들였다. 그리고 쓰레드 A 는 이 변수값에 1을 더하였고, 쓰레드 B 도 같은 작업을 수행했다. 두 CPU 에서 각각 공유된 한 객체의 변수 count 에 두 번의 더하기 작업이 수행되어, 결과적으로 변수의 값은 +2 가 되었다.


이 작업이 순차적 - 동시적이 아닌 - 으로 수행되었다면 변수 count 의 값은 기존 값에서 +2 가 되어 메인 메모리에 저장될 것이다.


그러나 이 두 번의 작업은 동시에 수행되었다. 쓰레드 A 나 쓰레드 B 중 어느 쪽의 객체가 메인 메모리로 저장될지와는 관계없이, 메인 메모리에 저장될 값은 초기 값에서 1 만을 더한 값이 될 것이다. 다음 다이어그램은 이 경합 조건을 보여준다.



이 상황에 대한 해결책으로는 synchronized 블록을 사용할 수 있다. 이 블록은 한 시점에 오직 하나의 쓰레드만이 특정 코드 영역에 접근할 수 있도록 보장해준다. 또한 volatile 키워드처럼, 이 블록 안에서 접근되는 모든 변수들은 메인 메모리로부터 읽어들여지고, 쓰레드의 실행이 이 블록을 벗어나면 블록 안에서 수정된 모든 변수들이 즉각 메인 메모리로 반영될 수 있도록 해준다 - volatile 키워드가 선언되어 있든 없든.



이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/thread-safety.html


 경합 조건은 다수의 쓰레드가 같은 자원에 접근하고 이 자원에 쓰기 작업을 시도할 때 발생한다. 만일 쓰레드들이 같은 자원에 접근하더라도 읽기 작업을 시도할 때는 경합 조건이 발생하지 않는다.


우리는 쓰레드들이 어떤 객체의 상태를 변경(update)할 수 없도록 함스로써 이 객체에게 불변성(immutability)을 부여할 수 있다. 그리고 이렇게 할 때 이 객체는 쓰레드 세이프하게 된다.


public class ImmutableValue{

  private int value = 0;

  public ImmutableValue(int value){
    this.value = value;
  }

  public int getValue(){
    return this.value;
  }
}


여기서 value가 어떤 방식으로 ImmutableValue 클래스의 생성자로 넘겨지는지 보고, 이 클래스에 setter 메소드가 없다는 점에 주목하자. 한 번 만들어진 ImmutableValue 인스턴스의 value 값은 변경될 수 없다. value 는 불변(immutable)한다. getValue() 메소드를 호출해서 이 값을 읽을 수는 있지만, 바꿀 수는 없다.


ImmutableValue 인스턴스의 value 에 다른 값을 주고자 한다면, 새로운 인스턴스를 생성하여 반환하는 방법으로 구현할 수는 있다.


public class ImmutableValue{

  private int value = 0;

  public ImmutableValue(int value){
    this.value = value;
  }

  public int getValue(){
    return this.value;
  }

  
      public ImmutableValue add(int valueToAdd){
      return new ImmutableValue(this.value + valueToAdd);
      }
  
}


add() 메소드는 기존 value 값에 다른 값을 더하며 새로운 ImmutableValue 인스턴스를 반환한다. 기존 인스턴스의 value 값은 변하지 않는다.



참조는 쓰레드 세이프하지 않다!


한 객체가 불변하고 쓰레드 세이프하다 하더라도, 이 객체로의 참조는 쓰레드 세이프하지 않을 수 있다는 점을 기억해야 한다. 예제를 보자.


public class Calculator{
  private ImmutableValue currentValue = null;

  public ImmutableValue getValue(){
    return currentValue;
  }

  public void setValue(ImmutableValue newValue){
    this.currentValue = newValue;
  }

  public void add(int newValue){
    this.currentValue = this.currentValue.add(newValue);
  }
}


Calculator 클래스는 ImmutableValue 인스턴스의 참조를 지닌다. 이 클래스가 setValue()와 add() 메소드를 통해 어떻게 ImmutableValue 인스턴스의 값을 변경하는지 보자. 이렇게 Calculator 클래스가 내부적으로 불변하는 객체를 사용한다 하더라도, 여기에 불변성은 존재하지 않으며, 쓰레드 세이프하지 않다. 


다시 말해서, ImmutableValue 클래스는 쓰레드 세이프하다. 하지만 이 클래스의 사용은 쓰레드 세이프하지 않다. 이것은 불변성을 이용하여 쓰레드의 안정성을 구현하려 할 때 기억해야 할 사실이다. 


여기서 Calculator 클래스를 쓰레드 세이프하게 만들기 위해서는 getValue(), setValue(), add() 메소드에 synchronized 를 사용하는 방법이 있다.



이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/thread-safety.html


 다수의 쓰레드에 의한 동시 호출에서 안정성이 보장되는 코드를 쓰레드 세이프(thread safe)하다고 한다. 쓰레드 세이프한 코드에는 경합 조건이 없다. 경합 조건은 다수의 쓰레드가 공유 자원에 쓰기 작업을 시도할 때 발생하기 때문에, 자바 쓰레드가 실행될 때 어떤 자원을 공유하게 되는지 아는 것이 중요하다.



지역변수


 지역변수는 각 쓰레드의 스택에 저장된다. 이는 지역변수는 쓰레드간에 절대로 공유되지 않는다는 것을 의미한다. 즉, 모든 기본형 지역변수는 쓰레드 세이프하다.


public void someMethod(){

  long threadSafeInt = 0;

  threadSafeInt++;
}



지역 객체 참조


 지역에서의 객체 참조의 경우는 약간 다르다. 객체 참조는 각 쓰레드의 스택에 저장되지 않는다. 모든 객체는 공유된 메모리인 힙(heap)에 저장된다.


지역적으로 생성된 객체가 자신이 생성된 메소드에서 벗어나지 않는다면, 이 객체는 쓰레드 세이프하다. 이 객체는 다른 메소드로 넘겨질 수 있지만 그 어떤 메소드나 객체도 이 넘겨진 객체를 다른 쓰레드로 하여금 접근하도록 할 수는 없다.


다음은 쓰레드 세이프한 지역 객체의 예제이다.


public void someMethod(){

  LocalObject localObject = new LocalObject();

  localObject.callMethod();
  method2(localObject);
}

public void method2(LocalObject localObject){
  localObject.setValue("value");
}


예제의 LocalObject 인스턴스는 someMethond() 메소드에서 반환되지 않았고, someMethod() 메소드 밖에서 접근 가능한 다른 객체에게 넘겨지지도 않았다. someMethod() 메소드를 실행하는 각 쓰레드는 자신만의 LocalObject 인스턴스를 가지며, localObject 에 이 인스턴스의 참조를 할당한다. 때문에 이 코드에서의 LocalObject는 쓰레드 세이프하다.


사실, someMethod() 메소드 전체가 쓰레드 세이프하다. LocalObject 인스턴스가 같은 클래스 혹은 다른 클래스 안의 다른 메소드의 파라미터로 넘겨진다 해도 이 메소드의 안정성은 보장된다.


여기서 있을 수 있는 단 하나의 예외는, LocalObject를 파라미터로 넘겨받은 메소드가 LocalObject 인스턴스를 다른 쓰레드로부터 접근 가능하도록 저장하게 되는 경우이다.



객체 멤버변수


 객체 멤버변수(필드)는 그 객체(클래스의 인스턴스)를 따라 힙에 저장된다. 때문에, 만약 두 쓰레드가 같은 객체 인스턴스의 메소드를 호출하고 이 메소드가 객체 멤버변수를 update한다면 이 메소드는 쓰레드 세이프하지 않다.


여기 그 예제가 있다.


public class NotThreadSafe{
    StringBuilder builder = new StringBuilder();

    public add(String text){
        this.builder.append(text);
    }
}


두 쓰레드가 동시에 같은 NotThreadSafe의 인스턴스의 add() 메소드를 호출한다면, 이는 경합 조건을 일으킨다. 예를 들어,


NotThreadSafe sharedInstance = new NotThreadSafe();

new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();

public class MyRunnable implements Runnable{
  NotThreadSafe instance = null;

  public MyRunnable(NotThreadSafe instance){
    this.instance = instance;
  }

  public void run(){
    this.instance.add("some text");
  }
}


두 MyRunnable 인스턴스가 한 NotThreadSate 인스턴스를 어떻게 공유하는지 주목하자. 이 때문에 NotThreadSafe의 add() 메소드는 경합 조건을 일이키는 것이다.


하지만 만약 두 쓰레드가 동시에 다른 인스턴스의 add() 메소드를 호출한다면 이야기가 달라진다. 이 때는 경합 조건이 발생하지 않는다. 약간 수정된 코드를 보자.


new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();


이제 두 쓰레드는 각자의 NotThreadSate 인스턴스를 가지며, 이 쓰레드들의 add() 메소드 호출은 더이상 서로 간섭하지 않는다. 이제 이 코드에서 경합 조건은 사라졌다. 여기서 알 수 있는 점은, 어떤 객체가 경합 조건을 일으키는 코드를 포함하고 있다고 해도 이 객체를 사용하는 방식에 따라서 경합 조건은 발생하지 않을 수 있다는 것이다.



쓰레드 컨트롤 이스케이프 룰


 코드의 자원 접근이 쓰레드 세이프한지 판단하기 위한 방법으로, 쓰레드 컨트롤 이스케이프 룰이 있다.


한 자원의 생성과 사용, 소멸이 동일한 쓰레드 안에서 이루어지고, 



이 쓰레드에서 절대 벗어나지 않는다면 이 자원의 사용은 쓰레드 세이프하다.


자원은 객체, 배열, 파일, 데이터베이스 커넥션, 소켓 기타 등등 어떤 방식으로든 공유 자원이 될 수 있다. 자바에서 객체들은 반드시 정확하게 소멸되지 않아도 된다. 여기서 '소멸'이란 객체로의 참조를 잃거나 nulling 되는 것을 의미한다.


어떤 어플리케이션에서 한 객체의 사용이 쓰레드 세이프하더라도, 그 객체가 파일이나 데이터베이스처럼 공유된 자원을 가리킨다면, 전체적으로 봤을 때 이 어플리케이션은 쓰레드 세이프하지 않을 수 있다. 예를 들어, 쓰레드1과 쓰레드2이 자신들의 데이터베이스 커넥션인 커넥션1, 커넥션2를 각각 갖는다면, 이들 커넥션의 사용을 쓰레드 세이프하다. 하지만 이 커넥션들이 가리키는 데이터베이스의 사용은 쓰레드 세이프하지 않을 수 있다. 

두 쓰레드가 다음과 같은 코드를 실행한다고 생각해보자.


check if record X exists
if not, insert record X


두 쓰레드가 이 코드를 동시에 실행한다면, 그리하여 record X 체크가 두 번 이루어진다면, 이 두 쓰레드의 코드 실행은 다음과 같이 처리될 위험이 있다.


Thread 1 checks if record X exists. Result = no
Thread 2 checks if record X exists. Result = no
Thread 1 inserts record X
Thread 2 inserts record X


이런 결과는 다수의 쓰레드가 파일이나 기타 다른 공유 자원을 처리할 때 얼마든지 발생할 수 있다. 때문에 쓰레드에 의해 컨트롤되는 객체가 자원 그 자체인지, 혹은 그저 자원으로의 참조인 것인지 구분해야 한다(데이터베이스 커넥션처럼).



이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/race-conditions-and-critical-sections.html



 경합 조건은 임계 영역 안에서 발생할 수 있는 특별한 현상이다. 임계 영역이란, 멀티쓰레드 환경에서 다수의 쓰레드들이 동시에 어떤 코드를 실행할 때, 각 쓰레드들이 서로 다른 결과를 내놓는, 동시 접근 문제가 발생할 수 있는 코드의 한 부분(영역)이다.


다수의 쓰레드가 코드의 임계 영역을 실행하게 되면 그 결과는 쓰레드의 실행 시퀀스에 따라 다르게 나올 수 있다. 임계 영역의 의미는 경합 조건을 포함하는데, 경합 조건이라는 용어는 쓰레드들이 임계 영역에서 서로 경쟁하게 되는 현상에 대한 비유로써 생겨났다. 그리고 이 경쟁의 결과는 임계 영역의 실행 결과에 영향을 미친다.


이 설명은 다소 복잡하게 느껴질 수 있는데, 때문에 이 글에서는 경합 조건과 임계 영역에 대해 보다 상세히 설명하기로 한다.


 

임계 영역


 한 어플리케이션에서 다수의 쓰레드를 실행하는 일이 스스로 문제를 유발하는 것은 아니다. 문제는 다수의 쓰레드가 같은 자원에 접근할 때 발생한다. 여기서 같은 자원이란, 같은 메모리(변수, 배열, 혹은 객체), 시스템(데이터베이스, 웹서비스 기타등등), 또는 파일을 의미한다.


하나 이상의 쓰레드가 이들 자원에 접근하여 쓰기 작업을 수행할 때 문제는 발생한다. 다수의 쓰레드가 같은 자원에 접근하더라도, 자원에 변경(수정)을 시도하지 않는 한 문제는 발생하지 않는다.


아래는 자바 코드의 임계 영역 예제이다. 이 코드는 멀티쓰레드로 동시에 실행될 때 문제가 발생할 수 있다.


  public class Counter {

     protected long count = 0;

     public void add(long value){
         this.count = this.count + value;
     }
  }


두 쓰레드 A, B 가 있고, 이 쓰레드들이 Counter 클래스의 add() 메소드를 호출하는 상황을 생각해보자. 명령 시스템이 두 쓰레드 중 어느 쪽을 언제 실행할지 알 수 있는 방법은 없다. 이 코드에서 add() 메소드는 원자적인 명령으로 실행되지 않는다. 정확히 말하자면 여기서 이 코드는 세 개의 작은 명령들로 실행되는데, 다음과 비슷하다.


  1. count를 메모리에서 레지스터로 읽는다.
  2. 레지스터에 값을 더한다.
  3. 레지스터의 값을 메모리에 쓴다(writing).


쓰레드 A와 B의 실행이 섞여서 무슨 일이 일어나는지 보자.


       this.count = 0;

   A:  Reads this.count into a register (0)
   B:  Reads this.count into a register (0)
   B:  Adds value 2 to register
   B:  Writes register value (2) back to memory. this.count now equals 2
   A:  Adds value 3 to register
   A:  Writes register value (3) back to memory. this.count now equals 3


이 두 쓰레드는 값 2와 3을 count에 더하려고 하였고, 쓰레드의 실행이 끝난 뒤의 최종 값은 5가 되어야 했다. 그러나 두 쓰레드의 실행이 끼워넣어지면서 전혀 다른 결과값이 나온다.


위에 나열된 실행 순서에서, 두 쓰레드는 메모리로부터 값 0을 읽어들이고 각자의 값 2와 3을 더한다. 그리고 다시 메모리로 이 값을 쓴다. this.count의 값은 5 대신 마지막에 실행이 완료된 쓰레드가 더한 값으로 쓰여진다. 위의 케이스에서 이 쓰레드는 A였지만, 다음 실행에서는 B가 될 수도 있는 일이다.


 

임계 영역에서의 경합 조건


위 예제 코드의 add() 메소드는 임계 영역을 가진다. 다수의 쓰레드가 이 영역을 실행할 때, 경합 조건이 발생한다.


더 형식적으로 말하자면, 두 쓰레드가 같은 자원으로 접근하기 위해 경쟁하고, 이 자원으로 접근되는 순서가 중요한 의미를 지니는 이러한 상황을 경합 조건이라 한다. 그리고 경합 조건을 불러오는 코드는 임계 영역이라 부른다.


 

경합 조건 예방


 경합 조건의 발생을 예방하기 위해서는 임계 영역 실행의 원자성을 보장해야 한다. 이는 한 쓰레드가 코드를 실행하면 이 쓰레드가 작업을 마치고 임계 영역에서 손을 놓을 때까지 때까지 다른 쓰레드는 이 코드를 실행할 수 없어야 함을 의미한다.


경합 조건은 임계 영역에 대한 적절한 쓰레드 동기화로 예방될 수 있다. 자바에서의 쓰레드 동기화는 기본적으로 synchronized 블록을 사용하여 이루어지고, locks 생성자나 java.util.concurrent.atomic.AtomicInteger 같은 원자성 변수와 같은 동기화 생성자/변수를 사용하여 이루어지기도 한다.



임계 영역 처리량


 코드에 임계 영역들이 작게 분포되어 있는 경우, 코드의 임계 영역 전체를 synchronized 블록으로 감싸는 것이 경합 조건 예방을 위한 방법이 될 수 있다.하지만 임계 영역의 범위가 큰 경우에는 이를 다수의 작은 임계 영역으로 분리하는 편이 좋을 수도 있다. 이렇게 하면 쓰레드들이 나뉘어진 임계 영역을 각각 실행할 수 있고, 이는 공유 자원으로의 경합을 줄일 수 있게 된다. 결과적으로 임계 영역의 처리량을 향상시킬 수 있는 것이다.


이게 무슨 의미인지 간단한 자바 코드로 살펴보자.


public class TwoSums {
    
    private int sum1 = 0;
    private int sum2 = 0;
    
    public void add(int val1, int val2){
        synchronized(this){
            this.sum1 += val1;   
            this.sum2 += val2;
        }
    }
}


add() 메소드가 두 개의 멤버변수에 어떻게 값을 더하는지 보자. 경합 조건을 예방하기 위해 값을 더하는 작업은 synchronized 블록 안에서 이루어진다. 이렇게 하여 한 시점에 오직 하나의 쓰레드만이 이 코드를 실행할 수 있다.


아니면, 두 변수가 서로 독립적인 관계에 있으니 이 둘에 값을 더하는 작업을 분리할 수도 있다.


public class TwoSums {
    
    private int sum1 = 0;
    private int sum2 = 0;
    
    public void add(int val1, int val2){
        synchronized(this){
            this.sum1 += val1;   
        }
        synchronized(this){
            this.sum2 += val2;
        }
    }
}


이제 add() 메소드는 한 시점에 두 개의 쓰레드가 실행 가능하게 됐다. 한 쓰레드는 첫 번째 synchronized 블록을 실행하고, 다른 하나는 두 번째 블록을 실행한다. add() 메소드 전체를 synchronized 블록으로 감쌌을 때 쓰레드들은 먼저 블록에 진입한 한 쓰레드가 메소드 전체를 실행할 동안 기다려야 했지만, 이렇게 블록을 나눔으로써 쓰레드의 기다리는 시간을 줄인 것이다.


이 예제는 매우 단순한 것이다. 당연하게도 실제 프로그램에서 임계 영역을 나누는 작업은 훨씬 복잡하고, 실행 순서의 면밀한 분석이 요구된다.



 

이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/creating-and-starting-threads.html



자바 쓰레드는 자바의 다른 오브젝트들과 다르지 않다. 쓰레드는 java.lang.Thread 클래스의 인스턴스이거나, 이 클래스의 서브클래스의 인스턴스이다. 그리고 쓰레드는 오브젝트이면서 코드를 실행하는 일도 가능하다.


 

자바 쓰레드의 생성과 시작


자바에서 쓰레드의 생성을 다음과 같은 코드로 이루어진다.


  Thread thread = new Thread();


자바 쓰레드를 실행하기 위해 start() 메소드를 호출한다.


  thread.start();


이 예제에 쓰레드의 실행을 위한 구체적인 코드는 없다. 쓰레드는 실행된 다음 곧바로 종료될 것이다.


쓰레드를 실행하는 코드에는 두 가지 방법이 있다. 하나는 Thread의 서브클래스를 만들어 run() 메소드를 오버라이딩 하는 것이고, 다른 하나는 java.lang.Runnable 인터페이스를 구현한 오브젝트를 Thead의 생성자의 파라미터로 넘기는 방법이다. 두 방법을 아래에서 소개한다.


 

Thread의 서브클래스


쓰레드를 실행하는 첫 번째 방법은 Thread의 서브클래스를 만들어 run() 메소드를 오버라이딩 하는 것인데, run() 메소드는 Thread의 start() 메소드가 호출됐을 때 실행되는 메소드이다. Thread의 서브클래스를 만드는 방법은 다음과 같다.


  public class MyThread extends Thread {

    public void run(){
       System.out.println("MyThread running");
    }
  }


그리고 다음은 위 쓰레드를 실행하기 위한 코드이다.


  MyThread myThread = new MyThread();
  myTread.start();


start() 메소드는 호출이 이루어지면 곧바로 종료되며, run() 메소드가 끝나기를 기다리지 않는다. run() 메소드는 마치 다른 CPU에 의해 실행되는 것처럼 실행된다. run() 메소드가 실행될 때, "MyThread running" 문구가 출력될 것이다.


Thread의 익명 클래스를 생성할 수도 있다.


  Thread thread = new Thread(){
    public void run(){
      System.out.println("Thread Running");
    }
  }

  thread.start();


이 예제는 새로운 쓰레드에 의해 "Thread running" 문구를 출력할 것이다.


 

Runnable 인터페이스 구현


쓰레드를 실행하는 두 번째 방법은 java.lang.Runnable 인터페이스를 구현하는 것이다. Runnable 객체는 Thread에 의해 실행된다.

여기 그 예제가 있다.


  public class MyRunnable implements Runnable {

    public void run(){
       System.out.println("MyRunnable running");
    }
  }


run() 메소드가 호출되도록 하기 위해 MyRunnable 클래스의 인스턴스를 Thread의 생성자로 전달한다.


   Thread thread = new Thread(new MyRunnable());
   thread.start();


이렇게 하면 쓰레드가 시작될 때 Thread 자신의 run() 메소드 대신 생성자로 전달된 MyRunnable 인스턴스의 run() 메소드를 호출한다.위의 예제는 "MyRunnable running" 문구를 출력한다.


여기서도 역시 익명 클래스를 생성할 수 있다.


   Runnable myRunnable = new Runnable(){

     public void run(){
        System.out.println("Runnable running");
     }
   }

   Thread thread = new Thread(myRunnable);
   thread.start();

 


서브클래스와 java.lang.Runnable


두 방법 모두 잘 작동하며, 이들 중 어느쪽이 더 나은가에 대한 정답은 없다. 개인적으로는 Runnable 인터페이스를 구현하는 쪽을 선호하는데, 쓰레드 풀(thread pool)에게 Runnable의 인스턴스를 실행하게 하는 쪽이 Runnable 인스턴스를 Thread가 대기할 때까지 줄세워두기(queue up) 쉽다. 이 작업은 Thread의 서브클래스로는 약간 더 어려운 일이다.


종종 Runnable 인터페이스의 구현과 함께 Thread의 서브클래스를 만들어야 할 때가 있다. 예를 들자면 하나 이상의 Runnable 인스턴스를 실행하는 경우이다. 이런 상황이 쓰레드 풀을 구현할 때의 전형적인 케이스이다.


 

위험요소: start() 메소드가 아닌 run() 메소드의 호출


쓰레드를 생성하고 실행하는 데에 있어서 흔히 발생하는 실수는 start() 메소드 대신 run() 메소드를 실행하는 일이다.


  Thread newThread = new Thread(MyRunnable());
  newThread.run();  //should be start();


예상대로 Runnable의 run() 메소드가 실행되기 때문에 여기서 무엇이 문제인지 모를 수도 있다. 하지만 이 코드는 새로 생성된 쓰레드에 의해 실행된 것이 아니다. 여기서 실행된 run() 메소드는 이 코드를 실행하는 현재 쓰레드(current thread)에 의해 작동되는데, MyRunnable 인스턴스의 run() 메소드를 새로 생성된 쓰레드 - 코드를 실행하는 쓰레드(current thread)가 아닌 - 가 실행하도록 하기 위해서는 newThread.start() 메소드를 호출해야만 한다.


 

쓰레드의 이름


자바 쓰레드를 생성할 때 쓰레드에게 이름을 부여할 수 있다. 쓰레드의 이름은 멀티쓰레드 환경에서 각 쓰레드를 쉽게 구분할 수 있게 해준다. 예를 들어, 다수의 쓰레드가 System.out 을 사용한다고 하면, 콘솔에 찍힌 문구가 어떤 쓰레드에 의해 출력된 것인지 식별하는 데에 유용하다.


   Thread thread = new Thread("New Thread") {
      public void run(){
        System.out.println("run by: " + getName());
      }
   };


   thread.start();
   System.out.println(thread.getName());


Thread의 생성자에 전달된 "New Thead" 문구를 보자. 이 문자열이 쓰레드의 이름이다. 쓰레드는 Thread의 getName() 메소드를 통해 이 이름을 획득한다. Runnable 인스턴스를 이용한 쓰레드 생성에서도 쓰레드 이름을 지정할 수 있다.


   MyRunnable runnable = new MyRunnable();
   Thread thread = new Thread(runnable, "New Thread");

   thread.start();
   System.out.println(thread.getName());


하지만 MyRunnable 클래스는 Thread의 서브클래스가 아니기 때문에, 여기서 getName() 을 호출할 수는 없다.


 

Thread.currentThread()


Thread.currentThread() 메소드는 currentThread() 를 실행하는 Thread 인스턴스의 레퍼런스를 반환한다. 이 방법으로 자바 코드블럭을 실행하는 쓰레드의 객체에 접근할 수 있다.


Thread thread = Thread.currentThread();


Thread 객체를 가지게 되면 이 객체의 메소드를 호출할 수 있는다. 예를 들어 현재 코드를 실행중인 쓰레드의 이름은 다음과 같이 획득할 수 있다.


 

자바 쓰레드 예제


아래 작은 예제가 하나 있다. 이 코드는 먼저 main() 메소드를 실행하는 쓰레드의 이름을 출력한다. 이 쓰레드는 JVM에 의해 할당된 것이다. 그리고 다음으로는 10개의 쓰레드를 실행하고 이 쓰레드들에게 숫자로 된 이름("" + i)을 부여한다. 각 쓰레드는 자신의 이름을 출력하고는 동작이 종료된다.


public class ThreadExample {

  public static void main(String[] args){
    System.out.println(Thread.currentThread().getName());
    for(int i=0; i<10; i++){
      new Thread("" + i){
        public void run(){
          System.out.println("Thread: " + getName() + " running");
        }
      }.start();
    }
  }
}


쓰레드를 순차적으로(1, 2, 3, ...) 시작했지만 이들이 이 순서대로 실행되지 않는다는 점에 주목하자. 처음 시작된 1번 쓰레드는 콘솔에 자신의 이름을 출력하는 첫 번째 쓰레드가 되지 않을 수도 있다. 이는 쓰레드의 실행이 순차적이 아닌, 병렬적으로 이루어지기 때문이다. JVM과 명령 시스템, 혹은 JVM이나 명령 시스템(and/or)은 쓰레드의 실행 순서를 결정하고, 이 순서는 실행할 때마다 달라질 수 있다.

 

이 글은 원 저자 Jakob Jenkov의 허가로 포스팅된 번역물이다.

원문 URL : http://tutorials.jenkov.com/java-concurrency/concurrency-vs-parallelism.html



 용어 '컨커런시' 와 '페러럴리즘' 은 멀티쓰레드 프로그램 관계에서 흔히 사용된다. 하지만 이 두 용어의 정확한 의미가 무엇일까?

이에 대한 간단한 대답은, "아니오" 이다. 이 둘은 상당히 비슷해 보이긴 하지만 다른 용어이다. 이 둘의 차이를 이해하는 데에 시간이 조금 필요했고, 때문에 이 주제를 튜토리얼에 추가하기로 마음먹었다. '컨커런시 vs '페러럴리즘'.


 

컨커런시


 컨커런시는 한 어플리케이션이 하나 이상의 task를 가지고, 동시에 실행된다. 만일 컴퓨터의 CPU가 하나라면, 어플리케이션은 '동시에' 다수의 task를 가질 수 없을 수 있다. 하지만 하나 이상의 task는 어플리케이션 내부에서 작동한다. 한 task가 시작되기 전, 이전의 task는 완전히 종료되지 않는다.

 

 


페러럴리즘


 페러럴리즘은 한 어플리케이션이 다수의 subtasks로 나뉘고 이 subtasks들이 병렬로 작동하는 것이다. 정확히 같은 시간에 작동하는 다수의 CPU를 예로 들 수 있다.


 


컨커런시 vs. 페러럴리즘 In Detail


 보다시피, 컨커런시는 한 어플리케이션이 다수의 task를 어떻게 다루는가와 관련이 있다. 어플리케이션은 한번에 한 task를 순차적으로 가지거나, 다수의 task를 동시에 가질 수 있다. 반면 페러럴리즘은 한 어플리케이션이 각각의 task를 어떻게 다루는가와 관련이 있다. 어플리케이션은 task를 처음부터 끝까지 연속적으로 가지거나, 한 task를 다수의 subtask 로 나누고 이들이 병렬로 작동하게끔 한다.


한 어플리케이션은 동시성(concurrency)을 가질 수 있고, 병행성(parallelism)은 가지지 않을 수 있다. 이것은 하나 이상의 task를 동시에 가진다는 말이고, 이 task들은 subtask로 나뉘지 않는다. 또한, 한 어플리케이션은 병행성을 가지고 동시성은 가지지 않을 수 있다. 이것은 한번에 하나의 task만을 가진다는 말이고, 이 task가 다수의 subtask로 나뉘어 병렬로 작동한다.


이에 더하여, 어플리케이션은 동시성과 병행성 둘 다 가지지 않을 수도 있다. 이는 오직 한번에 한 task만을 가지고 이 task는 절대 subtask로 나뉘지 않는다는 말이다.


마지막으로, 어플리케이션은 동시성과 병행성 모두를 가질 수도 있다. 이 두 형태는 동시에 다수의 task로 작동하며, 각 task는 subtask로 나뉘어 병렬로 작동하기도 한다. 하지만, 이 경우에 동시성과 병행성의 이점을 잃을 수도 있다. 컴퓨터의 CPU가 이미 다른 작업을 처리하느라 바쁘게 돌아가고 있다면, 동시성과 병행성 모두 의미가 없다. 이 둘을 결합하는 일은 그저 약간의 성능 향상 혹은 오히려 성능의 저하를 야기하기도 한다. 맹목적으로 이 모델을 사용하기 전에 정확한 분석과 측정이 요구되는 이유이다.

 

'Java > Concurrency' 카테고리의 다른 글

경합 조건과 임계 영역(Race Conditions and Critical Section)  (1) 2017.04.09
자바 쓰레드 시작하기  (0) 2017.04.09
컨커런시 모델  (0) 2017.04.09
멀티쓰레딩의 단점  (0) 2017.04.09
멀티쓰레딩의 장점  (0) 2017.04.09

+ Recent posts