이 글은 원 저자 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


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



+ Recent posts