[필기정리]Day28 - 쓰레드 문제(중심내용)

SW/Java

2020. 7. 17. 11:16

Q1. 프로그램의 실행요청은 컴퓨터 사용자에 의해 이뤄지지만,

     실질적인 프로그램의 실행은 ( )에 의해서 이뤄진다.

A. 프로그램의 실행요청은 컴퓨터 사용자에 의해 이뤄지지만,

     실질적인 프로그램의 실행은 ( 운영체제 )에 의해서 이뤄진다.

 

Q2. 프로그램의 실행이 요청 되면, 다음의 형태로 메모리 공간이 할당된다. 
    <프로세스에 할당된 메모리> 
    이렇듯 할당된 메모리 공간을 기반으로 실행 중에 있는 프로그램을 가리켜  ( )라 한다. 
    따라서 ( )를 간단히 '실행 중인 프로그램'으로 설명하기도 한다.

 

A. 프로그램의 실행이 요청되면, 다음의 형태로 메모리 공간이 할당된다.

 

이렇듯 할당된 메모리 공간을 기반으로 실행 중에 있는 프로그램을 가리켜 ( 프로세스(Process) )라 한다.

따라서 ( 프로세스 )를 간단히 '실행중인 프로그램'으로 설명하기도 한다.

 

# 메모리공간 구성

  자바 가상 머신은 운영체제 위에서 동작하므로,

  다른 응용 프로그램과 같이 운영체제에 의해 메모리 공간(RAM)을 할당받아 동작한다.

  자바는 이 할당받은 메모리 공간을 효율적으로 사용하기 위해 3가지로 구분한다.

 

 메소드 영역(메소드의 바이트 코드, static 변수)

   클래스의 바이트코드(JVM에 의해 실행 가능한 코드)가 로딩되는 메모리 공간, 인스턴스를 만들거나

   클래스 멤버의 접근을 위해 클래스의 주소가 저장되는 공간이다.

 

 스택 영역(지역변수, 매개변수)

   중괄호 내에 할당된 이후에 해당 중괄호를 벗어나면 바로 소멸되는 특성의 데이터 저장을 위한 영역.

   지역변수와 매개변수는 선언되는 순간에 스택에 할당되었다가 자신이 할당된 영역을 벗어나면 소멸된다.

 

 힙 영역(인스턴스)

    인스턴스의 소멸시점 및 소멸 방법이 지역변수와 다르기 때문에 별도의 공간에 할당되는데,

    인스턴스의 참조변수는 메소드 내에 있기 때문에 지역변수이지만 참조변수가 가리키는 인스턴스는

    힙영역에 저장된다.

 

Q3. 하나의 프로세스에 둘 이상의 쓰레드를 실행시키자.

      그리고 각각의 쓰레드 이름에 "멋진 쓰레드"

      "예쁜 쓰레드"라는 이름을 주고 반복을 100번을 시키자.

class ShowThread extends Thread
{
	String threadName;

	public ShowThread(String name)
	{
		threadName=name;
	}

	public void run()
	{
		for(int i=0; i<100; i++)
		{
			System.out.println("안녕하세요. "+threadName+"입니다.");
			try
			{
				sleep(100);
			}
			catch(InterruptedException e)
			{
				e.printStackTrace();
			}
		}
	}
}

class ThreadUnderstand
{
	public static void main(String[] args)
	{
		ShowThread st1=new ShowThread("멋진 쓰레드");
		ShowThread st2=new ShowThread("예쁜 쓰레드");
		st1.start();
		st2.start();
	}
}

A. 

class ShowThread extends Thread
{
	public ShowThread(String name)
	{
		super(name); // Thread 클래스의 getName 메소드를 통해서 언제든지 문자열의 형태로 참조할 수 있다
	}

	public void run()
	{
		for(int i=0; i<100; i++)
		{
			System.out.println("안녕하세요. "+getName()+"입니다."); // 쓰레드 내 getName 존재
			try
			{
				sleep(100); 
			}
			catch(InterruptedException e)
			{
				e.printStackTrace();
			}
		}
	}
}

class ThreadUnderstand
{
	public static void main(String[] args)
	{
		ShowThread st1=new ShowThread("멋진 쓰레드");
		ShowThread st2=new ShowThread("예쁜 쓰레드");
		st1.start();
		st2.start();
	}
}

-  sleep은 Thread 클래스의 static 메소드로서, 실행흐름을 일시적으로 멈추는 역할을 한다.
   이 문장에서는 인자로 100을 전달했으니, 이 메소드가 호출되면, 이 부분에서 1/1000*100초간 흐름을 멈추게 된다.

 

Q4.

- 쓰레드의 우선순위를 가져오는 메소드?

A. getPriority()

- 쓰레드의 우선순위를 설정하는 메소드?

A. setPriority()

- 쓰레드의 우선순위를 설정할 때 쓰는 상수는?

A.        

Thread.MAX_PRIORITY   10
Thread.NORM_PRIORITY  5
Thread.MIN_PRIORITY   1

 

- 쓰레드의 라이프 사이클(Life Cycle)?

A.

 

# 쓰레드가 생성되면 위 그림이 보여주는 네 가지 상태 중 한 가지 상태에 있게 된다.

  각각의 상태에 놓이는 시점과 상태가 변경되는 시점을 정리하면 다음과 같다.

 

① New 상태

쓰레드 클래스가 키워드 new를 통해서 인스턴스화 된 상태를 가리켜 'New 상태'라 한다.

이 상태에서는 자바 가상머신에 의해 관리가 되는 쓰레드의 상태는 아니다.

즉, 운영체제 입장에서는 쓰레드라 부르기에는 이른 감이 있는 상태다.

그러나 자바에서는 이 상태에서부터 쓰레드라 표현한다.

 

② Runnable 상태

쓰레드 인스턴스를 대상으로 start 메소드가 호출되면, 해당 쓰레드는 비로소 'Runnable 상태'가 된다.

이는 모든 실행의 준비를 마치고, 스케줄러에 의해서 선택되어 실행될 수 있기만을 기다리는 상태이다.

이로써 우리는 start 메소드가 호출된다고 해서 바로 run 메소드가 호출되는 것이 아님을 알 수 있다.

Runnable 상태에 있다가,

스케줄러에 의해서 실행의 대상으로 선택이 되어야 비로소 run 메소드가 처음 호출이 된다.

 

③ Blocked 상태

실행 중인 쓰레드가 sleep 또는 join 메소드를 호출하거나

CPU의 할당이 필요치 않는 입출력 연산을 하게 되면 

CPU를 다른 쓰레드에게 양보하고, 본인은 'Blocked 상태'가 된다.

Blocked 상태에서는 스케줄러의 선택을 받을 수 없다.

 

다시 스케줄러의 선택을 받아서 실행이 되려면,

Blocked 상태에 놓이게 된 원인이 제거되고 Runnable 상태로 돌아와야 한다.

입출력 작업으로 인해서 Blocked 상태가 되었다면, 입출력 작업이 완료되면서 Runnable 상태가 된다.

 

그리고 sleep 메소드의 호출로 인해서 Blocked 상태가 되었다면,

sleep 메소드가 반환이 되면서 다시 Runnable 상태가 된다.

 

④ Dead 상태

run 메소드의 실행이 완료되어서 run 메소드를 빠져 나오게 되면, 해당 쓰레드는 'Dead 상태'가 된다.

그리고 이 상태는 쓰레드의 실행을 위해서 할당 받았던 메모리를 비롯해서

각종 쓰레드 관련 정보가 완전히 사라지는 상태이다.

참고로 한번 Dead 상태가 된 쓰레드는 다시 Runnable 상태가 되지 못한다.

쓰레드의 실행을 위해 필요한 모든 것이 소멸되기 때문이다.

 

 

- 쓰레드의 메모리 구성

쓰레드가 생성되면 가상머신은 쓰레드의 실행을 위한 별도의 메모리 공간을 할당한다.

그렇다면 이러한 별도의 메모리 공간은 정확히 무엇을 의미하는 것일까?

쓰레드의 가장 큰 역할은 별도의 실행흐름 형성이다.

그리고 별도의 실행흐름은 메소드의 호출을 통해서 형성된다.

 

즉, 처음에는 run 메소드가 호출되고,

run 메소드 내에서는 또 다른 메소드를 호출하면서 main 메소드와는 다른 흐름을 형성한다.

이렇듯 main 메소드와는 전혀 다른 실행흐름을 형성하기 위해서는 별도의 스택이 쓰레드에게 할당되어야 한다.

따라서 main 쓰레드 이외에 두 개의 쓰레드가 추가로 생성되면, 가상머신은 다음의 형태로 메모리를 구성한다.

 

위 그림에서 보이듯이 모든 쓰레드는 자신의 스택을 할당 받는다.

그러나 힙과 메소드 영역은 모든 쓰레드가 공유한다.

여기서 특히 힙이 공유됨에 주목하자.

힙 영역이 공유된다는 것은 모든 쓰레드가 동일한 힙 영역에 접근이 가능함을 의미하는 것이고,

이는 다음과 같은 일이 가능함을 의미하는 것이다.

 

"A 쓰레드가 만든 인스턴스의 참조 값(사실상 주소 값)만 알면

B 쓰레드도 A 쓰레드가 만든 인스턴스에 접근 가능하다."

그래서 쓰레드 사이에 데이터를 주고받아야 할 때에는(쓰레드간 통신이 필요할 때에는) 힙 영역을 활용한다.

 

[참고]

Q. 쓰레드 인스턴스를 생성하고 나서 start 메소드를 호출하면 run 메소드가 실행되는데,

    run 메소드를 직접 호출하면 안되나?

A. run 메소드를 직접 호출하는 것도 불가능한 일은 아니다.

   단, 이러한 경우에는 단순한 메소드의 호출일뿐, 쓰레드의 생성으로 이어지지는 않는다.

   쓰레드는 자신만의 메모리 공간을 할당 받아서 별도의 실행 흐름을 형성한다.

   따라서 자바 가상머신은 start 메소드의 호출을 요구하는 것이다.

   메모리 공간의 할당 등 쓰레드의 실행을 위한 기반을 마련한 다음에

   run 메소드를 대신 호출해 주기 위해서 말이다.

   이는 우리가 main 메소드를 직접 호출하지 않는 것과 비슷한 이치이다.

 

Q. CPU가 하나인데, 어떻게 둘 이상의 쓰레드가 동시에 실행 가능한가?

A. 이 질문에 대한 답변은 생각보다 간단하다.

   모든 쓰레드는 CPU를 공유한다.

   물론 CPU를 공유하는 방식에는 원칙이 존재한다.

   참고로 코어(CPU 내에 존재하는 연산장치)가 여러 개 존재하는 CPU에서는

   쓰레드 각각에 코어가 하나씩 할당되어 실행되기도 한다.

 

Q. main 메소드가 종료되어도 쓰레드는 실행을 계속하나?

    그리고 쓰레드는 run 메소드의 실행이 완료되면 종료되나?

A. 쓰레드의 main 메소드가 run 메소드이다.

   따라서 run 메소드의 실행이 완료되면, 해당 스레드는 종료가 되고 소멸된다.

   

   그리고 앞서 보인 예제에서는 main 메소드 내에서 쓰레드를 생성했었다.

   그런데 쓰레드를 생성하고 start 메소드를 호출한다고 해서 main 메소드가 멈추는 것은 아니다.

 

  main 메소드도 여느 쓰레드와 마찬가지로 자신만의 실행흐름을 이어간다.

  따라서 main 메소드가 먼저 종료될 수도 있다.

  하지만 main 메소드가 종료되어도 실행 중에 있는 쓰레드가 있다면, 프로그램은 종료되지 않는다.

  사실 main 메소드도 쓰레드에 의해 실행된다.

  그리고 main 메소드를 실행하는 쓰레드를 가리켜 별도로 'main 쓰레드'라 부르기도 한다.

  결국 마지막 남은 쓰레드까지 실행을 완료해야 프로그램은 종료된다.

 

Q. 쓰레드가 별도의 실행흐름을 구성하는 것은 알겠는데, 그렇다면 정확히 무엇을 가리켜 쓰레드라 하나?

   인스턴스가 쓰레드인가?

A. Thread를 상속하는 클래스의 인스턴스를 가리켜 쓰레드라고도 하지만, 이는 엄밀히 말해서 잘못된 표현이다.

   쓰레드는 자바 가상머신이 생성 하는 것이기 때문이다.

   start 메소드가 호출되면, 자바 가상머신은 별도의 실행흐름을 형성하기 위한 여러 가지 준비에 들어간다.

 

  그 중 대표적인 것은 메모리 공간의 할당이다.

  실행흐름을 구성하기 위해서는 메모리 공간의 할당이 필수 아니겠는가?

  그리고 이미 생성된 쓰레드들과 CPU를 나눠 쓰기 위한 각종 정보들이 등록된다.

  이렇듯 별도의 실행흐름을 형성하기 위해서

  자바 가상머신에 의해 만들어지는(또는 준비되는) 모든 리소스와 각종 정보들을 총칭해서 쓰레드라 한다.

 

Q5. class Sum이 있고 여기에는 숫자를 저장할 수 있는 Instance 변수와

     숫자를 더하는 메소드 숫자를 반환하는 메소드가 있다.

 

     class AddThread 라는 클래스를 쓰레드를 돌리기 위해서 인터페이스를 구현해서

     두 개의 숫자 인스턴스 변수와 이 두 개의 숫자 인스턴스 변수를

     시작 값부터 끝 값까지 더하도록 메소드를 만들자.

 

     그리고 메인메소드에서 쓰레드를 두 개 생성해서 하나는 1부터 50까지 더하고

     하나는 51부터 100까지 더해서 두 개의 쓰레드 실행결과 그 더한 값을 출력하도록 하자.

 

- Runnable 인터페이스는 run 메소드 하나로 이뤄져 있다.

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

/*
Sum 클래스를 상속하면서 Runnable 인터페이스를 구현하고 있다. 
이 인터페이스는 run 메소드 하나로 이뤄져 있다.
 */

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();

		// start 메소드의 호출을 통해서 최종으로 쓰레드를 생성 및 실행시키고 있다.               

		try
		{
			tr1.join();
			tr2.join();

			/* 쓰레드 인스턴스를 대상으로 join 메소드를 호출하고 있다. 
				이는 해당 쓰레드가 종료될 때까지 실행을 멈출 때 호출하는 메소드이다. */
		}
		catch(InterruptedException e)
		{
			e.printStackTrace();
		}
		System.out.println("1~100까지의 합: "+(at1.getNum()+at2.getNum()));
	}
}

 

+ 해당 쓰레드가 종료될 때까지 실행을 멈출 때 호출하는 메소드는?

A. join() 

 

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

     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 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()));
	}
}

 

# 동기화(Synchronization)

Q7. Increment라는 num이라는 인스턴스 변수를 가진 클래스가 있고

     그 클래스는 1씩 증가시키는 메소드와 숫자 값을 가져오는 메소드가 있다.

     IncThread라는 클래스는 Increment라는 클래스의 참조변수를 인스턴스변수로 가지고 있고

     쓰레드를 실행시키면 중첩된 반복문으로 각각 10000  Increment 1씩 증가시키는 메소드를 호출한다.

     그리고 main메소드가 있는 클래스에서 Increment 인스턴스 한 개 IncThread 3개를 만들고

     3개의 쓰레드를 실행한 후에 Increment num의 값을 출력해본다.

class Increment
{
	int num=0;
	public void increment(){ num++; }
	public int getNum() { return num; }
}

class IncThread extends Thread
{         
	Increment inc;

	public IncThread(Increment inc)
	{
		this.inc=inc;
	}

	public void run()
	{
		for(int i=0; i<10000; i++)
			for(int j=0; j<10000; j++)
				inc.increment();
	}
}

class ThreadSyncError
{
	public static void main(String[] args)
	{
		Increment inc=new Increment();
		IncThread it1=new IncThread(inc);
		IncThread it2=new IncThread(inc);
		IncThread it3=new IncThread(inc);

		it1.start();
		it2.start();
		it3.start();

		try
		{
			it1.join();
			it2.join();
			it3.join();
		}

		catch(InterruptedException e)
		{
			e.printStackTrace();
		}
		System.out.println(inc.getNum());
	}
}

 

Q8. Q7번 문제는 동기화가 제대로 되지 않아 잘못된 값을 출력할 확률이 높다.

     이것을 동기화 시키는 두 가지 방법으로 동기화가 제대로 이루어 지도록 하자.

 

① '동기화 메소드' 선언

public synchronized void increment()
{
           num++;
}

 

② '동기화 블록' 지정

public void increment()
{
           synchronized(this)
           {
                      num++;
           }
}

쓰레드의 동기화로 인하여 성능이 매우 많이 저하된다.

따라서 동기화가 필요한 것은 사실이지만,

필요한 위치에 제한적으로 사용해서 성능에 영향을 주지 않도록 주의해야 한다.

 

ex) 

public synchronized int add(...)
{
           ...
}

public synchronized int min(...)
{
           ...
}

public void shoOpCnt()
{
           ...
}

 

- 자바의 모든 인스턴스에는 하나의 열쇠가 존재한다(비록 눈에 보이지는 않지만).

  전문용어로 이 열쇠를 가리켜 lock 또는 monitor라 하는데, 이를 그냥 '열쇠'로 이해해도 된다

  (이는 운영체제라는 과목의 전통적인 이해방식이다.)

 

  그리고 synchronized 선언된 메소드에는 자물쇠가 걸린다.

  따라서 synchronized로 선언된 메소드를 호출하려면 먼저 열쇠를 획득해야 한다.

  그리고는 열쇠로 자물쇠를 열고 들어가야 한다.

 

  그런데 위에 코드에서 열쇠는 하나다.

  따라서 인스턴스 내에서 synchronized로 선언된 모든 메소드는 동시에 둘 이상이 실행될 수 없다.

 

  물론 여러분은 열쇠의 획득과 반납을 코드상에 명시할 필요는 없다.

  synchronized로 선언된 메소드를 호출하면 열쇠는 자동으로 획득이 되고,

  메소드를 빠져나오면 획득한 열쇠는 자동으로 반납이 되기 때문이다.

  (참고로 이것이 synchrozied 선언의 매력이다. 실수로 열쇠를 반납하지 않는 문제가 발생하지 않기 때문이다.)

  물론 자물쇠가 걸려있지 않은(synchronized로 선언되지 않은) 메소드의 호출은 제한을 받지 않는다.

 

  동기화의 대상이 인스턴스이긴 하지만

  동기화의 대상이 인스턴스라고 표현을 하다 보니 synchronized로 선언된 메소드가 호출이 되면 

  호출된 인스턴스에는 다른 쓰레드의 접근이 아예 불가능한 것으로 오해하는 경우가 많다.

  하지만 synchronized로 선언되지 않은 메소드에는 얼마든지 접근이 가능하다.

 

Q9. 아래의 SyncObjectKy를 적절하게 Key를 사용하여 동기화해보자. (두 가지 방법)

class IHaveTwoNum
{
	int num1=0;
	int num2=0;
	
	public void addOneNum1() { num1+=1; }
	public void addTwoNum1() { num1+=2; }
	
	public void addOneNum2() { num2+=1; }
	public void addTwoNum2() { num2+=2; }
	
	public void showAllNums()
	{
		System.out.println("num1: "+num1);
		System.out.println("num2: "+num2);
	}
}

class AccessThread extends Thread
{
	IHaveTwoNum twoNumInst;
	
	public AccessThread(IHaveTwoNum inst)
	{
		twoNumInst=inst;
	}
	
	public void run()
	{
		twoNumInst.addOneNum1();
		twoNumInst.addTwoNum1();
		
		twoNumInst.addOneNum2();
		twoNumInst.addTwoNum2();
	}
}

class SyncObjectKey
{
	public static void main(String[] args)
	{
		IHaveTwoNum numInst=new IHaveTwoNum();
		
		AccessThread at1=new AccessThread(numInst);
		AccessThread at2=new AccessThread(numInst);
		
		at1.start();
		at2.start();
		
		try
		{
			at1.join();
			at2.join();
		}
		catch(InterruptedException e)
		{
			e.printStackTrace();
		}
		numInst.showAllNums();
	}
}

 

A. ①

class IHaveTwoNum
{
	int num1=0;
	int num2=0;

	public void addOneNum1()
	{
		synchronized(key1)
		{
			num1+=1;
		}
	}

	public void addTwoNum1()
	{
		synchronized(key1)
		{
			num1+=2;
		}
	}         

	public void addOneNum2()
	{
		synchronized(key2)
		{
			num2+=1;
		}
	}

	public void addTwoNum2()
	{
		synchronized(key2)
		{
			num2+=2;
		}
	}

	public void showAllNums()
	{
		System.out.println("num1: "+num1);
		System.out.println("num2: "+num2);
	}

	Object key1=new Object();
	Object key2=new Object();
}

class AccessThread extends Thread
{
	IHaveTwoNum twoNumInst;

	public AccessThread(IHaveTwoNum inst)
	{
		twoNumInst=inst;
	}

	public void run()
	{
		twoNumInst.addOneNum1();
		twoNumInst.addTwoNum1();

		twoNumInst.addOneNum2();
		twoNumInst.addTwoNum2();
	}
}

class SyncObjectKeyAnswer
{
	public static void main(String[] args)
	{
		IHaveTwoNum numInst=new IHaveTwoNum();

		AccessThread at1=new AccessThread(numInst);
		AccessThread at2=new AccessThread(numInst);

		at1.start();
		at2.start();

		try
		{
			at1.join();
			at2.join();
		}

		catch(InterruptedException e)
		{
			e.printStackTrace();
		}
		numInst.showAllNums();
	}
}

 

A. 

class IHaveTwoNum
{
	int num1=0;
	int num2=0;

	public void addOneNum1()
	{
		synchronized(this)
		{
			num1+=1;
		}
	}

	public void addTwoNum1()
	{
		synchronized(this)
		{
			num1+=2;
		}
	}         

	public void addOneNum2()
	{
		synchronized(key1)
		{
			num2+=1;
		}
	}

	public void addTwoNum2()
	{
		synchronized(key1)
		{
			num2+=2;
		}
	}

	public void showAllNums()
	{
		System.out.println("num1: "+num1);
		System.out.println("num2: "+num2);
	}

	Object key1=new Object();
	Object key2=new Object();
}

class AccessThread extends Thread
{
	IHaveTwoNum twoNumInst;

	public AccessThread(IHaveTwoNum inst)
	{
		twoNumInst=inst;
	}

	public void run()
	{
		twoNumInst.addOneNum1();
		twoNumInst.addTwoNum1();

		twoNumInst.addOneNum2();
		twoNumInst.addTwoNum2();
	}
}

class SyncObjectKeyAnswer
{
	public static void main(String[] args)
	{
		IHaveTwoNum numInst=new IHaveTwoNum();

		AccessThread at1=new AccessThread(numInst);
		AccessThread at2=new AccessThread(numInst);

		at1.start();
		at2.start();

		try
		{
			at1.join();
			at2.join();
		}
		
		catch(InterruptedException e)
		{
			e.printStackTrace();
		}
		numInst.showAllNums();
	}
}

 

- 동기화는 쓰레드의 접근 순서(방식)를 컨트롤한다는 의미이다.

  쓰레드의 실행순서를 조절하는(결정하는) 것도 동기화의 범주에 포함된다.

 

이전에 StringBuffer 클래스가 쓰레드에 안전하다고(Thread-safe하다고) 설명한 적이 있다.

이는 StringBuffer 클래스에 이미 동기화 처리가 되어있어,

둘 이상의 쓰레드가 동시에 접근을 해도 문제가 발생하지 않는다는 뜻이다.

때문에 이러한 클래스를 사용할 때에는 동기화를 적용할 필요가 없다.

 

혹시 앞서 공부한 ArrayList<E> 클래스와 HashSet<E> 클래스에는 동기화 처리가 되어있는지 궁금하지 않은가?

그렇다면 API 문서를 통해서 확인하자.

다음과 같이 매우 진한 글씨체로 동기화 처리가 되어있지 않음을 명시하고 있으니 말이다.

Note that this implementation is not synchronized

 

동기화 처리 유무는 매우 중요한 정보에 해당한다.

따라서 문서를 꼼꼼히 살피지 않아도 해당 클래스의 동기화 처리 유무는 쉽게 확인할 수 있다.

 

Q10. 문자열이 있는 신문 클래스

       신문작가(쓰레드)는 신문을 쓰고 독자(쓰레드)는 신문을 읽는다.

 

      독자 실행.

      작가 실행.

 

A.

class NewsPaper
{
	String todayNews;

	public void setTodayNews(String news)
	{
		todayNews=news;
	}

	public String getTodayNews()
	{
		return todayNews;
	}
}

class NewsWriter extends Thread
{
	NewsPaper paper;

	public NewsWriter(NewsPaper paper)
	{
		this.paper=paper;
	}

	public void run()
	{
		paper.setTodayNews("자바의 열기가 뜨겁습니다.");
	}
}

class NewsReader extends Thread
{
	NewsPaper paper;

	public NewsReader(NewsPaper paper)
	{
		this.paper=paper;
	}

	public void run()
	{
		System.out.println("오늘의 뉴스: "+paper.getTodayNews());
	}
}

class NewsPaperStory
{
	public static void main(String[] args)
	{
		NewsPaper paper=new NewsPaper();
		NewsReader reader=new NewsReader(paper);
		NewsWriter writer=new NewsWriter(paper);

		reader.start();
		writer.start();

		try
		{
			reader.join();
			writer.join();
		}

		catch(InterruptedException e)
		{
			e.printStackTrace();
		}
	}
}

 

- 동기화는 쓰레드의 접근 순서(방식)을 컨트롤한다는 의미이다.

  앞서 보인 동기화는 순서에 상관 없이, 쓰레드의 동시 접근만을 막는 동기화였다.

  그러나 쓰레드의 실행순서를 조절하는(결정하는) 것도 동기화의 범주에 포함된다.

 

ex) A쓰레드가 먼저 생성되었지만 늦게 생성된 B쓰레드가 먼저 실행되는 상황의 예

     A 쓰레드와 B 쓰레드는 우선순위가 동일하다.

    이러한 상황에서 A 쓰레드가 먼저 생성되고, 이어서 B 쓰레드가 생성되었다.

    그리고는 먼저 생성된 A 쓰레드의 run 메소드가 호출되었는데,

    그 순간 우선순위가 높은 C 쓰레드가 등장하여, A 쓰레드는 C 쓰레드에게 실행의 기회를 넘기고 말았다.

    결국 A 쓰레드의 run 메소드는 하나도 실행되지 않은 상태가 되었다.

 

    이어서 C 쓰레드는 종료되고, 이번에는 B 쓰레드의 run 메소드가 호출 및 실행되었다.

    결과적으로 먼저 생성된 A 쓰레드보다 나중에 생성된 B 쓰레드가 먼저 실행되었다.

    그리고 이는 하나의 예일뿐, 보다 다양한 상황에서 이와 유사한 일은 얼마든지 쉽게 일어날 수 있다.

 

Q11. 쓰레드를 기다리게 하는 메소드의 원형 (Object 클래스의 메소드이다.)

public final void wait() throws InterruptedException

 

Q12. 하나의 쓰레드만 깨우는 메소드의 원형 (Object 클래스의 메소드이다.)

public final void notify()

 

Q13. 모든 쓰레드를 깨우는 메소드의 원형 (Object 클래스의 메소드이다.)

public final void notifyAll()

 

Q14. Q10번문제를 동기화가 가능한 상태로 수정하자.(독자 두 명 생성. 실행)

 A. 

class NewsPaper
{
	String todayNews;
	boolean isTodayNews=false;
	
	public void setTodayNews(String news)
	{
		todayNews=news;
		isTodayNews=true;
		
		synchronized(this)
		{
			notifyAll();		// 모두 일어나세요
		}
	}
	
	public String getTodayNews()
	{
		if(isTodayNews==false)
		{
			try
			{
				synchronized(this)
				{
					wait();	// 한숨 자면서 기다리겠습니다.
				}
			}
			catch(InterruptedException e)
			{
				e.printStackTrace();
			}
		}
		return todayNews;
	}
}

class NewsWriter extends Thread
{
	NewsPaper paper;
	
	public NewsWriter(NewsPaper paper)
	{
		this.paper=paper;
	}
	public void run()
	{
		paper.setTodayNews("자바의 열기가 뜨겁습니다.");
	}
}

class NewsReader extends Thread
{
	NewsPaper paper;
	
	public NewsReader(NewsPaper paper)
	{
		this.paper=paper;
	}
	public void run()
	{
		System.out.println("오늘의 뉴스: "+paper.getTodayNews());
	}
}

class SyncNewsPaper
{
	public static void main(String[] args)
	{
		NewsPaper paper=new NewsPaper();
		NewsReader reader1=new NewsReader(paper);
		NewsReader reader2=new NewsReader(paper);
		NewsWriter writer=new NewsWriter(paper);

		try
		{
			reader1.start();
			reader2.start();
			
			Thread.sleep(1000);			
			writer.start();
	
			reader1.join();
			reader2.join();
			writer.join();
		}
		catch(InterruptedException e)
		{
			e.printStackTrace();
		}
	}
}

 

- wait notifyAll(notify) 메소드는 동기화 처리를 해서,

  한 순간에 하나의 쓰레드만 호출이 가능하도록 해야 한다.

- notify 메소드는 잠이 든 여러 쓰레드들 중 하나만 깨울 때 사용되는 메소드이고,

  notifyAll 메소드는 잠이 든 모든 쓰레드들을 함께 깨울 때 사용되는 메소드이다.

 

Q15. [쓰레드의 동기화]

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();
		}
	}
}

 

Q16. 아래에 있는 소스를 ReentrantLock을 이용하여 수정하시오.

import java.util.concurrent.locks.ReentrantLock;

class IHaveTwoNum
{
	int num1=0;
	int num2=0;
	
	public void addOneNum1() 
	{
		key1.lock();
		try
		{
			num1+=1;
		}
		finally
		{
			key1.unlock();
		}
	}
	public void addTwoNum1() 
	{ 
		key1.lock();
		try
		{
			num1+=2;
		}
		finally
		{
			key1.unlock();
		}
	}	
	public void addOneNum2() 
	{ 
		key2.lock();
		try
		{
			num2+=1;
		}
		finally
		{
			key2.unlock();
		}
	}
	public void addTwoNum2() 
	{ 
		key2.lock();
		try
		{
			num2+=2;
		}
		finally
		{
			key2.unlock();
		}
	}
	
	public void showAllNums()
	{
		System.out.println("num1: "+num1);
		System.out.println("num2: "+num2);
	}
	
	private final ReentrantLock key1=new ReentrantLock();
	private final ReentrantLock key2=new ReentrantLock();
}

class AccessThread extends Thread
{
	IHaveTwoNum twoNumInst;
	
	public AccessThread(IHaveTwoNum inst)
	{
		twoNumInst=inst;
	}
	
	public void run()
	{
		twoNumInst.addOneNum1();
		twoNumInst.addTwoNum1();
		
		twoNumInst.addOneNum2();
		twoNumInst.addTwoNum2();
	}
}

class UseReentrantLock
{
	public static void main(String[] args)
	{
		IHaveTwoNum numInst=new IHaveTwoNum();
		
		AccessThread at1=new AccessThread(numInst);
		AccessThread at2=new AccessThread(numInst);
		
		at1.start();
		at2.start();
		
		try
		{
			at1.join();
			at2.join();
		}
		catch(InterruptedException e)
		{
			e.printStackTrace();
		}
		numInst.showAllNums();
	}
}

A.

import java.util.concurrent.locks.ReentrantLock;

class IHaveTwoNum
{
	int num1=0;
	int num2=0;
	
	public void addOneNum1() 
	{
		key1.lock();
		try
		{
			num1+=1;
		}
		finally
		{
			key1.unlock();
		}
	}
	public void addTwoNum1() 
	{ 
		key1.lock();
		try
		{
			num1+=2;
		}
		finally
		{
			key1.unlock();
		}
	}	
	public void addOneNum2() 
	{ 
		key2.lock();
		try
		{
			num2+=1;
		}
		finally
		{
			key2.unlock();
		}
	}
	public void addTwoNum2() 
	{ 
		key2.lock();
		try
		{
			num2+=2;
		}
		finally
		{
			key2.unlock();
		}
	}
	
	public void showAllNums()
	{
		System.out.println("num1: "+num1);
		System.out.println("num2: "+num2);
	}
	
	private final ReentrantLock key1=new ReentrantLock();
	private final ReentrantLock key2=new ReentrantLock();
}

class AccessThread extends Thread
{
	IHaveTwoNum twoNumInst;
	
	public AccessThread(IHaveTwoNum inst)
	{
		twoNumInst=inst;
	}
	
	public void run()
	{
		twoNumInst.addOneNum1();
		twoNumInst.addTwoNum1();
		
		twoNumInst.addOneNum2();
		twoNumInst.addTwoNum2();
	}
}

class UseReentrantLock
{
	public static void main(String[] args)
	{
		IHaveTwoNum numInst=new IHaveTwoNum();
		
		AccessThread at1=new AccessThread(numInst);
		AccessThread at2=new AccessThread(numInst);
		
		at1.start();
		at2.start();
		
		try
		{
			at1.join();
			at2.join();
		}
		catch(InterruptedException e)
		{
			e.printStackTrace();
		}
		numInst.showAllNums();
	}
}

 

- lock 메소드는 한번 호출되면, unlock 메소드가 호출될 때까지 재호출이 불가능하기 때문에 

  lock 메소드가 호출되는 시점부터 unlock 메소드가 호출되는 시점까지

 둘 이상의 쓰레드에 의해서 동시에 실행되지 않는 영역이 된다.

 

-  ReentrantLock 인스턴스를 대상으로 newCondition이라는 이름의 메소드를 호출하면,

   Condition형 인스턴스가 반환된다.

   (정확히는 Condition 인터페이스를 구현하는 인스턴스의 참조 값이 반환된다).

   그리고 반환된 인스턴스를 대상으로 다음의 메소드를 호출할 수 있다.

 

- await             낮잠을 취한다. (wait 메소드에 대응)

- signal            낮잠 자는 쓰레드 하나를 깨운다. (notify 메소드에 대응)

- signalAll         낮잠 자는 모든 스레드를 깨운다. (notifyAll 메소드에 대응)

 

이 메소드들 역시 한번에 하나의 메소드만 호출될 수 있도록 동기화 처리가 되어야 한다.

단, 반드시 앞서보인 ReentrantLock 인스턴스 기반으로 동기화 처리가 되어야 한다.

(synchronized 기반이 아닌)

 

Q17. 예제를 통해서 Condition 인스턴스 기반의 실행순서 동기화의 사례를 보여라.

이 예제에서는 두 개의 쓰레드가 생성되는데,

하나는 프로그램 사용자로부터 문자열을 입력 받는 쓰레드이고,

다른 하나는 입력 받은 문자열을 출력하는 쓰레드이다.

A.

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
import java.util.Scanner;

class StringComm
{
	String newString;
	boolean isNewString=false;
	
	private final ReentrantLock entLock=new ReentrantLock();
	private final Condition readCond=entLock.newCondition();
	private final Condition writeCond=entLock.newCondition();
	
	public void setNewString(String news)
	{
		entLock.lock();
		try
		{
			if(isNewString==true)
				writeCond.await();
				
			newString=news;
			isNewString=true;
			readCond.signal();
		}
		catch(InterruptedException e)
		{
			e.printStackTrace();
		}
		finally
		{
			entLock.unlock();
		}
	}
	
	public String getNewString()
	{
		String retStr=null;
		
		entLock.lock();
		try
		{
			if(isNewString==false)
				readCond.await();
			
			retStr=newString;
			isNewString=false;		
			writeCond.signal();
		}
		catch(InterruptedException e)
		{
			e.printStackTrace();
		}		
		finally
		{
			entLock.unlock();
		}
			
		return retStr;
	}
}

class StringReader extends Thread
{
	StringComm comm;
	
	public StringReader(StringComm comm)
	{
		this.comm=comm;
	}
	public void run()
	{
		Scanner keyboard=new Scanner(System.in);
		String readStr;
		
		for(int i=0; i<5; i++)
		{
			readStr=keyboard.nextLine();
			comm.setNewString(readStr);
		}
	}
}

class StringWriter extends Thread
{
	StringComm comm;
	
	public StringWriter(StringComm comm)
	{
		this.comm=comm;
	}
	public void run()
	{
		for(int i=0; i<5; i++)
			System.out.println("read string: "+comm.getNewString());
	}
}

class ConditionSyncStringReadWrite
{
	public static void main(String[] args)
	{
		StringComm strComm=new StringComm();
		StringReader sr=new StringReader(strComm);
		StringWriter sw=new StringWriter(strComm);
	
		System.out.println("입출력 쓰레드의 실행...");
		sr.start();
		sw.start();
	}
}

 

 

 

 

 

728x90