[스터디할래 10] 멀티쓰레드 프로그래밍
이 글은 ‘백기선’ 개발자님과 함께하는 온라인 자바 스터디에 참여하여 준비/학습한 내용을 정리하는 글입니다.📚
- 10주차 : https://github.com/whiteship/live-study/issues/10
- 목표 : 자바의 멀티쓰레드 프로그래밍에 대해 학습하세요.
정리는 Java Tutorials - Concurrency를 기반으로 하였습니다.
여담_
Thread
는 자바의 근간인 만큼 스레드 관련 자바 API DOCS(예 - Enum Thread.State docs) 는 관리가 굉장히 잘 되어 있다. 개발을 하다보면 두 종류의 문서를 만난다: 관리가 잘 된 문서와 그렇지 않은 문서. 관리가 잘 된 문서를 보면 클래스의 기능, 역할이 꼼꼼히 기술되어 있고, 메서드에는 거의 모든 파라미터에 설명이 기술디어 있어 api 사용자가 궁금해할만한 거의 모든 부분을 찾을 수 있다. 이번 자바 api docs 처럼 이렇게 관리가 잘 된 문서를 보면 개발자가 얼마나 사용자를 배려했는지, 자신이 작성한 코드를 얼마나 사랑하는지를 엿볼 수 있어 감동적이다.
프로세스와 스레드
컴퓨터 프로그래밍에서 하나의 코어에서 time slicing 기법을 통해 하나의 프로세스가 나뉘어 실행된다.
프로세스
- 스스로 완전히 독립적인 런타임 리소스 실행 환경을 모두 갖고 있다. 자신만의 메모리 공간이 존재한다.
- 프로그램/애플리케이션과 동일한 의미로 불리기도 한다.
- 프로세스간 통신을 위해서는 OS에서는 파이프, 소켓 같은 IPC(Inter Process Communication) 기능을 제공한다. 보통 JVM은 하나의 프로세스로 실행된다.
스레드
- lightweight process라고도 한다. 프로세스를 생성하는 것 보다 더 적은 리소스가 든다.
- 스레드는 프로세스 내부에 존재한다. 모든 프로세스는 하나 이상의 스레드를 가진다.
- 스레드끼리는 메모리와 open file 을 공유한다.
- ‘멀티스레드’는 자바 플랫폼의 중요 기능이다. 모든 자바 앱은 하나 이상의 스레드를 포함한다. 기본적으로 메모리 관리(gc) 스레드 같은 시스템 스레드와 하나의
main
스레드가 실행된다.
Thread 클래스와 Runnable 인터페이스
- 자바에서 스레드는
Thread
클래스이다. 동시성 프로그램 작성을 위해 스레드를 관리하는 방법에는 두 가지가 있다.- 스레드를 직접 생성하여 관리한다.
Thread
객체를 생성하여 사용한다. - 작업을 스레드 관리 도구인
executor
에 넘긴다. <- 요즘은 보통 대부분의 앱에서executor
를 사용한다.
- 스레드를 직접 생성하여 관리한다.
스레드 정의, 시
Thread
를 생성할 때에는 스레드 내에서 실행할 코드를 반드시 같이 명시해야 한다. 코드는 아래 두 가지 방법으로 전달한다.
Runnable
객체를 제공Runnable
인터페이스를 구현한 객체를 생성한다. 객체의main
메서드에 실행할 코드를 기술하여Thread
생성 시 생성자에 넘겨준다.
Thread
를 subclassing 한다.Thread
클래스를 상속한 객체를 생성하여 실행 가능한 코드를run()
메소드에 기술한다.
Runnable 인터페이스
- https://docs.oracle.com/javase/8/docs/api/java/lang/Runnable.html
- 실행할 코드를 메서드에 담아야할 때 사용한다. 인자가 없는
run
메서드 하나만 구현하는 심플한FunctionalInterface
이다. Thread
를 서브클래싱 하지 않고 실행 가능한 코드를 제공하는 방법이다.
쓰레드의 상태
- 쓰레드에는 상태(Thread.State)가 있다. 특정 시점에 하나의 상태만 가질 수 있다. 이 스레드의 상태는 JVM 의 상태이고 OS의 스레드 상태와는 무관하다.
NEW
- 생성되었으나 시작되지는 않은 상태
RUNNABLE
- JVM 에서 현재 실행되고 있는 스레드. 실제로 os의 다른 프로세스나 자원을 기다리고 있는 상태인데 runnable 로 표현될 수도 있다. 즉, 특별한 경우가 아니면 다 RUNNABLE 인 기본 상태란 말이다.
BLOCKED
- monitor lock 을 얻기 위해 대기중인 스레드
- monitor lock이 synchronized 블럭/메서드로 진입하기를 기다리는 상태이다.
Monitor Lock?
monitor
는 자바의 멀티스레드 환경에서 스레드간 통신을 하기 위한 synchronization, 동기화 방법이 구현된 객체이다. 자바의 객체는 스레드가 lock/unlock을 할 수 있는 monitor를 가지고 있다. 스레드가 객체의 monitor lock을 가진다는 것은 그 객체의 사용권을 가진다고 생각할 수 있다. 하나의 monitor에는 하나의 스레드만 lock 소유권을 가진다. 즉, 하나의 객체는 동시에 하나의 스레드에서만 접근할 수 있다. 내가 접근하고자 하는 객체에 이미 다른 스레드가 monitor lock을 가지고 있다면, 나는 BLOCKED 상태로 기다리는 것이다. (from https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.1) 실제로 같은 객체에 접근하는 두 스레드는, 우리 눈에는 거의 동시에 접근하는 것처럼 보이지만 사실은 교대로 하나씩 접근된다. 누가 얼마나 먼저 접근하고 언제 다른 스레드에게 넘겨줄지는 JVM의 영역이다.
WAITING
- 다른 스레드를 무기한(indefinitely)으로 기다리는 상태. 기다리는 스레드로부터 응답이 와야 다음 상태가 동작한다.
- 대기는 아래 메서드 중 하나를 호출한 경우에 WAITING 상태가 된다.
- 타임아웃 없이 객체에
Object.wait()
를 호출한 경우 : 이 스레드 말고 다른 스레드가 해당 객체에Object.notify()
/Object.notifyAll()
를 호출할 때까지 기다린다. - 타임아웃 없이
Thread.join
: 특정 스레드가 종료될 때까지 기다린다. LockSupport.park
- 타임아웃 없이 객체에
TIMED_WAITING
WAITING
처럼 기다리고 있는데, 특정 시간만큼 기다리는 스레드이다.- 아래 메서드 중 하나를 호출한 경우에 TIMED_WAITING이 된다.
Thread.sleep
Object.wait
with timeoutThread.join
with timeoutLockSupport.parkNanos
LockSupport.parkUntil
TERMINATED
- 종료된 스레드. 실행이 완료된 상태.
쓰레드의 우선순위
- 모든 스레드는 ‘우선순위’를 가진다. 상대적으로 다른 스레드들보다 우선순위가 높으면 더 먼저 실행된다.
- 새로 생성된 스레드는 초기에 부모 스레드의 우선순위와 동일하게 설정된다.
- 우선순위는 애초에 ‘동시에’ 실행해야하는 상황을 가정으로 필요한 개념이다. 멀티스레드 환경에서는 한정된 코어에서 그 이상의 스레드를 돌리게 되는데, 동시에 실행될 수 있는 작업이 제한되어 있으므로 적절히 코어 사용 시간을 분배해야 한다. 우선순위는 시간을 분배할 때에 좀 더 빨리=많이 실행될 수 있도록 앞으로 당겨준다.
Main 쓰레드
동기화(Synchronization)
- 멀티스레드 환경에서 스레드간 통신할 때는 객체에 무방비 상태로 바로 접근하면, thread interference나 memory consistency error 상황이 발생한다. 이런 에러를 피하기 위해 동기화 기법이 필요하다.
- 하지만 동기화를 해도 두 개 이상의 스래드가 동시에 같은 리소르를 점유하려는 thread contention(Starvation, livelock)이 발생한다.
멀티스레드 환경에서의 문제상황
Thread Interference, 스레드 간섭
- 스레드 간섭은 서로 다른 스레드에서 두 연산이 동시에 실행되어 결과가 교착되는 것이다. 두 연산이 여러 단계로 이루어져 있고, 이 단계들이 오버랩되면서 결과가 꼬이는 것이다. 예를 들어 아래와 같은 상황에서 스레드 간섭이 일어난다.
Thread A: Retrieve c. Thread B: Retrieve c. Thread A: Increment retrieved value; result is 1. Thread B: Decrement retrieved value; result is -1. Thread A: Store result in c; c is now 1. Thread B: Store result in c; c is now -1.
- 스레드 간섭은 버그이고 고쳐야한다.
Memory Consistency Errors, 메모리 불균형
- 메모리 불균형은 동일 데이터에 대해서 여러 스레드에서 서로 다른 값을 얻게 되는 상황이다.
- 한 스레드에서의 변경이 메모리에 반영되지 않은 경우 이런 일이 일어날 수 있다.
동기화 방법
- 자바에서는 기본적으로
synchronized
키워드로 멀티스레드 환경에서의 문제를 해결하기 위한 동기화 방법을 제공한다.
Synchronized method
public synchronized void run()
와 같이 키워드를 통해 동기화 메소드를 제공한다.- 동기화 메서드는 동시에 하나만 실행된다. 서로 다른 스레드에서 동일 메서드를 호출했을 때에 서로 오버랩되지 않는다. 한 객체가 동기화 메서드를 실행하고 있으면(=monitor lock을 얻음), 다른 메서드는 block 상태로 기다린다.
- 동기화 메서드의 실행이 종료되면 메서드 내에서의 모든 업데이트를 메모리에 반영하여 다른 스레드에서 업데이트된 값을 읽을 수 있도록 보장한다.
Intrinsic Locks(Monitor Lock)
- 동기화는 내부적으로 intrinsic lock/monitor lock(=monitor) 으로 구현되어 있다.
- 모든 객체는 내부에 intrinsic lock 을 갖고 있다.
- 통상적으로, 배타적으로 실행해야 하는 객체에 접근할 때에는 락을 먼저 own하여 작업 후 락을 release 해야 한다. 한 스레드가 락을 소유하면 다른 스레드는 락을 소유할 수 없고 block 상태가 된다.
데드락
- 두 개 이상의 스레드가 영원히 block 상태로 서로 소유한 자원이 해제되기를 기다리는 상황이다.
- A 스레드는 B 스레드가 소유한 자원이 해제되기를 기다리고, B 스레드는 A 스레드가 소유한 자원이 해제되기를 기다리는 상황이다.
- 데드락은 영원히 풀리지 않는다. 앱을 종료해야 한다.