You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
volatile : 여러 스레드가 동시에 접근하는 변수에는 volatile 키워드를 붙어주어야 안전하다. 여기서는 main 스레드, printer 스레드 둘다 work 변수에 동시에 접근할 수 있다. volatile 에 대한 자세한 내용은 뒤에서 설명한다.
ConcurrentLinkedQueue : 여러 스레드가 동시에 접근하는 경우, 컬렉션 프레임워크가 제공하는 일반적인 자료구조를 사용하면 안전하지 않다. 여러 스레드가 동시에 접근하는 경우 동시성을 지원하는 동시성 컬렉션을 사용해야 한다. Queue 의 경우 ConcurrentLinkedQueue 를 사용하면 된다. 동시성 컬렉션의 자세한 내용은 뒤에서 설명한다. 여기서는 일반 큐라고 생각하면 된다.
참고 : volatile , ConcurrentLinkedQueue 에 대한 자세한 내용은 뒤에서 다룬다. 지금은 여러 스레드에서 접근하는 경우에 이런 것들을 사용하는구나 정도만 알아두면 된다.
실행 결과
16:35:02.771 [ main] 프린터할 문서를 입력하세요. 종료 (q): a
16:35:03.960 [ printer] 출력 시작: a, 대기 문서: []
16:35:03.954 [ main] 프린터할 문서를 입력하세요. 종료 (q): b
16:35:04.587 [ main] 프린터할 문서를 입력하세요. 종료 (q): c
16:35:04.936 [ main] 프린터할 문서를 입력하세요. 종료 (q): d
16:35:05.172 [ main] 프린터할 문서를 입력하세요. 종료 (q):
16:35:06.965 [ printer] 출력 완료: a
16:35:06.966 [ printer] 출력 시작: b, 대기 문서: [c, d]
16:35:09.971 [ printer] 출력 완료: b
16:35:09.972 [ printer] 출력 시작: c, 대기 문서: [d]
16:35:12.974 [ printer] 출력 완료: c
16:35:12.975 [ printer] 출력 시작: d, 대기 문서: []
16:35:15.977 [ printer] 출력 완료: d
q
16:35:18.118 [ printer] 프린터 종료
실행 결과는 보기 쉽게 다듬었다.
입력으로 a , b , c , d 를 사용했다.
main 스레드: 사용자의 입력을 받아서 Printer 인스턴스의 jobQueue 에 담는다.
printer 스레드: jobQueue 가 있는지 확인한다.
jobQueue 에 내용이 있으면 poll() 을 이용해서 꺼낸 다음에 출력한다.
출력하는데는 약 3초의 시간이 걸린다. 여기서는 sleep(3000) 를 사용해서 출력 시간을 가상으로
구현했다.
출력을 완료하면 while문을 다시 반복한다.
만약 jobQueue 가 비었다면 continue 를 사용해서 다시 while문을 반복한다.
이렇게 해서 jobQueue 에 출력할 내용이 들어올 때 까지 계속 확인한다.
main 스레드: 사용자가 q 를 입력한다. printer.work 의 값을 false 로 변경한다.
앞서 살펴보았듯이 이 방식의 문제는 종료( q )를 입력했을 때 바로 반응하지 않는다는 점이다. 왜냐하면 printer 스레드가 반복문을 빠져나오려면 while문을 체크해야 하는데, printer 스레드가 sleep(3000) 을 통해 대기 상태에 빠져서 작동하지 않기 때문이다. 따라서 최악의 경우 q 를 입력하고 3초 이후에 프린터가 종료된다.
프린터할 문서를 입력하세요. 종료 (q): a
17:13:16.994 [ printer] 출력 시작: a, 대기 문서: []
프린터할 문서를 입력하세요. 종료 (q): b
프린터할 문서를 입력하세요. 종료 (q): c
프린터할 문서를 입력하세요. 종료 (q): d
프린터할 문서를 입력하세요. 종료 (q):
17:13:19.999 [ printer] 출력 완료: a
17:13:20.001 [ printer] 출력 시작: b, 대기 문서: [c, d]
q
17:13:20.920 [ printer] 인터럽트!
17:13:20.921 [ printer] 프린터 종료
종료( q )를 입력하면 즉시 종료되는 것을 확인할 수 있다. 따라서 반응성이 매우 좋아진다.
종료시 main 스레드는 work 변수도 false 로 변경하고, printer 스레드에 인터럽트도 함께 호출한다.
-sleep(1) 을 사용해서 스레드의 상태를 1밀리초 동안 아주 잠깐 RUNNABLE TIMED_WAITING 으로 변경
한다. 이렇게 되면 스레드는 CPU 자원을 사용하지 않고, 실행 스케줄링에서 잠시 제외된다.
1 밀리초의 대기 시간 이후 다시 TIMED_WAITING RUNNABLE 상태가 되면서 실행 스케줄링에 포함된다.
결과적으로 TIMED_WAITING 상태가 되면서 다른 스레드에 실행을 양보하게 된다. 그리고 스캐줄링 큐에 대기중인 다른 스레드가 CPU의 실행 기회를 빨리 얻을 수 있다.
하지만 이 방식은 RUNNABLE TIMED_WAITING RUNNABLE 로 변경되는 복잡한 과정을 거치고, 또 특정 시간만큼 스레드가 실행되지 않는 단점이 있다.
예를 들어서 양보할 스레드가 없다면, 차라리 나의 스레드를 더 실행하는 것이 나은 선택일 수 있다. 이 방법은 나머지 스레드가 모두 대기 상태로 쉬고 있어도 내 스레드까지 잠깐 실행되지 않는 것이다. 쉽게 이야기해서 양보할 사람이 없는데 혼자서 양보한 이상한 상황이 될 수 있다.
자바의 스레드가 RUNNABLE 상태일 때, 운영체제의 스케줄링은 다음과 같은 상태들을 가질 수 있다.
실행 상태(Running): 스레드가 CPU에서 실제로 실행 중이다.
실행 대기 상태(Ready): 스레드가 실행될 준비가 되었지만, CPU가 바빠서 스케줄링 큐에서 대기 중이다.
운영체제는 실행 상태의 스레드들을 잠깐만 실행하고 실행 대기 상태로 만든다. 그리고 실행 대기 상태의 스레드들을 잠깐만 실행 상태로 변경해서 실행한다. 이 과정을 계속 반복한다. 참고로 자바에서는 두 상태를 구분할 수는 없다.
yield()의 작동
Thread.yield() 메서드는 현재 실행 중인 스레드가 자발적으로 CPU를 양보하여 다른 스레드가 실행될 수
있도록 한다.
yield() 메서드를 호출한 스레드는 RUNNABLE 상태를 유지하면서 CPU를 양보한다. 즉, 이 스레드는 다시 스케줄링 큐에 들어가면서 다른 스레드에게 CPU 사용 기회를 넘긴다.
자바에서 Thread.yield() 메서드를 호출하면 현재 실행 중인 스레드가 CPU를 양보하도록 힌트를 준다. 이는 스레드가 자신에게 할당된 실행 시간을 포기하고 다른 스레드에게 실행 기회를 주도록 한다. 참고로 yield() 는 운영체제의 스케줄러에게 단지 힌트를 제공할 뿐, 강제적인 실행 순서를 지정하지 않는다. 그리고 반드시 다른 스레드가 실행되는 것도 아니다.
yield() 는 RUNNABLE 상태를 유지하기 때문에, 쉽게 이야기해서 양보할 사람이 없다면 본인 스레드가 계속 실행될 수 있다.
참고로 최근에는 10코어 이상의 CPU도 많기 때문에 스레드 10개 정도만 만들어서 실행하면, 양보가 크게 의미가 없다. 양보해도 CPU 코어가 남기 때문에 양보하지 않고 계속 수행될 수 있다. CPU 코어 수 이상의 스레드를 만들어야 양 보하는 상황을 확인할 수 있다. 그래서 이번 예제에서 1000개의 스레드를 실행한 것이다.
참고
log() 가 사용하는 기능은 현재 시간도 획득해야 하고, 날짜 포멧도 지정해야 하는 등 복잡하다. 이 사이에
스레드의 컨텍스트 스위칭이 발생하기 쉽다. 이런 이유로 스레드의 실행 순서를 일정하게 출력하기 어렵다. 그래서 여기서는 단순한 System.out.println() 을 사용했다.
프린터 예제4 - yield 도입
앞서 개발한 프린터 예제를 보면 yield() 를 적용하기 딱 좋은 곳이 있다.
while (!Thread.interrupted()) {
if (jobQueue.isEmpty()) {
continue;
}
...
}
이 코드를 보면 인터럽트가 발생하기 전까지 계속 인터럽트의 상태를 체크하고 또 jobQueue 의 상태를 확인한다. 문제는 쉴 틈 없이 CPU에서 이 로직이 계속 반복해서 수행된다는 점이다. 1초에 while문을 수억 번 반복할 수도 있다! 결과적으로 CPU 자원을 많이 사용하게 된다.
현재 작동하는 스레드가 아주 많다고 가정해보자. 인터럽트도 걸리지 않고, jobQueue 도 비어있는데, 이런 체크 로직에 CPU 자원을 많이 사용하게 되면, 정작 필요한 스레드들의 효율이 상대적으로 떨어질 수 있다.
차라리 그 시간에 다른 스레드들을 더 많이 실행해서 jobQueue 에 필요한 작업을 빠르게 만들어 넣어주는게 더 효율적일 것이다. 그래서 다음과 같이 jobQueue 에 작업이 비어있으면 yield() 를 호출해서, 다른 스레드에 작업을 양보하는게 전체 관점에서 보면 더 효율적이다.
while (!Thread.interrupted()) {
if (jobQueue.isEmpty()) {
Thread.yield(); // 추가
continue;
}
...
}
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
프린터 예제1 - 시작
참고 : volatile , ConcurrentLinkedQueue 에 대한 자세한 내용은 뒤에서 다룬다. 지금은 여러 스레드에서 접근하는 경우에 이런 것들을 사용하는구나 정도만 알아두면 된다.
실행 결과
실행 결과는 보기 쉽게 다듬었다.
구현했다.
앞서 살펴보았듯이 이 방식의 문제는 종료( q )를 입력했을 때 바로 반응하지 않는다는 점이다. 왜냐하면 printer 스레드가 반복문을 빠져나오려면 while문을 체크해야 하는데, printer 스레드가 sleep(3000) 을 통해 대기 상태에 빠져서 작동하지 않기 때문이다. 따라서 최악의 경우 q 를 입력하고 3초 이후에 프린터가 종료된다.
프린터 예제2 - 인터럽트 도입
실행 결과
종료시 main 스레드는 work 변수도 false 로 변경하고, printer 스레드에 인터럽트도 함께 호출한다.
프린터 예제3 - 인터럽트 코드 개선
printer 스레드는 자신이 인터럽트 상태인지 다음 코드로 확인하면 된다.
main 스레드도 work 변수의 사용을 제거하고, 인터럽트만 걸어주면 된다.
실행 결과
실행 결과는 기존과 같다.
yield - 양보하기
여기서는 3가지방식을 사용한다.
Empty
실행 결과 - 1. Empty
sleep()
-sleep(1) 을 사용해서 스레드의 상태를 1밀리초 동안 아주 잠깐 RUNNABLE TIMED_WAITING 으로 변경
한다. 이렇게 되면 스레드는 CPU 자원을 사용하지 않고, 실행 스케줄링에서 잠시 제외된다.
yield()
실행 결과3 - yield
자바의 스레드가 RUNNABLE 상태일 때, 운영체제의 스케줄링은 다음과 같은 상태들을 가질 수 있다.
운영체제는 실행 상태의 스레드들을 잠깐만 실행하고 실행 대기 상태로 만든다. 그리고 실행 대기 상태의 스레드들을 잠깐만 실행 상태로 변경해서 실행한다. 이 과정을 계속 반복한다. 참고로 자바에서는 두 상태를 구분할 수는 없다.
yield()의 작동
있도록 한다.
자바에서 Thread.yield() 메서드를 호출하면 현재 실행 중인 스레드가 CPU를 양보하도록 힌트를 준다. 이는 스레드가 자신에게 할당된 실행 시간을 포기하고 다른 스레드에게 실행 기회를 주도록 한다. 참고로 yield() 는 운영체제의 스케줄러에게 단지 힌트를 제공할 뿐, 강제적인 실행 순서를 지정하지 않는다. 그리고 반드시 다른 스레드가 실행되는 것도 아니다.
yield() 는 RUNNABLE 상태를 유지하기 때문에, 쉽게 이야기해서 양보할 사람이 없다면 본인 스레드가 계속 실행될 수 있다.
참고로 최근에는 10코어 이상의 CPU도 많기 때문에 스레드 10개 정도만 만들어서 실행하면, 양보가 크게 의미가 없다. 양보해도 CPU 코어가 남기 때문에 양보하지 않고 계속 수행될 수 있다. CPU 코어 수 이상의 스레드를 만들어야 양 보하는 상황을 확인할 수 있다. 그래서 이번 예제에서 1000개의 스레드를 실행한 것이다.
참고
스레드의 컨텍스트 스위칭이 발생하기 쉽다. 이런 이유로 스레드의 실행 순서를 일정하게 출력하기 어렵다. 그래서 여기서는 단순한 System.out.println() 을 사용했다.
프린터 예제4 - yield 도입
All reactions