쓰레드

ppp·2025년 7월 12일

Java 공부

목록 보기
11/13
post-thumbnail

프로세스와 쓰레드

  • 프로세스란 실행 중인 프로그램, 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다.

  • 프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있으며 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드이다.

  • 모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재하며, 둘 이상의 쓰레드를 가진 프로세스를 “멀티쓰레드 프로세스”라고 한다.

  • 쓰레드를 프로세스라는 작업공간에서 작업을 처리하는 일꾼이라고 생각할 수 있다.

  • 프로세스를 생성하는 것은 쓰레드를 생성하는 것에 비해 더 많은 시간과 메모리 공간이 필요하다. 쓰레드를 경량 프로세스(light-weight process)라고 부른다.

멀티쓰레딩

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

  • 여러 사용자에게 서비스를 해주는 서버 프로그램의 경우 멀티쓰레드로 작성하는 것은 필수적이어서 하나의 서버 프로세스가 여러 개의 쓰레드를 생성해서 쓰레드와 사용자의 요청이 일대일로 처리되도록 프로그래밍해야 한다.

장점

  • CPU의 사용률을 향상시킨다.

  • 자원을 보다 효율적으로 사용할 수 있다.

  • 사용자에 대한 응답성이 향상된다.

  • 작업이 분리되어 코드가 간결해진다.

단점

  • 여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 발생하는 문제가 있다.

  • 동기화, 교착상태 등

쓰레드의 구현과 실행

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

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

Thread 클래스 상속

class MyThread extends Thread {
	public void run() { /* 작업내용 */ } // Thread 클래스의 run()을 오버라이딩
}

Runnable 인터페이스 구현

class MyThread implements Runnable {
	public void run() { /* 작업내용 */ } // Runnable 인터페이스의 run()을 구현
}

Thread 클래스 실행

class Ex13_1 {
	public static void main(String args[]) {
		Thread1 thread1 = new Thread1();
		
		thread1.start();
	}
}

class Thread1 extends Thread {
	public void run() {
		for(int i = 0; i < 5; i++) {
			// Thread 클래스의 getName()
			// 쓰레드의 이름을 반환
			System.out.println(getName());
		}
	}
}

Runnable 인터페이스 실행

class Ex13_1 {
	public static void main(String args[]) {
		Runnable runnable = new Thread2();
		Thread thread2 = new Thread(runnable);
		
		thread2.start();
	}
}

class Thread2 implements Runnable {
	public void run() {
		for(int i = 0; i < 5; i++) {
			// Thread.currentThread()는 현재 실행중인 쓰레드의 참조를 반환
			System.out.println(Thread.currentThread().getName());
		}
	}
}
  • Runnable 인터페이스를 구현한 경우, Runnable 인터페이스를 구현한 클래스의 인스턴스를 생성한 다음, 이 인스턴스를 Thread 클래스의 생성자의 매개변수로 제공해야 한다.

  • Runnable 인터페이스는 run()만 정의되어 있는 간단한 인터페이스라 현재 실행중인 Thread를 반환하기 위해 Thread.currentThread()를 사용했다.

쓰레드의 실행 - start()

  • start()가 호출되면 쓰레드는 실행대기 상태에 있다가 자신의 차례가 되어야 실행된다. 물론 실행대기중인 쓰레드가 없으면 곧바로 실행 상태가 된다. 쓰레드의 실행순서는 OS의 스케쥴러가 작성한 스케쥴에 의해 결정된다.

  • 한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다. 즉, 하나의 쓰레드에 대해 start()가 한 번만 호출될 수 있다. 그래서 쓰레드의 작업을 한 번 더 수행해야 한다면 새로운 쓰레드를 생성한 다음에 start()를 호출해야 한다.

Thread1 t1 = new Thread1();
t1.start();

t1 = new Thread1();
t1.start();
  • 만일 하나의 쓰레드에 대해 start()를 두 번 이상 호출하면 실행시에 IllegalThreadStateException이 발생한다.
Thread1 t1 = new Thread1();
t1.start();
...
t1.start(); // 예외 발생

start()와 run()

  • main 메서드에서 run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 선언된 메서드를 호출하는 것이다.

  • start()는 새로운 쓰레드가 작업을 실행하는데 필요한 콜스택을 생성한 다음에 run()을 호출해서, 생성된 콜스택에 run()이 첫 번째로 올라가게 한다.

  • 모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 콜스택을 필요로 한다. 새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 콜스택이 생성되고 쓰레드가 종료되면 작업에 사용된 콜스택은 소멸된다.

main 쓰레드

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

  • 프로그램이 실행되면 기본적으로 하나의 쓰레드를 생성하고, 그 쓰레드가 main 메서드를 호출해서 작업을 수행한다.

  • 쓰레드는 “사용자 쓰레드”와 “데몬 쓰레드” 두 종류가 있다. 실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.

싱글쓰레드와 멀티쓰레드

  • 하나의 쓰레드로 두 작업을 처리하는 경우는 한 작업을 마친 후에 다른 작업을 시작하지만, 두 개의 쓰레드로 작업 하는 경우에는 짧은 시간동안 2개의 쓰레드가 번갈아 가면서 작업을 수행해서 동시에 두 작업이 처리되는 것처럼 보인다.

  • 멀티쓰레드로 작업시 컨텍스트 스위칭 시간이 소요된다. 단순히 CPU만을 사용하는 계산작업이라면 멀티쓰레드보다 싱글쓰레드로 프로그래밍하는 것이 효율적이다.

싱글쓰레드로 작업

class Ex13_2 {
	public static void main(String args[]) {
		long startTime = System.currentTimeMillis();

		for(int i=0; i < 300; i++)
			System.out.printf("%s", new String("-"));		
		
		// 15
		System.out.print("소요시간1:" +(System.currentTimeMillis()- startTime)); 

		for(int i=0; i < 300; i++) 
			System.out.printf("%s", new String("|"));		
		
		// 17
 		System.out.print("소요시간2:"+(System.currentTimeMillis() - startTime));
	}
}
  • 수행시간을 측정하기 쉽게 “-” 대신 new String(”-”)을 사용해서 수행 속도를 늦췄다.

멀티쓰레드로 작업

class Ex13_3 {
	static long startTime = 0;

	public static void main(String args[]) {
		ThreadEx3_1 th1 = new ThreadEx3_1();
		th1.start();
		startTime = System.currentTimeMillis();

		for(int i=0; i < 300; i++)
			System.out.printf("%s", new String("-"));	
		
		// 19
		System.out.print("소요시간1:" + (System.currentTimeMillis() - Ex13_3.startTime));
	} 
}

class ThreadEx3_1 extends Thread {
	public void run() {
		for(int i=0; i < 300; i++)
			System.out.printf("%s", new String("|"));	
		
		// 19
		System.out.print("소요시간2:" + (System.currentTimeMillis() - Ex13_3.startTime));
	}
}
  • 두 작업이 아주 짧은 시간동안 번갈아가면서 실행되었으며 거의 동시에 작업이 완료되었다.

  • 컨텍스트 스위칭와 한 쓰레드가 화면에 출력하고 있는 동안 다른 쓰레드는 출력이 끝나기를 기다려야 하는 대기시간 때문에 멀티쓰레드로 작업할 때 더 오래 걸린다.

  • 싱글 코어인 경우에는 멀티쓰레드라도 하나의 코어가 번갈아가면서 작업을 수행하므로 두 작업이 절대 겹치지 않는다. 그러나, 멀티 코어에서는 멀티쓰레드로 두 작업을 수행하면, 동시에 두 쓰레드가 수행될 수 있다. 그래서 화면(console)이라는 자원을 놓고 두 쓰레드가 경쟁하게 되는 것이다.

  • 위의 결과는 실행할 때마다 다른 결과를 얻을 수 있는데 그 이유는 실행 중인 예제프로그램(프로세스)이 OS의 프로세스 스케줄러의 영향을 받기 때문이다. (쓰레드는 OS 종속적이다.)

쓰레드의 I/O블로킹

  • 쓰레드가 입출력(I/O)처리를 위해 기다리는 것을 I/O블로킹이라고 한다.

  • 두 쓰레드가 서로 다른 자원을 사용하는 작업을 수행하는 경우에는 싱글쓰레드 프로세스보다 멀티쓰레드 프로세스가 더 효율적이다. 예를 들면 사용자로부터 데이터를 입력받는 작업, 네트워크로 파일을 주고받는 작업, 프린터로 파일을 출력하는 작업과 같이 외부기기와 입출력을 필요로 하는 경우가 이에 해당한다.

  • 싱글쓰레드의 경우 사용자로부터 입력을 받는 경우 입력이 완료될 때까지 대기해야 하지만 멀티쓰레드의 경우 대기하지 않고 다른 작업을 수행할 수 있다.

싱글쓰레드로 작업

import javax.swing.JOptionPane;

class Ex13_4 {
	public static void main(String[] args) throws Exception {
		String input = JOptionPane.showInputDialog("아무 값이나 입력하세요."); 
		System.out.println("입력하신 값은 " + input + "입니다.");

		for(int i=10; i > 0; i--) {
			System.out.println(i);
			try {
				Thread.sleep(1000);  // 1초간 시간을 지연한다.
			} catch(Exception e ) {}
		}
	}
}
  • 하나의 쓰레드로 사용자의 입력을 받는 작업과 화면에 숫자를 출력하는 작업을 처리하기 때문에 사용자가 입력을 마치기 전까지는 화면에 숫자가 출력되지 않는다.

멀티쓰레드로 작업

import javax.swing.JOptionPane;

class Ex13_5 {
	public static void main(String[] args) throws Exception  {
		ThreadEx5_1 th1 = new ThreadEx5_1();
		th1.start();

		String input = JOptionPane.showInputDialog("아무 값이나 입력하세요."); 
		System.out.println("입력하신 값은 " + input + "입니다.");
	}
}

class ThreadEx5_1 extends Thread {
	public void run() {
		for(int i=10; i > 0; i--) {
			System.out.println(i);
			try {
				sleep(1000);
			} catch(Exception e ) {}
		}
	}
}
  • 사용자로부터 입력받는 부분과 화면에 숫자를 출력하는 부분을 두 개의 쓰레드로 나누어서 처리했기 때문에 사용자 입력을 기다리지 않는다.

쓰레드의 우선순위

  • 쓰레드는 우선순위(priority)라는 속성(멤버변수)를 가지고 있다. 우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다.

  • 쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있다. 시각적인 부분이나 사용자에게 빠르게 반응해야하는 작업을 하는 쓰레드의 우선순위는 다른 작업을 수행하는 쓰레드에 비해 높아야 한다.

우선순위 지정하기

void setPriority(int newPriority) // 쓰레드의 우선순위를 지정한 값으로 변경한다.
int getPriority() // 쓰레드의 우선순위를 반환한다.

public static final int MAX_PRIORITY = 10 // 최대우선순위
public static final int MIN_PRIORITY = 1  // 최소우선순위
public static final int NORM_PRIORITY = 5 // 보통우선순위
  • 쓰레드가 가질 수 있는 우선순위의 범위는 1~10이며 숫자가 높을수록 우선순위가 높다.

  • 쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받는다. main 메서드를 수행하는 쓰레드는 우선순위가 5이므로 main 메서드 내에서 생성하는 쓰레드의 우선순위는 자동적으로 5가 된다.

  • 쓰레드를 실행하기 전에만 우선순위를 변경할 수 있다.

  • 주의할 점은 쓰레드에 높은 우선순위를 준다고 더 많은 실행시간과 실행기회를 갖는 것은 아니다. 자바는 쓰레드가 우선순위에 따라 어떻게 다르게 처리되어야 하는지에 대해 강제하지 않으므로 쓰레드의 우선순위과 관련된 구현이 JVM마다 다를 수 있다.

  • 굳이 우선순위에 차등을 두어 쓰레드를 실행하려면, 특정 OS의 스케쥴링 정책과 JVM의 구현을 직접 확인해봐야 한다. 만일 확인한다 하더라도 OS의 스케쥴러에 종속적이라서 어느 정도 예측만 가능한 정도일 뿐 정확히 알 수 없다.

쓰레드 그룹

  • 쓰레드 그룹은 서로 관련된 쓰레드를 그룹으로 관리할 수 있다. 쓰레드 그룹은 보안상의 이유로 도입된 개념으로, 자신이 속한 쓰레드 그룹이나 하위 쓰레드 그룹은 변경할 수 있지만 다른 쓰레드 그룹의 쓰레드를 변경할 수 없다.

  • 쓰레드를 쓰레드 그룹에 포함시키려면 Thread의 생성자를 이용하면 된다.

Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
  • 모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 하기 때문에, 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 기본적으로 자신을 생성한 쓰레드와 같은 쓰레드 그룹에 속하게 된다.

  • 자바 애플리케이션이 실행되면, JVM은 main와 system이라는 쓰레드 그룹을 만들고 JVM 운영에 필요한 쓰레드들을 생성해서 이 쓰레드 그룹에 포함시킨다. 예를 들어 main 메서드를 수행하는 main이라는 이름의 쓰레드는 main 쓰레드 그룹에 속하고, 가비지컬렉션을 수행하는 Finalizer 쓰레드는 system 쓰레드 그룹에 속한다.

  • 우리가 생성하는 모든 쓰레드 그룹은 main 쓰레드 그룹의 하위 쓰레드 그룹이 되며, 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 자동적으로 main 쓰레드 그룹에 속하게 된다.

데몬 쓰레드

  • 데몬 쓰레드는 다른 일반 쓰레드의 작업을 돕는 보조적인 역할을 한다. 데몬 쓰레드의 예로는 가비지 컬렉터, 워드프로세서의 자동저장, 화면자동갱신 등이 있다.

  • 데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 된다.

  • 데몬 쓰레드는 일반 쓰레드의 보조역할을 수행하므로 일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동 종료된다.

  • 보통 무한루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.

public void run() {
	while(true) {
		try {
			Thread.sleep(3 * 1000); // 3초
		} catch(InterruptedException e) {}
		
		if(autoSave) autoSave();
	}
}
  • 데몬 쓰레드는 일반 쓰레드의 작성방법과 실행방법이 같으며 다만 쓰레드를 생성한 다음 실행하기 전에 setDaemon(true)를 호출하기만 하면 된다.
class Ex13_7 implements Runnable  {
	static boolean autoSave = false;

	public static void main(String[] args) {
		Thread t = new Thread(new Ex13_7());
		t.setDaemon(true);		// 이 부분이 없으면 종료되지 않는다.
		t.start();

		for(int i=1; i <= 10; i++) {
			try{
				Thread.sleep(1000);
			} catch(InterruptedException e) {}
			System.out.println(i);

			if(i==5) autoSave = true;
		}

		System.out.println("프로그램을 종료합니다.");
	}

	public void run() {
		while(true) {
			try { 
				Thread.sleep(3 * 1000); // 3초 마다
			} catch(InterruptedException e) {}

			if(autoSave) autoSave();
		}
	}

	public void autoSave() {
		System.out.println("작업파일이 자동저장되었습니다.");
	}
}

1
2
3
4
5
작업파일이 자동저장되었습니다.
6
7
8
작업파일이 자동저장되었습니다.
9
10
프로그램을 종료합니다.
  • main 쓰레드가 종료되면 데몬 쓰레드도 종료된다.

쓰레드의 상태

상태 이름설명
NEW스레드 객체가 생성되었지만 start() 메서드가 호출되지 않은 상태이다. 아직 운영체제의 스레드 스케줄러에 등록되지 않았다.
RUNNABLE스레드가 실행 중이거나 CPU 할당을 기다리는 상태이다. 실제 실행 여부는 JVM의 스케줄러가 결정한다.
BLOCKED다른 스레드가 소유하고 있는 **동기화 블럭(lock)**에 진입하려고 할 때 대기 중인 상태이다. 해당 lock이 해제될 때까지 기다린다.
WAITING스레드가 작업을 일시 중지하고 다른 스레드의 작업 완료를 기다리는 상태이다. 명시적으로 지정된 시간이 없기 때문에 무기한 대기하며, notify() 또는 interrupt()에 의해 깨어난다.
예: Object.wait(), Thread.join()
TIMED_WAITINGWAITING과 유사하지만, 지정된 시간만큼만 대기하는 상태이다. 시간이 경과되면 자동으로 RUNNABLE 상태로 전환된다.
예: Thread.sleep(1000), join(1000)
TERMINATED스레드의 작업이 완료되어 종료된 상태이다. 예외가 발생하거나 정상적으로 종료되었을 때 이 상태로 전환된다.

생명주기

  1. 쓰레드를 생성하고 start()를 호출하면 실행대기열에 저장되어 자신의 차례를 기다린다. 실행대기열은 큐와 같은 구조로 먼저 실행대기열에 들어온 쓰레드가 먼저 실행된다.

  2. 실행대기 상태에 있다가 자신의 차례가 되면 실행 상태가 된다.

  3. 주어진 실행시간이 다되거나 yield()를 만나면 다시 실행대기 상태가 되고 다음 차례의 쓰레드가 실행 상태가 된다.

  4. 실행 중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있다. 예를 들어 사용자 입력을 기다리는 경우 일시정지 상태에 있다가 사용자가 입력을 마치면 다시 실행대기 상태가 된다.

  5. 지정된 일시정지 시간이 다되거나(time-out), notify(), resume(), interrupt()가 호출되면 일시정지 상태를 벗어나 다시 실행대기열에 저장된다.

  6. 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸된다.

쓰레드의 실행제어

  • 쓰레드 프로그래밍이 어려운 이유는 동기화(synchronization)과 스케줄링(scheduling) 때문이다.

  • 효율적인 멀티쓰레드 프로그램을 만들기 위해서는 보다 정교한 스케줄링을 통해 프로세스에게 주어진 자원과 시간을 여러 쓰레드가 낭비없이 잘 사용하도록 프로그래밍 해야 한다.

  • 쓰레드의 스케줄링을 잘하기 위해서는 쓰레드의 상태와 관련 메서드를 잘 알아야 한다.

메서드 시그니처설명
static void sleep(long millis)
static void sleep(long millis, int nanos)
지정된 시간 동안 스레드를 일시정지(TIMED_WAITING 상태) 시킨다. 시간이 지나면 자동으로 실행대기(RUNNABLE) 상태로 전환된다.
InterruptedException 예외가 발생할 수 있으므로 예외 처리가 필요하다.
void join()
void join(long millis)
void join(long millis, int nanos)
다른 스레드가 작업을 마칠 때까지 대기하게 만든다. 시간 제한을 줄 수도 있으며, 해당 스레드가 종료되거나 시간이 경과되면 다시 실행된다. 주로 메인 스레드가 다른 작업 스레드의 완료를 기다릴 때 사용된다.
void interrupt()sleep()이나 join() 등에 의해 일시정지된 스레드를 깨워서 실행대기 상태로 만든다. 이때 InterruptedException이 발생하며, 적절한 처리 로직이 필요하다.
void stop()스레드를 즉시 강제 종료한다. 현재는 비추천(Deprecated) 되었으며, 자원 해제나 일관성 문제로 인해 사용 지양해야 한다.
void suspend()스레드를 무기한 일시정지시킨다. resume()을 호출해야 재개된다. 하지만 데드락 가능성 때문에 역시 비추천(Deprecated) 상태이다.
void resume()suspend()에 의해 중지된 스레드를 다시 실행 가능하게 만든다. 하지만 위와 같은 이유로 현업에서는 사용하지 않는 것이 원칙이다.
static void yield()실행 중인 스레드가 CPU를 양보하고 다시 실행대기 상태로 전환된다. 스케줄러가 다음에 어떤 스레드를 실행할지는 보장되지 않는다.
  • resume(), stop(), suspend()는 쓰레드를 교착상태로 만들기 쉽게 때문에 deprecated 되었다.

sleep()

  • 지정된 시간동안 쓰레드를 멈추게 한다.

  • 밀리세컨드(millis, 1000분의 일초)와 나노세컨드(nanos, 10억분의 일초)의 시간단위로 세밀하게 값을 지정할 수 있지만 어느 정도 오차가 발생할 수 있다.

try {
	Thread.sleep(1, 500000);
} catch(InterruptedException e) {}
  • sleep()에 의해 일시정지 상태가 된 쓰레드는 지정된 시간이 다 되거나 interrupt()가 호출되면, InterruptedException이 발생되어 실행대기 상태가 된다. 그래서 항상 try-catch문으로 예외 처리를 해야 한다.

interrupt()

  • 진행 중인 쓰레드의 작업이 끝나기 전에 취소시켜야할 때가 있다. 예를 들어 큰 파일을 다운로드받을 때 시간이 너무 오래 걸리면 중간에 다운로드를 포기하고 취소할 수 있어야 한다.

  • interrupt()는 쓰레드의 interrupted 상태(인스턴스 변수)를 바꿔 작업을 멈추도록 요청할 수 있다.

Thread th = new Thread();
th.start();

th.interrupt()

class MyThread extends Thread {
	public void run() {
		while(!interrupted()) {
			...
		}
	}
}

void interrupt() // 쓰레드의 interrupted 상태를 false -> true
boolean isInterrupted() // 쓰레드의 interrupted 상태를 반환
static boolean interrupted() // 현재 쓰레드의 interrupted 상태를 반환 후, false로 변경
  • interrupted()는 쓰레드에 대해 interrupt()가 호출되었는지 알려준다. (boolean 값)

  • 한 쓰레드가 sleep(), wait(), join()에 의해 일시정지 상태에 있을 때, interrupt()를 호출하면 sleep(), wait(), join()에서 InterruptedException이 발생하고 이 쓰레드는 실행대기 상태로 바뀐다.

suspend(), resume(), stop()

  • suspend(), resume(), stop()은 쓰레드의 실행을 제어하는 가장 손쉬운 방법이지만, suspend()와 stop()이 교착상태를 일으키기 쉽게 작성되어있으므로 사용이 권장되지 않는다.

  • 해당 메서드들은 모두 deprecated 되었다.

join()

  • 쓰레드 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 한다. 시간을 지정하지 않으면, 해당 쓰레드가 작업을 모두 마칠 때까지 기다리게 된다.

  • 작업 중에 다른 쓰레드의 작업이 먼저 수행되어야할 필요가 있을 때 join()을 사용한다.

try {
	th1.join();
} catch(InterruptedException e) {}
  • interrupt()에 의해 InterruptedException이 발생해 대기상태에서 벗어나 실행대기 상태로 바뀔 수 있다.

yield()

  • 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보(yield)한다. 예를 들어 스케쥴러에 의해 1초의 실행시간을 할당받은 쓰레드가 0.5초의 시간동안 작업한 상태에서 yield()가 호출되면, 나머지 0.5초는 포기하고 다시 실행대기 상태가 된다.
try {
	th1.join(); // main 쓰레드가 th1의 작업을 끝날 때까지 기다린다.
	th2.join(); // main 쓰레드가 th2의 작업을 끝날 때까지 기다린다.
} catch (InterruptedException e) {}

쓰레드의 동기화

  • 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것

  • 멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 된다.

  • 쓰레드A가 작업하던 도중에 다른 쓰레드B에게 제어권이 넘어갔을 때, 쓰레드A가 작업하던 공유데이터를 쓰레드B가 임의로 변경하였다면, 다시 쓰레드A가 제어권을 받아서 나머지 작업을 마쳤을 때 원래 의도했던 것과는 다른 결과를 얻을 수 있다.

  • 이러한 일이 발생하는 것을 방지하기 위해서 한 쓰레드가 특정 작업을 끝마치기 전까지 다른 쓰레드에 의해 방해받지 않도록 하는 것이 필요하다. 그래서 도입된 개념이 바로 “임계 영역(critical section)”과 “잠금(락, lock)”이다.

  • 공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정하여 공유 데이터(객체)가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한다. 그리고 해당 쓰레드가 임계 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납해야만 다른 쓰레드가 반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 있게 된다.

synchronized 사용

  • 메서드 전체를 임계 영역으로 지정하는 방법과 특정한 영역을 임계 영역으로 지정하는 방법이 있다.
// 메서드 전체를 임계 영역으로 지정
public synchronized void caclSum() {
	// 임계 영역
}

// 특정한 영역을 임계 영역으로 지정
synchronized(객체의 참조변수) {
	// 임계 영역
}
  • “객체의 참조변수”는 락(lock)을 걸고자하는 객체를 참조하는 것이어야 한다. (this 등)

  • 임계 영역은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에 가능하면 메서드 전체에 락을 거는 것보다는 특정 영역으로 임계 영역을 최소화하는 것이 좋다.

wait()과 notify()

  • wait(), notify(), notifyAll()은 모두 Object 클래스에 정의되어 있으며 동기화 블럭(synchronized) 내에서만 사용할 수 있다.

  • synchronized로 동기화해서 공유 데이터를 보호할 때 특정 쓰레드가 객체의 락을 오랜 시간 유지하지 않도록 해야 한다. 특정 쓰레드가 락을 오랜 시간 유지하는 상황을 개선하기 위해 wait()과 notify()를 사용한다.

  • 동기화된 임계 영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, 일단 wait()을 호출하여 쓰레드가 락을 반납하고 기다리게 한다. 나중에 작업을 진행할 수 있는 상황이 되면 notify()를 호출해서, 작업을 중단했던 쓰레드가 다시 락을 얻어 작업을 수행할 수 있게 한다.

  • wait()이 호출되면, 실행 중이던 쓰레드는 해당 객체의 대기실(waiting pool)에서 notify()를 기다린다. notify()가 호출되면, 해당 객체의 대기실에 있던 모든 쓰레드 중에서 임의의 쓰레드만 락을 얻을 수 있다.

  • notifyAll()은 기다리고 있는 모든 쓰레드에게 통보를 하지만, 락을 얻을 수 있는 것은 하나의 쓰레드일 뿐이다.

  • waiting pool은 객체마다 존재한다. 따라서 특정 객체에서 notify()를 호출하면 해당 객체의 waiting pool에서 대기하는 쓰레드에만 통보할 수 있다.

0개의 댓글