이것이 자바다 - Part 14

mj·2023년 1월 25일
0
post-thumbnail

Part 14 멀티 스레드

멀티 스레드 개념

운영체제는 실행 중인 프로그램을 프로세스로 관리한다.
멀티 태스킹은 두 가지 이상의 작업을 동시에 처리하는 것을 말하는데, 이때 운영체제는 멀티 프로세스를 생성해서 처리한다.

하나의 프로세스에서 멀티 스레드를 이용하여 두 가지 이상의 작업을 처리할 수 있다.
스레드는 코드의 실행 흐름을 말하는데, 프로세스 내에 스레드가 두 개라면 두 개의 코드 실행 흐름이 생긴다는 의미이다.

멀티 프로세스들은 서로 독립적이므로 하나의 프로세스에서 오류가 발생해도 다른 프로세스에게 영향을 미치지 않는다.
하지만 멀티 스레드는 프로세스 내부에서 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스가 종료되므로 다른 스레드에게 영향을 미친다.

메인 스레드

모든 자바 프로그램은 메인 스레드가 main() 메소드를 실행하면서 시작된다.
메인 스레드는 main() 메소드의 첫 코드부터 순차적으로 실행하고, main() 메소드의 마지막 코드를 실행하거나 return 문을 만나면 실행을 종료한다.

public static void main(String[] args) {
	String data = null;
    if( ... ) {
    }
    while( ... ) {
    }
    System.out.println("...");
}

메인 스테드는 필요에 따라 추가 작업 스레드들을 만들어서 실행시킬 수 있다.

싱글 스레드에서는 메인 스레드가 종료되면 프로세스가 종료되지만 멀티 스레드에서는 실행 중인 스레드가 하나라도 있다면 프로세스는 종료되지 않는다.

작업 스레드 생성과 실행

멀티 스테드로 실행하는 프로그램을 개발하려면 먼저 몇 개의 작업을 병렬로 실행할지 결정하고 각 작업별로 스레드를 생성해야 한다.

자바는 작업 스레드도 객체로 관리하므로 클래스가 필요하다. Thread 클래스로 직접 객체를 생성해도 되지만, 하위 클래스를 만들어 생성할 수도 있다.

Thread 클래스로 직접 생성

java.lang 패키지에 있는 Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면 다음과 같이 Runnable 구현 객체를 매개값으로 갖는 생성자를 호출하면 된다.

Thread thread = new Thread(Runnable target);

Runnable 은 스레드가 작업을 실행할 때 사용하는 인터페이스이다.
Runnable 에는 run() 메소드가 정의되어 있는데, 구현 클래스는 run()을 재정의해서 스레드가 실행할 코드를 가지고 있어야 한다.

class Task implements Runnable {
	@Override
    public void run() {
    	//스레드가 실행할 코드
    }
}

Runnable 구현 클래스는 작업 내용을 정의한 것이므로, 스레드에게 전달해야 한다.

Runnable task = new Task();
Thread thread = new Thread(task);

명시적인 Runnable 구현 클래스를 작성하지 않고 Thread 생성자를 호출할 때 Runnable 익명 구현 객체를 매개값으로 사용할 수 있다.

Thread thread = new Thread(new Runnable() {
	@Override
    public void run() {
    	//스레드가 실행할 코드
    }
}

작업 스레드를 실행하려면 스레드 객체의 start() 메소드를 호출해야 한다.

thread.start();

Thread 자식 클래스로 생성

작업 스레드 객체를 생성하는 또 다른 방법은 Thread 의 자식 객체로 만드는 것이다.
Thread 클래스를 상속한 다음 run() 메소드를 재정의해서 스레드가 실행할 코드를 작성하고 객체를 생성하면 된다.

public class WorkerThread extends Thread {
	@Override
    public void run() {
    	//스레드가 실행할 코드
    }
}

Thread thread = new WorkerThread();

명시적인 자식 클래스를 정의하지 않고, 다음과 같이 Thread 익명 자식 객체를 사용할 수도 있다.

Thread thread = new Thread() {
	@Override
    public void run() {
    	//스레드가 실행할 코드
    }
};
thread.start();

스레드 이름

스레드는 자신의 이름을 가지고 있다. 메인 스레드는 'main' 이라는 이름을 가지고 있고, 작업 스레드는 자동적으로 Thread-n 이라는 이름을 가진다.

작업 스레드의 이름을 다른 이름으로 설정하고 싶다면 Thread 클래스의 setName() 메소드를 사용하면 된다.

thread.setName("스레드 이름");

스레드 이름은 디버깅할 때 어떤 스레드가 작업을 하는지 조사할 목적으로 주로 사용된다.
스레드의 이름을 확인할려면 getName() 메소드를 사용하면 된다.

Thread thread = new Thread.currentThread();
System.out.println(thread.getName());

스레드 상태

스레드 객체를 생성하고 start() 메소드를 호출하면 곧바로 스레드가 실행되는 것이 아니라 실행 대기 상태(RUNNABLE)가 된다.
실행 대기 상태란 실행을 기다리고 있는 상태를 말한다.

실행 대기하는 스레드는 CPU 스케줄링에 따라 CPU를 점유하고 run() 메소드를 실행한다. 이때를 실행(RUNNING) 상태라고 한다.

실행 스레드는 run() 메소드를 모두 실행하기 전에 스케줄링에 의해 다시 실행 대기 상태로 돌아갈 수 있다. 그리고 다른 스레드가 실행상태가 된다.

이렇게 스레드는 실행 대기 상태와 실행 상태를 번갈아 가면서 자신의 run() 메소드를 조금씩 실행한다.
실행 상태에서 run() 메소드가 종료되면 더 이상 실행할 코드가 없기 때문에 스레드의 실행은 멈추게 된다. 이 상태를 종료상태(TERMINATED) 라고 한다.

실행 상태에서 일시 정지 상태로 가기도 하는데, 일시 정지 상태는 스레드가 실행할 수 없는 상태를 말한다.

다음은 일시 정지로 가기 위한 메소드와 벗어나기 위한 메소드이다.

구분메소드설명
일시 정지로 보냄sleep(long millis)주어진 시간동안 스레드를 일시 정지 상태로 만든다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 된다.
일시 정지로 보냄join()join() 메소드를 호출한 스레드는 일시 정지 상태가 된다. 실행 대기 상태가 되려면, join() 메소드를 가진 스레드가 종료되어야 한다.
일시 정지로 보냄wait()동기화 블록 내에서 스레드를 일시 정지 상태로 만든다.
일시 정지에서 벗어남interrupt()일시 정지 상태일 경우, InterruptedException을 발생시켜 실행 대기 상태 또는 종료 상태로 만든다.
일시 정지에서 벗어남notify(), notifyAll()wait() 메소드로 인해 일시 정지 상태인 스레드를 실행 대기 상태로 만든다.
실행 대기로 보냄yield()실행 상태에서 다른 스레드에게 실행을 양보하고 실행 대기 상태가 된다.

주어진 시간동안 일시 정지

실행 중인 스레드를 일정 시간 멈추게 하고 싶다면 Thread 클래스의 정적 메소드인 sleep() 을 이용하면 된다.

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

일시 정지 상태에서는 InterruptedException 이 발생할 수 있기 때문에 sleep()은 예외 처리가 필요한 메소드이다.

다른 스레드의 종료를 기다림

스레드는 다른 스레드와 독립적으로 실행하지만 다른 스레드가 종료될 때까지 기다렸다가 실행을 해야 하는 경우도 있다.

이를 위해 스레드는 join() 메소드를 제공한다.

다른 스레드에게 실행 양보

스레드가 처리하는 작업은 반복적인 실행을 위해 for 문이나 while 문을 포함하는 경우가 많은데, 가끔 반복문이 무의미한 반복을 하는 경우가 있다.
이때는 다른 스레드에게 실행을 양보하고 자신은 실행 대기 상태로 가는 것이 프로그램 성능에 도움이 된다.

이런 기능을 위해 Thread 는 yield() 메소드를 제공한다.
yield() 를 호출한 스레드는 실행 대기 상태로 돌아가고, 다른 스레드가 실행 상태가 된다.

스레드 동기화

멀티 스레드는 하나의 객체를 공유해서 작업할 수도 있다.
이 경우, 다른 스레드에 의해 객체 내부 데이터가 쉽게 변경될 수 있기 때문에 의도했던 것과는 다른 결과가 나올 수 있다.

스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸면 된다.

이를 위해 자바는 동기화 메소드와 블록을 제공한다.

객체 내부에 동기화 메소드와 동기화 블록이 여러 개가 있다면 스레드가 이들 중 하나를 실행할 때 다른 스레드는 해당 메소드는 물론이고 다른 동기화 메소드 및 블록도 실행할 수 없다.

동기화 메소드 및 블록 선언

동기화 메소드를 선언하는 방법은 다음과 같이 synchronized 키워드를 붙이면 된다. 해당 키워드는 인스턴스와 정적 메소드 어디든 붙일 수 있다.

public synchronized void method() {
	//단 하나의 스레드만 실행하는 영역
}

스레드가 동기화 메소드를 실행하는 즉시 객체는 잠금이 일어나고, 메소드 실행이 끝나면 잠금이 풀린다.
메소드 전체가 아닌 일부 영역을 실행할 때만 객체 잠금을 걸고 싶다면 다음과 같이 동기화 블록을 만들면 된다.

public void method() {
	//여러 스레드가 실행할 수 있는 영역
    
    synchronized(공유객체) {
    	//단 하나의 스레드만 실행하는 영역
    }
    
    //여러 스레드가 실행할 수 있는 영역
}

wait()과 notify()를 이용한 스레드 제어

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

이 방법의 핵심은 공유 객체에 있다. 공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메소드로 정해 놓는다.

한 스레드가 작업을 완료하면 notify() 메소드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고, 자신은 두 번 작업을 하지 않도록 wait() 메소드를 호출하여 일시 정지 상태로 만든다.

주의할 점은 notify(), wait() 메소드는 동기화 메소드 또는 동기화 블록 내에서만 사용 가능하다.

스레드 안전 종료

스레드는 자신의 run() 메소드가 모두 실행되면 자동적으로 종료되지만, 경우에 따라서는 실행 중인 스레드를 즉시 종료할 필요가 있다.

스레드를 안전하게 종료할 때는 주로 조건 이용 방법과 interrupt() 메소드 이용 방법을 사용한다.

조건 이용

스레드가 while 문으로 반복 실행할 경우, 조건을 이용해서 run() 메소드의 종료를 유도할 수 있다.
다음 코드는 stop 필드 조건에 따라서 run() 메소드의 종료를 유도한다.

public class XXXThread extends Thread {
	private boolean stop;
    
    public void run() {
    	while( !stop ) {
        	//스레드가 반복 실행하는 코드
        }
        //스레드가 사용한 리소스 정리
    }
}

interrupt() 메소드 이용

interrupt() 메소드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 한다.

이것을 이용하면 예외 처리를 통해 run() 메소드를 정상 종료시킬 수 있다.

스레드가 실행 대기/실행 상태일 때는 interrupt() 메소드가 호출되어도 InterruptedException 이 발생하지 않는다.

그러나 스레드가 어떤 이유로 일시 정지 상태가 되면 예외가 발생한다.

일시 정지를 만들지 않고도 interrupt() 메소드 호출 여부를 할 수 있는 방법이 있다.
Thread 의 interrupted()와 isInterruped() 메소드는 interrupt() 메소드 호출 여부를 리턴한다.

boolean status = Thread.interruped();
boolean status = objThread.isInterrupted();

데몬 스레드

데몬 스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다.
주 스레드가 종료되면 데몬 스레드도 자동으로 종료된다.

스레드를 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDaemon(true) 를 호출하면 된다.
다음 예를 보면 메인 스레드는 주 스레드, AutoSaveThread는 데몬 스레드가 된다.

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

스레드풀

병렬 작업 처리가 많아지면 스레드의 개수가 폭증하여 CPU가 바빠지고 메모리 사용량이 늘어난다.
이에 따라 애플리케이션의 성능 또한 급격히 저하된다. 이렇게 병렬 작업 증가로 인한 스레드의 폭증을 막으려면 스레드풀을 사용하는 것이 좋다.

스레드풀은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해놓고 작업 큐에 들어오는 작업들을 스레드가 하나씩 맡아 처리하는 방식이다.

작업 처리가 끝난 스레드는 다시 작업 큐에서 새ㅑ로운 작업을 가져와 처리한다.

스레드풀 생성

자바는 스레드풀을 생성하고 사용할 수 있도록 java.util.concurrent 패키지에서 ExecutorService 인터페이스와 Executors 클래스를 제공하고 있다.
Executors의 다음 두 정적 메소드를 이용하면 간단하게 스레드풀인 ExecutorService 구현 객체를 만들 수 있다.

메소드명(매개변수)초기 수코어 수최대 수
newCachedThreadPool()00Integer.MAX_VALUE
newFixedThreadPool(int nThreads)0생성된 수nThreads
  • 초기 수 : 스레드풀이 생성될 때 기본적으로 생성되는 스레드 수
  • 코어 수 : 스레드가 증가된 후 사용되지 않는 스레드를 제거할 때 최소한 풀에서 유지하는 스레드 수
  • 최대 수 : 증가되는 스레드의 한도 수

newCachedThreadPool() 메소드로 생성된 스레드풀에서 60초 동안 스레드가 아무 작업을 하지 않으면 스레드를 풀에서 제거한다.

ExecutorService executorService = Executors.newCachedThreadPool();

newFixedThreadPool() 로 생성된 스레드풀에서는 생성된 스레드를 제거하지 않는다.

ExecutorService executorService = Executors.newFixedThreadPool();

위 두 메소드를 사용하지 않고 직접 ThreadPoolExecutor 로 스레드풀을 생성할 수도 있다.
여기에서는 추가된 스레드가 120초 동안 놀고 있을 경우 해당 스레드를 풀에서 제거한다.

ExecutorService threadPool = new ThreadPoolExecutor(
	3,		//코어 스레드 개수
    100,	//최대 스레드 개수
    120L	//놀고 있는 시간
    TimeUnit.SECONDS,	//놀고 있는 시간 단위
    new SynchronousQueue<Runnable>()	//작업 큐
)

스레드 풀 종료

스레드풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아 있다.
스레드풀의 모든 스레드를 종료하려면 ExecutorService 의 다음 두 메소드 중 하나를 실행해야 한다.

리턴 타입메소드명(매개변수)설명
voidshutdown()현재 처리 중인 작업뿐만 아니라 작업 큐에 대기하고 있는 모든 작업을 처리한 뒤에 스레드풀을 종료시킨다.
List<Runnable>shutdownNow()현재 작업 처리 중인 스레드를 interrupt 해서 작업을 중지시키고 스레드 풀을 종료시킨다. 리턴값은 작업 큐에 있는 미처리된 작업(Runnable)의 목록이다.

작업 생성과 처리 요청

하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현한다.
Runnable과 Callable 의 차이점은 작업 처리 완료 후 리턴값이 있느냐 없느냐이다.

  • Runnable 익명 구현 클래스
new Runnable() {
	@Override
    public void run() {
    	//스레드가 처리할 작업 내용
    }
}
  • Callable 익명 구현 클래스
new Callable<T> {
	@Override
    public T call() throws Exception {
    	//스레드가 처리할 작업 내용
        return T;
    }
}

작업 처리 요청이란 ExecutorService의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위를 말한다.

작업 처리 요청을 위해 ExecutorService 는 다음 두 가지 메소드를 제공한다.

리턴 타입메소드명(매개변수)설명
voidexecute(Runnable command)- Runnable 을 작업 큐에 저장
- 작업 처리 결과를 리턴하지 않음
Future<T>submit(Callable<T> task)- Callable 을 작업 큐에 저장
- 작업 처리 결과를 얻을 수 있도록 Future를 리턴

문제

  1. 스레드에 대한 설명 중 틀린 것은 무엇입니까?
    ➊ 자바 애플리케이션은 메인(main) 스레드가 main() 메소드를 실행시킨다.
    ➋ 작업 스레드 클래스는 Thread 클래스를 상속해서 만들 수 있다.
    ➌ Runnable 객체는 스레드가 실행해야 할 코드를 가지고 있는 객체라고 볼 수 있다.
    ➍ 스레드 실행을 시작하려면 run() 메소드를 호출해야 한다.
  • 답 : ➍
  1. 동영상과 음악을 재생하기 위해 두 가지 스레드를 실행하려고 합니다. 밑줄 친 부분에 적당한 코
    드를 작성해보세요.
public class ThreadExample {
	public static void main(String[] args) {
 		Thread thread1 = new MovieThread();
 		thread1.start();

 		Thread thread2 = new Thread(__________________________________);
 		thread2.start();
	}
}
public class MovieThread ______________________________________ {
	@Override
	public void run() {
 		for(int i=0;i<3;i++) {
 			System.out.println("동영상을 재생합니다.");
 			try {
 				Thread.sleep(1000);
 			} catch (InterruptedException e) {
 			}
 		}
	}
}
public class MusicRunnable _____________________________________ {
	@Override
	public void run() {
 		for(int i=0;i<3;i++) {
 			System.out.println("음악을 재생합니다.");
 			try {
 				Thread.sleep(1000);
 			} catch (InterruptedException e) {
 			}
 		}
	}
}
  • 답 :
new MusicRunnable()
extends Thread
implements Runnable
  1. 동기화 메소드와 동기화 블록에 대한 설명 중 틀린 것은 무엇입니까?
    ➊ 동기화 메소드와 동기화 블록은 싱글(단일) 스레드 환경에서는 필요 없다.
    ➋ 스레드가 동기화 메소드를 실행할 때 다른 스레드는 일반 메소드를 호출할 수 없다.
    ➌ 스레드가 동기화 메소드를 실행할 때 다른 스레드는 동기화 메소드를 호출할 수 없다.
    ➍ 스레드가 동기화 블록을 실행할 때 다른 스레드는 동기화 메소드를 호출할 수 없다.
  • 답 : ➋
  1. 스레드 일시 정지 상태에 대한 설명 중 틀린 것은 무엇입니까?
    ➊ sleep() 메소드는 주어진 시간 동안 스레드가 일시 정지 상태가 된다.
    ➋ 스레드가 동기화 메소드를 실행할 때 다른 스레드가 동기화 메소드를 호출하게 되면 일시 정지 상태가 된다.
    ➌ 동기화 메소드 내에서 wait() 메소드를 호출하면 현재 스레드가 일시 정지 상태가 된다.
    ➍ yield() 메소드를 호출하면 현재 스레드가 일시 정지 상태가 된다.
  • 답 : ➍
  1. interrupt() 메소드를 호출한 효과에 대한 설명 중 틀린 것은 무엇입니까?
    ➊ 일시 정지 상태에서 InterruptedException을 발생시킨다.
    ➋ 스레드를 즉시 종료한다.
    ➌ 스레드가 일시 정지 상태가 될 때까지 InterruptedException이 발생하지 않는다.
    ➍ InterruptedException이 발생하지 않았다면 isInterrupted() 메소드는 true를 리턴한다.
  • 답 : ➋
  1. 메인 스레드에서 3초 후 MovieThread의 interrupt() 메소드를 호출해서 MovieThread를 안전하게 종료하고 싶습니다. 비어있는 부분에 적당한 코드를 작성해보세요.
  • 답 :
if(this.isInterrupted()) {
	break;
}
  1. wait()와 notify() 메소드에 대한 설명 중 틀린 것은 무엇입니까?
    ➊ 스레드가 wait()를 호출하면 일시 정지 상태가 된다.
    ➋ notify()를 호출하면 wait()로 일시 정지 상태에 있던 스레드가 실행 대기 상태가 된다.
    ➌ wait()와 notify()는 동기화 메소드 또는 블록에서 호출할 필요가 없다.
    ➍ wait()와 notify()는 두 스레드가 균등하게 번갈아 가면서 실행할 때 사용할 수 있다.
  • 답 : ➌
  1. 3초 뒤에 메인 스레드가 종료하면 MovieThread도 같이 종료되게 만들고 싶습니다. 밑줄 친 부분에 적당한 코드를 넣어보세요.
  • 답 :
thread.setDaemon(true);
  1. while 문으로 반복적인 작업을 하는 스레드를 종료시키는 방법에 대한 설명 중 최선의 방법이 아
    닌 것은 무엇입니까?
    ➊ 조건식에 boolean 타입의 stop 플래그를 이용해서 while 문을 빠져나가게 한다.
    ➋ 스레드가 반복적으로 일시 정지 상태가 된다면 InterruptedException을 발생시켜 예외 처리 코드에서
    break 문으로 while 문을 빠져나가게 한다.
    ➌ 스레드가 일시 정지 상태로 가지 않는다면 isInterrupted()나 interrupted() 메소드의 리턴값을 조사
    해서 true일 경우 break 문으로 while 문을 빠져나가게 한다.
    ➍ stop() 메소드를 호출한다.
  • 답 : ➍
  1. 스레드풀에 대한 설명 중 틀린 것은 무엇입니까?
    ➊ 갑작스러운 작업의 증가로 스레드의 폭증을 막기 위해 사용된다.
    ➋ ExecutorService 객체가 스레드풀이며 newFixedThreadPool() 메소드로 얻을 수 있다.
    ➌ 작업은 Runnable 또는 Callable 인터페이스를 구현해서 정의한다.
    ➍ execute() 메소드로 작업 처리 요청을 하면 작업이 완료될 때까지 대기(블로킹)된다.
  • 답 : ➍
profile
사는게 쉽지가 않네요

0개의 댓글