Java는 multi-thread 프로그래밍 언어이며 race condition이 발생할 위험이 높다. 동일한 리소스를 여러 스레드에서 동시에 접근할 수 도 있고 데이터가 변경될 수 도 있기 때문이다.
race condition은 동시성 버그라고 말할 수 있으며 deadlock과 밀접한 관련이 있다.
임계영역(critical section : 공유 메모리에 접근하는 프로그램의 일부)이 두 개 이상의 스레드에 의해 동시에 실행되는 조건이다. 다시 말해, 특정 공유 리소스를 얻기 위해 두개 이상의 스레드가 함께 경쟁하는 조건으로 정의할 수 있으며 이는 프로그램의 잘못된 동작으로 이어진다.
예를 들어 스레드A가 연결목록에서 데이터를 읽고 있고, 스레드B가 동일한 데이터를 삭제하려고 하는 경우, 이 프로세스로 인해 런타임 오류가 발생할 수 있다.
이러한 Race Conditions에는 2가지 유형이 있다
counter++;
if(map.contaions(key)){
map.remove(key)
}
예시
3개의 쓰레드가 동기화 없이 공유하는 변수 c
를 +1증가시킨 후 -1감소시킨다.
public class Counter implements Runnable {
private int c = 0;
public void increment() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
//Auto-generated catch block
e.printStackTrace();
}
c++;
}
public void decrement() {
c--;
}
public int getValue() {
return c;
}
@Override
public void run() {
//incrementing
this.increment();
System.out.println("Value for Thread After increment " + Thread.currentThread().getName() + " " + this.getValue());
//decrementing
this.decrement();
System.out.println("Value for Thread at last " + Thread.currentThread().getName() + " " + this.getValue());
}
}
public class RaceConditionDemo {
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(counter, "Thread-1");
Thread t2 = new Thread(counter, "Thread-2");
Thread t3 = new Thread(counter, "Thread-3");
t1.start();
t2.start();
t3.start();
}
}
결과
1과 0을 반복하며 나올 것이라는 예상과 다르게 c
가 잘못된 값을 제공하는 것을 관찰할 수 있다.
Value for Thread After increment Thread-1 2
Value for Thread After increment Thread-3 2
Value for Thread After increment Thread-2 2
Value for Thread at last Thread-1 1
Value for Thread at last Thread-3 0
Value for Thread at last Thread-2 -1
Java에서는 동기화 기법으로 상호배제(보완된 세마포어)를 구현한 Monitor를 Object내부에 구현하여 모든 인스턴스에 Thread동기화를 가능하게 한다.
이 Monitor를 활용하여 상호배제하기 위해서는 synchronized
키워드를 사용해야하는데 사용하는 방법으로는 메서드 앞에 키워드를 붙이거나, 메서드 내부에 synchronized(모니터 인스턴스){구현}
으로 사용할 수 있다.
synchronized
:synchronized
메서드를 사용하려면 모든 Thread는 lock을 가지고 있어야하며 이는 2가지 효과를 가진다
- 만약 스레드A가 한 객체의 동기화된 메서드를 호출 중이라면, 스레드B는 동일한 객체의 동기화된 메서드를 호출할 수 없다. 따라서 스레드B는 스레드A의 작업이 완료될 때 까지 차단(실행 일시 중단)된다.
- 스레드A의 동기화된 메서드가 종료되면, 사전 발생 관계(스레드B의 후속 호출)를 자동으로 설정한다. 이렇게 하면 객체의 상태 변화를 모든 스레드에서 볼 수 있다.
출처 :docs.oracle - Synchronized Methods
예시
증가와 감소 연산 시 다른 스레드가 끼어들지 못하게 run 메서드 내부에 동기화 block을 만들었다.
public class Counter implements Runnable {
private int c = 0;
public void increment() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
//Auto-generated catch block
e.printStackTrace();
}
c++;
}
public void decrement() {
c--;
}
public int getValue() {
return c;
}
@Override
public void run() {
synchronized (this){
//incrementing
this.increment();
System.out.println("Value for Thread After increment " + Thread.currentThread().getName() + " " + this.getValue());
//decrementing
this.decrement();
System.out.println("Value for Thread at last " + Thread.currentThread().getName() + " " + this.getValue());
}
}
}
결과
Value for Thread After increment Thread-1 1
Value for Thread at last Thread-1 0
Value for Thread After increment Thread-3 1
Value for Thread at last Thread-3 0
Value for Thread After increment Thread-2 1
Value for Thread at last Thread-2 0
참고 :
javatpoint - Race Condition in Java
stackoverflow - Race conditions "check-then-act" and "read-modify-write"
tecoble - java synchronize