IT/JAVA

스레드(Thread)

So1_b 2022. 7. 7. 16:30

배운점

 

프로세스와 스레드

프로세스란

실행 중인 프로그램(cup를 할당 받은 상태)이나

cup할당을 기다리는 실행 대기 상태의 프로그램을 의미한다.

프로세스는 적어도 하나의 스레드(작업단위)를 가지고 있다. 

 

한 프로세스에서 작업을 수행하는 단위인 스레드가 하나 존재하는 것을 싱글 스레드 프로세스

여러개 있는 것을 멀티 스레드 프로세스라고 한다. 

 

멀티 스레드 환경일 경우 

   1. CPU의 유휴 시간을 줄이는 등 시스템 자원을 효율적으로 이용할 수 있다.

       ( 한 프로그램이 실행되다가 사용자에게 입출력을 요구하는 경우 CPU는 입력이 완료될때 까지 기다린다.

         이렇게 CPU가 쉬는 시간을 유휴시간(idle time)이라고 한다. 유휴 시간에 다른 스레드/ 프로세스를 실행 시켜

         효율적으로 사용할 수 있도록 한다.)

 

   2. 단위 시간 당 처리되는 프로그램의 수를 의미하는 시스템의 처리율이 향상된다.

 

   3. 문맥 교환 시간이 걸린다는 단점이 있다.

       (  실행되는 스레드가 변경 될 때 실행에 필요한 정보를 저장, 복구시키는 과정이 필요하다. 이것을

          문맥 교환이라고 한다. )

 

스레드의 종류

1. 사용자 스레드 

2. 데몬 스레드

      일반 스레드의 작업을 보조적으로 돕는 역할을 한다.

      일반 스레드가 모두 종료되면 보조할 대상이 사라져 자동으로 종료된다.

      사용 예) 가비지 컬렉터, 자동저장 기능, 화면 자동갱신 등

 

    보통 무한 루프나 조건문을 이용해 대기하다가 특정조건이 만족되면 작업을 수행하는 형식으로 작성한다.

더보기

     관련 메서드

threadClass t = new threadClass( );  //Thread를 상속받는 클래스 threadClass
t.setDaemon(true);	//데몬 스레드 설정 (초기값 flase: 사용자 스레드)
t.start();

boolean isDaemon() : 데몬 스레드인지 확인 return true: 데몬스레드, false: 사용자 스레드. 

void setDaemon(boolean on) :   스레드를 데몬/ 사용자 스레드로 변경. 

반드시 t.start()사용전에 호출해 사용해야 한다. 안그러면 illegalThreadStateException발생.

 

 

스레드의 생명주기

New 스레드 객체가 만들어진 상태
Runnable 실행가능한 상태로 start()를 사용하면 New 에서 이동한다.
Running cpu를 할당받아 실행 중인 상태. run()를 사용하는 중이다.
Blocked  I/O request, synchronized 블록으로 인해 Lock이 풀릴 때 까지 일시정지
Wait 일시정지 상태
Dead run()이 끝나 종료된 상태 (== TERMINATED?)

 

 

스레드 생성 방식 

1. Thread 클래스 상속

   

public class ThreadEx extends Thread {
	@Override
    public void run() {
    	//스레드가 실행할 동작 구현
    }
    
    public static void main(String[] args) {
    	ThreadEx t = new ThreadEx();
        t.start();
    }
}

2. Runnable인터페이스 구현

 

public class ThreadEx implements Runnable{
	@Override
    public void run() {
    	//스레드가 실행 할 동작 구현
    }
    
    public static void main(String[] args) {
    	ThreadEx th = new ThreadEx();
        Thread t = Thread(th); //Thread(Runnable r);
        t.start();
        
    }
 }

 

- 자바는 단일 상속만 지원한다. 

  스레드를 사용하는 클래스가 다른 부모 클래스를 상속하고 있어 Thread클래스를 상속할 수 없을 경우

  Runnable인터페이스를 이용해 스레드를 사용한다.

 


 

 

Thread의 메서드

void start() 생성된 스레드(New)를 Runnable상태로 만들어 줌.
내부적으로는 Stack을 생성함.
static void sleep(long millis) 현재 실행중인 스레드(Running)를 지정된 시간만큼 Wait상태로 만듦

InterruptedException예외처리를 반드시 해야 함.

* 다시 Runnable상태로 가는 조건 
                          1. time up (= 시간 종료)
                          2. interrupt (= 깨우는 것)
void join ( ) join()를 호출한 스레드가 종료될 때까지 현재 실행중인 스레드를 Wait상태로 만듦
void interrupt() non-Runnable상태의 스레드에 InterruptedException을 발생시켜 Runnable상태로 만든다.
주로 현재 실행 중인 스레드를 정상적으로 종료시키고 다른 스레드를 실행하고 싶을 때 사용함

종료시키고 싶은 스레드가 Runnable상태일 경우 예외 발생하지 않고 non-Runnable상태가 되을 때 발생됨. ( 인터럽트를 예약하는 것)

종료 상태의 스레드라고 할지라도 interrupt()를 요청받으면 interrupted상태가 기록되고 
isInterrupt(), interrupted()를 통해 확인할 수 있다. 

현재 스레드가 interrupt를 가할 수 없는 경우 SecurityException 발생

 

boolean isInterrupted() 인터럽트 예약을 확인 (interrupted변수 확인)
static boolean interrupted() 현재 실행 중인 스레드의 인터럽트 예약 확인 후 예약 취소 ( interrupted변수 false으로 초기화)
* static이기 때문에 현재 실행 중인 스레드가 똑같다면 Thread.interrupted()와 다른 스레드명.interrupted()는 똑같이 현재 실행 중인 스레드의 인터럽트를 확인함. 
void setPriority( int newPriority ) 스레드의 우선순위를 정한다 
초기값은 5
JVM의 우선순위는 1~10으로 os의 스케줄러가 참고하는 수준에 그침
int getPriority() 스레드의 우선순위 반환
static void yield( ) 현재 실행중인 스레드의 남은 cpu사용시간을 다음 스레드에게 양보함.
static Thread currentThread( ) 현재 실행중인 스레드 객체를 반환함.

 


동기화와 임계영역

 

임계 영역이란 프로세스의 영역 중에서 공유 자원을 접근하는 코드 영역을 의미한다.

     (공유 자원으로는 static 변수, 인스턴스 변수 등 다양하다.)

멀티 스레드 프로세스 환경에서 여러 스레드끼리 자원을 공유할때 스레드는 서로 영향을 미치게 되어 있어 스레드의 결과값이 달라질 수 있는 문제점을 안고 있다.

그래서 스레드가 공유자원을 사용하고 있을 경우 그 작업이 종료될 때까지 기다려주는 동기화가 필요하다.

 

자바에서는 객체가 heap영역에 생성될 때 자동으로 lock이 생성되는데 이를 임계영역의 열쇠처럼 사용해 임계영역에 한 스레드만 접근가능하도록 동기화를 구현한다.

 

스레드의 생명주기를 기준으로 설명을 하면

 Thread1이 Running상태가 되어 실행 중 Synchronized블록 (임계영역) 을 만나면 Blocked상태가 되지만 더 정확하게는 Object's Lock Pool로 이동해 lock을 할당받는다. 

lock을 획득해 다시 Runnable상태로 이동한 후 스레드 스케줄러에의해 Running상태가 되어 Synchronized블록을 수행하는 것이다. Synchronized블록이 끝나면 자동으로 lock이 해제가 되고 Object's Lock Pool에서 대기중인 다른 스레드가 lock을 얻게 된다.

 

다 생략하고 요약하자면 공유 객체의 lock이 있어야 Synchronized블록(임계 영역)에 진입이 가능하다.

 

동기화 하는 방법

1. 메서드의 동기화

2. 특정 부분의 동기화

 

 

 

 

 

Object클래스의 wait(), notify(), notifyAll()

 

동기화를 할 경우 한 스레드만 임계영역에 진입할 수 있기 때문에 효율성이 떨어짐. 상황에 따라 lock을 획득한 스레드를 일시중지하고 다른 스레드에게 제어권을 넘겨 이를 극복하기 위함.

 

 wait():

InterruptedException을 발생시켜 현재 실행 중인 스레드(Running)의 lock을 회수하고 깨어날 때까지 공유객체의 wait pool에서 대기한다.  (interrupted status가 초기화 된다?)

만약 현재스레드가 공유객체의 lock소유자가 아니라면 IllegalMonitorStateException예외 발생.

 

notify():

 Object's wait pool에서 대기중이었던 스레드를 Object's Lock pool로 이동시킨다. 

 만약 대기중인 스레드가 없으면 그냥 넘어간다. 

공유 객체의 lock을 가진 스레드가 호출할 수 있고 이로 인해 깨어난 스레드는 현재 스레드가 lock을 해제하기 전까지 실행될 수 없다. 

 

 

package javaBible;
import java.util.*;

class Car {
	private List<String> carList = null;
	
	public Car() {
		carList=new ArrayList<String>();
	}
	
	public String getCar() {
		String carName = null;
		switch ( (int)(Math.random()*3) ) { //0 ~ 2 랜덤
		case 0: carName="SM5"; break;
		case 1: carName="매그넌스"; break;
		case 2: carName="카렌스";  break;
		}
		return carName;
	}
	
	public synchronized String pop() {
		String carName=null;
		if(carList.size() == 0) {
			try {
				System.out.println("차가 없습니다. 생산이 완료될때 까지 기다리세요.");
				this.wait(); //현재 스레드가(lock회수) wait pool으로 이동 
			}catch(InterruptedException e) {
				System.out.println("InterruptedException : "+Thread.currentThread().getName());
			}
		}
		
		carName =(String)carList.remove(carList.size()-1);
		System.out.printf("손님이 차를 구매했습니다.. 기종(%s)%n",carName);
		
		return carName;
	}
	
	public synchronized void push(String car) {
		carList.add(car);
		System.out.printf("자동차 생산완료 기종(%s)%n",car);
		
		if(carList.size() == 5) this.notify(); //Car의 wait pool에 대기중인 스레드를 Lock pool로 이동시킴.
	}
	
	
}


class Producer implements Runnable {
	private Car car;
	
	public Producer(Car car) {
		this.car = car;
	}
	
	public void run() {
		String carName = null;
		for(int i=0; i<20; i++) {
			carName=car.getCar();	// 생산할 차량 선택
			car.push(carName);		// 자동차 생산
			try {
				Thread.sleep((int)Math.random()*200);  //0.001 ~ 0.199초 동안 sleep
			}catch(InterruptedException e) {e.printStackTrace();}
			
		}
	}
}

class Customer implements Runnable {
	private Car car;
	
	public Customer(Car car) {
		this.car=car;
	}
	
	public void run() {
		String carName = null;
		for(int i=0; i<20; i++) {
			carName= car.pop();	//자동차 구매
			
			try { Thread.sleep((int)Math.random()*200);}catch(InterruptedException e) {e.printStackTrace();}
		}
	}
}

public class ProducerCustomerTest {
	
	public static void main(String[] args) {
		Car c= new Car();  //공유 자원 Car객체
		
		Producer producer = new Producer(c);
		Thread tProducer = new Thread(producer);
		
		Customer customer = new Customer(c);
		Thread tCustomer = new Thread(customer);
		
		tCustomer.start();
		tProducer.start();
	}
}

 

참고 사이트

http://ojc.asia/bbs/board.php?bo_table=LecJava&wr_id=768

https://harrydony.tistory.com/421