Welcome to My World (www.dgmayor.com)

소프트웨어 (과거)/자바 GUI & C# 등...

7. 자바 쓰레드 정리

dgmayor 2022. 1. 29. 17:53
728x90

1. 스레드의 정의

Thread(스레드)의 사전적 의미는 '프로세스 혹은 프로그램을 구성하는 흐름의 단위'이다.

 

프로세스나 프로그램은 하나의 단일 스레드로 구성되어 있을 수도 있고, 2개 이상의 여러 개의 스레드로 구성될 수도 있다. 전자를 싱글 스레드(Single Thread), 후자를 멀티 스레드(Multi Thread)라고 한다.

싱글 스레드 vs 멀티 스레드의 비교 (출처 : 일리노이 시카고대, https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/4_Threads.html)

 

2. 스레드의 특징

스레드는 병렬성과 동시성이라는 특징을 갖는다.

 

① 병렬성(Parallelism)

다수의 코어가 각각에 할당된 스레드를 동시에 실행하는 특징

 

② 동시성(Concurrency)

하나의 코어에서 여러 개의 스레드가 동시에 실행되는 특징

싱글 스레드 vs 멀티 스레드 (출처 : https://www.codeproject.com/Articles/1267757/Concurrency-vs-Parallelism)

 

3. 자바에서 스레드 구현하기

자바는 멀티 스레드(Multi-Thread) 프로그래밍이 가능한 언어로서, 컴퓨터가 동시에 여러 가지 일을 할 수 있도록 코드를 개행할 수 있다. 자바에서 스레드를 구현하기 위해서는 크게 다음의 2가지 방식을 사용할 수 있다.

 

방법 1. Thread 클래스를 상속한다.
방법 2. Runnable 인터페이스를 구현한다.

 

위 두 가지 방법을 각각 사용하여, 0부터 100까지의 숫자를 출력하는 단일 스레드를 구성해보려고 한다.

 

① Thread 클래스를 상속받는 방법

스레드를 사용하고자 하는 클래스에 Thread 클래스를 상속한 후, Thread 클래스의 run() 메서드를 오버라이드 한다. 이때, run() 메서드 안에 스레드에서 실행할 구체적인 내용을 적어주면 된다.

 

0부터 100까지의 숫자를 출력해 주는 내용을 적고, main 메서드에서 객체를 생성한 후, start() 메서드를 호출해 주면 된다.

public class ThreadExtends extends Thread {

	@Override
	public void run() {
		int i=0;
		while(i <= 100){
			System.out.println("i==>" + i);
			i++;
		}
	}

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

}
// 출력결과
i==>0
i==>1
i==>2
..........
i==>95
i==>96
i==>97
i==>98
i==>99
i==>100

 

② Runnable 인터페이스를 구현 받는 방법

Runnable 인터페이스를 사용하는 방법 또한 Thread 클래스를 상속받아 구현하는 방법과 크게 다르지 않다. run() 메서드를 강제로 오버라이드 해서 사용하면 되지만, 스레드의 객체를 생성하는 방법이 조금 다르다.

 

Thread 클래스의 객체를 생성하되, runnable 인터페이스를 구현한 클래스의 객체를 runnable target의 매개변수로 선언한다. 그 이후 start() 메서드를 통해 스레드를 시작하는 과정은 동일하다.

public class ThreadImplements implements Runnable {

	@Override
	public void run() {
		int i=0;
		while(i <= 100){
			System.out.println("i==>" + i);
			i++;
		}
	}
	
	public static void main(String[] args) {
         // Runnable Target으로 ThreadImplements의 객체를 넣어줌
		Thread th1 = new Thread(new ThreadImplements ()); 
		th1.start();
	}

}

 

아래처럼 Thread 클래스를 직접 뜯어보면, 내부적으로 runnable 인터페이스를 구현 받고 있는 것을 확인해볼 수 있다. 결국 runnable 인터페이스가 갖고 있는 왠만한 기능은 Thread 클래스도 갖고 있다는 이야기... 입맛에 맞게 원하는 방법으로 구현하면 될 것 같다.

public class Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }

    private volatile String name;
    private int priority;

    /* Whether or not the thread is a daemon thread. */
    private boolean daemon = false;

    /* Interrupt state of the thread - read/written directly by JVM */
    private volatile boolean interrupted;

    /* Fields reserved for exclusive use by the JVM */
    private boolean stillborn = false;
    private long eetop;

    /* What will be run. */
    private Runnable target;
......
}

 

3. 멀티 스레드(Multi Thread) 구현하기

여러 개의 스레드도 동시에 실행하여 멀티 스레드를 구현하는 것도 가능하다. 방법은 여러 개의 스레드 객체를 생성하여 동시에 start() 메서드로 실행해 주기만 하면 된다.

 

상단의 예제처럼 0부터 100까지 1씩 증가하는 내용의 스레드 객체 3개를 생성하여 실행해본다.

 

단, 어떤 어떤 스레드가 실행되는지 구분이 어려우므로 id를 각각 1,2,3으로 설정하여 console에 찍어본다.

public class MultiThread extends Thread {

	int id;

	public MultiThread(int id) {
		this.id = id;
	}

	@Override
	public void run() {
		int i=0;
		while(i < 100){
			System.out.println("id(" + this.id +"), i==>" + i);
			i++;
		}
	}

	public static void main(String[] args){
		MultiThread th1 = new MultiThread(1);
		MultiThread th2 = new MultiThread(2);
		MultiThread th3 = new MultiThread(3);
		
		th1.start();
		th2.start();
		th3.start();
	}
	
}

 

실행 결과 여러 개의 스레드가 동시에 실행되는 것을 확인할 수 있다.

id(1), i==>0
id(2), i==>0
id(2), i==>1
id(2), i==>2
id(2), i==>3
id(2), i==>4
id(2), i==>5
id(2), i==>6
id(2), i==>7
id(2), i==>8
id(2), i==>9
id(2), i==>10
....

 

4. 스레드 관련 메서드

① 스레드 스케줄링(Thread Scheduling)

* 우선순위 지정(setPriority(priority), getPriority())

 

싱글 스레드인 경우에는 무관하지만, 멀티 스레드 환경에서는 각 스레드 별로 실행 상의 순서를 정해야 할 상황이 올 수 있다. 이를 스레드 스케줄링(Thread Scheduling),이라고 하는데, 스케줄링 방법에는 우선순위를 지정하는 방법, Round Robin(순환 할당) 방법이 있다.

 

1. 우선순위 지정(Priority)

실행의 중요성 등을 고려하여, 선&후 순위를 지정하여 스레드를 실행하는 방식

 

2. 순환 할당(Round Robin)

스레드 간 우선순위의 설정 없이, 순서대로 시간 단위를 할당하여 실행하는 방식

 

Thread 클래스는 이중 우선순위 지정 스케줄링 방법을 지원하며, 이때 사용되는 메서드가 set, getPriority 메서드이다. 우선순위의 범위는 1부터 10까지의 정수이며, 별도의 설정을 하지 않으면 default 값인 5로 설정된다숫자가 10에 가까워질수록 우선순위가 높으며, 1에 가까워질수록 우선순위가 낮아진다.

 

setPriority(우선순위) - 우선순위 설정

setPriority 메서드에 우선순위로 설정하고자 하는 정수를 넣어주면 된다. 직접 숫자를 적어줘도 되고, thread 클래스에 상수로 정의된 값을 넣어줘도 된다. thread 클래스에 상수로 정의된 값은 3가지로 각각 (MIN_PRIORITY, 1), (NORM_PRIORITY, 5), (MAX_PRIORITY, 10)으로 매칭된다.

 

getPriority() - 우선순위 출력

스레드의 우선순위 값이 얼마로 설정될지를 가져오는 메서드다.

/**
 * The minimum priority that a thread can have.
 */
public static final int MIN_PRIORITY = 1;

/**
 * The default priority that is assigned to a thread.
 */
public static final int NORM_PRIORITY = 5;

/**
 * The maximum priority that a thread can have.
 */
public static final int MAX_PRIORITY = 10;
public class ThreadExtends extends Thread {

	@Override
	public void run() {
		int i=0;
		while(i < 100){
			System.out.println("i==>" + i);
			i++;
		}
	}

	public static void main(String[] args){
		ThreadExtends thread = new ThreadExtends();
		
		System.out.println("Priority Initial ==>" + thread.getPriority());
		
		thread.setPriority(Thread.MAX_PRIORITY); // 10
		System.out.println("Thread.MAX_PRIORITY ==>" + thread.getPriority());
		
		thread.setPriority(Thread.MIN_PRIORITY); // 1
		System.out.println("Thread.MIN_PRIORITY ==>" + thread.getPriority());
		
		thread.setPriority(Thread.NORM_PRIORITY); // 5
		System.out.println("Thread.NORM_PRIORITY ==>" + thread.getPriority());

		thread.setPriority(10); // 10
		System.out.println("10 ==>" + thread.getPriority());
		
		thread.setPriority(1); // 1
		System.out.println("1 ==>" + thread.getPriority());
		
		thread.setPriority(5); // 5
		System.out.println("5 ==>" + thread.getPriority());
		
	}
}
// 실행 결과
Priority Initial ==>5
Thread.MAX_PRIORITY ==>10
Thread.MIN_PRIORITY ==>1
Thread.NORM_PRIORITY ==>5
10 ==>10
1 ==>1
5 ==>5

 

② 스레드 이름 설정 - setName(name), getName()

여러 스레드들을 구분하기 위해 스레드 이름을 설정하고, 출력해볼 수도 있다. 스레드의 이름 설정은 필수가 아니며, 직접 설정하지 않아도 내부적으로 자동 설정해 준다.

 

setName(스레드 이름 설정)

별도의 명시가 없으면 thread-0, thread-1, thread-2, thread-N...으로 이름이 설정된다. thread.setName(스레드명)으로 지정할 수 있다.

 

setName(스레드 이름 가져오기)

이름이 지정된 스레드는 thread.getName() 메서드를 통해 가져올 수 있다.

public class ThreadExtends extends Thread {

	@Override
	public void run() {
		int i=0;
		while(i < 100){
			System.out.println("i==>" + i);
			i++;
		}
	}

	public static void main(String[] args){
		
		ThreadExtends thread = new ThreadExtends();
		ThreadExtends thread2 = new ThreadExtends();
		ThreadExtends thread3 = new ThreadExtends();
		ThreadExtends thread4 = new ThreadExtends();
		
		// 별도의 설정이 없을 시에
		System.out.println("thread.getName()==>" + thread.getName());
		System.out.println("thread2.getName()==>" + thread2.getName());
		System.out.println("thread3.getName()==>" + thread3.getName());
		System.out.println("thread4.getName()==>" + thread4.getName());
		
		
		thread.setName("첫번째 스레드");
		thread2.setName("두번째 스레드");
		thread3.setName("세번째 스레드");
		thread4.setName("네번째 스레드");
		
		// 별도의 설정시에
		System.out.println("thread.getName()==>" + thread.getName());
		System.out.println("thread2.getName()==>" + thread2.getName());
		System.out.println("thread3.getName()==>" + thread3.getName());
		System.out.println("thread4.getName()==>" + thread4.getName());
		
	}

}
// 실행결과
thread.getName()==>Thread-0
thread2.getName()==>Thread-1
thread3.getName()==>Thread-2
thread4.getName()==>Thread-3
thread.getName()==>첫번째 스레드
thread2.getName()==>두번째 스레드
thread3.getName()==>세번째 스레드
thread4.getName()==>네번째 스레드

 

 

③ 데몬(Daemon) 스레드의 동작 여부 설정 - setDaemon(true|false)

데몬 스레드는 주가 되는 스레드를 돕는 일종의 조력자 역할을 하는 스레드를 의미한다.

 

주로 프로그램 혹은 스레드가 실행되는 백그라운에서 garbage collection과 같은 역할을 수행하며, 우선순위가 낮다. 조력자의 역할답게 주 스레드가 소멸하면 자연스럽게 함께 없어지는 특징이 있다.

 

이러한 데몬 스레드를 함께 실행할지 여부도 설정할 수 있는데, thread.setDaemon(ture|false)이라는 메서드의 매개변수로 true(실행)/false(실행하지 않음) flag만 바꾸어서 실행해 주면 된다.

public class ThreadExtends extends Thread {

	@Override
	public void run() {
		int i=0;
		while(i < 100){
			System.out.println("i==>" + i);
			i++;
		}
	}

	public static void main(String[] args){
		ThreadExtends thread = new ThreadExtends();
		thread.setDaemon(true);
		thread.setDaemon(false);
	}
}

 

 

④ 스레드의 상태 값 가져오기(getState())

getState() 메서드를 사용하면, 실행 중인 스레드의 상태 값도 출력해볼 수 있다.

public class ThreadExtends extends Thread {

	@Override
	public void run() {
		int i=0;
		while(i < 100){
			System.out.println("i==>" + i);
			i++;
		}
	}

	public static void main(String[] args){
		ThreadExtends thread = new ThreadExtends();
		System.out.println("Before Start==> " + thread.getState());
		thread.start();
		System.out.println("After Start==> " + thread.getState());
	}
}

 

 

 

주의 사항!

  • 이 글은 제가 직접 공부하는 중에 작성되고 있습니다.
  • 따라서 제가 이해하는 그대로의 내용이 포함됩니다.
  • 따라서 이 글은 사실과는 다른 내용이 포함될 수 있습니다.


사용자는 미디어 플레이어에서 동영상을 보다가 일시 정지시킬 수도 있고, 종료시킬 수도 있습니다. 일시 정지는 조금 후 다시 동영상을 보겠다는 의미이므로 미디어 플레이어는 동영상 스레드를 일시 정지 상태로 만들어야 합니다. 그리고 종료는 더 이상 동영상을 보지 않겠다는 의미이므로 미디어 플레이어는 스레드를 종료 상태로 만들어야 합니다. 이와 같이 스레드의 상태를 변경하는 것을 스레드 상태 제어라고 합니다.

 

멀티 스레드 프로그램을 만들기 위해서는 정교한 스레드 상태 제어가 필요합니다. 상태 제어가 잘못되면 프로그램은 불안정해져서 먹통이 되거나 다운됩니다. 멀티 스레드 프로그래밍이 어렵다고 하는 이유가 바로 여기에 있습니다. 스레드는 잘 사용하면 약이 되지만, 잘못 사용하면 치명적인 프로그램 버그가 되기 때문에 스레드를 정확하게 제어하는 방법을 잘 알고 있어야 합니다.

 

스레드 상태 제어를 제대로 하기 위해서는 스레드의 상태 변화를 가져오는 메서드들을 파악하고 있어야 합니다.

메서드 설명
interrupt() 일시 정지 상태의 스레드에서 interruptedException 예외를 발생시켜, 예외 처리 코드(catch)에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 합니다.
notify()
notifyAll()
동기화 블록 내에서 wait() 메서드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만듭니다.
resume() suspend() 메서드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만듭니다.
이 메서드는 Deprecated 메서드로 권하지 않기 때문에 대신 notiry(), notifyAll() 메서드를 사용합니다.
sleep(long millis)
sleep(long millis, int nanos)
주어진 시간 동안 스레드를 일시 정지 상태로 만듭니다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 됩니다.
join()
join(long millis)
join(long millis, int nanos)
join() 메서드를 호출한 스레드는 일시 정지 상태가 됩니다. 실행 대기 상태로 가려면 join() 메서드를 멤버로 가지는 스레드가 종료되거나, 매개 값으로 주어진 시간이 지나야 합니다.
wait()
wait(long millis)
wait(long millis, int nanos)
동기화(Synchronized) 블록 내에서 스레드를 일시 정지 상태로 만듭니다. 매개 값으로 주어진 시간이 지나면 자동적으로 실행 대기 상태가 됩니다. 시간이 주어지지 않으면 notify(), notifyAll() 메서드에 의해 실행 대기 상태로 갈 수 있습니다.
suspend() 스레드를 일시 정지 상태로 만듭니다. resume() 메서드를 호출하면 다시 실행 대기 상태가 됩니다.
이 메서드는 Deprecated 메서드로 권하지 않기때문에 대신 wait() 메서드를 사용합니다.
yield() 실행 중에 우선순위가 동일한 다른 스레드에게 실행을 양보하고 실행 대기 상태가 됩니다.
stop() 스레드를 즉시 종료시킵니다.
이 메서드는 Deprecated 메서드로 권하지 않습니다.

 

위 표에서 wait(), notify(), notifyAll()은 Object 클래스의 메서드이고, 그 이외의 메서드들은 모두 Thread 클래스의 메서드들입니다. wait(), notify(), notifyAll() 메서드의 사용 방법은 스레드의 동기화에서 자세히 알아보고, 이번에는 Thread 클래스의 메서드들을 먼저 살펴보겠습니다. 

 

1. 주어진 시간 동안 일시 정지 : sleep()

다음과 같이 Thread.sleep() 메서드를 호출한 스레드는 주어진 시간 동안 일시 정지 상태가 되고, 다시 실행 대기 상태로 돌아갑니다.

try {
	Thread.sleep(1000);
} catch (InterruptedException e) {
	//interrupt() 메서드가 호출되면 실행
}

매개 값에는 얼마 동안 일시 정지 상태로 있을 것인지, 밀리세컨드(㎳) 단위로 시간을 주면 됩니다. 일시 정지 상태에서 주어진 시간이 되기 전에 interrupt() 메서드가 호출되면 interruptedException 예외가 발생하기 때문에 예외 처리가 필요합니다.

 

다음 예제는 3초 주기로 비프(beep) 음을 10번 발생시킵니다.

//Main.java
package Example;

import java.awt.Toolkit;

public class Main {
	public static void main(String[] args) {		
		Toolkit toolkit = Toolkit.getDefaultToolkit();
		
		for(int i = 0; i < 10; i++) {
			toolkit.beep();
			try {
				Thread.sleep(3000);
			} catch(InterruptedException e) {}
		}
	}
}

 

2. 다른 스레드에게 실행 양보 : yield()

스레드가 처리하는 작업은 반복적인 실행을 위해 for문이나 while문을 포함하는 경우가 많습니다. 가끔은 이 반복문들이 무의미한 반복을 하는 경우가 있습니다. 다음 코드를 보겠습니다.

public void run() {
	while(true) {
		if(work) {
			System.out.println("ThreadA 작업 내용");
		}
	}
}

스레드가 시작되어 run() 메서드를 실행하면 while(true) { } 블록을 무한 반복 실행합니다. 만약 work의 값이 false라면 그리고 work의 값이 false에서 true로 변경되는 시점이 불분명하다면, while문은 어떠한 실행문도 실행하지 않고 무의미한 반복을 하게 됩니다. 이것보다는 다른 스레드에게 실행을 양보하고 자신은 실행 대기 상태로 가는 것이 전체 프로그램 성능에 도움이 됩니다.

 

이런 기능을 위해서 스레드는 yield() 메서드를 제공하고 있습니다. yield() 메서드를 호출한 스레드는 실행 대기 상태로 돌아가고 동일한 우선순위 또는 높은 우선순위를 갖는 다른 스레드가 실행 기회를 가질 수 있도록 해줍니다.

 

다음 코드는 의미 없는 반복을 줄이기 위해 yield() 메서드를 호출해서 다른 스레드에게 실행 기회를 주도록 수정한 것입니다.

public void run() {
	while(true) {
		if(work) {
			System.out.println("ThreadA 작업 내용");
		} else {
			Thread.yield();
		}
	}
}

 

다음 예제는 처음 실행 후 처음 2밀리 세컨드 동안은 ThreadA와 ThreadB가 번갈아가며 실행됩니다. 2밀리 세컨드 이후에는 ThreadA의 work를 flase로 변경하여 ThreadA가 yield() 메서드를 호출하게 합니다. 이 동안에는 ThreadB가 더 많은 실행 기회를 가집니다. 다시 2밀리 세컨드 후에는 ThreadA의 work를 true로 변경하고 다음 2밀리 세컨드 후에는 두 스레드 모두 stop을 true로 변경하여 실행을 종료합니다.

//ThreadA.java
package Example;

public class ThreadA extends Thread {
	public boolean stop = false;    //종료 플래그
	public boolean work = true;     //작업 진행 여부 플래그
	
	public void run() {
		while(!stop) {
			if(work) {
				System.out.println("ThreadA 작업 내용");
			} else {
				Thread.yield();
			}
		}
		
		System.out.println("ThreadA 종료!");
	}
}
//ThreadB.java
package Example;

public class ThreadB extends Thread {
	public boolean stop = false;
	public boolean work = true;
	
	public void run() {
		while(!stop) {
			if(work) {
				System.out.println("ThreadB 작업 내용");
			} else {
				Thread.yield();
			}
		}
		
		System.out.println("ThreadB 종료!");
	}
}
//Main.java
package Example;

public class Main {
	public static void main(String[] args) {		
		ThreadA threadA = new ThreadA();
		ThreadB threadB = new ThreadB();
		
		System.out.println("--------------------------시작--------------------------");
		threadA.start();
		threadB.start();
		
		try {
			Thread.sleep(2);
		} catch (InterruptedException e) {}
		System.out.println("ThreadA work : false");
		threadA.work = false;
		
		try {
			Thread.sleep(2);
		} catch(InterruptedException e) {}
		System.out.println("ThreadA work : ture");
		threadA.work = true;
		
		try {
			Thread.sleep(2);
		} catch(InterruptedException e) {}
		threadA.stop = true;
		threadB.stop = true;
	}
}

 

3. 다른 스레드의 종료를 기다림 : join()

스레드는 다른 스레드와 독립적으로 실행하는 것이 기본이지만 다른 스레드가 종료될 때까지 기다렸다가 실행해야 하는 경우가 발생할 수 있습니다. 예를 들어 계산 작업을 하는 스레드가 모든 계산 작업을 마쳤을 때, 계산 결괏값을 받아 이용하는 경우가 이에 해당합니다.

 

ThreadA가 ThreadB의 join() 메서드를 호출하면 ThreadA는 ThreadB가 종료될 때까지 일시 정지 상태가 됩니다. ThreadB의 run() 메서드가 종료되면 비로소 ThreadA는 일시 정지에서 풀려 다음 코드를 실행하게 됩니다.

 

다음 예제를 보면 메인 스레드는 SumThread가 계산 작업을 모두 마칠 때까지 일시 정지 상태에 있다가 SumThread가 최종 계산된 결괏값을 산출하고 종료하면 결괏값을 받아 출력합니다.

//SumThread.java
package Example;

public class SumThread extends Thread {
	private long sum;
	
	public long getSum() {
		return sum;
	}
	
	public void setSum(long sum) {
		this.sum = sum;
	}
	
	@Override
	public void run() {
		for(int i = 1; i <= 100; i++) {
			sum += i;
		}
	}
}
//Main.java
package Example;

public class Main {
	public static void main(String[] args) {		
		SumThread sumThread = new SumThread();
		sumThread.start();
		
		try {
			sumThread.join();
		} catch (InterruptedException e) {}
		
		System.out.println("1 ~ 100의 합 : " + sumThread.getSum());
	}
}

/*
실행결과

1 ~ 100의 합 : 5050

*/

 

4. 스레드 간 협업 : wait(), notify(), notifyAll()

경우에 따라서는 두 개의 스레드를 교대로 번갈아가며 실행해야 할 경우가 있습니다. 정확한 교대 작업이 필요할 경우, 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고, 자신은 일시 정지 상태로 만드는 것입니다.

 

이 방법의 핵심은 공유 객체에 있습니다. 공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메서드로 구분해 놓습니다. 한 스레드가 작업을 완료하면 notify() 메서드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고, 자신은 두 번 작업을 하지 않도록 wait() 메서드를 호출하여 일시 정지 상태로 만듭니다.

 

만약 wait() 대신 wait(long timeout)이나, wait(long timeout, int nanos)를 사용하면 notify()를 호출하지 않아도 지정된 시간이 지나면 스레드가 자동적으로 실행 대기 상태가 됩니다. notify() 메서드와 동일한 역할을 하는 notifyAll() 메서드도 있는데, notify()는 wait()에 의해 일시 정지된 스레드 중 한 개를 실행 대기 상태로 만들고, notifyAll() 메서드는 wait()에 의해 일시 정지된 모든 스레드들을 실행 대기 상태로 만듭니다. 

 

이 메서드들은 Thread 클래스가 아닌, Object 클래스에 선언된 메서드이므로 모든 공유 객체에서 호출이 가능합니다. 주의할 점은 이 메서드들은 동기화 메서드 또는 동기화 블록 내에서만 사용할 수 있다는 것입니다.

 

다음 예제는 두 스레드의 작업을 WorkObject의 methodA()와 methodB()에 정의해 두고, 두 스레드 ThreadA와 ThreadB가 교대로 methodA()와 methodB()를 호출하도록 했습니다.

//WorkObject.java
package Example;

public class WorkObject {
	public synchronized void methodA() {
		System.out.println("ThreadA의 methodA() 작업 실행");
		notify();
		
		try {
			wait();
		} catch(InterruptedException e) {}
	}
	
	public synchronized void methodB() {
		System.out.println("ThreadB의 methodB() 작업 실행");
		notify();
		
		try {
			wait();
		} catch(InterruptedException e) {}
	}
}
//ThreadA.java
package Example;

public class ThreadA extends Thread {
	private WorkObject workObject;
	
	public ThreadA(WorkObject workObject) {
		this.workObject = workObject;
	}
	
	@Override
	public void run() {
		for(int i = 0; i < 10; i++) {
			workObject.methodA();
		}
	}
}
//ThreadB.java
package Example;

public class ThreadB extends Thread {
private WorkObject workObject;
	
	public ThreadB(WorkObject workObject) {
		this.workObject = workObject;
	}
	
	@Override
	public void run() {
		for(int i = 0; i < 10; i++) {
			workObject.methodB();
		}
	}
}
//Main.java
package Example;

public class Main {
	public static void main(String[] args) {		
		WorkObject sharedObject = new WorkObject();
		
		ThreadA threadA = new ThreadA(sharedObject);
		ThreadB threadB = new ThreadB(sharedObject);
		
		threadA.start();
		threadB.start();
	}
}

 

5. 스레드의 안전한 종료 : stop 플래그, interrupt()

스레드는 자신의 run() 메서드가 모두 실행되어야 종료됩니다. 하지만 경우에 따라서는 실행 중인 스레드를 즉시 종료할 필요가 있습니다. 예를 들어 동영상을 끝까지 보지 않고, 사용자가 멈춤을 요구할 경우 그렇습니다.

 

Thread는 스레드를 즉시 종료시키기 위해서 stop() 메서드를 제공하고 있는데, 이 메서드는 Deprecated 되었습니다. 이유는 stop() 메서드로 스레드를 갑자기 종료하게 되면 스레드가 사용 중이던 자원들이 불안전한 상태로 남겨지기 때문입니다. 여기서 자원이란, 파일, 네트워크 연결 등을 말합니다. 그렇다면 스레드를 즉시 종료하기 위한 최선의 방법은 무엇일까요?

 

5. 1. stop 플래그를 이용하는 방법

스레드는 run() 메서드가 끝나면 자동으로 종료되기 때문에 run() 메서드가 정상적으로 종료되도록 유도하는 것이 최선의 방법입니다. 다음 코드는 stop 플래그를 이용해서 run() 메서드의 종료를 유도합니다.

public class XXXThread extends Thread {
	private boolean stop;

	public void run() {
		while(!stop) {
			//스레드가 반복 실행할 코드;
		}
		//스레드가 사용한 자원 정리
	}
}

위 코드에서 stop 필드가 false일 경우에는 while문의 조건식이 true가 되어 반복 실행하지만, stop 필드가 true일 경우에는 while문의 조건식이 false가 되어 while문을 빠져나옵니다. 그리고 스레드가 사용한 자원을 정리하고, run() 메서드가 끝나게 됨으로써 스레드는 안전하게 종료됩니다.

 

다음 예제는 PrintThread을 실행한 후 1초 후에 PrintThread을 멈추도록 setStop() 메서드를 호출합니다.

//PrintThread.java
package Example;

public class PrintThread extends Thread {
	private boolean stop;
	
	public void setStop(boolean stop) {
		this.stop = stop;
	}
	
	@Override
	public void run() {
		while (!stop) {
			System.out.println("실행 중");
		}
		System.out.println("자원 정리");
		System.out.println("실행 종료");
	}
}
//Main.java
package Example;

public class Main {
	public static void main(String[] args) {		
		PrintThread printThread = new PrintThread();
		printThread.start();
		
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {}
		
		printThread.setStop(true);
	}
}

/*
실행결과

...
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
실행 중
자원 정리
실행 종료

*/

 

5. 2. interrupt() 메서드를 이용하는 방법

interrupt() 메서드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 합니다. 이것을 이용하면 run() 메서드를 정상 종료시킬 수 있습니다.

 

예를 들어 ThreadA에서 ThreadB를 실행했다고 가정해 보겠습니다. ThreadA가 ThreadB의 interrupt() 메서드를 실행하게 되면 ThreadB가 sleep() 메서드로 일시 정지 상태가 될 때 ThreadB에서 InterruptedException 예외가 발생하게 됩니다. 

 

다음 예제는 PrintThread를 실행한 후 1초 후에 PrintThread를 멈추도록 interrupt() 메서드를 호출합니다.

//PrintThread.java
package Example;

public class PrintThread extends Thread {
	@Override
	public void run() {
		try {
			while (true) {
				System.out.println("실행 중");
				Thread.sleep(1);
			}
			
		} catch (InterruptedException e) {}
		
		System.out.println("자원 정리");
		System.out.println("실행 종료");
	}
}
//Main.java
package Example;

public class Main {
	public static void main(String[] args) {		
		PrintThread printThread = new PrintThread();
		printThread.start();
		
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {}
		
		printThread.interrupt();
	}
}

/*
실행결과

...
실행 중
실행 중
실행 중
실행 중
실행 중
자원 정리
실행 종료

*/

주목할 점은 스레드가 실행 대기 또는 실행 상태에 있을 때 interrupt() 메서드가 실행되면 즉시 InterruptedException 예외가 발생하지 않고, 스레드가 미래에 일시 정지 상태가 되면 InterruptedException 예외가 발생한다는 것입니다. 따라서 스레드가 일시 정지 상태가 되지 않으면 interrupt() 메서드 호출은 아무런 의미가 없습니다. 그래서 짧은 시간이나마 일시 정지시키기 위해 Thread.sleep(1)을 사용했습니다.

 

일시 정지를 만들지 않고도 interrupt() 호출 여부를 알 수 있는 방법이 있습니다. interrupted() 메서드가 호출되었다면 스레드의 interrupted()와 isInterrupted() 메서드는 true를 리턴합니다. interrupted() 메서드는 정적 메서드이고, isInterrupted()는 인스턴스 메서드입니다. 둘 중 어느 것을 사용해도 좋습니다.

 

다음은 PrintThread를 수정한 것입니다. 일시 정지 코드인 Thread.sleep(1)을 사용하지 않고, Thread.interrupted()를 사용해서 PrintThread의 interrupt()가 호출되었는지 확인한 다음 while문을 빠져나가도록 했습니다.

//PrintThread.java
package Example;

public class PrintThread extends Thread {
	@Override
	public void run() {
		while (true) {
			System.out.println("실행 중");
			
			if(Thread.interrupted()) {
				break;
			}
		}
		System.out.println("자원 정리");
		System.out.println("실행 종료");
	}
}


출처: https://koey.tistory.com/262 [기계공학과졸업하고게임만들기]

 

 

지금까지 스레드의 정의, 자바에서 스레드(싱글&멀티)를 구현하는 방법과 연관 메서드들에 대해 알아보았다. 스레드는 비단 자바뿐만 아니라 cpu 등 컴퓨터 자원에 대해 공부할 때 매우 중요한 개념이라고 생각한다. 즉, 앞으로도 꾸준히 공부하고 많이 활용해 나가야 한다는 이야기...

 

쓰레드는 자신의 run() 메소드가 모두 실행되면 자동적으로 종료된다.

하지만, 경우에 따라서는 실행 중인 쓰레드를 즉시 종료할 필요가 있다.

public class StopThread extends Thread {
    public void run() {
        while(true) {
            System.out.println("실행 중");
        }
        System.out.println("자원 정리");
        System.out.println("실행 종료");
    }
}

StopThread는 while(true) 이므로 무한 반복을 하게 된다.

 

쓰레드를 즉시 종료시키기 위해서 stop() 메소드를 제공하고 있는데, 이는 쓰지 않는다(deprecated 됨).
stop() 메소드는 쓰레드가 사용 중이던 자원들이 불완전한 상태로 남겨지기 때문이다.

 

안전하게 Thread를 종료시키는 방법은

boolean 변수를 사용하는 방법과 interrupted() 메소드를 이용하는 방법이 있다.

 


interrupted() 메소드를 이용하는 방법 (권장)

interrupt() 메소드는 일시 정지 상태일 때 정지가 된다.
Thread.sleep(1); // 일시 정지 상태일 경우 interruptedException을 발생시킨다.
실행대기 또는 실행상태에서는 interruptedException이 발생하지 않는다.
일시 정지 상태를 만들지 않고 while문을 빠져나오는 방법 즉 쓰레드를 종료시키는 방법이 있다.
Thread.interrupted() 메소드를 이용하면 된다.

public class StopThread extends Thread {

    public void run() {
        while(true) {
            System.out.println("실행 중");
            if(Thread.interrupted()) {
                break;
            }
        }
        System.out.println("자원 정리");
        System.out.println("실행 종료");
    }
}

public class ThreadStopExample {

    public static void main(String[] args) {
        Thread thread = new StopThread();
        thread.start();
       
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
       
        thread.interrupt();
    }
}

1초 뒤에 호출되는 thread.interrupt()에 의해 무한 루프는 종료된다.

하지만 더 깔끔하게 처리하는 방법은 StopThread 의 while 문을 아래와 같이 하는 것이다.

public class StopThread extends Thread {

    public void run() {
        while(!Thread.currentThread().isInterrupted()) {
            System.out.println("실행 중");
        }
        System.out.println("자원 정리");
        System.out.println("실행 종료");
    }
}

 

 

boolean 변수를 사용하는 방법

public class StopThread extends Thread {
    private boolean flag = false;
   
    StopThread() {
        this.flag = true; // 생성자에 flag 설정
    }
   
    public void Action(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        while(flag) {
            System.out.println("실행 중");
        }
        System.out.println("자원 정리");
        System.out.println("실행 종료");
    }
}

public class ThreadStopExample {

    public static void main(String[] args) {
        Thread thread = new StopThread();
        thread.start();
       
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
       
        ((StopThread) thread).Action(false);
    }
}


출처: https://link2me.tistory.com/1731 [소소한 일상 및 업무TIP 다루기]

 

쓰레드에서 가장 중요한 것은 runnable도 있지만 정지 방법을 아는 것도 중요....

 

쓰레드에 대해서 자료를 많이 뒤져 봤는데.....

이것이 제일 나은 자료라서 이걸 가져오게 되었다.

내 대학시절 컴퓨터 공학과 입학한 이야기를 하고 싶다.

728x90