[Java] Thread

Juhye Pyoun·2023년 9월 8일

Java

목록 보기
5/9

프로세스(Process)란?
cpu에 의해 메모리에 올라가 실행중인 프로그램
즉, 사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당받아 실행 중인 것을 말한다
프로세스는 프로그램에 사용되는 데이터와 메모리 등의 자원 그리고 스레드로 구성된다

스레드(Thread)란?

프로세스 안에서 실질적으로 작업을 실행하는 단위

멀티 스레드(Multi Thread)

하나의 프로세스 내에서 둘 이상의 스레드가 동시에 작업을 수행하는 것

멀티 스레드는 각 스레드가 자신이 속한 프로세스의 메모리를 공유하므로, 시스템 자원의 낭비가 적다
또한, 하나의 스레드가 작업을 할 때 다른 스레드가 별도의 작업을 할 수 있어 사용자와의 응답성도 좋아진다

 

스레드 구현

자바에서 스레드 구현 방법은 2가지가 있다
모두 run() 메소드를 오버라이딩한다

1. Runnable 인터페이스 구현

public class MyThread implements Runnable {
    @Override
    public void run() {
        // 수행 코드
    }
}

2. Thread 클래스 상속

public class MyThread extends Thread {
    @Override
    public void run() {
        // 수행 코드
    }
}

 

스레드 생성

두가지 방법은 인스턴스 생성 방법에 차이가 있다

1. Runnable 인터페이스 구현

  • Runnable 인터페이스는 몸체가 없는 메소드인 run() 메소드 단 하나만을 가지는 간단한 인터페이스이다
  • Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없으므로, 일반적으로 Runnable 인터페이스를 구현하는 방법으로 스레드를 생성한다
  • Thread 객체의 생성자로 Runnable 인터페이스를 구현한 객체를 전달한다
  • run()을 호출하면 Runnable 인터페이스에서 구현한 run()이 호출되므로 따로 오버라이딩하지 않아도 된다

2. Thread 클래스 상속

  • 상속받은 클래스 자체를 스레드로 사용할 수 있다.
  • Thread 클래스를 상속받으면 스레드 클래스의 메소드(getName())를 바로 사용할 수 있지만, Runnable 구현의 경우 Thread 클래스의 static 메소드인 currentThread()를 호출하여 현재 스레드에 대한 참조를 얻어와야만 호출이 가능하다.
class ThreadWithClass extends Thread {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(getName()); // 현재 실행 중인 스레드의 이름을 반환함.
            try {
                Thread.sleep(10);          // 0.01초간 스레드를 멈춤.
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


class ThreadWithRunnable implements Runnable {
    public void run() {
        for (int i = 0; i < 5; i++) {
        	System.out.println(Thread.currentThread().getName()); // 현재 실행 중인 스레드의 이름을 반환함.
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


public class Thread01 {
    public static void main(String[] args){
        ThreadWithClass thread1 = new ThreadWithClass();       // Thread 클래스를 상속받는 방법
        Thread thread2 = new Thread(new ThreadWithRunnable()); // Runnable 인터페이스를 구현하는 방법

        thread1.start(); // 스레드의 실행
        thread2.start(); // 스레드의 실행
    }
}

 

스레드 실행

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

스레드를 이용한다는 건 JVM이 다수의 콜 스택(call stack)을 번갈아가며 일처리를 하고 사용자는 동시에 작업하는 것처럼 보여주는 것이다.
* 콜 스택(call stack, 호출 스택) : 메소드 수행에 필요한 메모리가 제공되는 공간. 실질적인 명령어들을 담고 있는 메모리로, 하나씩 꺼내서 실행시키는 역할을 한다

run() 메소드를 사용한다면 main()의 콜 스택 하나만 이용하는 것으로 스레드를 활용하는 것이 아닌, 그냥 스레드 객체의 run이라는 메소드를 호출하는 것 뿐이다

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

새로운 콜 스택을 만들어 작업을 해야 스레드 일처리가 되는 것이기 때문에 start() 메소드를 써야한다

 

스레드의 실행제어

스레드의 상태는 5가지가 있다

상태열거 상수설명
객체 생성NEW스레드가 생성되고 아직 start()가 호출되지 않은 상태
실행 대기RUNNABLE실행 중 또는 실행 가능 상태
일시 정지BLOCKED동기화 블럭에 의해 일시정지된 상태(lock이 풀릴 때까지 기다림)
일시 정지WAITING
TIME_WAITING
실행가능하지 않은 일시정지 상태
종료TERMINATED스레드 작업이 종료된 상태

스레드로 구현하는 것이 어려운 이유는 바로 동기화와 스케줄링 때문이다

스케줄링과 관련된 메소드
sleep(), join(), yield(), interrupt() ...

모든 스레드가 종료된 후에 main 메서드를 종료하고 싶은 경우(스레드가 모두 종료된 후 그 다음 작업을 진행해야 하는 경우) join 메소드를 사용하면 된다.

 

동기화

멀티스레드 환경에서 여러 스레드가 하나의 공유자원에 동시에 접근하지 못하도록 막는것

멀티 스레드 프로그래밍에서 동기화는 필수적이다
동기화가 필요한 이유는, 여러 스레드가 같은 프로세스 내의 자원을 공유하면서 작업할 때 서로의 작업이 다른 작업에 영향을 주기 때문이다.
스레드의 동기화를 하기 위해서 임계 영역(critical section)과 잠금(lock)을 활용한다.

임계영역(동시에 리소스를 사용할 수 없는 구역)을 지정하고,
임계영역을 가지고 있는 lock을 단 하나의 스레드에게만 빌려준다. 따라서 임계구역 안에서 수행할 코드가 완료되면 lock을 반납해야한다.

스레드 동기화 방법

  • 임계 영역(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()를 받아 수행한다

 

[참고자료]

링크1🔗
링크2🔗
링크3🔗
링크4🔗

0개의 댓글