본문 바로가기
Language/Java

[Java] 쓰레드 1 - 구현 및 실행(start, run)

by 계범 2022. 3. 13.

프로세스와 쓰레드

프로세스란 '실행중인 프로그램'을 뜻한다.

프로그램을 실행하면 OS(운영체제)로부터 실행에 필요한 자원(메모리)를 할당받아 프로세스가 된다.

 

프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있고,

프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 쓰레드이다.

 

모든 프로세스는 하나 이상의 쓰레드가 존재하며,

둘 이상의 쓰레드를 가진 프로세스를 '멀티쓰레드 프로세스(muti-threaded process)'라고 한다.

 

멀티태스킹과 멀티쓰레딩

대부분의 OS는 멀티태스킹(다중작업)을 지원하기 때문에 여러 개의 프로세스가 동시에 실행될 수 있다.

이와 유사하게 멀티쓰레딩은 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것이다.

 

CPU의 core는 한 번에 하나의 작업만 수행할 수 있으므로, 동시에 처리할 수 있는 작업의 개수는 core의 개수와 일치한다. 실제로 실행하는 쓰레드의 숫자는 core의 개수보다 훨씬 많기때문에 동시에 수행하는 것처럼 아주 짧은 시간을 번갈아가면서 수행한다.

 

멀티쓰레딩의 장단점

장점

  1. CPU 사용률 향상
  2. 효율적인 자원 사용
  3. 사용자에 대한 응답성 향상
  4. 작업이 분리되어 코드가 간결

메신저로 채팅하면서 파일을 다운로드 받거나 음성대화를 나눌 수 있는 것이 멀티쓰레드로 되어있기 때문이다.

만약 싱글스레드였다면, 한가지를 할때 다른 것을 못했을 것이다.

 

단점

  1. 동기화와 교착상태 문제

교착상태란 두 쓰레드가 서로에게 필요한 자원을 점유한 상태에서 서로를 기다리느라 멈춰있는 상태를 말한다.

 

2022.01.05 - [CS/OS(운영체제)] - 동기화 툴(프로세스 동기화)

2022.01.05 - [CS/OS(운영체제)] - 동시성 제어 예제(Bounded-Buffer, Readers-Writers

2022.01.05 - [CS/OS(운영체제)] - 데드락(Deadlock)

 

쓰레드의 구현과 실행

쓰레드의 구현

쓰레드를 구현하는 방법은 Thread클래스를 상속받는 방법Runnable인터페이스를 구현하는 방법이 있다.

 

클래스는 상속받으면 다른 클래스를 상속받을 수 없기 때문에, 인터페이스인 Runnable로 구현하는 방법이 일반적이다.

또한 재사용성(reusability)이 높고 코드의 일관성(consistency)을 유지할 수 있기 때문에 보다 객체지향적인 방법이다.

 

// Thread Class
class MyThread extends Thread{
	public void run() { /* 작업내용 */} // Thread클래스의 run()을 오버라이딩
}
//Runnable Interface
class MyThread implements Runnable{
	public void run() { /* 작업내용 */ } // Runnable 인터페이스의 run()을 구현
}

 

예시

class ThreadEx1 {
	public static void main(String args[]) {
		ThreadEx1_1 t1 = new ThreadEx1_1();

		Runnable r  = new ThreadEx1_2();
		Thread   t2 = new Thread(r);	  // 생성자 Thread(Runnable target)

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

class ThreadEx1_1 extends Thread {
	public void run() {
		for(int i=0; i < 5; i++) {
			System.out.println(getName()); // 조상인 Thread의 getName()을 호출
		}
	}
}

class ThreadEx1_2 implements Runnable {
	public void run() {
		for(int i=0; i < 5; i++) {
			// Thread.currentThread() - 현재 실행중인 Thread를 반환한다.
		    System.out.println(Thread.currentThread().getName());
		}
	}
}

Thread Class를 상속받은 것은 자손 클래스의 인스턴스를 생성하여 바로 사용한다.

 

Runnable Interface를 통해 구현한 것은 Runnable 인스턴스를 생성하고,

Thread클래스의 생성자에 Runnable 인터페이스에서 구현한 인스턴스를 매개변수로 넘겨줘서 Runnable에서 구현한 것을 참조하게 된다.

 

Thread Class 상속

  • 자손 클래스의 인스턴스를 생성하여 사용
  • 상속받은 클래스에서 Thread클래스의 메서드를 직접 호출 가능

 

Runnable Interface 구현

  • Runnable 인스턴스를 생성한 후에, Thread 생성자에 넘겨줘서 사용
  • 구현한 클래스에서 run을 제외한 메서드 호출 불가. Thread.currentThread()를 통해 현재 실행중인 쓰레드의 참조를 받아야한다. (Thread.currentThread().getName())

 

setName(String name)을 통해 쓰레드의 이름을 지정해 줄 수 있고,

지정해주지 않으면 'Thread-번호'의 형식을 가지게 된다.

 

 

쓰레드의 실행 - start()

쓰레드를 생성하고나서 실행하는 것은 start()메서드를 호출하면 된다.

 

t1.start(); // 쓰레스 t1을 실행시킨다.

 

호출한다고 바로 실행되는 것은 아니고, 실행대기 상태에 있다가 자신의 차례가 되면 실행한다.

실행대기중인 쓰레드가 없을 경우 바로 실행된다.

실행순서는 OS(운영체제)가 작성한 스케줄에 의해 결정.

 

한번 실행이 종료된 쓰레드는 재실행 불가. start()는 단 한번만 가능하다.

 

Start()와 run()의 차이

run()을 호출하는 것은 단순히 클래스에 선언된 메서드를 호출하는 것일 뿐이다.

main 메서드에서 run() 호출했을 때의 호출스택

 

start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택(call stack)을 생성한 다음에 run()을 호출해서,

생성된 호출스택에 run()이 첫번째로 올라가게 한다.

 

모든 쓰레드는 독립적인 작업을 수행하기위해 자신만의 호출스택을 필요로 하기 때문에,

새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출스택이 생성되고, 쓰레드가 종료되면 작업에 사용된 호출스택은 소멸한다.

 

  1. main메서드에서 쓰레드의 start()를 호출한다.
  2. start()는 새로운 쓰레드를 생성하고, 쓰레드가 작업하는 사용될 호출스택을 생성한다.
  3. 새로 생성된 호출스택에 run()이 호출되어, 쓰레드가 독립된 공간에서 작업을 수행한다.
  4. 호출스택이 2개이므로 스케줄러가 정한 순서에 의해 번갈아가며 수행한다.

 

main쓰레드

main메서드의 작업을 수행하는 것도 쓰레드이며, 이를 main쓰레드라고 한다.

지금까지는 main 메서드가 수행을 마치면 프로그램이 종료되었으나, 다른 쓰레드가 아직 작업을 마치지 않은 상태라면 프로그램이 종료되지 않는다.

 

// 예제 1

 class ThreadEx2 {
	public static void main(String args[]) throws Exception {
		ThreadEx2_1 t1 = new ThreadEx2_1();
		t1.start();
	}
}

class ThreadEx2_1 extends Thread {
	public void run() {
		throwException();
	}

	public void throwException() {
		try {
			throw new Exception();		
		} catch(Exception e) {
			e.printStackTrace();	
		}
	}
}

start()를 통해 새로 생성한 쓰레드에서 고의로 예외를 발생시켰다.

 

한 쓰레드가 예외를 발생해서 종료되어도 다른 쓰레드의 실행에는 영향을 미치지 않는다.

main쓰레드의 호출스택이 없는 이유는 main쓰레드가 이미 종료되었기 때문이다.

 

//예제 2

class ThreadEx3 {
	public static void main(String args[]) throws Exception {
		ThreadEx3_1 t1 = new ThreadEx3_1();
		t1.run();
	}
}

class ThreadEx3_1 extends Thread {
	public void run() {
		throwException();
	}

	public void throwException() {
		try {
			throw new Exception();		
		} catch(Exception e) {
			e.printStackTrace();	
		}
	}
}

 

쓰레드를 새로 생성하지 않고, run() 호출 후 예외 발생.

 

 

참조

'Java의 정석' 책

댓글