자바의 Thread

박시시·2022년 10월 5일
0

JAVA

목록 보기
9/13

Process vs Thread

로세스는 운영체제로부터 자원을 할당받은 작업의 단위이며 스레드는 프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위이다.
프로그램을 실행시키면 운영체제가 프로그램에 독립적인 메모리 공간을 할당해준다. 즉 프로세스는 메모리에 올라간 프로그램 실행 단위이다.
하나의 프로세스 내에 여러 스레드를 생성 가능하다. 프로세스와 다르게 프로세스 내에서 메모리를 공유해가며 작동이 가능하다.

프로세스가 메모리에 올라갈 때 운영체제로부터 시스템 자원을 할당받는다 했다. 이때 운영체제는 프로세스마다 각각 독립된 메모리 영역을, code/data/heap/stack의 형식으로 할당해준다.
각각 독립된 메모리 영역을 할당해주므로 다른 프로세스의 변수나 자료에 접근할 수 없다.

이와 다르게 스레드는 메모리를 공유할 수 있다. stack 부분만 따로 할당받고 나머지(code,data,heap)은 공유한다.
이러한 메모리 공유의 이유는, 운영체제 관점에서는 프로세스가 최소 단위이고 이 프로세스만 관리할 수 있는데, 각 스레드가 메모리를 각각 할당받아 사용해버리면 관리가 안되기 때문이다(정확한 답은 아닐거같다. 좀 더 찾아보자).

정리하자면, 운영체제가 프로세스에게 code/data/heap/stack 메모리 영역을 할당해주고 최소 단위로 삼는다.
스레드는 프로세스 내에서 stack을 제외한 다른 메모리 영역을 공유한다. 다만 동기화 문제를 신경써야만 한다.

자바에서의 스레드 구현

자바에서 스레드를 구현하는 방법에는 2가지가 있다.

  • Thread 클래스 상속
  • Runnable 인터페이스 구현

Thread 클래스 상속

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 클래스를 확장했기 때문에 다른 클래스의 확장이 불가능하다. 자바에서는 다중 상속을 지원하지 않기 때문이다.

Runnable 인터페이스 구현

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 or Thread?

Runnable 사용을 권장한다. 이유는 아래와 같다.
1. Thread클래스를 상속받을 때 Thread 클래스의 메서드를 오버라이딩 하는게 아니다. Thread가 구현한 Runnable의 메서드를 오버라이드한 것이다. 이는 IS-A 쓰레드 원칙을 위반한 것이다.
2. 다중상속이 지원안되므로 Thread클래스를 상속받으면 다른 클래스 상속 못받는다.
3. Runnable 인터페이스를 람다식에서 사용 가능하다. 그로인해 좀 더 간결한 코드 작성이 가능하다.

Thread Life Cycle

스레드는 라이프 사이클 동안 다음과 같은 다양한 상태를 거친다.


(출처: https://www.baeldung.com/java-thread-lifecycle)

java.lang.Thread 클래스는 State라는 enum 클래스를 가지며 Thread.getState()를 통해 스레드의 상태를 얻을 수 있다. 각 특정 시점에 대한 스레드의 상태는 아래 6개 중 하나가 된다.

  • NEW – 아직 실행되지않은(start메서드로 실행되지 않은) 새로 만들어진 스레드
  • RUNNABLE – 실행 중이거나 실행 준비가 되어있으나 아직 리소스 할당을 기다리는 스레드
  • BLOCKED – synchronized 블록이나 메서드에 진입하거나 재진입하기 위해 모니터 락 취득을 대기중인 스레드
  • WAITING – 시간 제한 없이 다른 스레드가 특정 작업 수행하길 기다리는 스레드
  • TIMED_WAITING – 특정 기간동안 다른 스레드가 특정 작업 수행하길 기다리는 스레드
  • TERMINATED – 실행 종료된 스레드

NEW

New 상태의 스레드는 생성은 되었으나 아직 start되지 않은 스레드이다. start() 메서드를 통해 실행되기 전의 모든 스레드는 이 상태에 속한다.

Runnable runnable = new NewState();
Thread t = new Thread(runnable);
Log.info(t.getState());

[결과]

NEW

Runnable

스레드를 만들고 start() 메서드를 호출하면 비로소 Runnable 상태가 된다(NEW -> RUNNABLE). 이 상태의 스레드는 실행 중이거나 시스템으로부터의 리소스 할당을 위해 대기중인 상태이다.
멀티스레드 환경에서는 jvm의 파트 중 하나인 스레드 스케쥴러가 고정된 일정 시간을 각 스레드에 할당해주는데, 그래서 각 스레드는 일정 시간동안 실행된 후 다른 RUNNABLE 스레드에 제어를 넘겨준다. 이러한 이유로 Runnable 상태의 스레드는 아래와 같이 cpu가 픽업한 Running상태의 스레드, 리소스 할당 대기중인 Ready to Run 상태의 스레드로 나뉘게 된다.


(출처: https://www.baeldung.com/java-thread-lifecycle)

Blocked

다른 스레드에 의해 잠긴 코드 섹션, 즉 임계영역에 접근하고자 모니터 락 취득을 대기하고 있는 상태의 스레드이다.
예를 들어 스레드A가 프린터에서 어떠한 데이터를 출력하고자 하는데 스레드 B에서 해당 프린터를 사용하고 있다 하면, 프린터 메서드에는 하나의 스레드만 접근 가능하므로 스레드 A는 대기해야만 한다. 즉 스레드B가 쥐고 있는 모니터 락을 취득하기 위해 대기해야하며 이때 스레드A는 Blocked 상태에 있다고 할 수 있다.

Waiting

다른 스레드가 특정 작업을 수행할 때까지 기다리고 있을 때 해당 스레드는 WAITING 상태라 할 수 있다. 아래 3가지 메서드 중 하나를 호출한다면 WAITING 상태에 진입하게 된다.

  • object.wait()
  • thread.join()
  • LockSupport.park()
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 상태이다. 아래의 다섯가지 방법으로 TIMED_WAITING 상태에 진입 가능하다.

  • thread.sleep(long millis)
  • wait(int timeout) or wait(int timeout, int nanos)
  • thread.join(long millis)
  • LockSupport.parkNanos
  • LockSupport.parkUntil

baeldung의 예제 코드

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

스레드 실행이 종료되거나 비정상적으로 종료될 경우 해당 스레드는 TERMINATED 상태이다.

baeldung의 예제 코드

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
자바의신

0개의 댓글