Computer Science/Java

멀티 스레드 환경에서 발생하는 이슈

eunnnn 2023. 4. 17. 03:32

프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원(resources)과 쓰레드로 구성되어 있다. 프로세스의 자원을 이용해서 실제 작업을 수행하는 것이 바로 쓰레드이다.

 

하나의 프로세스는 하나 이상의 쓰레드를 가지며, 둘 이상의 쓰레드를 가진 프로세스를 '멀티쓰레드 프로세스(multi-threaded process)'라고 한다. 멀티 스레드 환경에서는 여러 스레드가 동시에 하나의 자원을 공유한다. 그렇기 때문에 같은 자원을 두고 경쟁상태(raceCondition)와 같은 문제가 발생하는 것이다.

 

가시성 이슈

가시성 문제는 여러 개의 스레드가 사용됨에 따라, CPU Cache Memory와 RAM의 데이터가 서로 일치하지 않아 생기는 문제를 의미한다. 한 스레드가 변경된 값을 cache memory에서 ram에 데이터를 저장하기 전, 다른 스레드에서 Ram에서 해당 값을 읽어 변경되기 이전의 값을 처리하게 되는 상황을 가시성이 보장되지 않는다 라고 말한다. 

CPU1은 7까지 증가했으나, CPU2의 cache는 아직도 0으로 알고있다.

 

가시성을 제어하는 방법 - volatile

이 상황의 해결 방법은 가시성이 보장되어야하는 변수를 cache memory에서 읽는 것이 아니라, Ram에서만 읽도록 보장하는 것이다. 이를 위해 volatile 키워드를 사용한다.

  • volatile 키워드는 변수를 'Main Memory에 저장하겠다'라고 명시하는 것이다.
  • 변수의 값을 Read할 때마다 CPU cache에 저장된 값이 아닌, Main Memory에서 읽는 것이다.
더보기

private static volatile boolean isStop;


동시성 이슈

. 여러 스레드가 동시에 같은 인스턴스의 필드 값을 변경하려고 할 때 발생하는 문제를 의미한다. (자원을 읽기만 한다면 문제가 발생되지 않는다)

 

동시성을 제어하는 방법

1. synchronized 키워드 사용

동시성을 해결하는 데 가장 간단하면서 쉬운 방법은 Lock을 걸어 버리는 것이다.
이는 문제가 된 메서드, 변수에 각각 synchronized라는 키워드를 넣는 것으로 구현될 수 있다. synchronized 가 선언된 블럭에는 동시에 하나의 스레드만 접근할 수 있다.

class Count {
    private int count;
    public synchronized int view() {return count++;}
}

class Count {
    private Integer count = 0;
    public int view() {
        synchronized (this.count) {
            return count++;
        }
    }
}

한 스레드가 synchronized 메서드를 호출하면 해당 메서드의 작업이 끝날때까지 다른 스레드에서는 synchronized 메서드를 호출하지 못한다. 스레드가 synchronized 메서드를 실행하는 경우 자동으로 lock을 획득하고, 메서드가 반환될 때 lock을 해제하는 과정을 거친다.

 

그러나 synchronized 키워드에는 단점 또한 존재한다.

synchronized 는 선언된 메서드의 코드 섹션 전체에 락을 걸고 접근하는 스레드들은 block or suspended 상태로 변경되게 된다. 스레드들이 blocking 되는 과정과 다시 resuming 되는 과정에서 시스템의 자원을 소모하게 된다. 100개의 스레드가 동시에 접근을 한다면, 99개의 스레드가 이러한 과정을 거치게 되는 것이다. 바로 이 부분에서 성능 저하가 발생한다.

 

 

2.  Atomic, Concurrent 패키지의 사용

concurrent패키지에 존재하는 컬랙션들은 락을 사용할 때 발생하는 성능 저하를 최소한으로 만든다. 

class Count {
    private AtomicInteger count = new AtomicInteger(0);
    public int view() {
            return count.getAndIncrement();
    }
}

위 코드에서는 Integer 대신 AtomicInteger가 사용되었다. Collection의 경우 ConcurrentHashMap 같은 클래스들이 제공된다.

 

Atomic과 Concurrent 클래스들은 상호 배제를 위해 CAS 알고리즘과 volatile 키워드를 이용해 구현되었다.

CAS는 Compare-And-Swap의 줄임말로 말 그대로 비교하고 변경한다. 어떤 값을 변경하려고 한다고 가정했을 때, 먼저 내가 갖고 있는 값과 메모리에 위치한 값이 일치하는지 비교한다. 만약 두 값이 일치한다면 내가 변경을 원하는 값으로 변경한다. 일치하지 않을 경우 기존 교체가 실패되고, 이에 대해 계속 재시도하는 방식이다.

 

 

출처

더보기