이 글에서는 java에서 사용하는 thread와 multi-thread의 개념만 간략히 짚고 넘어 간다.
Thread
Thread는 process 내에서 작업의 흐름 단위이다. 하나의 thread는 하나의 작업만 처리하지만, 여러 개의 thread는 병렬로 여러 개의 작업을 처리하기 때문에 더 빠르다.
Concurrency & Parallelism
모든 작업은 컴퓨터의 CPU가 작업하며, CPU의 core가 thread를 실행한다. 하나의 core가 여러 개의 thread를 수행할 수도 있고(concurrency), 여러 개의 core에서 여러 개의 thread를 수행할 수도 있다.(parallelism) 하나의 core가 여러 개의 thread를 작업할 때는 각 thread를 조금씩 실행하고 다른 thread를 실행하는 것을 반복하고 결과적으로 여러 개의 thread가 동시에 실행되는 것처럼 보인다.
Java의 Thread
모든 java application은 JVM 위에서 동작한다. JVM은 static main() method가 있는 class를 찾고 main thread를 만들며, main thread는 main() method를 실행하며 main() method가 끝이 나면 종료한다. Java application은 실행 중인 thread가 하나라도 있다면 종료하지 않는다. 모든 thread가 종료되면 그 때 종료된다. 따라서 single-thread에서 main thread가 종료되면 모든 thread가 종료되었으므로 application이 종료된다. 반면 multi-thread에서 main thread가 종료되어도 다른 thread가 종료되지 않았으면 application이 종료되지 않는다.
daemon thread : 보조 thread이다. main method가 종료되면 같이 종료한다. setDaemon() method로 daemon thread로 만들 수 있다.
Thread 생성과 실행
Java에서 thread의 생성 방법은 아래 3가지가 있다. 위의 두 방법은 모두 thread 내부에서 작동할 코드는 run() method를 override하고 작성해야 한다.
- Thread class를 extend하는 방법
- Runnable interface를 implement하고 Thread object로 실행하는 방법
// ExtendThread.java
// 방법 1. Thread class 상속
public class ExtendThread extends Thread{
@Override
public void run(){
// thread running code
}
}
// RunnableThread.java
// 방법 2. Runnable interface implement
public class RunnableThread implements Runnable{
@Override
public void run(){
// threa의 running code
}
}
// App.java
public class App {
public static void main(String[] args) throws Exception {
// 방법 1. Thread class 상속
Thread thread1 = new ExtendThread();
thread1.start();
// 방법 2. Runnable interface implement
RunnableThread runnableThread = new RunnableThread();
Thread thread2 = new Thread(runnableThread);
thread2.start();
}
}
꼭 위 코드와 같이 구현할 필요는 없다. anonymous class를 이용해도 되고 lambda function을 이용해도 된다.
Thread class를 extend할 경우에는 다른 class로부터 상속받지 못한다. 반면 Runnable interface를 implement해서 thread를 생성할 때는 다른 class들로부터 상속받을 수 있다. 상황에 맞춰 좋은 방법을 택하면 될 것이다.
이렇게 정의한 thread를 실행할 때는 start() method를 호출하면 된다. run() method가 아니다!
run() method를 call하면 작성한 class 안에 있는 run() method를 호출하는 것이고 thread를 호출하는 것이 아니다. start() method는 해당 thread를 ready queue에 등록하고, 이후 thread scheduler가 해당 thread를 run시키면 그 때 해당 thread의 run() method가 실행된다.
Callable
이외에도 다른 방법이 있다. 이 방법은 call() method를 override해야 하며, generic이기 때문에 type을 넣어 주어야 한다.
- Callable interface를 impelement한다. 이후
- FutureTask를 사용할 수도 있고
- ExecutorService를 사용할 수도 있다.
// CallableThread.java
public class CallableThread implements Callable<Integer>{
@Override
public Integer call() throws Exception{
// thread의 running code
return 1;
}
}
// App.java
public class App {
public static void main(String[] args) throws Exception {
// 방법 3. Callable interface implement
CallableThread callableThread = new CallableThread();
// 3-1. FutureTask로 callable 호출
FutureTask futureTask = new FutureTask(callableThread);
Thread thread3 = new Thread(futureTask);
thread3.start();
System.out.println(futureTask.get()); // 1
// 3-2.
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(callableThread);
System.out.println(future.get()); // 1
}
}
callable interface를 implement해서 thread를 생성하면 return value를 지정할 수 있다. 또한 exception도 관리할 수 있다.
* runnable vs callable
runnable은 return value가 없다. callable은 return value가 있다.
runnable은 exception이 없다. callable은 exception이 있다.
상황에 맞춰 적합한 방법을 쓰면 될 것이다.
JVM thread scheduling
Java의 thread scheduling은 아래와 같은 3가지 조건을 이용해 scheduling한다.
- first time, first serve : 먼저 온 것을 먼저 처리한다.
- preemptive priority : ready queue에서 priority가 제일 높은 것부터 먼저 처리한다.
- 만약 priority가 같다면 먼저 온 것을 먼저 처리한다.
- time-slicing (round-robin) : thread는 일정 시간만큼 실행하고 다시 ready queue에 넣는다.
- time slice 값은 JVM이 결정하기 때문에 코드로 제어할 수 없다.
ready queue에 있는 thread 중 제일 priority가 높은 것을 수행한다. 만약 실행하고 있는 thread보다 높은 priority가 있다면 그 thread를 실행한다.(preemptive priority) 만약 priority가 같다면 먼저 온 것을 수행한다.(first time, first serve)
Thread Life Cycle
thread는 다음과 같은 state를 가진다.
- new : 새로 만들어졌지만 실행되지 않은 상태
- active : thread의 start() method가 호출되면 active state로 바뀐다.
- runnable : 실행 대기중인 상태
- running : 실행 중인 상태
- blocked : synchronized block이나 method에 진입, 또는 monitor lock을 획득하기 위해 대기하고 있는 상태
- waiting : 다른 thread의 작업을 기다리고 있는 상태
- timed waiting : 특정 시간동안 다른 thread가 작업하기를 기다리는 상태
- terminated : 실행이 완료되어 정상적으로 끝났든, 오류 발생으로 비정상적으로 끝났든 thread가 작업을 종료한 상태
Synchronization
앞선 포스팅에서 소개했지만 JVM runtime data area에서 Heap(static, objects, constant pool)과 metaspace는 thread가 공유하고 있다. 따라서 만약 여러 thread가 같은 resouce에 접근한다면 각각 thread의 작업이 다른 thread에 영향을 줄 수 있기 때문에 의도하지 않은 결과가 나올 수도 있다.
이러한 현상을 막기 위해 critical section, mutex lock, semaphore, monitor, event 등 여러 lock 기법을 사용한다. 이외에도 volatile, atomic class 등을 사용해 thread-safe하게 프로그래밍할 수 있다.
- critical section : resource에 한 process 내에서 단 하나의 thread만 접근할 수 있다.
- mutex : resouce에 단 하나의 thread만 접근할 수 있다. process 상관 없이 딱 하나이다.
- event : 특정한 action이 일어났을 때 다른 thread에게 이를 알린다.
- semaphore : resource에 n개의 thread만 접근할 수 있다.
Deadlock
OS에서도 다루는 이 개념은 multi-thread 환경에서 synchronization을 고려할 때 발생할 수 있는 문제이다. 아래 4가지 조건이 충족될 때 발생하는 문제이며 thread끼리 서로 가진 resource를 요청하고 대기하는 상태가 계속 이어지는, 일종의 무한 루프 상태이다. synchronization을 고려할 때 deadlock이 발생하지 않도록 유의해야 한다.
- mutual exclusion : 오직 하나의 thread만이 resource에 접근한다.
- hold and wait : resource를 가진 thread가 다른 thread가 가진 resource를 요청하고 wait하고 있다.
- non-preemption : 다른 thread가 가진 resource를 빼앗을 수 없다
- circular wait : hold-and-wait 관계가 cycle을 이루고 있다.
Thread Pool
Thread를 thread pool에 넣지 않으면 thread의 개수 제한이 없기 때문에 수많은 thread들을 scheduling해야 하므로 성능이 떨어질 수 있다. Java에서는 이러한 현상을 막기 위해 Thread Pool을 사용한다.
Thread Pool은 thread 개수를 정해 놓고 ready queue에 있는 thread들을 처리한다. 따라서 thread가 많이 생겨도 thread의 개수 제한이 걸리기 때문에 처리 속도를 보장할 수 있다.
'Development > Java' 카테고리의 다른 글
[Java] Collection (0) | 2023.03.06 |
---|---|
[Java] String vs StringBuffer vs StringBuilder (0) | 2023.03.04 |
[Java] Primitive Wrapper Class (0) | 2023.03.04 |
[Java] Generic (0) | 2023.03.02 |
[Java] Java Virtual Machine (0) | 2023.03.01 |