728x90

스레드 생성

  • Thread 클래스를 상속받아 run() 메서드를 오버라이딩하여 스레드를 생성
  • Runnable 인터페이스를 구현하여 스레드를 생성

 

Thread 클래스 상속

스레드 클래스 정의

public class HelloThread extends Thread {

    @Override
    public void run() {
        doSomething();
    }
}

 

스레드 생성 및 실행

  • start() 메서드를 호출하여 스레드를 실행
    • main 스레드와 별도의 스레드를 실행
  • run() 메서드를 직접 호출하면 main 스레드에서 실행
    • main 스레드는 HelloThread 인스턴스에 있는 run() 메서드를 호출하는 형태
public class HelloThreadMain {

    public static void main(String[] args) {
        HelloThread helloThread = new HelloThread();
        helloThread.start();
    }
}

 

 

데몬 스레드

스레드는 사용자(user) 스레드와 데몬(daemon) 스레드로 구분

차이는 JVM 종료 시점

  • 모든 user 스레드가 종료되면 JVM도 종료
  • 데몬 스레드는 user 스레드가 모두 종료되면 데몬 스레드도 종료 (why? JVM이 종료돼서)

setDaemon(true) 메서드를 호출하여 데몬 스레드로 설정

  • 데몬 스레드 여부는 start() 실행 전에 결정 (이후에 수정 불가)
public class DaemonThreadMain {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + ": main() start");
        DaemonThread daemonThread = new DaemonThread();
        daemonThread.setDaemon(true); // 데몬 스레드 여부
        daemonThread.start();
        System.out.println(Thread.currentThread().getName() + ": main() end");
    }

  static class DaemonThread extends Thread {

    @Override
    public void run() {
      System.out.println(Thread.currentThread().getName() + ": run()");
      try {
        Thread.sleep(10000); // 10초간 실행
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }

      System.out.println(Thread.currentThread().getName() + ": run() end");
    }
  }
}

 

 

run() 메서드 안에서 Thread.sleep() 를 호출할 때 checked exception 인 InterruptedException 을 밖으로 던질 수 없고 반드시 잡아야 함

  • why? run() 메서드는 부모 클래스 Thread의 인터페이스인 Runnable 인터페이스에서 throws를 선언하지 않고 있음
    • 자바 오버라이드 규칙에 따라 부모 메서드 또는 인터페이스가 체크 예외를 던지지 않는 경우, 재정의된 자식 메서드도 체크 예외를 던질 수 없음
  • InterruptedException은 checked exception 이기 때문에 try-catch 블록으로 예외를 잡아야 함

 

 

Runnable 인터페이스 구현

Runnable 인터페이스를 구현하는 방식으로 스레드를 생성

public interface Runnable {
     void run();
}
 public class HelloRunnable implements Runnable {
     @Override
     public void run() {
         System.out.println(Thread.currentThread().getName() + ": run()");
     }
}

 

Thread 상속 vs Runnable 구현

스레드 사용할 때는 Thread 를 상속 받는 방법보다 Runnable 인터페이스를 구현하는 방식을 사용하자

 

  • 자바는 다중 상속이 안 된다 -> Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없음
  • 코드의 분리가 안 된다 -> 굳이 Thread 클래스를 상속받아서 디펜던시 및 추가적인 메모리를 할당 할 필요가 없음
  • 여러 스레드가 동일한 Runnable 객체를 공유할 수 있어 자원 관리를 효율적으로 할 수 있다 -> Runnable 인스턴스를 여러 스레드에서 사용할 수 있음 (개별 스레드에서 run() 참조 값이 같음)
728x90

'Java > 멀티스레드' 카테고리의 다른 글

[JAVA] 스레드 제어와 생명주기  (0) 2024.09.18
728x90

프로세스와 스레드

  • 멀티태스킹과 멀티프로세싱
  • 프로세스와 스레드
  • 스레드와 스케줄링
  • 컨텍스트 스위칭

 

멀티태스킹과 멀티프로세싱

구분 관점 설명
멀티태스킹 소프트웨어
(운영체제)
하나의 컴퓨터 시스템이 동시에 여러 작업을 수행
(CPU 사용 시간을 분할하여 동시에 작업이 수행하는 것처럼 보임)
멀티프로세싱 하드웨어 둘 이상의 프로세서(여러 개의 CPU 코어)를 사용하여 여러 작업을 동시에 처리

 

프로세스와 스레드

구분 설명
프로세스 실행중인 프로그램을 프로세스라고 함
(자바로 비유하면 클래스는 프로그램, 인스턴스는 프로세스)
스레드 프로세스 내에서 실행되는 작업의 단위

 

 

프로세스의 메모리 구성

  • 코드 섹션: 실행할 프로그램의 코드가 저장되는 부분
  • 데이터 섹션: 전역 변수 및 정적 변수가 저장되는 부분(그림에서 기타에 포함)
  • 힙 (Heap): 동적으로 할당되는 메모리 영역
  • 스택 (Stack): 메서드(함수) 호출 시 생성되는 지역 변수와 반환 주소가 저장되는 영역(스레드에 포함)

그렇다면 JVM은 프로세스 구성 요소 중 어디로 올라가는가? → Heap 영역

 

 

스레드와 스케줄링

작업(스레드)이 실행되기 위해서는 CPU를 할당받아서 연산을 실행해야 함

  • 실제로 CPU에 의해 실행되는 단위는 스레드)
  • 프로세스는 스레드들의 컨테이너 역할을 수행 (스레드들의 실행 환경을 제공)

이를 위해 OS는 스케줄링을 통해 스레드를 어떻게, 얼마나 오랫동안 CPU를 할당할지 결정 (CPU 시간을 여러 작업에 나누어 배분)

 

 

컨텍스트 스위칭

멀티태스킹을 위해 CPU가 여러 작업(스레드)을 번갈아가며 수행

스레드를 번갈아가며 호출하기 위해 CPU는 스레드의 상태를 저장하고 복원하는 작업을 수행

이러한 과정을 컨텍스트 스위칭이라고 함

  • 멀티스레드는 대부분 효율적이지만, 컨텍스트 스위칭 과정이 필요하므로 항상 효율적인 것은 아님

 

 

최적의 CPU 수, 최적의 스레드 수

일반적으로 스레드가 하는 작업은 CPU 바운드 작업, I/O 바운드 작업으로 구분

  • CPU 바운드 작업: CPU를 많이 사용하는 작업
    • 복잡한 수학 연산, 데이터 분석, 비디오 인코딩
  • I/O 바운드 작업: I/O 작업(파일 읽기/쓰기, 네트워크 통신 등)이 많은 작업
    • 데이터베이스 쿼리 처리, 파일 읽기/쓰기, 네트워크 통신, 사용자 입력 처리

일반적으로 웹 애플리케이션 서버는 CPU 바운드 작업 보다는 I/O 바운드 작업이 많음

I/O 바운드 작업이 많다면 CPU를 거의 사용하지 않고 대기

  • 따라서 CPU 코어 수에 맞게 스레드를 생성하면 CPU를 효율적으로 사용할 수 없음
  • 이 때는 스레드 수를 증가시켜 CPU를 최대한 활용할 수 있도록 함

 

CPU-바운드 작업: CPU 코어 수 + 1개

  • CPU를 거의 100% 사용하는 작업이므로 스레드를 CPU 숫자에 최적화

I/O-바운드 작업: CPU 코어 수 보다 많은 스레드를 생성

  • CPU를 최대한 사용할 수 있는 숫자까지 스레드 생성 CPU를 많이 사용하지 않으므로 성능 테스트를 통해 CPU를 최대한 활용하는 숫자까지 스레드 생성
  • 단 너무 많은 스레드를 생성하면 컨텍스트 스위칭 비용도 함께 증가 - 적절한 성능 테스트 필요
728x90
728x90

스레드(Thread)

  • 프로그램 코드를 이동하면서 실행하는 하나의 제어

자바의 멀티태스킹

  • 멀티쓰레딩만 가능
    - 자바에 프로세스 개념은 존재하지 않고, 스레드 개념만 존재
    • 스레드는 실행 단위이자 스케쥴링 단위
  • 하나의 응용 프로그램은 여러 개의 스레드로 구성 가능

자바 스레드

  • JVM(Java Virtual Machine)에 의해 스케쥴되는 실행 단위의 코드 블럭
  • 스레드의 생명 주기는 JVM에 의해 관리
  • 하나의 JVM은 하나의 자바 응용 프로그램만 실행
    - 하나의 응용 프로그램은 하나 이상의 스레드로 구성 가능

스레드를 만드는 2가지 방법

  • java.lang.Thread 클래스를 이용하는 경우
  • java.lang.Runnable 인터페이스를 이용하는 경우

1. Thread 클래스를 이용한 스레드 생성

1) Thread 클래스 상속, 새 클래스 작성
2) run() 메소드 오버라이딩

class TestThread extends Thread {
    ...
    public void run() {
    	// run() 메소드 오버라이딩
        ...
    }
}

3) 스레드 객체 생성

TestThread thread = new TestThread();

4) 스레드 시작 - start() 메소드 호출

thread.start();
  • 주의 사항
    - run() 메소드가 종료되면 스레드는 종료됨
    - 한 번 종료된 스레드는 다시 시작할 수 없음 = 다시 스레드 객체를 생성하고 등록해야 함
    - 스레드에서 다른 스레드를 강제 종료 가능

2. Runnable 인터페이스로 스레드 생성

1) Runnable 인터페이스로 새 클래스 구현
2) 스레드 코드 작성

class TestRunnable implements Runnable {
	...
    public void run() {
    	// run() 메소드 구현
        ...
    }
}

3) 스레드 객체 생성

Thread thread = new Thread(new TestRunnable());

4) 스레드 시작

thread.start();

Thread 클래스 상속과 Runnable 인터페이스 구현의 차이

  • 자바는 다중 상속을 지원하지 않는다. 그렇기 때문에 Thread 클래스를 상속받는 경우, 다른 클래스를 상속받을 수 없다. 그렇기 때문에 Runnable 인터페이스를 구현하는 것이 일반적이다.

스레드 상태 6가지

  • NEW : 스레드가 생성되었지만 아직 실행할 준비가 되지 않음
  • RUNNABLE : 스레드가 JVM에 의해 실행되고 있거나 실행 준비되어 스케쥴링을 기다리는 상태
  • WAITING : 다른 스레드가 notify(), notifyAll()을 불러주기를 기다리고 있는 상태, 보통 스레드 동기화를 위해 사용
  • TIMED_WAITING : 스레드가 sleep(n)을 호출하여 n ms동안 잠을 자는 상태
  • BLOCK : 스레드가 I/O 작업 요청을 하면 JVM이 자동으로 BLOCK 상태로 만듦
  • TERMINATED : 스레드 종료

스레드 생명 주기

스레드 종료와 다른 스레드 강제 종료

  • 스스로 종료하는 경우 : run() 메소드 리턴
  • 다른 스레드에서 강제 종료하는 경우 : interrupt() 메소드 사용
class TestThread extends Thread {
	int n=0;
    public void run() {
    	while(true) {
            n++;
            try {	
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            	return;	// 예외 처리로 종료
            }
        }
    }
}
public class InterruptEx {
    public static void main(Stirng[] args) {
        TestThread thread = new TestThread();
        thread.start();
        thread.interrupt(); // 스레드 강제 종료
    }
}

flag를 이용한 종료

  • 스레드 안의 flag 변수를 이용하여 종료
class TestThread extends Thread {
	int n=0;
    bool flag = false; // false로 초기화
    public void finish() {
    	flag = true;
    }
    public void run() {
    	while(true) {
            n++;
            try {
                Thread.sleep(1000);
                if (flag == true)
                	return;
            } catch (InterruptException e) {
            	return;
            }
        }
    }
}
public FlagEx {
	public static void main(String[] args) {
    	TestThread thread = new TestThread();
        thread.start();        
        tread.finish();	// TestThread 강제 종료
    }
}

스레드 동기화 (Thread Synchronization)

  • 멀티스레드 프로그램 작성 시 주의점
    - 다수의 스레드가 공유 데이터에 동시에 접근하는 경우
    - 공유 데이터의 값에 예상치 못한 결과 발생 가능
  • 이를 해결하기 위해 스레드 동기화 사용
  • 공유 데이터를 접근하는 모든 스레드를 관리
  • 한 스레드가 공유 데이터에 접근하는 경우 작업이 끝날 때까지 다른 스레드들은 대기

synchronized

  • 한 스레드가 독점적으로 실행해야 하는 부분을 표시하는 키워드
  • synchronized 메소드
synchronized void add() {
	int n = getCurrentSum();
    n+=10;
    setCurrentSum(n);
}
  • synchronized 코드 블럭
void execute() {
	...
    synchronized(this) {
    	int n = getCurrentSum();
        n+=10;
        setCurrentSum(n);
    }
    ...
}

wait(), notify(), notifyAll()

  • 동기화 객체 : 2개 이상의 스레드 사이에 동기화 작업에 사용되는 객체
  • 동기화 메소드
    • synchronized 블록 내에서만 사용되어야 함
    • wait()
      • 다른 스레드가 notify()를 불러줄 때까지 대기
    • notify()
      • wait()을 호출하여 대기 중인 스레드를 깨우고 RUNNABLE 상태로 만듦
      • 2개 이상의 스레드가 대기 중이어도 한 개의 스레드만 깨움
    • notifyAll()
      • wait()를 호출하여 대기 중인 모든 스레드를 깨우고 모두 RUNNABLE 상태로 만듦

이미지 출처 : 명품 JAVA 프로그래밍 (황기태, 김효수 저)

728x90

+ Recent posts