프로세스와 멀티스레드의 개념
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) : 스레드의 실행 순서가 달라지는 이유
스레드는 운영체제의 스케줄러가 관리하며, 스케줄러는 여러 요인에 따라 스레드 실행 순서를 결정함.
왜 실행 순서가 달라질까?
- 비결정적(Non-deterministic) 실행:
- 스레드는 동시에 실행되거나 교차 실행될 수 있으며, 어떤 스레드가 먼저 실행될지는 운영체제가 결정합니다.
- 이로 인해 실행 결과가 매번 다를 수 있습니다.
- 운영체제 스케줄링:
- 운영체제는 우선순위(priority), 스레드 상태(예: 실행 가능 상태, 대기 상태), 가용 CPU 자원 등을 기준으로 어떤 스레드를 실행할지 결정합니다.
- 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 객체를 사용하는 이유
- 코드 재사용
- Runnable을 사용하면, 작업 로직(DecrementCounter)을 다른 곳에서도 재사용할 수 있습니다.
- 작업 로직은 Runnable 객체에 캡슐화되고, 스레드는 그 작업을 실행하는 도구로 사용됩니다.
- 다중 상속 문제 해결
- 자바는 다중 상속을 지원하지 않으므로, 클래스가 Thread를 상속하면 다른 클래스를 상속할 수 없습니다.
- 반면, Runnable은 인터페이스이므로, 다른 클래스를 상속하면서 동시에 Runnable을 구현할 수 있습니다.
- 유연한 설계
- 작업 로직과 실행 로직을 분리하여 더 유연하고 유지보수하기 쉬운 구조를 만듭니다.
동작 흐름
- new DecrementCounter()는 Runnable 인터페이스를 구현한 객체를 생성합니다.
- new Thread(Runnable target)은 해당 객체를 인수로 받아 새로운 스레드를 생성합니다.
- Thread 객체의 start() 메서드를 호출하면, 내부적으로 전달받은 Runnable 객체의 run() 메서드를 실행합니다.
Runnable과 멀티스레드는 웹 프로그래밍에서 일부 상황에서 잘 쓰이기도 하지만, 일반적인 웹 프로그래밍에서는 직접적으로 사용되는 일이 드뭅니다. 대신, 웹 환경에 적합한 기술과 프레임워크가 멀티스레드를 효율적으로 관리합니다.
'멋쟁이사자처럼_부트캠프 > Java' 카테고리의 다른 글
[멋쟁이사자처럼 부트캠프 TIL 회고] 백엔드 부트캠프 13기: Java 20일차 POJO, 디자인 패턴 (2) | 2024.12.30 |
---|---|
[멋쟁이사자처럼 부트캠프 TIL 회고] 백엔드 부트캠프 13기: Java 19일차 OOP, SOLID 원칙 (1) | 2024.12.27 |
[멋쟁이사자처럼 부트캠프 TIL 회고] 백엔드 부트캠프 13기: Java 17일차 Java IO, 버퍼 (1) | 2024.12.24 |
[멋쟁이사자처럼 부트캠프 TIL 회고] 백엔드 부트캠프 13기: Java 16일차 컬렉션 프레임워크, Iterator (0) | 2024.12.23 |
[멋쟁이사자처럼 부트캠프 TIL 회고] 백엔드 부트캠프 13기: Java 10일차 내부 클래스, 익명 개체, 자주 사용되는 클래스, 제네릭 (2) | 2024.12.13 |