티스토리 뷰

1. 자바의 지역변수 스레드 세이프

자바에서 지역변수는 람다나 익명클래스 내에서 값을 변경할 수 없다. 람다나 익명클래스에서만 값을 변경할 수 없을뿐아니라 람다나 익명클래스에서 읽고(read) 있다면 외부 메서드 내에서도 값을 변경할 수 없다. 이 규약을 지키기위해 람다가 없던 자바7까지는 익명클래스에서 외부 메서드의 지역변수를 읽기위해선 지역변수를 final 로 선언했어야했고, 람다가 추가된 자바8 이상에서는 effectively final(사실상 final) 이라는 규약으로 final 변수가 아니더라도 람다나 익명클래스에서 읽고있다면 사실상 final 이 되어 어디서도 값을 변경할 수 없다.

public void test() {                        
    int number = 100;                       
                                            
    new Runnable() {                        
        @Override                           
        public void run() {                 
            number++;
        }                                   
    }.run();                                
}                                           

(이렇게 값을 변경할 수 없다.)

public void test() {                        
    int number = 100;                       
                                            
    new Runnable() {                        
        @Override                           
        public void run() {                 
            System.out.println(number);     
        }                                   
    }.run();                                
}                                           

(이런식으로 값을 읽는건 가능하며, 외부 지역변수를 읽는 순간 number 변수는 사실상 상수가 되어 그 어디서도 값을 변경할 수 없다.)

private int number = 100;        
                                 
public void test() {             
    new Runnable() {             
        @Override                
        public void run() {      
            number ++;           
        }                        
    }.run();                     
}                                

(지역변수가 아닌 인스턴스 필드는 값을 변경할 수 있다.)

 

지역변수를 람다나 익명클래스 내에서 변경하지 못하게 막은 이유는 자바의 지역변수는 스레드 세이프해야하기 때문이다. 만약 람다나 익명클래스 내에서 지역변수를 변경할 수 있다면 메서드내에서 스레드를 생성해서 지역변수를 조작하는 순간 지역변수의 스레드 세이프를 보장할 수 없어진다. 참고로 지역변수가 아닌 인스턴스 필드는 스레드 세이프를 보장하지않으므로 람다나 익명클래스 내에서도 값을 변경하는게 가능하다.

 

자바 스펙 문서(읽기 힘들다면 17.4.1 만 번역기 돌려서 봐보자): https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html

 

Chapter 17. Threads and Locks

class A { final int x; A() { x = 1; } int f() { return d(this,this); } int d(A a1, A a2) { int i = a1.x; g(a1); int j = a2.x; return j - i; } static void g(A a) { // uses reflection to change a.x to 2 } } In the d method, the compiler is allowed to reorder

docs.oracle.com

 

다만 이 스펙은 변수가 기본타입이라면 변수의 값을, 참조타입이라면 변수의 레퍼런스를 변경하지 못하게 하는것이다. 가변 객체를 참조하고있는 레퍼런스라면 해당 객체의 상태는 변경할 수 있다.

public void test() {               
    Refer refer = new Refer();     
                                   
    new Runnable() {               
        @Override                  
        public void run() {        
            refer.n ++;            
        }                          
    }.run();                       
}                                  
                                   
static class Refer {               
    private int n = 0;             
}                                  

말로 표현하니까 좀 어려웠는데 이런건 허용된다는 것이다. 지역변수인 refer 의 레퍼런스를 변경하는건 불가하지만 refer 의 내부 상태를 변경하는건 가능하다.

 

2. 코틀린의 지역변수

처음에는 별 생각없이 코틀린도 자바와 동일할 것이라고 생각했다. 일단 코틀린에서는 합당한 이유가 없는 이상 기본적으로 변수를 val 로 선언하게된다. val 은 final 이므로 스펙같은걸 고려할 필요없이 변경 불가능하므로 스레드 세이프하다. 람다나 익명클래스뿐 아니라 그 어디서도 변경할 수 없다. 변경가능한 var 는 어떨까? 자바의 스펙을 대입해본다면 var 는 기본적으로 변경할 수 있지만 람다나 익명클래스(코틀린에서는 object) 에서는 변경할 수 없으며 변수를 읽는 순간 effectively final 이 되어 외부에서도 변경할 수 없어야된다.

fun test() {
    var n = 100

    Runnable {
        n++
    }.run()
}

하지만.. 너무 잘된다. 결과를 확인해봐도 실제로 값이 변경되어있는걸 확인 할 수 있다. 그럼 이렇게되면 멀티 스레드 환경에서 스레드 세이프하지못할텐데? 확인해보자.

fun test() {
    var n = 0
    val latch = CountDownLatch(3)
    val runnable = {
        for(i in 0..100000) {
            n++
        }
        latch.countDown()
    }

    val service = Executors.newCachedThreadPool()
    service.execute(runnable)
    service.execute(runnable)
    service.execute(runnable)
    service.shutdown()
    latch.await()
    println("@@@ => $n")
}

엉성한 코드지만.... 궁금했던 점을 확인하기엔 충분한 결과를 보여주는 코드다. 예상했던대로 정확한 연산결과를 출력하지 못한다. 결론을 내보자. 일단 코틀린의 지역변수는 멀티 스레드에 안전하지 않다. 다만 자바와 호환해야하는 코틀린에서 어떻게 지역변수의 값을 변경하는 스펙을 구현할 수 있었을까?

 

3. 코틀린의 구현

public static final void test() {       
   final IntRef n = new IntRef();       
   n.element = 0;                       
   ((Runnable)(new Runnable() {         
      public final void run() {         
         int var10001 = n.element++;    
      }                                 
   })).run();                           
}                                       

코틀린의 첫번째 예제코드를 자바로 디컴파일한 결과물이다. 자바에서 지역변수가 직접 참조하는 레퍼런스는 변경하지못하지만 그 레퍼런스가 가변객체인경우 객체의 상태를 변경하는건 가능했던걸 기억할 것이다. 코틀린은 개발자의 코드를 그런식으로 변경하여 지역변수의 변경을 가능하게 만드는 것이다.

 

이처럼 코틀린의 지역변수는 var 로 선언할 경우 멀티스레드에 안전하지 않다. 자바와의 스펙과는 다른점이므로 이 점을 유의하자. 물론 변수를 val 로 선언해서 쓴다면 크게 신경쓸 필요는 없다.

댓글
댓글쓰기 폼