13장 쓰레드

slee2·2021년 9월 28일
0

Java의 정석

목록 보기
25/28
post-thumbnail
post-custom-banner

프로세스와 쓰레드(process & thread)

프로세스 : 실행 중인 프로그램, 자원(resuources)과 쓰레드로 구성
쓰레드 : 프로세스 내에서 실제 작업을 수행. 모든 프로세스는 하나의 쓰레드를 가지고 있다.

하나의 새로운 프로세스를 생성하는 것보다 하나의 새로운 쓰레드를 생성하는 것이 더 적은 비용이 든다.

멀티쓰레드의 장단점

장점단점
시스템 자원을 보다 효율적으로 사용할 수 있다.
사용자에 대한 응답성이 향상된다.
작업이 분리되어 코드가 간결해 진다.
동기화에 주의해야 한다.
교착상태가 발생하지 않도록 주의해야 한다.
각 쓰레드가 효율적으로 고르게 실행될 수 있게 해야 한다.

쓰레드의 구현과 실행

Tread클래스를 상속

class MyThread extends Thread {
	public void run() {	// Thread클래스의 run()을 오버라이딩
   		/* 작업내용 
   		System.out.println(getName());
   		*/
  	}
}

Runnable인터페이스를 구현

class MyThread2 implements Runnable {
	public void run() {	// Runnable 인터페이스의 추상메서드 run()을 구현
   		/* 작업내용 
   		System.out.println(Thread.currentThreadgetName());
  		*/
    }
}
--- Thread 클래스를 상속 ---
MyThread t1 = new MyThread();	// 쓰레드의 생성
t1.start();	// 쓰레드의 실행

--- Runnable 인터페이스 ---
Runnable r = new MyThread2();
Thread t2 = new Thread(r);	// Thread(Runnable r)
// Thread t2 = new Thread(new MyThread2());
t2.start();

별도의 쓰레드로 나눠서 돌리면 자동으로 비동기화 처리가 되어 따로 돌아간다.

쓰레드의 실행 - start()

  • 쓰레드를 생성한 후에 start()를 호출해야 쓰레드가 작업을 시작한다.
    쓰레드의 실행 순서는 OS스케줄러가 결정한다.
ThreadEx1_1 t1 = new ThreadEx1_1();
ThreadEx1_1 t2 = new ThreadEx1_1();

t1.start();
t2.start();

main 쓰레드

  • main 메서드의 코드를 수행하는 쓰레드
  • 쓰레드는 '사용자 쓰레드'와 '데몬 쓰레드' 두 종류가 있다.

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

join - 동기화

쓰레드가 종료될때까지 기다린다.

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

입출력시 작업이 중단되는 것. 막힘.

사용자가 입력을 안해서 막혀있는 현상을 말한다. 이는 멀티 쓰레드의 비동기화를 이용하면 입력이 안해서 막혀있는 동안에 다른 작업을 시켜 전체 시간을 줄일 수 있는 효과를 가져올 수 있다.

쓰레드의 우선순위

우선순위를 따로 지정하지 않으면 자동으로 5로 설정됨
우선순위를 변경하고 싶으면

void setPriority(int newPriority)	// 쓰레드의 우선순위를 지정한 값으로 변경한다.

쓰레드의 우선순위를 알고싶으면

int getPriority()	// 쓰레드의 우선순위를 반환한다.

클수록 우선되는거임.

쓰레드 그룹

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

쓰레드 그룹의 메서드

메서드설명
ThreadGroup(String name)지정된 이름의 새로운 쓰레드 그룹을 생성
ThreadGroup(ThreadGroup parent, String name)지정된 쓰레드 그룹에 포함되는 새로운 쓰레드 그룹을 생성
int activeCount()쓰레드 그룹에 포함된 활성상태에 있는 쓰레드의 수를 반환
int activeGroupCount()쓰레드 그룹에 포함된 활성상태에 있는 쓰레드 그룹의 수를 반환
void checkAccess()현재 실행중인 쓰레드가 쓰레드 그룹을 변경할 권한이 있는지 체크.
void destroy()쓰레드 그룹과 하위 쓰레드 그룹까지 모두 삭제한다. 단, 비어있어야 삭제 가능
int enumerate(Thread[] list)
int enumerate(Thread[] list, boolean recurse)
int enumerate(ThreadGroup[] list)
int enumerate(ThreadGroup[] list, boolean recurse)
쓰레드 그룹에 속한 쓰레드 또는 하위 쓰레드 그룹의 목록을 지정된 배열에 담고 그 개수를 반환.
두 번째 매개변수인 recurse의 값을 true로 하면 쓰레드 그룹에 속한 하위 쓰레드 그룹에 쓰레드 또는 쓰레드 그룹까지 배열에 담는다.
int getMaxPriority()쓰레드 그룹의 최대 우선순위를 반환
String getName()쓰레드 그룹의 이름을 반환
ThreadGroup getParent()쓰레드 그룹으 ㅣ상위 쓰레드 그룹을 반환
void interrupt()쓰레드 그룹에 속한 모든 쓰레드를 interrupt
boolean isDaemon()쓰레드 그룹이 데몬 쓰레드 그룹인지 확인
boolean isDestroyed()쓰레드 그룹이 삭제되었는지 확인
void list()쓰레드 그룹에 속한 쓰레드와 하위 쓰레드 그룹에 대한 정보를 출력
boolean parentOf(ThreadGroup g)지정된 쓰레드 그룹의 상위 쓰레드 그룹인지 확인
void setDaemon(boolean daemon)쓰레드 그룹을 데몬 쓰레드 그룹으로 설정/해제
void setMaxPriority(int pri)쓰레드 그룹의 최대 우선순위를 설정

데몬 쓰레드(daemon thread)

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

setDaemon은 start()호출전에 설정되어야 한다. 안그러면 예외발생함.

쓰레드의 상태

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

쓰레드의 실행제어 메서드

sleep()

쓰레드 멈춰!

static void sleep(long millis)	// 천분의 일초 단위
static void sleep(long millis, int nanos)	// 천분의 일초 + 나노초

천분의 일초 = 10^(-3)초
나노초 = 10^(-9)초

깨울 때 InterruptedException이 발생한다. 그래서 예외처리를 필수로 해줘야한다.

void delay(long millis) {
  try {
      Thread.sleep(1, 500000);	// 쓰레드를 0.0015초 동안 멈추게 한다.
  } catch(InterruptedException e) {}
}

이렇게 하나의 메서드로 묶어서 사용하는게 좋다.

특정 쓰레드를 지정해서 재우지는 마라. 안좋은 코드. 오해할 수도 있기 때문

try {
	th1.sleep(2000);	// 이렇게 하지마라
} catch(InterruptedException e) {}

interrupt()

대기상태(WAITING)인 쓰레드를 실행대기 상태(RUNNABLE)로 만든다.
대기상태는 잠잔다던가 wait한다던가 join으로 대기중이라던가 있는데 얘를 그냥 깨우는것이다.

void	interrupt()	// 쓰레드의 interrupted상태를 false에서 true로 변경
boolean	isInterrupted() // interrupted가 어떤상태인지 반환. 잠자고 있는상태인지 알수있음
static boolean interrupted // 위처럼 상태를 알려주는데, 그 후에 false로 초기화시켜버림. (재워버림)

interrupted() 는 static 이므로 사용할때 Thread.interrupted() 형태로 써야한다.
isInterrupted()는 th1.isInterrupted() 처럼 인스턴스 선언한 클래스 사용가능.

while (downloaded && !isInterrupted()) {
// 다운로드를 진행하다가 isInterrupted()로 인해 다운로드 취소버튼을 눌렀다면(false) 와일문을 빠져나오게 한다.

suspend(), resume(), stop()

쓰레드의 실행을 일시정지, 재개, 완전정지 시킨다.

void suspend()	쓰레드를 일시정지 시킨다.
void resume()	suspend()에 의해 일시정지된 쓰레드를 실행대기상태로 만든다.
void stop()	쓰레드를 즉시 종료시킨다.

근데 쓰지 말라고 한다. 왜냐, dead-lock(교착상태)을 일으킬 가능성이 있기 때문이다. 이클립스에서도 쓰지말라고 suspend() 이렇게 나옴
직접 구현해서 쓰면 된다고 한다. 아래처럼

class ThreadEx17_1 implements Runnable {
	boolean suspended = false;
   	boolean stopped = false;
    
   	public void run() {
   		while(!stopped) {
  			if(!suspended) {
				/* 쓰레드가 수행할 코드 */
   			}
  		}
   	}
   	public void suspend() { suspended = true; }
   	public void resume() { suspended = false; }
   	public void stop() { stopped = true; }
}

join()

지정된 시간동안 특정 쓰레드가 작업하는 것을 기다린다.

void join()	// 작업이 모두 끝날 때까지
void join(long millis)	// 천분의 일초 동안
void join(long millis, int nanos)	// 천분의 일초 + 나노초 동안

예외처리를 해야한다.(InterruptedException이 발생하면 작업 재개)

ThreadEx11_1 th1 = new ThreadEx11_1();
ThreadEx11_2 th2 = new ThreadEx11_2();
th1.start();
th2.start();

try {
	th1.join();	// main쓰레드가 th1의 작업이 끝날때까지 기다림
   	th2.join();	// th2
} catch(InterruptedException e) {}

yield()

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

boolean suspended = false;
boolean stopped = false;

Thread th;

MyThreadEx18(String name) {
	th = new Thread(this, name);
}

public void run() {
	while(!stopped) {
   		if(!suspended) {	// 작업이 필요하면 작업
  			/* 작업 수행 */
 			try {
 				Thread.sleep(1000);
  			} catch(InterruptedException e) {}
  		} else {	// 작업이 끝났다면
  			Thread.yeild();	// 남은 시간을 양보
   		}
   	}
}

쓰레드의 동기화

멀티 쓰레드 프로세스에서는 다른 쓰레드의 작업에 영향을 미칠 수 있다.
진행중인 작업이 다른 쓰레드에게 간섭받지 않게 하려면 '동기화'가 필요

동기화 - 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 막는 것

간섭받기 않아야 하는 문장들을 임계 영역으로 설정
임계영역은 락(lock)을 얻은 단 하나의 쓰레드만 출입가능(뮤텍스?)

sysnchronized를 이용한 동기화

방법 2가지

  1. 메서드 전체를 임계영역으로 지정
public synchronized void withdraw(int money) {
	//...
}	// 메서드 전체가 임계 영역
  1. 특정한 영역을 임계 영역으로 지정
public void widthdraw(int money) {
	synchronized(객체의 참조변수) {
		//...
	}	// 여기 안에만 임계 영역
}

임계영역이 많아지면 비효율적이 되기때문에 웬만하면 2번으로 하고 사용을 자제하자.

wait()과 notify()

동기화의 효율을 높이기 위해 wait(), notify()를 사용
Object클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있다.

wait()		// 객체의 lock을 풀고 쓰레드를 해당 객체의 waiting pool에 넣는다.
notify()	// waiting pool에서 대기중인 쓰레드 중의 하나를 깨운다.
notifyAll()	// waiting pool에서 대기중인 모든 쓰레드를 깨운다.
class Account {
	int balance = 1000;
    
   	public synchronized void withdraw(int money) {
   		while (balance < money) {	// 인출할 돈이 부족하면
  			try {
   				wait();	// 대기 - 락을 풀고 기다린다. 통지를 받으면 락을 재흭득(ReEntrance)
   			} catch(InterruptedException e) {}
   		}
  		balance -= money;
   	}
    
   	public synchronized void deposit(int money) {
   		balance += money;
  		notify();	// 통지 - 대기중이 쓰레드 중 하나에게 알림.
   	}
}

예제

import java.util.ArrayList;

class Customer2 implements Runnable {
	private Table2 table;
	private String food;
	
	Customer2(Table2 table, String food) {
		this.table = table;
		this.food = food;
	}
	
	public void run() {
		while(true) {
			try { Thread.sleep(100);} catch(InterruptedException e) {}
			String name = Thread.currentThread().getName();
			
			table.remove(food);
			System.out.println(name + " ate a " + food);
		}
	}
}

class Cook2 implements Runnable {
	private Table2 table;
	
	Cook2(Table2 table) {this.table = table; }
	
	public void run() {
		while(true) {
			int idx = (int)(Math.random()*table.dishNum());
			table.add(table.dishNames[idx]);
			try { Thread.sleep(10); } catch(InterruptedException e) {}
		}
	}
}

class Table2 {
	String[] dishNames = { "donut", "donut", "burger" };
	final int MAX_FOOD = 6;
	private ArrayList<String> dishes = new ArrayList<>();
	
	public synchronized void add(String dish) {
		while(dishes.size() >= MAX_FOOD) {
			String name = Thread.currentThread().getName();
			System.out.println(name + " is waiting.");
			try {
				wait();	// COOK쓰레드를 기다리게 한다.
				Thread.sleep(500);
			} catch(InterruptedException e) {}
		}
		dishes.add(dish);
		notify();	// 기다리고 있는 CUST를 깨우기 위함.
		System.out.println("Dishes:" + dishes.toString());
	}
	
	public void remove(String dishName) {
		synchronized(this) {
			String name = Thread.currentThread().getName();
			
			while(dishes.size()==0) {
				System.out.println(name+" is waiting.");
				try {
					wait();	// CUST쓰레드를 기다리게 한다.
					Thread.sleep(500);
				} catch(InterruptedException e) {}
			}
			while(true ) {
				for(int i=0; i<dishes.size();i++) {
					if(dishName.equals(dishes.get(i))) {
						dishes.remove(i);
						notify();	// 잠자고 있는 COOK을 깨우기 위해
						return;
					}
				}
				
				try {
					System.out.println(name + " is waiting.");
					wait();	// 원하는 음식이 없는 CUST쓰레드를 기다리게 한다.
					Thread.sleep(500);
				} catch(InterruptedException e) {}
			}
		}
		
	}
	public int dishNum() { return dishNames.length; }
}

class Ex13_15 {

	public static void main(String[] args) throws Exception {
		// TODO Auto-generated method stub
		Table2 table = new Table2();
		
		new Thread(new Cook2(table), "COOK").start();
		new Thread(new Customer2(table, "donut"), "CUST1").start();
		new Thread(new Customer2(table, "burger"), "CUST2").start();
		Thread.sleep(2000);
		System.exit(0);
	}

}
post-custom-banner

0개의 댓글