본문 바로가기

멋쟁이사자처럼_부트캠프/Java

[멋쟁이사자처럼 부트캠프 TIL 회고] 백엔드 부트캠프 13기: Java 18일차 멀티 스레드

프로세스와 멀티스레드의 개념


1. 프로세스(Process)

  • 정의: 실행 중인 프로그램의 인스턴스. 운영체제에서 프로그램을 실행하면 해당 프로그램을 위한 독립적인 실행 공간과 리소스(메모리, CPU 등)가 생성되며 이를 프로세스라고 합니다.
  • 특징:
    • 독립된 메모리 공간: 각각의 프로세스는 독립적인 메모리 영역(Code, Data, Stack, Heap)을 가집니다.
    • 자원 소모: 프로세스 간의 데이터 교환은 상대적으로 비용이 높습니다. (Inter-Process Communication, IPC)
    • 운영체제의 스케줄링 대상: 프로세스는 운영체제의 스케줄링에 의해 CPU 시간을 할당받아 실행됩니다.

예시:

  • 크롬 웹 브라우저를 실행하면 여러 개의 탭이 각각 별도의 프로세스로 동작.
  • MS Word와 Spotify를 실행하면 각 프로그램이 별개의 프로세스로 실행.

2. 스레드(Thread)

  • 정의: 프로세스 내에서 실행되는 작업의 단위. 프로세스는 적어도 하나 이상의 스레드를 포함하며, 기본적으로 메인 스레드로 시작합니다.
  • 특징:
    • 공유 메모리 공간: 같은 프로세스 내의 모든 스레드는 메모리 공간(Code, Data, Heap)을 공유하지만, 각각의 스택은 독립적입니다.
    • 경량화: 프로세스보다 적은 리소스를 사용하며, 생성/종료가 빠릅니다.
    • 효율적인 데이터 교환: 스레드는 같은 메모리 공간을 공유하므로 데이터 교환이 빠릅니다.

예시:

  • 웹 브라우저에서:
    • 하나의 스레드는 사용자 입력 처리.
    • 다른 스레드는 페이지 로딩.
    • 또 다른 스레드는 비디오 재생.
  • 게임에서:
    • 한 스레드는 그래픽 렌더링.
    • 또 다른 스레드는 물리 연산.

3. 프로세스와 스레드의 차이점

특징 프로세스 스레드
메모리 공간 독립적 메모리 공간(Code, Data, Stack, Heap) 프로세스의 메모리 공간 공유(Code, Data, Heap), 독립된 Stack
생성 비용 생성 및 관리 비용이 높음 생성 및 관리 비용이 낮음
실행 단위 독립적인 실행 단위 프로세스 내에서 실행되는 작업 단위
통신 방식 IPC(메시지 큐, 파이프 등) 필요 메모리 공간을 공유하여 빠른 데이터 교환 가능
장점 안정성: 한 프로세스의 문제는 다른 프로세스에 영향을 미치지 않음 효율성: 자원을 공유하므로 더 빠른 실행 가능

 

4. 멀티스레드(Multithreading)

  • 정의: 하나의 프로세스 내에서 여러 스레드가 동시에 실행되는 방식.
  • 장점:
    • 병렬 처리: CPU의 코어를 활용하여 작업을 병렬적으로 처리.
    • 빠른 응답성: GUI 프로그램에서 사용자 입력과 백그라운드 작업을 병렬 처리.
    • 리소스 절약: 메모리를 공유하므로 프로세스 간 통신보다 효율적.
  • 단점:
    • 동기화 문제: 여러 스레드가 같은 자원에 접근할 때 충돌 발생 가능. 이를 방지하기 위해 synchronized 같은 동기화 메커니즘 필요.
    • 디버깅 복잡성: 멀티스레드 프로그램의 디버깅은 단일 스레드보다 어려움.

5. 예제: 자바에서의 멀티스레드 구현

(1) Thread 클래스 상속

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " is running");
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        t1.start(); // 스레드 실행
        t2.start();
    }
}

 

(2) Runnable 인터페이스 구현

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " is running");
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyRunnable());
        Thread t2 = new Thread(new MyRunnable());

        t1.start();
        t2.start();
    }
}

 

스레드 스케줄링(Thread Scheduling) : 스레드의 실행 순서가 달라지는 이유

스레드는 운영체제의 스케줄러가 관리하며, 스케줄러는 여러 요인에 따라 스레드 실행 순서를 결정함.

왜 실행 순서가 달라질까?

  1. 비결정적(Non-deterministic) 실행:
    • 스레드는 동시에 실행되거나 교차 실행될 수 있으며, 어떤 스레드가 먼저 실행될지는 운영체제가 결정합니다.
    • 이로 인해 실행 결과가 매번 다를 수 있습니다.
  2. 운영체제 스케줄링:
    • 운영체제는 우선순위(priority), 스레드 상태(예: 실행 가능 상태, 대기 상태), 가용 CPU 자원 등을 기준으로 어떤 스레드를 실행할지 결정합니다.
  3. Java 스레드 스케줄러:
    • Java 스레드 스케줄러는 기본적으로 운영체제 스케줄러에 의존합니다.
    • 자바의 Thread 클래스에는 setPriority() 메서드로 우선순위를 설정할 수 있지만, 이는 운영체제에 따라 무시될 수 있습니다.

중요한 점

  • 멀티스레딩은 비결정적이기 때문에 실행 순서를 예측할 수 없습니다.
  • 이를 해결하려면 스레드 동기화나 제어를 명시적으로 적용해야 합니다. 예를 들어, join() 메서드로 특정 스레드가 끝날 때까지 기다리게 하거나, 동기화 블록을 사용하는 방식이 있습니다.

 

6. 언제 멀티스레드 사용?

  • 병렬 처리: 대규모 데이터 처리, 파일 다운로드, 웹 크롤링.
  • 백그라운드 작업: GUI 프로그램에서의 백그라운드 연산.
  • 네트워크 I/O: 비동기 서버, 클라이언트 처리.

결론

  • 프로세스는 독립적인 실행 단위로 리소스를 많이 사용하지만 안정적.
  • 스레드는 프로세스 내의 작업 단위로 경량화되어 효율적이나 동기화 이슈를 관리해야 함.
  • 실제 구현에서는 두 가지를 조합하여 성능과 안정성을 모두 고려한 프로그램 설계가 중요합니다.

 


 

synchronized : 멀티스레드 환경에서 공유 자원의 일관성을 보장하기 위해 사용.

synchronized의 동작

  • 동기화 메서드:
    • 메서드에 synchronized를 붙이면, 같은 객체에 대해 하나의 스레드만 해당 메서드를 실행할 수 있습니다.
    • 다른 스레드가 동기화 메서드에 접근하려면, 현재 실행 중인 스레드가 해당 메서드 실행을 끝낼 때까지 기다려야 합니다.

1. synchronized를 붙인 경우 (코드 예시)

public synchronized void methodA() {
    for (int i = 0; i < 5; i++) {
        System.out.println("method A :: " + i);
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

 

(1) 동기화가 없는 경우

method A :: 0
method B :: 0
method C :: 0
method A :: 1
method C :: 1
method B :: 1
...

 

(2) 동기화가 있는 경우

method A :: 0
method A :: 1
method A :: 2
method A :: 3
method A :: 4
method B :: 0
method B :: 1
method B :: 2
method B :: 3
method B :: 4
method C :: 0
method C :: 1
method C :: 2
method C :: 3
method C :: 4

 

그렇다면 언제 synchronized를 사용하는가?

  • 공유 자원(Shared Resource)을 여러 스레드가 사용할 때, 데이터 일관성을 유지하고자 할 때 사용합니다.
  • 동기화를 통해 Critical Section을 보호합니다.
  • 하지만 동기화를 지나치게 사용하면, 스레드 간 경합이 발생하고 성능 저하가 생길 수 있으므로 필요한 경우에만 사용해야 합니다.

주의사항

  • synchronized는 객체 수준의 락을 사용하므로, 동일 객체에 대해 동기화된 모든 메서드가 영향을 받습니다.
  • 고성능 요구 사항에서는 ReentrantLock 같은 더 세밀한 제어가 가능한 동기화 도구를 고려할 수 있습니다.
  • 메서드 단위 외에도 블록 단위로도 동기화할 수 있습니다:
public void methodA() {
    synchronized (this) {
        // Critical Section
    }
}

 


 

실습문제 코드 기록.

Thread decrementThread = new Thread(new DecrementCounter());

 

생성자 안에 또 다른 객체를 생성해서 전달하는 것은 Thread 클래스가 Runnable 객체를 받는 생성자를 가지고 있기 때문.

(Runnable은 자바에서 기본으로 제공되는 인터페이스.)

 

Thread 클래스의 생성자

  • Thread 클래스는 Runnable 인터페이스를 구현한 객체를 인수로 받는 생성자를 제공합니다:
public Thread(Runnable target) { ... }
  • 이 생성자를 호출하면, 스레드는 전달받은 Runnable 객체의 run() 메서드를 실행합니다.

Runnable 인터페이스

  • Runnable 인터페이스는 단 하나의 메서드 run()을 가지고 있습니다:
public interface Runnable {
    void run();
}
  • 따라서, DecrementCounter 클래스가 Runnable 인터페이스를 구현하고 있다면, 이를 Thread 객체에 전달할 수 있습니다.

역할 분리

  • Runnable 객체와 Thread 객체를 분리하면, 작업과 스레드 실행 로직을 분리할 수 있습니다.
  • Runnable은 작업(로직)을 정의하고, Thread는 이를 실행하는 역할을 맡습니다.

장점: Runnable 객체를 사용하는 이유

  1. 코드 재사용
    • Runnable을 사용하면, 작업 로직(DecrementCounter)을 다른 곳에서도 재사용할 수 있습니다.
    • 작업 로직은 Runnable 객체에 캡슐화되고, 스레드는 그 작업을 실행하는 도구로 사용됩니다.
  2. 다중 상속 문제 해결
    • 자바는 다중 상속을 지원하지 않으므로, 클래스가 Thread를 상속하면 다른 클래스를 상속할 수 없습니다.
    • 반면, Runnable은 인터페이스이므로, 다른 클래스를 상속하면서 동시에 Runnable을 구현할 수 있습니다.
  3. 유연한 설계
    • 작업 로직과 실행 로직을 분리하여 더 유연하고 유지보수하기 쉬운 구조를 만듭니다.

동작 흐름

  • new DecrementCounter()는 Runnable 인터페이스를 구현한 객체를 생성합니다.
  • new Thread(Runnable target)은 해당 객체를 인수로 받아 새로운 스레드를 생성합니다.
  • Thread 객체의 start() 메서드를 호출하면, 내부적으로 전달받은 Runnable 객체의 run() 메서드를 실행합니다.

 

Runnable과 멀티스레드는 웹 프로그래밍에서 일부 상황에서 잘 쓰이기도 하지만, 일반적인 웹 프로그래밍에서는 직접적으로 사용되는 일이 드뭅니다. 대신, 웹 환경에 적합한 기술과 프레임워크가 멀티스레드를 효율적으로 관리합니다.