멀티스레드 환경에서 여러 스레드(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 변수가 아닌 레퍼런스 변수를 명시하면 이 레퍼런스가 변하지 않음을 보장할 수 없기 때문에 이와 같은 문제가 발생할 수 있다.
이런 류의 버그는 잘 발생하지 않지만 한번 발생했을 때 동시성 문제를 발생시켜 코드 수행을 엉망진창으로 만들 수도 있으므로 주의해야한다.
댓글