Thread란 실행 중인 프로그램을 의미한다. 둘 이상의 쓰레드가 실행 될 때 멀티 쓰레드라고 부른다. 아래의 그림은 오늘 포스트 할 내용을 함축적으로 담고 있다. 글을 모두 읽고 나서 보게 되면 이해가 빠를 것이다.
쓰레드를 구현하는 방법에는 2가지가 있다. 첫번째가 Thread를 상속 받는 것. 두번째가 Runnable Interface를 구현하는 방법이다. 상속을 받던지 구현을 하던지 run()이라는 메서드를 오버라이드 해야만 한다. 차이 점이라면 runnable은 자신을 담아줄 thread가 필요하다는 점이다. thread 상속시에는 그 자체로 하나의 쓰레드가 된다.
재밌는 점은 쓰레드를 실행시킬 때에는 start() 메서드를 사용해야 한다는 점이다. 왜 run()이 아니라 start()인가? 사실 start()라는 것은 run()을 실행시킬 장소를 마련해 주는 메서드다. 이 말은 start()를 실행시키면 호출스택이라고 해서 run()을 위한 환경을 구성해주는 역할을 한다.
쓰레드는 우선순위 지정이 가능하다. thread.setPriority(우선순위 점수); 를 이용한다. 각각의 쓰레드에 우선순위를 지정해주지 않으면 모두가 같은 우선순위를 가지기 때문에 같은 정도의 시간을 소요하게 된다.
쓰레드 그룹이라는 것은 보안상의 이유로 탄생했다. 마치 폴더와 같은 개념인데 같은 폴더이거나 하위 폴더에 있는 쓰레드는 변경할 수 있지만 상위 쓰레드는 건드릴 수 없다. 간단하게 ThreadGroup를 사용하면 된다.
데몬 쓰레드는 일반 쓰레드를 돕는 역할을 한다. 그래서 일반쓰레드가 모두 종료되면 데몬 쓰레드는 자동으로 종료된다. 사실 데몬쓰레드는 CallBack 메서드와 아주 흡사하다. 이놈은 무한 루프와 조건문을 이용해서 실행 후 대기하고 있다가 특정 조건이 만족되면 작업을 수행한다. 사용방법은 쓰레드를 하나 만들어서 setDaemon(boolean on)을 이용하면 데몬 쓰레드로 변경 시킬 수 있다. 데몬 쓰레드는 주쓰레드가 죽으면 같이 죽어버린다. 안드로이드에서 이러한 것을 자주 사용한다.
사실 쓰레드 프로그래밍이 어려운 이유는 쓰레드들을 실행 제어하는 부분이 까다롭기 때문이다. 여기서 쓰레드 메서드를 이해하는 것이 중요하다. 메서드들은 다음과 같다.
1. join()
join()은 그 쓰레드가 끝날 때 까지 다른 쓰레드들을 모두 대기시킨다. 예를 들면 th1.start -> th1.join -> th2.start 를 하면 th1이 종료될때까지 th2는 실행되지 않는다.
2. sleep()
sleep()은 static 멤버다. 이 말은 th1.sleep()을 하건, th2.sleep()을 하건 슬립하는 그 순간은 동일하다는 것이다. 그 외에 yeild() 나 stop(), suspend(), interrupted() 같은 걸 어떻게 사용하는지 예제를 통해 연습하자. 필요할때 가져다 써도 무방하지 싶다.
class MyThreadEx19 implements Runnable{
boolean suspended = false;
boolean stopped = false;
Thread th;
public MyThreadEx19(String name) {
th = new Thread(this, name);
}
@Override
public void run() {
String name = Thread.currentThread().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("interrupted() in suspend()");
}
public void resume(){
suspended = false;
}
public void stop(){
stopped = true;
th.interrupt();
System.out.println("interrupted() in stop()");
}
public void start(){
th.start();
}
}
쓰레드는 동기화는 성능에 큰 영향을 미친다. 동기화를 예를 들어 설명하자면 svn에서 하나의 클래스 파일을 오직 한명이 건드리게 해야하는 방식과 같다. 만약에 한명이 파일을 건드리고 있는데 다른 사람이 또 그 파일을 건드리게 되면 데이터가 유실되거나 생각지 않은 방향으로 변경되게 될 것이다. 동기화는 다음과 같은 방법이 있다.
synchronized은 해당 작업의 공유데이터에 lock을 거는 방식이다. synchronized는 객체에 lock을 걸거나 메서드에 lock을 걸 수 있다. 이 락이 걸린 데이터를 쓰레드 하나가 사용하게 되면 그 synchronized가 걸린 블록이 끝나기 전까지는 lock이 걸려 있게 된다.
그런데 여기서 의문이 하나 생긴다. 두개의 쓰레드가 lock이 걸린 데이터를 써야 한다면, 하나의 쓰레드가 먼저 데이터를 가져가고 두번째 쓰레드는 첫번째 쓰레드의 작업이 끝날때까지 기다려야만 한다. 이 문제를 어떻게 해결할 것인가?
이러한 비효율을 개선하기 위해서 wait()와 notify(), notifyAll()를 사용한다. 이 메서드들은 synchronized 블록 내에서만 사용된다. 예들 들어보자.
하나의 쓰레드가 synchronized 메서드를 이용하는 중에 특정 조건이 되면 wait()가 걸리게 된다. 그러면 그 쓰레드는 waiting pool이라는 대기실에서 잠을 자게 되고, 이 쓰레드가 잡고 있던 synchronized 메서드는 lock이 풀리게 되어 다른 쓰레드가 사용하게 된다. 나중에 필요할 때 notify()를 사용해 깨울 수 있다. 그런데 notify()는 특정 쓰레드를 깨우는게 아니라 우선순위가 높은 쓰레드를 깨우기 때문에 어떤 쓰레드가 깨어날 지 알 수 없다. 그래서 notifyAll()을 사용해서 모든 쓰레드를 깨워놓고 JVM의 쓰레드 스케줄링에 의해서 처리되도록 한다.(그것이 안전)
댓글 없음:
댓글 쓰기