5/17 쓰레드

박세현·2024년 5월 17일

JAVA

목록 보기
22/22
post-thumbnail

쓰레드

1. 프로세스와 쓰레드

  • 프로세스 : 실행 중인 프로그램(program)
  • 쓰레드 : 작업 메서드 + 호출 스택 : 작업대
    • 작업메서드 : main(), run() // main()외에는 다 run() 작업메서드
    • main() -> main 쓰레드
    • run() -> 사용자 정의 쓰레드 : 실행시에 호출스택이 필요하므로 별도 메서드 start() 실행
      -> 호출스택 + run() 메서드 실행

1) 프로세스

  • 실행 중인 프로그램(program)
  • 프로그램을 수행하는 데 필요한 데이터와 메모리등의 자원 그리고 쓰레드로 구성
  • 프로세스의 메모리 한계에 따라 생성할 수 있는 쓰레드의 수가 결정

예시)



2) 쓰레드

  • 작업 메서드 + 호출 스택 : 작업대
    • 호출스택 : 스택공간
    • 작업메서드 : main(), run() // main()외에는 다 run() 작업메서드
      • main() -> main 쓰레드
      • run() -> 사용자 정의 쓰레드 : 실행시에 호출스택이 필요하므로 별도 메서드 start() 실행
        -> 호출스택 + run() 메서드 실행
  • 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 쓰레드이다
  • 모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재하며, 둘 이상의 쓰레드를 가진 프로세스를 멀티쓰레드 프로세스(multi-threaded process)라고 한다.



2. 멀티쓰레딩

  • 하나의 프로세스내에서 여러 쓰레드가 동시에 작업을 수행하는 것



3. 멀티쓰레딩의 장점

  • CPU의 사용률을 향상시킨다.
  • 자원을 보다 효율적으로 사용할 수 있다.
  • 사용자에 대한 응답성이 향상된다.
  • 작업이 분리되어 코드가 간결해진다.



4. 멀티쓰레딩의 단점

  • 여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업을 하기 때문에 발생할 수 있는 동기화(synchronization), 교착상태(deadlock)와 같은 문제를 고려해서 신중하게 프로그래밍해야 한다.
    • 동기화 : 1번 쓰레드가 작업 상태이면 2번쓰레드는 데이터 접근 못하게 락 검
    • 교착상태 : 쓰레드끼리 서로 작업중일까봐 눈치만 보고 데이터 접근 안하는 상태




쓰레드의 구현과 실행

  • 쓰레드의 구현에는 2가지 방법이 존재

    • Thread클래스를 상속받는 방법
    • Runnable인터페이스를 구현 -> Thread 생성자 매개변수로 투입
  • Thread클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에 Runnable 인터페이스를 구현하는 방법이 일반적

  • 메인 작업대 외의 작업대는 모두 run()



1.쓰레드의 구현

1) Thread클래스를 상속

  • run()을 재정의 하는 것
  • 재정의를 통해 run()이 실행됨
  • 상속 : 단일상속만 가능
    -> 유연성에서 불리, 상속은 1개만 받기 때문
  • Thread 클래스에 정의된 인스턴스 자원을 하위클래스에서 바로 접근 가능

예시)

ㄴ ② Thread클래스를 상속
ㄴ 쓰레드 총 3개 = 작업대 총 3개 : ① ② ③

ㄴ 출력결과 (병렬적 수행 = 동시에 수행)
ㄴ 호출스택이 각각 있다ㅏㅏ



예시2)

ㄴ 쓰레드 분리가 안됨
ㄴ 왜? start()메서드가 쓰레드 분리(호출스택)해줘야 함
ㄴ run()은 코드만 가져올 뿐 호출스택을 새로 만들지 않음

ㄴ 이제 쓰레드 분리 됨
-> 동시작업



2) Runnable인터페이스를 구현 -> Thread 생성자 매개변수

  • Thread 클래스의 run()이 Runnable 인터페이스 객체의 run()을 대신 수행
    • Runnable 인터페이스 자체로는 Thread의 기능이 없음
    • Thread클래스의 start()가 쓰레드 생성(호출스택 분리)해줘야 함

예시)

ㄴ ③ Runnable인터페이스를 구현 -> Thread 생성자 매개변수
ㄴ 쓰레드 총 3개 = 작업대 총 3개 : ① ② ③

참고) 람다식 풀어서보기

ㄴ 출력결과 (병렬적 수행 = 동시에 수행)
ㄴ 호출스택이 각각 있다ㅏㅏ



예시2)

ㄴ 러너블 인터페이스는 run()메서드를 구현만 했을 뿐 쓰레드의 기능은 없음



2-1) 로직해설

  • Thread 클래스의 run()이 Runnable 인터페이스 객체의 run()을 대신 수행하는 과정 해설

참고) Interface Runnable
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Runnable.html

ㄴ Runnable 인터페이스의 추상메서드 : run()
ㄴ 함수형 인터페이스 -> 람다식 굳굳

ㄴ Thread클래스 : Runnable 인터페이스의 구현체

ㄴ 2) 방식인 Runnable 인터페이스를 구현하고 Thread 생성자 매개변수로 투입하는 방식



예시)

ㄴ 쓰레드와 러너블 인터페이스 구조도 : implements

ㄴ 현재 인스턴스 변수(멤버변수)로 Runnable target이 있음
-> 생성자 매개변수에 러너블 인터페이스 객체가 들어오면 여기로 할당되게 되있음

ㄴ 생성자 매개변수 : Runnable target
ㄴ 객체가 된 러너블 인터페이스가 매개변수로 들어오게 되면 멤버변수에 있던 Runnable target쪽에 대입

ㄴ Thread클래스의 run()에서 대신 수행



예시) 위 예시 좀 더 간결하게

ㄴ Thread 생성자 매개변수로 러너블 인터페이스 객체가 들어오면 private Runnable target 멤버변수에 대입된다

ㄴ if문 : target = Runnable target
-> 러너블 인터페이스 객체가 들어 왔다 = 값이 있다 = null 이 아님을 의미하여 if문 실행 됨
-> if문 실행코드 : 러너블 인터페이스에 정의된 run()실행 되게끔 하는 코드

ㄴ 근데 이 if문(러너블 인터페이스에 정의된 run()실행)을 Thread클래스의 run()이 실행 시킴
= Thread 클래스의 run()이 Runnable 인터페이스 객체의 run()을 대신 수행한다




2. 쓰레드의 실행 : start()

1) start()와 run()

  • start() : 독립적인 호출 스택 + run() 실행 -> 병령적인 작업이 가능
  • run()만 호출하면 -> main 쓰레드에서 순차적으로 실행(병렬x)
  • 정리
    • run()만 호출하면 작업대(작업메서드)만 있지 스택(호출스택 = 공간)이 분리 되지 않음
      = 동시작업❌
    • start() : 호출 스택(공간 생성) 및 run() 실행(작업대, 작업메서드, 코드 실행)
      = 동시작업⭕

예시) 새로운 쓰레드를 생성하고 start()를 호출한 후 호출스택의 변화

  1. main() 메서드에서 쓰레드의 start()를 호출한다.
  2. start()는 새로운 쓰레드를 생성하고 쓰레드가 작업하는데 사용될 호출 스택을 생성한다.
  3. 새로 생성된 호출스택에 run()이 호출되어, 쓰레드가 독립된 공간에서 작업을 수행한다.
  4. 이제는 호출 스택이 2개이므로 스케줄러가 정한 순서에 의해서 번갈아가며 실행된다.


예시)

ㄴ 쓰레드 총 2개




3. 쓰레드 종료

  • 실행중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.



4. 쓰레드클래스에 정의된 메서드

  • getName() : 쓰레드의 이름
  • getPriority() : 실행 우선순위
  • setName(Stirng name) : 이름 직접 설정
  • static Thread currentThread() : 현재 동작하고 있는 쓰레드를 가져오는 정적 메서드 (반환값이 Thread)
    -> 러너블 인터페이스 : 추상메서드 run() 밖에 없음
    -> 1) 쓰레드클래스를 상속받아서 쓰레드를 구현한 경우 자식이 부모의 메서드를 쓸 수 있는 원리를 이용해 getName()을 통해 쓰레드 이름 조회 가능
    -> 2) 벗 러너블 인터페이스를 구현한 객체를 쓰레드 생성자 함수에 매개변수로 대입하여 쓰레드를 만든 클래스는 상속이 아니니까 getName()을 쓸 수 x -> 예시3 참조

참고) 쓰레드
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Thread.html


예시) getName()

ㄴ 출력결과



예시2) 생성자 매개변수 통해 이름 변경, getName()

package exam01;

public class Ex01 {
    public static void main(String[] args) { // + 호출 스택 -> 메인 쓰레드
        Runnable r = () -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("쓰레드2-" + i);

                for(long j = 0; j < 10000000000L; j++);
            }
        };

        Ex01_1 th1 = new Ex01_1();
        Thread th2 = new Thread(r);

        //th1.run();
        //th2.run();
        th1.start(); // 호출스택 생성 + run() 메서드 실행
        th2.start(); // 호출스택 생성 + run() 메서드 실행

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

class Ex01_1 extends Thread {
    public Ex01_1() {
        super("변경된이름1");
    }

    public void run() {
        for (int i = 0; i < 5; i++) {
            //System.out.println("쓰레드1-" + i);
            System.out.println(getName() + "-" + i);
            for(long j = 0; j < 10000000000L; j++);
        }
    }
}

ㄴ 이름이 바껴서 출력



예시3) static Thread currentThread()

ㄴ 러너블 인터페이스를 구현한 객체를 쓰레드 생성자 함수에 매개변수로 대입하여 쓰레드를 만든 클래스가 Thread클래스에 정의 된 메서드 쓰는 법





싱글쓰레드와 멀티쓰레드

1. 싱글쓰레드

  • 하나의 쓰레드로 두 작업을 처리하는 경우 한 작업을 마친 후에 다른 작업을 시작한다.



2. 멀티쓰레드

  • 시분할 방식

    • 시간을 분할하는 방식
    • 빠르게 번갈아가면서 작업을 하니까 동시에 하는 것 처럼 보인다
    • 두 개의 쓰레드로 작업하는 경우에는 짧은 시간동안 2개의 쓰레드가 번갈아 가면서 작업을 수행해서 동시에 두 작업이 처리되는 것과 같이 느끼게 한다.
  • 과거엔 하나의 쓰레드로 두개의 작업을 수행한 시간과 두개의 쓰레드로 두 개의 작업을 수행한 시간은 거의 같았다. 오히려 두 개의 쓰레드로 작업한 시안이 싱글쓰레드로 작업한 시간보다 더 걸리게 되는데, 쓰레드 간의 작업 전환(context switching)에 시간이 걸리기 때문이다.

  • 작업전환 (context switching)

    • 과거 :
      번갈아가면서 작업을 하기 위해선 1번쓰레드 하다가 2번 쓰레드 잡업하고 다시 1번쓰레드 작업하러 갈 때 이전 1번작업을 언제가지 작업했는지 시간을 찾고 작업을 이어가야 하니까 시간이 더 걸릴 수 있음
    • 현대 :
      근데 요즘은 코어가 여러개인 pc가 일반적이라 멀티쓰레드가 더 빠르다




쓰레드의 우선순위

1. 쓰레드 우선순위 지정하기

  • 우선순위가 높은 경우
    -> 시간분할을 더 많이 해서 실행을 더 많이 확보

  • 1~10 : 10에 가까울 수록 우선순위가 높다, 1에 가까울수록 우선순위가 낮다
    setPriority(1~10)
    -> 요즘은 피씨에 코어가 많아서(멀티코어) 큰 차이가 없다...

참고) 시분할 방식

  • 빠르게 번갈아가면서 작업을 하니까 동시에 하는 것 처럼 보인다

예시) getPriority()



1) 쓰레드 그룹(thread group)

  • 쓰레드 그룹을 설정하지 않으면 모두 main그룹
  • 우선순위 등 그룹별로



2) 데몬 쓰레드(daemon thread)

boolean isDaemon()  // 쓰레드가 데몬 쓰레드인지 확인한다. 데몬 쓰레드이면 true를 반환한다.
void setDaemon(boolean on) // 쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경한다. 매개변수 on의 값을 true로 지정하면 데몬 쓰레드가 된다.
  • daemon : 유령
  • 현재 작업중인 쓰레드의 작업이 종료가 되면 함께 종료되는 쓰레드
  • 백그라운드에서 동작하는 방식
  • 백그라운드에서 계속 대기중임

예시)





쓰레드의 실행제어

1. 쓰레드와 스케줄링과 관련된 메서드




2. 쓰레드의 상태

  • 시분할 방식이다 보니 내 순번 아니면 실행대기 상태



3. sleep(long millis)




4. interrupt()와 interrupted()

  • interrupt() : 실행 정지 상태인 sleep(), join()를 다시 실행 대기 상태로 변경

  • interrupt()를 호출

    • interruptedException 라는 예외 발생됨
      -> interruptedException이 발생됨으로서 WAITING상태(일지정지)인 sleep(), join()을 RUNNABLE상태(실행 대기)로 바꿈

    • interrupted() 는 true로 바뀜
      -> interrupt() 실행⭕ : Interrupted() = true
      -> interrupt() 실행❌ : Interrupted() = false

    • isInterrupted() : interrupt() 가 발생했는지 안했는지 상태를 알려줌
      -> interrupt() 실행⭕ : isInterrupted() = true
      -> interrupt() 실행❌ : isInterrupted() = false

      				
  • interrupt() 호출
    -> 1) isInterrupted()가 true 변경
    -> 2) interrupted() 호출, InterruptedException도 발생, isInterrupted로 false 변경




5. suspend(), resume(), stop()

  • suspend() : 일시정지
  • resume() : 재시작
  • stop() : 정지
    -> 교착상태를 유발할 가능성이 크므로 사용 지양

예시)




6. yield()

  • 다른 쓰레드에게 작업 양보
  • 정지 상태 일 때 바로 다른 쓰레드에게 작업 양보



7. join()

  • join한 쓰레드가 완료되면 현재 쓰레드가 종료

예시)

ㄴ th1, th2 쓰레드가 완료 된 후 main쓰레드 종료




쓰레드의 동기화

1. synchronized를 이용한 동기화

1) 메서드 전체를 임계영역으로 지정

synchronized



2) 특정한 영역을 임계 영역으로 지정

synchronized(this) {
///
}




2. volatile

profile
귤귤

0개의 댓글