여러 스레드가 동시에 하나의 공유 변수에 접근해 읽기·쓰기를 수행하면, 각 스레드의 연산이 서로 엇갈리면서 최종 결과가 예상과 달라지는 경우가 있습니다. 이러한 현상을 스레드 간섭(Thread Interference) 오류라고 하며, 자바에서 대표적인 동시성(concurrency) 문제 중 하나입니다. 아래 예제를 통해 구체적으로 살펴보겠습니다.
예제 코드
import java.io.*;
class Demo_instance{
static int val_1 = 6;
void increment_val(){
for(int j=1;j<11;j++){
val_1 = val_1 + 1;
System.out.println("증가 후 i의 값 : "+val_1);
}
}
void decrement_val(){
for(int j=1;j<11;j++){
val_1 = val_1 - 1;
System.out.println("감소 후 i의 값 : "+val_1);
}
}
}
public class Demo{
public static void main(String[] args){
System.out.println("Demo_instance 인스턴스 생성");
System.out.println("스레드 인스턴스 생성");
final Demo_instance my_inst = new Demo_instance();
Thread my_thread_1 = new Thread(){
@Override
public void run(){
my_inst.increment_val();
}
};
Thread my_thread_2 = new Thread(){
@Override
public void run(){
my_inst.decrement_val();
}
};
my_thread_1.start();
my_thread_2.start();
}
}실행 결과
Demo_instance 인스턴스 생성 스레드 인스턴스 생성 증가 후 i의 값 : 7 증가 후 i의 값 : 7 감소 후 i의 값 : 6 증가 후 i의 값 : 8 감소 후 i의 값 : 7 증가 후 i의 값 : 8 증가 후 i의 값 : 8 감소 후 i의 값 : 7 증가 후 i의 값 : 9 감소 후 i의 값 : 8 감소 후 i의 값 : 7 감소 후 i의 값 : 6 감소 후 i의 값 : 5 감소 후 i의 값 : 4 감소 후 i의 값 : 3 감소 후 i의 값 : 2 증가 후 i의 값 : 3 증가 후 i의 값 : 4 증가 후 i의 값 : 5 증가 후 i의 값 : 6
코드 설명
Demo_instance 클래스는 정적(static) 변수 val_1을 선언하고, 반복문을 돌며 값을 1씩 증가시킨 뒤 콘솔에 출력하는 increment_val() 메서드와, 같은 방식으로 값을 1씩 감소시키며 출력하는 decrement_val() 메서드를 가지고 있습니다.
Demo 클래스의 main 함수는 먼저 Demo_instance 객체를 생성한 뒤, 익명 내부 클래스 형태로 두 개의 스레드를 만듭니다. 첫 번째 스레드는 run() 메서드를 오버라이드하여 increment_val()을 호출하고, 두 번째 스레드는 decrement_val()을 호출합니다. 마지막으로 두 스레드를 start() 함수로 실행하면, 두 스레드가 동시에 같은 변수 val_1을 수정하게 됩니다.
스레드 간섭이 발생하는 이유
실행 결과를 보면 증가와 감소가 순서대로 진행되지 않고 뒤엉켜 있는 것을 확인할 수 있습니다. val_1 = val_1 + 1은 단일 연산처럼 보이지만, 실제로는 ① 현재 값 읽기 → ② 값 더하기 → ③ 결과 저장의 세 단계로 나뉩니다. 두 스레드가 이 단계들을 교차해서 수행하면 한 스레드가 저장한 결과를 다른 스레드가 덮어쓰게 되고, 그 결과 10번의 증가와 10번의 감소가 실행되었음에도 중간 값들이 기대와 다르게 출력됩니다. 이것이 바로 스레드 간섭 오류입니다.
스레드 간섭 방지 방법
가장 간단한 해결책은 공유 자원에 접근하는 메서드를 synchronized로 선언하여, 한 시점에 하나의 스레드만 실행하도록 만드는 것입니다.
synchronized void increment_val(){
for(int j=1;j<11;j++){
val_1 = val_1 + 1;
System.out.println("증가 후 i의 값 : "+val_1);
}
}이 외에도 java.util.concurrent.atomic 패키지의 AtomicInteger를 사용하면 락(lock) 없이도 원자적(atomic) 연산을 보장할 수 있어, 상황에 따라 성능 면에서 더 유리한 선택이 될 수 있습니다.