생산자와 소비자 입장 멀티스레드

Park sang woo·2024년 11월 4일
0

인프런 공부

목록 보기
10/13

🐹 생산자와 소비자


생산자는 데이터를 생산하는 역할로 파일에서 데이터를 읽어오거나 네트워크에서 데이터를 받아오는 스레드가 생산자 역할을 할 수 있습니다.
그림에서는 사용자 입력을 받아서 프린터라는 인스턴스의 큐에 넣어준 것이 생산자의 역할입니다.

소비자는 생성된 데이터를 사용하는 역할을 합니다. 데이터를 처리하거나 저장하는 스레드가 소비자 역할을 합니다. 그림에서 프린터의 경우 생산자가 넣어둔 큐에 있는 데이터를 꺼내서 받아가지고 출력하는 스레드가 소비자 역할을 하는 것입니다.

버퍼는 생산자가 생성한 데이터를 일시적으로 저장하는 공간입니다. 이 버퍼는 한정된 크기를 가지며, 생산자와 소비자가 이 버퍼를 통해 데이터를 주고받습니다. (생산자가 생산한 데이터를 소비자가 받기)
그림에서는 프린터 큐가 버퍼의 역할을 한다고 할 수 있습니다.


🕹️ 문제 상황

1.생산자가 너무 빨리 생산해서 더는 데이터를 넣을 수 없을 때까지 생산해버리면 버퍼가 가득 찹니다. 그런 경우 생산자는 버퍼에 빈 공간이 생길 때까지 대기해야 합니다.
2.반대로 소비가 너무 빠르다면 소비할 데이터가 없을 때까지 소비자가 데이터를 다 처리해버려서 이 또한 새로운 데이터가 들어올 때까지 대기해야 합니다.

생산자 소비자 문제 (생산과 소비에서 문제) = 한정된 버퍼 문제 (버퍼의 크기가 한정)

비유가 궁금하다면 PDF 보기
a. 레스토랑 주방과 손님
b. 음료 공장과 상점






🐹 생산자와 소비자 코드 결과

생산자와 소비자의 스레드는 각각 3개씩 해서 List에 담음!!

🕹️ 생산자가 먼저 실행된 결과

13:00:22.390 [     main] == [생산자 먼저 실행 시작],  BoundedQueueV1 ==

13:00:22.391 [     main] 생산자 시작
13:00:22.396 [producer1] [생산 시도] data1 -> []
13:00:22.396 [producer1] [생산 완료] data1 -> [data1]
13:00:22.495 [producer2] [생산 시도] data2 -> [data1]
13:00:22.495 [producer2] [생산 완료] data2 -> [data1, data2]
13:00:22.600 [producer3] [생산 시도] data3 -> [data1, data2]
13:00:22.600 [producer3] [put] 큐가 가득 차서 버림 : data3
13:00:22.600 [producer3] [생산 완료] data3 -> [data1, data2]

13:00:22.705 [     main] 현재 상태 출력, 큐 데이터 : [data1, data2]
13:00:22.707 [     main] 스레드 상태 : producer1 : TERMINATED : 5
13:00:22.708 [     main] 스레드 상태 : producer2 : TERMINATED : 5
13:00:22.708 [     main] 스레드 상태 : producer3 : TERMINATED : 5

13:00:22.708 [     main] 소비자 시작
13:00:22.708 [consumer1] [소비 시도]     ? <- [data1, data2]
13:00:22.708 [consumer1] [소비 완료] data1 <- [data2]
13:00:22.811 [consumer2] [소비 시도]     ? <- [data2]
13:00:22.812 [consumer2] [소비 완료] data2 <- []
13:00:22.914 [consumer3] [소비 시도]     ? <- []
13:00:22.915 [consumer3] [소비 완료] null <- []

13:00:23.019 [     main] 현재 상태 출력, 큐 데이터 : []
13:00:23.020 [     main] 스레드 상태 : producer1 : TERMINATED : 5
13:00:23.020 [     main] 스레드 상태 : producer2 : TERMINATED : 5
13:00:23.020 [     main] 스레드 상태 : producer3 : TERMINATED : 5
13:00:23.021 [     main] 스레드 상태 : consumer1 : TERMINATED : 5
13:00:23.021 [     main] 스레드 상태 : consumer2 : TERMINATED : 5
13:00:23.021 [     main] 스레드 상태 : consumer3 : TERMINATED : 5
13:00:23.022 [     main] == [생산자 먼저 실행 종료],  BoundedQueueV1 ==


🕹️ 소비자가 먼저 실행된 결과

13:01:05.117 [     main] == [소비자 먼저 실행 시작],  BoundedQueueV1 ==

13:01:05.119 [     main] 소비자 시작
13:01:05.120 [consumer1] [소비 시도]     ? <- []
13:01:05.123 [consumer1] [소비 완료] null <- []
13:01:05.221 [consumer2] [소비 시도]     ? <- []
13:01:05.221 [consumer2] [소비 완료] null <- []
13:01:05.325 [consumer3] [소비 시도]     ? <- []
13:01:05.326 [consumer3] [소비 완료] null <- []

13:01:05.430 [     main] 현재 상태 출력, 큐 데이터 : []
13:01:05.433 [     main] 스레드 상태 : consumer1 : TERMINATED : 5
13:01:05.433 [     main] 스레드 상태 : consumer2 : TERMINATED : 5
13:01:05.433 [     main] 스레드 상태 : consumer3 : TERMINATED : 5

13:01:05.433 [     main] 생산자 시작
13:01:05.434 [producer1] [생산 시도] data1 -> []
13:01:05.434 [producer1] [생산 완료] data1 -> [data1]
13:01:05.537 [producer2] [생산 시도] data2 -> [data1]
13:01:05.537 [producer2] [생산 완료] data2 -> [data1, data2]
13:01:05.642 [producer3] [생산 시도] data3 -> [data1, data2]
13:01:05.643 [producer3] [put] 큐가 가득 차서 버림 : data3
13:01:05.643 [producer3] [생산 완료] data3 -> [data1, data2]

13:01:05.745 [     main] 현재 상태 출력, 큐 데이터 : [data1, data2]
13:01:05.746 [     main] 스레드 상태 : consumer1 : TERMINATED : 5
13:01:05.746 [     main] 스레드 상태 : consumer2 : TERMINATED : 5
13:01:05.746 [     main] 스레드 상태 : consumer3 : TERMINATED : 5
13:01:05.746 [     main] 스레드 상태 : producer1 : TERMINATED : 5
13:01:05.746 [     main] 스레드 상태 : producer2 : TERMINATED : 5
13:01:05.747 [     main] 스레드 상태 : producer3 : TERMINATED : 5
13:01:05.747 [     main] == [소비자 먼저 실행 종료],  BoundedQueueV1 ==





🐹 생성자 우선의 경우 결과 분석


p1인 생성자가 먼저 실행되어 data1이 생산 시도를 하고 임계 영역이기 때문에 락을 먼저 얻어야 합니다. put 호출했을 락을 흭득하고 그 후에는 안전한 임계 영역 안에 들어가서 버퍼(Queue)의 데이터를 채울 수 있습니다. (이렇게 p2도 반복)

하지만 p3의 경우 Queue에 데이터가 가득 차서 데이터 추가가 불가능하기 때문에 버립니다.
여기서 우리는 어떻게 이 데이터를 버리지 않을 수 있을까요? 이 고민이 필요합니다.

Queue에 빈 공간이 생길 때까지 p3가 기다리면 됩니다. (언젠가는 소비자 스레드가 실행되어서 Queue의 데이터를 가져감.)
생산자가 반복문으로 Queue의 빈 공간이 생기는지 주기적으로 체크한 다음에 p3가 while문을 체크하면서 돌면 됩니다. Queue에 빈 공간이 없다면 sleep을 짧게 해서 잠깐 대기하고 깨어난 다음에 다시 반복문에서 Queue의 빈 공간을 체크하는 식으로 Queue가 피어서를 계속 확인하는 방법이 있습니다.
단순하게 스레드가 반복문에서 sleep이나 yield 같은 걸로 계속 Queue를 체크하면서 기다리면 됩니다.


🕹️ 소비자의 먼저의 경우

처음 소비 시도할 때 데이터가 없기 때문에 그냥 null로 받아버립니다. 좋지 않은 상황으로 c1, c2, c3 모두 데이터를 받지 못하고 종료됩니다.
그래서 언젠가 생산자가 데이터를 넣어준다고 가정하고 Queue에 데이터가 추가될 때까지 기다리면 됩니다.



🕹️ 해결 (하지만 실행 결과가 이상.)

put() 구현체에서 while 문으로 적용하여 생산자 스레드가 기다리도록 합니다. 언젠가는 소비자 스레드가 실행되어서 Queue의 데이터를 가져갈 것이고, 그러면 Queue에 데이터를 넣을 수 있는 공간이 생깁니다.
생산자 스레드가 반복문을 사용해서 Queue에 빈 공간이 생기는지 주기적으로 체크하고 만약 빈 공간이 없다면 sleep()을 사용해서 잠시 대기. 깨어나면 또 반복문에서 빈 공간 체크.

14:05:48.798 [     main] == [생산자 먼저 실행 시작],  BoundedQueueV2 ==

14:05:48.800 [     main] 생산자 시작
14:05:48.804 [producer1] [생산 시도] data1 -> []
14:05:48.804 [producer1] [생산 완료] data1 -> [data1]
14:05:48.904 [producer2] [생산 시도] data2 -> [data1]
14:05:48.904 [producer2] [생산 완료] data2 -> [data1, data2]
14:05:49.006 [producer3] [생산 시도] data3 -> [data1, data2]
14:05:49.006 [producer3] [put] 큐가 가득 차서 대기 : data3

14:05:49.111 [     main] 현재 상태 출력, 큐 데이터 : [data1, data2]
14:05:49.113 [     main] 스레드 상태 : producer1 : TERMINATED : 5
14:05:49.114 [     main] 스레드 상태 : producer2 : TERMINATED : 5
14:05:49.114 [     main] 스레드 상태 : producer3 : TIMED_WAITING : 5

14:05:49.114 [     main] 소비자 시작
14:05:49.114 [consumer1] [소비 시도]     ? <- [data1, data2]
14:05:49.218 [consumer2] [소비 시도]     ? <- [data1, data2]
14:05:49.321 [consumer3] [소비 시도]     ? <- [data1, data2]

14:05:49.427 [     main] 현재 상태 출력, 큐 데이터 : [data1, data2]
14:05:49.427 [     main] 스레드 상태 : producer1 : TERMINATED : 5
14:05:49.427 [     main] 스레드 상태 : producer2 : TERMINATED : 5
14:05:49.428 [     main] 스레드 상태 : producer3 : TIMED_WAITING : 5
14:05:49.429 [     main] 스레드 상태 : consumer1 : BLOCKED : 5
14:05:49.429 [     main] 스레드 상태 : consumer2 : BLOCKED : 5
14:05:49.430 [     main] 스레드 상태 : consumer3 : BLOCKED : 5
14:05:49.431 [     main] == [생산자 먼저 실행 종료],  BoundedQueueV2 ==
14:05:50.011 [producer3] [put] 큐가 가득 차서 대기 : data3
14:05:51.013 [producer3] [put] 큐가 가득 차서 대기 : data3
14:05:52.017 [producer3] [put] 큐가 가득 차서 대기 : data3

~~~~

생산자 먼저 실행한 경우 producer3가 종료되지 않고 계속 수행됩니다. 그리고 consumer1,2,3 은 BLOCKED가 됩니다.
소비자 먼저 실행의 경우에는 consumer1이 종료되지 않고 계속 수행돼서 consumer1만 돌아가고 멈춰버립니다. 기다려서 값이 오기를 기다리고 있는데 값이 안 오고 로그가 멈춰버립니다.






🐹 대기했지만 멈춰버린 이유

생산자 먼저일 경우 p1, p2, p3가 락을 흭득하면서 진행하는데 p3가 락을 가지고 있는 상태에서 Queue에 빈자리가 나올 때까지 대기하기 때문입니다. 결국 임계 영역 안에서 계속 머무는 것입니다. p3는 TIMED_WAITING 상태인데 c1이 소비 시도를 할 때 락을 먼저 얻어야 하는데 현재 락을 p3가 들고 슬립 상태로 들어갔기 때문에 c1이 락이 없어서 BLOCKED 상태가 되고 락을 얻게 될 때까지 기다립니다. (p3가 락을 반납하려면 c1이 먼저 Queue의 데이터를 소비해야 하므로 p3는 반납이 불가능한 상태)

이렇게 c2, c3도 락이 없으므로 BLOCKED 상태에 빠지는 것입니다.


소비자 먼저일 경우에도 비슷합니다. c1이 소비할 데이터가 없기 때문에 c2는 소비하지 못하고 BLOCKED 상태가 됩니다. (락을 c1이 소유 중)
이렇게 되면 나머지 p1, p2, p3도 모두 BLOCKED 상태가 됩니다.






🐹 완전 해결 (wait(), notify())

Object.wait()
현재 스레드가 가진 락을 반납하고 대기합니다. 현재 스레드를 대기 상태로 전환하는데 현재 스레드가 synchronized인 블록이나 메서드에서 락을 소유하고 있을 때만 호출이 가능합니다. (다른 스레드가 락을 흭득)
그래서 다른 스레드가 notify() or notifyAll()을 호출할 때까지 대기 상태를 유지.

Object.notify()
대기 중인 스레드를 깨웁니다. (하나만)

Object.notifyAll()
대기 중인 모든 스레드를 깨웁니다.



생산자 먼저 실행 결과

14:52:13.950 [     main] == [생산자 먼저 실행 시작],  BoundedQueueV3 ==

14:52:13.951 [     main] 생산자 시작
14:52:13.956 [producer1] [생산 시도] data1 -> []
14:52:13.956 [producer1] [put] 생산자 데이터 저장, notify() 호출
14:52:13.956 [producer1] [생산 완료] data1 -> [data1]
14:52:14.056 [producer2] [생산 시도] data2 -> [data1]
14:52:14.056 [producer2] [put] 생산자 데이터 저장, notify() 호출
14:52:14.056 [producer2] [생산 완료] data2 -> [data1, data2]
14:52:14.159 [producer3] [생산 시도] data3 -> [data1, data2]
14:52:14.159 [producer3] [put] 큐가 가득 차서 대기 : data3

14:52:14.264 [     main] 현재 상태 출력, 큐 데이터 : [data1, data2]
14:52:14.265 [     main] 스레드 상태 : producer1 : TERMINATED
14:52:14.265 [     main] 스레드 상태 : producer2 : TERMINATED
14:52:14.265 [     main] 스레드 상태 : producer3 : WAITING

14:52:14.265 [     main] 소비자 시작
14:52:14.265 [consumer1] [소비 시도]     ? <- [data1, data2]
14:52:14.266 [consumer1] [take] 소비자 데이터 흭득, notify() 호출
14:52:14.266 [producer3] [put] 생산자 깨어남
14:52:14.266 [consumer1] [소비 완료] data1 <- [data2]
14:52:14.266 [producer3] [put] 생산자 데이터 저장, notify() 호출
14:52:14.266 [producer3] [생산 완료] data3 -> [data2, data3]
14:52:14.371 [consumer2] [소비 시도]     ? <- [data2, data3]
14:52:14.371 [consumer2] [take] 소비자 데이터 흭득, notify() 호출
14:52:14.371 [consumer2] [소비 완료] data2 <- [data3]
14:52:14.476 [consumer3] [소비 시도]     ? <- [data3]
14:52:14.477 [consumer3] [take] 소비자 데이터 흭득, notify() 호출
14:52:14.478 [consumer3] [소비 완료] data3 <- []

14:52:14.581 [     main] 현재 상태 출력, 큐 데이터 : []
14:52:14.581 [     main] 스레드 상태 : producer1 : TERMINATED
14:52:14.581 [     main] 스레드 상태 : producer2 : TERMINATED
14:52:14.581 [     main] 스레드 상태 : producer3 : TERMINATED
14:52:14.581 [     main] 스레드 상태 : consumer1 : TERMINATED
14:52:14.581 [     main] 스레드 상태 : consumer2 : TERMINATED
14:52:14.581 [     main] 스레드 상태 : consumer3 : TERMINATED
14:52:14.582 [     main] == [생산자 먼저 실행 종료],  BoundedQueueV3 ==

notify() 할 경우 깨어나는 스레드
어떤 스레드가 깨어날지는 알 수 없습니다. (JVM 스펙에 명시되어 있지 않음. 그냥 임의의 하나를 깨움.)
스레드 대기 집합에 있는 스레드가 깨어난다고 바로 작동하는 것이 아닌 임계 영역안에 있음. 이 임계 영역에 있는 코드를 실행하려면 먼저 락이 필수.

약간 비효율적??
어떤 스레드가 먼저 먼저 실행되고 깨어나고 그런 것을 알 수가 없습니다. 그래서 최종 결과에는 p1, p2, p3 모두 데이터 정상 생산하고 c1, c2, c3의 경우 모두 데이터를 정상 소비하지만 c1이 같은 소비자인 c2, c3를 깨울 수가 있습니다. 이 경우 큐에 데이터가 없을 가능성이 있기 때문에 깨어난 소비자 스레드가 CPU 자원만 소모하고 다시 대기 집합에 들어가씩 때문에 비효율적일 수 있습니다.
하지만 결과에는 문제 없고 약간 돌아서 갈 뿐입니다.
이해가 가지 않는다면 그림보면서 다시 이해!!!!






🐹 wait(), notify() 한계

wait, notify 방식은 스레드 대기 집합 하나에 생산자, 소비자 스레드 모두 관리가 가능합니다. notify를 하게 되면 하나의 스레드를 깨우는데 어떤 것을 깨울지를 정할 수가 없다는 것입니다. 그래서 스레드가 깨어나서 실행해야 하는데 데이터가 없는 경우 다시 그냥 wait를 해버려서 약간의 비효율이 발생합니다. (깨어나도 다시 대기 상태로 돌아가는 것)

깨워야할 스레드를 계속 깨우지 못해서 대기 상태의 스레드가 실행 순서를 계속 얻지 못해ㅓㅅ 실행되지 않는 상황도 발생할 수 있는데 이러한 상황ㅇ르 스레드 기아 상태라고 합니다.


![](https://velog.velcdn.com/images/lusate/post/ae3f1884-9571-4604-995a-769691f0813d/image.png)

notifyAll()로 해서 모든 대기 상태에 있던 스레드가 다 깨어나서 임계 영역에 있는 락을 하나씩 흭득하고 흭득하지 못하면 BLOCKED 상태가 됩니다. 그러면 스레드 대기 집합에 빠져버립니다. 최악의 경우라도 남은 하나의 스레드가 락을 흭득할 수 있습니다.
순서 보장 X






🐹 비효율 완전 해결

같은 생산자나 소비자를 깨워서 문제였으니 생산자 스레드는 데이터를 생성하고 나서 데이터가 생성되면 소비자 스레드를 깨우고 소비자 스레드는 데이터를 소비하고 대기 중인 생산자에게 알려주면 해결이 됩니다.

즉 각각 대기하는 집합을 나누면 됩니다. (synchronized 에서는 불가능)

// wait, notify할 때 대기 집합에 들어가는 곳 (Rock Interface가 제공)
// ReentrantLock을 사용하는 스레드가 대기하는 스레드 대기 공간
private final Condition condition = lock.newCondition();

lock.newCondition() 메서드를 호출하면 스레드 대기 공간이 만들어집니다.

이 대기 집합 사용해서 wait(), notify() 대신 condition.await(), condition.signal() 사용!!!

Condition
Object.wait()/notify() 해서 사용한 스레드 대기 공간은 모든 객체 인스턴스가 내부에 기본으로 가지고 있는 것이었지만 Condition의 경우 스레드 대기 공간을 직접 만들어서 사용한 것입니다.



🕹️ 생산자, 소비자 대기 공간 분리

private final Condition producerCond = lock.newCondition(); // 생산자 대기 집합
private final Condition consumerCond = lock.newCondition(); // 소비자 대기 집합

put과 take 구현체에는 Queue가 가득 차면 생산자는 생산자용 대기 공간에 들어가고 데이터 잘 들어가면 소비자를 깨우면 됩니다. 반대로 Queue에 데이터가 없으면 소비자가 대기하고 소비한 후에는 생산자를 깨우면 됩니다.

실행 결과

16:39:20.727 [     main] == [생산자 먼저 실행 시작],  BoundedQueueV5 ==

16:39:20.728 [     main] 생산자 시작
16:39:20.732 [producer1] [생산 시도] data1 -> []
16:39:20.733 [producer1] [put] 생산자 데이터 저장, consumerCond.signal() 호출
16:39:20.733 [producer1] [생산 완료] data1 -> [data1]
16:39:20.835 [producer2] [생산 시도] data2 -> [data1]
16:39:20.836 [producer2] [put] 생산자 데이터 저장, consumerCond.signal() 호출
16:39:20.836 [producer2] [생산 완료] data2 -> [data1, data2]
16:39:20.937 [producer3] [생산 시도] data3 -> [data1, data2]
16:39:20.937 [producer3] [put] 큐가 가득 차서 대기 : data3

16:39:21.042 [     main] 현재 상태 출력, 큐 데이터 : [data1, data2]
16:39:21.042 [     main] 스레드 상태 : producer1 : TERMINATED
16:39:21.043 [     main] 스레드 상태 : producer2 : TERMINATED
16:39:21.043 [     main] 스레드 상태 : producer3 : WAITING

16:39:21.043 [     main] 소비자 시작
16:39:21.043 [consumer1] [소비 시도]     ? <- [data1, data2]
16:39:21.043 [consumer1] [take] 소비자 데이터 흭득, producerCond.signal() 호출
16:39:21.044 [producer3] [put] 생산자 깨어남
16:39:21.044 [consumer1] [소비 완료] data1 <- [data2]
16:39:21.044 [producer3] [put] 생산자 데이터 저장, consumerCond.signal() 호출
16:39:21.044 [producer3] [생산 완료] data3 -> [data2, data3]
16:39:21.146 [consumer2] [소비 시도]     ? <- [data2, data3]
16:39:21.146 [consumer2] [take] 소비자 데이터 흭득, producerCond.signal() 호출
16:39:21.146 [consumer2] [소비 완료] data2 <- [data3]
16:39:21.247 [consumer3] [소비 시도]     ? <- [data3]
16:39:21.247 [consumer3] [take] 소비자 데이터 흭득, producerCond.signal() 호출
16:39:21.248 [consumer3] [소비 완료] data3 <- []

16:39:21.353 [     main] 현재 상태 출력, 큐 데이터 : []
16:39:21.353 [     main] 스레드 상태 : producer1 : TERMINATED
16:39:21.354 [     main] 스레드 상태 : producer2 : TERMINATED
16:39:21.354 [     main] 스레드 상태 : producer3 : TERMINATED
16:39:21.354 [     main] 스레드 상태 : consumer1 : TERMINATED
16:39:21.355 [     main] 스레드 상태 : consumer2 : TERMINATED
16:39:21.355 [     main] 스레드 상태 : consumer3 : TERMINATED
16:39:21.355 [     main] == [생산자 먼저 실행 종료],  BoundedQueueV5 ==

이해 안 가면 그림 보기 -> 생산자와 소비자 문제2

Condition.signal()
대기 중인 스레드 중 하나를 깨우는 것은 Object.notify와 같지만 일반적으로는 FIFO 순서로 깨웁니다. 자바 버전과 구현에 따라 달라질 수 있지만 Queue 구조를 사용하기 때문에 FIFO 순이라 합니다.






🐹 스레드의 대기

위에서 생산자 소비자 코드 진행과 관련해서 한 가지 생각하지 않은 점이 있습니다.
락 대기 집합 이라는 것이 있습니다. 사실 이것을 "그냥 스레드가 락이 없으면 BLOCKED인 상태이구나" 라고 생각하고 넘어가도 되기는 하는데 스레드가 모니터 락을 기다리는 상태와 Object.wait()를 통해서 스레드 대기 집합에서 기다리는 요 2가지 대기 상태에 대한 혼돈을 위한 것입니다.

예를 들어 c1 스레드가 데이터가 없어서 스레드 대기 집합으로 들어가는데 이때 락을 반납하고 대기합니다. 그 다음 다른 스레드인 c2, c3가 락을 흭득할 때도 모두 WAITING 상태로 락을 반납하고 대기합니다. (소비자 스레드 모두 대기)

생산자가 실행하면 p1이 락을 흭득하고 데이터를 넣고 notify로 알리면 c1이 대기 집합에서 빠져나가는데 락 대기 집합까지 빠져나가야 임계 영역을 수행할 수 있습니다. 이때는 p1이 락을 가지고 있고 c1은 아직 락을 흭득하지 못한 상태이므로 BLOCKED 상태에서 다시 락 대기 집합에서 관리가 됩니다. (p1이 아직 락을 반납하지 않은 상태이기 때문에)

p1이 락을 반납하고 나서야 락 대기 집합에 있던 c1이 락을 흭득하고 임계 영역을 수행!!!

정리
자바의 모든 객체 인스턴스는 멀티스레드와 임계 영역을 다루기 위해 내부에 3가지 기본 요소를 가짐. -> 모니터 락, 프레드 대기 집합, 락 대기 집합

  • synchronized를 사용한 임계 영역에 들어가려면 모니터 락이 필수
  • 스레드 대기 집합 안에 있으면 WAITING 상태
  • 락이 없으면 락 대기 집합에 들어가서 BLOCKED 상태로 락 기다림 (락을 기다리는 스레드)
  • 집합 빠져나오면 BLOCKED 상태 -> 락을 아직 흭득 못했을 경우
  • 락 대기 집합에 있는 스레드 중 하나가 락을 흭득하면 BLOCKED -> RUNNABLE
  • 스레드가 notify를 호출하면 스레드 대기 집합에 있는 스레드 중 하나가 스레드 대기 집합을 빠져나오고 락 흭득을 시도


🕹️ synchronized 와 ReentrantLock 대기 비교

먼저 그림 비교

  1. BLOCKED 상태로 락 대기 / WAITING 상태로 락 흭득 대기
  2. synchronized를 시작할 때 락이 없으면 대기, ReentrantLock의 경우 lock.lock() 호출했을 때 락이 없으면 대기
  3. 다른 스레드가 notify()호출했을 때 스레드 대기 집합 빠져나감 / 다른 스레드가 condition.signal()을 호출했을 때 condition객체의 스레드 대기 공간에서 빠져나감





🐹 BlockingQueue

생산자와 소비자 각각의 스레드 대기 공간을 만들어서 효율적으로 해결했는데 내가 사용하는 다양한 프로젝트에 재사용하거나 다른 개발자들이 사용할 수 있게 코드 배포가 가능합니다.

BlockingQueue 는 스레드 관점에서 Queue가 특정 조건이 만족 될 때까지 스레드의 작업을 차단합니다.
Queue가 가득 차면 데이터 추가 작업을 시도하는 스레드는 공간이 생길 때까지 차단하고 Queue가 비어있으면 소비를 시도하는 스레드는 Queue에 데이터가 들어올 때까지 차단합니다.

자바는 생산자,소비자 문제 , 한정된 버퍼라 불리는 문젤르 해결하기 위해 java.util.concurrent.BlockingQueue 라는 인터페이스와 구현체들을 제공합니다.



🕹️ BlockingQueue 인터페이스의 대표적 구현체

  • ArrayBlockingQueue : 배열 기반으로 구현되어 있고, 버퍼의 크기가 고정되어 있다.
  • LinkedBlockingQueue : 링크 기반으로 구현되어 있고, 버퍼의 크기를 고정할 수도, 무한하게 사용할 수도 있다.

BlockingQueue를 보면 이전 예제에서 BoundedQueueV5에서 했던 것과 비슷하게 구현되어 있습니다.

  • ArrayBlockingQueue는 내부에서 ReentrantLock을 사용
  • 생산자 전용 대기실과 소비자 전용 대기실이 존재
  • 버퍼가 가득 차면 생산자 스레드는 생산자 전용 대기실에서 대기, 생산자 스레드가 생산을 완료하면 소비자 전용 대기실에 signal()로 신호를 전달.
  • 차이점으로는 lock() 대신에 lock.lockInterruptibly() 를 사용 (lock()은 인터럽트를 무시)
  • 내부 자료 구조의 차이

🐹 내가 직접 작성하는 것이 아닌 인터페이스 구현체로 구현되어 있으니 내가 어디 썼지 하지 말기



🕹️ BlockingQueue

상황 설명
생산자 소비자 구조로 되어 있을 때 고객이 주문을 하면 고객의 요청을 생산자 스레드가 받아서 중간에 있는 Queue에 넣어준다고 가정합니다. 그러면 소비자 스레드는 Queue에 있는 주문 요청을 꺼내서 주문을 처리합니다.

그런데 선착순 할인 이벤트처럼 주문 폭주로 생산자 스레드가 매우 바쁘게 주문을 Queue에 담는데 소비자 스레드는 이보다 적게 주문을 처리한다면 소비자는 생산을 따라가지 못하여 Queue가 가득 차버립니다. 그러면 수많은 생산자 스레드는 결국 BlockingQueue 앞에서 대기하게 됩니다. -> 아주 나쁜 무한 대기

그래서 데이터 추가를 포기하고 좀 기다리다가 안 되면 포기하고 고객에게 주문 폭주로 너무 많은 사용자가 몰려서 요청을 처리할 수 없다거나 나중에 다시 시도해 달라고 요청하는 것이 바람직합니다.

하지만 요청, 무한 대기, 일정 시간 대기, 인터럽트 와서 취소됨 등 수많은 경우가 있는데 이러한 문제를 해결하기 위해 BlockingQueue가 상황에 맞는 다양한 메서드를 제공합니다.


선택지 4가지

  • 예외 던지기
  • 대기하지 않고 즉시 false를 반환
  • 대기
  • 특정 시간 만큼만 대기

BlockingQueue메서드는 네 가지 형태로 제공되며, 즉시 충족될 수 없지만 나중에 어느 시점에서 충족될 수 있는 작업을 처리하는 다양한 방법이 있습니다. 하나는 예외를 throw하고, 두 번째는 특수 값( 작업에 따라 null또는 false)을 반환하고, 세 번째는 작업이 성공할 때까지 현재 스레드를 무기한 차단하고, 네 번째는 포기하기 전에 지정된 최대 시간 제한 동안만 차단합니다. 이러한 메서드는 다음 표에 요약되어 있습니다.
이처럼 "대기 시 예외", "대기 시 즉시 반환", "대기", "시간 대기" 가 있습니다.


⚙️ BlockingQueue 공식 문서


profile
일상의 인연에 감사하라. 기적은 의외로 가까운 곳에 있을지도 모른다.

0개의 댓글