이 포스팅의 코드 및 정보들은 강의를 들으며 정리한 내용을 토대로 작성한 것입니다.
Concurrent 소프트웨어
란?동시에 여러 작업을 할 수 있는 소프트웨어
어떤 한 프로세스에서 다른 프로세스를 만드는 게 가능
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
}
메인 메서드에서 어떤 어플리케이션이 동작하는지 스레드 이름을 확인하면 당연히 main
이라는 이름이 출력된다.
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println("Main!");
}
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("[Thread] " + Thread.currentThread().getName());
}
}
}
MyThread
라는 Thread 클래스를 상속받은 static class를 만들고 run()을 재정의하여 출력 한 문장 하나 하기로 한다.
main()에서는 MyThread 객체 하나를 만들어서 start()를 실행하고, 그 뒤에 Main!이라고 한 문장 출력한다.
순서상 myThread.start()가 실행되고 Main!이 출력되어야 할 텐데
Main!이 먼저 출력되고 그 다음 Thread를 출력하는 문장이 나온다. 무조건 이렇게 나오는 것은 아니고, 간혹 이렇게 실행되는 것이다.
이번에는 [Thread]로 시작하는 스레드 이름 출력하는 부분이 먼저 실행됐다.
이런 현상이 발생하는 이유는 스레드의 순서를 보장할 수 없기 때문이다.
Java 8 이전에 new Runnable()
로 익명 클래스를 만들어서 재정의 하는 방식으로 만들었었다.
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("[Thread] " + Thread.currentThread().getName());
}
});
...
}
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("[Thread] " + Thread.currentThread().getName()));
...
}
이렇게 람다식을 통해서 한 줄로 줄일 수 있다.
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("[Thread] " + Thread.currentThread().getName())
});
...
}
여러 줄 작업일 때는 중괄호를 활용하면 좋다.
Runnable
이 functional interface로 바뀌었기 때문에 이런 게 가능하다.
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("[Thread] " + Thread.currentThread().getName());
});
thread.start();
System.out.println("[Main] " + Thread.currentThread().getName());
}
이렇게 Thread.sleep()
을 통해서 대기시키면 다른 스레드한테 서버 리소스를 사용할 수 있는 우선권(우선순위)이 주어진다. 즉, 다른 스레드가 먼저 처리되는 것이다.
위 소스에서는 thread.start()를 하자마자 sleep() 상태에 들어가고, System.out.println("[Main] " + Thread.currentThread().getName())
이 부분인 메인 스레드가 먼저 처리된다. 그러고 나서 자고 있던 스레드가 일어나서 처리될 것이다.
실행해보면 거의 [Main]을 출력하는 스레드(print문)가 먼저 실행될 거고, 그 다음 [Thread]부분이 출력될 것이다.
try~catch문에서 catch에 설정된 InterruptedException은 자는 동안에 누군가가 이 스레드를 깨우면 이 안에 들어온다. 즉, 깨우는 방법이 있다는 것이다.
interrupt는 다른 스레드를 깨우는 방법이다.
public static void main(String[] args) {
Thread thread = new Thread(() -> {
while(true) {
System.out.println("[Thread] " + Thread.currentThread().getName());
try {
Thread.sleep(1000L);
} catch (InterruptedException e) { // 누군가 이 스레드를 깨우면 종료됨
System.out.println("exit!");
return;
}
}
});
...
}
구현한 run()메서드 안에는 무한 루프를 돌면서 try~catch 문으로 sleep()을 실행하는 로직이 있다. 만약 interrupt가 발생하면 이 스레드는 종료되며, return문으로 인해 프로세스도 종료된다.
Runnable 인터페이스에 있는 run()은 다음 공식문서의 설명과 같다.
When an object implementing interface Runnable is used to create a thread, starting the thread causes the object's run method to be called in that separately executing thread.
The general contract of the method run is that it may take any action whatsoever.
Runnable 인터페이스를 구현하는 객체가 스레드를 만드는 데 사용되는 경우, 스레드를 시작하면 객체의 run()이 별도로 실행되는 스레드에서 호출된다.
메서드 실행의 일반적인 조건은 어떤 작업이든 수행할 수 있다는 것이다.
run()은 반환 타입이 void라 return을 굳이 안 해도 되지만 interrupt로 빠져나와야 하기에 추가한다.
// try~catch 스레드 부분
...
thread.start();
System.out.println("[Main] " + Thread.currentThread().getName());
Thread.sleep(3000L);
thread.interrupt();
}
[Main] 스레드를 출력하는 부분 밑에는 sleep(3초)과 interrupt()를 추가했다. 참고로 interrupt()는 종료시키는 operation이 아니고 단지 깨우는(위 소스에서는 예외를 발생시키는) 메서드이다.
[Thread]가 세번 출력되고 마지막에는 interrupt()로 인해 exit!라는 출력과 함께 종료된다.
예외를 발생시키고 return
문처럼 빠져나올 수 있는 장치가 없다면 스레드 출력은 계속될 것이다.
다른 스레드를 종료시킬 수 있는 방법은 있지만, 우리가 직접 해야 한다. 종료시키는 메서드가 따로 있는 것은 아니다.
join()은 다른 스레드를 기다리는 것이다.
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
System.out.println("[Thread] " + Thread.currentThread().getName());
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
// Runtime Exception - Unchecked Exception
// 에러를 감싸서 예외 던지기, 로깅, 예외로 변환처리하면 적절
throw new IllegalStateException(e);
}
});
...
}
이번에는 [Thread] 스레드 내에서 3초 정도 기다리는 로직을 구현했다. [Main] 스레드에서는 [Thread]를 기다리도록 join()을 활용할 것이다.
// [Thread] 스레드 로직
...
thread.start();
System.out.println("[Main] " + Thread.currentThread().getName());
thread.join();
System.out.println(thread + "is finished");
}
join()은 기다릴 스레드에다가 join()을 쓰면 된다. 그러면 먼저 [Main] 스레드의 이름이 출력될 거고 (혹은 [Thread] 스레드가 먼저 실행될 수 있음), 그 다음 [Thread] 스레드가 종료될 때까지 join()을 실행시킨 [Main] 스레드는 기다릴 것이다. 다 기다리면 그때 스레드 + "is finished" 라는 문장과 함께 스레드가 끝날 것이다.
join()이 실행된 상황에서 스레드가 계속 대기하고 있지만
...
System.out.println("[Main] " + Thread.currentThread().getName());
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread + "is finished");
}
이런 상황에서 누군가가 [Main] 스레드를 interrupt한다면 InterruptedException이 발생하게 될 것이다.
하지만 [Main] 스레드에서도 try~catch문으로 InterruptedException으로 예외 처리를 하게 해놨다면 스레드가 많이 복잡해질 것이다.
이런 스레드 로직이 더 많아진다면 관리하기 힘들어질 것이다.
그래서 Executors, Future, CompletableFuture을 정리하면서 많은 스레드를 어떻게 관리할 것인지 알아볼 것이다.
Concurrent Software
Java에서 지원하는 Concurrent Programming
Java MultiThread Programming
스레드 주요 기능