본문 바로가기

Java 길찾기/이것이 자바다

[Java] 작업 스레드 생성과 실행

멀티 스레드로 실행하는 애플리케이션을 개발하려면 먼저 몇 개의 작업을 병렬로 실행할지 결정하고 각 작업별로 스레드를 생성해야 한다.

어떤 자바 애플리케이션이건 메인 스레드는 반드시 존재하기 때문에 메인 작업 이외에 추가적인 병렬 작업의 수만큼 스레드를 생성하면 된다. 자바에서는 작업 스레드도 객체로 생성되기 때문에 클래스가 필요하다. java.lang.Thread 클래스를 직접 객체화해서 생성해도 되지만, Thread를 상속해서 하위 클래스를 만들어 생성할 수도 있다.

 

Thread 클래스로부터 직접 생성

java.lang.Thread 클래스로부터 작업 스레드 객체를 직접 생성하려면 다음과 같이 Runnable을 매개값으로 갖는 생성자를 호출해야 한다.

Thread thread = new Thread(Runnable target);

 

Runnable은 작업 스레드가 실행할 수 있는 코드를 가지고 있는 객체라고 해서 붙여진 이름이다. Runnable은 인터페이스 타입이기 때문에 구현 객체를 만들어 대입해야 한다. Runnable에는 run() 메소드 하나가 정의되어 있는데, 구현 클래스는 run()을 오버라이딩해서 작업 스레드가 실행할 코드를 작성해야 한다. 다음은 Runnable 구현 클래스를 작성하는 방법을 보여준다.

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

 

Runnable은 작업 내용을 가지고 있는 객체이지 실제 스레드는 아니다. Runnable 구현 객체를 생성한 후, 이것을 매개값으로 해서 Thread 생성자를 호출하면 비로소 작업 스레드가 생성된다.

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

 

코드를 좀 더 절약하기 위해 Thread 생성자를 호출할 때 Runnable 익명 객체를 매개값으로 사용할 수 있다. 오히려 이 방법이 더 많이 사용된다.

Thread therad = new Thread(new Runnable() {
    public void run() {
        스레드가 실행할 코드;
    }
});

 

Runnable 인터페이스는 run() 메소드 하나만 정의되어 있기 때문에 함수적 인터페이스이다. 따라서 다음과 같은 람다식을 매개값으로 사용할 수도 있다. 람다식은 뒤에서 설명하니 여기서는 그냥 넘어가도록 하자.

Trhead thread = new Thread( () -> {
    스레드가 실행할 코드;
});

 

작업 스레드는 생성되는 즉시 실행되는 것이 아니라, start() 메소드를 다음과 같이 호출해야만 비로소 실행된다.

thread.start();

 

start() 메소드가 호출되면, 작업 스레드는 매객밧으로 받은 Runnable의 run() 메소드를 실행하면서 자신의 작업을 처리한다.

 

0.5초 주기로 비프(beep)음을 발생시키면서 동시에 프린팅하는 작업이 있다고 가정해보자. 비프음 발생과 프린팅은 서로 다른 작업이므로 메인 스레드가 동시에 두 가지 작업을 처리할 수 없다. 만약 다음과 같이 작성했다면 메인 스레드는 비프음을 모두 발생한 다음, 프린팅을 시작한다.

// BeepPrintExample1.java -- 메인 스레드만 이용한 경우
import java.awt.*;

public class BeepPrintExample1 {
    public static void main(String[] args) {
        Toolkit toolkit = Toolkit.getDefaultToolkit)_;
        for(int i=0; i<5; i++) {
            tollkit.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) {}
        }
    }
}
// BeepTask.java -- 비프음을 들려주는 작업 정의
public class BeepTask implements Runnable {
    public void run() {
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        for(int i=0; i<5; i++) {
            toolkit.beep();
            try { Thread.sleep(500); } catch(Exception e) {}
        }
    }
}

 

BeepPrintExample1.java 에서 비프음을 발생하는 코드를 다음과 같이 작업 스레드 생성 및 실행 코드로 변경한다.

// BeepPrintExample2.java -- 메인 스레드와 작업 스레드가 동시에 실행
public class BeepPrintExample2 {
    public static void main(String[] args) {
        Runnable beepTask = new BeepTask();
        Thread thread = new Thread(beepTask);
        thread.start();
        
        for(int i=0; i<5; i++) {
            System.out.println("띵");
            try { Thread.sleep(500); } catch(Exception e) {}
        }
    }
}

 

3라인에서 BeepTask 객체를 생성하고, 이것을 매개값으로 해서 4라인에서 작업 스레드를 생성한다. 5라인에서 작업 스레드의 start() 메소드를 호출하면 작업 스레드에 의해 BeepTask 객체의 run() 메소드가 실해오디어 비프음이 발생한다. 그와 동시에 메인 스레드는 7라인의 for문을 실행시켜 0.5초 간격으로 "띵"을 프린팅한다. 다음은 3~4라인을 대체하여 작업 스레드를 만들 수 있는 또 다른 두 가지 방법을 보여준다.

// Runnable 익명 객체 이용
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 thread = new Thread(() -> {
    Toolkit toolkit = Toolkit.getDefaultToolkit();
    for(int i=0; i<5; i++) {
            toolkit.beep();
            try { Thread.sleep(500); } catch(Exception e) {}
        }
    }
});

 

Thread 하위 클래스로부터 생성

작업 스레드가 실행할 작업을 Runnable로 만들지 않고, Thread의 하위 클래스로 작업 스레드를 정의하면서 작업 내용을 포함시킬 수도 있다. 다음은 작업 스레드 클래스를 정의하는 방법인데, Thread 클래스를 상속할 후 run 메소드를 오버라이딩해서 스레드가 실행할 코드를 작성하면 된다. 작업 스레드 클래스로부터 작업 스레드 객체를 생성하는 방법을 일반적인 객체를 생성하는 방법과 동일하다.

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

 

코드를 좀 더 절약하기 위해 다음과 같이 Thread 익명 객체로 작업 스레드 객체를 생성할 수도 있다.

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

 

이렇게 생성된 작업 스레드 객체에서 start() 메소드를 호출하면 작업 스레드는 자신의 run() 메소드를 실행하게 된다.

thread.start();

다음 BeepThread 클래스는 이전 예제를 수정해서 Runnable을 생성하지 않고 Thread의 하위 클래스로 작업 스레드를 정의한 것이다.

// BeepThread.java -- 비프음을 들려주는 스레드
import java.awt.Toolkit;

public class BeepThread extends 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) {}
        }
    }
}

 

다음은 BeepThread 클래스를 이용해서 작업 스레드 객체를 생성하고 실행한다.

// BeepPrintExample3.java -- 메인 스레드와 작업 스레드가 동시에 실행
public class BeepPrintExample3 {
    public static void main(String[] args) {
        Thread thread = new BeepThread();
        thread.start();
        
        for(int i=0; i<5; i++) {
            System.out.println("띵");
            try { Thread.sleep(500; ) catch(Exception e) {}
        }
    }
}

 

3라인에서 BeepThread 객체를 생성하고, 4라인에서 start() 메소드를 호출하여 BeepThread가 run() 메소드를 실행하도록 했다. 그와 동시에 메인 스레드는 6라인의 for문을 실행시켜 0.5초 간격으로 "띵"을 프린팅한다. 다음은 3라인을 대체하여 작업 스레드를 만들 수 있는 또 다른 방법을 보여준다.

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) {}
        }
    }
}

 

스레드의 이름

스레드는 자신의 이름을 가지고 있다. 스레드의 이름이 큰 역할을 하는 것 은 아니지만, 디버깅할 때 어떤 스레드가 어떤 작업을 하는지 조사할 목적으로 가끔 사용된다. 메인 스레드는 "main"이라는 이름을 가지고 있고, 우리가 직접 생성한 스레드는 자동적으로 "Thread-n"이라는 이름으로 설정된다. n은 스레드의 번호를 말한다. Thread-n 대신 다른 이름으로 설정하고 싶다면 Thread 클래스의 setName() 메소드로 변경하면 된다.

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

 

반대로 스레드 이름을 알고 싶을 경우에는 getName() 메소드를 호출하면 된다.

thread.getName();

 

setName()과 getName()은 Thread의 인스턴스 메소드이므로 스레드 객체의 참조가 필요하다. 만약 스레드 객체의 참조를 가지고 있지 않다면, Thread의 정적 메소드인 currentThread()로 코드를 실행하는 현재 스레드의 참조를 얻을 수 있다.

Thread thread = Thread.currentThread();

 

다음 예제는 메인 스레드의 참조를 얻어 스레드 이름을 콘솔에 출력하고, 새로 생성한 스레드의 이름을 setName() 메소드로 설정한 후, getName() 메소드로 읽어오도록 했다.

// ThreadNameExample.java -- 메인스레드 이름 출력 및 userThread 생성 및 시작
public class ThreadNameExample {
    public static void main(String[] args) {
        Thread mainThread = Thread.currentThread();
        System.out.println("프로그램 시작 스레드 이름: " + mainThread.getName());
        
        ThreadA threadA = new ThreadA();
        System.out.println("작업 스레드 이름: " + threadA.getName());
        threadA.start();
        
        ThreadB threadB = new ThreadB();
        System.out.println("작업 스레드 이름: " + threadB.getName());
        threadB.start();
    }
}
// ThreadA.java -- ThreadA 클래스
public class ThreadA extends Thread {
    public ThreadA() {
        setName("ThreadA");
    }
    
    public void run() {
        for(int i=0; i<2; i++) {
            System.out.println(getName() + "가 출력한 내용");
        }
    }
}
// ThreadB.java -- ThreadB 클래스
public class ThreadB extends Thread {
    public void run() {
        for(int i=0; i<2; i++;) {
            System.out.println(getName() + "가 출력한 내용");
        }
    }
}