Multithread Concurrency
영한님의 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 강의를 요약한 내용입니다.
Process And Thread
멀티태스킹과 멀티프로세싱
프로그램 실행
프로그램을 구성하는 코드를 순서대로 CPU에서 연산(실행)하는 일
CPU 코어가 하나일 경우, 한 번에 하나의 프로그램 코드만 실행
이를 해결하기 위해 하나의 CPU 코어로 여러 프로그램을 동시에 실행하는
Multitasking
기술 등장
ℹ️ Multitasking
하나의 컴퓨터 시스템이 동시에 여러 작업을 수행하는 능력
운영체제가 스케줄링을 수행하고, CPU를 최대한 사용하면서 작업이 골고루 수행될 수 있게 최적화
현대의 CPU는 초당 수십억 번 이상의 연산을 수행
CPU가 매우 빠르게 두 프로그램의 코드를 번갈아 수행한다면, 사람이 느낄 때 두 프로그램이 동시에 실행되는 것처럼 느껴짐
시분할(Time Sharing) 기법: 프로그램의 실행 시간을 분할해서 마치 동시에 실행되는 것 처럼 하는 기법
...
ℹ️ Multiprocessing
컴퓨터 시스템에서 둘 이상의 프로세서(CPU Core)를 사용하여 여러 작업을 동시에 처리하는 기술
하나의 CPU 안에 보통 2개 이상의 코어
멀티프로세싱 시스템은 하나의 CPU 코어만을 사용하는 시스템보다 동시에 더 많은 작업 을 처리
Multiprocessing VS. Multitasking
여러 CPU(여러 CPU 코어)를 사용하여 동시에 여러 작업을 수행하는 것을 의미
단일 CPU(단일 CPU 코어)가 여러 작업을 동시에 수행하는 것처럼 보이게 하는 것
하드웨어 기반으로 성능을 향상
소프트웨어 기반으로 CPU 시간을 분할하여 각 작업에 할당
다중 코어 프로세서를 사용하는 현대 컴퓨터 시스템
현대 운영 체제에서 여러 애플리케이션이 동시에 실행되는 환경
프로세스와 스레드
ℹ️ Proccess
운영체제 안에서 실행중인 프로그램
프로세스는 실행 중인 프로그램의 인스턴스
각 프로세스는 독립적인 메모리 공간을 갖고 있으며, 운영체제에서 별도의 작업 단위로 분리해서 관리
프로세스가 서로의 메모리에 직접 접근/간섭할 수 없다
자바 언어로 비유를 하자면 클래스는 프로그램이고, 인스턴스는 프로세스
프로세스의 메모리 구성
Code Section: 실행할 프로그램의 코드가 저장되는 부분
Data Section: 전역 변수 및 정적 변수가 저장되는 부분
Heap: 동적으로 할당되는 메모리 영역
Stack: 메서드(함수) 호출 시 생성되는 지역 변수와 반환 주소가 저장되는 영역(스레드에 포함)
...
ℹ️ Thread
프로세스 내에서 실행되는 작업의 단위
프로세스는 하나 이상의 스레드를 반드시 포함
한 프로세스 내에서 여러 스레드가 존재할 수 있으며, 이들은 프로세스가 제공하는 동일한 메모리 공간을 공유(단일 스레드, 멀티 스레드)
프로세스보다 단순하므로 생성 및 관리가 단순하고 가볍
하나의 프로그램도 그 안에서 동시에 여러 작업이 필요하므로 멀티스레드가 필요
메모리 구성
공유 메모리: 같은 프로세스의 코드 섹션, 데이터 섹션, 힙(메모리)은 프로세스 안의 모든 스레드가 공유
개별 스택: 각 스레드는 자신의 스택을 보유
프로세스
는 실행 환경과 자원을 제공하는 컨테이너 역할, 스레드
는 CPU를 사용해서 코드를 하나하나 실행하는 역할
스레드와 스케줄링
단일 코어 스케줄링
운영체제는 내부에 스케줄링 큐를 가지고 있고, 각 스레드는 스케줄링 큐에서 대기
각 스레드는 번갈아가면서 코드를 실행
멀티 코어 스케줄링
CPU 코어가 2개 이상이면 한 번에 더 많은 스레드를 물리적으로 동시에 실행
프로세스, 스레드와 스케줄링
멀티태스킹과 스케줄링
멀티태스킹
은 동시에 여러 작업을 수행하는 것을 의미이를 위해 운영체제는
스케줄링
이라는 기법을 사용스케줄링
은 CPU 시간을 여러 작업에 나누어 배분하는 방법
프로세스와 스레드
프로세스
는 실행 중인 프로그램의 인스턴스각 프로세스는 독립적인 메모리 공간을 가지며, 운영체제에서 독립된 실행 단위로 취급
스레드
는 프로세스 내에서 실행되는 작은 단위여러 스레드는 하나의 프로세스 내에서 자원을 공유하며, 프로세스의 코드, 데이터, 시스템 자원등을 공유
실제로 CPU에 의해 실행되는 단위는 스레드
프로세스의 역할
프로세스
는 실행 환경(컨테이너 역할)을 제공메모리 공간, 파일 핸들, 시스템 자원(네트워크 연결) 등이 포함
프로세스 자체는 운영체제의 스케줄러에 의해 직접 실행되지 않으며, 프로세스 내의 스레드가 실행
1개의 프로세스 안에 하나의 스레드만 실행되는 경우도 있고, 여러 스레드가 실행되는 경우도 존재
컨텍스트 스위칭
컨텍스트 스위칭(context switching)
스레드A를 멈추는 시점에 CPU에서 사용하던 값들을 메모리에 저장해두어야 한다.
그리고 이후에 스레드A를 다시 실행할 때 이 값들을 CPU에 다시 불러오는 과정
멀티스레드는 대부분 효율적이지만, 컨텍스트 스위칭 과정이 필요하므로 항상 효율적인 것은 아니다
현재 작업하는 문맥이 변하기 때문에 컨텍스트(현재 작업하는 문맥) 스위칭
컨텍스트 스위칭 과정에서 이전에 실행 중인 값을 메모리에 잠깐 저장하고,
이후에 다시 실행하는 시점에 저장한 값을 CPU에 다시 불러와야 한다.
컨텍스트 스위칭 과정에는 약간의 비용이 발생
연산 시간 + 컨텍스트 스위칭 시간
실제로 컨텍스트 스위칭에 걸리는 시간은 아주 짧지만 스레드가 매우 많다면 이 비용이 커질 수 있음
CPU와 스레드의 관계
CPU 바운드 작업 vs I/O 바운드 작업
각 스레드가 하는 작업은 크게 2가지로 구분
CPU-bound tasks
CPU의 연산 능력을 많이 요구하는 작업을 의미
계산, 데이터 처리, 알고리즘 실행 등 CPU의 처리 속도가 작업 완료 시간을 결정
ex. 복잡한 수학 연산, 데이터 분석, 비디오 인코딩, 과학적 시뮬레이션 등.
I/O-bound tasks
디스크, 네트워크, 파일 시스템 등과 같은 입출력(I/O) 작업을 많이 요구하는 작업
I/O 작업이 완료될 때까지 대기 시간이 많이 발생하며, CPU는 상대적으로 유휴(대기) 상태에 있는 경우가 많음(스레드가 CPU를 사용하지 않고 I/O 작업이 완료될 때 까지 대기)
ex. 데이터베이스 쿼리 처리, 파일 읽기/쓰기, 네트워크 통신, 사용자 입력 처리 등.
스레드 설정
스레드의 숫자는 CPU-바운드 작업이 많은지, I/O-바운드 작업이 많은지에 따라 다르게 설정이 필요
CPU-bound tasks
: CPU 코어 수 + 1개CPU를 거의 100% 사용하는 작업이므로 스레드를 CPU 숫자에 최적화
I/O-bound tasks
: CPU 코어 수 보다 많은 스레드를 생성(CPU를 최대한 사용할 수 있는 숫자까지 스레드 생성)CPU를 많이 사용하지 않으므로 성능 테스트를 통해 CPU를 최대한 활용하는 숫자까지 스레드 생성
단, 너무 많은 스레드를 생성하면 컨텍스트 스위칭 비용도 함께 증가하므로 적절한 성능 테스트 필요
웹 애플리케이션 서버라도 상황에 따라 CPU 바운드 작업이 많을 수 있다.
이 경우 CPU-바운드 작업에 최적화된 CPU 숫자를 고려
Thread creation and execution
자바 메모리 구조
메서드 영역(Method Area)
메서드 영역은 프로그램을 실행하는데 필요한 공통 데이터를 관리하고, 프로그램의 모든 영역에서 공유
클래스 정보: 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드등 모든 실행 코드 존재
static 영역: static 변수들을 보관
런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관
스택 영역(Stack Area)
자바 실행 시, 하나의 실행 스택이 생성되고, 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함
스택 프레임: 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임. 메서드를 호출할 때 마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거
스택 영역은 더 정확히 각 스레드별로 하나의 실행 스택이 생성. 따라서 스레드 수 만큼 스택이 생성
힙 영역(Heap Area)
객체(인스턴스)와 배열이 생성되는 영역이다
가비지 컬렉션(GC)이 이루어지는 주요 영역이며, 더 이상 참조되지 않는 객체는 GC에 의해 제거
extends Thread
Thread 클래스를 상속 받거나 Runnable 인터페이스를 구현하여 스레드를 생성
Thread.start()
스레드 실행 메서드
HelloThread 스레드가 별도의 스레드에서 run() 메서드를 실행
main 스레드는 start() 메서드를 통해 Thread-n 스레드에게 실행을 지시
main 스레드가 run() 을 호출할 경우 별도의 스레드가 아닌 main 스레드에서 직접 실행
스레드는 순서와 실행 기간을 모두 보장하지 않는다.
Daemon Thread
스레드는 User Thread, Daemon Thread 2가지 종류로 구분
User Thread(non-daemon Thread)
프로그램의 주요 작업을 수행
작업이 완료될 때까지 실행
모든 user thread가 종료되면 JVM도 종료
Daemon Thread
백그라운드에서 보조적인 작업을 수행
모든 user thread가 종료되면 daemon thread는 자동으로 종료
implements Runnable
실무에서는 보통 Runnable을 구현하는 방식으로 사용
실행 결과는 기존과 동일
스레드와 해당 스레드가 실행할 작업이 서로 분리되어 있다는 차이
스레드 객체를 생성할 때, 실행할 작업을 생성자로 전달
Thread 클래스 상속 방식
장점
간단한 구현: Thread 클래스를 상속받아 run() 메서드만 재정의
단점
상속의 제한: 자바는 단일 상속만을 허용
유연성 부족: 인터페이스를 사용하는 방법에 비해 유연성 감소
Runnable 인터페이스를 구현 방식
장점
상속의 자유로움: 다른 클래스를 상속받아도 문제없이 구현 가능
코드의 분리: 스레드와 실행할 작업을 분리하여 코드의 가독성을 향상
여러 스레드가 동일한 Runnable 객체를 공유할 수 있어 자원 관리에 효율적
단점
코드가 약간 복잡: Runnable 객체를 생성하고 이를 Thread 에 전달하는 과정이 추가
Runnable 인터페이스를 구현하는 방식을 사용하자.
스레드와 실행할 작업을 명확히 분리하고, 인터페이스를 사용하므로 Thread 클래스를 직접 상속하는 방식보다 더 유연하고 유지보수 하기 쉬운 코드를 만들 수 있다.
스레드 제어와 생명 주기
스레드의 기본 정보
Thread 클래스는 스레드를 생성하고 관리하는 기능을 제공
스레드의 생명 주기
스레드는 생성하고 시작하고, 종료되는 생명주기를 가진다.
ℹ️ New (새로운 상태)
스레드가 생성되고 아직 시작되지 않은 상태
Thread 객체가 생성되었지만 start() 메서드가 호출되지 않은 상태
Thread thread = new Thread(runnable);
ℹ️ Runnable (실행 가능 상태)
스레드가 실행될 준비가 되어 CPU에서 실행될 수 있는 상태
thread.start()
메서드가 호출되면 스레드는 Runnable 상태로 전이
Runnable 상태에 있는 모든 스레드가 동시에 실행되는 것은 아니다.
운영체제의 스케줄러가 각 스레드에 CPU 시간을 할당하여 실행하기 때문에,
Runnable 상태에 있는 스레드는 스케줄러의 실행 대기열에 포함되어 있다가 차례로 CPU에서 실행
운영체제 스케줄러의 실행 대기열에 있든, CPU에서 실제 실행되고 있든 모두 RUNNABLE 상태
자바에서 둘을 구분해서 확인할 수 없어서 보통 실행 상태라고 부름
ℹ️ Blocked (차단 상태)
스레드가 다른 스레드에 의해 동기화 락을 얻기 위해 기다리는 상태
synchronized 블록에 진입하기 위해 락을 얻어야 하는 경우
ℹ️ Waiting (대기 상태)
스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태
wait()
,join()
메서드가 호출될 때 Waiting 상태로 전이
다른 스레드가
notify()
ornotifyAll()
메서드를 호출하거나,join()
이 완료될 때까지 대기
ℹ️ Timed Waiting (시간 제한 대기 상태)
스레드가 특정 시간 동안 다른 스레드의 작업이 완료되기를 기다리는 상태
sleep(long millis)
,wait(long timeout)
,join(long millis)
메서드가 호출될 때 Timed Waiting 상태로 전이
주어진 시간이 경과하거나 다른 스레드가 해당 스레드를 깨우면 이 상태에서 벗어남
ℹ️ Terminated (종료 상태)
스레드의 실행이 완료된 상태
스레드가 정상적으로 종료되거나, 예외가 발생하여 종료된 경우 Terminated 상태로 전이
스레드는 한 번 종료되면 다시 시작할 수 없음
참고. 자바에서 메서드 재정의 시, 재정의 메서드가 지켜야할 예외와 관련된 규칙
체크 예외
부모 메서드가 체크 예외를 던지지 않는 경우, 재정의된 자식 메서드도 체크 예외를 던질 수 없음
자식 메서드는 부모 메서드가 던질 수 있는 체크 예외의 하위 타입만 던질 수 있음
언체크(런타임) 예외
예외 처리를 강제하지 않으므로 상관없이 던질 수 있음
run() 메서드는 체크 예외를 던질 수 없도록 강제하여 프로그램이 비정상 종료되는 상황을 방지
멀티 스레딩 환경에서 예외 처리를 강제함으로써 스레드의 안정성과 일관성을 유지
최근에는 체크 예외보다는 언체크 예외를 선호
...
ℹ️ Join
기대와 다르게 두 결과 모두 0이 나온다.
Waiting (대기 상태)
스레드가 다른 스레드의 특정 작업이 완료되기를 무기한 기다리는 상태
join()
을 호출하는 스레드는 대상 스레드가TERMINATED
상태가 될 때 까지 대기대상 스레드가
TERMINATED
상태가 되면 호출 스레드는 다시RUNNABLE
상태가 되면서 다음 코드를 수행
특정 스레드가 완료될 때 까지 기다려야 하는 상황이라면 join() 을 사용하자.
다른 스레드의 작업을 일정 시간 동안만 기다릴 경우
join()
: 호출 스레드는 대상 스레드가 완료될 때 까지 무한정 대기join(ms)
: 호출 스레드는 특정 시간 만큼만 대기호출 스레드는 지정한 시간이 지나면 다시 RUNNABLE 상태가 되면서 다음 코드를 수행
...
ℹ️ Interrupt
인터럽트를 사용하면
WAITING
,TIMED_WAITING
같은 대기 상태의 스레드를 직접 깨워서,작동하는
RUNNABLE
상태로 만들 수 있다.
스레드가 인터럽트 상태일 때
InterruptedException 을 던지는 메서드(ex. Thread.sleep())를 호출하거나
이미 위 메서드를 호출하고 대기중이라면 InterruptedException 발생
Thread.currentThread().isInterrupted()
스레드의 인터럽트 상태를 단순히 확인
Thread.interrupted()
인터럽트를 직접 체크해서 사용할 경우
스레드가 인터럽트 상태일 경우,
true 반환
후 해당 스레드의 인터럽트상태를 false 로 변경
스레드가 인터럽트 상태가 아닐 경우,
false 반환
후 해당 스레드의 인터럽트 상태를 변경하지 않음
인터럽트 예외가 발생하고, 스레드의 인터럽트 상태를 정상(false)으로 돌리지 않으면, 이후에도 계속 인터럽트가 발생
그러므로, 자바에서 인터럽트 예외가 발생하거나 인터럽트의 목적을 달성하면, 스레드의 인터럽트 상태를 다시 정상(false)으로 돌린다.
InterruptedException
Thread.interrupted()
프린터 예제
종료 입력 시 바로 반응하지 않는 문제
종료(q)를 입력하면 즉시 종료
Thread.interrupted() 메서드를 사용하여 해당 스레드가 인터럽트 상태인지 아닌지 확인하고, 스레드의 인터럽트 상태를 다시 정상으로 전환
작업이 비어있으면 다른 스레드에 작업을 양보
...
ℹ️ Yield
현재 실행 중인 스레드가 자발적으로 다른 스레드에게 CPU를 양보
yield() 메서드를 호출한 스레드는 RUNNABLE 상태를 유지
자바에서 Thread.yield() 메서드를 호출하면 현재 실행 중인 스레드가 CPU를 양보하도록 힌트 제공
다른 스레드에게 실행 기회를 제공
RUNNABLE
상태를 유지하기 때문에, 양보할 스레드가 없다면 본인 스레드가 계속 실행
...
ℹ️ volatile
메모리 가시성(memory visibility)
멀티스레드 환경에서 한 스레드가 변경한 값이 다른 스레드에서 언제 보이는지에 대한 문제
이름 그대로 메모리에 변경한 값이 보이는가, 보이지 않는가의 문제
스레드가 while 문에서 빠져나오지 못하고 계속 실행 상태
.
volatile
여러 스레드에서 같은 값을 읽고 써야 할 경우 사용되는 키워드
캐시 메모리를 사용하면 CPU 처리 성능을 개선하지만, 때로는 성능 향상보다 여러 스레드에서 같은 시점에 정확히 같은 데이터를 보는 것이 더 중요할 수 있음
이 경우, 성능을 약간 포기하는 대신 값을 읽고 쓸 때 모두 메인 메모리에 직접 접근하도록 자바에서는
volatile
키워드 제공
Java Memory Model
ℹ️ 메모리 가시성(memory visibility)
멀티스레드 환경에서 한 스레드가 변경한 값이 다른 스레드에서 언제 보이는지에 대한 것
메모리에 변경한 값이 보이는가, 보이지 않는가의 문제
...
ℹ️ Java Memory Model
Java Memory Model(JMM)은 자바 프로그램이 어떻게 메모리에 접근하고 수정할 수 있는지를 규정
특히 멀티스레드 프로그래밍에서 스레드 간의 상호작용을 정의
핵심은 여러 스레드들의 작업 순서를 보장하는 happens-before 관계에 대한 정의
...
ℹ️ happens-before
Java Memory Model(JMM)에서 스레드 간의 작업 순서를 정의하는 개념
한 스레드에서 수행한 작업을 다른 스레드가 참조할 때 최신 상태가 보장
ex. A 작업에서 변경된 내용은 B 작업이 시작되기 전에 모두 메모리에 반영
.
happens-before 관계가 발생하는 경우
프로그램 순서 규칙
volatile 변수 규칙
스레드 시작/종료/인터럽트 규칙
객체 생성 규칙
모니터 락 규칙
전이 규칙
...
volatile 또는 스레드 동기화 기법(synchronized, ReentrantLock ..)을 사용하면 메모리 가시성의 문제가 발생하지 않는다
synchronized
자바에서 동기화(synchronization)는 여러 스레드가 동시에 접근할 수 있는 자원(예: 객체, 메서드)에 대해 일관성 있고 안전한 접근을 보장하기 위한 메커니즘
동기화는 주로 멀티스레드 환경에서 발생할 수 있는 문제(데이터 손상이나 예기치 않은 결과)를 방지하기 위해 사용
자바는 멀티스레드를 고려하고 나온 언어로 JDK 1.0 부터 synchronized 같은 동기화 방법을 프로그래밍 언어의 문법에 포함해서 제공
장점.
편리함: 프로그래밍 언어에 문법으로 제공하여
편리
한 사용자동 잠금 해제: synchronized 메서드나 블록이 완료되면 자동으로 락을 대기중인 다른 스레드의
잠금이 해제
개발자가 직접 특정 스레드를 깨우도록 관리해야 한다면, 매우 어렵고 번거로움
단점
단순한 기능: 편리하지만 제공하는
기능이 너무 단순
무한 대기: BLOCKED 상태의 스레드는 락이 풀릴 때 까지
무한 대기
타임아웃이나 중간에 인터럽트 불가
공정성: 락이 돌아왔을 때 BLOCKED 상태의 여러 스레드 중에 어떤 스레드가 락을 획득할 지 알 수 없음
더 유연하고, 세밀한 제어가 가능한 동기화 방법들이 필요하게 되어 JDK 1.5 부터 동시성 문제 해결을 위한 java.util.concurrent
패키지가 추가
Concurrency Issue
멀티스레드를 사용할 때 가장 주의해야 할 점은, 같은 자원(리소스)에 여러 스레드가 동시에 접근할 때 발생하는 동시성 문제
공유 자원: 여러 스레드가 접근하는 자원
대표적인 공유 자원은 인스턴스의 필드(멤버 변수)
멀티스레드 사용 시 공유 자원에 대한 접근을 적절하게 동기화(synchronization)해서 동시성 문제가 발생하지 않게 방지하는 것이 중요
...
ℹ️ 임계 영역(critical section)
여러 스레드가 동시에 접근하면 데이터 불일치나 예상치 못한 동작이 발생할 수 있는 위험하고, 중요한 코드
여러 스레드가 동시에 접근해서는 안 되는 공유 자원을 접근하거나 수정하는 부분
ex. 공유 변수나 공유 객체를 수정
임계 영역은 한 번에 하나의 스레드만 접근할 수 있도록 안전하게 보호가 필요
자바는 synchronized 키워드를 통해 간단하게 임계 영역을 보호
synchronized method
메서드를 synchronized 로 선언해서, 메서드에 접근하는 스레드가 하나뿐이도록 보장
모든 객체(인스턴스)는 내부에 자신만의 락(lock)을 보유
aka. 모니터 락(monitor lock)
객체 내부에 있다보니 확인하기는 어려움
스레드가 synchronized 키워드가 있는 메서드에 진입하려면 반드시 해당 인스턴스의 락이 필요
(thread1) synchronized 메서드 호출 시 락 획득 시도
(thread2) 락이 없을 경우 획득할 때까지 BLOCKED 상태로 대기
(thread1) 메서드 호출이 끝나면 락을 반납
(thread2) 락 획득을 대기하는 스레드는 자동으로 락을 획득
참고) 락을 획득하는 순서는 보장되지 않음
참고) 자바 메모리 가시성 문제는 자동으로 해결
synchronized code block
코드 블록을 synchronized 로 감싸서, 동기화를 구현
synchronized 의 가장 큰 장점이자 단점은 한 번에 하나의 스레드만 실행할 수 있다는 점
여러 스레드가 동시에 실행하지 못하기 때문에, 전체로 보면 성능이 떨어질 수 있음
따라서
synchronized
를 통해 여러 스레드를 동시에 실행할 수 없는 코드 구간은 꼭 필요한 곳으로 한정해서 설정이 필요synchronized code block
으로 필요한 부분에 임계 영역을 지정 가능여러 스레드가 동시에 수행되는 부분을 더 늘려서, 전체적으로 성능을 향상
...
동기화를 사용해서 해결할 수 있는 문제들
경합 조건(Race condition)
두 개 이상의 스레드가 경쟁적으로 동일한 자원을 수정할 때 발생하는 문제
데이터 일관성
여러 스레드가 동시에 읽고, 쓰는 데이터의 일관성을 유지
동기화는 멀티스레드 환경에서 필수적인 기능이지만, 과도하게 사용할 경우 성능 저하를 초래
꼭 필요한 곳에 적절히 사용 필요
concurrent.Lock
LockSupport
LockSupport 은
ReentrantLock
에서 활용되어 synchronized 의 단점을 극복하면서도매우 편리하게 임계 영역을 다룰 수 있는 다양한 기능 제공
자바 1.0부터 존재한 synchronized 의 문제를 해결하기 위해 더 유연하고, 세밀한 제어가 가능한 방법들이 필요
자바 1.5부터 java.util.concurrent 라는 동시성 문제 해결을 위한 라이브러리 패키지가 추가
.
synchronized 단점
무한 대기
: BLOCKED 상태의 스레드는 락이 풀릴 때 까지 무한 대기특정 시간까지만 대기하는 타임아웃 불가
→ LockSupport.parkNanos() 를 사용하면 특정 시간까지만 대기 가능
중간에 인터럽트 불가
→ park(), parkNanos() 는 인터럽트를 걸 수 있음
공정성
: 락이 돌아왔을 때 BLOCKED 상태의 여러 스레드 중에 어떤 스레드가 락을 획득할 지 알 수 없음최악의 경우 특정 스레드가 너무 오랜기간 락을 획득하지 못할 수 있음
.
LockSupport 대표 기능
park()
: 스레드를 WAITING 상태로 변경누가 깨워주기 전까지는 계속 대기
CPU 실행 스케줄링에 들어가지 않음
parkNanos(nanos)
: 스레드를 나노초 동안만 TIMED_WAITING 상태로 변경지정한 나노초가 지나면 TIMED_WAITING 상태에서 빠져나오고 RUNNABLE 상태로 변경
unpark(thread)
: WAITING 상태의 대상 스레드를 RUNNABLE 상태로 변경
LockSupport.unpark example LockSupport.parkNanos(nanos) example
...
ℹ️ BLOCKED vs WAITING
BLOCKED , WAITING , TIMED_WAITING 상태 모두 스레드가 대기
실행 스케줄링에 들어가지 않기 때문에, CPU 입장에서 보면 실행하지 않는 비슷한 상태
BLOCKED 상태
synchronized 에서 락을 획득하기 위해 대기할 때 사용
synchronized 에서만 사용하는 특별한 대기 상태
인터럽트가 걸려도 대기 상태를 빠져나오지 못함
WAITING
, TIMED_WAITING
상태
범용적으로 활용할 수 있는 대기 상태
인터럽트가 걸리면 대기 상태를 빠져나오고, RUNNABLE 상태로 변경
다양한 상황에서 사용
Thread.join() , Thread.join(long millis)
Thread.park() , Thread.parkNanos(long millis)
Object.wait() , Object.wait(long timeout)
ReentrantLock
synchronized 와 BLOCKED 상태를 통한 통한 임계 영역 관리의 한계를 극복하기 위해
자바 1.5부터 Lock 인터페이스와 ReentrantLock 구현체를 제공
참고. 여기서 사용하는 락은 객체 내부에 있는 모니터 락이 아님
Lock 인터페이스와 ReentrantLock 이 제공하는 기능
모니터 락과 BLOCKED 상태는 synchronized 에서만 사용
...
ℹ️ Lock Interface
동시성 프로그래밍에서 쓰이는 안전한 임계 영역을 위한 락을 구현하는데 사용
synchronized 의 단점인 무한 대기 문제 해결
고수준의 동기화 기법을 구현 가능
락을 특정 시간 만큼만 시도하거나, 인터럽트 가능한 락을 사용할 때 유용
개발자에게 다양한 선택권을 제공
대표적인 구현체로 ReentrantLock
...
ℹ️ 공정성
Lock 인터페이스의 대표적인 구현체 ReentrantLock 클래스는
스레드가 공정하게 락을 얻을 수 있는 모드를 제공
ReentrantLock 락은 공정성(fairness) 모드와 비공정(non-fair) 모드로 설정 가능
이 두 모드는 락을 획득하는 방식에서 차이
.
비공정 모드(Non-fair mode)
ReentrantLock 의 기본 모드
락을 먼저 요청한 스레드가 락을 먼저 획득한다는 보장이 없음
락을 풀었을 때, 대기 중인 스레드 중 아무나 락을 획득
락을 빨리 획득할 수 있지만, 특정 스레드가 장기간 락을 획득하지 못할 가능성도 존재
그래도, 내부에서 큐 구조를 사용하기 때문에
스레드 경합이 심할 때 새로운 스레드가 가끔 몇 개씩 먼저 실행되는 정도
비공정 모드 특징
성능 우선
: 락을 획득 속도가 빠름선점 가능
: 새로운 스레드가 기존 대기 스레드보다 먼저 락을 획득할 수 있음기아 현상 가능성
: 특정 스레드가 계속해서 락을 획득하지 못할 수 있음
.
공정 모드(Fair mode)
락을 요청한 순서대로 스레드가 락을 획득
먼저 대기한 스레드가 먼저 락을 획득하게 되어 스레드 간의 공정성을 보장
그러나 이로 인해 성능 저하
공정 모드 특징
공정성 보장
: 대기 큐에서 먼저 대기한 스레드가 락을 먼저 획득기아 현상 방지
: 모든 스레드가 언젠가 락을 획득할 수 있게 보장성능 저하
: 우선수위 선정을 위해 락을 획득하는 속도가 느려질 수 있음
ReentrantLock fair mode example
생산자 소비자 문제
생산자(Producer)
데이터를 생성하는 역할
ex. 파일에서 데이터를 읽어오거나 네트워크에서 데이터를 받아오는 스레드
소비자(Consumer)
생성된 데이터를 사용하는 역할
ex. 데이터를 처리하거나 저장하는 스레드s
버퍼(Buffer)
생산자가 생성한 데이터를 일시적으로 저장하는 공간
한정된 크기를 가지며, 생산자와 소비자가 이 버퍼를 통해 데이터를 주고 받음
.
생산자/소비자 문제가 발생할 수 있는 상황
생산자가 너무 빠를 때
버퍼가 가득 차서 더 이상 데이터를 넣을 수 없을 때까지 생산자가 데이터를 생성
버퍼가 가득 찬 경우 생산자는 버퍼에 빈 공간이 생길 때까지 대기
소비자가 너무 빠를 때
버퍼가 비어서 더 이상 소비할 데이터가 없을 때까지 소비자가 데이터를 처리
버퍼가 비어있을 때 소비자는 버퍼에 새로운 데이터가 들어올 때까지 대기
.
생산자-소비자 문제(producer-consumer problem)
생산자-소비자 문제는, 생산자 스레드와 소비자 스레드가 특정 자원을 함께 생산하고, 소비하면서 발생하는 문제
한정된 버퍼 문제(bounded-buffer problem)
이 문제는 결국 중간에 있는 버퍼의 크기가 한정되어 있기 때문에 발생
한정된 버퍼 문제라고도 불림
Object wait(), notify()
Object.wait()
현재 스레드가 가진 락을 반납하고 대기(WAITING) 상태로 전환
현재 스레드가 synchronized 블록이나 메서드에서 락을 소유하고 있을 때만 호출 가능
대기 상태로 전환된 스레드는 다른 스레드가 notify() or notifyAll()을 호출할 때까지 대기 상태를 유지
Object.notify()
대기 중인 스레드 중 하나를 깨움
대기 중인 스레드가 여러 개일 경우 그 중 하나만 깨움
synchronized 블록이나 메서드에서 호출되어야 함
깨운 스레드는 락을 다시 획득할 기회를 얻음
Object.notifyAll()
대기 중인 모든 스레드를 깨움
synchronized 블록이나 메서드에서 호출되어야 함
모든 대기 중인 스레드가 락을 획득할 수 있는 기회를 얻음
모든 스레드를 깨워야 할 필요가 있는 경우에 유용
.
Object wait(), notify()의 한계
스레드 대기 집합 하나에 생산자, 소비자 스레드를 모두 관리하고,
notify()
를 호출할 때 임의의 스레드가 선택큐에 데이터가 없는 상황에 소비자가 같은 소비자를 깨우거나
큐에 데이터가 가득 차있는데 생산자가 같은 생산자를 깨우는 비효율 발생
대기 상태의 스레드가 실행 순서를 계속 얻지 못해서 실행되지 않는 상황이 올 수 있음 → 스레드 기아(starvation) 상태
notify()
대신notifyAll()
을 사용해서 스레드 기아 문제는 막을 수 있지만, 비효율은 막지 못함
.
Example
스레드를 제어할 수 없기 때문에 한정된 버퍼 상황에서 문제가 발생
버퍼가 가득 찬 경우: 생산자의 데이터를 버린다.
버퍼에 데이터가 없는 경우: 소비자는 데이터를 획득할 수 없다.(null)
반복문을 사용해서 스레드를 대기하는 방법
임계 영역 안에서 락을 들고 대기하기 때문에, 다른 스레드가 임계 영역에 접근할 수 없음
나머지 스레드는 모두 BLOCKED 상태가 되고, 스레드들이 멈추는 문제 발생
생산자 소비자 문제 Object wait(), notify() 적용
wait(), notify(), notifyAll()을 사용해서 문제 해결
하지만, 원하는 스레드를 선택해서 깨울 수 없어 비효율 발생
Lock Condition
Example
Object.notify() vs Condition.signal()
Object.notify()
대기 중인 스레드 중 임의의 하나를 선택해서 깨움
스레드가 깨어나는 순서는 정의되어 있지 않고, JVM 구현에 따라 차이
보통은 먼저 들어온 스레드가 먼저 수행되지만 구현에 따라 다를 수 있음
synchronized 블록 내에서 모니터 락을 가지고 있는 스레드가 호출해야 함
Condition.signal()
대기 중인 스레드 중 하나를 깨움
일반적으로는 FIFO 순서로 깨우고, 자바 버전과 구현에 따라 차이
보통 Condition 구현은 Queue 구조를 사용하기 때문에 FIFO 순서
ReentrantLock 을 가지고 있는 스레드가 호출해야 함
스래드의 대기
synchronized 대기
대기1: 모니터 락 획득 대기
자바 객체 내부의 락 대기 집합(모니터 락 대기 집합)에서 관리
BLOCKED
상태로 락 획득 대기synchronized 를 시작할 때 락이 없으면 대기
다른 스레드가 synchronized 를 빠져나갈 때 락을 획득 시도
락을 획득하면 락 대기 집합 탈출
대기2: wait() 대기
wait()
호출 시 자바 객체 내부의 스레드 대기 집합에서 관리WAITING
상태로 대기다른 스레드가 notify() 호출 시 스레드 대기 집합 탈출
자바(synchronized)의 모든 객체 인스턴스는 멀티스레드와 임계 영역을 다루기 위해 내부에 3가지 기본 요소를 가짐
모니터 락
락 대기 집합(모니터 락 대기 집합) / 1차 대기소
스레드 대기 집합 / 2차 대기소
...
ReentrantLock 대기
대기1: ReentrantLock 락 획득 대기
ReentrantLock 의 대기 큐에서 관리
WAITING
상태로 락 획득 대기lock.lock()
호출 시 락이 없으면 대기다른 스레드가
lock.unlock()
호출 시 대기가 풀리며 락 획득 시도락을 획득하면 대기 큐 탈출
대기2: await() 대기
condition.await()
호출 시 condition 객체의 스레드 대기 공간에서 관리WAITING
상대로 대기다른 스레드가
condition.signal()
호출 시 condition 객체의 스레드 대기 공간에서 탈출
BlockingQueue
자바는 생산자 소비자 문제를 해결하기 위해
java.util.concurrent.BlockingQueue
라는 특별한 멀티스레드 자료 구조를 제공
큐가 특정 조건이 만족될 때까지 스레드의 작업을 차단(blocking)
데이터 추가 차단
큐가 가득 차면 데이터 추가 작업(
put()
)을 시도하는 스레드는 공간이 생길 때까지 차단
데이터 획득 차단
큐가 비어 있으면 획득 작업(
take()
)을 시도하는 스레드는 큐에 데이터가 들어올 때까지 차단
BlockingQueue 인터이스 주요 메서드
대표적인 구현체
ArrayBlockingQueue
LinkedBlockingQueue
BlockingDeque
...
BlockingQueue 기능
큐가 가득 찼을 때 생각할 수 있는 선택지
예외를 던지고, 예외를 받아서 처리
대기하지 않고, 즉시 false 반환
대기
특정 시간 만큼만 대기
Insert
add(e)
offer(e)
put(e)
offer(e, time, unit)
Remove
remove()
poll()
take()
poll(time, unit)
Examine
element()
peek()
not applicable
not applicable
Throws Exception / 대기 시 예외
add(e): 지정된 요소를 큐에 추가
큐가 가득 차면 IllegalStateException 예외
remove(): 큐에서 요소를 제거하며 반환
큐가 비어 있으면 NoSuchElementException 예외
element(): 큐의 머리 요소를 반환하지만, 요소를 큐에서 제거하지 않음
큐가 비어 있으면 NoSuchElementException 예외
Special Value / 대기 시 즉시 반환
offer(e): 지정된 요소를 큐에 추가하려고 시도
큐가 가득 차면 false 반환
poll(): 큐에서 요소를 제거하고 반환
큐가 비어 있으면 null 반환
peek(): 큐의 머리 요소를 반환하지만, 요소를 큐에서 제거하지 않음
큐가 비어 있으면 null 반환
Blocks / 대기
put(e): 지정된 요소를 큐에 추가할 때까지 대기
큐가 가득 차면 공간이 생길 때까지 대기
take(): 큐에서 요소를 제거하고 반환
큐가 비어 있으면 요소가 준비될 때까지 대기
Examine (관찰): 해당 사항 없음.
Times Out / 시간 대기
offer(e, time, unit): 지정된 요소를 큐에 추가하려고 시도
지정된 시간 동안 큐가 비워지기를 기다리다가 시간이 초과되면 false 반환
poll(time, unit): 큐에서 요소를 제거하고 반환
큐에 요소가 없다면 지정된 시간 동안 요소가 준비되기를 기다리다가 시간이 초과되면 null 반환
Examine (관찰): 해당 사항 없음
.
기능 테스트
CAS / 원자적 연산
원자적 연산(atomic operation)
해당 연산이 더 이상 나눌 수 없는 단위로 수행된다는 것을 의미
중단되지 않고, 다른 연산과 간섭 없이 완전히 실행되거나 전혀 실행되지 않는 성질
멀티스레드 상황에서 다른 스레드의 간섭 없이 안전하게 처리되는 연산
commit
멀티스레드 상황에 안전한 증가, 감소 연산 제공
AtomicLong, AtomicBoolean 등 다양한 Atomic 클래스 존재
...
CAS 연산
우리가 직접 CAS 연산을 사용하는 경우는 거의 없기 때문에 참고만
AtomicInteger와 같은 CAS 연산을 사용하는 라이브러리들을 잘 사용하는 정도면 충분
CAS(CompareAnd-Swap, Compare-And-Set) 연산
락을 걸지 않고
원자적인 연산
을 수행락을 사용하지 않기 때문에
lock-free
기법
락을 완전히 대체하는 것은 아니고, 작은 단위의 일부 영역에 적용 가능
기본은 락을 사용하고, 특별한 경우에 CAS를 적용
.
CPU 하드웨어의 지원
CAS 연산은 원자적이지 않은 두 개의 연산을 CPU 하드웨어 차원에서 하나의 원자적인 연산으로 묶어서 제공하는 기능
중간에 다른 스레드가 개입 불가
CPU 입장에서 보면 아주 찰나의 순간이므로 성능에 큰 영향을 끼치지는 않음
소프트웨어가 제공하는 기능이 아니라 하드웨어가 제공하는 기능
대부분의 현대 CPU들은 CAS 연산을 위한 명령어를 제공
CPU는 두 연산을 묶어서 하나의 원자적인 명령으로 만들어버린다. 따라서
.
CAS(Compare-And-Swap)와 락(Lock) 방식의 비교
락(Lock) 방식
비관적(pessimistic) 접근법
데이터에 접근하기 전에 항상 락을 획득
다른 스레드의 접근을 막음
"다른 스레드가 방해할 것이다"라고 가정
충돌 시 동작
스레드 충돌을 방지하기 위해 1,000개의 스레드가 모두 락을 획득하고 반환
락을 사용하기 때문에 1,000개의 스레드는 순서대로 하나씩 수행
스레드의 상태가 계속 전환되면서 OS는 Context switch 로 큰 오버헤드 발생 가능
CAS(Compare-And-Swap) 방식
낙관적(optimistic) 접근법
락을 사용하지 않고 데이터에 바로 접근
충돌이 발생하면 그때 재시도
"대부분의 경우 충돌이 없을 것이다"라고 가정
충돌이 많이 없는 경우에 CAS 연산이 빠른 성능
충돌 시 동작
1000개의 스레드를 모두 한 번에 실행
충돌이 나는 50개의 경우만 재시도
충돌이 적은 간단한 CPU 연산에는 좋은 성능
...
CAS 락
CAS는 단순한 연산 뿐만 아니라, 락을 구현하는데 사용
commit
원자적이지 않아서 동시성 문제 발생
락 사용 여부 확인, 락의 값 변경
원자적으로 연산
락을 사용하지 않는다면 락의 값을 변경
동기화 락
을 사용하는 경우 스레드가 락을 획득하지 못하면 BLOCKED, WAITING 등으로 상태가 전환
대기 상태의 스레드를 깨워야 하는 무겁고 복잡한 과정이 추가로 동작
따라서 성능이 상대적으로 느릴 수 있음
반면, CAS를 활용한 락
방식은 락이 없는 방식
단순히 while문을 반복
따라서 대기하는 스레드도 RUNNABLE 상태를 유지하면서 가볍고 빠르게 작동
...
ℹ️ 스핀 락
락을 획득하기 위해 자원을 소모하면서 반복적으로 확인(스핀)하는 락 메커니즘
스레드가 락을 획득 할 때 까지 대기하는 것을 스핀 대기(spin-wait)
또는 CPU 자원을 계속 사용하면서 바쁘게 대기한다고 해서 바쁜 대기(busy-wait)라고 불림
따라서, 스핀 락 방식은 아주 짧은 CPU 연산을 수행할 때 사용해야 효율적
잘못 사용하면 오히려 CPU 자원을 더 많이 사용
CAS를 사용해서 구현
...
락 VS CAS 사용 방식
동기화 락(synchronized , Lock(ReentrantLock))을 사용하는 방식과 CAS를 활용하는 락 프리 방식의 장단점 비교
CAS
장점
낙관적 동기화
락을 걸지 않고도 값을 안전하게 업데이트
CAS는 충돌이 자주 발생하지 않을 것이라고 가정하여 충돌이 적은 환경에서 높은 성능을 발휘
락 프리(Lock-Free)
락을 획득하기 위해 대기하는 시간이 없음
따라서 스레드가 블로킹되지 않으며, 병렬 처리가 더 효율적일 수 있음
단점
충돌이 빈번한 경우
여러 스레드가 동시에 동일한 변수에 접근하여 업데이트를 시도할 때 충돌 발생
충돌 발생 시 CAS는 루프를 돌며 재시도, 이에 따라 CPU 자원을 계속 소모
반복적인 재시도로 인해 오버헤드가 발생 가능성 존재
스핀락과 유사한 오버헤드
충돌 시 반복적인 재시도로 계속 반복되면 스핀락과 유사한 성능 저하 발생
특히 충돌 빈도가 높을수록 이런 현상이 두드러짐
사용 사례
CPU 사이클이 금방 끝나지만 안전한 임계 영역/원자적인 연산이 필요한 경우
숫자 값의 증가
자료 구조의 데이터 추가/삭제
.
동기화 락
장점
충돌 관리
락을 사용하면 하나의 스레드만 리소스에 접근하여 충돌 방지
여러 스레드가 경쟁할 경우에도 안정적으로 동작
안정성
복잡한 상황에서도 일관성 있는 동작을 보장
스레드 대기
락을 대기하는 스레드는 CPU를 거의 사용하지 않음
단점
락 획득 대기 시간
스레드가 락을 획득하기 위해 대기하여, 대기 시간이 길어질 수 있음
컨텍스트 스위칭 오버헤드
락 획득을 대기하는 시점과 또 락을 획득하는 시점에 스레드의 상태가 변경
이때 컨텍스트 스위칭이 발생할 수 있으며, 이로 인해 오버헤드가 증가
사용 사례
오래 기다리는 작업
데이터베이스 대기
다른 서버의 요청 대기
직접 CAS 연산을 사용하는 경우는 거의 없고, 대부분 복잡한 동시성 라이브러리들이 CAS 연산을 사용
AtomicInteger와 같은 CAS 연산을 사용하는 라이브러리들을 잘 사용하는 정도면 충분
동시성 컬렉션
컬렉션 프레임워크가 제공하는 대부분의 연산은 원자적인 연산이 아니므로 스레드 세이프 하지 않다.
commit
모든 컬렉션을 복사해서 동기화용으로 새로 구현해야 하는 문제점
객체에 대한 접근을 제어하기 위해 대리인/인터페이스 역할을 하는 객체를 제공하는 패턴
프록시 패턴의 주요 목적
접근 제어: 실제 객체에 대한 접근을 제한하거나 통제
성능 향상: 실제 객체의 생성을 지연시키거나 캐싱하여 성능을 최적화
부가 기능 제공: 실제 객체에 추가적인 기능(로깅, 인증, 동기화 등)을 투명하게 제공
.
자바 synchronized 프록시
Collections는 다양한 synchronized 동기화 메서드 지원
List, Collection, Map, Set 등 다양한 동기화 프록시 생성 가능
synchronizedList()
synchronizedCollection()
synchronizedMap()
synchronizedSet()
synchronizedNavigableMap()
synchronizedNavigableSet()
synchronizedSortedMap()
synchronizedSortedSet()
synchronized 프록시 방식의 단점
동기화 오버헤드 발생
전체 컬렉션에 대해 동기화가 이루어지므로 잠금 범위가 넓어짐
정교한 동기화 불가
.
자바 동시성 컬렉션
java.util.concurrent
패키지에는 고성능 멀티스레드 환경을 지원하는 다양한 동시성 컬렉션 클래스들을 제공synchronized, Lock(ReentrantLock), CAS, 분할 잠 금 기술(segment lock)등 다양한 방법을 섞어서 매우 정교한 동기화를 구현하면서 동시에 성능도 최적화
동시성 컬렉션의 종류
List
CopyOnWriteArrayList → ArrayList 대안
Set
CopyOnWriteArraySet → HashSet 대안
ConcurrentSkipListSet → TreeSet 대안(정렬된 순서 유지, Comparator 사용 가능)
Map
ConcurrentHashMap → HashMap 대안
ConcurrentSkipListMap → TreeMap 대안(정렬된 순서 유지, Comparator 사용 가능)
Queue
ConcurrentLinkedQueue: 동시성 큐, 비 차단(non-blocking) 큐
Deque
ConcurrentLinkedDeque: 동시성 데크, 비 차단(non-blocking) 큐
LinkedHashSet, LinkedHashMap 처럼 입력 순서를 유지하면서 멀티스레드 환경에서 사용할 수 있는 구현체는 제공하지 않고, 필요하다면 Collections.synchronizedXxx() 사용
스레드를 차단하는 블로킹 큐(BlockingQueue)
ArrayBlockingQueue
크기가 고정된 블로킹 큐
공정(fair) 모드 사용 가능. 단, 공정 모드 사용 시 성능 저하 가능성
LinkedBlockingQueue
크기가 무한하거나 고정된 블로킹 큐
PriorityBlockingQueue
우선순위가 높은 요소를 먼저 처리하는 블로킹 큐
SynchronousQueue
데이터를 저장하지 않는 블로킹 큐
생산자가 데이터를 추가하면 소비자가 그 데이터를 받을 때까지 대기
생산자-소비자 간의 직접적인 핸드오프(hand-off) 메커니즘 제공
중간에 큐 없이 생산자, 소비자가 직접 거래
DelayQueue
지연된 요소를 처리하는 블로킹 큐
각 요소는 지정된 지연 시간이 지난 후에야 소비
일정 시간이 지난 후 작업을 처리해야 하는 스케줄링 작업에 사용
Thread Pool And Executor
ℹ️ 스레드를 직접 사용할 경우의 문제점
1). 스레드 생성 시간으로 인한 성능 문제
메모리 할당:
각 스레드는 자신만의 호출 스택(call stack)을 가지고 있어야 함
이 호출 스택은 스레드가 실행되는 동안 사용하는 메모리 공간
따라서 스레드를 생성할 때는 이 호출 스택을 위한 메모리를 할당
운영체제 자원 사용:
스레드를 생성하는 작업은 운영체제 커널 수준에서 이루어지며
시스템 콜(system call)을 통해 처리
이는 CPU와 메모리 리소스를 소모하는 작업
운영체제 스케줄러 설정:
새로운 스레드가 생성되면 운영체제의 스케줄러는 이 스레드를 관리하고 실행 순서를 조정해야 함
이는 운영체제의 스케줄링 알고리즘에 따라 추가적인 오버헤드가 발생할 수 잇음
참고로 스레드 하나는 보통 1MB 이상의 메모리를 사용
2). 스레드 관리 문제
서버의 CPU, 메모리 자원은 한정되어 있으므로 스레드는 무한하게 만들 수 없음
시스템이 버틸 수 있는 최대 스레드의 수 까지만 스레드를 생성할 수 있게 관리 필요
인터럽트 등의 신호를 통해 스레드를 종료하고 싶을 경우 스레드 관리가 필요
3). Runnable 인터페이스의 불편함
반환 값이 없음:
실행 결과를 얻기 위해서는 별도의 메커니즘 필요
스레드가 실행한 결과를 멤버 변수에 넣어두고, join() 등을 사용해서 스레드가 종료되길 기다린 후 멤버 변수에 보관한 값을 받아야 함
예외 처리:
체크 예외(checked exception)를 던질 수 없음
체크 예외의 처리는 메서드 내부에서 처리 필요
Executor Framework
멀티스레딩 및 병렬 처리를 쉽게 사용할 수 있도록 돕는 기능의 모음
작업 실행 관리, 스레드 풀 관리를 효율적으로 처리해서 개발자가 직접 스레드를 생성하고 관리하는 복잡함 해소
스레드 풀, 스레드 관리, Runnable의 문제점, 생산자 소비자 문제까지 해결해주는 자바 멀티스레드 최고의 도구
ExecutorService
Runnable과 Callable 비교
.
ExecutorService 중료 메서드
서비스 종료
void shutdown()
새로운 작업을 받지 않고, 이미 제출된 작업을 모두 완료한 후에 종료
논 블로킹 메서드
List<Runnable> shutdownNow()
실행 중인 작업을 중단하고, 대기 중인 작업을 반환하며 즉시 종료
실행 중인 작업을 중단하기 위해 인터럽트 발생
논 블로킹 메서드
서비스 상태 확인
boolean isShutdown()
서비스가 종료되었는지 확인
boolean isTerminated()
shutdown(), shutdownNow() 호출 후, 모든 작업이 완료되었는지 확인
작업 완료 대기
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException
서비스 종료 시 모든 작업이 완료될 때까지 대기
이때 지정된 시간까지만 대기
블로킹 메서드
close()
자바 19부터 지원하는 서비스 종료 메서드
shutdown() 과 동일
정확히 shutdown() 호출 후, 하루를 기다려도 작업이 완료되지 않으면 shutdownNow() 호출
호출한 스레드에 인터럽트가 발생해도 shutdownNow() 호출
example
Future
작업의 미래 결과를 받을 수 있는 객체
Future Interface
submit() 호출 시 future 는 즉시 반환
덕분에 요청 스레드는 블로킹 되지 않고, 필요한 작업을 수행
작업의 결과가 필요하면 Future.get() 호출
Future가 완료 상태:
Future 에 결과도 포함 → 요청 스레드는 대기하지 않고, 값을 즉시 반환
Future가 미완료 상태:
작업이 아직 수행되지 않았거나 수행 중 → 요청 스레드가 결과를 받기 위해 블로킹 상태로 대기
Future 동작
Future 미사용: 두 스레드가
순차적으로
수행Future 사용: 두 스레드가
동시에
수행
Last updated