[Java] Java에서의 Thread

giggle·2023년 8월 31일
0

📌 Process와 Thread

먼저 프로세스란 cpu에 의해 메모리에 올라가 실행중인 프로그램을 말합니다. 이러한 프로세스는 자신만의 메모리 공간을 포함한 독립적인 실행공간을 갖고있습니다.

자바 JVM은 주로 하나의 프로세스로 실행되며, 동시에 여러 작업을 수행하기 위해 멀티 스레드를 지원합니다.

스레드란 프로세스 안에서 실질적으로 작업을 실행하는 단위를 말합니다.
Java에서는 JVM에 의해 관리가 되며, 한 프로그램에 여러개의 스레드가 존재 가능하며, 스레드가 1개이면 단일 스레드, 2개 이상이면 멀티 스레드 환경이 됩니다.

📌 Thread State

  • Thread.State NEW : 스레드가 실행 준비가 완료된 상태, 스레드의 첫 시작.

  • Thread.State RUNNABLE : 스레드가 실행 가능한 상태, 스레드가 대기열에서 실행을 기다리고 있음을 의미.

  • Thread.State BLOCKED : 스레드가 차단되어 있는 상태, 스레드가 잠금(lock) 습득을 기다리는 상태.

  • Thread.State WAITING : 스레드가 대기중인 상태, 대기 상태의 스레드는 다른 스레드가 작업을 완료하기를 기다리고 있는 상태이다.

  • Thread.State TIMED_WAITING : 스레드가 정해진 시간동안 대기하는 상태, WAITING과의 차이는 정해진 시간동안 대기를 한다는 것이다.

  • Thread.State TERMINATED : 스레드가 종료되거나 죽은 상태, 종료된 스레드는 실행이 완료되었음을 의미한다.

Thread 우선순위

public class Thread implements Runnable {

    public static final int MIN_PRIORITY = 1;    // 가장 높은 우선 순위
 
    public static final int NORM_PRIORITY = 5;   // 일반 쓰레드의 우선 순위

    public static final int MAX_PRIORITY = 10;   // 가장 낮은 우선 순위
}

모든 쓰레드는 1 ~ 10 사이의 우선순위를 가집니다. 디폴트는 5입니다.

두개의 쓰레드가 동시성에 따라 2개의 작업을 번갈아가며 실행할 때 우선순위가 높은 쓰레드가 더 많은 시간을 할당받습니다.

하지만, 우선순위는 스레드 스케줄링의 정확한 동작을 보장하지 않기 때문에 되도록 사용하지 않는 것이 좋습니다.

데몬 스레드(Daemon Thread)

JVM은 모든 스레드가 끌날 때까지 기다립니다.

만약 스레드가 무한정 실행된다면, JVM 무한루프를 돌 것입니다. 이러한 문제를 해결하기 위한 것이 데몬 스레드입니다.

특정 스레드를 데몬 스레드로 지정한다면, 그 쓰레드가 수행하고 있던, 수행되지 않고 있든 상관 없이 JVM이 끝날 수 있습니다. (단, start()가 수행되기 전에 데몬 쓰레드로 지정되어야 합니다.)

즉, 데몬 스레드는 해당 스레드가 다 끝나지 않았음에도 프로그램이 바로 종료가 되는 것을 확인할 수 있습니다.

public class Test {
    public static void main(String[] args) {
        Test test = new Test();
        test.runCommonThread();
    }

    public void runCommonThread() {
        DaemonThread thread = new DaemonThread();
        thread.setDaemon(true);   // 데몬 쓰레드 만들기
        thread.start();
    }
}

📌 Thread 구현 방법

Thread vs Runnable

Java에서 Thread를 생성하는 방법은 크게 두가지입니다.
1. Thread 클래스를 상속 받아 생성하는 방법입니다. run() 메서드를 오버라이딩

public class MyThread extends Thread {
    @Override
    public void run() {
        // 수행 코드
    }
}
  1. Runnable 인터페이스를 구현하여 생성하는 방법이 있다. run() 메서드 구현
public class MyThread implements Runnable {
    @Override
    public void run() {
        // 수행 코드
    }
}

단, Runnable을 구현하여 스레드를 생성하는 경우, 객체 참조변수를 인자값으로 하는 Thread를 생성하여 사용해야 됩니다.

반면, java.lang.Thread 클래스를 상속받아 사용하는 경우 실행 스레드로 자신의 콜 스택을 가진 독립적인 프로세스가 됩니다.

Thread 실행

스레드의 실행은 run() 호출이 아닌 start() 호출로 해야합니다.

자바에는 콜 스택이 있습니다. 이 영역이 실질적인 명령어들을 담고 있는 메모리로, 하나씩 순서대로 꺼내 실행시키는 역할을 합니다.

만약 동시에 두 가지 작업을 한다면, 두 개 이상의 콜 스택이 필요합니다.

스레드를 이용한다는 건, JVM이 다수의 콜 스택을 번갈아가며 일처리를 하고 사용자는 동시에 작업하는 것처럼 보여줍니다.

즉, run() 메소드를 이용한다는 것은 main()의 콜 스택 하나만 이용하는 것으로 스레드 활용이 아닙니다.

그러므로 start() 메소드를 호출하면, JVM은 알아서 스레드를 위한 콜 스택을 새로 만들어주고 context switching을 통해 스레드답게 동작하도록 해줍니다.

즉, start()는 스레드가 작업을 실행하는데 필요한 콜 스택을 생성한 다음 run()을 호출해서 그 스택 안에 run()을 저장할 수 있도록 해줍니다.

📌 동기화

동기화가 필요한 이유는, 여러 스레드가 같은 프로세스 내의 자원을 공유하면서 작업할 때 서로의 작업이 다른 작업에 영향을 주기 때문입니다.

스레드의 동기화를 위해선, 임계 영역(critical section)과 잠금(lock)을 활용합니다.

임계영역을 지정하고, 임계영역을 가지고 있는 lock을 단 하나의 스레드에게만 빌려주는 개념으로 이루어져있습니다.

따라서 임계구역 내에서 수행할 코드가 완료되었다면, lock을 다시 반환해줘야 합니다.

Thread 동기화 방법

  • 임계 영역(critical section) : 공유 자원에 단 하나의 스레드만 접근하도록(하나의 프로세스에 속한 스레드만 가능)

  • 뮤텍스(mutex) : 공유 자원에 단 하나의 스레드만 접근하도록(서로 다른 프로세스에 속한 스레드도 가능)

  • 이벤트(event) : 특정한 사건 발생을 다른 스레드에게 알림

  • 세마포어(semaphore) : 한정된 개수의 자원을 여러 스레드가 사용하려고 할 때 접근 제한

  • 대기 가능 타이머(waitable timer) : 특정 시간이 되면 대기 중이던 스레드 깨움

synchronized 활용

synchronized를 활용해 임계영역을 설정할 수 있습니다.

서로 다른 두 객체가 동기화를 하지 않은 메소드를 같이 오버라이딩해서 이용한다면, 두 스레드가 동시에 진행되므로 원하는 출력 값을 얻지 못합니다.

이때 오버라이딩되는 부모 클래스의 메소드에 synchronized 키워드로 임계영역을 설정해주면 해결할 수 있습니다.

//synchronized : 스레드의 동기화. 공유 자원에 lock
public synchronized void saveMoney(int save){    // 입금
    int m = money;
    try{
        Thread.sleep(2000);    // 지연시간 2초
    } catch (Exception e){

    }
    money = m + save;
    System.out.println("입금 처리");

}

public synchronized void minusMoney(int minus){    // 출금
    int m = money;
    try{
        Thread.sleep(3000);    // 지연시간 3초
    } catch (Exception e){

    }
    money = m - minus;
    System.out.println("출금 완료");
}

wait()과 notify() 활용

쓰레드가 서로 협력관계일 경우에는 무작정 대기시키는 것으로 올바르게 실행되지 않기 때문에 사용합니다.

  • wait() : 쓰레드가 lock을 가지고 있으면, lock 권한을 반납하고 대기하게 만듬
  • notify() : 대기 상태인 쓰레드에게 다신 lock 권한을 부여하고 수행하게 만듬

해당 메소들들은 동기화 된 영역(임계 영역)내에서 사용되어야 합니다.

동기화 처리한 메소드들이 반복문에서 활용된다면, 의도한대로 결과가 나오지 않습니다. 이때 wait()과 notify()를 try-catch 문에서 적절히 활용해 해결할 수 있습니다.

/**
* 스레드 동기화 중 협력관계 처리작업 : wait() notify()
* 스레드 간 협력 작업 강화
*/

public synchronized void makeBread(){
    if (breadCount >= 10){
        try {
            System.out.println("빵 생산 초과");
            wait();    // Thread를 Not Runnable 상태로 전환
        } catch (Exception e) {

        }
    }
    breadCount++;    // 빵 생산
    System.out.println("빵을 만듦. 총 " + breadCount + "개");
    notify();    // Thread를 Runnable 상태로 전환
}

public synchronized void eatBread(){
    if (breadCount < 1){
        try {
            System.out.println("빵이 없어 기다림");
            wait();
        } catch (Exception e) {

        }
    }
    breadCount--;
    System.out.println("빵을 먹음. 총 " + breadCount + "개");
    notify();
}

조건 만족 안할 시 wait(), 만족 시 notify()를 받아 수행합니다.


참고


피드백 및 개선점은 댓글을 통해 알려주세요😊

profile
배움을 글로 기록하는 개발자가 되겠습니다.

0개의 댓글