쓰레드 구현
자바에서 쓰레드를 구현하기 위햐서는 Thread 클래스를 상속받거나 Runnable 인터페이스를 구현해야 합니다.
두 방법 사이의 차이가 없으나 자바는 다중 상속을 지원하지 않기 때문에
유연성의 관점에서 Runnable 인터페이스를 구현하여 사용하는 방법이 더 좋습니다.
예시 코드는 다음과 같습니다.
public class Example {
static class MyRunnableImpl implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Start!!");
}
}
public static void main(String[] args) {
new Thread(new MyRunnableImpl()).start();
new Thread(new MyRunnableImpl()).start();
new Thread(new MyRunnableImpl()).start();
}
}
쓰레드의 실행
start() 를 사용하여 실행합니다.
start()를 사용하면 스레드가 실행대기 상태로 전환됩니다.
이후 자신의 차례가 되어야 실행됩니다.
쓰레드는 한번 실행이 종료되면 다시 실행될 수 없습니다.
즉 하나의 쓰레드에 대하여 두번의 start()는 호출될 수 없습니다.
만약 다음과 같이 하나의 쓰레드에 대하여 두번의 start() 호출을 진행한다면 예외가 발생하게 됩니다.
public class Example {
static class MyRunnableImpl implements Runnable {
@Override
public void run() {
System.out.println("!!");
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnableImpl());
thread.start();
}
}
start()와 run()
분명 Runnable 혹은 Thread를 상속받아 구현한 메서드를 run()입니다.
그러나 쓰레드를 실행시킬 때에는 run()이 아닌 start()를 사용하여 실행합니다.
이제부터 쓰레드의 실행 과정을 살펴보며 이러한 이유에 대해 알아보겠습니다.
public class Example {
static class MyRunnableImpl implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " Start!!");
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnableImpl());
thread.run();
thread.start();
}
}
다음 코드를 실행한 결과는 다음과 같습니다.
main 메서드 내부에서 run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아닌 단순히 메서드를 호출하는 것입니다.
즉 Call Stack은 다음과 같은 상황이 됩니다.
반면 start()는 새로운 쓰레드가 작업을 실행하는 데 필요한 Call Stack을 생성한 이후, run()에 대한 스택프레임을 push합니다. Call Stack을 그려보면 다음과 같습니다.
- main 메서드에서 start를 호출합니다.
- start는 새로운 Thread와 해당 쓰레드가 작업을 실행하는 데 필요한 Call Stack을 생성합니다.
- 새로운 쓰레드에서는 run을 실행합니다.
- Call Stack이 2개이므로 스케줄러가 정한 순서에 따라 작업이 번갈아 가며 실행됩니다.
즉 새로운 쓰레드를 start할 때마다 새로운 Call Stack이 생성되며, 작업이 완료된 이후에는 사용된 Call Stack이 소멸합니다.
기본적으로 Call Stack의 최상위 메서드를 제외한 메서드는 대기상태에 존재하고, 최상위 메서드만이 실행 중인 메서드가 됩니다.
그러나 위와 같이 쓰레드가 둘 이상인 경우에는 Call Stack의 최상위 메서드라고 하더라도 대기상태에 있을 수 있습니다.
OS 스케줄러는 실행대기 상태의 쓰레드들의 우선순위를 고려하여 실행순서와 실행시간을 결정합니다.
각 쓰레드들을 스케줄에 따라서 자신의 순서가 되면, 자신에게 할당된 시간만큼 작업을 수행합니다.
만약 주어진 시간동안 작업을 마치지 못했다면, 다시 다신의 차례가 돌아올 때까지 실행대기 상태로 대기합니다.
작업을 마친 쓰레드의 경우에는 해당 쓰레드에 대한 Call Stack이 모두 비워지므로 해당 쓰레드가 사용하던 Call Stack은 제거됩니다.
Main Thread
main 메서드의 작업을 실행하는 쓰레드를 main thread라고 합니다.
이전 포스팅(https://ttl-blog.tistory.com/779)에서 모든 프로세스에서는 하나 이상의 쓰레드가 필요하다고 하였습니다.
프로그램을 실행하게 되면 기본적으로 하나의 쓰레드가 생성되고,
해당 쓰레드가 main 메서드를 호출하여 작업이 수행되도록 하는 것입니다.
main 메서드가 수행을 마쳤더라도, 다른 쓰레드가 작업중인 경우에는 프로그램은 종료되지 않습니다.
실행 중인 쓰레드가 하나도 없는 경우에만 프로그램은 종료됩니다.
(이후 배울 데몬(daemin) 쓰레드는 예외입니다.)
main 메서드가 수행을 마치더라도, 다른 쓰레드에서 작업을 마치지 않아 프로그램이 종료되지 않는 예시입니다.
public class Example {
static class MyRunnableImpl implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 시작 !");
sleep();
System.out.println(Thread.currentThread().getName() + " 종료 !");
}
private void sleep() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+" 시작 !");
Thread thread = new Thread(new MyRunnableImpl());
thread.start();
System.out.println(Thread.currentThread().getName()+" 종료 !");
}
}
쓰레드의 실행과 관련된 예제들
이해를 돕기 위해 몇가지 예제들을 더 살펴보도록 하겠습니다.
public class Example {
static class MyRunnableImpl implements Runnable {
@Override
public void run() {
throwException();
}
private void throwException() {
try {
throw new Exception();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnableImpl());
thread.start();
}
}
위 코드의 결과는 다음과 같습니다.
호출스택의 첫 번째 메세드가 main이 아닌 run 메서드입니다.
public class Example {
static class MyRunnableImpl implements Runnable {
@Override
public void run() {
throwException();
}
private void throwException() {
try {
throw new Exception();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnableImpl());
thread.run();
}
}
이전 예시에서 start() 대신 run()을 사용하였습니다.
실행 결과는 다음과 같습니다.
이전 예시와 달리 쓰레드가 새로 생성되지 않을 것을 확인할 수 있습니다.
public class Example {
static class MyRunnableImpl implements Runnable {
@Override
public void run() {
throwException();
}
private void throwException() {
try {
throw new Exception();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnableImpl());
thread.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
throwException();
}
private static void throwException() {
try {
throw new Exception();
} catch (Exception e) {
e.printStackTrace();
}
}
}
위 코드의 실행결과는 다음과 같습니다.
Main Thread의 Call Stack과 새로 생성한 쓰레드의 Call Stack이 다른 것을 확인할 수 있습니다.
또한 하나의 Thread에서 예외가 발생하더라도 이는 다른 쓰레드에 영향을 미치지 않음을 확인할 수 있습니다.
'☕️ Java > 기본' 카테고리의 다른 글
[Java] Thread (4) - 쓰레드의 우선순위 (0) | 2022.07.13 |
---|---|
[Java] Thread (3) - 싱글쓰레드와 멀티쓰레드 (0) | 2022.07.09 |
[Java] Thread (1) - 프로세스와 쓰레드 (0) | 2022.07.09 |
[Java] Double Dispatch란? (feat. Visitor Pattern) (0) | 2022.07.08 |
[Java] Method Dispatch란? (Static, Dynamic) (0) | 2022.07.08 |