본문 바로가기
Old Posts/Java

[Java] synchronized 블럭 사용시 주의 사항 - final 필드

by A6K 2021. 7. 18.

멀티스레드 환경에서 여러 스레드(Thread)가 동시에 하나의 리소스에 접근하는 경우가 있다. 불변 객체의 경우 문제가 없지만 객체에 대한 수정이 가해질 경우 동시성 문제가 발생한다. 자바는 프로그래밍 언어 문법으로 동시성을 제어할 수 있는 synchronized 키워드를 제공하여 별도의 라이브러리를 사용하지 않고도 손쉽게 멀티 스레드 환경에서의 동시성 제어를 구현할 수 있다.

예를 들어 다음 코드를 살펴보자.

class TestObject {

    private long value;

    TestObject(long value) {
        this.value = value;
    }

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

    public void setValue(long value) {
        this.value = value;
    }
}


public class SynchronizedWithFinalTest1 implements Runnable {

    private TestObject object;

    private SynchronizedWithFinalTest1(TestObject obj) {
        this.object = obj;
    }

    public void run() {

        synchronized (this.object) {

            long threadId = Thread.currentThread().getId();

            System.out.println("critical section start [" + threadId + "]");

            try {
                System.out.println("Work within critical section[" + threadId + "]");
                Thread.sleep(10000);
            } catch (Exception e) { /* No-op */ }

            System.out.println("critical section end [" + threadId + "]");
        }
    }

    public static void main(String args[]) {

        TestObject obj = new TestObject(100);

        Runnable r = new SynchronizedWithFinalTest1(obj);

        new Thread(r).start();

        new Thread(r).start();

    }
}

 

두 개의 스레드가 TestObject 객체에 synchronized 되어 동기화된다. 이 코드를 실행하면 다음 결과를 얻게 된다.

critical section start [13]
Work within critical section[13]
critical section end [13]
critical section start [14]
Work within critical section[14]
critical section end [14]

일단은 13번 스레드와 14번 스레드가 동시에 수행된다. 그러다가 synchronized 키워드에 줄을 서게되고 13번 스레드가 먼저 critical section으로 들어가서 실행된다. 이후 10초간 Sleep 한 다음 13번 스레드가 synchronized 블럭을 빠져나간 다음 기다리던 14번 스레드가 critical section으로 들어간다. 의도했던대로 잘 수행된다.

그럼 다음 코드를 보자.

class TestObject {

    private long value;

    TestObject(long value) {
        this.value = value;
    }

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

    public void setValue(long value) {
        this.value = value;
    }
}


public class SynchronizedWithFinalTest2 implements Runnable {

    private TestObject object;

    private SynchronizedWithFinalTest2(TestObject obj) {
        this.object = obj;
    }

    public void run() {
        synchronized (this.object) {

            long threadId = Thread.currentThread().getId();

            System.out.println("critical section start [" + threadId + "]");

            System.out.println("Work within critical section [" + threadId + "]");
            this.object = new TestObject(200);

            try {

                Thread.sleep(10000);
            } catch (Exception e) { /* No-op */ }

            System.out.println("critical section end [" + threadId + "]");
        }
    }

    public static void main(String args[]) {

        TestObject obj = new TestObject(100);

        Runnable r = new SynchronizedWithFinalTest2(obj);

        new Thread(r).start();

        try {
            Thread.sleep(1000);
        } catch (Exception e) { /* No-op */ }

        new Thread(r).start();
    }
}

이 코드를 실행하면 다음 결과를 얻게 된다.

critical section start [13]
Work within critical section [13]
critical section start [14]
Work within critical section [14]
critical section end [13]
critical section end [14]

critical section에 두 개의 스레드가 동시에 들어간 모습이다. 이런 동작은 의도하지 않은 동작으로 동시성 이슈가 생겨 시스템에 문제를 일으킬 수 있다. 문제 상황을 명확하게 재현하기 위해서 코드에 명시적으로 sleep을 넣었지만 실제 상황에서는 타이밍 이슈로 빈번하게 재현되지 않을 수 있다.

이 문제의 원인은 synchronized 블럭으로 줬던 레퍼런스 변수를 블럭 안쪽에서 수정했기 때문이다. 첫 번째 스레드가 원래 있던 객체를 기준으로 Critical Section을 만들고 들어갔다. 하지만 안쪽에서 this.object 변수의 참조를 새로운 객체로 바꿔버렸고, 새로 들어온 스레드는 첫 번째 스레드가 기다리는 객체가 아닌 다른 객체에 synchronized 를 걸게 되고, 서로 충돌없이 둘다 Critical Section으로 들어가서 실행하게 된다. 두 스레드는 같은 코드를 실행하지만 syncrhonized 대상이 되는 객체는 다르기 때문에 둘 다 동시에 실행되는 것이다.

IDE에서 개발을 하는 경우 똑똑하게 경고(Warning) 메시지를 뿌려줘서 잠재적인 문제가 있음을 경고해 준다. "Synchronization on a non-final field 'this.object'" 라는 경고 문구가 뜬다. synchronized 블럭에 final 변수가 아닌 레퍼런스 변수를 명시하면 이 레퍼런스가 변하지 않음을 보장할 수 없기 때문에 이와 같은 문제가 발생할 수 있다.

이런 류의 버그는 잘 발생하지 않지만 한번 발생했을 때 동시성 문제를 발생시켜 코드 수행을 엉망진창으로 만들 수도 있으므로 주의해야한다.

댓글