728x90
멀티 스레드
@2024.04.18
프로세스와 멀티 스레드
프로세스
💡
프로세스는 실행 중인 프로그램의 인스턴스를 뜻하며,
운영체제는 각 프로세스에 메모리와 시스템 리소스를 할당한다.
운영체제는 각 프로세스에 메모리와 시스템 리소스를 할당한다.
- 독립적인 메모리 공간(코드, 데이터, 힙, 스택 등)을 가지며, 다른 프로세스와는 격리되어있다.
- 프로세스 간의 안정성 보장
- 리소스 관리 측면에서는 비효율적이며, 프로세스 간 통신이 복잡해진다.
멀티 스레드
💡
하나의 프로세스 내에서 여러 개의 스레드를 동시에 실행하는 기법
- 스레드 : 프로세스 내에서 실제로 작업을 수행하는 최소 단위
- 프로세스의 리소스를 공유한다.
- 각 스레드는 독립적인 실행 흐름을 가지며, 자신만의 스택을 가지나, 힙 메모리와 코드 영역은 공유
- 장점
- 성능 향상
- 여러 작업을 동시에 처리할 수 있기에 응용 프로그램 처리 성능이 향상
- ex) 채팅 프로그램 → 클라이언트의 입력을 받으면서, 서버로부터 메시지를 받는다.
- 자원 효율성
- 프로세스보다 스레드를 생성하고 관리하는 비용이 낮다.
- 응답성 향상
- 사용자 인터페이스와 같이 빠른 응답이 필요한 애플리케이션에서는 멀티 스레드 사용 시 사용자 경험을 개선할 수 있다.
- 성능 향상
- 단점
- 동시성 문제
- 여러 스레드가 동일한 자원에 접근할 때 발생 → 동기화가 필요
- 복잡성
- 스레드 간의 상호작용과 동시성 관리는 프로그램의 복잡성을 증가시킨다.
- 디버깅의 어려움
- 디버깅이 어렵고, 잠재적인 버그의 원인을 찾기가 더 어렵다.
- 동시성 문제
프로세스와 멀티 스레드의 차이점
- 메모리 공유
- 프로세스 : 완전히 독립적인 메모리 공간
- 멀티 스레드 : 여러 스레드가 같은 메모리 공간(heap)을 공유
- 통신
- 프로세스 : 프로세스 간 통신을 하려면 IPC라는 방법을 사용하므로 어렵다.
- 멀티 스레드 : 같은 프로세스의 메모리를 공유하기에 데이터 공유가 쉽다
- 오버헤드
- 어떤 작업을 수행하기 위해 추가로 필요한 리소스/시간
- 멀티 스레드 vs 프로세스에서의 "오버헤드"는 추가적으로 발생하는 비용을 의미
- 프로세스 : 더 많은 시스템 리소스를 필요로 한다.
- 멀티 스레드 : 프로세스보다 생성과 관리 측면에서 오버헤드가 적다.
- 어떤 작업을 수행하기 위해 추가로 필요한 리소스/시간
- 안정성
- 프로세스 : 서로 독립적이라 안정성이 높다
- 멀티 스레드 : 한 스레드의 오류가 전체 프로세스에 영향을 줄 수 있다.
멀티 스레드 생성과 실행
Thread 클래스 상속 받기
class MYThread extends Thread{
@Override
public void run(){
// 스레드가 실행 할 작업
}
}
public static void main(String[] args){
MYThread t = new MyThread();
t.start(); // 스레드 실행
- 장점
- 구현이 간단하고 직관적
- Thread 클래스의 다른 메서드들을 직접 사용 가능
- 단점
- 자바는 다중 상속이 지원하지 않으므로, Thread를 상속받으면 다른 클래스를 상속받을 수 없다
Runnable Interface 구현하기
class MyRunnable implments Runnable{
@Override
public void run(){
// 스레드가 실행 할 작업
}
}
public static void main(String[] args){
Thread thread = new Thread(new MyRunnable());
}
- 장점
- 클래스가 다른 클래스를 상속받을 수 있으므로, 유연성이 높아진다.
- 단점
- Runnable 인터페이스에는 Thread 클래스의 다른 유용한 메서드들이 포함되어 있지 않다.
- 명시적으로 Thread 객체를 생성해야 한다.
멀티 스레드 제어
- 멀티 스레드 제어의 필요성
- 데이터 일관성과 안정성 유지
- 여러 스레드가 동일한 데이터를 접근/변경 → 데이터 일관성과 안정성 손상
- 동기화를 사용하여 관리해야 한다.
- 데드락 방지
- 여러 스레드가 서로 소유한 리소스의 해제를 기다리며 무한 대기하는 것
- 스레드 간 리소스 할당과 해제를 적절히 관리해야 한다.
- 자원 공유 최적화
- 여러 스레드가 시스템 자원을 공유하기에, 스레드 수가 많아지면 자원 경쟁이 심화
- 자원을 효율적으로 공유하고 관리하는 전략 필요
- 스레드 생명 주기 관리
- 사용하지 않는 스레드는 자원 낭비한다.
- 필요에 따라 스레드를 중지하거나, 재활용
- 성능 최적화 및 부화 관리
- 멀티 스레드는 시스템의 멀티 코어 프로세서를 효율적으로 활용한다.
- 스레드를 너무 많이 생성 시, 컨텍스트 스위칭으로 인해 오버헤드가 증가
- 적절한 스레드 수와 스레드 풀 관리는 필수다.
- 데이터 일관성과 안정성 유지
동기화(Synchronization)
💡
멀티 스레드를 제어하기 위해 자바가 제공하는 기능 중 하나로,
한 시점에 하나의 스레드 만이 해당 코드 영역에 접근할 수 있도록 한다.
한 시점에 하나의 스레드 만이 해당 코드 영역에 접근할 수 있도록 한다.
- 공유 자원에 대한 동시 접근을 방지하고, 데이터의 일관성과 안정성을 보장
- synchoronized 키워드 사용
- 예시
private Map<String,PrintWriter> chatClients; // 만약 여러 스레드에서 put 메소드를 사용한다면? // 동기화 작업이 필요하다! synchronized (chatClients){ chatClients.put(this.id,out); }
스레드 생명 주기

💡
프로그래밍에서 스레드가 생성되고 실행되며 종료될 때 거치는 여러 상태들의 시퀀스로
생성(Creation), 실행(Runnable), 대기(Blocked/Waiting/Timed Waiting), 종료(Terminated)
단계로 구성되어 있다.
생성(Creation), 실행(Runnable), 대기(Blocked/Waiting/Timed Waiting), 종료(Terminated)
단계로 구성되어 있다.
- 사용하지 않는 스레드는 자원을 낭비하므로, 필요에 따라 스레드를 중지하거나 재활용하는 등의 관리를 해주어야 한다!
- 단계
- 생성(Creation)
- 스레드가 생성되는 단계
- 스레드 객체가 생성되고 초기화한다. ← 실행 X
- 실행(Runnable)
- 생성된 스레드가 실행 가능한 상태에 들어가는 단계
- 스레드 스케줄러에 의해 선택되어 CPU를 할당받을 수 있는 상태
- 대기(Blocked/Waiting/Timed Waiting)
- 스레드가 실행되는 필요조건이 충족되지 않아 일시 중단되는 상태
- ex) 다른 스레드의 자원을 기다리거나, 입출력 연산을 기다리는 동안 스레드는 대기 상태
- 종료(Terminated)
- 스레드가 실행을 완료하거나 중단되었을 때 상태
- 종료 시 해당 스레드의 수명이 끝나게 된다.
- 생성(Creation)
- 메서드
- start()
- 스레드를 실행시키기 위해 호출되는 메서드
- 메서드가 호출되면 스레드가 생성되고 run()가 실행된다.
- run()
- 스레드의 주된 작업을 정의하는 메서드
- start()가 호출되면 이 메서드가 실행된다.
- sleep(long millisseconds)
- 현재 스레드를 일정 시간 동안 정지시키는 메서드
- 일정 시간이 지나면 스레드는 다시 실행된다.
- yield()
- 다른 스레드에게 실행을 양보하고 현재 스레드를 실행 대기 상태로 만드는 메서드
- join()
- 다른 스레드가 종료될 때까지 현재 스레드를 대기시키는 메서드
- ex) threadA.join() → threadA가 종료될 때까지 현재 스레드의 실행을 일시 정지
class TaskThread extends Thread { private String taskName; public TaskThread(String taskName) { this.taskName = taskName; } @Override public void run() { System.out.println(taskName + " 작업 시작"); try { Thread.sleep(2000); // 2초 동안 스레드 일시 정지 (작업 시뮬레이션) } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(taskName + " 작업 완료"); } } public class ThreadJoinExample { public static void main(String[] args) { TaskThread task1 = new TaskThread("작업 1"); TaskThread task2 = new TaskThread("작업 2"); DemonThread demonThread = new DemonThread(); task1.start(); task2.start(); try { System.out.println("모든 작업의 완료를 기다립니다."); task1.join(); // task1의 완료를 기다림 task2.join(); // task2의 완료를 기다림 } catch (Exception e) { e.printStackTrace(); } System.out.println("모든 작업이 완료되었습니다."); } }
- SetPriority(), getPriority()
- 스레드의 우선순위를 설정하거나 가져오는 메서드
- interrupt()
- 스레드를 중단시키는 요청을 하며, 스레드에 인터럽트 플래그를 설정하는 메서드
- InterruptedExcption 예외를 발생시킨다.
isInterrupted()
,interttupted()
→ 스레드가 인터럽트 되었는지를 여부를 확인
public class ThreadInterruptExample { static class MyThread extends Thread { public void run() { try { for (int i = 0; i < 5; i++) { Thread.sleep(1000); // 1초 동안 일시 정지 System.out.println("Processing step " + (i + 1)); } } catch (InterruptedException e) { System.out.println("스레드가 중단되었습니다."); return; // 스레드를 안전하게 종료 } } } public static void main(String[] args) { MyThread t = new MyThread(); t.start(); // 스레드 시작 try { Thread.sleep(2500); // 메인 스레드를 2.5초 동안 일시 정지 } catch (InterruptedException e) { e.printStackTrace(); } t.interrupt(); // 스레드에 인터럽트 신호 보내기 } }
- isAlive()
- 스레드가 아직 실행 중 인지를 여부를 확인하는 메서드
- wait()
- 스레드를 대기 상태로 만드는 메서드
- notify()
- 대기 중인 스레드를 깨우는 메서드
public class WaitNotifyExample { private static final Object lock = new Object(); private static boolean itemAvailable = false; static class Producer extends Thread { public void run() { synchronized (lock) { System.out.println("생산자가 아이템을 생산 중입니다."); itemAvailable = true; lock.notify(); // 생산이 끝났으므로 소비자에게 알림 System.out.println("생산자가 알림을 보냈습니다."); } } } static class Consumer extends Thread { public void run() { synchronized (lock) { // lock의 소유권을 가진다. while (!itemAvailable) { try { System.out.println("소비자가 아이템을 기다리고 있습니다."); lock.wait(); // wait하면 lock의 소유권을 포기한다. } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("소비자가 아이템을 소비했습니다."); itemAvailable = false; // 아이템 소비 후 상태 업데이트 } } } public static void main(String[] args) { Producer producer = new Producer(); Consumer consumer = new Consumer(); consumer.start(); // 소비자 스레드 시작 try { Thread.sleep(1000); // 생산자 시작 전에 잠시 대기 } catch (InterruptedException e) { e.printStackTrace(); } producer.start(); // 생산자 스레드 시작 } }
- notifyAll()
- 대기 중인 모든 스레드를 깨우는 메서드
public class WaitNotifyAllExample2 { private static final Object lock = new Object(); private static int itemsAvailable = 0; // 사용 가능한 아이템 수 static class Producer extends Thread { public void run() { synchronized (lock) { itemsAvailable += 5; // 5개의 아이템을 생산 System.out.println("생산자가 " + itemsAvailable + "개의 아이템을 생산하였습니다."); lock.notifyAll(); // 모든 대기 중인 소비자 스레드에 알림 System.out.println("생산자가 모든 소비자에게 알림을 보냈습니다."); } } } static class Consumer extends Thread { private int id; Consumer(int id) { this.id = id; } public void run() { synchronized (lock) { while (itemsAvailable <= 0) { try { System.out.println("소비자 " + id + "가 아이템을 기다리고 있습니다."); lock.wait(); // 아이템을 기다림 } catch (InterruptedException e) { e.printStackTrace(); } } itemsAvailable--; // 아이템 소비 System.out.println("소비자 " + id + "가 아이템을 소비했습니다. 남은 아이템: " + itemsAvailable); } } } public static void main(String[] args) { Producer producer = new Producer(); Consumer consumer1 = new Consumer(1); Consumer consumer2 = new Consumer(2); Consumer consumer3 = new Consumer(3); consumer1.start(); // 소비자 1 스레드 시작 consumer2.start(); // 소비자 2 스레드 시작 consumer3.start(); // 소비자 3 스레드 시작 try { Thread.sleep(1000); // 생산자 시작 전에 잠시 대기 } catch (InterruptedException e) { e.printStackTrace(); } producer.start(); // 생산자 스레드 시작 } }
- start()
Demon Thread
💡
백 그라운드에서 특정 작업을 수행하는 스레드로,
일반적으로 다른 스레드들이 종속되지 않은
보조적인 작업을 한다
(가비지 컬렉션, 자동 저장, 데몬 서비스의 백그라운드 작업)
일반적으로 다른 스레드들이 종속되지 않은
보조적인 작업을 한다
(가비지 컬렉션, 자동 저장, 데몬 서비스의 백그라운드 작업)
- 자바 프로그램이 실행되는 동안 백그라운드에서 계속 실행되며, 데몬 스레드가 남아 있는 한
자바 프로그램은 종료되지 않는다.
⇒ 단!
모든 일반 스레드가 종료되면 데몬 스레드는 강제 종료
- 주 스레드나 다른 일반 스레드들에 의해서 생성되며, 데몬 스레드는 해당 스레드들이 종료될 때 함께 종료
- 예시
- 무한 루프로 인해 모든 스레드가 종료되어도 demonThread는 계속 출력
- demonThread.setDaemon(true)으로 데몬 스레드를 생성하면, task1과 task2가 종료되면
demonThread도 종료된다.// 스레드 생성 class DemonThread extends Thread { @Override public void run() { while (true) { System.out.println("배경음악 재생중!!!"); try { sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } public class ThreadJoinExample { static class TaskThread extends Thread { private String taskName; public TaskThread(String taskName) { this.taskName = taskName; } public void run() { System.out.println(taskName + " 작업 시작"); try { Thread.sleep(2000); // 2초 동안 스레드 일시 정지 (작업 시뮬레이션) } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(taskName + " 작업 완료"); } } public static void main(String[] args) { TaskThread task1 = new TaskThread("작업 1"); TaskThread task2 = new TaskThread("작업 2"); DemonThread demonThread = new DemonThread(); task1.start(); task2.start(); demonThread.setDaemon(true); demonThread.start(); try { System.out.println("모든 작업의 완료를 기다립니다."); task1.join(); // task1의 완료를 기다림 task2.join(); // task2의 완료를 기다림 } catch (Exception e) { e.printStackTrace(); } System.out.println("모든 작업이 완료되었습니다."); } }
728x90