[Java][Thread] 쓰레드의 동기화 #3 volatile

2022. 7. 5. 22:08JAVA/Language

1. volatile의 필요성

멀티 코어 프로세서 환경에서 쓰레드를 사용하는 경우 각각의 코어에는 별도의 캐시를 가지고 있습니다. 그리고 쓰레드를 수행하는 CPU 코어는 메모리에서 읽어온 값을 캐시에 저장하고 캐시에서 값을 읽어서 작업을 수행합니다. 다시 같은 값을 읽어올 때는 먼저 캐시에 있는지 확인을 한 다음에 없을 때만 메모리에서 읽어오게 됩니다. 때문에 메모리에 저장된 변수의 값이 변경이 되었는데도 캐시에 저장된 값이 갱신되지 않아서 메모리에 저장된 값이 다른 경우가 발생합니다.

 

다음 예제는 쓰레드 3개의 이름을 출력하는 예제입니다. 주목할 점은 쓰레드 인스턴스의 메소드를 사용하지 않고 변수를 이용해서 중지했다가 다시 수행한다는 점입니다.

public class Driver {
	public static void main(String[] args) {
		RunImplEx r1 = new RunImplEx();
		RunImplEx r2 = new RunImplEx();
		RunImplEx r3 = new RunImplEx();
		
		Thread th1 = new Thread(r1, "*");
		Thread th2 = new Thread(r1, "**");
		Thread th3 = new Thread(r1, "***");
		
		th1.start();
		th2.start();
		th3.start();
		
		try {
			Thread.sleep(2000);
			r1.suspend();
			Thread.sleep(2000);
			r2.suspend();
			Thread.sleep(3000);
			r1.resume();
			Thread.sleep(3000);
			r1.stop();
			r2.stop();
			Thread.sleep(2000);
			r3.stop();
			
		}catch (InterruptedException e) {
		}
		
	}
}

class RunImplEx implements Runnable{
	boolean suspended = false;
	boolean stoped    = false;
	
	
	@Override
	public void run() {
		// 멈춘 상태가 아닌 경우
		while(!stoped) {
			// 멈춤 상태가 아닌 경우
			if(!suspended) {
				System.out.println(Thread.currentThread().getName());
				
				try {
					Thread.sleep(1000);
				} catch (InterruptedException e) {
				}
			}
		}
		System.out.println(Thread.currentThread().getName() + " - stopped");
	}
	
	public void suspend() {
		suspended = true;
	}
	public void resume() {
		suspended = false;
	}
	public void stop() {
		stoped = true;
	}
}
*
**
***
***
**
*
... 무한대기

위 실행결과와 같이 멀티코어 프로세서 환경에서는 캐시에 저장된 suspended, stoped 변수의 값이 캐시에 저장되고 메모리에서 변경되었음에도 캐시의 저장된 변수가 갱신되지 않아 무한대기를 유지하는 결과입니다. 이 결과를 그림으로 표현하면 다음과 같습니다.

 

위 예제에서 suspended, stop 변수 앞에 volatile 키워드를 적용하면 CPU 코어가 변수의 값을 읽어올 때 캐시가 아닌 메모리에서 읽어오기 때문에 캐시와 메모리간의 값의 불일치가 해결됩니다.

 

 // before
 boolean suspended = false;
 boolean stopped   = false;
 
 // after
 volatile boolean suspended = false;
 volatile boolean stopped   = false;

 

정리하면 volatile이 필요한 이유는 멀티 프로세서 환경에서 다중 쓰레드를 수행할때 각각의 코어는 캐시를 가지게 되고 메모리에서 읽어온 값을 캐시에 저장하고 캐시에서 값을 읽어서 작업합니다. 이때 메모리에서 값이 변경되면 캐시와 저장된 값과 동기화가 되지 않아서 CPU 코어는 이미 캐시에 저장된 동기화 되지 않은 값을 읽어서 데이터 불일치를 발생시킵니다. 그래서 CPU 코어가 캐시에 저장된 값이 아닌 메모리에 저장된 값을 읽기 위해서 변수 앞에 volatile 키워드를 적용합니다.

 

2. volatile로 long과 double을 원자화

  • JVM은 4byte 단위로 처리하기 때문에 int를 포함한 작은 타입들은 한번에 읽거나 쓰는것이 가능합니다. 즉, 단 하나의 명령어로 읽거나 쓰기가 가능하기 때문에 다른 쓰레드가 선점할 수 없습니다.
  • long, double 타입은 8byte이기 때문에 다른 쓰레드에 의해서 선점될 수 있습니다.
  • long, double과 같은 변수에 volatile 키워드를 적용하면 다른 쓰레드들이 선점할 수 없도록 읽기 쓰기 연산이 원자화됩니다.
  • 주의점은 변수의 읽기나 쓰기가 원자화 됬을 뿐이지 동기화가 된 것은 아닙니다. 예를 들어 한 쓰레드가 동기화된 메소드인 withdraw()를 호출하여 계좌의 잔액을 바꾸고 있는데 동기화되지 않은 getBalance() 메서드를 호출하여 계좌의 잔액에 접근한다면 이는 데이터의 불일치가 발생할 수 있습니다.

References

source code : https://github.com/yonghwankim-dev/java_study/tree/main/ch13
[도서] Java의 정석, 남궁 성 지음