자바의 정석 ch13. 쓰레드(thread) 上

yuju9·2022년 4월 9일
0

자바의 정석 스터디

목록 보기
16/18

1. 프로세스와 쓰레드

  • 프로레스: 실행중인 프로그램 (프로그램을 실행하면 OS로부터 실행에 필요한 자원을 할당받아 프로세스가 됨)
  • 쓰레드: 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것
  • 멀티쓰레드 프로세스: 둘 이상의 쓰레드를 가진 프로세스

멀티태스킹과 멀티쓰레딩

현재 우리가 사용하고 있는 윈도우나 유닉스를 포함한 대부분의 OS는 멀티태스킹(다중작업)을 지원하기 때문에 여러 개의 프로세스가 동시에 실행될 수 있음. 이와 마찬가지로 멀티쓰레딩은 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것임. 프로세스의 성능이 단순히 쓰레드 개수에 비례하는 것은 아니며, 하나의 쓰레드를 가진 프로세스보다 두 개의 쓰레드를 가진 프로세스가 오히려 더 낮은 성능을 보일 수 있음

멀티쓰레딩의 장단점

  • 멀티쓰레딩의 장점
    1. CPU의 사용률을 향상시킴
    2. 자원을 보다 효율적으로 사용 가능
    3. 사용자에 대한 응답성 향상
    4. 작업이 분리되어 코드가 간결해짐

메신저로 채팅하면서 파일을 다운로드 받거나 음성대화를 나눌 수 있는 것이 가능한 이유가 멀티쓰레드로 작성되어 있기 때문임.

  • 멀티쓰레딩의 단점
    여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업을 하기 때문에 발생할 수 있는 동기화, 교착상태와 같은 문제들을 고려해서 신중히 프로그래밍해야함.

2. 쓰레드의 구현과 실행

쓰레드 구현 방법

  1. Thread클래스 상속
class MyThread extends Thread {
	public void run() {} //Thread클래스의 run()을 오버라이딩
}
  1. Runnable인터페이스 구현
class MyThread implements Runnable {
	public void run() {} //Runnable인터페이스의 추상메서드 run()을 오버라이딩
}

//Runnable인터페이스를 구현하기 위해서 해야 할 일은 추상메서드인 run()의 몸통{}을 만들어주는 것 뿐

public interface Runnable {
	public abstract void run();
}

별 차이는 없지만 Thread클래스를 상속받으면 다른 클래스를 상속받을 수 없기 때문에, Runnable인터페이스를 구현하는 방법이 일반적임.

쓰레드의 실행 - start()

쓰레드를 생성했다고해서 자동적으로 실행X. start()를 호출해야만 쓰레드가 실행됨

t1.start(); //쓰레드 t1을 실행
t2.start(); //쓰레드 t2을 실행

start()가 호출되었다고 해서 바로 실행되는 것이 아니라, 일단 실행대기 상태에 있다가 자신의 차례가 되어야 실행됨. 물론 실행대기중인 쓰레드가 하나도 없으면 곧바로 실행상태가 됨. 그리고 한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다. 즉, 하나의 쓰레드에 대해 start()가 한번만 호출될 수 있다는 뜻이다. 따라서 쓰레드 작업을 한번 더 수행해야 한다면 새로운 쓰레드를 생성한 다음에 start()를 호출해야함.

ThreadEx1_1 t1 = new ThreadEx1_1();
t1.start();
t1 = new ThreadEx1_1(); //다시 생성
t1.start(); //ok!

3. start()와 run()


start()와 run()의 차이와 쓰레드가 실행되는 과정.

  • main메서드에서 run()을 호출하는 것은 생성된 쓰레드를 실행시키는 것이 아니라 단순히 클래스에 선언된 메서드를 호출하는 것일 뿐임.
  • 반면에 start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택을 생성한 다음에 run()을 호출해서, 생성된 호출스택에 run()이 첫번째로 올라가게 함.
  • 새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출스택이 생성되고 쓰레드가 종료되면 작업에 사용된 호출스택은 소멸됨

main쓰레드

  • main메서드의 작업을 수행하는 것도 쓰레드(main쓰레드라고 함)
  • main메서드가 수행을 마쳤더라도 다른 쓰레드가 아직 작업을 마치지 않은 상태라면 종료되지 않음. 실행중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료됨.

4. 싱글쓰레드와 멀티쓰레드

하나의 쓰레드로 두 작업을 처리하는 경우는 한 작업을 마친 후에 다른 작업을 시작하지만, 두 개의 쓰레드로 작업하는 경우에는 짧은 시간동안 2개의 쓰레드가 번갈아 작업을 수행해서 동시에 두 작업이 처리되는 것과 같이 느껴짐.

하나의 쓰레드로 두 개의 작업을 수행한 시간과 두 개의 쓰레드로 두 개의 작업을 수행한 시간은 거의 같다. 오히려 두 개의 쓰레드로 작업한 시간이 싱글쓰레드로 작업한 시간보다 더 걸리게 되는데 그 이유는 쓰레드간의 작업전환에 시간이 걸리기 때문이다.
※ 작업전환을 할 때는 현재 진행 중인 작업의 상태, 예를 들면 다음에 실행해야할 위치 등의 정보를 저장하고 읽어오는 시간이 소요됨.

따라서 싱글 코어에서 단순히 CPU만을 사용하는 계산작업이라면 오히려 멀티쓰레드보다 싱글쓰레드로 프로그래밍하는 것이 효율적임.
두 스레드가 서로 다른 자원을 작업하는 작업의 경우에는 싱글쓰레드보다 멀티쓰레드 프로레스가 더 효율적임.(ex. 사용자로부터 데이터를 입력받는 작업, 네트워크로 파일을 주고받는 작업 등)

5. 쓰레드의 우선순위

우선순위의 값에 따라 쓰레드가 얻는 실행시간이 달라진다. 쓰레드가 수행하는 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 지정하여 특정 쓰레드가 더 많은 작업시간을 갖도록 할 수 있다.

쓰레드 우선순위 지정하기

void setPriority(int new Priority) //우선순위를 지정한 값으로 변경
int getPriority() //쓰레드의 우선순위를 반환

public static final int MAX_PRIORITY = 10 //최대우선순위
public static final int MIN_PRIORITY = 1 //최소우선순위
public static final int NORM_PRIORITY = 5 //보통우선순위

쓰레드가 가질 수 있는 우선순위의 범위는 1~10이며 숫자가 높을수록 우선순위가 높음. 쓰레드의 우선순위는 쓰레드를 생성한 쓰레드로부터 상속받음. 쓰레드를 실행하기 전에만 우선순위를 변경할 수 있음.

6. 쓰레드 그룹

서로 관련된 쓰레드를 그룹으로 다루기 위한 것으로, 폴더를 생성해서 관련된 파일들을 함께 넣어서 관리하는 것처럼 쓰레드 그룹을 생성해서 쓰레드 그룹으로 묶어서 관리할 수 있다.

쓰레드를 쓰레드 그룹에 포함시키려면 Thread 생성자를 이용해야함.

Thread (ThreadGroup group, String name)
Thread (ThreadGroup group, Runnable target)
Thread (ThreadGroup group, Runnable target, String name)
Thread (ThreadGroup group, Runnable target, String name, long stackSize)

모든 쓰레드는 반드시 쓰레드 그룹에 포함되어 있어야 하기 때문에 위와 같이 쓰레드 그룹을 지정하는 생성자를 사용하지 않은 쓰레드는 기본적으로 자신을 생성한 쓰레드와 같은 쓰레드 그룹에 속하게 됨.

ThreadGroup getThreadGroup() //쓰레드 자신이 속한 쓰레드 그룹을 반환
void uncaughtException(Thread t, Throwable e) //쓰레드 그룹의 쓰레드가, 처리되지 않은 예외에 의해 실행이 종료되었을 때, JVM에 의해 이 메서드가 자동적으로 호출

7. 데몬 쓰레드

  • 다른 일반 쓰레드의 작업을 돕는 보조적인 역할을 수행하는 쓰레드
  • 일반 쓰레드가 모두 종료되면 데몬 쓰레드는 강제적으로 자동 종료됨
    → 데몬 쓰레드는 일반 쓰레드의 보조역할을 수행하므로 일반 쓰레드가 모두 종료되고 나면 데몬 쓰레드의 존재의 의미가 없기 때문임.
  • 데몬 쓰레드는 일반 쓰레드의 작성방법과 실행방법이 같으며 다만 쓰레드를 생성한 다음 실행하기 전에 setDaemon(true)를 호출하기만 하면 된다. 그리고 데몬 쓰레드가 생성한 쓰레드는 자동적으로 데몬 쓰레드가 된다.
boolean isDaemon() //쓰레드가 데몬 쓰레드인지 확인한다. 데몬 쓰레드면 true반환
void setDaemon(boolean on) //쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경함. 매개변수 on의 값을 true로 지정하면 데몬 쓰레드가 됨.
Thread t = new Thread(new ThreadEx8());

t.setDaemon(true); //이 부분이 없으면 종료되지 않는다
t.start();

setDaemon메서드는 반드시 start()를 호출하기 전에 실행되어야함. 그렇지 않으면 IllegalThreadStateException발생

8. 쓰레드의 실행제어

쓰레드 프로그래밍이 어려운 이유는 동기화와 스케줄링 때문임. 앞서 우선순위를 통해 쓰레드간의 스케줄링을 하는 방법을 배우기는 했지만 이것만으로는 부족. 쓰레드의 스케줄링을 잘하기 위해서는 쓰레드의 상태와 관련 메서드를 잘 알아야 함.
<쓰레드의 스케줄링과 관련된 메서드>

<쓰레드의 상태>

1. 쓰레드를 생성하고 start()를 호출하면 바로 실행되는 것이 아니라 실행 대기열에 저장되어 자신의 차례가 될 때 까지 기다려야한다. 실행대기열은 큐와 같은 구조로 먼저 실행대기 열에 들어온 쓰레드가 먼저 실행된다.
2. 실행대기상태에 있다가 자신의 차례가 되면 실행 상태가 된다.
3. 주어진 실행시간이 다되거나 yield()를 만나면 다시 실행대기상태가 되고 다음 차례의 쓰레드가 실행상태가 된다.
4. 실행중에 suspend(), sleep(), wait(), join(), I/O block에 의해 일시정지상태가 될 수 있다. I/O block은 입출력작업에서 발생하는 지연상태를 말한다. 사용자의 입력을 기다리는 경우를 예로 들 수 있는데, 이런 경우 일시정지 상태에 있다가 사용자가 입력을 마치면 다시 실행대기 상태가 된다.
5. 지정된 일시정지시간이 다 되거나 notify(), resume(), interrupt()가 호출되면 일시 정지상태를 벗어나 다시 실행대기열에 저장되어 자신의 차례를 기다리게 된다.
6. 실행을 모두 마치거나 stop()이 호출되면 쓰레드는 소멸된다.

sleep(long millis) - 일정시간동안 쓰레드를 멈추게 한다.

sleep(long millis)
sleep(long millis, int nanos)

//sleep()을 호출할 때는 try-catch문으로 예외를 처리해주어야 함
try {
	Thread.sleep(1, 500000); //쓰레드를 0.0015초동안 머무게 함
} catch (InterruptedException e) {}

interrupt()와 interrupted() - 쓰레드 작업 취소

진행 중인 쓰레드의 작업이 끝나기 전에 취소시켜야할 때

void interrupt() //쓰레드의 interrupted상태를 false에서 true로 변경
boolean isInterrupted() //쓰레드의 interrupted상태를 반환
static boolean interrupted() //현재 쓰레드의 interrupted상태를 알려주고, false로 초기화

suspend(), resume(), stop()

  • suspend(): sleep()처럼 스레드를 멈춤. resume()을 호출해야 다시 실행대기 상태가 됨.
  • stop(): 호출되는 즉시 쓰레드 종료

yield() - 다른 쓰레드에게 양보

쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보하도록 함

join() - 다른 쓰레드의 작업을 기다림

쓰레드 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 할 때 사용. 시간을 지정하지 않으면, 해당 쓰레드가 작업을 모두 마칠 때까지 기다리게 됨. 작업 중에 다른 쓰레드의 작업이 먼저 수행되어야할 필요가 있을 때 join() 사용.

0개의 댓글

관련 채용 정보