[JAVA] Threading issues (스레드 이슈)

이지우·2021년 2월 1일
0

JAVA

목록 보기
1/2

'스레드 이슈'란?

  • 스레드 간섭 오류
  • 메모리 일관성 오류

스레드 이슈 해결 방법

'스레드 이슈'란?

여러 스레드가 공유 데이터를 동시에 읽고 쓰려고 할 때 발생하는 문제이다.

  1. 스레드 간섭 오류 (Thread interference errors)
  2. 메모리 일관성 오류 (Memory consistency errors)

위 두 가지 유형의 문제가 발생하게 된다. 하나씩 살펴보자.

스레드 간섭 오류

- 상황

스레드 간섭 오류를 이해하기 위해 아래 예시를 보자.
Counter클래스에는 count값을 하나씩 올려주는 increament() 메소드와 현재 카운트값을 알려주는 getCount()메소드가 존재한다.

메인클래스를 보면, counter의 값을 1씩 1000번 증가시켜주는 기능을 10개의 스레드가 처리하고 있다.

과연 우리가 예상하는대로 getCount했을때 1000이 나올까 ???

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class RaceConditionExample {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        Counter counter = new Counter();
        
	// counter++
        for(int i = 0; i < 1000; i++) {
            executorService.submit(() -> counter.increment());
        }
        
	// 현재 작업중인것까지만 완료하고, 모든 작업이 완료되었는지 체크
        executorService.shutdown();
        executorService.awaitTermination(60, TimeUnit.SECONDS);	
    
        System.out.println("Final count is : " + counter.getCount());
    }
}

class Counter {
    int count = 0;

    public void increment() {
        count = count + 1;
    }

    public int getCount() {
        return count;
    }
}

결과 )
1000이 나올 때도 있지만, 대개 실행 마다 다른 값이 나오게 된다 !

스레드가 한개뿐이면 단 하나의 스레드가 increase()를 담당하게 되므로 count를 공유하지 않아 값이 예상대로 1000이 나오게 되는데,

예시에서는 10개의 스레드가 count에 접근하게 된다.
따라서 한 개의 공유자원(count)에 여러개의 스레드가 접근하려고 하면서 오차가 발생하게 된다.

좀 더 파고들자면 스레드가 increament()메소드를 실행시키면,

  1. count의 현재값을 검색
  2. 검색된 값을 1 증가
  3. 증가된값을 count에 저장

의 과정을 거치게 되기 때문에 만약 스레드 두 개가 둘 다 현재값을 0으로 봤다면, 두 개의 스레드A, 스레드B가 각각 count를 1했음에도 A의 결과를 B가 덮어버려서 결과는 2가 아닌 1이 된다. 이런 경우를 스레드 A와 B가 서로 간섭하고 있다고 한다.
이와 같이 결과를 예상할 수 없기 때문에 스레드 관련 버그는 찾아서 고치기가 어렵다.

메모리 일관성 오류

공유 메모리의 일관적이지 않은 모습때문에 발생하는 오류이다.
메모리 일관성 오류를 이해하기 위해 이번에도 예시를 들자.

- 상황

// 단순한 int형 필드가 선언 및 초기화되어 있음
int count = 0;

count는 두 개의 스레드 A와 B간에 공유되어지고 있다.
쓰레드 A는 count를 증가시키고,

// 스레드 A
count++;

곧바로 쓰레드 B는 count를 출력한다.

// 스레드 B
System.out.println(count);

증가시키고 출력하는 기능을 하나의 스레드가 같이 한다면 "1"이 출력될것이다.
하지만 두 기능이 다른 스레드에서 실행된다면 스레드A에서 있었던 변경을 스레드B가 감지할 수 있을것이라는 보장이 없기 때문에 "0"이 출력될수도 있다.
이를 처리해주기 위해서는 프로그래머가 두 개의 표현식 간에 선처리(happens-before)를 해주어야 한다. 바로 이 선처리 관계를 형성하는 방법이 동기화(Synchronization)이다.

해결방안

이렇게 여러 스레드가 공유 변수를 동시에 읽고 쓰려고 할 때 작업이 실행에서 겹치는 경우가 생기고, 이를 Race Condition(경쟁 조건)이라고 한다.
여기서 이 공유 변수가 액세스되는 구역을 Critical Section(임계 구역)이라고 하고,
공유 변수에 대한 액세스를 동기화하여 스레드 간섭 오류를 피할 수 있다.

// 동기화된 메소드들, 생성자는 동기화할 수 없다(문법오류남)
class SynchronizedCounter {
    private int count = 0;

    public synchronized void increment() {
        count = count + 1;
    }

    public synchronized int getCount() {
        return count;
    }
}

결과 )
원하던 예상 결과값을 얻을 수 있다 !

메소드들을 동기화한 경우 두 가지 효과를 가진다.

  • 동일 객체 상에서 동기화된 메소드의 두 번의 호출이 서로 간섭할 수 없다. 하나의 스레드가 어떤 객체의 동기화된 메소드를 실행 중일 때, 그 객체의 동기화된 메소드를 호출하려는 다른 모든 스레드들은 첫번째 스레드가 해당 객체의 작업을 끝낼때까지 실행이 차단된다.
  • 동기화된 메소드는 이어서 발생하는 동일 객체의 동기화된 메소드의 호출들과 자동으로 선처리관계를 수립하므로, 모든 스레드들은 이 객체의 상태변화를 인지할 수 있다.

+) 하지만 동기화는 두 개 이상의 스레드가 같은 자원을 동시에 접근하는 스레드 경쟁을 일으킬 수 있고, 이 경우 자바 런타임이 하나 이상의 스레드를 더 느리게 실행되도록 만들거나, 실행을 중단시킬 수 있다.
이런 현상들은 Starvation(기아 상태)와 Livelock(라이브락)으로 나타난다.

[참고] https://www.daleseo.com/synchronization/

profile
개발 관찰일지

0개의 댓글