로세스는 운영체제로부터 자원을 할당받은 작업의 단위이며 스레드는 프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위이다.
프로그램을 실행시키면 운영체제가 프로그램에 독립적인 메모리 공간을 할당해준다. 즉 프로세스는 메모리에 올라간 프로그램 실행 단위이다.
하나의 프로세스 내에 여러 스레드를 생성 가능하다. 프로세스와 다르게 프로세스 내에서 메모리를 공유해가며 작동이 가능하다.
프로세스가 메모리에 올라갈 때 운영체제로부터 시스템 자원을 할당받는다 했다. 이때 운영체제는 프로세스마다 각각 독립된 메모리 영역을, code/data/heap/stack의 형식으로 할당해준다.
각각 독립된 메모리 영역을 할당해주므로 다른 프로세스의 변수나 자료에 접근할 수 없다.
이와 다르게 스레드는 메모리를 공유할 수 있다. stack 부분만 따로 할당받고 나머지(code,data,heap)은 공유한다.
이러한 메모리 공유의 이유는, 운영체제 관점에서는 프로세스가 최소 단위이고 이 프로세스만 관리할 수 있는데, 각 스레드가 메모리를 각각 할당받아 사용해버리면 관리가 안되기 때문이다(정확한 답은 아닐거같다. 좀 더 찾아보자).
정리하자면, 운영체제가 프로세스에게 code/data/heap/stack 메모리 영역을 할당해주고 최소 단위로 삼는다.
스레드는 프로세스 내에서 stack을 제외한 다른 메모리 영역을 공유한다. 다만 동기화 문제를 신경써야만 한다.
자바에서 스레드를 구현하는 방법에는 2가지가 있다.
public class ThreadSample extends Thread {
@Override
public void run() {
System.out.println("This is ThreadSample's run() method");
}
}
public class RunThreads {
public static void main(String[] args) {
RunThreads threads = new RunThreads();
threads.runBasic();
}
public void runBasic() {
Thread threadSample = new ThreadSample();
threadSample.start();
}
}
ExecutorService를 사용하여 스레드를 실행할수도 있다.
@Test
public void givenAThread_whenSubmitToES_thenResult()
throws Exception {
executorService.submit(new ThreadSample()).get();
}
보다시피 코드가 꽤 복잡해지는 것을 알 수 있다.
또 하나의 문제는 ThreadSample 클래스는 이미 Thread 클래스를 확장했기 때문에 다른 클래스의 확장이 불가능하다. 자바에서는 다중 상속을 지원하지 않기 때문이다.
public class RunnableSample implements Runnable {
@Override
public void run() {
System.out.println("This is RunnableSample's run() method");
}
}
public class RunThreads {
public static void main(String[] args) {
RunThreads threads = new RunThreads();
threads.runBasic();
}
public void runBasic() {
Thread runnableThread = new Thread(new RunnableSample());
runnableThread.start();
}
}
ExecutorService를 사용하여 스레드를 실행할수도 있다.
@Test
public void givenARunnable_whenSubmitToES_thenResult()
throws Exception {
executorService.submit(new RunnableSample()).get();
}
Thread 클래스 상속과 달리 Runnable 인터페이스를 구현하였으므로 RunnableSample은 다른 클래스의 상속을 받을 수 있다.
또한 Runnable 인터페이스는 함수형 인터페이스(단일 추상 메서드를 갖고 있는 인터페이스)이므로 람다식을 사용할 수 있다.
@Test
public void givenARunnableLambda_whenSubmitToES_thenResult()
throws Exception {
executorService.submit(
() -> System.out.println("lambda"));
}
Runnable 사용을 권장한다. 이유는 아래와 같다.
1. Thread클래스를 상속받을 때 Thread 클래스의 메서드를 오버라이딩 하는게 아니다. Thread가 구현한 Runnable의 메서드를 오버라이드한 것이다. 이는 IS-A 쓰레드 원칙을 위반한 것이다.
2. 다중상속이 지원안되므로 Thread클래스를 상속받으면 다른 클래스 상속 못받는다.
3. Runnable 인터페이스를 람다식에서 사용 가능하다. 그로인해 좀 더 간결한 코드 작성이 가능하다.
스레드는 라이프 사이클 동안 다음과 같은 다양한 상태를 거친다.
(출처: https://www.baeldung.com/java-thread-lifecycle)
java.lang.Thread 클래스는 State라는 enum 클래스를 가지며 Thread.getState()를 통해 스레드의 상태를 얻을 수 있다. 각 특정 시점에 대한 스레드의 상태는 아래 6개 중 하나가 된다.
New 상태의 스레드는 생성은 되었으나 아직 start되지 않은 스레드이다. start() 메서드를 통해 실행되기 전의 모든 스레드는 이 상태에 속한다.
Runnable runnable = new NewState();
Thread t = new Thread(runnable);
Log.info(t.getState());
[결과]
NEW
스레드를 만들고 start() 메서드를 호출하면 비로소 Runnable 상태가 된다(NEW -> RUNNABLE). 이 상태의 스레드는 실행 중이거나 시스템으로부터의 리소스 할당을 위해 대기중인 상태이다.
멀티스레드 환경에서는 jvm의 파트 중 하나인 스레드 스케쥴러가 고정된 일정 시간을 각 스레드에 할당해주는데, 그래서 각 스레드는 일정 시간동안 실행된 후 다른 RUNNABLE 스레드에 제어를 넘겨준다. 이러한 이유로 Runnable 상태의 스레드는 아래와 같이 cpu가 픽업한 Running상태의 스레드, 리소스 할당 대기중인 Ready to Run 상태의 스레드로 나뉘게 된다.
(출처: https://www.baeldung.com/java-thread-lifecycle)
다른 스레드에 의해 잠긴 코드 섹션, 즉 임계영역에 접근하고자 모니터 락 취득을 대기하고 있는 상태의 스레드이다.
예를 들어 스레드A가 프린터에서 어떠한 데이터를 출력하고자 하는데 스레드 B에서 해당 프린터를 사용하고 있다 하면, 프린터 메서드에는 하나의 스레드만 접근 가능하므로 스레드 A는 대기해야만 한다. 즉 스레드B가 쥐고 있는 모니터 락을 취득하기 위해 대기해야하며 이때 스레드A는 Blocked 상태에 있다고 할 수 있다.
다른 스레드가 특정 작업을 수행할 때까지 기다리고 있을 때 해당 스레드는 WAITING 상태라 할 수 있다. 아래 3가지 메서드 중 하나를 호출한다면 WAITING 상태에 진입하게 된다.
public class WaitingState implements Runnable {
public static Thread t1;
public static void main(String[] args) {
t1 = new Thread(new WaitingState()); // 1. Runnable 인터페이스 구현한 WaitingState로 스레드 생성한다.
t1.start(); // 2. start() 메서드를 실행하면 아래 run() 메서드가 실행된다.
}
public void run() {
Thread t2 = new Thread(new DemoThreadWS());
t2.start(); // 3. t1.start()를 통해 DemoThreadWS t2라는 새로운 스레드를 생성후 start한다.
try {
t2.join(); // 4. t2.join()을 호출함으로써 t1은 t2 스레드가 실행이 끝날때까지 WAITING 상태에 진입하게 된다.
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
}
class DemoThreadWS implements Runnable {
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
Log.info(WaitingState.t1.getState()); // 5. t1은 t2의 실행이 끝날때까지 대기하는 상태이다. 즉 해당 로그의 결과는 WAITING이다.
}
}
baeldung의 예제 코드이다. 주석에 써놓은 대로 작동하게 된다.
object.wait()을 하면 모니터락을 갖고 있는 스레드가 자신의 제어권을 양보한다. 즉 모니터락을 푼다는 얘기다. 이때의 스레드 상태는 waiting이다. blocked와는 다른데 blocked는 모니터락을 취득하기 위해 대기하는 상태이기 때문이다. 둘 다 대기하는건 맞지만 하나는 자신의 제어권을 양보 후 대기하는 것이고, 다른 하나는 모니터락을 획득하기 위해 대기하는 것이다.
다른 스레드가 특정 기간동안 특정 작업을 수행할 때까지 대기할 때 해당 스레드는 TIMED_WAITING 상태이다. 아래의 다섯가지 방법으로 TIMED_WAITING 상태에 진입 가능하다.
public class TimedWaitingState {
public static void main(String[] args) throws InterruptedException {
DemoThread obj1 = new DemoThread();
Thread t1 = new Thread(obj1);
t1.start();
Thread.sleep(1000);
Log.info(t1.getState()); // TIMED_WAITING
}
}
class DemoThread implements Runnable {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.error("Thread interrupted", e);
}
}
}
t1 스레드는 5초간 슬립상태이다. 이 상태에서 1초 뒤에 t1의 상태를 확인해보면 TIMED_WAITING 상태인 것을 확인할 수 있다.
스레드 실행이 종료되거나 비정상적으로 종료될 경우 해당 스레드는 TERMINATED 상태이다.
public class TerminatedState implements Runnable {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new TerminatedState());
t1.start();
Thread.sleep(1000); // 1초 뒤에 상태를 확인한다.
Log.info(t1.getState()); // Terminated
}
@Override
public void run() {
// No processing in this block
}
}
또한 t1.isAlive()
메서드로 스레드가 죽었는지 살아있는지 확인가능하다.
https://www.baeldung.com/java-runnable-vs-extending-thread
https://www.baeldung.com/java-thread-lifecycle
https://www.javatpoint.com/life-cycle-of-a-thread
자바의신