Haneul's Blog

[Java] 멀티쓰레드 프로그래밍 본문

Java

[Java] 멀티쓰레드 프로그래밍

haneulss 2022. 10. 22. 21:37

목표 [백기선 자바 스터디 10주차]

자바의 멀티쓰레드 프로그래밍에 대해 학습하세요.

학습할 것

  • Thread 클래스와 Runnable 인터페이스
  • 쓰레드의 상태
  • 쓰레드의 우선순위
  • Main 쓰레드
  • 동기화
  • 데드락

 

먼저 간단하게 Thread가 무엇인지와 Process가 무엇인지 그리고 둘의 차이점이 어떤 점이 있는지부터 간단하게 알아보고 가겠습니다.

 

Process와 Thread

먼저 프로세스는 실행 중인 프로그램으로 어떤 프로그램이 운영체제에 의해서 메모리 공간을 할당받아 실행 중인 것을 말합니다. 이러한 프로세스는 프로그램에 사용되는 데이터와 메모리 등의 자원 그리고 쓰레드로 구성이 됩니다.

 

그렇다면 쓰레드는 무엇일까요?

쓰레드는 프로세스 내에서 실제로 작업을 수행하는 주체로, 모든 프로세스는 하나 이상의 쓰레드를 가져서 작업을 수행합니다.
만약 두 개 이상의 쓰레드를 가지는 프로세스가 있다면 이를 멀티 쓰레드 프로세스라고 합니다.

 

둘의 차이점은 프로세스는 프로세스들끼리 메모리 공간을 공유하지 않지만 쓰레드는 프로세스 내의 메모리 공간을 스택 영역을 제외하고 모두 공유한다는 점이 가장 큰 차이점입니다.
그렇기 때문에 컨텍스트 스위칭을 하는 비용이 멀티 프로세스보다 멀티 쓰레드가 더 적게 든다는 장점이 존재하게 됩니다. 하지만 메모리 공간을 공유하여 속도가 빨라진다는 것이 무조건적으로 좋아보이지만 꼭 그런 것 만은 아닙니다. 메모리 공간을 공유하기 때문에 공유 자원에 동시에 접근할 수 있게 되어 프로그램에 문제가 생길 수 있고, 이를 방지해주어야 해서 조금 더 개발하기 까다롭다는 단점도 있습니다.

 

이번에는 이러한 쓰레드를 자바에서 어떻게 생성하는지 알아보겠습니다.

Thread 클래스와 Runnable 인터페이스

자바에서 쓰레드를 생성하는 방법은 크게 두 가지가 있습니다.

  • Runnable 인터페이스 사용
  • Thread 클래스 사용

사실 Thread 클래스는 Runnable 인터페이스를 구현한 클래스로 둘 모두 java.lang 패키지에 포함되어 있습니다.

 

그렇다면 둘 중 아무거나 사용해도 되는 걸까요?

상황에 따라서 선태하면 좋습니다. Thread 클래스를 확장할 필요가 있을 경우에는 Runnable 클래스, 그렇지 않은 경우는 Thread 클래스를 사용하는 것이 편합니다.

 

또한 쓰레드는 실행한 순서대로 동작되진 않습니다. 컴퓨터가 몇 개의 쓰레드를 가지고 있는지에 따라서 달라질 수 있고 매번 결과가 달라집니다.

 

Thread 클래스 사용 예시 -> 재활용 불가능

 

Runnbale 클래스 사용 예시 -> 재활용 가능

위의 예시를 보면 약간 이상한 점이 있습니다. run() 메서드를 재정의하여 구현하였는데 실행할 때는 start() 메서드를 사용하고 있는 것을 볼 수 있습니다. 왜 이렇게 사용하는 걸까요?
사실 run() 메서드를 실행하면 쓰레드가 동작하는 것이 아니라 그냥 클래스의 메서드를 호출하는 것입니다. start()를 호출하여 실제 쓰레드를 동작시킬 수 있기 때문에 쓰레드를 사용하기 위해서는 start()를 사용하면 됩니다.

 

쓰레드의 상태

상태 열거 상수 실행
객체 생성 NEW 스레드 객체가 생성 후, 아직 start() 메소드가 호출되지 않은 상태
실행 대기 RUNNABLE 실행 상태로 언제든지 갈 수 있는 상태
일시 정지 WAITING 다른 스레드가 통지할 때까지 기다리는 상태
TIMED_WAITING 주어진 시간 동안 기다리는 상태
BLOCKED 사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태
종료 TERMINATED 실행을 마친 상태

getState() 메서드로 위의 상태를 확인할 수 있습니다.

 

쓰레드의 상태는 메서드를 통해 제어할 수 있는데 일반적으로 start()를 통해 쓰레드를 실행 가능한 상태로 만들면 run 메서드에 의해 코드가 실행되고 모든 작업을 마치면 TERMINATED 상태가 되지만, 메서드를 이용해 쓰레드를 정지시키거나 다시 실행시킬 수 있습니다.

 

메서드 설명
static void sleep(long millis)
static void sleep(long millis, int nanos)
지정된 시간(밀리세컨드, or 나노세컨드)동안 쓰레드를 일시정지시키다. 지정한 시간이 지나고 나면, 자동적으로 다시 실행대기 상태가 된다.
void join()
void join(long millis)
void join(long millis, int nanos)
지정된 시간동안 쓰레드가 실행되도록 한다. join()을 호출한 쓰레드는 그동안 일시정지 상태가 된다. 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 실행을 계속한다.
void interrupt() sleep()이나 join()에 의해 일시정지 상태인 쓰레드를 깨워서 실행대기 상태로 만든다. 해당 쓰레드에서는 InterruptedException이 발생함으로써 일시정지 상태를 벗어나게 된다.
void stop() 쓰레드를 즉시 종료시킨다.
void suspend() 쓰레드를 일시정지시킨다. resume()을 호출하면 다시 실행대기 상태가 된다.
void resume() suspend()에 의해 일시정지 상태에 있는 쓰레드를 실행대기 상태로 만든다.
static void yield() 실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보하고 자신은 실행대기 상태가 된다.

stop(), suspend(), resume()은 쓰레드를 교착상태(dead-lock)로 만들기 쉽기 때문에 deprecated 되었습니다.

 

프로세스 상태 전이도

  1. 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아닌 실행 큐에 저장되어 자신의 차례가 될 때까지 기다리다가 자기 차례가 된다면 실행상태가 됩니다.
  2. 할당된 실행시간이 다되거나 yield() 메서드를 만나면 다시 실행 대기 상태가 되고 다음 쓰레드가 실행상태가 됩니다.
  3. 실행 중 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있습니다.
    (I/O block은 입출력 작업에서 발생하는 지연 상태를 말하고, 사용자의 입력을 받는 경우를 예로 들 수 있습니다.)
  4. 지정된 일시정지 시간이 다 되거나 notifi(), resume(), interrupt()가 호출되면 일시정지상태를 벗어나 다시 실행 큐에 저장되어 자신의 차례를 기다리게 됩니다.
  5. 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸됩니다.

 

쓰레드의 우선순위

쓰레드에는 우선 순위라는 속성이 있고, 이 우선순위에 따라서 쓰레드가 얻는 실행시간이 달라지게 됩니다. 그렇기에 쓰레드가 수행하는 작업의 중요도에 따라서 쓰레드의 우선순위를 다르게 지정하여 특정 쓰레드가 더 많은 작업 시간을 갖게 할 수 있습니다.

우선순위 속성의 값은 1~10까지 가질 수 있고 숫자가 높은 수록 높은 우선순위를 갖습니다.(기본값 5) 즉, 많은 작업 시간을 가진다고 할 수 있습니다.

 

 

Main 쓰레드

Java의 실행환경인 JVM은 하나의 프로세스로 실행되고, 실제 실행의 단위는 쓰레드이기 때문에 Java를 실행하기 위해서는 main() 메서드를 실행해야 합니다. 즉, main() 메서드가 Main Thread의 시작점이라고 할 수 있습니다.

 

Daemon 쓰레드

Main 쓰레드의 작업을 돕는 보조적인 역할을 하는 쓰레드로, Main 쓰레드가 종료된다면 데몬 쓰레드는 강제적으로 종료됩니다. 저희가 알고 있는 데몬 쓰레드는 대표적으로 JVM의 가비지 컬렉터가 있습니다.

 

그렇다면 왜 데몬 쓰레드가 있는 걸까요? 그냥 쓰레드를 사용하면 되는 것 아닌가요?

예를 들면 모니터링을 하는 쓰레드를 별도로 띄워 모니터링은 하는데, Main 쓰레드가 종료되면 관련된 모니터링 쓰레드가 종료되어야 프로세스가 종료될 수 있습니다. 이러한 모니터링 쓰레드를 일반 쓰레드로 만들게 된다면 프로세스가 언제 종료될지 모르기 때문에 데몬 쓰레드로 만들어 사용합니다.

 

동기화

아까 위에서 멀티 쓰레드 환경에서 생기는 문제점에 대해서 약간 말했었는데 그에 대해서 방지할 수 있도록 하는 것이 동기화인데, 자세하게 설명하자면 여러 개의 쓰레드가 한 개의 리소스를 사용하려 할 때 사용 하려는 쓰레드를 제외한 나머지들이 접근하지 못하도록 막는것입니다.

 

자바에서 동기화를 하는 방법은 Syncronized 키워드, Atomic 클래스, Volatile 키워드와 같이 3가지가 있습니다.

 

Synchrozined 키워드

메서드 자체를 synchorized로 선언하는 방법과 메서드 내의 특정 문장만 synchronized로 감싸는 방법 두 가지가 있습니다.

Atomic

Atomicity(원자성)의 개념은 '쪼갤 수 없는 가장 작은 단위'를 의미하는데, 자바의 Atomic Type은 Wrapping 클래스의 일종으로 참조 타입과 원시 타입 두 종류의 변수에 모두 적용이 가능합니다. 사용시 내부적으로 CAS 알고리즘을 사용하여 lock 없이 동기화 처리가 가능합니다.

Atomic Type의 경우 volatile과 synchronized와 달리 java.util.concurrent.atomic 패키지에 정의된 클래스로 CAS는 특정 메모리 위치와 주어진 위치의 value를 비교하여 다르면 대체하지 않습니다. 사용법은 변수를 선언할 때 타입을 Atomic Type으로 선언하면 됩니다.

 

여기서 CAS 알고리즘이라는 것이 나오는데 이에 대해 간단하게 알아보겠습니다.

 

CAS(Compare-And-Swap)

메모리 위치의 내용과 주어진 값과 비교하고 동일한 경우에만 해당 위치의 내용을 새로 주어진 값으로 수정하는 것으로 즉, 현재 주어진 값과 실제 데이터가 저장된 데이터를 비교하여 두 개가 일치할 때만 값을 업데이트 하는 알고리즘입니다.

 

Volatile

volatile 키워드는 자바 변수를 메인 메모리에 저장하겠다는 것을 명시하는 것입니다. 변수 값을 읽어올 때 CPU cache 값이 아닌 메인 메모리에서 읽고 변수 값을 저장할 때도 메인 메모리에 저장하는 것입니다.

 

데드락(교착상태)

Deadlock(교착상태) 란 , 둘 이상의 쓰레드가 lock을 획득하기 위해 대기하는데, 이 lock을 잡고 있는 쓰레드들도 똑같이 다른 lock을 기다리면서 서로 block 상태에 놓이는 것을 말합니다. Deadlock은 다수의 쓰레드가 같은 lock을 동시에, 다른 명령에 의해 획득하려 할 때 발생할 수 있습니다. 

 

 

'Java' 카테고리의 다른 글

[Java] 애노테이션  (0) 2022.10.24
[Java] Enum  (0) 2022.10.23
[Java] 예외 처리  (0) 2022.10.20
[Java] 인터페이스  (0) 2022.10.19
[Java] 패키지  (0) 2022.10.18