CAS, CountDownLatch 동작 원리 : 유저, 커널 모드 연산 구분하기 본문
배경
지난 글에서 논 블록킹을 구현하는 두 가지 방법을 확인했다.
Netty는 커널의 I/O 멀티플렉싱 연산을 사용하고, DelayQueue는 커널 단의 스레드 제어 연산을 사용했다.
이번에는 CAS와 CountDownLatch가 궁금했다.
사실 CAS와 CountDownLatch도 앞선 두 경우와 비슷할 것 같다는 생각에 파본 것인데 아니었다.
CAS는 전혀 다른 방법으로 동작했고, CountDownLatch는 짬뽕이었다.
나는 컴퓨터공학을 전공했다. 부끄럽지만 컴퓨터구조와 운영체제 수업을 가장 재밌게 수강했다.
앞선 멀티 플렉싱이라는 키워드도, 앞으로 얘기할 커널 모드와 시스템 콜도, 다 공부한 개념인데 참 낯설다.
오랜만에 복습할 겸, 운영체제와 자바 개발을 스르륵 녹일 수 있는 글이 되었으면 좋겠다는 생각에 정리한다.
스레드 상태와 스케줄러
자바에서 Thread.sleep()을 하면 스레드가 일정 시간 동안 멈춘다.
해당 스레드를 반복문처럼 계속 연산시키며 시간을 확인하는 것이 아니라, 커널 단에서 스레드 연산을 멈춘다.
스레드를 멈춘다는 얘기는 흐르는 물을 막는 것과는 다르다.
그보다는 그 스레드를 CPU 연산에서 일정 시간 동안 제외해 둔다.
커널은 스레드 정보와 상태를 담은 블록, TCB를 관리하고,
스케줄러는 그 블록들을 확인해 스레드 상태를 체크하면서, 연산의 대기열로의 추가, 삭제를 결정한다.
즉, Thread.sleep은 해당 스레드의 상태를 변경하여, 일정 시간 동안 연산 대기열에서 빠지게 하는 것이다.
더 자세히는 해당 스레드의 TCB에서 상태를 TIMED_WAITING으로 값을 변경한다.
커널 모드와 유저 모드
운영체제 첫 개념이 아직도 생각난다.
운영체제가 하는 주요 업무 네 가지, 파일 관리, 메모리 관리, 프로세스 관리, 입출력 관리.
이런 주요 업무들은 일반 애플리케이션 코드에서 수행할 수 없다.
이렇게 보안과 시스템 안정성을 위해, 수행할 수 있는 명령어 모드를 구분한 것이 커널 모드와 유저 모드이다.
커널 모드는 완전한 권한을 얻어 모든 연산이 가능하고,
일반 애플리케이션은 유저 모드로서, 제한된 명령어만 수행할 수 있다.
즉, JVM은 감히 TCB를 직접 건들 수 없다.
시스템 콜
그럼 어떻게 Thread.sleep은 스레드의 상태를 변경할 수 있을까.
직접 처리하지 못하고 커널에 부탁한다. 그 요청이 시스템 콜이다.
애플리케이션은 시스템 콜로 간접적으로 커널이 관리하는 자원을 다룬다.
시스템 콜을 하면, CPU는 권한이 바뀌어 커널 모드로 진입하고, 작업을 마치고 다시 유저 모드로 돌아온다.
현재 모드를 나타내는 레지스터의 비트를 변경하는 것이다.
운영체제는 그렇게 수행되는 연산의 권한 검증하거나 리소스를 격리한다.
대표적으로 MMU는 프로세스별로 사용할 수 있는 영역을 나누고 관리하는 하드웨어다.
커널 모드에 접근해서 메모리를 다루더라도, 프로세스에 할당 밖 영역이나 커널의 메모리 영역의 침범을 MMU가 보호한다.
자바 예시
이제 슬슬 두 모드로 처리할 수 있는 연산이 헷갈려진다.
익숙한 자바를 기준으로 유저 모드와 커널 모드의 연산을 분리해 본다.
반복문, 조건문은 유저 모드이다.
Thread.sleep(), System.out.println(), File.write()은 커널 모드이다.
자바에서 변수의 값을 바꾸는 것은 커널 모드일까?
이미 할당된 내 메모리를 다루는 것은 유저 모드로 처리 가능하다.
new LinkedList()로 객체 메모리를 만드는 것은 커널 모드일까?
이미 JVM에서 할당된 힙 영역을 단순 사용하는 것은 유저 모드로 가능하다.
만약 힙 영역이 부족해서, 메모리를 추가로 할당받는다면 이는 커널 모드가 필요하다.
CAS 의 동작 원리
하드웨어 명령어와 커널 단의 명령어는 구분해야 한다.
JNI, Netive method로 하드웨어 명령어를 사용한다고 모두 커널 모드는 아니다.
돌고 돌아, CompareAndSet은 단순 유저 모드를 사용한다.
자바가 컴파일되는 바이트 코드에는 "조회"와 "값 변경"을 한 번에 할 수 있는 명령어가 없다.
그래서 "조회 후 맞으면 값 변경"은 CPU의 명령어로 처리해야 한다.
그렇기에 JNI, Native method로 직접 하드웨어 명령어를 수행하는 것뿐이지,
그렇게 수행하는 명령어는 커널 모드가 필요 없는 유저 모드의 명령어이다.
CountDownLatch 의 동작 원리
CountDownLatch는 짬뽕이다.
그 동작 원리를 들으면 이제는 감이 올 것이다.
1. await(), 스레드를 대기 모드로 전환한다.
2. countDown(), 원자적으로 메모리의 값을 1 줄인다.
3. count가 0이 되면, 대기 중이던 스레드를 깨운다.
1번과 3번은 TCB의 스레드 상태를 변경해야 하기에 커널 모드 진입이 필요하다.
2번은 CPU(하드웨어) 원자성 명령어를 사용한 것이고, 그 대상이 이미 할당된 메모리이니, 유저 모드로 가능하다.
정리
이슈 : CAS, CountDownLatch의 동작 원리가 궁금하다.
OS :
- 스레드 대기를 위해선 스레드 상태 변경이 필요하고, 이는 커널의 역할이다.
- 스케줄러는 TCB의 스레드 상태를 읽고 CPU 연산 대상 여부를 결정한다.
- 일반 애플리케이션이 커널의 자원과 명령어를 수행하고자 하면 시스템 콜이 필요하다.
- 운영체제는 커널 모드와 유저 모드를 분리하여, 리소스를 격리하고 안정성을 지킨다.
Java :
- 파일, 메모리, 네트워크, 입출력과 관련된 연산은 커널 모드가 필요하다.
- 대신 할당된 메모리에서 객체를 생성하거나, 값을 변경하는 것은 유저 모드로 가능하다.
- JNI, Native method를 사용한 하드웨어 명령어 수행과 커널 단의 명령어 수행을 분리해야 한다.
결과 :
- CAS는 하드웨어의 원자적 명령어 수행이다.
- 자바 바이트 코드에는 원자적 연산이 없어, JNI/Native method를 사용해야 하는 것이지만, 커널 모드가 필요하진 않다.
- CountDownLatch는 둘 다 사용한다.
- Count를 위한 원자적 값 변경은 사용자 모드, 스레드를 대기하고 다시 활성화시키는 것은 커널 모드의 연산이다.
'Language > Java, Kotlin' 카테고리의 다른 글
| Mono.delay 동작 원리 : 어떻게 딜레이 시간을 체크할까 (0) | 2025.11.14 |
|---|---|
| 두 가지 GC와 처리 영역들 (2) | 2023.12.04 |
| JitPack 으로 자바 라이브러리 배포하기 (2) | 2022.01.24 |
| Local Maven Repository 에 라이브러리 배포하기 (0) | 2022.01.22 |
| Optional 로 Null 을 알리는 습관 (0) | 2021.03.15 |
