TIL - Thread, JVM

DY_DEV·2023년 3월 10일
0

TIL

목록 보기
7/17

0310 TIL 정리 및 요약

public class ThreadExample2 {
    public static void main(String[] args) {

        ThreadTask2 thread2 = new ThreadTask2();

        // 작업 스레드를 실행시켜, run() 내부의 코드를 처리하도록 합니다.
        thread2.start();

        // 반복문 추가
        for (int i = 0; i < 100; i++) {
            System.out.print("@");
        }
    }
}

class ThreadTask2 extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
}

프로세스: 실행중인 애플리케이션이다. 데이터, 컴퓨터 자원, 스레드로 구성된다.

스레드: 프로세스내에서 실행되는 소스 코드의 실행 흐름

  • 스레드는 싱글 스레드 프로세스와 멀티 스레드 프로세스로 나뉜다.
  • 스레드는 데이터와 애플리케이션이 확보한 자원을 활용하여 소스 코드를 실행
  • 하나의 코드 실행 흐름이다.

메인 스레드

  • 자바 애플리케이션을 실행하면 가장 먼저 실행되는 메서드
  • 메인 스레드가 main 메서드를 실행시켜준다. (처음부터 끝까지 순차적으로 실행, 코드가 끝나거나 return 문을 만나면 실행을 종료한다. )
  • 메인 스레드에서 또다른 스레드를 생성하여 실행시키면 해당 애플리케이션은 멀티 스레드로 동작하게 된다.

스레드의 생성과 실행

  • 메인 스레드 외 별도의 작업 스레드를 활용: 작업 스레드가 수행할 코드를 작성하고, 작업 스레드를 생성하여 실행시키는 것
  • 자바는 객체지향이라 모든 코드는 클래스 내부에 작성되므로 스레드가 수행할 코드 또한 클래스 내부에 작성한다. > run()이라는 메서드 내에 스레드가 처리할 작업을 작성하도록 규정

run() : Runnable 인터페이스와 Thread 클래스에 정의

  • 두 가지 방법으로 구현가능 (스레드를 생성하고 실행)
    • Runnable 인터페이스를 구현한 객체에서 run()
    • Thread 클래스를 상속 받은 하위 클래스에서 run()을 구현
  1. Runnable 인터페이스를 구현한 객체에서 run()
  • run() 메서드 바디에 스레드가 수행할 작업 내용 작성
class ThreadTask1 implements Runnable {

    // run() 메서드 바디에 스레드가 수행할 작업 내용 작성
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
}
  • Runnable 구현 객체를 인자로 전달하면서 Thread 클래스를 인스턴스화
public class ThreadExample1 {
    public static void main(String[] args) {

        // Runnable 인터페이스를 구현한 객체 생성
        Runnable task1 = new ThreadTask1();

        // Runnable 구현 객체를 인자로 전달하면서 Thread 클래스를 인스턴스화하여 스레드를 생성
        Thread thread1 = new Thread(task1);

        // 위의 두 줄을 아래와 같이 한 줄로 축약할 수도 있습니다.
        // Thread thread1 = new Thread(new ThreadTask1());

    }
}

class ThreadTask1 implements Runnable {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
}
  • 이후 start() 메서드를 통해 스레드를 실행시킨다.
public class ThreadExample1 {
    public static void main(String[] args) {
        Runnable task1 = new ThreadTask1();
        Thread thread1 = new Thread(task1);

        // 작업 스레드를 실행시켜, run() 내부의 코드를 처리하도록 합니다.
        thread1.start();
				// 차이 확인을 위해 예제코드 추가 
				for (int i = 0; i < 100; i++) {
            System.out.print("@");
    }
}

class ThreadTask1 implements Runnable {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
}
  • 실행 시 #,@가 번갈아 가면서 출력되는 걸 확인할 수 있다. @는 메인 스레드의 반복문 코드 실행에 의해 출력되고, #는 작업 스레드의 반복문에 의해 실행되었다. > 메인 스레드와 작업 스레드가 동시에 병렬로 실행된 것을 확인.
  1. Thread 클래스를 상속 받은 하위 클래스에서 run()을 구현
  • Thread 클래스를 상속 받는 하위 클래스를 만든다.
  • Thread 클래스에는 run() 메서드가 정의되어져 있으며, 따라서 run() 메서드를 오버라이딩.
public class ThreadExample2 {
    public static void main(String[] args) {

        ThreadTask2 thread2 = new ThreadTask2();

        // 작업 스레드를 실행시켜, run() 내부의 코드를 처리하도록 합니다.
        thread2.start();

        // 반복문 추가
        for (int i = 0; i < 100; i++) {
            System.out.print("@");
        }
    }
}

class ThreadTask2 extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print("#");
        }
    }
}

익명 객체를 사용하여 스레드 생성하고 실행하기

  • 익명객체란?
  • 사용 목적 : UI 이벤트 처리 객체 / 스레드 객체를 간편하게 생성하기 위해 많이 활용
  • 사용 용도 : 필드 / 로컬변수 의 초기값, 매개변수의 매개값으로 사용
  • 단독으로 생성 불가, 클래스 상속 or 인터페이스 구현해서 사용 가능
  • 재사용 목적이 아닌 1번만 사용 하려고 할 때 쓴다.
public class ThreadExample1 {
    public static void main(String[] args) {

        // 익명 Runnable 구현 객체를 활용하여 스레드 생성
        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("@");
        }
    }
}
  • Thread 익명 하위 객체를 활용한 스레드 생성 및 실행
public class ThreadExample2 {
    public static void main(String[] args) {

        // 익명 Thread 하위 객체를 활용한 스레드 생성
        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 ThreadExample3 {
    public static void main(String[] args) {

        Thread thread3 = new Thread(new Runnable() {
            public void run() {
                System.out.println("Get Thread Name");
            }
        });

        thread3.start();

        System.out.println("thread3.getName() = " + thread3.getName());
    }
}

출력 결과

thread3.getName() = Thread-0

Process finished with exit code 0

스레드의 이름 설정하기

스레드의_참조값.setName()

public class ThreadExample4 {
    public static void main(String[] args) {

        Thread thread4 = new Thread(new Runnable() {
            public void run() {
                System.out.println("Set And Get Thread Name");
            }
        });

        thread4.start();

        System.out.println("thread4.getName() = " + thread4.getName());

        thread4.setName("Code States");

        System.out.println("thread4.getName() = " + thread4.getName());
    }
}

스레드 인스턴스의 주소값 얻기

  • 위 두 메서드는 모두 Thread 클래스로부터 인스턴스화된 인스턴스의 메서드이므로, 호출할 때에 스레드 객체의 참조가 필요
  • 실행 중인 스레드의 주소값을 사용해야 하는 상황이 발생한다면 Thread 클래스의 정적 메서드인 currentThread()를 사용
ublic class ThreadExample1 {
    public static void main(String[] args) {

        Thread thread1 = new Thread(new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        });

        thread1.start();
        System.out.println(Thread.currentThread().getName());
    }
}

출력결과

  • /Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home/bin/java -javaagent:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/lib/idea_rt.jar=52293:/Users/0hyun.cho/Library/Application Support/JetBrains/Toolbox/apps/IDEA-C/ch-0/221.5080.210/IntelliJ IDEA CE.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/0hyun.cho/study/example/out/production/classes ThreadExample1
    main
    Thread-0
    
    Process finished with exit code 0

스레드 동기화

멀티 스레드 프로세스의 경우, 두 스레드가 동일한 데이터를 공유하게 되어 문제가 발생할 수 있습니다.

try { Thread.sleep(1000); } catch (Exception error) {}

  • try { Thread.sleep(1000); } catch (Exception error) {}
    • Thread.sleep(1000);
      • 스레드를 일시 정지시키는 메서드입니다. 어떤 스레드가 일시 정지되면, 대기열에서 기다리고 있던 다른 스레드가 실행된다.
      • 또한, Thread.sleep()은 반드시 try … catch문의 try 블럭 내에 작성해주어야 한다.
    • try { … } catch ( ~ ) { … }
      • try … catch문은 예외 처리에 사용되는 문법이다.
      • 쉽게 설명하자면, try의 블록 내의 코드를 실행하다가 예외 또는 에러가 발생하면 catch문의 블럭에 해당하는 내용을 실행하라는 의미가 된다.
      • Thread.sleep(1000);의 동작을 위해 형식적으로 사용한 문법 요소.

임계 영역(Critical section)과 락(Lock)

임계 영역은 오로지 하나의 스레드만 코드를 실행할 수 있는 코드 영역을 의미
하며, 락은 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한을 의미한다.

특정 코드 구간을 임계 영역으로 설정할 때에는 synchronized라는 키워드를 사용한다. synchronized 키워드는 두 가지 방법으로 사용할 수 있다.

  1. 메서드 전체를 임계 영역으로 지정하기
  • 메서드의 반환 타입 좌측에 synchronized 키워드를 작성하면 메서드 전체를 임계 영역으로 설정할 수 있다.
  • 메서드 전체를 임계 영역으로 지정하면 메서드가 호출되었을 때, 메서드를 실행할 스레드는 메서드가 포함된 객체의 락을 얻는다.
  1. 특정한 영역을 임계 영역으로 지정하기
  • synchronized키워드와 함께 소괄호(()) 안에 해당 영역이 포함된 객체의 참조를 넣고, 중괄호({})로 블럭을 열어, 블럭 내에 코드를 작성
class Account {
	...
	public synchronized boolean withdraw(int money) {
	    if (balance >= money) {
	        try { Thread.sleep(1000); } catch (Exception error) {}
	        balance -= money;
	        return true;
	    }
	    return false;
	}
}
  • 임계 영역으로 설정한 블럭의 코드로 코드 실행 흐름이 진입 할 때, 해당 코드를 실행하고 있는 스레드가 this에 해당하는 객체의 락을 얻고, 배타적으로 임계 영역 내의 코드를 실행
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;
			}
	}
}

JVM

C++의 문제점: os로부터 비독립적인 특징. > 이걸 해결하기 위해 자바가 만들어졌고, JVM이라는 프로그램을 통해 구현된다.

  • JVM(Java Virtual Machine)은 자바 프로그램을 실행시키는 도구다. 즉, JVM은 자바로 작성한 소스 코드를 해석해 실행하는 별도의 프로그램이다.
  • JVM은 각 운영체제에 적합한 버전이 존재합니다. 즉, Windows용 JVM, Mac OS용 JVM, Linux용 JVM이 따로 존재한다.

JVM 구조

자바로 소스 코드를 작성하고 실행하면, 먼저 컴파일러가 실행되면서 컴파일이 진행된다. 컴파일의 결과로 .java 확장자를 가졌던 자바 소스 코드가 .class 확장자를 가진 바이트 코드 파일로 변환된다.

이후, JVM은 운영 체제로부터 소스 코드 실행에 필요한 메모리를 할당받고 그것이 바로 위 그림 상의 런타임 데이터 영역(Rumtime Data Area)이다.

그 다음에는 클래스 로더(Class Loader)가 바이트 코드 파일을 JVM 내부로 불러들여 런타임 데이터 영역에 적재시킨다. 자바 소스 코드를 메모리에 로드시키는 것.

로드가 완료되면 이제 실행 엔진(Execution Engine)이 런타임 데이터 영역에 적재된 바이트 코드를 실행.

이 때, 실행 엔진은 두 가지 방식으로 바이트 코드를 실행.

  1. 인터프리터(Interpreter)를 통해 코드를 한 줄씩 기계어로 번역하고 실행시키기
  2. JIT Compiler(Just-In-Time Compiler)를 통해 바이트 코드 전체를 기계어로 번역하고 실행시키기

실행 엔진은 기본적으로 1번의 방법을 통해 바이트 코드를 실행시키다가, 특정 바이트 코드가 자주 실행되면 해당 바이트 코드를 JIT Compiler를 통해 실행.

즉, 중복적으로 어떤 바이트 코드가 등장할 때, 인터프리터는 매 번 해당 바이트 코드를 해석하고 실행하지만, JIT 컴파일러가 동작하면 한 번에 바이트 코드를 해석하고 실행시킨다.

Stack과 Heap

JVM 메모리 구조

STACK

Last In First Out

  • JVM 내부에서 STACK의 작동
    • 메서드가 호출되면 그 메서드를 위한 공간인 Method Frame이 생성된다. 메서드 내부에서 사용하는 다양한 값들이 있는데 참조변수, 매개변수, 지역변수, 리턴값 및 연산시 일어나는 값들이 임시로 저장.

HEAP

JVM이 작동되면 이 영역은 자동 생성

이 영역안에 객체나 인스턴스 변수, 배열이 저장

Person person = new Person();

위의 예시에서 new Person()이 실행되면 Heap 영역에 인스턴스가 생성되며, 스턴스가 생성된 위치의 주소값을 person에게 할당해주는데, 이 person은 Stack 영역에 선언된 변수다.

객체를 다룬다는 것은 Stack 영역에 저장되어 있는 참조 변수를 통해 Heap 영역에 존재하는 객체를 다룬다는 의미가 된다.

정리하자면, Heap 영역은 실제 객체의 값이 저장되는 공간이다.

**Garbage Collection**

메모리를 자동으로 관리하는 프로세스. 프로그램에서 더 이상 사용하지 않는 객체를 찾아 삭제하거나 제거해 메모리를 확보하는 것을 의미.

  • 참조변수에 null이 할당된 경우 > 기존에 가리키던 인스턴스와 참조변수간의 연결이 끊어짐. 또는 아무도 인스턴스를 참조하지 않는 경우 가비지 컬렉션을 사용한다.
  • 가비지 컬렉터는 이렇게 아무한테도 참조되고 있지 않은 객체 및 변수들을 검색하여 메모리에서 점유를 해제하며, 그럼으로써 메모리 공간을 확보하여 효율적으로 메모리를 사용할 수 있게 해준다.

가비지 컬렉션 동작방식

  • JVM의 Heap 영역은 객체는 대부분 일회성 > 메모리에 남아 있는 기간이 대부분 짧다는 전제로 설계
  • 따라서 객체가 얼마나 살아있냐에 따라서 Heap 영역 안에서도 영역을 나누게 되는데 Young, Old영역 이렇게 2가지로 나뉜다.

Young 영역에서는 새롭게 생성된 객체가 할당되는 곳이고 여기에는 많은 객체가 생성되었다 사라지는 것을 반복한다.

이 영역에서 활동하는 가비지 컬렉터를 Minor GC라고 부른다.

Old 영역에서는 Young영역에서 상태를 유지하고 살아남은 객체들이 복사되는 곳으로 보통 Young 영역보다 크게 할당되고 크기가 큰 만큼 가비지는 적게 발생한다.

이 영역에서 활동하는 가비지 컬렉터를 Major GC라고 부른다.

가비지 컬렉션 실행 2가지 단계

  1. stop the world : 가비지 컬렉션을 실행시키기 위해 JVM이 애플리케이션의 실행을 멈추는 작업. 가비지 컬렉션이 실행될때 가비지 컬렉션을 실행하는 스레드를 제외한 모든 스레드들의 작업은 중단되고, 가비지 정리가 완료되면 재개
  2. mark and sweep : 사용되는 메모리와 사용하지 않는 메모리를 식별하는 작업을 의미하며, Sweep은 Mark단계에서 사용되지 않음으로 식별된 메모리를 해제하는 작업을 의미

Stop The World를 통해 모든 작업이 중단되면, 가비지 컬렉션이 모든 변수와 객체를 탐색해서 각각 어떤 객체를 참고하고 있는지 확인한다.

이후, 사용되고 있는 메모리를 식별해서(Mark) 사용되지 않는 메모리는 제거(Sweep)하는 과정을 진행한다.

참고: codestates

0개의 댓글