siino's 개발톡

[Java] Thread와 동기화에 대해 알아보자. 본문

Java

[Java] Thread와 동기화에 대해 알아보자.

siino 2024. 1. 13. 23:17

스레드란?

스레드(thread)란 프로세스(process) 내에서 실제로 작업을 수행하는 주체를 의미합니다.

모든 프로세스에는 한 개 이상의 스레드가 존재하여 작업을 수행합니다.

즉, 스레드는 프로세스 내의 작업의 흐름을 의미합니다.

 

Java에서 스레드를 생성하는 방법

Java에서 스레드는 다음과 같은 2가지 방식으로 생성할 수 있습니다.

(저는 익명 구현 객체방식으로 구현했습니다.)

1. Runnable 인터페이스 구현

Runnable runnable = new Runnable() {
            @Override
            public void run() {
                //실행할 작업
            }
        };
Thread thread = new Thread(runnable);
thread.start();

2. Thread 클래스 상속

Thread thread = new Thread() {
            @Override
            public void run() {
                super.run();
            }
        };
        thread.start();

 

위 코드에서 조금 의아할 수 있는데 왜 run()메서드를 재정의 해놓고 호출할때는 start()메서드를 호출할까요?

그 이유를 알기 위해서는 스레드의 "상태"에 대해서 알야야 합니다.

스레드의 상태

public enum State{

	NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;
}

java 스레드의 상태 종류

스레드는 만들어지면 NEW라는 상태를 갖게 되고 start()메서드를 호출하게 되면 바로 실행되는 것이 아니라 RUNNABLE한 상태로 바뀌게 됩니다.

스레드의 실행은 전적으로 JDK, OS, CPU스케쥴러에 의존하기 때문에 언제 실행될지는 알 수 없습니다.

따라서,

1. 우리는 start()메서드만 호출하여 스레드의 상태를 RUNNABLE로 바꿔만 준 후 

2. 스케줄러에 의해 해당 스레드가 선택되면, RUNNABLE 상태의 스레드는 RUNNING 상태가 되어 실제로 실행됩니다 . (이때 우리가 재정의한 run메서드를 실행시키는 것이죠)

 

스레드 제어

위 그림에서 보이는 것처럼 스레드는 NEW, RUNNABLE, RUNNING상태 이외에도

TIMED_WATING, WAITING, BLOCKED, TERMINATED상태를 갖습니다.

각각에 대해 조금 더 자세하게 알아보겠습니다.

 

TIMED_WATING: 실행가능하지 않은 일시정지 상태 (지정된 시간동안)

WAITING: 실행가능하지 않은 일시정지 상태 (특정 조건이 만족되기 전까지 무한)

BLOCKED: 동기화 블럭에 의해 일시정지된 상태(lock이 풀릴 때까지 기다림)

TERMINATED: 스레드 작업이 종료된 상태

notify() 대기중인 스레드 중 하나를 깨우기 위해 사용합니다.
wait() 다른 스레드가 notify 또는 notifyAll을 호출할 때까지 현재 스레드를 대기상태로 만듭니다.
sleep() 현재 스레드를 지정된 시간동안 일시중지 시킵니다. 이는 스레드를 대기상태로 만들지만, 락을 해제하지는 않습니다.
join() join을 호출한 스레드는 지정한 스레드가 종료될 때까지 대기합니다. 시간 제한을 설정할 수도 있습니다.

 

자, 그럼 스레드에 대해서 조금 알게 된 것 같으니 무작정 사용하면 될까요?  답은 NO..... 

스레드는 주의해서 사용해야합니다.

스레드에서 발생한 오류가 프로세스 전체에 영향을 미치는 심각한 오류일 경우 전체 프로세스가 다운될 수 있기 때문입니다.

(구글 Chrome  VS  Internet Explorer)

멀티 스레드 환경에서 주의점(race condition)

더보기

공유 자원: 멀티 스레드 프로그래밍에서 여러 스레드가 함께 공유하는 변수

임계 영역: 한번에 한개의 스레드만 접근해서 작업할 수 있는 코드 영역

경쟁 상태(race condition): 여러 개의 스레드가 동시에 공유 자원에 접근하고 이를 수정할 때, 타이밍이나 순서에 따라 그 결과가 달라질 수 있는 상황

멀티스레드 환경에서 '공유자원'이란, 여러 스레드가 동시에 접근할 수 있는 자원들을 의미합니다.

여러 개의 스레드가 동시에 공유 자원에 접근하고 이를 수정할 때, 타이밍이나 순서에 따라 그 결과가 달라질 수 있는 상황이 발생할 수 있습니다.  (Race condition)

 

ex ) 경쟁 상태의 예 - Ready-Modify-Write / Check-then-act

이러한 Race condition문제를 해결하기 위해서 원자성가시성을 보장해야합니다.

 

원자성 - 공유 자원에 대한 작업의 단위가 더 이상 쪼갤 수 없는 하나의 연산인 것처럼 동작하는 것.

(연산 사이의 텀에 다른 스레드의 연산 개입을 방지할 수 있음)

(기계어(machine instruction)으로 인한 문제)

 

가시성 - CPU가 메인 메모리의 값을 CPU cache에 불러와 연산을 할때, 

작업을 하는 변수가 실제 메모리에 올라와 있는 변수의 값과 달라서 발생하는 문제를 해결하기 위한 원칙

[CPU가 Memory에서 값을 읽어들여오고 다시 쓰고 하는 시간을 아끼기 위함.]

(CPU - Cache - Memory 관계상의 문제)

가시성 보장 - volatile키워드, volatile로 선언된 변수를 CPU에서 연산을 하면 바로 메모리에 쓴다.

 

이렇게 원자성과 가시성을 보장하기 위한 방법을 동기화라고 합니다.

(스레드가 수행되는 시점을 조절하여 서로가 알고있는 정보가 일치하는 것)

 

동기화

동기화의 방법은 크게 소프트웨어/하드웨어/프로그래밍 언어 수준으로 나눌 수 있습니다.

본 글에서 이 개념까지 설명하기엔 양이 너무 많아져서 키워드만 나열하겠습니다.

 

소프트웨어 수준 - Mutex / Semaphore

하드웨어 수준 - CAS (Compare And Set)

프로그래밍 언어 수준 - 모니터

Java의 동기화

자 그럼 실제 Java에서는 동기화를 어떻게 구현해야 할까요?

실제 동기화 하는 방법은 많지만 가장 대표적이고 기본인 synchronized, Atomic을 설명하겠습니다.

 

1. synchronized

자바의 synchronized 키워드는 자바의 대표적인 동기화 기법입니다.

java의 synchronized로 동기화는 내부적으로 Mutex와 Monitor로 구현되어 있습니다.

synchronized키워드는 메서드 또는 코드 블럭으로 사용할 수 있고, 임계 영역으로서 상호배제를 구현합니다.

//함수에 선언
public synchronized void method() {
    // 임계 영역
}

//코드 블럭으로 선언
public void method() {
    synchronized(this) {
        // 임계 영역
    }
}

 

또한 Java의 모든 객체는 모니터를 구현하고 있기 때문에 (Object 클래스에 정의되어 있음) synchronized 블럭 내부에서 스레드 조작을 할 수 있습니다.

synchronized가 붙은 메서드가 한 스레드가 실행중일 때 다른 스레드가 접근하지 못하지만 다른 일반 메서드는 접근 가능합니다.

특징: Blocking 방식, Mutex, 모니터, 비관적 락

 

 

2. Atomic 변수

동시성을 보장하기 위해서 자바에서 제공하는 Wrapper class

연산의 원자성을 보장하는 atomic 클래스를 활용해서 동기화를 적용합니다.

내부적으로 낙관적 락 기법인 Compare-And-Set을 사용합니다. 이는 락을 사용하지 않고, 대신 연산이 성공적으로 수행될 때까지 반복적으로 시도합니다.

특징: Non-Blocking 방식, 낙관적 락

 

결론

결국, 우리의 최종 목적은 멀티스레드 환경에서 Thread Safe한 상황을 만족해야합니다.

Thread Safe 란?
여러 스레드가 동시에 클래스를 사용하려는 상황에서 클래스 내부의 값을 안정적인 상태로 유지할 수 있다.

 

Java에서 동기화를 하기 위해서 synchronized와 같은 키워드를 붙여야합니다.

공유 변수가 있을 때, Multi Thread의 안정성을 확보하기 위해 여기저기 synchronized 혹은 lock을 남발한다면 성능에 큰 오버헤드와 코드의 복잡성을 증가시킬 수 있고 결국 Dead-Lock이 발생할 가능성도 있습니다.

공유변수를 최소화하자!

 

따라서 우리는 스레드간에 공유하는 변수를 최소화 하여 최대한 동기화에 신경쓰지 않게 하는 것이 가장 좋습니다.

불가피하게 공유하는 변수가 생기게 된다면 상황에 맞는 동기화 전략을 선택하고 해당 전략에 맞도록 코드를 설계하면서 문서화도 가독성 좋게 해야할 것입니다.

 

-끝-

'Java' 카테고리의 다른 글

[Java] JVM이 도대체 뭐야?!  (1) 2024.01.17
[Java] 예외 클래스와 try-catch-finally / try-with-resource  (0) 2024.01.11