[JAVA] Thread, JVM

zirryo·2022년 7월 19일
0

⚡️ STUDY

목록 보기
10/15
post-thumbnail
week 4 / 22.07.19

Thread

📀 스레드

프로세스
컴퓨터에서 실행 중인 애플리케이션을 의미하며,
실행 시 운영체제로부터 필요한 만큼의 메모리를 할당받아 프로세스가 됨.
프로세스는 데이터, 컴퓨터 자원, 스레드로 구성되어 있음.

  • 싱글 스레드 프로세스 : 단 하나의 스레드를 가지는 프로세스
  • 멀티 스레드 프로세스 : 여러 개의 스레드를 가지는 프로세스

스레드는 프로세스 내에서 실행되는 소스 코드의 실행 흐름을 의미함.

  • 메인 스레드 (Main Thread)
    • 자바 애플리케이션을 실행하면 가장 먼저 실행되는 메서드인 main 메서드이며, 메인 스레드가 main 메서드를 실행시킴.
    • 메인 스레드는 main 메서드의 코드를 처음부터 끝까지 순차적으로 실행시키며, 코드의 끝을 만나거나 return문을 만나면 실행 종료.
      ⠀⠀⠀
  • 멀티 스레드 (Multi Thread)
    • 멀티 스레딩 : 여러 스레드가 동시에 작업을 수행할 수 있음을 의미하며, 멀티 태스킹을 구현하는 데 핵심적인 역할을 함.
      ⠀⠀⠀

작업 스레드 생성과 실행
작업 스레드가 수행할 코드를 작성하고, 작업 스레드를 생성하여 실행시키는 것.

  • 자바는 객체지향 언어이므로 모든 자바 코드는 클래스 안에 작성됨.
  • 스레드가 수행할 코드도 클래스 내부에 작성해야 함.
  • run 메서드 내에 스레드가 처리할 작업을 작성하도록 규정되어 있음.
  • 다음의 두 가지 방법을 통해 작업 스레드를 생성하고 실행함.
  1. Runnable 인터페이스를 구현한 객체에서 run()을 구현하는 방법
    public class ThreadExample1 {
        public static void main(String[] args) {
        	// Runnable 인터페이스를 구현한 객체 생성
            Runnable task1 = new ThreadTask1();
            // 구현 객체를 인자로 전달하면서, Thread 클래스를 인스턴스화 하여 객체 생성
            Thread thread1 = new Thread(task1);
    ⠀⠀⠀
            thread1.start(); // 작업 메서드 실행
    ⠀⠀⠀
            // 메인 메서드가 수행할 코드
            for (int i = 0; i < 30; i++) {
                System.out.print("@");
            }
        }
    }
    // Runnable 을 구현하는 임의의 클래스 생성⠀⠀⠀
    class ThreadTask1 implements Runnable {
        public void run() { // 반드시 run()을 구현해야함.
            for (int i = 0; i < 30; i++) {
                System.out.print("#");
            } // 작업 스레드가 수행할 코드
        }
    }
    // 출력 (실제로는 개행없이 일렬로 출력됨)
    // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    // ##############################
    // i의 범위를 늘릴경우 @,#이 교차출력되며, 매번 실행 결과가 조금씩 달라짐.
    /* 메인 스레드와 작업 스레드가 동시에 병렬로 실행되면서, 
       main 메서드와 run()메서드의 코드가 실행됨. */
    // 1-1 익명 Runnable 구현 객체를 활용하여 스레드를 생성하는 방법
    public class ThreadExample1 {
        public static void main(String[] args) {		
            Thread thread1 = new Thread(new Runnable() {
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        System.out.print("#");
                    }
                }
            });
    ⠀⠀⠀
            thread1.start();
    ⠀⠀⠀
            for (int i = 0; i < 100; i++) {
                System.out.print("@");
            }
        }
    }
  2. Thread 클래스를 상속 받은 하위 클래스에서 run()을 구현하는 방법
    public class ThreadExample2 {
        public static void main(String[] args) {
            // Thread 클래스를 상속받은 클래스를 인스턴스화 하여 객체 생성
            Thread thread2 = new ThreadTask2(task1);
    ⠀⠀⠀
            thread2.start(); // 작업 메서드 실행
    ⠀⠀⠀
            // 메인 메서드가 수행할 코드
            for (int i = 0; i < 30; i++) {
                System.out.print("@");
            }
        }
    }
    // Thread을 상속받는 하위 클래스 생성⠀⠀⠀
    class ThreadTask2 extends Thread {
        public void run() { // 반드시 run()을 구현해야함.
            for (int i = 0; i < 30; i++) {
                System.out.print("#");
            } // 작업 스레드가 수행할 코드
        }
    }
    // 출력은 runnable 과 유사함.
    /* 메인 스레드와 작업 스레드가 동시에 병렬로 실행되면서, 
       main 메서드와 run()메서드의 코드가 실행됨. */
    // 2-1 익명 Thread 하위 객체를 활용하여 스레드를 생성하는 방법
    public class ThreadExample2 {
        public static void main(String[] args) {		
            Thread thread2 = new Thread() {
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        System.out.print("#");
                    }
                }
            };
    ⠀⠀⠀
            thread2.start();
    ⠀⠀⠀
            for (int i = 0; i < 100; i++) {
                System.out.print("@");
            }
        }
    }

스레드의 이름
메인스레드는 main, 추가적으로 생성한 스레드는 기본적으로 Thread-n 사용.

  • 스레드의 이름 조회하기 스레드의_참조값.getName()
    public class GetThreadName {
        public static void main(String[] args) {
            Thread thread3 = new Thread() {
                public void run() {
                    System.out.println("Thread Name");
                }
            };
            thread3.start();
    ⠀⠀⠀
            System.out.println("thread3.getName() = " + thread3.getName());
        }
    }
    /* 출력
    Thread Name
    thread3.getName() = Thread-0 
    */
  • 스레드의 이름 설정하기 스레드의_참조값.setName()
    public class SetThreadName {
        public static void main(String[] args) {
            Thread thread4 = new Thread() {
                public void run() {
                    System.out.println("Set Thread Name");
                }
            };
            thread4.start();
    		System.out.println("thread4.getName() = " + thread4.getName());
            thread4.setName("t4");
            System.out.println("thread4.getName() = " + thread4.getName());
        }
    }
    /* 출력
    Set Thread Name
    thread4.getName() = Thread-0
    thread4.getName() = t4
    */
  • 스레드의 인스턴스 주소값 Thread.currentThread()
    • 위의 메서드는 모두 Thread 클래스로부터 인스턴스화된 인스턴스의 메서드이므로, 호출시 스레드 객체의 참조 필요.
    • 다만, 스레드의 주소값은 Thread 클래스의 정적 메서드를 통해 얻음.
      public class ThreadAddress {
          public static void main(String[] args) {
              Thread thread5 = new Thread() {
                  public void run() {
                      System.out.println(Thread.currentThread().getName());
                  }
              };
              thread5.start();
              System.out.println(Thread.currentThread().getName());
          }
      }
      /* 출력
      main
      Thread-0 
      */

스레드의 동기화
멀티 스레드 프로세스의 두 스레드가 동일한 데이터를 공유하면서 문제가 발생할 수 있기 때문에, 이를 막기 위해 스레드 동기화를 해야 함.

  • 임계 영역 (Criticak Section)
    : 오로지 하나의 스레드만 코드를 실행할 수 있는 코드 영역
  • (Lock)
    : 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한
    ⠀⠀⠀
  • 스레드 동기화 방법
    1. 임계 영역으로 설정된 객체가 다른 스레드에 의해 작업이 이루어지고 있지 않을 때, 임의의 스레드 A는 해당 객체에 대한 락을 획득하여 임계 영역 내의 코드 실행 가능.
    2. 스레드 A가 임계 영역 내의 코드를 실행 중일 때에는 다른 스레드들은 락이 없으므로 이 객체의 임계 영역 내의 코드 실행 불가.
    3. 스레드 A가 임계 영역 내의 코드를 모두 실행하면 락을 반납. 다른 스레드들 중 하나가 락을 획득하여 임계 영역 내의 코드를 실행 가능.
      ⠀⠀⠀
  • 메서드 전체 임계 영역 지정 synchronized
    • 반환 타입 좌측에 synchronized 키워드 작성하여 메서드 전체를 임계 영역으로 설정할 수 있음.
    • 메서드 전체를 임계 영역으로 지정하면 메서드가 호출되었을 때, 메서드를 실행할 스레드는 메서드가 포함된 객체의 락을 얻음.
        // 이해를 돕기 위한 예시 코드
      class Account {
      	...
          /* withdraw()메서드 호출시 실행하는 스레드는 withdraw()가 포함된 객체의 락을 얻으며, 
          해당 스레드가 락을 반납하기 이전에 다른 스레드는 해당 메서드의 코드를 실행하지 못함 */
      	public synchronized boolean withdraw(int money) {
      	    if (balance >= money) {
      	        try { Thread.sleep(1000); } catch (Exception error) {}
      	        balance -= money;
      	        return true;
      	    }
      	    return false;
      	}
      }
  • 특정 영역을 임계 영역으로 지정 synchronized
    • synchronized 키워드와 함께 소괄호()안에 해당 영역이 포함된 객체를 넣고, 중괄호 {}로 블럭을 열어, 블럭 내에 코드를 작성.
      class Account {
      	...
      	public boolean withdraw(int money) {
      		synchronized (this) {
      			if (balance >= money) {
      			        try { Thread.sleep(1000); } catch (Exception error) {}
      			        balance -= money;
      			        return true;
      			}
      			return false;
      		}
      	}
      }

스레드의 상태 / 실행제어

  • 스레드의 상태 변화 흐름도

  • start()
    • 스레드를 실행 대기 상태로 만들어 주는 메서드이며, 운영체제가 적절한 때에 대기 상태인 스레드를 실행.
      ⠀⠀
  • Thread.sleep(long milliSecond)
    • milliSecond 동안 스레드 일시 정지, 약간의 시간 오차 있음.
    • Thread의 클래스 메서드이므로, Thread.sleep(1000);와 같이 클래스를 통해 호출하는 것을 권장.
    • 메서드 호출시 실행 상태에서 일시정지 상태로 전환.
    • 실행 대기 상태로 복귀 : 요구 시간 경과 / interrupt() 호출.
    • interrupt() 사용시, try-catch 문으로 sleep을 감싸서 사용.
      ⠀⠀
  • interrupt()
    • sleep() , wait() , join() 으로 일시 정지 상태인 스레드를 실행 대기 상태로 복귀
    • 멈춰있지 않은 스레드에서 멈춰 있는 스레드.interrupt() 호출시, 메서드(sleep, wait, join)에서 예외 발생, 이에 따른 일시 정지 해제.
      ⠀⠀
  • yield()
    • 다른 스레드에게 자신의 실행 시간 양보
    • ex) 스케줄러에 의해 3초를 할당 받은 스레드가 1초 동안 작업을 수행하다가 yield() 를 호출하면 남은 2초는 다음 스레드에게 양보.
      public void run() {
      		while (true) { 
              // else 문이 없을 경우 example==false여도 무의미한 while문 반복.
      				if (example) {
      						...
      				} // 실행대기 상태로 바뀌며, 자신에게 남은 시간을 양보.
      				else Thread.yield();
      		}
      }
  • join(long milliSecond)
    • 특정 스레드가 작업하는 동안에 자신을 일시 중지 상태로 만듦.
    • 인자로 시간을 밀리초 단위로 전달할 수 있음.
    • 실행 대기 상태로 복귀 : 요구 시간 경과 / interrupt() 호출 / 호출 시 지정했던 다른 스레드가 모든 작업 마침.
    • sleep 과 상당 부분 유사하나, join은 특정 스레드에 대해 동작하는 인스턴스 메서드인 점에서 차이를 보임.
    • 특정스레드.join(); 의 형태로 호출.
      ⠀⠀
  • wait() notify()
    • 스레드 간의 교대 작업에 사용.
    • 스레드의 협업 플로우
       // 스레드A와 스레드B가 공유 객체를 두고 협업하는 상황
      1. 스레드A가 공유 객체에 자신의 작업을 완료
      2. 스레드B와 교대하기 위해 notify()를 호출
      3. 스레드B가 실행 대기 상태가 되며, 곧 실행됨.
      4. 스레드Await()을 호출하며 자기 자신을 일시 정지 상태로 만듦
      5. 스레드B가 작업을 완료하면 notify()를 호출하여 작업을 중단
      6. 스레드A를 다시 실행 대기 상태로 복귀시킴.
      7. 스레드Bwait()을 호출하며 자기 자신을 일시 정지 상태로 만듦
       // 두 스레드는 공유 객체에 대해 서로 배타적으로 접근하며 협업

JVM

💿 자바가상머신

JVM(Java Virtual Machine)은 자바 프로그램을 실행시키는 도구
자바로 작성한 소스 코드를 해석해 실행하는 별도의 프로그램

특징

  • 자바가 운영체제로부터 독립적일 수 있도록 함.
  • 자바 프로그램과 운영체제 사이에서 통역하는 역할.
  • 운영체제 별로 적합한 버전의 JVM을 사용하면, 자바 소스 코드를 각각의 운영체제에 맞게 변환하여 실행시켜 줌.
    ⠀⠀⠀

소스코드 실행 flow

  1. 컴파일러 실행, 컴파일 진행. .java 에서 .class (바이트 코드 파일)로 확장자 변환
  2. JVM은 운영 체제로부터 소스 코드 실행에 필요한 메모리를 할당받음.(Rumtime Data Area)
  3. 클래스 로더(Class Loader)가 바이트 코드 파일을 JVM 내부로 불러들여 런타임 데이터 영역에 적재. 소스 코드를 메모리에 로드.
  4. 실행 엔진(Execution Engine)이 런타임 데이터 영역에 적재된 바이트 코드를 실행.
    • 인터프리터(Interpreter)를 통해 코드를 한 줄씩 기계어로 번역하고 실행.
    • JIT Compiler(Just-In-Time Compiler)를 통해 바이트 코드 전체를 기계어로 번역 후 실행.
  5. 기본적으로 인터프리터로 바이트 코드를 실행시키다가, 특정 바이트 코드가 자주 실행되면 JIT Compiler를 통해 실행.
    ⠀⠀⠀

Stack / Heap

  • Rumtime Data Area
    • JVM에 Java 프로그램이 로드되어 실행될 때 특정 값 및 바이트코드, 객체, 변수등과 같은 데이터들이 저장되는 메모리 영역
    • 아래와 같이 크게 5가지로 분류됨.
      Method areaHeap areaStack areaPC
      register
      Native
      method stack
  • Stack Area
    • Stack은 일종의 자료구조(프로그램이 데이터를 저장하는 방식)
    • LIFO(Last In, First Out) 원칙에 의해 작동.
    • Method Frame이 호출되는 순서대로 쌓이며, Method의 동작이 완료되면 역순으로 제거됨.
      빈 stack->->메서드2 out
      호출된 메서드2완료된 메서드2
      ⠀⠀⠀⠀⠀⠀⠀⠀⠀호출된 메서드1호출된 메서드1호출된 메서드1
  • Heap Area
    • JVM에는 단 하나의 Heap 영역이 존재하며, JVM이 작동 시 이를 자동 생성
    • heap 영역 안에 객체나 인스턴스 변수, 배열이 저장됨.
    • 인스턴스는 Heap 영역에 생성되며, 인스턴스가 생성된 위치의 주소값을 참조변수에게 할당. (참조변수는 Stack 영역에 선언됨)
      ⠀⠀⠀

Garbage Collection
프로그램에서 더 이상 사용하지 않는 객체를 찾아 삭제하거나 제거하여 메모리를 확보하는 것

  • 가비지 컬렉션
    • 가비지 컬렉터는 이렇게 아무한테도 참조되고 있지 않은 객체 및 변수들을 검색하여 메모리에서 점유를 해제
    • 메모리 공간을 확보하여 효율적으로 메모리를 사용할 수 있게 함.
      Student student = new Student();
      student.setName("페퍼");
      student = null; // 인스턴스와 참조변수 간의 연결이 끊김, 가비지 발생
      student = new Student(); 
      student.setName("민트");
  • 힙 메모리 구조
    • Heap 영역은 객체는 대부분 일회성이며, 메모리에 남아 있는 기간이 대부분 짧다는 전제로 설계되어 있음.
    • 객체 생존 기간에 따라 Heap 영역이 Young, Old영역 으로 나뉨.
      HeapHeapHeapHeap
      Minor GC
      활동영역
      Major GC
      활동영역
      EdenSurvival 0Survival 1Old
      YoungYoungYoungOld
    • Minor GC(Garbage collector) : Young 영역에서는 새롭게 생성된 객체가 할당되는 곳이며, 많은 객체가 생성되었다 사라짐.
    • Major GC(Garbage collector) : Old 영역은 Young 영역에서 상태를 유지하고 살아남은 객체들이 복사되는 곳으로, 보통 Young 영역보다 크게 할당되고 크기가 큰 만큼 가비지는 적게 발생함.
      ⠀⠀⠀
  • 가비지 컬렉션 발생 과정
    영역마다 세부적인 동작 방식은 다르지만 기본적으로 가비지 컬렉션이 실행 될 때는 다음의 2가지 단계를 따름.
    1. Stop The World
    • 가비지 컬렉션을 실행시키기 위해 JVM이 애플리케이션의 실행을 멈추는 작업
    • 가비지 컬렉션이 실행될때 가비지 컬렉션을 실행하는 스레드를 제외한 모든 스레드들의 작업은 중단되고, 가비지 정리가 완료되면 재개됨.
    1. Mark and Sweep
    • Mark사용되는 메모리와 사용하지 않는 메모리를 식별하는 작업
    • Sweep은 사용되지 않음으로 식별된 메모리를 해제하는 작업.

0개의 댓글