Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

희디비

[Java] 메모리 가시성, 임계 영역 본문

Java

[Java] 메모리 가시성, 임계 영역

희디비 2024. 11. 15. 17:45

 

메모리 가시성

 

메모리 가시성을 알아 보기 위해 예제를 만들었습니다.

스레드A의 작업 : while( flag )

만약 메인 스레드에서 스레드A의 flag 값을 바꾼다면 작업이 중단 될까요?

public static void main(String[] args) {
        RoofTask task = new RoofTask();
        Thread thread = new Thread(task, "Thread-A");
        log("스레드-A 시작" + task.flag);
        thread.start();

        sleep(1000);
        log("스레드-A 루프 종료 요청");
        task.flag = false;
        log("task.flag = " + task.flag);
        log("메인 종료");
    }

    static class RoofTask implements Runnable{

        boolean flag = true;

        @Override
        public void run() {
            log("작업 시작");
            while(flag){

            }
            log("작업 완료");
        }
    }

 

실행 결과 메인 스레드는 종료 되었지만 스레드 A는 작업 완료가 되지 않습니다.

flag는 false로 바뀌었는데 왜 작업이 종료 되지 않을까요?

이 문제가 바로 메모리 가시성 문제 입니다.

 

 

메모리 영역에 있는 flag 값을 읽어서 실행 한다고 생각 되지만 실제론 그렇지 않습니다.

 

 

스레드는 캐시 메모리를 활용 하여 연산을 합니다. 왜 그럴까요?

  • 메인 메모리에 접근 하는 것 보다 CPU에 가까운 캐시 메모리를 통해 연산 하는 것이 효율적 입니다.
  • 메인 메모리 보다 캐시 메모리의 연산이 더 빠르기 때문 입니다.

캐시 메모리를 사용 함으로서 성능상의 이점을 가져 올 수 있습니다.

 

스레드가 어떻게 메인 메모리의 값을 읽어 오는지 순서대로 알아 보겠습니다.

  1. 스레드 시작시 메인 메모리 flag 값 ( true )를 읽습니다.
  2. 값 ( true )를 캐시 메모리에 저장 합니다.
  3. 스레드 작업의 결과 ( false )를 캐시 메모리에 저장 합니다.

즉 캐시 메모리의 값만 변하고 메인 메모리에는 즉시 반영이 되지 않아서 문제가 발생 한 것입니다.

그렇다면 캐시 메모리 값이 언제 메인 메모리에 업데이트 될까요?

메인 메모리의 값을 언제 캐시 메모리가 읽을까요?

이 부분은 CPU 설계 방식, 실행 환경에 따라 다릅니다.
주로 컨텍스트 스위칭이 될때 캐시 메모리가 갱신이 되는데 예시로
Thread.sleep(), 콘솔 출력 할 때 컨텍스트 스위칭이 되면서 주로 갱신 되지만 보장 하는 것은 아닙니다.

 

그러면.. 메모리 가시성 문제는 어떻게 해결 할까요?

해결 방안은 캐시 메모리 성능 향상을 포기 하고 메인 메모리에 접근 하도록 하는 것입니다.

자바에선 vloatile 키워드를 제공 합니다.

volatile boolean flag = true;

 

vloatile을 사용 함으로서 성능은 느려졌지만 메모리 가시성 문제는 해결할 수 있습니다.

메모리 가시성 문제는 vloatile, synchronized, lock을 사용 하면 발생 하지 않습니다.

 


 

임계 영역 ( Critical Section )

 

임계영역에 대해 알아 보기 위해 간단한 예제를 보겠습니다.

마켓은 물건 개수가 정해져 있고 구매자가 원하는 개수 만큼 물건을 구매 합니다.

이때 두 구매자가 동시에 물건을 구매 해보겠습니다.

  public static void main(String[] args) throws InterruptedException {
        Market market = new MarketV1(10);
        Thread t1 = new Thread(() -> market.selling(5), "buyer-1");
        Thread t2 = new Thread(() -> market.selling(6), "buyer-2");

        t1.start();
        t2.start();

        t1.join();
        t2.join();
        log("[최종] 남은 물건 개수 : " + market.getSelling());
    }

    private int count;

    @Override
    public boolean selling(int buyCount) {
        log("[판매 시작]" + getClass().getSimpleName());
        log("[검증 시작] 구매 개수 : " + buyCount + " 남은 물건 수 " + count);

        if(count < buyCount){
            log("[검증 실패] 마켓의 물건 개수가 부족 합니다.");
            return false;
        }
        log("[검증 통과] 구매 개수 : " + buyCount + " 남은 물건 수 : " + count);

        sleep(1000);
        count -= buyCount;
        log("[판매 완료] 구매 개수 : " + buyCount + " 남은 물건 수 " + count);
        return true;
    }

    public MarketV1(int count) {
        this.count = count;
    }

    @Override
    public int getSelling() {
        return count;
    }

 

 

구매자가 동시에 물건을 구매 했습니다. 그런데 남은 물건 개수가 이상합니다.

예상한 결과는 5개를 구매 했으니 물건 5개가 남고 다음 구매자는 검증에 실패 한다고 생각 했습니다.

하지만 둘 구매자 모두 구매에 성공 했습니다. 왜 둘 구매자 모두 물건을 구매 할 수 있었을까요?

 

이유는 두 스레드가 동시에 접근 했기 때문입니다.

 

count -= buyCount;

 

위 코드는 3가지 작업을 하는 코드입니다

  1. 남은 재고인 count를 읽는다
  2. 남은 재고 count - 사는 개수 buyCount를 연산 한다.
  3. 결과 값은 좌변 count에 바인딩 한다.

검증 로직을 통과 한 것도 문제지만, 남은 재고 값을 계산 할 때도 문제가 발생 합니다.

만약  t1 스레드, t2 스레드 둘다 재고 count를 읽었을 때 재고가 10개 라면 어떻게 될까요?

  • t1 스레드 : 10개 - 5개 = 5개
  • t2 스레드 : 10개 - 6개 = 4개

두 스레드가 10개 재고를 읽었다면 실행 순서에 따라 남은 재고가 5,4개로 될 것 입니다.

 

다른 경우도 있습니다. t1 스레드가 먼저 재고를 업데이트 한다면 어떻게 될까요?

  • t1 스레드 : 10개 - 5개 = 5개
  • t2 스레드 : 5개 - 6개 = -1개

이렇게 된다면 재고가 -1개 남을 것 입니다.

 

공유 필드 값인 재고 count에 여러 스레드가 접근 하며 문제가 발생 했습니다.

여러 스레드가 동시에 접근 했을 때 데이터의 불일치나 예상치 못한 동작이

발생할 수 있는 코드 부분을 임계영역 이라고 합니다.

 

그렇다면 하나의 스레드만 임계영역에 들어 갈수 있게 하면 안되나요? 라는 질문이 생깁니다.

자바에서 그렇게 해결한 것이 synchronized 입니다. ( 자바 1.0 )

 public synchronized boolean selling(int buyCount)
 public synchronized int getSelling()

 

메서드에 synchronized을 통해 임계영역을 설정 하고 코드를 실행 해보겠습니다.

 

이제 원하는 대로 코드가 동작 하는 것을 볼수 있습니다.

synchronized는 어떻게 동작 하는 것일까요?

사실 자바 인스턴스 내부엔 모두 모니터 락을 가지고 있습니다.

임계영역에 들어 가기 위해선 락을 얻어야 들어 갈 수 있습니다.

 

코드의 동작 방식을 나타낸다면

  1. t1 스레드가 락을 얻어 물건을 구매하고 5개로 재고를 업데이트 합니다.
  2. t2 스레드는 락이 없어 Blocked 상태로 기다립니다.
  3. t1 스레드가 임계영역 종료 후 락을 반납 합니다.
  4. t2 스레드가 Blocked 상태에서 Runnable 상태로 깨어 납니다.
  5. t2 스레드가 물건을 구입 하려고 헀으나 재고가 부족하여 구매 하지 못했습니다.

synchronized는 동시성 문제를 해결 한다는 장점이 있지만 단점도 존재 합니다.

  • lock을 얻을때 까지 깨울 수 없는 Blocked 상태가 됩니다.
  • synchronized가 종료 될 때 락을 반납 하고 다음 스레드가 실행 됩니다.
  • 이때 다음 스레드는 기다린 순서를 보장 하지 않습니다.

위 글을 김영한 선생님 실전 자바 고급 1편을 보고 요약 한 것입니다.

자세한 내용이 궁금 하시다면 강의를 추천 드려요!

 

김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 강의 | 김영한 - 인프런

김영한 | 멀티스레드와 동시성을 기초부터 실무 레벨까지 깊이있게 학습합니다., 국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바 문법을 안다?

www.inflearn.com

 

'Java' 카테고리의 다른 글

[Java] 생산자 소비자 문제  (0) 2024.11.25
[Java] LockSupprot, ReentrantLock  (1) 2024.11.19
[Java] Join, Interrupt, Yield  (0) 2024.11.12
[Java] 스레드의 생성과 생명주기  (0) 2024.11.10
[Java] 프로세스와 스레드  (4) 2024.11.09