자바/프로세스와 쓰레드

쓰레드의 실행제어

백_곰 2022. 2. 20. 17:45

1. 쓰레드의 실행제어

- 쓰레드 프로그래밍이 어려운 이유는 동기화(synchronization)스케줄링(scheduling) 때문이다. 

 

- 멀티쓰레드를 구현하기 위해서는 보다 정교한 스케줄링을 통해 자원 낭비없이 프로그래밍

해야한다.

 

 

- 아래의 코드는 쓰레드 상태 관련 메서드들이다.

 

메서드 설명
static void sleep(long millis), sleep(long millis, int nanos) 지정된 시간(천분의 일초 단위) 동안 쓰레드를 일시정지한다. 시간이 지나면 다시 실행대기 상태가
된다.
void join(), join(long millis), join(long millis, int nanos) 지정된 시간만큼 작업을 수행 후, 호출한 쓰레드로 다시 돌아온다.
void interrupt() sleep() 또는 join()에 의해 일시정지상태인 쓰레드를 깨워서 실행대기 상태로 만든다.
해당 쓰레드에서는 InterruptedException이 발생함으로써 일시정지 상태를 벗어나게 한다.
void stop() 쓰레드를 즉시 종료시킨다.
void suspend() 쓰레드를 일시정지 시킨다.
resume()을 호출하면 다시 실행대기 상태가 된다.
void resume()  
static void yield() 실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보하고 자신은 실행대기 상태가 된다.
응답성을 높이기 위해서 만들어지게 되었다.

 

( resume(), stop(), suspend()쓰레드를 교착상태(dead-lock)로 만들기 쉽기 때문에, deprecated가 되었다. )

 

( 쓰레드가 실제로 실행될 때 쓰레드 상태에 대한 과정은 749p를 참고한다. )

 

 

 

 

1-1. 쓰레드의 실행제어에 따른 메서드를 이해하기 위한 예제(1)

: sleep에 대한 함수를 구현해보고 프로그래밍 순서를 파악하는 예제이다.

 

public class Exercise009 {
	public static void main(String[] args) {
		ThreadEx7 t1 = new ThreadEx7();
		ThreadEx8 t2 = new ThreadEx8();
		t1.start();
		t2.start();
		
		try {
			t1.sleep(2000);
			//Thread.sleep(1000);
		}catch (Exception e) {
			// TODO: handle exception
		}
		System.out.println("<<main 종료>>");
	}
}

class ThreadEx7 extends Thread{
	public void run() {
		for(int i=0; i<300; i++) {
			System.out.print("-");
		}
		System.out.println("<<t1 종료>>");
	}
}

class ThreadEx8 extends Thread{
	public void run() {
		for(int i=0; i<300; i++) {
			System.out.print("|");
		}
		System.out.println("<<t2 종료>>");
	}
}

 

( 위 코드만 보면 출력이 "<<t2 종료>>"가 먼저될 거 같지만 "<<t1 종료>>"가 먼저될 수 있다. )

( 저렇게 순서가 다른 이유는 "t1.sleep(2000);" 부분 때문이다. )

 

( t1.sleep(2000)을 하면 해당 쓰레드가 sleep에 빠져들 것 같지만 사실 실제로 영향을 받는 것은

main 메서드를 실행하는 main 쓰레드이다. )

 

( 그래서 참조변수로 호출하기 보다는 "Thread.sleep()"으로 선언하는 것이 좋다. )

( (참고) "<<main 종료>>"는 제일 마지막에 출력된다. )

 

 

 

 

1-2. 쓰레드의 실행제어에 따른 메서드를 이해하기 위한 예제(2)

: interrupt()와 interrupted(), isInterrupted()에 대한 함수를 알아보고 구현해보는 예제이다.

 

( interrupt(): 쓰레드에게 작업을 멈추라고 요청한다. 단, 강제로 종료시키지는 못한다. )

( interrupted(): 쓰레드의 상태가 interrupted인지 true, false로 받아온다. )

( isInterrupted(): interrupted()와 같은 내용이지만 만약 true라면 상태를 false로 초기화시킨다. )

 

import javax.swing.JOptionPane;

public class Exercise010 {
	public static void main(String[] args) throws Exception {
		ThreadEx9 t1 = new ThreadEx9();
		t1.start();
		
		String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
		System.out.println("입력하신 값은 " + input + "입니다.");
		t1.interrupt();//첫 번째 실행
		System.out.println("isInterrupted(): " + t1.isInterrupted());
	}
}

class ThreadEx9 extends Thread{
	public void run() {
		int i=10;
		
		while(i!=0 && !isInterrupted()) {
			System.out.println(i--);
			try {
				Thread.sleep(500);
			}catch (InterruptedException e) {
				interrupt();//두번쨰 실행
			}
		}
		System.out.println("종료");
	}
}

( 10에서 0까지 while문을 돌리는데 만약 main에서 interrupt가 발생하게 되면, InterruptedException

발생시켜 쓰레드를 중단시키는 작업을 한다. )

 

( 순서는 interrupt()를 발생시켜서 catch문을 돌아가게 한 다음, 그 안에서 interrupt() 발생시켜

while문 종료를 수행한다. )

 

 

 

 

1-3. 쓰레드의 실행제어에 따른 메서드를 이해하기 위한 예제(3)

: suspend(), resume(), stop()을구현해보는 예제이다.

 

import javax.swing.JOptionPane;

public class Exercise011 {
	public static void main(String[] args) {
		RunImplEx11 r = new RunImplEx11();
		Thread t1 = new Thread(r, "*");
		Thread t2 = new Thread(r, "**");
		Thread t3 = new Thread(r, "***");
		
		t1.start();
		t2.start();
		t3.start();
		
		try {
			Thread.sleep(2000);
			t1.suspend();
			Thread.sleep(2000);
			t2.suspend();
			Thread.sleep(3000);
			t1.resume();
			Thread.sleep(3000);
			t1.stop();
			t2.stop();
			Thread.sleep(2000);
			t3.stop();
		}catch (InterruptedException e) {
			// TODO: handle exception
		}
	}
}

class RunImplEx11 implements Runnable{
	int i=1;
	
	public void run() { 
		while(true) {
			System.out.println(Thread.currentThread().getName() + " " + i++);
			try {
				Thread.sleep(1000);
			}catch (InterruptedException e) {
						// TODO: handle exception
			}
		}
	}
}

( suspend()stop()은 교착상태를 유발하기 때문에, deprecated된 상태인데, 일단 이해하기 위해

예제를 수행했다. )

 

( 위의 숫자를 보여준 것은 new Thread()를 할 때 같은 참조변수를 사용해서 Runnable을 초기화

시켰기 때문에 저렇게 하면 상태에 대한 내용이 공유될 수 있다는 위험을 알려준다. )

 

 

( 그래서 아래의 코드처럼 suspend()stop()에 대해 코드를 새로 정의하고 각각의 Runnable을

만들어 준다. )

public class Exercise012 {
	public static void main(String[] args) {
		RunImplEx12 r1 = new RunImplEx12();
		RunImplEx12 r2 = new RunImplEx12();
		RunImplEx12 r3 = new RunImplEx12();
		Thread t1 = new Thread(r1, "*");
		Thread t2 = new Thread(r2, "**");
		Thread t3 = new Thread(r3, "***");
		
		t1.start();
		t2.start();
		t3.start();
		
		try {
			Thread.sleep(2000);
			r1.suspend();
			Thread.sleep(2000);
			r2.suspend();
			Thread.sleep(3000);
			r1.resume();
			Thread.sleep(3000);
			r1.stop();
			r2.stop();
			Thread.sleep(2000);
			r3.stop();
		}catch (InterruptedException e) {
			// TODO: handle exception
		}
	}
}

class RunImplEx12 implements Runnable{
	boolean suspended = false;
	boolean stopped = false;
	int i=1;
	
	public void run() {
		while(!stopped) {
			if(!suspended) {
				System.out.println(Thread.currentThread().getName() + " " + i++);
				try {
					Thread.sleep(1000);
				}catch (InterruptedException e) {
					// TODO: handle exception
				}
			}
		}
	}
	public void suspend() { suspended = true; }
	public void stop() { stopped = true; }
	public void resume() { suspended = false; }
}

( 위와 다르게 공유가 안 되어있기 때문에 출력문의 숫자가 각각 따로 나온다. )

( Runnable에 suspend()와 resume()과  stop()을 따로 구현했다. )

 

 

( 아래의 코드는 더 객체지향적으로 만든 코드이다. )

public class Exercise013 {
	public static void main(String[] args) {
		ThreadEx13 t1 = new ThreadEx13("*");
		ThreadEx13 t2 = new ThreadEx13("**");
		ThreadEx13 t3 = new ThreadEx13("***");
		t1.start();
		t2.start();
		t3.start();
		
		try {
			Thread.sleep(2000);
			t1.suspend();
			Thread.sleep(2000);
			t2.suspend();
			Thread.sleep(3000);
			t1.resume();
			Thread.sleep(3000);
			t1.stop();
			t2.stop();
			Thread.sleep(2000);
			t3.stop();
		}catch (InterruptedException e) {
		}
	}
}
class ThreadEx13 implements Runnable{
	boolean suspended = false;
	boolean stopped = false;
	Thread th;
	
	public ThreadEx13(String name) {
		th = new Thread(this,name);
		//Thread(Runnable r, String name)
	}
	
	@Override
	public void run() {
		while(!stopped) {
			if(!suspended) {
				System.out.println(Thread.currentThread().getName());
				try {
					Thread.sleep(1000);
				}catch (InterruptedException e) {}
			}
		}
		System.out.println(Thread.currentThread().getName() + " ~ stopped");
	}
	public void suspend() { suspended = true; }
	public void stop() { stopped = true; }
	public void resume() { suspended = false; }
	public void start() { th.start(); }
}

 

 

 

 

1-4. 쓰레드의 실행제어에 따른 메서드를 이해하기 위한 예제(4)

: yield()에 대해 알아보고 구현해보는 예제이다. 

 

( yield(): 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보한다. )

( yield()interrupt()와 적절히 사용한다면, 프로그램의 응답성을 높이고 보다 효율적인 실행이 가능

하게 할 수 있다. )

 

public class Exercise014 {
	public static void main(String[] args) {
		ThreadEx14 t1 = new ThreadEx14("*");
		ThreadEx14 t2 = new ThreadEx14("**");
		ThreadEx14 t3 = new ThreadEx14("***");
		t1.start();
		t2.start();
		t3.start();
		
		try {
			Thread.sleep(2000);
			t1.suspend();
			Thread.sleep(2000);
			t2.suspend();
			Thread.sleep(3000);
			t1.resume();
			Thread.sleep(3000);
			t1.stop();
			t2.stop();
			Thread.sleep(2000);
			t3.stop();
		}catch (InterruptedException e) {
			// TODO: handle exception
		}
	}
}
class ThreadEx14 implements Runnable{
	boolean suspended = false;
	boolean stopped = false;
	Thread th;
	
	public ThreadEx14(String name) {
		th = new Thread(this,name);
		//Thread(Runnable r, String name)
	}
	
	@Override
	public void run() {
		String name = th.getName();
		while(!stopped) {
			if(!suspended) {
				System.out.println(name);
				try {
					Thread.sleep(1000);
				}catch (InterruptedException e) {
					System.out.println(name + " - interrupted");
				}
			}else {
				Thread.yield();
			}
		}
		System.out.println(name + " ~ stopped");
	}
	public void suspend() { 
		suspended = true; 
		th.interrupt();
		System.out.println(th.getName() + " - interrupt() by suspend()");
	}
	public void stop() { 
		stopped = true; 
		th.interrupt();
		System.out.println(th.getName() + " - interrupt() by stop()");
	}
	public void resume() { suspended = false; }
	public void start() { th.start(); }
}

( 50~52번째 줄에 suspend가 true이면 else를 넣어서 Thread.yield()를 발생시켰다. 그러므로,

while문을 돌지 않고 효율적으로 사용하게 된다. )

( 또한 suspend()와 stop()에 th.interrupt()를 넣었는데, 그 이유는 Thread.sleep(1000)에 상태에 있을때

바로 나올 수 있게 하기 위해서이다. 그러므로, 응답성이 좋아진다. )

 

 

 

 

1-5. 쓰레드의 실행제어에 따른 메서드를 이해하기 위한 예제(5)

: join()에 대해 알아보고 구현해본다.

 

( join(): 지정된 시간만큼 작업을 수행 후, 호출한 쓰레드로 다시 돌아온다. )

( 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 할 때  join()을 사용한다. )

( 만약 지정된 시간이 없다면, 다른 쓰레드가 작업이 완료될 때까지 기다린다. )

( join() 또한 sleep()처럼 try-catch문으로 감싸야 하며, sleep()과 다른 점은 join()은 현재 쓰레드가

아닌 특정 쓰레드에 대해 동작해야 하므로 static 메서드가 아니다. )

 

public class Exercise015 {
	static long startTime;
	
	public static void main(String[] args) {
		ThreadEx15_1 th1 = new ThreadEx15_1();
		ThreadEx15_2 th2 = new ThreadEx15_2();
		th1.start();
		th2.start();
		startTime = System.currentTimeMillis();
		
		try {
			th1.join();  //maain 쓰레드가 th1의 작업이 끝날 때까지 기다린다.
			th2.join();  //main 쓰레드가 th2의 작업이 끝날 때까지 기다린다.
		}catch (InterruptedException e) {
			
		}
		System.out.println("소요시간: " + (System.currentTimeMillis() - Exercise015.startTime));
	}
}

class ThreadEx15_1 extends Thread {
	public void run() {
		for(int i=0; i<300; i++) {
			System.out.print(new String("-"));
		}
	}
}

class ThreadEx15_2 extends Thread {
	public void run() {
		for(int i=0; i<300; i++) {
			System.out.print(new String("|"));
		}
	}
}

 

( join()을 사용하지 않았다면, main 쓰레드는 먼저 종료되었을 것이다. )

 

 

 

 

1-6. 쓰레드의 실행제어에 따른 메서드를 이해하기 위한 예제(6)

: join()을 활용하여 가비지 컬렉터를 구현한다.

 

public class Exercise016 {
	public static void main(String[] args) {
		ThreadEx16 gc = new ThreadEx16();
		gc.setDaemon(true);
		gc.start();
		
		int requireMemory = 0;
		
		for(int i=0; i<20; i++) {
			requireMemory = (int) (Math.random() * 10) *20;	
			//필요한 메모리가 사용할 수 있는 양보다 크거나 전체 메모리의 60%인 경우
			// gc를 꺠운다.
			if(gc.freeMemory() < requireMemory || gc.freeMemory() < gc.totalMemory()*0.4) {
				gc.interrupt();
			}
			gc.usedMemory += requireMemory;
			System.out.println("usedMemory: " + gc.usedMemory);
		}
	}
}

class ThreadEx16 extends Thread {
	final static int MAX_MEMORY = 1000;
	int usedMemory;
	
	public void run() {
		while(true) {
			try {
				Thread.sleep(10000);
			}catch (InterruptedException e) {
				System.out.println("Awaken by interrupt().");
			}
			gc();
			System.out.println("Garbage Collected. Free Memory: " + freeMemory());
		}
	}
	
	public void gc() {
		usedMemory -= 300;
		if(usedMemory<0) usedMemory = 0;
	}
	public int totalMemory() { return MAX_MEMORY; }
	public int freeMemory() { return MAX_MEMORY - usedMemory; }
}

( 가비지 컬렉터를 수행하는 쓰레드를 데몬으로 만들어서 10초동안 대기하다가 gc() 함수를 수행

하도록 만들었다. )

( gc() 함수는 가비지 컬렉션을 수행하는 것이다. )

 

 

( 그러나 출력문을 보면 1000이 넘었음에도 계속 수행되었다. 그 이유는 쓰레드 gc가 interrupt()에

의해서 깨어났음에도 불구하고 gc()가 수행되기 이전에 main 쓰레드의 작업이 수행되어 메모리를

사용하기 때문이다. )

 

( 그래서 gc()가 돌아갈 수 있게 join()을 이용해서 시간을 줘야한다. 즉, 메모리를 확보한 다음 main

쓰레드를 돌린다는 의미이다. )

 

( 아래와 같이 try-catch문을 넣어서 다시한번 돌려보자. )

( 1000을 넘기지 못하는 것을 볼 수 있다. )