JAN's History
자바의 정석 CH 13 쓰레드 본문
1. 쓰레드
프로세스 : 실행중인 프로그램, 자원(resources, Ex) 메모리, CPU)과 쓰레드로 구성
쓰레드 : 프로세스 내에서 실제 작업을 수행. 모든 프로세스는 최소한 하나의 쓰레드를 가지고 있다.
프로세스 : 쓰레드 = 공장 : 일꾼 <= 비유하자면, 프로세스가 공장이고 쓰레드가 일꾼이라는 뜻

싱글 쓰레드 프로세스 = 자원 + 쓰레드
멀티 쓰레드 프로세스 = 자원 + 쓰레드 + 쓰레드 + ... + 쓰레드
멀티 쓰레드는 한 프로세스 내에서 여러 일꾼이 있기 때문에 동시에 많은 일을 할 수 있다.
대부분 우리가 사용하는 프로그램은 멀티쓰레드로 작성되어있다.
=> 여태껏 작성해온 건 싱글 쓰레드 프로세스 ! 앞으로는 멀티 쓰레드 프로세스를 작업하려고 하는 것
" 하나의 새로운 프로세스를 생성하는 것보다 하나의 새로운 쓰레드를 생성하는 것이 더 적은 비용이 든다 "
=> 공장이 두개냐 일꾼이 두명이냐.
대부분의 프로그램이 멀티쓰레드로 작성되어있다. 그러나, 멀티쓰레드 프로그래밍이 장점만 있는 것은 아니다.
| 장점 | - 시스템 자원을 보다 효율적으로 사용할 수 있다. - 사용자에 대한 응답성이 향상된다. - 작업이 분리되어 코드가 간결해진다. " 여러모로 좋다. " |
| 단점 | - 동기화에 주의해야한다. - 교착 상태가 발생하지 않도록 주의해야한다 Ex) A와 B가 각각 톱과 망치를 가지고 있을 때, 서로 교환하지 못하고 있는 상황 - 각 쓰레드가 효율적으로 고르게 실행될 수 있게 해야한다. " 프로그래밍할 때 고려해야 할 사항들이 많다. " |
1-2 쓰레드의 구현과 실행
1. 쓰레드 클래스를 상속한다.
=> 자바는 단일 상속이기 때문에 쓰레드를 구현하면 다른 객체를 상속받지 못하므로 인터페이스 구현이 best


=>상속받은 쓰레드 클래스를 실행하는 방법
2. Runnable 인터페이스를 구현한다.
=> 다른 클래스를 상속받을 수 있으니 인터페이스 구현이 더 BEST

Runnable이라는 인터페이스를 만들고 추상메서드로 run을 생성한 후 구현하면 된다.
=> 작업할 내용을 run(){}으로 적는 것은 둘 같다.

=> 인터페이스를 쓰레드로 구현하는 방법.
외부에서 구현한 run을 Thread로 호출하는 것.
(=sort(comparator c) 에서 comparator이 compare(){}을 구현한 인터페이스를 호출한 것과 같은 원리)
+ 두 줄을 한 줄로 줄이면 주석 처리된 것과 같다.


상속받을 땐 this.getName()으로 (this생략 가능) Thread의 getName을 호출해서 사용가능하지만
Runnable에선 Thread.currentThread()를 this대신 사용해야한다.

=> 쓰레드를 t1, t2로 생성해서 두개를 굴리고 있기 때문에 동시에 굴러간다! 0000111이 아니라 001100..
- 쓰레드를 생성한 후 start()를 호출해야 쓰레드가 작업을 시작한다.

t1이 먼저 실행시켰다고 해서 먼저 실행되는 것은 아님.
Os스케쥴러가 실행순서를 결정하기 때문에 실행 상태가 된 것일 뿐 바로 실행되는것은 아니다.
=>start했다고 해서 바로 실행되는 것은 아니며 먼저 start해서 먼저 실행되는 것도 아니다.

run()을 실행시킨 것이 아니라 MyThread를 생성하고 t1를 실행시킨 것인데 왜 run이 실행될까?
- start로 쓰레드를 사용하면 새로운 호출스택이 생성되어 쓰레드에 있는 객체가 올라온다.
=> 반드시 start로 호출해야 서로 독립적인 작업을 수행할 수 있다. 그래서 run을 호출하는 것이 아니라 start를 호출!

- main메서드의 코드를 수행하는 쓰레드
- 쓰레드는 '사용자 쓰레드'와 '데몬 쓰레드' 두 종류가 있다.
=> 데몬 쓰레드는 보조 쓰레드와 같은 역할이다.
실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램을 종료된다.

=> main 쓰레드가 사용을 종료해도 아직 실행중인 쓰레드 run이 있기 때문에 종료되지 않는다.

main 쓰레드와 run쓰레드가 각각 2개 생겨 총 3개의 독립적인 쓰레드가 생성되었다.
th1.join은 main쓰레드의 th1작업이 끝날때 까지 기다리는 메서드!
즉, 2번 join을 썼으니 th1 작업이 끝날때 까지 main이 기다리고, 또 th2가 끝날때까지도 기다리는 것
이것이 없으면 main이 실행되고 쓰레드가 생성되고, 할일을 다 했으므로 종료되어 소요시간이 바로 출력되어버린다.
=>main 쓰레드가 죽었다고해서 프로그램이 종료되는 것이 아니라 나머지 쓰레드가 끝날 때 까지 기다린다.

싱글쓰레드로 작성되었을 땐 작업 두가지가 절대 겹치지 않는다.
멀티쓰레드는 작업 두가지가 번갈아가며 실행된다.
(그러나 Os스케쥴러가 멀티쓰레드를 스케쥴링하기 때문에 순서와 시간은 매번 달라 우리가 제어할 수는 없다.)
=>멀티쓰레드는 th1와 th2의 context switching을 진행해서 오히려 시간이 더 걸리지만 동시에 작업을 수행한다는 장점이 있기 때문에 사용하는 것이다.
쓰레드의 I/O블락킹

싱글쓰레드에선 A와 B의 객체가 따로 수행되기 때문에 A에서 입력을 받을때까지 기다려야한다.
즉, 사용자로부터 입력을 기다리는 구간엔 아무 일도 하지 않는다.

멀티쓰레드는 사용자로부터 입력을 기다리는 동안 B의 작업을 동시에 수행할 수 있다.
=> 싱글쓰레드보다 작업을 빨리 마칠 수 있다는 장점이 있다.
쓰레드의 우선순위
- 작업의 중요도에 따라 쓰레드의 우선순위를 다르게 하여 특정 쓰레드가 더 많은 작업시간을 갖게 할 수 있다.

자바에선 1~10까지 우선순위를 가진다. 10이 최대 1이 최소.
우리가 쓰레드를 만들고 지정순위를 만들해주지 않으면 5로 지정된다.
우선순위를 알고 싶으면 getPriority로 우선순위를 알 수 있다.
우선순위를 변경하고 싶으면 setPriority로 (7)과 같이 변경할 수도 있다.

우선순위가 같으면 비슷한 시간을 할당받아 작업을 수행한다.
A의 우선순위가 높으면 더 많은 시간을 할당받아 작업을 수행해 결과적으로 빠르게 종료된다.
=> 우선순위는 단지 희망사항일 뿐 희망사항대로 되지 않는다. Os스케쥴러는 참고를 할 뿐..!

=> th2가 우선순위가 더 높지만 th1이 더 먼저 끝나는 경우도 있다. 확률적으로는 th2가 더 일찍 끝나긴 한다.
쓰레드 그룹
- 서로 관련된 쓰레드를 그룹으로 묶어서 다루기 위한 것
- 모든 쓰레드는 반드시 하나의 쓰레드 그룹에 포함되어 있어야 한다.
- 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 기본적으로 'main쓰레드 그룹'에 속한다.
- 자신을 생성한 쓰레드(부모 쓰레드)의 그룹과 우선순위 5를 상속받는다.



=> 대부분 쓰레드 그룹은 묶여져서 메서드로 다루어진다. 또, 쓰레드를 다룰 땐 그룹으로 다룬다.
1-3 데몬 쓰레드
- 일반 쓰레드의 작업을 돕는 보조적인 역할을 수행(쓰레드의 종류는 일반 쓰레드와 데몬쓰레드)
- 일반 쓰레드가 모두 종료되면 자동적으로 종료된다.
- 가비지 컬렉터, 자동저장, 화면 자동갱신 등에 사용된다.
- 무한루프와 조건문을 이용해서 실행 후 대기하다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성.

=> 무한루프이기 때문에 계속 실행되면 안되니까 if문을 사용해서 특정 조건을 만들어 종료할 수 있게 한다.
3초마다 autosave가 되어있는지 확인하고 autosave가 되어있으면 autosave, 자동저장할 수 있도록 한다.

=> setDaemon에서 boolean 값이 true이 되면 데몬쓰레드가 된다.
=> isDaemon은 데몬쓰레드인지 확인하여 맞으면 true를 반환한다.
*setDaemin(boolean on)은 반드시 start()를 호출하기 전에 실행되어야 한다.
그렇지 않으면 IllegalThreadStateException이 발생한다.

데몬쓰레드는 보조역할이기 때문에 main쓰레드가 종료되면 자동종료된다.
=>원래는 프로그램을 종료합니다. 에서 종료되어야하는데 데몬쓰레드를 주석처리해서 지금 t쓰레드가 계속 진행되는것
+쓰레드 객체 방법 1. Thread상속 2.Runnable인터페이스 구현 중 2번을 사용한 것
run 객체를 구현한 클래스 Ex13_7를 받아 쓰레드 생성


쓰레드는 생성되면 NEW가 된다.
start를 호출하면 실행중, 혹은 실행대기 상태인 RUNNABLE이 된다. = 줄서기
그러다가 차례가 되면 자기에게 할당된 시간동안 작업하고 다시 돌아가 줄선다.
그리고 작업을 다 끝내면 stop()가 되어 TERMINATED 소멸된다
작업 중에 suspend(일시정지), sleep(잠자기), wait, join(기다리기), I/O block (입출력대기)를 만나면 일시정지가 된다.
WAITING, BLOCKED는 즉 쉼터, 대기실과 같은 존재이다.
그러다가 time-out, resume(), notify, interrupt()를 만나면 다시 작업을 재개한다. interrput는 깨우기.
쓰레드의 실행제어
- 쓰레드의 실행을 제어할 수 있는 메서드가 제공된다.
- 이 들을 활용해 보다 효율적인 프로그램을 작성할 수 있다.

sleep - 잠들게하기. 즉 일시정지 시킨다
join - 다른 쓰레드 기다리기
interrupt - sleep이나 join 즉 자거나 기다리는 것을 깨우는 것
stop - 종료시키기
suspend - 일시정지 시키기
resume - suspend를 재개시키기
yield - 자신에게 주어진 시간을 양보하기
=> static 이 붙은 sleep와 yield는 쓰레드 자기 자신에게만 호출이 가능하다.
내가 잘 순 있어도 다른 사람을 재울 순 없다는 뜻.
1-4 sleep()
- 현재 쓰레드(sleep(), yield())를 지정된 시간동안 멈추게 한다.

3000을 넣어야 3초 멈추는 것
- 예외 처리를 꼭 해야한다. (InterruptedException이 발생하면 깨어남)

sleep 상태에 있는 메서드는 1. time ip이 되던가 2.interrupted 깨워지던가 하여 끝난다.
여기서 아무런 인터럽가 발생하기 않으면 0.0015초 후에 끝난다.
만약 자는동안 누군가 깨우면 throw new Interrupted Exception이 발생하여 예외를 던져 잠자는 상태를 벗어나게 된다.
+ catch는 아무런 내용도 적지 않아도 된다. 예외와 try-catch문을 이용해 잠자는 상태를 벗어나게 하기 위함이기 때문에 catch블럭에서 할 일이 아무것도 없다.

그러나 대부분은 계속해서 예외처리를 하기 귀찮으니 메서드로 만든다
사용할 때에는 try-catch문 대신 delay(15)처럼 사용하면 된다.
- 특정 쓰레드를 지정해서 멈추게 하는 것은 불가능하다.

th1.sleep처럼 쓰면 쓰레드 1을 sleep하는 것처럼 오해할 수 있기 때문에 에러는 나지 않지만 저렇게 쓰면 안된다.
저렇게 써도 main쓰레드 안에 있으면 main쓰레드가 2초 잠자는 것으로 코드가 실행된다.
=>반드시 클래스 이름을 써야 오해의 소지가 없다.

try-catch문에서 th1.sleep(2000)을 main쓰레드에서 사용했으므로 th1이 아니라 main 쓰레드가 2초동안 잠드는 것이다.
=> 오해의 소지를 덜기 위해 th1을 Thread.sleep(2000);으로 바꿔야한다!
그래서 결과물을 보면 <main 종료>가 먼저 찍히지 않고 2초 후에 작업이 다 끝난 후 찍힌 것!

이렇게 예외처리 메서드를 하나 따로 만들어주는게 더 깔끔하다
sleep이 static이니까 메서드를 따로 만들때에도 static으로 생성해야한다.
+ InterruptedException은 필수 예외라서 꼭 try-catch문 생성해야함
1-5 interrupt()
- 대기상태(WAITING)인 쓰레드를 실행대기 상태(RUNNABLE)로 만든다.
대기 상태 : 작업이 중단된 상태 ex)sleep, join, wait..


th1 를 인터럽트하고 isInterrupted하면 true가 반환된다.

쓰레드엔 인터럽트가 호출되었는지 알려주는 interrupted가 있고 호출되면 그 값이 false에서 true로 바뀐다.
그리고 isInterrupted를 호출하면 그 값을 반환하는 것 뿐이다.
그런데 interrupted는 그 값을 다시 true에서 false로 바꾸는 것이다. 다음에 또 누군가가 호출할 수 있으니 초기화하는 것

while문은 다운로드를 다 끝내거나 인터럽트가 발생했을 때까지 도는 것
isInterrupted가 호출되면 true인데 !가 붙어서 false가 되는 것! 즉 취소버튼을 누를때까지 돈다.
: isInterrupted가 true라는 말은 인터럽트가 실행되었다는 뜻.

값을 입력받기 전엔 인터럽트가 발생되기 않았으므로 false.
값을 입력받은 후엔 인터럽트가 발생했으므로 true. 즉, 카운트다운이 종료된다.

isInterrupted는 static이 아니므로 th1.으로 하면 th1 쓰레드의 인터럽트 값을 반환해주지만
*interrupted는 static 메서드이므로 th1.interrupted라고 해도 main쓰레드 인터럽트 값을 반환해줘 false값이 나온다*

th1의 쓰레드 인터럽트값을 interrupted 메서드로 반환받고 싶으면 th1 쓰레드 안에 넣어야함
그리고 isinterrupted는 this.isinterrupted가 됨
interrupted는 isinterrupted와달리 다시 false로 초기화되기 때문에 두번 호출하면 false값을 반환
1-6 suspend, resume, stop()
- 쓰레드의 실행을 일시정지, 재개 ,완정정치 시킨다.

suspend, sleep, wait .. 를 만나면 일시정지 박스에 들어간다. resume 같은 애들을 만나면 다시 줄서기한다.
=> 그러나 위 세가지 메서드는 교착상태를 일으키기 쉬워 deprecated(사용권장하지않음)이 되었다.

=> 그렇다면 우리가 직접 구현하면 된다.
while문은 stoped이 true가 될 때 까지. if문은 suspended가 true라면 코드를 반복해서 수행하는 것.
그러다가 suspend를 호출하면 suspended가 !true가 되니까 false가 되어 if문은 실행이 안된다.
+if문은 조건문이 true일 때 실행
resume이 되면 !false가 되어 if문이 작업을 수행하게된다.
stop이 되면 stopped가 true가 되어 while문이 false가 되며 반복문이 끝이난다.
1-7 join()
- 지정된 시간동안 특정 쓰레드가 작업하는 것을 기다린다.

=> 얼마나 기다릴지 안주면 작업이 모두 끝날때까지 기다린다.

- 예외 처리를 해야한다.(InterruptedException 가 발생하면 작업 재개)

작업시간을 확인하기 위해선 쓰레드가 작업이 끝날때까지 기다려야 하기 때문에 사용!

가비지 컬렉터(=데몬쓰레드) 예시!
+데몬쓰레드는 일반쓰레드가 없으면 종료되기 때문에 무한루프이다.
메모리가 부족한 경우 잠자고 있는 쓰레드 gc를 깨운다.
그리고 그 메모리를 사용하는데, 그 전에 gc가 메모리를 정리할 시간을 줘야하기 때문에 join을 사용한다.
1-8 yield()
- 남은 시간을 다음 쓰레드에게 양보하고, 자신(현재 쓰레드)은 실행대기한다.

=> yield도 static 메서드이기 때문에 자신에게만 사용 가능
-yield()와 interrupt()를 적절히 사용하면 응답성과 효율을 높일 수 있다.

만약 suspended가 true라서 if문이 false면 작업을 수행하지 못하고 while문만 반복해서 돌게 된다.
그럴 땐 else로 Thread.yield로 자신의 시간을 양보하는 것이 더 효율적이다!
그리고 suspend와 stop에도 interrupt를 넣어야한다.
자고있을 수 있기 때문에 호출 시 1초 후에 다시 깨워서 다시 멈추거나 일시정지 시키는 것이다.
그래야 응답성이 좋아지는 것.
1-9 쓰레드의 동기화
- 멀티 쓰레드 프로세스에서는 다른 쓰레드의 작업에 영향을 미칠 수 있다.
- 그래서 진행중인 작업이 다른 쓰레드에게 간섭받지 않게 하려면 '동기화'가 필요
쓰레드의 동기화 - 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 하는 것
- 동기화 하려면 간섭받지 않아야 하는 문장들은 '임계 영역'으로 설정한다.
- 임계영역은 락(lock)을 얻은 단 하나의 쓰레드만 출입가능(객체 1개에 락 1개) 락이 없으면 임계영역에 들어올 수 없다.
-synchronized로 임계영역(lock이 걸리는 영역)을 설정하는 방법 2가지

1. 메서드 전체를 임계영역으로 지정 (가능한 지양)
- synchronized를 메서드 앞에 붙여주면 다 임계영역이 된다.
그러나 임계영역은 한 스레드당 하나만 선언이 가능해서 최소화하는 것이 좋다.
임계영역이 많을 수록 성능이 떨어진다. 임계영역은 1번에 1쓰레드만 들어올 수 있기 때문에 성능이 떨어지는 것
2. 특정한 영역을 임계영역으로 지정
- synchronized(객체의 참조변수)로 지정하면 된다.

this는 전체 객체를 가르키는 것.

출금메서드는 1개의 1메서드만 실행할 수 있다.
run메서드는 랜덤한 금액을 출금한 후 withdraw를 호출한다.
만약 synchronized가 없을 땐 if문을 통과하는 동안 A객체와 B 객체가 동시에 들어와 마음대로 출금할 수 있다.
=> 그래서 임계영역으로 묶어 한번에 한 쓰레드 들어올 수 있도록 한다.
그러면 음수가 나오지 않는다.
1-10 wait()와 notify()
- 동기화의 효율을 높이기 위해 사용 wait - 기다리기, notify - 통보, 알려주기
- object클래스에 정의되어 있으며 동기화 블럭 내에서만 사용가능
- wait() - 객체의 lock를 풀고 쓰레드를 해당 객체의 waiting pool에 넣는다.
- notify() - waiting pool에서 대기중인 쓰레드 중 하나를 깨운다
- notifyAll() - waiting pool에서 대기중인 모든 쓰레드를 깨운다.

출금할 돈이 없으면 반복문으로 기다리게 된다.
임계영역에서 쓰레드가 이 때 멈춰있으면 다른 쓰레드가 못들어온다.
어떤 쓰레드가 입금하려고 해도 출금에 lock이 걸려있기 때문에 진행이 안됨
이때 wait을 이용해 waiting pool에서 lock를 반납하고 기다린다.
그렇다면 다른 쓰레드가 자물쇠를 갖고 입출금을 진행 후 notify로 알려준다.
그럼 기존의 쓰레드는 다시 자물쇠를 갖고 다시 출금으로 간다. 이때 또 돈이 부족하면 waiting pool에 들어간다.
+동기화를 읽고 쓰는 객체는 모두 동기화가 되어있어야하기 때문에 입금영역도 동기화되어있는 것

- 요리사는 Table에 음식을 추가. 손님은 Table의 음식을 소비
- 요리사와 손님이 같은 객체(Table)을 공유하므로 동기화가 필요.
Table에는 음식을 추가하는 배열 메서드가 있고 요리를 테이블에서 제거하는 메서드가 있다.
+ArrayList는 동기화가 되어있지 않고 Vector은 되어있다.
Cook(요리사)에는 table에 음식(dish)을 추가하는 일을 한다.
Customer은 table의 음식(dish)를 먹는 일 즉, 제거하는 일을 한다.
=> Table을 모두 사용하기 때문에 동기화를 사용해야한다.

예외 1 - 요리사가 Table에 요리를 추가하는 과정에 손님이 요리를 먹음
=>ArrayList를 읽기 중에 add나 remove가 발생함.
예외 2 - 하나남은 요리를 손님2가 먹으려 하는데 손님1이 먹음.

[문제점] Table을 여러 쓰레드가 공유하기 때문에 작업 중 끼어들기 발생
[해결책] Table의 add()와 remove()를 synchronized로 동기화
synchronized를 remove에 그냥 넣어도 되는데 블럭으로 사용하는 방법도 보여주기 위한 것.

[문제] 예외는 발생하지 않지만 손님(CUST2)이 table에 lock건 상태로 지속
요리사가 table의 lock을 얻을 수 없어 음식을 추가하지 못함. => 비효율적 , wait() & notify()를 사용하면 된다.

[문제] 음식이 없을 때, 손님이 Table의 lock을 쥐고 안놓는다.
요리사가 lock을 얻지 못해 음식을 추가할 수 없다.
[해결책]음식이 없을 때 wait()으로 손님이 lock을 풀고 기다리게 하자. 그리고 대기실에서 손님이 기다린다.
만약 테이블에 음식이 있으면 음식을 먹고 요리사에게 통보! (음식을 채워야하니까)
MAX_FOOD, 테이블에 음식이 가득차면 대기실에 요리사도 기다릴 수 있다.
요리사가 음식을 추가하면 notify로 손님에게 알리자(손님이 lock을 재획득)
try-catch문은 원하는 음식이 없는 경우엔 wait하고 대기실에서 기다린다.
=> 요리사는 테이블이 가득 차면 대기wait하고, 음식을 추가하고 나면 손님에게 notify한다.
=> 손님은 음식이 없으면 대기wait하고, 음식을 먹고나면 요리사에게 notify한다.

- 전과 달리 한 쓰레드가 lock을 오래 쥐는 일이 없어져 효율적이 되었다.
=> 그러나 누구에게 wait하고 notify할 지 구분이 되어있지 않아 불분명하다.
그래서 나온 것이 Lock & Condition.
즉, 동기화를 더 효율적으로 하려면 wait, norify하는 것
'자바' 카테고리의 다른 글
| 자바의 정석 CH14 람다식 (0) | 2023.04.25 |
|---|---|
| 자바의정석 CH 12 - 2 열거 (0) | 2023.04.23 |
| 자바의정석 CH 12 - 1 지네릭스, 열거형, 애너테이션 (0) | 2023.04.20 |
| 자바의정석 CH 11 컬렉션 프레임웍 - 2 (0) | 2023.04.19 |
| 자바의정석 CH 11 컬렉션 프레임웍 - 1 (0) | 2023.04.18 |