본문 바로가기
[Naver Cloud Camp 7] 교육 정리

네이버 클라우드 캠프 56일차 230713

by 우기37 2023. 7. 13.

#1 교육정리

1) Thread 2

 

 

 

#2 Thread 2

어제는 쓰레드에 대한 전반적인 개념과 큰 범위의 용어들로 정리했었습니다.

오늘은 쓰레드의 사용법에 대한 조금 더 구체적인 내용을 코드 예제와 함께 정리해보겠습니다.

 

 

Thread의 계층도)

계층도에 들어가기전에 Thread는 지난 블로그에 정리한 것과 같이 아래와 같은 구조를 갖고 있습니다.

 

java.exe JVM에서 start()를 하면 부모 스레드인 main thread에서 main()을 호출합니다. 이렇기에 main()을 호출하는 것은 JVM 입니다.

또한, OS는 프로세스나 스레드를 동등한 자격으로 대우하여 CPU스케줄링 할 때, 차별을 두지 않습니다.

스레드는 프로세스와 동일한 자격으로 'CPU쟁탈전(Racing)'에 참여합니다.

 

 

 

JVM 메모리는 각 스레드에 저장된 JVM Stack을 JVM 메모리인 Method Area와 Heap에 공유합니다.

 

 

그리고 CPU 스케줄링은 아래와 같습니다.

CPU 스케줄링은 여러 프로세스를 돌아가면서 실행하는 알고리즘을 뜻하며, Round-Robin(Windows)방식과 Priority-Aging(Linux) 방식이 있습니다.

 

 

 

context-switching 방식도 있는데,

의미는 프로세스 실행 정보를 교환 한다는 의미입니다.

 

구조는 아래와 같으며, 파란글씨의 예를 들어보면 뷔폐에서 RAM은 음식 진열대 이고, 사람이 움직여서 앞접시 즉 메모리를 가지고 음식을 덜어서 갖고옵니다. 그러면 CPU 내에 있는 PC나 MAR, MBR, IR과 같은 레지스터를 수저와 포크 또는 젓가락 처럼 사용해서 먹는것입니다.

 

 

어느 정도 구조와 메커니즘에 대해서는 이해가 되었으니, 코드와 함께 계층도부터 알아보겠습니다.

 

현재의 실행 라인을 알아내기)

currentTread()를 통해서 현재 실행 중인 흐름(스레드)이 무엇인지 알 수 있습니다.

// 현재의 실행 라인을 알아내기
package com.eomcs.concurrent.ex2;

public class Exam0110 {

  public static void main(String[] args) {
    // JVM은 여러 개의 스레드를 실행한다.
    // main() 호출도 별도의 스레드가 실행한다.
    // 확인해보자!

    // 이 순간 실행 중인 흐름이 무엇인지 알고 싶다면?
    Thread t = Thread.currentThread();

    System.out.println("실행 흐름명 = " + t.getName());

    // 실행 흐름을 전문적인 용어로 "Thread(실 타래)"라 부른다.
    // JVM이 실행될 때 main() 메서드를 호출하는 실행 흐름(스레드)의 이름은 "main"이다.
  }
}
// JVM의 스레드 계층도:
// main(T)

 

스레드 그룹)

그레드는 그룹에 소속되기도 하고 main 스레드가 소속된 그룹은 "main" 그룹 입니다.

// 스레드 그룹
package com.eomcs.concurrent.ex2;

public class Exam0120 {

  public static void main(String[] args) {
    Thread main = Thread.currentThread();

    // 스레드는 그룹에 소속되기도 한다.
    // 현재 스레드의 소속 그룹을 알고 싶다면?
    ThreadGroup group = main.getThreadGroup();
    System.out.println("그룹명 = " + group.getName());

    // main() 메서드를 호출하는 스레드는 "main" 스레드이고,
    // "main" 스레드가 소속된 그룹은 "main" 그룹이다.
  }
}

// JVM의 스레드 계층도:
// main(TG)
// => main(T)

 

 

스레드 그룹에 소속된 하위 그룹들)

// 스레드 그룹에 소속된 하위 그룹들
package com.eomcs.concurrent.ex2;

public class Exam0140 {

  public static void main(String[] args) {
    Thread main = Thread.currentThread();
    ThreadGroup mainGroup = main.getThreadGroup();

    // 스레드 그룹에 소속된 하위 그룹을 알고 싶다면?
    ThreadGroup[] groups = new ThreadGroup[100];
    int count = mainGroup.enumerate(groups, false);
    // 두 번째 파라미터 값을 false로 지정하면,
    // 하위 그룹에 소속된 그룹들은 제외한다.
    // 즉, 현재 그룹에 소속된 하위 그룹의 목록만 가져오라는 뜻!

    System.out.println("main 그룹에 소속된 하위 그룹들:");
    for (int i = 0; i < count; i++)
      System.out.println("   => " + groups[i].getName());
  }

}

// JVM의 스레드 계층도:
// main(TG)
// => main(T)
// => 다른 하위 그룹은 없다!

 

 

스레드 그룹의 부모 그룹)

// 스레드 그룹의 부모 그룹
package com.eomcs.concurrent.ex2;

public class Exam0150 {

  public static void main(String[] args) {
    Thread main = Thread.currentThread();
    ThreadGroup mainGroup = main.getThreadGroup();

    // 스레드 그룹의 부모 그룹을 알고 싶다면?
    ThreadGroup parentGroup = mainGroup.getParent();
    System.out.printf("main 스레드 그룹의 부모: %s\n", parentGroup.getName());


    // "system" 그룹의 부모 그룹은?
    ThreadGroup grandparentGroup = parentGroup.getParent();
    if (grandparentGroup != null) {
      System.out.printf("%s 스레드 그룹의 부모: %s\n", 
          parentGroup.getName(), 
          grandparentGroup.getName());
    }
  }
}

// JVM의 스레드 계층도:
// system(TG)
// => main(TG)
// ...=> main(T)

 

 

"system" 스레드 그룹의 자식 그룹들)

// "system" 스레드 그룹의 자식 그룹들
package com.eomcs.concurrent.ex2;

public class Exam0160 {

  public static void main(String[] args) {
    Thread main = Thread.currentThread();
    ThreadGroup mainGroup = main.getThreadGroup();
    ThreadGroup systemGroup = mainGroup.getParent();

    ThreadGroup[] groups = new ThreadGroup[100];
    int count = systemGroup.enumerate(groups, false);

    System.out.println("system 스레드 그룹의 자식 그룹들:");
    for (int i = 0; i < count; i++) {
      System.out.println("   =>" + groups[i].getName());
    }
  }
}

// JVM의 스레드 계층도:
// system(TG)
// => main(TG)
// ...=> main(T) : main() 메서드를 호출한다.
// => InnocuousThreadGroup(TG)

 

 

"system" 스레드 그룹에 소속된 스레드들)

// "system" 스레드 그룹에 소속된 스레드들
package com.eomcs.concurrent.ex2;

public class Exam0170 {

  public static void main(String[] args) {
    Thread main = Thread.currentThread();
    ThreadGroup mainGroup = main.getThreadGroup();
    ThreadGroup systemGroup = mainGroup.getParent();

    Thread[] arr = new Thread[100];
    int count = systemGroup.enumerate(arr, false);

    System.out.println("system 스레드 그룹에 소속된 스레드들:");
    for (int i = 0; i < count; i++) {
      System.out.println("   =>" + arr[i].getName());
    }
  }
}

// JVM의 스레드 계층도:
// system(TG)
// => Reference Handler(T)
// => Finalizer(T)
// => Signal Dispatcher(T)
// => Attach Listener(T) => mac os에는 없습니다.
// => Notification Thread(T)
// => main(TG)
// ...=> main(T) : main() 메서드를 호출한다.
// => InnocuousThreadGroup(TG)

 

 

JVM의 전체 스레드 계층도)

// JVM의 전체 스레드 계층도
package com.eomcs.concurrent.ex2;

public class Exam0180 {

  public static void main(String[] args) {
    // JVM의 최상위 스레드 그룹인 system의 계층도 출력하기
    Thread mainThread = Thread.currentThread();
    ThreadGroup mainGroup = mainThread.getThreadGroup();
    ThreadGroup systemGroup = mainGroup.getParent();

    printThreads(systemGroup, "");
  }

  static void printThreads(ThreadGroup tg, String indent) {
    System.out.println(indent + tg.getName() + "(TG)");

    // 현재 스레드 그룹에 소속된 스레드들 출력하기
    Thread[] threads = new Thread[10];
    int size = tg.enumerate(threads, false);
    for (int i = 0; i < size; i++) {
      System.out.println(indent + "  ==> " + threads[i].getName() + "(T)");
    }

    // 현재 스레드 그룹에 소속된 하위 스레드 그룹들 출력하기
    ThreadGroup[] groups = new ThreadGroup[10];
    size = tg.enumerate(groups, false);
    for (int i = 0; i < size; i++) {
      printThreads(groups[i], indent + "  ");
    }
  }
}

// JVM의 스레드 계층도: (Oracle JDK 17 기준)
// system(TG)
//   ==> Reference Handler(T)
//   ==> Finalizer(T)
//   ==> Signal Dispatcher(T)
//   ==> Attach Listener(T) => mac os 에는 없습니다.
//   ==> Notification Thread(T)
//   ==> main(TG)
//         ==> main(T)
//   ==> InnocuousThreadGroup(TG)
//         ==> Common-Cleaner(T)

 

 

Thread의 생명주기)

// 스레드의 생명주기(lifecycle)
package com.eomcs.concurrent.ex4;

public class Exam0110 {
  public static void main(String[] args) {
    // 스레드의 생명주기
    // new Thread()    start()              sleep()/wait()
    //     준비 -------------------> Running ---------------> Not Runnable
    //                               ^  |    <---------------
    //                               |  |    timeout/notify()
    //                               X  |
    //                               |  |  run() 메서드 종료
    //                               |  V
    //                               Dead
    // Running 상태?
    // - CPU를 받아서 실행 중이거나 CPU를 받을 수 있는 상태
    //
    // Not Runnable 상태?
    // - CPU를 받지 않는 상태
    // 
    // run() 메서드 종료 후 다시 running 상태로 돌아갈 수 없다. 
    // => 새로 스레드를 만들어 실행하는 방법 밖에 없다!
    // 
    System.out.println("스레드 실행 전");
    new Thread() {
      @Override
      public void run() {
        for (int i = 0; i < 1000; i++) {
          System.out.println("===> " + i);
        }
      }
    }.start();

    System.out.println("스레드 실행 후");
    // main() 메서드의 호출이 끝나더라도 다른 스레드의 실행이 종료될 때까지 
    // JVM은 종료하지 않는다.
  }

}

 

실행결과)

===> 999 까지 실행됩니다.

 

 

스레드의 생명주기(lifecycle) - 죽은 스레드는 다시 살릴 수 없다.)

// 스레드의 생명주기(lifecycle) - 죽은 스레드는 다시 살릴 수 없다.
package com.eomcs.concurrent.ex4;

import java.util.Scanner;

public class Exam0111 {
  public static void main(String[] args) {
    System.out.println("스레드 시작시킴.");

    Thread t = new Thread(() -> { // Runnable 구현체를 정의하고 생성한다.
      for (int i = 0; i < 1000; i++) {
        System.out.println("===> " + i);
      }
      System.out.println("스레드의 run() 실행 종료!");
    });

    t.start();

    Scanner keyboard = new Scanner(System.in);
    keyboard.nextLine(); // 스레드가 종료될 때까지 시간을 벌기 위함.
    keyboard.close();

    // 죽은 스레드 객체를 또 실행할 수 없다.
    t.start(); // 예외 발생! ==> IllegalThreadStateException

    System.out.println("main() 종료!");
  }

}

 

 

스레드의 생명주기(lifecycle) - join()

// 스레드의 생명주기(lifecycle) - join()
package com.eomcs.concurrent.ex4;

public class Exam0120 {
  public static void main(String[] args) throws Exception {
    System.out.println("스레드 실행 전");

    Thread t = new Thread(() -> {
      for (int i = 0; i < 1000; i++) {
        System.out.println("===> " + i);
      }
    });

    t.start(); // 스레드를 생성하고 시작시킨다.

    t.join(); // t 스레드가 종료될 때까지 "main" 스레드는 기다린다.

    // 즉 t 스레드가 종료된 후 다음 코드를 실행한다.
    System.out.println("스레드 종료 후");

    // 스레드 종료 후 다시 시작시킨다면?
    // => IllegalThreadStateException 발생!
    // => 즉 종료된 스레드는 다시 running 할 수 없다.
    // t.start();

  }

}

 

 

스레드의 생명주기(lifecycle) - sleep()

// 스레드의 생명주기(lifecycle) - sleep()
package com.eomcs.concurrent.ex4;

public class Exam0130 {
  public static void main(String[] args) throws Exception {
    System.out.println("스레드 실행 전");

    new Thread() {
      @Override
      public void run() {
        System.out.println("Hello!");
      }
    }.start();

    // 3초 동안 not runnable 상태로 만든다.
    // => 즉 3초 동안 CPU가 놀고 있더라도 CPU를 사용하지 않는다.
    // => 3초가 지나면(timeout) 다시 "main" 스레드는 CPU를 받아 실행할 수 있다.
    // => sleep()을 호출하면 그 순간에 실행하는 스레드를 잠들게 한다.
    Thread.sleep(3000); // milliseconds

    System.out.println("스레드 실행 후");
  }

}

 

 

스레드의 생명주기(lifecycle) - running 상태 : CPU 쟁탈전(racing)

// 스레드의 생명주기(lifecycle) - running 상태 : CPU 쟁탈전(racing)
package com.eomcs.concurrent.ex4;

public class Exam0140 {
  public static void main(String[] args) throws Exception {

    class MyThread extends Thread {
      public MyThread(String name) {
        super(name);
      }
      @Override
      public void run() {
        for (int i = 0; i < 1000; i++)
          System.out.printf("%s %d\n", this.getName(), i);
      }
    }

    MyThread t1 = new MyThread("홍길동 =====>");
    MyThread t2 = new MyThread("오호라 ------------>");
    MyThread t3 = new MyThread("우헤헤 ##");

    // 스레드를 시작시키는 순간 running 상태로 접어든다.
    // running 상태는 실행하고 있는 상태 뿐만 아니라,
    // CPU를 받을 수 있는 상태이기도 하다. 
    // => CPU는 OS의 관리 정책(CPU Scheduling)에 따라 스레드나 프로세스에 배분된다. 
    //    물론 OS가 CPU를 배분한 후 임의시간 후에 
    //    다시 회수하여 다른 스레드(현재 스레드 포함)나 프로세스에 배분한다.
    //    때에 따라서 같은 스레드가 연속해서 배분 받는 경우도 있을 것이다.
    // 
    t1.start();
    t2.start();
    t3.start();

    for (int i = 0; i < 1000; i++)
      System.out.printf("main 스레드: %d\n", i);
  }

}

// 프로세스(스레드) 스케줄링
// => OS가 프로세스나 스레드에 CPU 사용을 배분하는 정책
// 1) Round-Robin 방식
//    - Windows 운영체제에서 사용하는 방식이다.
//    - 우선 순위 보다는 일정 시간 위주로 프로세스나 스레드에게 CPU를 배분하는 방식이다.
// 2) Priority + Aging 방식
//    - Unix나 Linux 운영체제에서 사용하는 방식이다.
//    - 우선 순위가 높은 프로세스나 스레드에게 CPU를 먼저 배분하는 방식이다.
//    - 우선 순위 배분 방식에서는 우선 순위가 낮은 경우 실행에서 소외되는 문제가 발생하기 때문에
//      우선 순위가 높은 프로세스나 스레드 때문에 실행 순서가 밀릴 때 마다 
//      원래의 낮은 순위를 높임으로써(aging) 결국에는 모든 프로세스와 스레드의 
//      실행을 완료할 수 있게 한다.
//
// "컨텍스트 스위칭(context switching)"
// - 동시에 여러 개의 프로세스나 스레드를 실행할 때
//   CPU 사용권을 뺏어 다른 프로세스나 스레드에게 주기 전에 
//   현재까지 실행한 코드의 위치 정보를 저장해야 한다.
//   또한 CPU 사용권을 주기 전에 그 프로세스나 스레드가 이전에 어디까지 실행했었는지
//   이전 실행 위치 정보를 로딩해야 한다.
//   즉 실행 위치에 대한 정보를 저장하고 로딩하는 것을 말한다.

 

 

스레드의 생명주기(lifecycle) - 우선 순위 조회)

// 스레드의 생명주기(lifecycle) - 우선 순위 조회
package com.eomcs.concurrent.ex4;

public class Exam0210 {

  public static void main(String[] args) throws Exception {
    // 스레드 우선 순위 정보
    //
    // => 스레드의 우선 순위 범위
    System.out.printf("우선 순위 범위: %d ~ %d\n", //
        Thread.MIN_PRIORITY, //
        Thread.MAX_PRIORITY);

    // => 스레드의 기본 우선 순위
    System.out.printf("우선 순위 기본값: %d\n", Thread.NORM_PRIORITY);

    // => "main" 스레드의 우선 순위 조회
    System.out.printf("main 스레드 우선 순위: %d\n", //
        Thread.currentThread().getPriority());

    class MyThread extends Thread {
      public MyThread(String name) {
        super(name);
      }

      @Override
      public void run() {
        for (int i = 0; i < 1000; i++)
          System.out.printf("%s %d\n", this.getName(), i);
      }
    }


    MyThread t1 = new MyThread("t1");

    // "t1" 스레드의 우선 순위 조회
    // => "main" 스레드를 실행하는 동안 만든 스레드는 "main"의 자식 스레드라 부른다.
    // => 자식 스레드는 부모 스레드의 우선 순위와 같은 값을 갖는다.
    // 그래서 "t1" 스레드는 "main"의 우선 순위 값과 같다.
    System.out.printf("%s 스레드 우선 순위: %d\n", t1.getName(), t1.getPriority());

    // 우선 순위가 높으면 CPU 사용 배분을 좀 더 자주 받는다.
    // => 스레드는 JVM에서 관리하는 것이 아니라 OS가 관리한다.
    // => 즉 OS의 스레드를 이용하는 것이다.
    // => 따라서 우선 순위에 따라 실행 스케줄을 어떻게 관리할지는 OS에 따라 다르다.
    // => Windows OS는 우선 순위를 크게 고려하지 않는다.
    // 그래서 Windows에서 실행할 때는 우선 순위에 영향을 적게 받을 것이다.
    // => Unix, Linux 계열 OS는 우선 순위를 고려한다.
    // 그래서 이런 OS에서 실행할 때는 우선 순위에 영향을 받을 것이다.
    //
    // 주의!
    // => Java 의 캐치프레이즈가 "Write Once, Run Anywhere!" 이다.
    // => 즉 OS에 상관없이 동일하게 동작하게 만드는 것이 자바의 목적이다.
    // => 그런데 우선 순위에 따라 실행률이 달라지고,
    // OS 마다 차이가 난다면, 자바의 목적에 부합하는 것이 아니다.
    // => 그래서 가능한 OS에 영향을 덜 받는 방식으로 코딩해야 한다.
    // => 이런 이유로 스레드를 다룰 때 우선 순위를 고려하는 방식으로
    // 프로그래밍을 하지 말라!
  }

}

 

실행결과)

 

 

스레드의 생명주기(lifecycle) - 우선 순위 설정)

// 스레드의 생명주기(lifecycle) - 우선 순위 설정
package com.eomcs.concurrent.ex4;

public class Exam0220 {
  public static void main(String[] args) throws Exception {
    class MyThread extends Thread {
      public MyThread(String name) {
        super(name);
      }

      @Override
      public void run() {
        long startTime = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++)
          Math.asin(38.567); // 시간 끌기 용. 왜? 부동소수점 연산은 시간을 많이 소요.
        long endTime = System.currentTimeMillis();
        System.out.printf("MyThread = %d\n", endTime - startTime);
      }
    }

    // main 스레드의 우선 순위를 가장 작은 1로 설정한다.
    Thread.currentThread().setPriority(1);

    MyThread t1 = new MyThread("t1");
    t1.setPriority(10);
    // 유닉스 계열의 OS는 스케줄링에서 우선 순위를 고려하여 CPU를 배분한다.
    // 그러나 Windows OS는 우선 순위를 덜 고려하여 CPU를 배분한다.
    // 그러다보니 우선 순위를 조정하여 작업을 처리하도록 프로그램을 짜게 되면,
    // 유닉스 계열에서 실행할 때는 의도한 대로 동작할지 모르지만,
    // 윈도우에서는 의도대로 동작하지 않을 것이다.
    // 따라서 프로그램을 짤 때 스레드의 우선 순위를 조정하는 방법에 의존하지 말라!

    System.out.printf("main 스레드 우선 순위: %d\n", Thread.currentThread().getPriority());

    System.out.printf("%s 스레드 우선 순위: %d\n", t1.getName(), t1.getPriority());


    // t1 스레드 작업 시작
    t1.start();

    // main 스레드 작업 시작
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++)
      Math.asin(38.567); // 부동 소수점 연산을 수행하는 코드를 넣어서 실행 시간을 약간 지연시킨다.
    long endTime = System.currentTimeMillis();
    System.out.printf("main = %d\n", endTime - startTime);
  }
}

 

 

실행결과)