ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java] Blocking/Non-blocking이란?
    Java 2025. 12. 1. 12:17

    1. Blocking이란?

    Blocking은 프로세스(또는 쓰레드)의 상태 변화제어권의 흐름을 기준으로 정의됩니다.

    호출된 함수가 외부 작업 (쓰레드가 하는 작업이 아닌 db, 네트워크, I/O 등 외부 장치가 하는 작업)을 완료할 때까지 제어권(쓰레드를 사용하여 코드를 실행할 권한)을 반환하지 않는 것을 의미합니다. 이로 인해 호출한 쓰레드는 실행(Running) 상태에서 대기(Waiting) 상태로 전환되며, CPU 점유를 해제(Context Switch)하고 멈춰 서게 됩니다.

    핵심 동작 메커니즘

    Blocking I/O가 발생할 때 운영체제 내부에서는 다음 3단계 프로세스가 일어납니다.

    1. System Call:
      • 사용자 애플리케이션(Java)이 커널(OS)에게 "파일 읽어줘" 또는 "네트워크 패킷 줘"와 같은 요청합니다.
      • 이때 User Mode에서 Kernel Mode로 전환됩니다.
    2. Suspend & Context Switch :
      • 커널은 응답 데이터가 아직 준비되지 않았음을 확인합니다.
      • 해당 쓰레드의 상태를 RunningWaiting(Blocked) 으로 변경합니다.
      • Context Switching을 수행하여 다른 쓰레드(Runnable 상태인 쓰레드)에게 CPU를 넘깁니다.
    3. Resume (재개):
      • 장치 컨트롤러가 "데이터 준비됨(Interrupt)" 신호를 보냅니다.
      • 대기하던 쓰레드는 다시 Runnable(Ready) 상태가 되고, 스케줄러에 의해 다시 CPU를 할당받을 때까지 기다립니다.

    Java 관점에서 보는 Blocking

    Java 개발자가 가장 흔하게 접하는 InputStream을 예시입니다.

    상황: 클라이언트로부터 데이터를 읽는 상황

    // 1. 입력 스트림 생성
    InputStream in = socket.getInputStream();
    
    // 2. 데이터를 읽기 위해 read() 호출 -> 여기서 Blocking 발생!
    // (데이터가 들어올 때까지 이 라인에서 실행이 멈춤)
    int data = in.read(); 
    
    // 3. 데이터가 도착해야만 이 라인이 실행됨
    System.out.println("데이터 수신 완료: " + data);
    

    단계별 상세 분석

    Step A: in.read() 호출 직전

    • Java 쓰레드 상태: RUNNABLE
    • CPU를 점유하며 명령어를 실행 중입니다.

    Step B: in.read() 호출 (Blocking 시작)

    • 동작: JVM은 OS의 네이티브 함수(read system call)를 호출합니다.
    • 상태 변화: 아직 네트워크로 들어온 패킷이 없다면, OS는 이 쓰레드를 대기시킵니다.
    • Java 쓰레드 상태: RUNNABLE → WAITING (또는 BLOCKED )
    • OS 레벨:
      • 현재 쓰레드의 정보(레지스터, PC 등)를 메모리(PCB/TCB)에 저장합니다.
      • Context Switching 발생: CPU 제어권을 다른 쓰레드로 넘겨버립니다.
      • 이 쓰레드는 CPU를 전혀 사용하지 않지만, Stack 메모리는 계속 점유하고 있습니다.

    Step C: 대기 중 (Waiting)

    • 상황: 랜선(NIC)을 타고 데이터가 들어오기를 기다립니다.
    • 문제점: 만약 데이터가 10초 동안 안 오면? 10초 동안 쓰레드와 메모리가 묶여 있습니다. (이것이 Tomcat 쓰레드 고갈의 원인)

    Step D: 데이터 도착 (Blocking 종료)

    • 동작: 하드웨어가 CPU에 인터럽트(신호)를 보내서 데이터가 도착했음을 알립니다.
    • 상태 변화: OS는 대기하던 쓰레드를 깨웁니다.
    • Java 쓰레드 상태: WAITING → RUNNABLE
    • 결과: 다시 CPU를 할당받는 순간(Context Switching), in.read() 함수가 리턴되며 다음 줄(System.out.println)로 넘어갑니다.

    요약 정리

    Blocking을 다음과 같이 정의하고 이해하시면 가장 정확합니다.

    1. 제어권(Control): 호출된 함수가 작업을 마칠 때까지 제어권을 독점한다. (호출자는 리턴받지 못함)
    2. 쓰레드 상태(Thread State): 호출한 쓰레드는 Waiting 상태가 되어 멈춘다.
    3. 리소스(Resource):
      • CPU: 사용하지 않는다. (Context Switching 발생)
      • Memory: 해당 쓰레드의 스택(Stack) 메모리는 계속 점유한다.

    2. Non-blocking이란?

    Non-blocking은 "호출된 함수가 작업 완료 여부와 상관없이 제어권을 즉시 호출자에게 반환하는 방식"입니다.

    호출한 쓰레드는 대기(Waiting) 상태로 빠지지 않고, 실행(Running) 상태를 유지하며 다른 작업을 계속 수행할 수 있습니다.

    핵심 동작 메커니즘 (Polling 방식)

    운영체제 내부에서는 Blocking과 전혀 다른 흐름이 전개됩니다.

    1. System Call:
      • 사용자 애플리케이션(Java)이 커널에게 "데이터 읽어줘"(read)를 요청합니다.
      • 단, 해당 소켓(파일)은 Non-blocking 모드로 설정되어 있어야 합니다.
    2. Immediate Return (즉시 반환):
      • 데이터가 없는 경우: 커널은 기다리지 않고 즉시데이터 없음이라는 에러 코드나 상태 값을 반환합니다.
      • Context Switching 미발생: 쓰레드는 멈추지 않았으므로 Running 상태를 유지합니다.
    3. Polling (반복 확인) & Other Work:
      • 호출자(Client)는 아직 데이터가 없음을 인지하고, 다른 계산 작업을 하거나 다시 커널에게 데이터가 왔는지 확인합니다.
      • 데이터가 준비될 때까지 이 확인 과정을 반복하는 것을 Polling(폴링)이라고 합니다.

    Java 관점에서 보는 Non-blocking

    기존 InputStream 대신, Java NIO(New I/O)의 SocketChannel을 사용해야 구현 가능합니다.

    상황: 클라이언트로부터 데이터를 읽는 상황

    // 1. 소켓 채널 생성 및 설정
    SocketChannel socketChannel = SocketChannel.open();
    socketChannel.configureBlocking(false); // 핵심! Non-blocking 모드로 전환
    
    // 2. 데이터 읽기 시도
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    int bytesRead = socketChannel.read(buffer); // 여기서 멈추지 않고 바로 리턴됨!
    
    // 3. 결과 확인 및 처리
    if (bytesRead == -1) {
        // 연결 끊김 처리
    } else if (bytesRead == 0) {
        // "데이터 아직 없음". 쓰레드는 멈추지 않았으므로 여기서 다른 작업 가능
        doOtherWork(); 
    } else {
        // 데이터 읽기 성공
        System.out.println("데이터 수신: " + bytesRead);
    }
    

    단계별 상세 분석

    Step A: read() 호출

    • Java 쓰레드 상태: RUNNABLE
    • 커널에게 읽기 요청을 보냅니다.

    Step B: 커널의 동작 (Kernel Mode)

    • 커널은 수신 버퍼를 확인합니다.
    • Case 1 (데이터 없음): 쓰레드를 대기(Waiting) 상태로 보내는 대신, 즉시 0 또는 EWOULDBLOCK 신호를 리턴합니다.
    • Case 2 (데이터 있음): 데이터를 복사해주고 읽은 바이트 수를 리턴합니다.

    Step C: 결과 처리 (User Mode)

    • Blocking과의 차이: 쓰레드는 제어권을 바로 돌려받았기 때문에, 다음 라인(if (bytesRead == 0))이 즉시 실행됩니다.
    • Java 쓰레드 상태: 계속 RUNNABLE (Context Switching 없음)

    치명적인 단점: Busy Waiting (바쁜 대기)

    여기서 한 가지 의문이 생기셔야 합니다.

    데이터가 올 때까지 계속 while문으로 물어보면(Polling), CPU가 쉴 새 없이 일하니까 오히려 낭비 아닌가?

    맞습니다. 이것이 순수 Non-blocking(Polling) 방식의 문제입니다.

    while (true) {
        int n = socketChannel.read(buffer);
        if (n > 0) process(buffer);
        // 데이터가 안 와도 계속 루프를 돌며 CPU를 100% 사용함 (Busy Waiting)
    }
    
    • Blocking: 쓰레드가 멈춰서(Waiting) 문제.
    • Non-blocking (단순 폴링): 쓰레드가 너무 바빠서(Busy Waiting) 문제.

    그래서 현대의 Non-blocking I/O (Spring WebFlux, Netty)는 단순히 계속 물어보는 방식(polling 방식)을 쓰지 않고, 이벤트가 발생하면 알려주는 방식(I/O Multiplexing, Selector)과 결합하여 사용합니다.

    요약 정리 

    Non-blocking을 다음과 같이 정리하시면 됩니다.

    1. 제어권(Control): 호출된 함수는 즉시 리턴하며 제어권을 호출자에게 돌려준다.
    2. 쓰레드 상태(Thread State): 데이터가 없어도 쓰레드는 Waiting으로 가지 않고 Runnable 상태를 유지한다.
    3. 리소스(Resource):
      • Context Switching: 발생하지 않는다.
      • CPU: 단순 폴링(Polling)으로 구현하면, 의미 없는 확인 작업으로 CPU 사용률이 치솟는 Busy Waiting 문제가 발생할 수 있다.
Designed by MSJ.