멀티쓰레드에서는 여러 쓰레드에서 같은 프로세스 내의 자원을 공유해서 작업을 하기 때문에 자칫하면 각각의 쓰레드가 하나의 자원에 접근 제어를 하게 되어 서로의 작업에 영향을 주게 될 수 있음.
주의할 점, 쓰레드가 교착상태(dead lock)에 빠질 수 있음
교착상태(Dead lock)
하나의 이상의 쓰레드가 lock 을 건 상태에서 서로 lock 이 풀리기를 기다리는 상황
교착 상태에서는 작업이 진행되지 않고 쓰레드는 작업을 영원히 기다리는 상황이 됨.
그리하여 자바에서는 stop(), suspend(), resume() 과 같은 쓰레드의 상태를 변경하는 메서드들이 deprecated 되어짐.
만일 객체에 lock 을 건 상태에서 쓰레드가 종료되거나 정지 된다면, 이 객체를 사용하기 위해 대기중인 쓰레드들은 영원이 기다려야 되는 교착상태에 처하게 됨. 그리하여 쓰레드들 종료시킬 때에는 작업중인 객체의 lock 을 풀고, 작업 중에 변경했던 데이터를 작업이전의 상태로 돌려놓아야 함.
package thread1; public class MainClass { /** * @param args * @throws InterruptedException */ public static void main(String[] args) throws InterruptedException { ManageThread mt = new ManageThread(); mt.startThread(); Thread.sleep(100); mt.stopThread(); } }
package thread1;
public class ManageThread {
TestThread xxx = new TestThread("첫번째 Thread"); TestThread yyy = new TestThread("두번째 Thread"); Thread t1 = new Thread(xxx); Thread t2 = new Thread(yyy);
public void startThread() { t1.start(); t2.start(); }
public void stopThread() { xxx.stopRunning(); yyy.stopRunning(); }
}
|
package thread1; public class TestThread implements Runnable { private boolean timeToStop = false; String name; int cnt = 0; TestThread(String name) { this.name = name; } @Override public void run() { while (timeToStop != true) { System.out.println(name + ":" + cnt++); } } public void stopRunning() { timeToStop = true; } }
이 예제를 수행해 보면 xxx와 yyy Thread가 각각 경쟁하면서 수행되는 것을 알 수 있다. 한 순간에는 하나의 Thread만이 수행된다. 따라서 적당한 시간을 분배받아서 Thread가 수행되다가 다른 Thread에게 우선권을 넘긴다. 그러면 우선권을 받을 Thread가 수행되는 것이다.
이런 방식을 통하여 여러 thread가 한꺼번에 수행될 수 있다. Java에서는 preemptive(선점형) 방식으로 Thread를 수행시킨다. 이 방식은 Thread에게 일률적으로 일정한 시간을 배당하는 기존의 방식(cooperative)과는 다르다. preemptive방식은 우선순위(priority)를 각 Thread에게 배정하고 우선순위가 높은 Thread에 CPU를 할당한다. 따라서 어떤 Thread가 높은 순위의 우선순위를 배정받은 후 그 순위가 바뀌지 않는다면 그 Thread를 계속 수행하게된다. 따라서 각 Thread의 우선순위를 변경해줌으로써 골고루 Thread가 배정되도록 해주어야 하는데 이 일은 Thread Scheduler가 담당한다(우리가 알 바 아니다).
위에서 살펴본 Thread.sleep()는 자신의 Thread를 잠깐 멈추고 자기보다 낮은 순위의 다른 Thread에게 우선권을 넘긴다.
여기서 참고로 알아둘 method가 있다. Thread.currentThread()라는 method인데 이를 호출하면 현재 수행되고 있는 Thread의 reference를 얻을 수 있다. 또한 Thread.currentThread().getName()하면 그 Thread의 이름도 알 수 있다. 참고하기 바란다.
만약 은행권 처럼 계좌금액에 동시에 + 하거나 - 하기도 전에 다른 스레드가와서 그 금액을 보고 돈을 판단하는 문제가 생기는 경우는 그럼 어떻게 할 것인가?
package thread2; public class ThreadProblem { /** * @param args */ public static void main(String[] args) { ManageThread mt = new ManageThread(); mt.startThread(); } }
package thread2;
public class ManageThread {
SharedData sd = new SharedData(); TestThread xxx = new TestThread("첫번째 Thread", sd); TestThread yyy = new TestThread("두번째 Thread", sd); Thread t1 = new Thread(xxx); Thread t2 = new Thread(yyy);
public void startThread() { t1.start(); t2.start(); }
}
|
package thread2; public class TestThread implements Runnable { String name; SharedData sd; TestThread(String name, SharedData sd) { this.name = name; this.sd = sd; } public void run() { System.out.println(name + " push data : " + sd.push('a')); System.out.println(name + " push data : " + sd.push('b')); System.out.println(name + " push data : " + sd.push('c')); System.out.println(name + " pop data : " + sd.pop()); System.out.println(name + " pop data : " + sd.pop()); System.out.println(name + " pop data : " + sd.pop()); } }
package thread2; public class SharedData { int stackPointer = 0; char[] stack = new char[100]; public synchronized char push(char data) { // synchronized method stack[stackPointer] = data; doForALongJob(); stackPointer = stackPointer + 1; return data; } public char pop() { synchronized (this) { // synchronized block stackPointer = stackPointer - 1; doForALongJob(); return stack[stackPointer]; } } public void doForALongJob() { for (long i = 0; i < 5000000; i++) { } } }
sychronized 의 2가지 방식을 보여주었다. 메소드에 선언하는거랑 메소드내부에서 묶는거랑...
위처럼 synchronized 키워드를 이용하면 간단히 동기화 할 수 있다...( 유닉스 스레드락보다 훨씬 간단하다 :) )
참고로 메소드에 거는것보다 블럭으로 만드는게 성능상으로 더 좋다. 메소드에 걸면.. 더 많은 과정을 거치게 된다.
wait() and notify()
synchronized만 있으면 Thread의 모든 문제가 해결될까? 그랬으면 얼마나 좋을까? 하여간 해결해야 할 문제가 하나 더 있다.
위에서 살펴보았던 stack의 예제를 좀 더 생각해 보자. 만약 stack이 비어있는 상태에서 pop()가 수행되었다면 어떻게 될 것인가? 이 문제는 synchronized로는 도저히 해결할 수 없는 문제이다(data가 없다고 해서 pop() 호출을 그냥 없었던 일로 해서는 안 된다. pop()가 호출되었다면, stack에서 반드시 data를 하나 꺼내야 한다).
이 문제를 어떻게 해결하면 좋을까? 또 생각해보자(이 생각은 사실 wait(), notify()를 설명할 때 단골 손님으로 등장하는예이다).
여러분이 서울역에서 잠실까지 가려고 택시를 탔다고 생각하자. 잠실까지 가는 동안 5분마다 운전기사에게 "여기 잠실이에요?"라고 물을 사람은 아마 아무도 없을 것이다. 대부분 타자마자 "잠실 갑시다."라고 말한 후 잠을 자거나 딴 생각할 것이다. 잠실에 도착하면 기사가 알아서 "잠실입니다."라고 말해줄 것이다. 이건 매우 합리적인 일이다.
자, 이 예를 stack에 적용해보자. 어떤 Thread가 stack에서 pop()을 했는데 stack이 비어 있었다. 그렇다면 하는 수 없이 이 Thread는 block상태(대기상태)가 된다. 즉, stack에 data가 push될 때까지 기다리게 된다. 이것은 잠실에 도착할 때까지 손님이 잠자는 것과 같은 이치이다.
그 후에 stack에 data가 들어오게 되면 block되어 있던 Thread에게 data가 왔다는 신호를 해준다. 이것은 운전기사가 잠자고 있는 손님에게 잠실에 도착했다는 신호를 보내는 것과 같다. 그러면 대기중인 Thread는 stack에서 data를 pop할 수 있는 상태가 되는 것이다.
Java의 Thread에서도 지금 설명한 방식이 사용된다. 그때 사용되는 method가 wait(), notify()이다.
출처: http://sinily.tistory.com/16
'Java > Threads' 카테고리의 다른 글
Controlling Thread Status (0) | 2013.02.07 |
---|---|
Thread & Runnable (0) | 2013.02.07 |