[Java] 프로세스(Process)와 쓰레드(Thread) - 실전편

JD_S·2022년 11월 7일
0

Java

목록 보기
13/21

프로세스

실행 중인 프로그램, 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드(일꾼)로 구성

쓰레드

프로세스 내에서 실제 작업을 수행. 모든 프로세스(공장)는 최소한 하나의 쓰레드(일꾼)를 가지고 있다.

멀티프로세스 vs 멀티쓰레드

멀티 태스킹(멀티 프로세싱) : 동시에 여러 프로세스를 실행시키는 것
멀티 쓰레딩 : 하나의 프로세스 내에 동시에 여러 쓰레드를 실행시키는 것

  • 프로세스를 생성하는 것보다 쓰레드를 생성하는 비용이 적다.
  • 같은 프로세스 내의 쓰레드들은 서로 자원을 공유 한다.

멀티쓰레딩의 장점

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

멀티쓰레딩의 단점

  • 동기화(Synchronization)
  • 교착상태(Deadlock)
  • 고려해야 할 사항들이 많다. (각 쓰레드가 효율적으로 고르게 실행될 수 있게 해야한다.)
  • 기아현상 (프로그램을 잘못짜게 된다면 특정 쓰레드는 작업할 기회를 계속 갖지 못하고 작업 진행이 안됨)

싱글쓰레드와 멀티쓰레드

두 개의 작업을 하나의 쓰레드(싱글 쓰레드)로 처리하는 경우와 두 개의 쓰레드로 처리하는 경우
왠지 두 개의 쓰레드로 처리하면 시간이 짧게 소요될 것이라고 생각하겠지만 예상과 다르게 거의 같다. 아니 오히려 더 오래 걸리게 된다. 그 이유는 쓰레드 간의 작업 전환(Context Switching)에 시간이 걸리기 때문이다. 작업 전환시 현재 진행 중인 작업의 상태 등의 정보를 저장하고 읽어 오는 시간이 소요된다.

  • 그래서 싱글 코어에서 단순히 CPU만을 사용하는 계산작업이면 멀티쓰레드보다 오히려 싱글쓰레드로 프로그래밍 하는 것이 더 효율적이다.

쓰레드 구현 방법

  • Thread 클래스를 상속받는 방법
  • Runnable 인터페이스를 구현하는 방법

Thread 클래스를 상속받으면 다른 클래스를 상속받을 수 없다. (Java는 다중상속을 허용하지 않음) 그래서 Runnable 인터페이스를 구현하는 방법이 일반적이다.

쓰레드의 I/O블락킹(Blocking)

쓰레드가 입출력(I/O)처리를 위해 기다리는 것

만약에 입력받는 작업과 출력 받는 작업을 하나의 쓰레드로 처리한다면 사용자가 입력을 마칠 때까지 아무 일도 하지 못한다. 하지만 두 개의 쓰레드로 처리한다면 사용자의 입력을 기다리는 동안 다른 쓰레드가 작업을 처리할 수 있다.

쓰레드의 우선순위

쓰레드는 우선순위(Priority)라는 속성(멤버변수)을 가지고 있는데, 작업의 중요도에 따라 쓰레드의 우선순위를 서로 다르게 지정하여 특정 쓰레드가 더 많은 작업 시간을 갖도록 할 수 있다.

	void setPriority(int newPriority) 쓰레드의 우선순위를 지정한 값으로 변경한다.
    int  getPriority()				  쓰레드의 우선순위를 반환한다.
    
    public static final int MAX_PRIORITY = 10 //최대 우선 순위
    public static final int MIN_PRIORITY = 1 //최소 우선 순위
    public static final int NORM_PRIORITY = 5 //보통 우선 순위
    
    //JVM은 1 ~ 10단계 WinOS는 1 ~ 32단계

쓰레드 그룹

  • 쓰레드 그룹은 서로 관련된 쓰레드를 그룹으로 다루기 위한 것이다. (보안상의 이유)
  • 모든 쓰레드는 반드시 하나의 쓰레드 그룹에 포함되어 있어야 한다.
  • 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 main쓰레드 그룹에 속한다.
  • 자신을 생성한 쓰레드(부모 쓰레드)의 그룹과 우선순위를 상속받는다.
	ThreadGroup getThreadGroup()  //쓰레드 자신이 속한 쓰레드 그룹을 반환한다.
    void uncaughtException(Thread thread, Throwable e)  //처리되지 않은 예외에 의해 쓰레드 그룹의 쓰레드가 실행이 종료되었을 때, JVM에 의해 이 메서드가 자동적으로 호출된다.

데몬(Daemon) 쓰레드

  • 일반 쓰레드의 작업을 돕는 보조적인 역할을 수행.
  • 일반 쓰레드가 모두 종료되면 자동적으로 종료된다.
  • 가비지 컬렉터, 자동저장, 화면자동갱신 등에 사용된다.
  • 무한 루프와 조건문을 이용해서 실행 후 대기하다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.
	boolean isDaemon()  //쓰레드가 데몬 쓰레드인지 확인한다. 데몬 쓰레드면 true 반환
    void setDaemon(boolean on)  //쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경한다. 매개변수 on의 값을 true로 지정하면 데몬 쓰레드가 된다.
    
    //setDaemon(boolean on)은 반드시 start()를 호출하기 전에 실행되어야 한다.
    //그렇지 않으면 IllegalThreadStateException이 발생한다.

쓰레드의 상태

  • NEW : 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태
  • RUNNABLE : 실행 중 또는 실행 가능한 상태
  • BLOCKED : 동기화 블럭에 의해서 일시정지된 상태(LOCK이 풀릴 때까지 기다리는 상태)
  • WAITING, TIME_WAITING : 쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은(UNRUNNABLE) 일시저장 상태, TIME_WAITING은 일시정지 시간이 지정된 경우를 의미
  • TERMINATED : 쓰레드의 작업이 종료된 상태

쓰레드의 실행 제어

쓰레드의 실행을 제어(스케줄링)할 수 있는 메서드가 제공된다. 이것들을 이용하여 효율적인 프로그램을 작성할 수 있다.

sleep()

	static void sleep(long millis)				//천분의 일초 단위
    static void sleep(long millis, int nanos)	//천분의 일초 + 나노초 
  • 현재 쓰레드를 지정된 시간동안 멈추게 한다.
  • 예외 처리를 해야 한다. (InterruptedException이 발생하면 깨어남)
  • 특정 쓰레드를 지정해서 멈추게 하는 것은 불가능하다.

interrupt()

	void interrupt()				//쓰레드의 interrupted상태를 false => true 변경
    boolean isInterrupted()			//쓰레드의 interrupted상태를 반환
    static boolean interrupted()	//현재 쓰레드의 interrupted상태를 반환 후, false로 변경
  • 대기상태(WAITING)인 쓰레드를 실행대기 상태(RUNNABLE)로 만든다.

suspend(), resume(), stop()

	void suspend()	//쓰레드를 일시정지 시킨다.
    void resume()	//suspend()에 의해 일시정지된 쓰레드를 실행대기 상태로 만든다.
    void stop()		//쓰레드를 즉시 종료시킨다.
  • 쓰레드의 실행을 일시정지, 재개, 완전정지 시킨다. 교착상태에 빠지기 쉽다.
  • suspend(), resume(), stop()은 deprecated되었으므로, 직접 구현해야 한다.

yield()

  • 남은 시간을 다음 쓰레드에게 양보하고, 자신(현재 쓰레드)은 실행대기한다.
  • yield()와 interrupt()를 적절히 사용한다면, 응답성과 효율을 높일 수 있다.

join()

	void join()							//작업이 모두 끝날 때까지
    void join(long millis)				//천분의 일초 동안
    void join(long millis, int nanos)	//천분의 일초 + 나노초 동안
  • 지정된 시간동안 특정 쓰레드가 작업하는 것을 기다린다.
  • 예외처리를 해야 한다.(InterruptedException이 발생하면 작업 재개)

쓰레드의 동기화(Synchronization)

한 번에 하나의 쓰레드만 객체에 접근할 수 있도록 객체에 락(lock)을 걸어서 데이터의 일관성을 유지하는 것.

  • 멀티 쓰레드 프로세스에서는 다른 쓰레드의 작업에 영향을 미칠 수 있다.
  • 진행중인 작업이 다른 쓰레드에게 간섭받지 않게 하려면 '동기화'가 필요
  • 동기화 하려면 간섭받지 않아야 하는 문장들을 '임계 영역'으로 설정
  • 임계영역은 락(lock)을 얻은 단 하나의 쓰레드만 출입가능 (객체 1개에 락 1개)

	//1. 메서드 전체를 임계 영역으로 지정
	public synchronized void calcSum() {	//임계 영역
    	//... 								//임계 영역
    }										//임계 영역

	//2. 특정한 영역을 임계 영역으로 지정
	synchronized(객체의 참조변수) {		    //임계 영역
    	//...							    //임계 영역
    }									    //임계 영역

wait(), notify(), notifyAll()를 이용한 동기화

  • 동기화의 효율을 높이기 위해 wait(), notify()를 사용.
  • Object클래스에 정의 되어 있으며, 동기화 블록 내에세만 사용할 수 있다.
  • wait() : 객체의 lock을 풀고 쓰레드를 해당 객체의 waiting pool에 넣는다.
  • notify() : waiting pool에서 대기중인 쓰레드 중의 하나를 깨운다.
  • notifyAll() : waiting pool에서 대기중인 모든 쓰레드를 깨운다.

Reference

  • Java의 정석 (남궁성 저자)
profile
Whatever does not destroy me makes me stronger.

0개의 댓글