[필기정리]Day27 - 쓰레드 이론 및 문제

SW/Java

2020. 7. 16. 22:00

# 프로세스(Process) : 실행 중인 프로그램(Program)

   쓰레드(Thread) : 프로세스의 자원을 이용해서 실제로 작업을 수행하는 단위

                        모든 프로세스 내에 존재하며 수행흐름을 여러 개 만들어 실행할 수 있다.

 

★ 자바에서 Thread를 만드는 방법

Thread 클래스를 상속받는 방법

- java.lang.Thread클래스를 상속받는다.

그리고 Thread가 가지고 있는 run()메소드를 오버라이딩한다.

 

- sleep() : 지정된 시간동안 쓰레드를 멈추게 하는 메소드

             호출 시 항상 try - catch문으로 예외 처리 해줘야 한다.

 

- 쓰레드 동작 시 run()이 아닌 start()를 호출한다.

  즉, start()를 호출하지 않으면 쓰레드는 동작하지 않는다.

 

메인 쓰레드가 종료되더라도 끝난 것이 아니다.

모든 쓰레드의 흐름이 종료되어야만 프로그램이 종료된다.

 

Runnable 인터페이스를 구현하는 방법

- Runnable 인터페이스는 run()을 가지고 있기 때문에 자동으로 run()이 생성된다.

 

// Tip : 자바는 단일상속만 지원하기 때문에 이미 다른 클래스를 상속받고 있던 경우

          쓰레드 클래스를 상속받을 수 없어 인터페이스 방법을 지원하는 것이다.

         다른 클래스 상속한 경우 반드시 Runnable 인터페이스 사용하기!

 

- 쓰레드 클래스를 상속받지 않은 경우 start()가 없기 때문에 사용할 수 없다.

  이런 경우 수행 시 Thread 객체를 만들어줘야 한다.

ex) - 수행결과는 동일하다.

MyThread t1 = new MyThread();
Thread trd = new Thread(t1);
trd.start();

 

# 쓰레드의 스케줄링(Scheduling)과 쓰레드의 우선순위 컨트롤
  둘 이상의 쓰레드가 생성될 수 있기 때문에, 
  자바 가상머신은(자바 가상머신의 일부로 존재하는 쓰레드 스케줄러는)

  쓰레드의 실행을 스케줄링(컨트롤)해야 한다. 


  스케줄링에 사용되는 알고리즘의 기본원칙은 다음과 같다.

- 우선순위가 높은 쓰레드의 실행을 우선한다.
- 동일한 우선순위의 쓰레드가 둘 이상 존재할 때는 CPU의 할당시간을 분배해서 실행한다.

 

자바의 쓰레드에는 우선순위라는 것이 할당된다. 

이는 가상머신에 의해서 우선적으로 실행되어야 하는 쓰레드의 순위를 의미하는 것으로, 

가장 높은 우선 순위는 정수 10으로, 가장 낮은 우선순위는 정수 1로 표현한다.

(따라서 총 10단계의 우선순위가 존재한다.) 

 

그리고 이러한 쓰레드의 우선순위는 프로그램상에서 변경 및 확인이 가능하다.
쓰레드의 실행방식은 시스템의 상황과 환경에 따라서 매우 많은 차이를 보인다. 

즉, "동일한 우선순위의 쓰레드들은 CPU의 할당시간을 적절히(골고루)나눠서 실행된다"라고만 이야기할 수 있을 뿐,

아주 엄밀하게 수치적으로 할당시간과 할당순서를 이야기할 수는 없다.

 

ex)

class MessageSendingThread extends Thread
{
	String message;
	int priority;
	
	public MessageSendingThread(String str) 
	{
		message=str;
	}
	public void run()
	{
		for(int i=0; i<1000000; i++)
			System.out.println(message+"("+getPriority()+")");
	}	
}

class PriorityTestOne
{
	public static void main(String[] args)
	{
		MessageSendingThread tr1=new MessageSendingThread("First");
		MessageSendingThread tr2=new MessageSendingThread("Second");
		MessageSendingThread tr3=new MessageSendingThread("Third");
		tr1.start();
		tr2.start();
		tr3.start();
	}
}

 

- 실행결과를 통해서 우선순위가 높은 쓰레드가 종료되어야,

  그 다음 우선순위의 쓰레드가 실행됨을 확인할 수 있다.
  보통 우선순위가 8인 쓰레드와 우선순위가 2인 쓰레드가

  대략 8대 2의 비율로 CPU를 할당 받아서 실행된다고 오해하는 경우가 있는데, 

  대부분의 시스템에서는 우선순위가 높은 쓰레드에게만 실행의 기회를 부여한다.

- 쓰레드의 우선순위가 지니는 의미
  사실 자바가 언어차원에서 쓰레드를 지원하고는 있지만,

  쓰레드는 그 특성상 운영체제에 상당히 의존적이다.


  즉, 가상머신이 동작하는 운영체제에 따라서 실행의 결과는 다르게 나타날 수 있다.

  특히 우선순위와 관련된 부분은 더욱 그러하다. 

 

  예를 들어서 총 7단계의 쓰레드 레벨을 지원하는 운영체제에서 자바 프로그램이 실행된다고 가정해 보자. 

  자바가 총 10단계의 쓰레드 우선순위를 제공한다 하더라도

  운영체제에서 7단계의 쓰레드 레벨을 지원하면, 실질적인 쓰레드 레벨은 7단계가 된다.

  때문에 자바의 우선순위 7과 8이 해당 운영체제의 우선순위 6으로 표현될 수도 있는 일이다. 

 

 이렇듯 우선순위를 기반으로 쓰레드 프로그래밍을 할 때에는 해당 운영체제에 대한 지식이 어느 정도 필요하다. 

 그리고 이러한 문제 때문에라도 쓰레드의 우선순위를 변경할 때에는 상수로 정의되어 있는
 MAX_PRIORITY, NORM_PRIORITY, MIN_PRIORITY 중 하나를 선택해서 변경하는 것이

 운영체제에 따른 차이를 최소화할 수 있는 방법이다.

 대부분의 시스템에서 우선순위가 높은 쓰레드에게만 실행의 기회를 부여하다 보니,

 우선순위가 낮은 쓰레드는 거의 실행되지 않는다고 생각할 수 있다. 

 그러나 프로그램의 실행내용을 잘 들여다 보면,

 CPU의 할당이 필요치 않는 데이터의 입출력에 대한 부분이 매우 높은 비율을 차지함을 알 수 있다. 

 

 간단하게는 파일의 입출력에서부터

 네트워크를 통한 데이터의 송수신 역시 CPU의 할당이 필요치 않는 데이터의 입출력에 해당이 된다.

 

 때문에 프로그램의 실질적인 흐름을 담당하는 쓰레드 역시 

 CPU의 할당이 필요치 않는 데이터의 입출력과 관련 있는 연산을 상당부분 처리한다고 볼 수 있다.
 그리고 이러한 상황에 놓였을 때(CPU의 할당이 필요치 않은 입출력을 처리하는 상황에 놓였을 때),

 쓰레드는 무리하게 CPU를 차지하려고 하지 않는다. 

 오히려 이러한 상황에서는 자신에게 할당된 CPU를 다른 쓰레드들에게 넘긴다.

 쓰레드의 바로 이러한 특성 때문에 우선순위가 낮은 쓰레드 역시 실행의 기회를 얻을 수 있는 것이다.

 

ex)

class PriorityTestTwo
{
	public static void main(String[] args)
	{
		MessageSendingThread tr1
			=new MessageSendingThread("First", Thread.MAX_PRIORITY);
		MessageSendingThread tr2
			=new MessageSendingThread("Second", Thread.NORM_PRIORITY);
		MessageSendingThread tr3
			=new MessageSendingThread("Third", Thread.MIN_PRIORITY);
		
		tr1.start();
		tr2.start();
		tr3.start();
	}
}

 

- 실행결과를 보면, 높은 우선순위의 쓰레드가 둘씩이나 존재함에도 불구하고

  꿋꿋이 실행되고 있는 가장 낮은 우선순위의 쓰레드를 볼 수 있다. 

 

  비록 우선순위가 낮은 쓰레드라 하더라도

  높은 우선순위의 쓰레드가 CPU를 양보해서 실행의 기회를 얻게 되면,

  최소 단위의 실행 시간은 보장을 받는다.

  따라서 위와 같은 실행결과를 보이는 것이다. 

 

  결론적으로 낮은 우선순위의 쓰레드도 충분히 실행의 기회를 얻을 수 있고,

  또 실제로 실행도 된다.

 

ex)

class MessageSendingThread extends Thread
{
	String message;
	
	public MessageSendingThread(String str, int prio) 
	{
		message=str;
		setPriority(prio);
	}
	public void run()
	{
		for(int i=0; i<1000000; i++)
		{
			System.out.println(message+"("+getPriority()+")");
			
			try
			{
				sleep(1);
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
			}
		}
	}	
}

class PriorityTestThree
{
	public static void main(String[] args)
	{
		MessageSendingThread tr1
			=new MessageSendingThread("First", Thread.MAX_PRIORITY);
		MessageSendingThread tr2
			=new MessageSendingThread("Second", Thread.NORM_PRIORITY);
		MessageSendingThread tr3
			=new MessageSendingThread("Third", Thread.MIN_PRIORITY);
		
		tr1.start();
		tr2.start();
		tr3.start();
	}
}

 

Q. [쓰레드 클래스의 정의와 쓰레드의 생성]

RunnableThread.java에서는 총 두 개의 쓰레드를 생성해서 각각 1부터 50까지,

그리고 51부터 100까지 덧셈을 진행하게 하고,

그 결과를 취해서 최종적으로 1부터 100까지의 덧셈결과를 출력하였다.

이번에는 이 예제를 Runnable 인터페이스를 구현하는 방식이 아닌,

쓰레드 클래스를 정의하는 방식으로 변경해보자.

class Sum
{
	int num;
	public Sum() { num=0; }
	public void addNum(int n) { num+=n; }
	public int getNum() { return num; }
}

class AdderThread extends Sum implements Runnable
{	
	int start, end;
	
	public AdderThread(int s, int e)
	{
		start=s;
		end=e;
	}
	public void run()
	{
		for(int i=start; i<=end; i++)
			addNum(i);
	}
}

class RunnableThread
{
	public static void main(String[] args)
	{
		AdderThread at1=new AdderThread(1, 50);
		AdderThread at2=new AdderThread(51, 100);
		Thread tr1=new Thread(at1);
		Thread tr2=new Thread(at2);
		tr1.start();
		tr2.start();
		
		try
		{
			tr1.join();
			tr2.join();
		}
		catch(InterruptedException e)
		{
			e.printStackTrace();
		}
		
		System.out.println("1~100까지의 합: "+(at1.getNum()+at2.getNum()));
	}
}

A.

/*
class Sum
{
	int num;
	public Sum() { num=0; }
	public void addNum(int n) { num+=n; }
	public int getNum() { return num; }
}
*/
class SumThread extends Thread
{
	int num;	
	int start, end;
	
	public SumThread(int s, int e)
	{
		num = 0;
		start = s;
		end = e;
	}
	public void run()
	{
		for(int i=start; i<=end; i++)
			addNum(i);
	}
	public void addNum(int n) { num+=n;}
	public int getNum() { return num; }
}

class Sum1To100
{
	public static void main(String[] args)
	{
		SumThread st1=new SumThread(1, 50);
		SumThread st2=new SumThread(51, 100);
		st1.start();
		st2.start();
		
		try
		{
			st1.join();
			st2.join();
		}
		catch(InterruptedException e)
		{
			e.printStackTrace();
		}
		
		System.out.println("1~100까지의 합: "+(st1.getNum()+st2.getNum()));
	}
}

 

ex)

public class ThreadJoin {

	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName() + "start");
		Runnable r = new MyRunnable();
		Thread thread = new Thread(r);
		thread.start();

		System.out.println(Thread.currentThread().getName() + "end");
	}

}

/*보통 메인이 끝나면 모든게 종료되는걸로 알고있는데 쓰레드는 그렇지 않다.
   메인이 종료되어도 백그라운드에서 돌아가기 때문이다.
하지만 이런 쓰레드도 join(조인)을 이용하여 제어를 할 수 있다.*/

class MyRunnable implements Runnable{

	@Override
	public void run() {
		System.out.println("쓰레드1단계");
		thread2();
	}

	public void thread2() {
		System.out.println("쓰레드2단계");
		thread3();
	}

	public void thread3() {
		System.out.println("쓰레드3단계");
	}
}

/* 결과 : mainstart
             mainend
             쓰레드1단계
             쓰레드2단계
             쓰레드3단계 */

public class ThreadJoin {

	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName() + "start");
		Runnable r = new MyRunnable();
		Thread thread = new Thread(r);
		thread.start();
		try {
			thread.join();
		} catch (Exception e) {
		}
		System.out.println(Thread.currentThread().getName() + "end");
	}

}

class MyRunnable implements Runnable{

	@Override
	public void run() {
		System.out.println("쓰레드1단계");
		thread2();
	}

	public void thread2() {
		System.out.println("쓰레드2단계");
		thread3();
	}

	public void thread3() {
		System.out.println("쓰레드3단계");
	}
}
//이와같이 join을 사용한다면 해당 쓰레드가 종료되기까지 기다렸다가 다음으로 넘어간다.

 

Q. main 메소드에서는 프로그램 사용자로부터 총 다섯 개의 정수를 입력 받아서 

   별도로 생성된 하나의 쓰레드에게 전달하고, 별도로 생성된 쓰레드는 전달받은 수의 총 합을 계산해서,

   그 결과를 출력하는 프로그램을 작성해 보자.

   이는 main 메소드를 실행하는 main 쓰레드와

   main 쓰레드로부터 전달받은 수의 총 합을 계산하는 별도의 쓰레드간 동기화에 관련된 문제이다.

 

A.

import java.util.Scanner;

class IntegerComm
{
	int num=0;
	boolean isNewNum = false;

	public void setNum(int n)
	{
		synchronized(this)
		{
			if(isNewNum == true)
			{
				try
				{
					wait();
				}
				catch(InterruptedException e)
				{
					e.printStackTrace();
				}
			}
			num=n;
			isNewNum = true;
			notify();
		}
	}
	public int getNum()
	{
		int retNum=0;
		synchronized(this)
		{
			if(isNewNum==false)
			{
				try
				{
					wait();
				}
				catch(InterruptedException e)
				{
					e.printStackTrace();
				}
			}
			retNum = num;
			isNewNum = false;
			notify();
		}
		return retNum;
	}
}

class IntegerSummer extends Thread
{
	IntegerComm comm;
	int sum;
	public IntegerSummer(IntegerComm comm)
	{
		this.comm = comm;
	}
	public void run()
	{
		for(int i=0;i<5;i++)
			sum += comm.getNum();
		System.out.println("입력된 정수의 총 합 : " + sum);
	}
}

class SummerThreadTest
{
	public static void main(String[] args)
	{
		IntegerComm comm = new IntegerComm();
		IntegerSummer summer = new IntegerSummer(comm);
		summer.start();

		Scanner keyboard = new Scanner(System.in);

		System.out.println("총 5개의 정수 입력...");
		for(int i=0;i<5;i++)
			comm.setNum(keyboard.nextInt());

		try
		{
			summer.join();
		}
		catch(InterruptedException e)
		{
			e.printStackTrace();
		}
	}
}
728x90