프로세스(process)란 간단히 말해서 실행 중인 프로그램(program)
이다. 프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)을 할당받아 프로세스가 된다.
프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있다.
프로세스 자원을 이용해서 실제로 작업을 수행하는 것이 쓰레드
이다.
쓰레드를 구현하는 방법은 2가지가 있다
Thread 클래스를 상속받는 방법
Runnable 인터페이스를 구현하는 방법
어느 쪽을 선택해도 별 차이는 없지만 Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에, Runnable 인터페이스를 구현하는 것이 일반적이다.
Runnable 인터페이스를 구현하는 방법은 재사용성(reusability)
이 높고 코드의 일관성(consistency)을 유지할 수 있기 때문에 보다 객체지향적인 방법이라 할 수 있다.
public class MyThread extends Thread {
@Override
public void run() {
// logic
}
}
public class MyThread implements Runnable {
@Override
public void run() {
// logic
}
}
Runnable 인터페이스는 오로지 run()만 정의되어 있는 간단한 인터페이스이다. Runnable 인터페이스를 구현하기 위해서 해야할 일은 추상메소드인 run()의 몸통을 만들면 된다.
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
쓰레드를 구현한다는 것은, 위의 두 방법 중 어떤 것을 선택하든지, 그저 쓰레드를 통해 작업하고자 하는 내용으로 run()의 몸통{}을 채우는 것이다.
public class MyThread {
public static void main(String[] args) {
Thread_Ex2 t1 = new Thread_Ex2(); // 방법1
Runnable r = new Thread_Ex1(); // 방법2
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
}
class Thread_Ex1 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; ++i) {
System.out.println(Thread.currentThread().getName()); // 현재 실행중인 Thread를 반환한다.
}
}
}
class Thread_Ex2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; ++i) {
System.out.println(getName());
}
}
}
Thread 클래스를 상속
받은 경우와 Runnable 인터페이스를 구현
한 경우의 인스턴스 생성 방법이 다르다.
Runnable 인터페이스를 구현한 경우, Runnable 인터페이스를 구현한 클래스의 인스턴스를 생성한 다음, 이 인스턴스를 Thread 클래스의 생성자의 매개변수로 제공해야 한다.
Thread 클래스를 상속받으면, 자손 클래스에서 조상인 Thread 클래스의 메소드를 직접 호출할 수 있지만, Runnable을 구현하면 Thread 클래스의 static 메소드인 currentThread()를 호출하여 쓰레드에 대한 참조를 얻어 와야만 호출이 가능하다.
static Thread currentThread() // 현재 실행중인 쓰레드의 참조를 반환한다.
String getName() // 쓰레드의 이름을 반환한다.
그래서 Thread를 구현한 Thread_Ex2는 바로 getName()으로 호출할 수 있지만, Runnable을 구현한 Thread_Ex1 클래스는 Thread.currentThread().getName()으로 호출해야 한다.
쓰레드를 생성했다고 해서 자동으로 실행되는 것이 아니라 start()를 호출해야만 쓰레드가 실행된다.
t1.start(); // 쓰레드 t1을 실행시킨다.
t2.start(); // 쓰레드 t2를 실행시킨다.
사실은 start()가 호출되었다고 해서 바로 실행되는 것이 아니라, 일단 실행 대기 상태에 있다가 자신의 차례가 되어야 실행된다. 물론 실행대기중인 쓰레드가 하나도 없으면 곧바로 실행상태가 된다.
한 가지 더 알아 두어야 하는 것은 한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다는 것
이다. 즉, 하나의 쓰레드에 대해 start()가 한 번만 호출될 수 있다는 뜻이다.
그래서 만약 쓰레드의 작업을 한 번 더 수행해야 한다면 아래의 오른 코드와 같이 새로운 쓰레드를 생성한 다음에 start()를 호출한다. 만일 아래 왼쪽의 코드처럼 하나의 쓰레드에 대해 start()를 두 번 이상 호출하면 실행시에 IllegalThreadStateException이 발생한다.
ThreadEx1 t1 = new ThreadEx1();
t1.start();
t1.start(); // 에러 발생
ThreadEx1 t1 = new ThreadEx1();
t1.start();
t1 = new ThreadEx1(); // 다시 생성
t1.start(); // OK
쓰레드를 실행시킬 때 run()이 아닌 start()를 호출한다는 것에 대해서 다소 의문이 들었을 것이다. start()와 run()의 차이와 쓰레드가 실행되는 과정에 대해서 알아보자.
main메소드에서 run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 선언된 메소드를 호출하는 것일뿐이다.
반면에 start는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택(call stack)을 생성한 다음에 run()을 호출해서, 생성된 호출스택에 run()이 첫 번째로 올라가게 한다.
모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택을 필요로 하기 때문에
, 새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출스택이 생성되고 쓰레드가 종요되면 작업에 사용된 호출스택은 사라진다.
- main메소드에서 쓰레드의 start()를 호출한다.
- start()는 새로운 쓰레드를 생성하고, 쓰레드가 작업하는데 사용될 호출스택을 사용한다.
- 새로 생성된 호출스택에 run()이 호출되어, 쓰레드가 독립된 공간에서 작업을 수행한다.
- 이제는 호출스택이 2개이므로 스케줄러가 정한 순서에 의해서 번갈아 가면서 실행된다.
호출스택에서는 가장 위에 있는 메소드가 현재 실행중인 메소드이고 나머지 메소드들은 대기상태에 있다는 것을 알고 있을 것이다.
그러나 위에 그림에서와 같이 쓰레드가 둘 이상일 때는 호출스택의 최상위에 있는 메소드일지라도 대기상태에 있을 수 있다.
스케줄러는 실행대기중인 쓰레드들의 우선순위를 고려하여 실행순서와 실행시간을 결정하고, 각 쓰레드들은 작성된 스케줄에 따라 자신의 순서가 되면 지정된 시간동안 작업을 수행한다.
이 때 주어진 시간동안 작업을 마치지 못한 쓰레드는 다시 자신의 차례가 돌아올 때 까지 대기 상태로
있게 되며, 작업을 마친 쓰레드, 즉 run()의 수행이 종료된 쓰레드는 호출스택이 모두 비워지면서
이 쓰레드가 사용하면 호출스택은 사라진다.
이것은 자바프로그램을 실행하면 호출스택이 생성되고 main메소드가 처음으로 호출되고, main메소드가 종료되면 호출스택이 비워지면서 프로그램도 종료되는 것과 같다.
main메소드의 작업을 수행하는 것도 쓰레드이며, 이를 main쓰레드라고 한다. 우리는 지금까지 우리도 모르는 사이에 이미 쓰레드를 사용하고 있었던 것이다. 앞서 쓰레드가 일꾼이라고 하였는데, 프로그램이 실행되기 위해서는 작업을 수행하는 일꾼이 취소한 하나는 필요할 것이다.
그래서 프로그램을 실행하면 기본적으로 하나의 쓰레드(일꾼)을 생성하고, 그 쓰레드가 main메소드를 호출해서 작업이 수행되도록 하는 것이다.
지금까지는 main메소드가 수행을 마치면 프로그램이 종료되었지만, main메소드가 수행을 마쳤다하더라도 다른 쓰레드가 아직 작업을 마치지 않은 상태라면 프로그램이 종료되지 않는다.
실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.
public class MyThread {
public static void main(String[] args) {
ThreadEx_2 t1 = new ThreadEx_2();
t1.start();
}
}
class ThreadEx_2 extends Thread {
public void run() {
throwException();
}
public void throwException() {
try {
throw new Exception();
} catch (Exception e) {
e.printStackTrace();
}
}
}
java.lang.Exception
at ExampleCode.ThreadEx_2.throwException(MyThread.java:17)
at ExampleCode.ThreadEx_2.run(MyThread.java:12)
새로 생성한 쓰레드에서 고의로 예외를 발생시키고 printStackTrace()을 이용해서 예외가 발생한 당시의 호출스택을 출력하는 예제이다. 호출스택의 첫 번째 메소드가 main메소드가 아니라 run메소드인 것을 확인하자. 호출 스택의 첫 번째 메소드가 main이 아니라 run인 것을 확인하자. 새로운 콜스택이 만들어졌기 때문이다.
한 쓰레드가 예외가 발생해서 종료되어도 다른 쓰레드의 실행에는 영향을 미치지 않는다. 아래의 그림에 main쓰레드의 호출스택이 없는 이유는 main쓰레드가 종료되었기 때문이다.
public class MyThread {
public static void main(String[] args) {
ThreadEx_2 t1 = new ThreadEx_2();
t1.run();
}
}
class ThreadEx_2 extends Thread {
public void run() {
throwException();
}
public void throwException() {
try {
throw new Exception();
} catch (Exception e) {
e.printStackTrace();
}
}
}
java.lang.Exception
at ExampleCode.ThreadEx_2.throwException(MyThread.java:17)
at ExampleCode.ThreadEx_2.run(MyThread.java:12)
at ExampleCode.MyThread.main(MyThread.java:6)
이번에는 start() 대신 run()을 호출하였는데 위에서 설명했던 것처럼 새로운 호출스택이 생기지 않고 main위에 run이 쌓이는 것을 볼 수 있다.