우리네 장

가시성 feat.volatile 본문

OS

가시성 feat.volatile

qpmi1zm29 2023. 12. 27. 03:39
최근 모던 컴퓨팅 환경에서는 우리가 작성한 명령어를 실행할 때, JVM이 최적화를 진행한다.

이 최적화 과정은 동시 메모리 접근이 없음을 기본 전제로 하며, 컴파일러에게 임의의 변수가 공유자원임을 명시해주면 생략되는 과정이기도 하다.

 

1. cpu 데이터 캐싱

cpu의 처리속도 < RAM 에서 데이터를 로딩하는 속도 는 아주 많은 차이가 난다.

매번 RAM에서 연산에 요구되는 값을 가져와 처리를 하면, 로딩 시간을 기다리느라 cpu가 낭비될 것이다.

 

cpu 캐싱은 l1~l3 같은 cpu 내부 캐시 메모리에 필요한 데이터 블록을 최초 로딩하고, 이후 참조가 있을 경우 캐시 메모리를 참조하여 데이터 접근 속도를 줄여 cpu 낭비를 예방할 수 있다.

 

캐싱 메모리에서 변경된 데이터는 바로 RAM과 동기화 되지 않는다. 해당 작업은 비용이 많이 쓰이기 때문에 일정 시간이 지나거나 반드시 필요한 경우 동기화 작업을 진행한다. → 이 때문에 가시성에 문제가 발생한다.

 

2. 명령어 재배치

 

x = x 할당 값

 

y = y 할당 값

z = x * y

 

위 같은 명령어가 있을 때, 컴파일러는 처리 속도를 높이기 위해 명령어를 임의로 재배치 한다.

순서가 변경되어도 문제가 없을 것으로 보이는 명령어들은 주로 병렬적으로 동시에 처리하거나 ( x,y 동시 할당 ),

필요에 의해 실행 순서를 뒤바꾸어 ( y를 먼저 할당하고 후에 x 값을 할당 ) 처리한다. 

( 그러나 z연산을 수행한 후 x나 y 연산을 수행하는 일은 없다. )

 

 

가시성에 문제가 생기는 경우 ?

 

public class Main {
    private static boolean a = false;

    public static void main( String[] args ) {

        Runnable hello = () -> {
            for ( for int i = 0; i < 1000; i++ ) {
                System.out.println( " hello : " + i );
            }
            a = true;
        }

        Runnable goodbye = () -> {
            int i = 1;
            while ( !a )
                a++;

            System.out.println( "goodbye : " + i );
        }
    }
}

 

 

예상되는 결과는 hello : 0 ~ hello : 999 가 찍힌 후에, goodbye의 while 문을 빠져나가 goodbye : 1000이 찍히는 것이다.

 

그러나 실제로 코드를 실행해보면 goodbye : 1000 가 찍히지 않고 프로세스가 종료되지 않는 경우가 있다.

a = true 가 효력이 없어 보인다. 왜 그럴까??

 

while ( !a ) a++; 은 할당과 관련된 코드가 없기 때문에 재배치를 하게 되면 → if ( !a ) while ( true ) a++; 로 변경된다.

 

만약 hello와 goodbye가 서로 다른 스레드를 통해 각각의 코어에서 실행이 된다면,

hello가 a를 cpu1의 캐시에 로드하여 값을 true로 변경한다고 해도,

goodbye는 cpu2를 통해 실행되고 있기 때문에 최초 로드 시 RAM을 통할 것이고 a = false로 인식할 것이다.

 

만약 hello와 goodbye가 같은 cpu에서 컨텍스트 스위칭을 통해 실행된다고 하면 같은 캐시 메모리를 사용하여 문제가 없었을 것이다.

 

멀티 코어를 사용하는 최근의 컴퓨팅 환경에서는 종종 일어나는 현상이다.

 

 

JVM이 변수 업데이트를 보이는 방법

1. final 변수 값은 초기화 이후 보인다.
2. static 변수 값은 클래스가 로딩된 이후 보인다.
3. volatile 변수는 변경 사항이 즉시 보인다.
4. 잠금을 해제 하기 전에 일어난 변경은 같은 잠금을 취득한 쪽에 보인다.

 

위 코드와 같은 경우는 a 변수를 volatile로 설정하면 해결이 된다.

private static volatile int a = false;

 

volatile 로 선언한 변수는 캐시 메모리를 거치지 않고, 바로 RAM을 통해서 읽고 쓰기 작업을 진행하기 때문에

서로 다른 cpu 코어를 사용 하더라도  바로 변경 사항을 확인 할 수 있다.

물론 그 만큼 부하가 생겨 처리 속도에 영향을 주기도 한다.

그래서 공유 변수를 volatile로 선언하는 것은 일반적인 해결 방법이 아니긴 하다.

 

 

'OS' 카테고리의 다른 글

[ 동기화 #5 ] 모니터 in JAVA  (2) 2023.12.27
[ 동기화 #4 ] 모니터  (1) 2023.12.27
[ 동기화 #3 ] 세마포어  (1) 2023.12.27
[ 동기화 #2 ] 뮤텍스  (3) 2023.12.27
[ 동기화 #1 ] 스핀락  (0) 2023.12.27