운영체제는 실행 중인 프로그램을 프로세스로 관리한다. 멀티 태스킹은 두 가지 이상의 작업을 동시에 처리하는 것을 말하는데, 이때 운영체제는 멀티 프로세스를 생성해서 처리한다. 하지만 멀티 태스킹이 꼭 멀티 프로세스를 뜻하지는 않는다.
하나의 프로세스 내에서 멀티 태스킹을 할 수 있도록 만들어진 프로그램들도 있다. 예를 들어 메신저는 채팅 작업을 하면서 동시에 파일 전송 작업을 수행하기도 한다.
하나의 프로세스가 두 가지 이상의 작업을 처리할 수 있는 이유는 멀티 스레드가 있기 때문이다. 스레드는 코드의 실행 흐름을 말하는데, 프로세스 내에 스레드가 두 개라면 두 개의 코드 실행 흐름이 생긴다는 의미이다.
멀티 프로세스가 프로그램 단위의 멀티 태스킹이라면 멀티 스레드는 프로그램 내부에서의 멀티 태스킹이라고 볼 수 있다.

멀티 프로세스들은 서로 독립적이므로 하나의 프로세스에서 오류가 발생해도 다른 프로세스에게 영향을 미치지 않는다. 하지만 멀티 스레드는 프로세스 내부에서 생성되기 때문에 하나의 스레드가 예외를 발생시키면 프로세스가 종료되므로 다른 스레드에게 영향을 미친다.
예를 들어 워드와 엑셀을 동시에 사용하는 도중에 워드에 오류가 생겨 먹통이 되더라도 엑셀은 여전히 사용 가능하다. 그러나 멀티 스레드로 동작하는 메신저의 경우, 파일을 전송하는 스레드에서 예외가 발생하면 메신저 프로세스 자체가 종료되기 때문에 채팅 스레드도 같이 종료된다. 그렇기 때문에 멀티 스레드를 사용할 경우에는 예외 처리에 만전을 기해야 한다.
멀티 스레드는 데이터를 분할해서 병렬로 처리하는 곳에서 사용하기도 하고, 안드로이드 앱에서 네트워크 통신을 하기 위해 사용하기도 한다. 또한 다수의 클라이언트 요청을 처리하는 서버를 개발할 때에도 사용된다.
모든 자바 프로그램은 메인 스레드가 main( ) 메소드를 실행하면서 시작된다.
메인 스레드는 main( ) 메소드의 첫 코드부터 순차적으로 실행하고, main( ) 메소드의 마지막 코드를 실행하거나 return문을 만나면 실행을 종료한다.
메인 스레드는 필요에 따라 추가 작업 스레드들을 만들어서 실행시킬 수 있다. 다음 그림에서 오른쪽의 멀티 스레드를 보면 메인 스레드가 작업 스레드1을 생성하고 실행시킨 다음, 곧이어 작업 스레드2를 생성하고 실행시키는 것을 볼 수 있다.

싱글 스레드에서는 메인 스레드가 종료되면 프로세스도 종료된다. 하지만 멀티 스레드에서는 실행중인 스레드가 하나라도 있다면 프로세스는 종료되지 않는다. 메인 스레드가 작업 스레드보다 먼저 종료되더라도 작업 스레드가 계속 실행 중이라면 프로세스는 종료되지 않는다.
멀티 스레드로 실행하는 프로그램을 개발하려면 먼저 몇 개의 작업을 병렬로 실행할지 결정하고 각 작업별로 스레드를 생성해야 한다.
자바 프로그램은 메인 스레드가 반드시 존재하기 때문에 메인 작업 이외에 추가적인 작업 수만큼 스레드를 생성하면 된다. 자바는 작업 스레드도 객체로 관리하므로 클래스가 필요하다. Thread 클래스로 직접 객체를 생성해도 되지만, 하위 클래스를 만들어 생성할 수도 있다.
java.lang 패키지에 있는 Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면 다음과 같이 Runnable 구현 객체를 매개값으로 갖는 생성자를 호출하면 된다.
Thread thread = new Thread(Runnable target);
Runnable은 스레드가 작업을 실행할 때 사용하는 인터페이스이다. Runnable에는 run( ) 메소드가 정의되어 있는데, 구현 클래스는 run( )을 재정의해서 스레드가 실행할 코드를 가지고 있어야 한다. 다음은 Runnable 구현 클래스를 작성하는 방법이다.
class Task implements Runnable {
@Override
public void run(){
//스레드가 실행할 코드
}
}
Runnable 구현 클래스는 작업 내용을 정의한 것이므로, 스레드에게 전달해야 한다. Runnable 구현 객체를 생성한 후 Thread 생성자 매개값으로 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();
start( ) 메소드가 호출되면, 작업 스레드는 매개값으로 받은 Runnable의 run( ) 메소드를 실행하면서 작업을 처리한다. 다음은 작업 스레드가 생성되고 실행되기까지의 순서를 보여준다.

다음은 메인 스레드가 동시에 두 가지 작업을 처리할 수 없음을 보여주는 예제이다. 원래 목적은 0.5초 주기로 비프음을 발생시키면서 동시에 프린팅까지 하는 작업이었지만, 메인 스레드는 비프음을 모두 발생한 다음에야 프린팅을 시작한다.
package ch14.sec03.exam01;
import java.awt.Toolkit;
public class BeepPrintExample {
public static void main(String[] args) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 5; i++) {
toolkit.beep();
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
}
실행결과
---------
(비프음 5번 실행됨)
띵
띵
띵
띵
띵
원래 목적대로 0.5초 주기로 비프음을 발생시키면서 동시에 프린팅을 하고 싶다면 두 작업 중 하나를 작업 스레드에서 처리하도록 해야 한다. 이제 프린팅은 메인 스레드가 담당하고 비프음을 들려주는 것은 작업 스레드가 담당하도록 수정해보자.
package ch15.thread01.ch14.sec03.exam02;
import java.awt.Toolkit;
public class BeepPrintExample {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 5; i++) {
toolkit.beep();
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
});
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
}
실행 결과
(비프음과 문자 출력이 동시에 실행됨)
띵
띵
띵
띵
띵
작업 스레드 객체를 생성하는 또 다른 방법은 Thread의 자식 객체로 만드는 것이다. Thread 클래스를 상속한 다음 run( ) 메소드를 재정의해서 스레드가 실행할 코드를 작성하고 객체를 생성하면 된다.
public class WorkerThread extends Thread {
@Override
public void run() {
//스레드가 실행할 코드
}
}
//스레드 객체 생성
Thread thread = new WorkerThread();
작업 스레드를 실행하는 방법은 동일하다. start( )메소드를 호출하면 작업 스레드는 재정의된 run( )을 실행시킨다.
thread.start();

명시적인 자식 클래스를 정의하지 않고, 다음과 같이 Thread 익명 자식 객체를 사용할 수도 있다. 오히려 이 방법이 더 많이 사용된다.
Thread thread = new Thread() {
@Override
public void run(){
//스레드가 실행할 코드
}
};
thread.start();
다음은 Thread의 익명 자식 객체로 작업 스레드를 정의하고 비프음을 실행하도록 이전 예제를 수정 한 것이다.
package ch15.thread01.ch14.sec03.exam03;
import java.awt.Toolkit;
public class BeepPrintExample {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
Toolkit toolkit = Toolkit.getDefaultToolkit();
for (int i = 0; i < 5; i++) {
toolkit.beep();
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
};
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("띵");
try {
Thread.sleep(500);
} catch (Exception e) {
}
}
}
}
실행 결과
(비프음과 문자 출력이 동시에 실행됨)
띵
띵
띵
띵
띵
스레드는 자신의 이름을 가지고 있다. 메인 스레드는 'main'이라는 이름을 가지고 있고, 작업 스레드는 자동적으로 'Thread-n'이라는 이름을 가진다. 작업 스레드의 이름을 Thread-n 대신 다른 이름으로 설정하고 싶다면 Thread 클래스의 setName( ) 메소드를 사용하면 된다.
thread.setName("Thread Name");
스레드 이름은 디버깅할 때 어떤 스레드가 작업을 하는지 조사할 목적으로 주로 사용된다. 현재 코드를 어떤 스레드가 실행하고 있는지 확인하려면 정적 메소드인 currentThread( )로 스레드 객체의 참조를 얻은 다음 getName( ) 메소드로 이름을 출력해보면 된다.
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
다음은 현재 실행 중인 스레드의 참조를 얻어 이름을 콘솔에 출력하고, 작업 스레드의 이름을 setName( ) 메소드로 수정하는 방법을 보여준다.
package ch15.thread01.ch14.sec04;
public class ThreadNameExample {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
System.out.println(mainThread.getName() + " 실행");
for (int i = 0; i < 3; i++) {
Thread threadA = new Thread() {
@Override
public void run() {
System.out.println(getName() + " 실행");
}
};
threadA.start();
}
Thread chatThread = new Thread() {
public void run() {
System.out.println(getName() + " 실행");
}
};
chatThread.setName("chat-thread");
chatThread.start();
}
}
실행 결과
main 실행
Thread-0 실행
Thread-1 실행
Thread-2 실행
chat-thread 실행
스레드 객체를 생성(NEW)하고, start( ) 메소드를 호출하면 곧바로 스레드가 실행되는 것이 아니라 실행 대기 상태(RANNABLE)가 된다. 실행 대기 상태란 실행을 기다리고 있는 상태다.
실행 대기하는 스레드는 CPU 스케쥴링에 따라 CPU를 점유하고 run( ) 메소드를 실행한다. 이때를 실행(RUNNING)상태라고 한다. 실행 스레드는 run( ) 메소드를 모두 실행하기 전에 스케줄링에 의해 다시 실행 대기 상태로 돌아갈 수 있다. 그리고 다른 스레드가 실행 상태가 된다.
이렇게 스레드는 실행 대기 상태와 실행 상태를 번갈아 가면서 자신의 run( ) 메소드를 조금씩 실행한다. 실행 상태에서 run( ) 메소드가 종료되면 더 이상 실행할 코드가 없기 때문에 스레드의 실행은 멈추게 된다. 이 상태를 종료 상태(TERMINATED)라고한다.

실행 상태에서 일시 정시 상태로 가기도 하는데 일시 정지 상태는 스레드가 실행할 수 없는 상태를 말한다. 스레드가 다시 실행 상태로 가기 위해서는 일시 정지 상태에서 실행 대기 상태로 가야만 한다. 다음은 일시 정지로 가기 위한 메소드와 벗어나기 위한 메소드를 보여준다.


멀티 스레드는 하나의 객체를 공유해서 작업할 수도 있다. 이 경우, 다른 스레드에 의해 객체 내부 데이터가 쉽게 변경될 수 있기 때문에 의도했던 것과는 다른 결과가 나올 수 있다.
public class ExtendsThread2 extends Thread{
public static int memory;
public static void main(String[] args) {
Thread t1 = new Thread() {
@Override
public void run() {
try {
memory = 100;
sleep(2000);
System.out.println("Memory : "+memory);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
try {
memory = 50;
sleep(2000);
System.out.println("Memory : "+memory);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
t1.start();
t2.start();
}
}
실행결과
Memory : 50
Memory : 50
스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없도록 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸면 된다. 이를 위해 자바는 동기화 메소드와 블록을 제공한다.
동기화 메소드를 선언하는 방법은 다음과 같이 synchronized키워드를 붙이면 된다. synchronized 키워드는 인스턴스와 정적 메소드 어디든 붙일 수 있다.
public synchronized void method(){
//단 하나의 스레드만 실행하는 영역
}
스레드가 동기화 메소드를 실행하는 즉시 객채는 잠금이 일어나고, 메소드 실행이 끝나면 잠금이 풀린다. 메소드 전체가 아닌 일부 영역을 실행할 때만 객체 잠금을 걸고 싶다면 다음과 같이 동기화 블록을 만들면 된다.
public void mothod () {
//여러 스레드가 실행할 수 있는 영역
synchrionzed(공유객체) {
//단 하나의 스레드만 실행하는 영역
}
//여러 스레드가 실행할 수 있는 영역
}
다음 예제는 공유 객체로 사용할 Caluator이다. setMemory1( )을 동기화 메소드로, setMemory2( )를 동기화 블록을 포함하는 메소드로 선언했다. 따라서 setmemory1과 setMemory2는 하나의 스레드만 실행 가능한 메소드가 된다.
package ch15.thread01.ch14.sec06.exam01;
public class Calculator {
private int memory;
public int getMemory() {
return memory;
}
public synchronized void setMemory1(int memory) { // 동기화 메소드
this.memory = memory;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}
public void setMemory2(int memory) {
synchronized (this) { // 동기화 블록
this.memory = memory;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {}
System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}
}
}
setMemory1( )과 setMemory2( )는 동일하게 매개값을 메모리에 저장하고, 2초간 일시 정지 후에 메모리값을 출력한다.
다음 예제는 Calculator를 공유해서 사용하는 User1Thread와 User2Thread를 보여준다. run( ) 메소드에서 User1Thread는 매개값 100으로 setMemory1( ) 메소드를 호출하고, User2Thread는 매개값 50으로 setMemory2( ) 메소드를 호출한다.
package ch15.thread01.ch14.sec06.exam01;
public class User1Thread extends Thread{
private Calculator calculator;
public User1Thread() {
setName("User1Thread");
}
public void setCalculator(Calculator calculator) {
this.calculator = calculator;
}
@Override
public void run() {
calculator.setMemory1(100);
}
}
package ch15.thread01.ch14.sec06.exam01;
public class User2Thread extends Thread{
private Calculator calculator;
public User2Thread() {
setName("User2Thread");
}
public void setCalculator(Calculator calculator) {
this.calculator = calculator;
}
@Override
public void run() {
calculator.setMemory2(50);
}
}
다음 예제는 Calculator를 생성해서 User1Thread와 User2Thread에서 사용하도록 setCalculator( ) 메소드를 호출하고, 두 스레드를 시작시킨다.
package ch15.thread01.ch14.sec06.exam01;
public class SynchronizedExample {
public static void main(String[] args) {
Calculator calculator = new Calculator();
User1Thread user1Thread = new User1Thread();
user1Thread.setCalculator(calculator);
user1Thread.start();
User2Thread user2Thread = new User2Thread();
user2Thread.setCalculator(calculator);
user2Thread.start();
}
}
실행결과
User1Thread : 100
User2Thread : 50
경우에 따라서는 두 개의 스레드를 교대로 번갈아 가며 실행할 때도 있다. 정확한 교대 작업이 필요 할 경우, 자신의 작업이 끝나면 상대방 스레드를 일시 정지 상태에서 풀어주고 자신은 일시 정지 상태로 만들면 된다.
이 방법의 핵심은 공유 객체에 있다. 공유 객체는 두 스레드가 작업할 내용을 각각 동기화 메소드로 정해 놓는다. 한 스레드가 작업을 완료하면 notify( ) 메소드를 호출해서 일시 정지 상태에 있는 다른 스레드를 실행 대기 상태로 만들고, 자신은 두 번 작업을 하지 않도록 wait( ) 메소드를 호출하여 일시 정지 상태로 만든다.

notify( )는 wait( )에 의해 일시 정지된 스레드 중 한 개를 실행 대기 상태로 만들고, notifyAll( )은 wait( )에 의해 일시 정지된 모든 스레드를 실행 대기 상태로 만든다. 주의할 점은 이 두 메소드는 동기화 메소드 또는 동기화 블록 내에서만 사용할 수 있다는 것이다.
다음 예제는 WorkObject에 두 스레드가 해야 할 작업을 동기화 메소드인 methodA( )와 methodB( )로 각각 정의해 두고, ThreadA와 ThreadB가 교대로 methodA( )와 methodB( )를 호출하도록 한 것이다.
package ch15.thread01.ch14.sec06.exam02;
public class WorkObject {
public synchronized void methodA() { // 동기화 메소드
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+ ": methodA 작업 실행");
notify(); // 다른 스레드를 실행 대기 상태로 만듦
try {
wait(); // 자신의 스레드는 일시 정지 상태로 만듦
} catch (Exception e) {
// TODO: handle exception
}
}
public synchronized void methodB() { // 동기화 메소드
Thread thread = Thread.currentThread();
System.out.println(thread.getName()+ ": methodB 작업 실행");
notify(); // 다른 스레드를 실행 대기 상태로 만듦
try {
wait(); // 자신의 스레드는 일시 정지 상태로 만듦
} catch (Exception e) {
// TODO: handle exception
}
}
}
package ch15.thread01.ch14.sec06.exam02;
public class ThreadA extends Thread{
private WorkObject workObjedt;
public ThreadA(WorkObject workObject) { //공유 작업 객체를 받음
setName("ThreadA"); //스레드 이름 변경
this.workObjedt = workObject;
}
@Override
public void run() {
for(int i=0;i<10;i++) {
workObjedt.methodA(); // 동기화 메소드 호출
}
}
}
package ch15.thread01.ch14.sec06.exam02;
public class ThreadB extends Thread{
private WorkObject workObjedt;
public ThreadB(WorkObject workObject) { // 공유 작업 객체를 받음
setName("ThreadB"); // 스레드 이름 변경
this.workObjedt = workObject;
}
@Override
public void run() {
for(int i=0;i<10;i++) {
workObjedt.methodB(); // 동기화 메소드 호출
}
}
}
package ch15.thread01.ch14.sec06.exam02;
public class WaitNotifyExample {
public static void main(String[] args) {
WorkObject workObject = new WorkObject(); // 공유 작업 객체 생성
//------------------------------------------
ThreadA threadA = new ThreadA(workObject);
ThreadB threadB = new ThreadB(workObject);
threadA.start();
threadB.start();
//-------------------------------------------
//작업 스레드 생성 및 실행
}
}
실행 결과
ThreadA : methodA 작업 실행
ThreadB : methodB 작업 실행
ThreadA : methodA 작업 실행
ThreadB : methodB 작업 실행
...
스레드는 자신의 run( ) 메소드가 모두 실행되면 자동적으로 종료되지만, 경우에 따라서는 실행 중인 스레드를 즉시 종료할 필요가 있다. 예를 들어 동영상을 끝까지 보지 않고 사용자가 멈춤을 요구하는 경우이다.
스레드를 강제 종료시키기 위해 Thread는 stop( ) 메소드를 제공하고 있으나. 이 메소드는 deprecated(더 이상 사용하지 않음)되었다. 그 이유는 스레드를 갑자기 종료하게 되면 사용 중이던 리소스들이 불안전한 상태로 남겨지기 때문이다. 여기에서 리소스란 파일, 네트워크 연결을 말한다.
스레드를 안전하게 종료하는 방법은 사용하던 리소스들을 정리하고 run( ) 메소드를 빨리 종료하는 것이다. 주로 조건 이용 방법과 interrupt( ) 메소드 이용 방법을 사용한다.
스레드가 while문으로 반복 실행할 경우, 조건을 이용해서 run( ) 메소드의 종료를 유도할 수 있다. 다음 코드는 stop필드 조건에 따라서 run( ) 메소드의 종료를 유도한다.
public class XXXThread extends Thread {
private boolean stop;
public void run() {
while( !stop ){
//스레드가 반복 실행하는 코드;
}
//스레드가 사용한 리소스 정리
}
}
다음 예제는 메인 스레드에서 3초 후에 stop 필드값을 true로 설정해서 PrintThread을 종료한다.
package ch15.thread01.ch14.sec07.exam01;
public class PrintThread extends Thread{
private boolean stop;
public void setStop(boolean stop) {
this.stop = stop;
}
/*
* [스레드를 종료하는 1번째 방법]
* 계속 동작해야 하는 스레드에서 while문을 종종 사용한다.
* 이 스레드를 종료하기 위해서는 아래처럼 boolean stop을 사용하면
* stop이 true일 때 while문은 더 이상 반복되지 않고 종료
*/
@Override
public void run() {
while(!stop){
System.out.println("실행중");
}
System.out.println("리소스 정리");
System.out.println("실행종료");
}
}
package ch15.thread01.ch14.sec07.exam01;
public class SafeStipExample {
public static void main(String[] args) {
PrintThread printThread =new PrintThread();
printThread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) { }
printThread.setStop(true);
//PrintThread를 종료하기 위해 stop 필드값 변경
}
}
실행결과
...
실행중
실행중
리소스 정리
실행종료
interrupt( ) 메소드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 예외를 발생시키는 역할을 한다. 이것을 이용하면 예외 처리를 통해 run( ) 메소드를 정상 종료시킬 수 있다. 다음 그림을 보자

XThread를 생성해서 start( ) 메소드를 실행한 후에 XThread의 interrupt( ) 메소드를 실행하면 XThread가 일시 정지 상태가 될 때 InterruptedException이 발생하여 예외 처리 블록으로 이동한다. 이것은 결국 while문을 빠져나와 자원을 정리하고 스레드가 종료되는 효과를 가져온다.
다음 에제는 interrupt( ) 메소드를 이용하여 PrintThread를 종료한다.
package ch15.thread01.ch14.sec07.exam02;
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("실행종료");
}
}
package ch15.thread01.ch14.sec07.exam02;
public class InterruptExample {
public static void main(String[] args) {
Thread thread = new PrintThread();
thread.start();
try {
thread.sleep(1000);
} catch (InterruptedException e) {
// TODO: handle exception
}
thread.interrupt(); // interrupt() 메소드 호출
}
}
실행 결과
...
실행중
실행중
리소스 정리
실행종료
스레드가 실행 대기/실행 상태일 때에는 interrupt( ) 메소드가 호출되어도 InterruptedException이 발생하지 않는다. 그러나 스레드가 어떤 이유로 일시 정지 상태가 되면, InterruptedException예외가 발생한다. 그래서 짧은 시간이나마 일시 정지를 위해 Thread.sleep(1)을 사용한 것이다.
일시 정지를 만들지 않고도 interrupt( ) 메소드 호출 여부를 알 수 있는 방법이 있다. Thread의 interrupted( )와 isInterrupted( )메소드는 interrupt( ) 메소드 호출 여부를 리턴한다. interrupted( )는 정적 메소드이고, isInterrpted( )는 인스턴스 메소드이다.
boolean status = Thread.interrupted();
boolean status = objThread.isInterrupted();
다음 예제는 Thread.interrupted( )를 사용해서 while문을 빠져나간 예제이다.
package ch15.thread01.ch14.sec07.exam03;
public class PrintThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println("실행중");
if(Thread.interrupted()) {
break;
}
}
System.out.println("리소스 정리");
System.out.println("실행종료");
}
}
package ch15.thread01.ch14.sec07.exam03;
public class InterruptExample {
public static void main(String[] args) {
Thread thread = new PrintThread();
thread.start();
try {
thread.sleep(1000);
} catch (InterruptedException e) {
// TODO: handle exception
}
thread.interrupt();
}
}
...
실행중
실행중
리소스 정리
실행종료
데몬 스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드이다. 주 스레드가 종료되면 데몬 스레드도 따라서 자동으로 종료된다.
데몬 스레드를 적용한 예로는 워드프로세서의 자동 저장, 미디어플레이어의 동영상 및 음악 재생, 가비지 컬렉터 등이 있는데, 여기에서 주 스레드(워드프로세스, 미디어플레이어, JVM)가 종료되면 데몬 스레드도 같이 종료된다.
스레드를 데몬으로 만들기 위해서는 주 스레드가 데몬이 될 스레드의 setDaemon(true)를 호출 하면 된다. 다음 예를 보면 메인 스레드는 주 스레드, AutoSaveThread는 데몬 스레드가 된다.
public static void main(String[] args){
AutoSaveThread thread = new AutoSaveThread();
thread.setDaemon(true);
thread.start();
...
}
다음 예제는 1초 주기로 save( ) 메소드를 호출하는 AutoSaveThread를 데몬 스레드로 실행시킨다. 그리고 메인 스레드가 3초 후 종료되면 AutoSaveThread도 따라서 자동 종료된다.
package ch15.thread01.ch14.sec08;
public class AutoSaveThread extends Thread{
public void save() {
System.out.println("작업 내용을 저장함");
}
public void run() {
while(true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
save();
}
}
}
package ch15.thread01.ch14.sec08;
public class DemonExample {
public static void main(String[] args) {
AutoSaveThread autoSaveThread = new AutoSaveThread();
/* C/C++는 원래 데몬 스레드이다
* C#/Java는 위의 속성을 없애기 위한 별도의 동기화작업이 필요하다.
* 그래서 불편해서 그냥 별개로 동작하도록 만들었고 아래처럼 setDaemon(true)
* 일 때 종속적이도록 만들었다.
*
* 데몬 스레드(종속 스레드)
* 부모 스레드가 종료되면 자식도 함께 종료
* 일반적으로는 자식 스레드는 부모 스레드와 별개로 동작한다.
*/
autoSaveThread.setDaemon(true);
autoSaveThread.start();
try {
Thread.sleep(3000);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println("메인 스레드 종료");
}
}
실행결과
작업 내용을 저장함
작업 내용을 저장함
메인 스레드 종료
병렬 작업 처리가 많아지면 스레드의 개수가 폭증하여 CPU가 바빠지고 메모리 사용량이 늘어난다. 이에 따라 애플리케이션의 성능 또한 급격히 저하된다. 이렇게 병렬 작업 증가로 인한 스레드의 폭증을 막으려면 스레드 풀을 사용하는 것이 좋다.
스레드풀은 작업 처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐에 들어오는 작업들을 스레드가 하나씩 맡아 처리하는 방식이다. 작업 처리가 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리한다. 이렇게 하면 작업량이 증가해도 스레드의 개수가 늘어나지 않아 애플리케이션의 성능이 급격히 저하되지 않는다.

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

초기 수는 스레드풀이 생성될 때 기본적으로 생성되는 스레드 수를 말하고, 코어 수는 스레드가 증가된 후 사용되지 않는 스레드를 제거할 때 최소한 풀에서 유지하는 스레드 수를 말한다. 그리고 최대 수는 증가되는 스레드의 한도 수이다.
다음과 같이 newCachedThreadPool( ) 메소드로 생성된 스레드풀의 초기 수와 코어 수는 0개이고, 작업 개수가 많아지면 새 스레드를 생성시켜 작업을 처리한다. 60초 동안 스레드가 아무 작업을 하지 않으면 스레드 풀에서 제거한다.
ExecutorService executorService = Executors.newCachedThreadPool();
다음과 같이 newFixedThreadPool( )로 생성된 스레드 풀의 초기 수는 0개이고, 작업 개수가 많아지면 최대 5개까지 스레드를 생성시켜 작업을 처리한다. 이 스레드풀의 특징은 생성된 스레드를 제거하지 않는다는 것이다.
ExecutorService executorService = Executors.newCachedThreadPool(5);
위 두 메소드를 사용하지 않고 직접 ThreadPoolExecutor로 스레드풀을 생성할 수도 있다. 다음 예시는 초기 수 0개, 코어 수 3개, 최대 수 100개인 스레드풀을 생성하는 코드이다. 그리고 추가된 스레드가 120초 동안 놀고 있을 경우 해당 스레드를 풀에서 제거한다.
ExecutorService threadPool = new ThreadPoolExecutor(
3, //코어 스레드 개수
100, //최대 스레드 개수
120L, //놀고 있는 시간
TimeUnit.SECONDS, //놀고 있는 시간 단위
new SynchroonousQueue<Runnable>() //작업 큐
);
스레드풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아 있다. 스레드풀의 모든 스레드를 종료하려면 ExecutorService의 다음 두 메소드 중 하나를 실행해야 한다.

남아있는 작업을 마무리하고 스레드풀을 종료할 때에는 shutdown( ) 을 호출하고, 남아있는 작업과는 상관업이 강제로 종료할 때에는 shutdwonNow( )를 호출하면 된다. 다음 예제는 최대 5개의 스레드로 운영되는 스레드풀을 생성하고 종료한다.
package ch15.thread01.ch14.sec09.exam01;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorServiceExample {
public static void main(String[] args) {
//스레드풀 생성(5개 스레드 생성)
ExecutorService executorService = Executors.newFixedThreadPool(5);
//작업 생성과 처리 요청
//스레드풀 종료
executorService.shutdown();
}
}
위 예제는 작업 생성과 처리 요청 코드가 없기 때문에 10라인을 실행하지 않아도 프로세스가 종료된다. 그 이유는 스레드풀에 생성된 스레드가 없기 때문이다.
하나의 작업은 Runnable 또는 Callable 구현 객체로 표현한다. Runnable과 Callable의 차이점은 작업 처리 완료 후 return 값이 있느냐 없느냐이다. 다음은 Runnable과 Callable 구현 객체를 작성하는 방법을 보여준다.

Runnable의 run( ) 메소드는 리턴값이 없고, Callable의 call( ) 메소드는 리턴값이 있다. call( )의 리턴 타입은 Callable<T>에서 지정한 T 타입 파라미터와 동일한 타입이어야 한다.
작업 처리 요청이란 ExecutorService의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위를 말한다. 작업 처리 요청을 위해 ExecutorService는 다음 두 가지 메소드를 제공한다.

Runnable 또는 Callable 객체가 ExecutorService의 작업 큐에 들어가면 ExecutorService는 처리할 스레드가 있는지 보고, 없다면 스레드를 새로 생성시킨다. 스레드는 작업 큐에서 Runnable 또는 Callable 객체를 꺼내와 run( ) 또는 call( ) 메소드를 실행하면서 작업을 처리한다.
다음 예제는 이메일을 보내는 작업으로, 1000개의 Runnable을 생성한 다음 execute( ) 메소드로 작업 큐에 넣는다. ExecutorService는 최대 5개 스레드로 작업 큐에서 Runnable을 하나씩 꺼내어 run( ) 메소드를 실행하면서 작업을 처리한다.
package ch15.thread01.ch14.sec09.exam02;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class RunnableExecuteExample {
public static void main(String[] args) {
//기본 1차원 배열 3개까지라 1000개 층으로 쌓여있다.
//1000개의 메일 생성
String[][] mails = new String[1000][3];
for(int i=0; i<mails.length;i++) {
mails[i][0] = "admin@my.com";
mails[i][1] = "member"+i+"@my.com";
mails[i][2] = "신상품 입고";
}
//ExecutorService 생성
//스레드풀은 5개의 스레드를 보유하고 있다.
ExecutorService executorService = Executors.newFixedThreadPool(5);
//이메일을 보내는 작업 생성 및 처리 요청
for(int i=0; i<1000; i++) {
final int idx = i;
executorService.execute(new Runnable() {
@Override
public void run() {
Thread thread = Thread.currentThread();
String from = mails[idx][0];
String to = mails[idx][1];
String content = mails[idx][2];
System.out.println("["+thread.getName()+"]"+
from + " ==> "+to+": "+content);
}
});
}
//ExecutorService 종료
executorService.shutdown();
}
}
실행결과
[pool-1-thread-5]admin@my.com ==> member4@my.com: 신상품 입고
[pool-1-thread-2]admin@my.com ==> member1@my.com: 신상품 입고
[pool-1-thread-4]admin@my.com ==> member3@my.com: 신상품 입고
[pool-1-thread-5]admin@my.com ==> member5@my.com: 신상품 입고
[pool-1-thread-4]admin@my.com ==> member6@my.com: 신상품 입고
[pool-1-thread-5]admin@my.com ==> member7@my.com: 신상품 입고
[pool-1-thread-4]admin@my.com ==> member8@my.com: 신상품 입고
[pool-1-thread-5]admin@my.com ==> member9@my.com: 신상품 입고
.....
다음 예제는 자연수를 뎃셈하는 작업으로, 100개의 Callable을 생성하고 submit( ) 메소드로 작업 큐에 넣는다. ExecutorService는 최대 5개 스레드로 작업 큐에서 Callable을 하나씩 꺼내어 call( ) 메소드를 실행하면서 작업을 처리한다. Future의 get( ) 메소드는 작업이 끝날때까지 기다렸다가 call( ) 메소드가 리턴한 값을 리턴한다.
package ch15.thread01.ch14.sec09.exam03;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableSubmitExample {
public static void main(String[] args) {
//ExecutorService 생성
ExecutorService executorService = Executors.newFixedThreadPool(5);
//계산 작업 생성 및 처리 요청
for(int i=0; i<=100;i++) {
final int idx = i;
Future<Integer> future = executorService.submit(new Callable<Integer>() {
public Integer call() throws Exception{
int sum = 0;
for(int i=0;i<=idx;i++) {
sum +=i;
}
Thread thread = Thread.currentThread();
System.out.println("["+thread.getName()+"] 1~"+idx+" 합 계산");
return sum;
}
});
try {
//스레드풀내의 임의의 스레드가 call()을 처리하고 난 결과를 아래처럼 받는다.
int result = future.get();
System.out.println("\t리턴값: "+result);
} catch (Exception e) {
e.printStackTrace();
}
}
//ExecutorService 종료
executorService.shutdown();
}
}
실행결과
...
...
[pool-1-thread-4] 1~98 합 계산
리턴값: 4851
[pool-1-thread-5] 1~99 합 계산
리턴값: 4950
[pool-1-thread-1] 1~100 합 계산
리턴값: 5050