티스토리 뷰
* 개인 스터디 목적으로 게재한 글입니다. 정확한 정보가 아닐 수 있습니다.
목차
1. 프로그램과 프로세스 그리고 쓰레드란 ?
2. 언제 어떻게 쓰일까요 ?
3. 쓰레드 환경
4. 멀티쓰레드 관련 개념 정리
1. 프로그램과 프로세스 그리고 쓰레드란?
01. 프로그램과 프로세스
- 프로그램은 하드디스크, SSD에 적재되어 실행되기를 기다리는 정적인 데이터 파일입니다. 이 프로그램의 명령어와 정적인 데이터가 메
모리에 올라가면 생명이 있는 프로세스가 프로그램을 실행시킵니다. 즉, 프로세스란 실행중인 프로그램입니다.
여기서, 추가로 운영체제 위에서 여러 프로그램이 실행될 수 있는 것을 멀티 프로세스이라고 합니다.
출처: https://bowbowbow.tistory.com/16 [멍멍멍]
02. 그럼 쓰레드란 ?
위에서 프로세스가 실행중인 프로그램이라고 정의하였습니다. 여기서 프로세스 내부에서도 하나 이상의 실행 흐름이 동작합니다. 하나의 실행 흐름단위를 쓰레드라고 하며, 여러개의 쓰레드가 동작되는 환경을 멀티쓰레드 환경이라고 합니다.
위의 전체적인 내용을 좀 더 쉽게 예를 들어 정리하겠습니다. 운동단체에서 교육프로그램을 진행한다고 가정하겠습니다. 각각의 탁구, 배드민턴 단체는 회원을 모집하기 위해 이러한 전단지를 붙일겁니다.
- Title : 탁구/배드민턴 운동프로그램 회원 모집
- 장소 : 종합운동경기장
- 시간 : 매주 토요일 14:00
위의 전단지에 써져있는 단어를 통해 개념들을 비유하여 정리하겠습니다.
운영체제 -> 종합운동경기장
프로그램 -> 운동프로그램(회원 모집을 기다리고, 운영되기 위해 정적으로 존재하는 개념)
프로세스 -> 매주 토요일 14:00에 각각의 프로그램들이 운영되는 것
쓰레드 -> 혼자 벽에 대고 탁구를 치는 하나의 실행 흐름은 쓰레드
선수 두명이서 경기 운영을 하는 것을 멀티쓰레드
요약정리
프로그램 : 하드디스크, SSD에 적재되어 있는 정적인 데이터
프로세스 : 실행중인 프로그램
쓰레드 : 하나의 프로세스의 실행 흐름 단위
멀티쓰레드 : 하나의 프로세스에 병렬적으로 여러 쓰레드가 처리
2. 언제 , 어떻게 쓰레드를 쓸까요?
보통 단일쓰레드 환경보다는 멀티쓰레드 환경에서 쓰레드와 관련된 프로그래밍을 자주 구현합니다. 많은 데이터를 동시에 처리할 때 여러 쓰레드를 사용하고 관리해야하기 때문이죠. 이번 단원부터는 멀티쓰레드 위주로 간략히 정리하겠습니다.
When ? 여러 데이터를 여러 쓰레드로 동시에 처리하여 응답속도개선과 메모리 공유로 인하여 시스템자원 소모를 줄일 때
ex) 테트리스 게임을 하다보면 상대방화면과 제 화면이 보이면서, 동시에 게임을 진행 할 수 있습니다. (멀티 쓰레드)
오목게임은 한명이 수를 둬야 다음 수를 상대방이 진행 할 수 있습니다. (싱글 쓰레드)
Problem? 쓰레드는 일부분의 메모리를 공유하기 때문에 시스템자원 소모를 줄일 수 있지만, 공유된 데이터가 엉뚱한 값이 나올 수 있습니
다. 이런 엉뚱한 값을 처리 하기 위해 쓰레드간 상호 배제를 할 수 있도록 하여 데이터에 대한 동기화 작업이 필요합니다. 하지
만, 과도한 동기화 작업은 병목 현상으로 시스템 성능이 저하 될 수 있습니다. 그래서 상황에 맞게끔 동기화 작업이 필요합니다.
* 밑줄로 그어진 자세한 내용은 아래 쓰레드 환경에서 자세히 설명하겠습니다.
How ?
01. 쓰레드의 상태와 생명주기
java.lang.Thread 내부에 State라는 Enum 타입이 있습니다. 쓰레드의 상태를 열거한 내용입니다.
① NEW : 쓰레드가 생성되었지만 아직 실행되지 않은 상태
② RUNNABLE : 쓰레드가 작업 진행중인 상태, 운영체제 자원분배로 리소스를 기다리고 있는 상태가 될 수도 있다.
③ BLOCKED : 동기화된 블록/메서드에 들어가기 위해 락 해제를 기다리는 상태
④ WAITING : 다른 쓰레드가 통지할 때까지 대기하는 상태. Object.wait, Thread.join, LockSupport.park 메소드를 이용하여 이 상태로 들어갈 수 있다.
⑤ TIMED_WAITING : 지정한 시간 동안 대기하는 상태. Thread.sleep, Thread.join, LockSupport.parkNanos, LockSupport.prkUntil 메소드를 이용하여 TIMED_WAITING 상태로 들어갈 수 있다.
⑥ TERMINATED : 실행을 완료한 상태
- BLOCKED와 WIAITING과 WIAITING / TIMED_WAITING 상태의 차이점
WIAITING : 쓰레드가 Object.wait 메소드를 호출하면 취득한 monitor를 해제하고 waiting 상태가 됩니다. waiting에 대
기중인 쓰레드는 notify(), notifyAll() 호출에 의해 쓰레드의 대기 상태가 끝나고, 다시 monitor를 얻기 위해
시도 합니다.
BLOCKED : 여러 쓰레드가 monitor를 획득하는 과정에서 하나의 스레드가 lock의 권한을 받고 monitor에 진입하면, 나
머지 쓰레드는 BLOCKED 상태가 됩니다.
* 여기서 monitor는 공유자원 획득 정도로 생각하시면 될 것 같습니다.
=> WIAITING은 monitor에 진입한 상황에서 monitor를 해제하고 waiting 상태로 가는 반면, BLOCKED는 monitor에 진입 전에 JVM 스케줄러에 의하여 대기 상태를 말합니다.
TIMED_WAITING과 WIAITING
=> WIAITING 상태는 다른 스레드 상태에 의존합니다. Object.wait 메서드를 호출하여 자신 쓰레드를 WAITING 상태로
전환하면, 다른 쓰레드가 동일한 객체에 대해 notify(), notifyAll() 메서드 호출하기 전 까지는 무기한 대기합니다.
반면, TIMED_WAITING 상태는 지정된 시간 동안만 대기시간을 갖고, 이후 다시 실행상태로 됩니다.
02. 주요 API 정리
- java.lang.Thread
Type | Method | Description |
void | start() | JVM이 해당 쓰레드의 run 메소드를 호출하여 쓰레드를 실행한다. |
void | run() | 쓰레드가 실행 할일을 정의한 메소드 |
void | join() | 쓰레드가 종료될 때까지 대기한다. |
static void | sleep() | 현재 실행중인 쓰레드를 지정된 초 동안 실행 중지되도록 한다. 쓰레드는 monitor의 소유권을 잃지 않는다. |
static void | yield() | 우선순위가 더 높거나 동일한 상태의 쓰레드에게 양보한다. |
- java.lang.Object
Type | Method | Description |
void | wait() | 해당 쓰레드는 해당 객체의 모니터링 락에 대한 권한을 가지고 있다면 모니터링 락의 권한을 놓고 waiting pool에 넣는다. |
void | notify() | waiting pool에 대기중인 쓰레드 하나를 깨운다. |
void | notifyAll() | waiting pool에 대기중인 모든 쓰레드를 깨운다. |
Thread와 관련된 내용인데 java.lang.Object에 쓰레드 관련 메서드가 있습니다. 그리고 monitor라는 생소한 용어가 나오는데 이건 뒤에서 다시 정리하겠습니다.
03. 간단한 구현 예제
- Thread Class 상속
- Runnable Interface 구현
Thread 구현은 상기의 두 가지 방식으로 구성되어있습니다. 일반적으로 Thread Class를 상속받으면 다른 클래스를 상속받지 못하기 때문에 Runnable Interface를 구현합니다.
- Thread Class 상속 예제
public class ThreadExtends extends Thread {
@Override
public void run() {
//start에 의해 호출된 run 메서드는 로직을 구현합니다.
}
public static void main(String[] args) throws InterruptedException {
ThreadExtends t1 = new ThreadExtends("t1");
t1.start(); // t1 인스턴스 쓰레드를 실행시킵니다.
}
}
-Runnable 구현 예제
public class ThreadRunnable implements Runnable {
@Override
public void run() {}
public static void main(String[]args) {
Runnable r = new ThreadRunnable();
Thread t1 = new Thread(r);
t1.start();
}
}
3. 쓰레드 환경
01) Thread 환경과 Object 환경
초록색박스의 단일 쓰레드 환경에서 Thread 1의 Object Context를 보시면 static으로 선언된 int 변수가 다른 Object끼리 내용을 공유 할 수 있습니다. 예를 들어 초기값인 Test 클래스의 staticInt값은 0입니다. 여기서 staticInt값을 7로 변경하면 서로 다른 Object끼리 변경된 7값을 공유 할 수 있습니다.
그런데 멀티쓰레드 환경이라면 어떨까요?
staticInt 초기값은 0입니다. 그런데 Thread1이 staticInt를 7로 바꾸면 Thread2가 staticInt를 호출하면 해당값이 7로 되어있지 않고, 최초 초기값이 0이 할당됩니다.
왜 그런지 아래의 그림을 통해 알아보겠습니다.
02) Thread 환경에서의 데이터 읽기/쓰기
Thread 환경에서는 성능상의 이유로 각각의 thread가 Main Memory로부터 읽은 변수 값을 CPU cache로 복사합니다.
무슨 의미냐면, 위의 예제의 staticInt의 초기값인 0이 Thread1과 Thread2의 각각의 CPU cache에 복사됩니다. 그래서 변경된 staticInt 7의 값은 Thread1의 CPU cache에만 존재하여 있고, Thread2의 CPU cache에는 최초 초기값인 0이 할당되어집니다.
그럼 static 변수의 의미가 없는 걸까요? 일단 제가 아는바로는 static으로 선언된 변수는 Main Memory로 할당은 되는데 Main Memory에 쓰는 시점과 Thread2의 CPU cache가 언제 복하는 시점을 모른다는 문제가 있습니다.
이러한 문제를 해결 하기 위하여 volitaile이라는 키워드가 있습니다. 자세한 내용은 좀 더 뒤에서 정리하겠습니다.
http://tutorials.jenkov.com/java-concurrency/volatile.html
위의 Thread 환경은 static 변수와 관련된 Thread 환경에 국한되어 있을 수 있습니다. Thread 환경 관련된 내용은 하드웨어의 메모리 구조, JVM 구조와 연관되어 설명된 글들이 많이 있습니다. 더 원하시는 분들은 별도로 찾아보시기 바랍니다.
04. 멀티쓰레드 관련 개념정리
Lock? 모니터의 상호 배타 기능을 구현하기 위해 하나의 스레드에게만 '소유'할 수 있는 특권과 같습니다. 예를 들어, 개인카
페에서 공용 화장실을 가기 위해 '열쇠'를 들고 화장실에 들어갈 수 있습니다.(열쇠는 단 하나만 존재) 여기서 '열쇠'가
Lock의 역할을 합니다.
synchronized? 특정 메서드나 코드 블록을 한 번에 한 스레드만 사용하도록 보장하는 키워드
축구에서 스로인하는 상황입니다. 축구공이 공유자원이고, 플레이어가 쓰레드라고 생각하시면 됩니다. 스로인 상황에서는 한명의 선수만 공을 취득하여 사용할 수 있습니다. 이런 스로인 상황을 synchronized라고 이해하시면 될 것 같습니다.
Monitor
- Lock과 synchronized 메서드를 사용하여 동기화의 상호 배제 측면을 구현하는 메커니즘
- Thread-safe object / class / module이라고도 함
특별실이 있는 건물로 볼 수 있습니다. 특별 실은 한 번에 한 명의 고객 (쓰레드) 만 사용할 수 있습니다. 방에는 대개 일부 데이터와 코드가 포함되어 있습니다. Monitor는 Lock(=mutext)와 condition variables로 구성되어 있으며, 두 종류의 monitor 영역으로 구성되어 있습니다.(synchronized methods / synchronized statements)
* Java 쓰레드 주요 API 문서 항목에서 java.lang.Object 클래스가 있었습니다. 모든 object가 상호 배제를 위한 메커니즘에 적용될 수 있도록(lock과 synchronized) 설계하기 위해 Thread클래스가 아닌 Object 클래스에 속해 있다고 추측합니다. (https://okky.kr/article/573595 답변글을 바탕으로 작성)
Volatile
1) 개념
- 변수를 Main Memory에 저장하는데 사용합니다.
- Volatile을 선언한 모든 Read가 CPU cache가 아닌 Main Memory에 읽히며, Write도 CPU cache뿐만 아니라 Main Meory에 기록된다
는 것을 의미합니다.
2) 예제
- 두 개 쓰레드가 다음과 같이 선언 된 counter 변수를 포함하는 SharedObject에 접근하는 예제입니다.
public class SharedObject {
public int counter = 0;
}
Thread1만 counter 변수를 증가시키고, Thread1과 Thread2는 counter 변수를 읽을 수 있는 상황입니다. 해당 예제를 실행하면 아래의 그림과 같은 결과가 나올겁니다.
Thread1이 변경한 counter 변수는 Thread1의 CPU cache에만 적용되어있습니다. Thread2는 최초 Main Memory로 부터 읽은 counter값을 읽어 변수값이 0이 나오게 됩니다.
해당 예제를 통해 한 쓰레드의 데이터 업데이트는 다른 쓰레드가 볼 수 없다는 것을 알 수 있습니다. 다른 쓰레드간 볼 수 없는 특성은 static 필드를 선언 하였을 때 문제가 발생합니다. 아래의 예제를 통해 알아보겠습니다.
- static 필드( Effective Java에 나온 예제 )
public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException{
Thread backgroundThread = new Thread(new Runnable() {
public void run() {
int i=0;
if(!stopRequested) {
while(true) {
i++;
System.out.println(i);
}
}
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
boolean 값이 static으로 선언되어 있습니다. main 쓰레드가 변경한 true 값이 background쓰레드에게 전역적으로 공유되어 1초만에 해당 프로그램이 종료가 될거라고 예상했지만, 해당 예제도 변수값 동기화 불일치로 계속 실행됩니다.
결국, static 필드도 일반 필드와 똑같이 Thread 환경에서는 각각의 CPU cache에만 반영 된다는 것을 알 수 있습니다. 이 문제를 해결하기 위해 volatile 키워드를 사용하여 해결 할 수 있습니다.
public class StopThread {
private static volatile boolean stopRequested;
public static void main(String[] args) throws InterruptedException{
// 생략
}
volatile가 상호 배제성을 실현하진 않지만, 모든 쓰레드에게 가장 최근에 기록된 값을 읽도록 보장합니다.
3) 단점
- CPU cache보다 Main Memory 사용 비용이 더 큽니다. 적합한 상황에만 volatile을 쓰는 것이 좋습니다.
- 하나의 쓰레드만 Write/Read하고, 다른 쓰레드는 Read만 하는 것이 신뢰성있는 데이터를 만들 수 있습니다.
//잘못된 예제
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber(){
return nextSerialNumber++;
}
nextSerialNumber ++ 연산자가 원자적이지 않는 문제점을 발생합니다. Thread1이 generateSerialNumber 메서드를 통해 static 변수 값을 올린 후, 변경된 값을 main memoriy 기록하기 전에 Thread2가 필드에서 같은 값을 읽으면, 두 쓰레드는 같은 값을 가지게 됩니다.
이상 쓰레드 개념편을 마치겠습니다. 해당 편은 아직 제가 지식이 부족하여 두서없이 작성한 부분이 많은 것 같습니다.
지속적으로 업데이트하도록 하겠습니다.
[Reference]
Effective Java2/E
http://tutorials.jenkov.com/java-concurrency/volatile.html
https://nesoy.github.io/articles/2018-06/Java-volatile
http://tutorials.jenkov.com/java-concurrency/volatile.html
https://nesoy.github.io/articles/2018-06/Java-volatile
https://sjh836.tistory.com/121 [빨간색코딩]
https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.State.html
http://happinessoncode.com/2017/10/05/java-object-wait-and-notify/
https://server-engineer.tistory.com/276
https://www.jpstory.net/2015/03/02/mutex-semaphore-monitor/
https://www.jpstory.net/2015/03/02/mutex-semaphore-monitor/
http://geekexplains.blogspot.com/2008/07/threadstate-in-java-blocked-vs-waiting.html
https://www.jpstory.net/2015/03/02/mutex-semaphore-monitor/
https://about-myeong.tistory.com/34
https://www.jpstory.net/2015/03/02/mutex-semaphore-monitor/
https://dailyworker.github.io/java-thread/
https://parkcheolu.tistory.com/14
https://goodgid.github.io/What-is-Multi-Thread/
https://ospace.tistory.com/109
https://stackoverflow.com/questions/2423622/volatile-vs-static-in-java