| |||
그럼 여기서 먼저 혼동이 될 수 있는 용어를 먼저 정리해 보겠습니다. | |||||||
| |||||||
각각의Process는 실행시 고유의메모리 공간을 점유하고 운영체제의 통제를 받아 다른 Process에 의해 간섭을 받지 않고 독립적으로 수행되지만Thread는 Process가 점유한 메모리 공간에서 다른 Thread와병렬적으로 수행되게 됩니다. | |||||||
Thread는 자신이 수행할인스트럭션의 순서를 포함한최소의 자원만 가지고 있습니다. 이런 특징으로 인해 여러 개의 Process로 실행되는 경우보다 Multithread 프로그래밍을 하게 되면시스템 자원을 좀더효율적으로 이용할 수 있게 됩니다. | |||||||
자바에서 대부분의 프로그램은 개발자가 원하든 원하지 않든Multithread를 사용하게 됩니다. | |||||||
왼쪽 그림은 개발자가콘솔기반의 프로그램을 작성했을 경우 실행되는 모습입니다. 우측의 그림은 개발자가 명시적으로 Thread를 생성한 경우이거나, 또는 명시적으로 Thread를 생성하지 않은 경우라도 AWT나스윙을 사용한 경우는 자동적으로 Multithread 환경에서 작동하는 경우가 됩니다. |
|
| |||
여러분들의 이해를 돕기 위해 Thread와 Process의 개념에 잘 비유되는 예문을 소개합니다. 먼저 정리를 하면 이 예문에서개인서재는Process의 개념과 일치하고, 시립도서관은Multithread의 개념과 유사합니다. | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||||||||||||||||||||||||
그렇다면 개인서재와 시립도서관의 특징은 무엇일까요? | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||||||||||||||||||||||||
개인서재와 같이Process는 오로지 자신의 실행영역과 자원을 가지며, 오직 자신만이 내부의 자원을 사용하므로 Multithread 환경과 같이 다른 Thread에 자원을 빼앗길 염려는 없습니다. 하지만 모든 사람이 개인서재를 만드는 것처럼 많은 비용(시스템의 리소스)을 지출해야 합니다. | |||||||||||||||||||||||||||||||||||||||||||||||||||||
반면에 시립도서관에 비유되는Multithread환경에서는 공유된 자원을 여러 Thread에서 사용하므로 동일한 비용을 들이더라도 좀더 효율적인 자원의 사용이 가능해집니다. 다만 다른 사람이 책을 빌려 간 상황이 발생하는 것처럼 자신이 필요한 자원을 다른 Thread가 점유하고 있다면 자원의 점유가 해제될 때까지 대기해야 하는 불편함은 있습니다. |
| |||
그렇다면 지금까지의 방법으로도 웬만한 프로그램 작성에는 별 문제가 없었는데 무엇 때문에 Thread까지 사용해서 개발자들을 어렵게 하는 걸까요? 크게 두 가지 관점에서 Thread 사용의 필요성을 정리할 수 있습니다. | |||||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||||
이상과 같은 이유에서 Thread는 수준높은 프로그램 작성에필수적인고려사항이 됩니다. 또한 자바는 언어적으로도 개발자가 쉽게 Thread 기반의 프로그램을 작성할 수 있는API를 제공함으로써 빠르고 안정적인 프로그램 작성을 도와줍니다. |
|
| |||
그러면 기존의 다른 언어와 비교해서 자바의 Thread는 어떠한 특징이 있을까요? | |||||||||
자바는언어 수준에서 Thread를 지원합니다. 다시 말해서, 일반적으로 다른 언어에서 Thread를 사용하기 위해서는 보조 라이브러리를 사용해야 하지만, 자바는 언어 수준에서 Thread를 사용하기 위한 클래스들이 준비되어 있기 때문에 좀더 쉽게 개발자가 Thread를 이용할 수 있습니다. | |||||||||
Thread를 사용하기 위해 준비된 클래스들에는java.lang.Thread와 java.lang.ThreadGroup이 있으며java.lang.Runnable인터페이스와 연관되어 있습니다. 또한 모든 클래스들의 조상 클래스인java.lang.Object에서 조차 Thread를 지원하기 위한 메쏘드가 준비되어 있습니다. | |||||||||
이런 클래스와 인터페이스는 Thread 프로그래밍시 앞으로 자주 참조를 해야 하므로 한번쯤 자바 API 문서에서 찾아보시기 바랍니다. 자바로 Thread 프로그래밍을 한다는 것은 위의 클래스들을 개발자가 효율적으로 이용해서 코딩한다는 것을 의미합니다. | |||||||||
|
그러면 이제 실제로 Thread를 만들어 보겠습니다. 자바에서 Thread를 생성하는 방법에는Thread 클래스를 상속받는 방법과 Runnable 인터페이스를 구현하는 방법등 두 가지가 있습니다. 먼저 두 가지 방법으로 Thread를 작성해 보고, 각각의 특징에 대해 간단히 알아 보겠습니다. | |
| |||
먼저 java.lang.Thread 클래스를 상속받아 작성하는 방법입니다. 다음의 코드를 작성해서 실행시켜 보시기 바랍니다. | |||
| |||
| |||
앞의 예제코드는 단순히 0.5초 지연되면서 지정된 문자열을 출력하는 코드입니다. 위의 코드에서 가장 중요한 부분은 Thread의 작업을 정의한run()메쏘드와 실제로 작업을 시작하는start()메쏘드입니다. | |||
자바 API 문서의 java.lang.Thread 부분을 보시면 Thread의 클래스 원형은 다음과 같이 선언되어 있습니다. | |||
| |||
이 선언문의 의미는, Thread라는 클래스는 Object 클래스(자바에서 모든 클래스의 조상 클래스)를 상속받았고, Runnable Interface를 implement했다는 말입니다. 그리고 Runnable Interface에는 run()이라는 오직 하나의 메쏘드만이 선언되어 있습니다. | |||
이런 이유에서 우리가 Thread 클래스를 상속받아 새로운 클래스를 만들기 위해서는 run()이라는 메쏘드를반드시 정의해야만 합니다. 다시 말해 Thread를 이용해 수행할 작업들은 바로 이 run() 메쏘드에서 구현되어야 하는 것입니다. 그리고 이를 사용하기 위해서는 작성된 클래스의 새로운 인스턴스를 생성하고 start() 메쏘드를 호출하는 것이 전부입니다. | |||
// TestThread1.java: Thread 클래스를 상속받아 Thread를 생성하는 예제
public class TestThread1 extends Thread {
// Thread의 작업 내용을 기술하는 run() 메쏘드
public void run() {
int i = 0;
while(i++ < 10) {
System.out.println("Extended Thread testing...\t Step " + i);
try {
sleep(500);
} catch(InterruptedException e) { }
}
}
// 프로그램의 진입점 main() 메쏘드
public static void main(String args[]) {
Thread t = new TestThread1();
t.start();
}
}
| |||
앞의 코드와 동일한 작업을 수행하는 코드를 Runnable 인터페이스를 이용해서 구현하였습니다. Thread를 상속받아 작성한 코드와의 차이점을 붉은색으로 표시했으니 주의 깊게 보시기 바랍니다. | ||||||||||||
| ||||||||||||
| ||||||||||||
| ||||||||||||
앞의 코드와 이번 코드는 run() 메쏘드를 구현한 것은 동일합니다. 하지만, 구현 방식에서는 위와 같은 차이점이 있습니다. 다시 한번 코드를 자세히 살펴보면서 확인하시기 바랍니다. | ||||||||||||
// TestThread2.java: Runnable Interface를 implement받아 Thread를 생성하는 예제
public class TestThread2 implements Runnable {
public void run() {
int i = 0;
while(i++ < 10) {
System.out.println("Implemented Thread testing...\t Step " + i);
try {
Thread.sleep(500);
} catch(InterruptedException e) { }
}
}
public static void main(String args[]) {
Runnable r = new TestThread2();
Thread t = new Thread(r);
t.start();
}
}
| |||
다음은 앞에서 살펴 본 두가지 구현 방법의 차이점과 이점에 대해 알아 보겠습니다. | |||||||||||||||||||||||||||||||||||
|
| |||
앞에서 살퍼본 바와 같이 Thread는 run() 메쏘드를 구현하고 start() 메쏘드를 호출하는 것으로 실행됩니다. 엄밀하게 말하면 실행된다기 보다는실행가능 상태가 되는 것인데 이에 대해서는Thread의 상태전이에서 자세히 알아보겠습니다. | ||
그렇다면 Thread의 수행은 어떻게 종료시킬 수 있을까요? JDK 1.2 이전에는 stop()이라는 메쏘드가 지원되어서 Thread를 중지시키기 위한 부분에서 호출해 사용할 수 있었습니다. | ||
하지만 이런 방법은 뒤에서 설명하게 될 Object의 Lock이나 Thread의 본질적인 개념에 관련해심각한 문제를 유발할 가능성이 있어서 버젼 1.2 이상에서는Deprecated되었고 더이상 사용하지 않게 되었습니다. | ||
따라서, Thread를 종료하는 유일한 방법은run() 메쏘드를 끝까지 진행시켜서 자연스럽게 종료시키는 것입니다. 물론 작업을 한번 수행하고 종료하는 메쏘드라면 문제가 없겠지만 계속해서 반복작업을 해야 하는 경우라면 문제가 될 수 있습니다. | ||
다음의 코드를 봅시다. | ||
| ||
만약 반복적인 작업을 수행하는 Thread의 run() 메쏘드를 구현해야 한다면 순환문을 벗어나 Thread가 정상 종료되지 못하고 무한반복을 할 위험이 있습니다. | ||
이런 경우는 앞의 예와 같이 Thread의 종료여부를 알려주도록 isFinished라는 boolean 타입의 변수를 먼저 선언하고 순환문 내부에 특정한 조건이 만족되면 이 변수의 값을 false로 변경합니다. | ||
이제 순환문이 다시 시작되는 시점에 isFinished값이 false로 되어 조건을 만족하지 않으므로 순환문을 벋어나게 됩니다. 이런 변수를조건변수라고 이 방법은 Thread에서 순환문을 사용시 자주 사용하는 방법입니다. | ||
혹시 지금까지의 학습 내용 중에서 다음과 같은 의문점이 생기시는 분들이 계신지 모르겠군요. | ||
"Thread를 사용하면 한번에 여러 job을 수행할 수 있다고 했는데, 지금까지의 예제는 기존에 작성하던 프로그램과 별로 틀린 게 없지 않나요?" | ||
맞습니다. 앞의 예제들은 여러분들의 이해를 돕기위해 한번에 하나의 Thread만 생성시켰습니다. 먼저 이런 간단한 예제에 익숙해지고 난 후에 약간의 수정과 확장을 하게 되면 실제로 우리가 생각했던 Multithread를 만드는 것은 별로 어렵지 않습니다. | ||
계속해서 다음 절에서는 이번 절에 학습한 Thread 기본지식을 가지고 Thread의 깊은 곳을 들여다 보도록 하겠습니다. |
|
|
| |||
앞절의 예제에서 Thread를 실행시키기 위해서 t.start() 메쏘드를 호출했던 것을 기억하실 겁니다. 실제로 Thread는 이 메쏘드의 호출과 동시에 run() 메쏘드가 시작되고 다양한 상태로 전이되면서 작업을 수행하다가 run() 메쏘드의 끝까지 실행된 후에 종료됩니다. 다음의 상태전이도는 Thread를 이해하는데 매우 중요하므로 상세히 살펴보도록 하겠습니다. | ||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||
대부분의 경우, Thread는 동시에 여러개가 생성되어 수행되고 이로 인해 컴퓨터에 있는 자원을 사용하기 위한경쟁을 하게 됩니다. 실제로는 하나의 CPU가 장착된 경우, 한번에 하나의 Thread만이 실행되게 됩니다. | ||||||||||||||||||||||||||||||||
다음은 이런 Thread들의 경쟁과 자원의 점유를 통해 실행되는 상황을 개념적으로 표시한 그림입니다. | ||||||||||||||||||||||||||||||||
이런 이유 때문에 개발자는 여러 개의 Thread가 균등하게 실행되어 원활히 작업을 수행할 수 있도록 배려를 해야 합니다. 만약 개발자가 이러한 배려를 해주지 않는다면 Thread의 수행순서는 전적으로운영체제에 맡겨져 진행되고, 최악의 경우, 어떤 Thread는 아예 실행될 기회도 얻을 수 없게 됩니다. 이 문제는 뒤에 나올 Thread의 Scheduling 부분에서 상세히 알아보겠습니다. | ||||||||||||||||||||||||||||||||
Thread의 사용에 있어서 점점 복잡도가 증가하면서 개발자는 실제 어떤 순서대로 Thread가 특정자원을 점유하고 작업을 진행할지 예측하기가 불가능해지기도 합니다. 바로 이 문제 때문에 효율적인 Thread의 작성이 쉽지 않은 것입니다. |
다음으로 Thread 클래스에서 제공하는 각종 메쏘드들을 알아보겠습니다. | |||
| |||
이번에는 Thread를 효율적으로 사용하기 위해 Thread 클래스에서 제공하는 중요한 메쏘드들에 대해 알아보겠습니다. (메쏘드의 전체 리스트는 자바 API 문서를 참조하세요.) | |||||||||||||||
| |||||||||||||||
다음은 Object 클래스(자바의 모든 클래스들의 조상)에 구현되어 Thread의 사용을 도와주는 메쏘드입니다. | |||||||||||||||
| |||||||||||||||
메쏘드들의 간단한 설명만으로는 이해하는데 한계가 있습니다. 앞으로 학습을 진행하면서 메쏘드가 계속 사용될 때 위의 설명을 다시 한번 참조하시기 바랍니다. 이외에도 Thread 클래스에는 많은 메쏘드들이 제공됩니다. 항상 API 문서를 보며 필요한 메쏘드들을 참조하는 것 잊지 마시기 바랍니다. | |||||||||||||||
다음은 JDK 1.1 이전에 사용하다가 Deprecate되어서 1.2 버젼 이후에는 사용하지 않는 메쏘드들입니다. 뒤에서 설명할 Object Lock이나 동기화에 문제가 있을 수 있으므로 앞으로는사용해서는 안됩니다. | |||||||||||||||
|
| |||
앞에서 언급된 것처럼 Thread가 한정적인 자원을 효율적으로 이용하게 하려면 개발자가 Thread의 실행순서를 잘 설정해 주어야 합니다. | |||||||||
이런 과정이 없다면 실제 Thread의 실행순서는 운영체제에 구현된 Scheduler에 의해 관리되므로 개발자가원하지 않는 결과를 초래할 수도 있습니다. 바로 이런 이유 때문에 개발자는 Thread들의우선순위와자원을 공유할 수 있도록 배려를 해야 합니다. 이 과정을Thread Scheduling이라고 합니다. | |||||||||
자바에서는 Thread의 Scheduling을 위해 두가지 Method를 제공합니다. | |||||||||
| |||||||||
또한 Thread에 정의된 상수는 우선순위에 관련된 것으로 다음의 세가지가 있습니다. | |||||||||
| |||||||||
앞에 Thread관련 메쏘드에 설명드린 대로 yield() 메쏘드는 동일한 Priority를 갖는 Thread에게 실행할 수 있는 기회를 주는 반면, sleep() 메쏘드는 자신보다 낮은 Priority의 Thread에게도 실행 기회를 준다는 것을 다시 한번 기억하시기 바랍니다. |
| |||
그렇다면 개발자가 Scheduling을 고려하지 않은 Thread가 어떻게 작동하는지 예제를 통해 알아보도록 하겠습니다. 다음의 예제를 작성하여 실행결과를 보시기 바랍니다. | |||
| |||
| |||
앞에서 사용한 예제를 조금 수정해서 두개의 Thread를 #1, #2라는 이름을 주어서 생성시켰습니다. 과연 두개의 Thread가 과연 어떤 순서로 수행이 될까요? | |||
// TestThread3.java: 이기적인 Thread 구현 // 이름을 갖는 Thread 생성 // Thread가 수행할 작업 정의 // 프로그램의 제어 } |
| |||
그렇다면 모든 Platform에서 동일한 결과가 나올까요? | ||
그렇지 않습니다. 만약 Linux 기반의 Platform에서 예제를 실행시킨다면 다음의 결과처럼 #1 Thread가 모두 실행이 완료가 되고, 그후에 #2 Thread가 실행되는 것을 알 수 있습니다. | ||
| ||
이는 각 운영체제에서 구현한Thread 모델이 서로 달라서 발생하는 현상이며, Thread 모델 유형과 각각의 차이점은 뒤에서 좀더 자세히 알아보겠습니다. | ||
그렇다면 이렇게 운영체제 마다 다른 결과가 개발자에게는 어떤 의미가 있을까요? 답은 자바의WORA(Write Once Run Anywhere)의 개념에 충실하도록 어느 Platform에서나 동일한 결과를 얻을 수 있는 프로그램을 작성해야 한다는 것입니다. | ||
위의 예제는 Thread의 Schedule 통제를운영체제에 위임한 경우입니다. 위와 같은 코딩이 문제가 없는 경우도 있겠지만 대부분의 경우 개발자의 의도와는 다르게 실행됩니다. 따라서 Thread 작성시는 반드시 모든 Thread가 골고루 실행기회를 갖도록 하는게 중요합니다. |
| |||
이번에는 앞의 예제를 조금 수정해서 운영체제에 무관하게 두개의 Thread가 골고루 수행될 수 있도록 만들어 보겠습니다. | |||
| |||
| |||
Thread가 한번의 Action, 즉 System.out.println()을 실행한 후에는 현재 대기중인 다른 Thread가 실행될 기회를 가질 수 있도록yield() 메쏘드를 호출하도록 수정했습니다. 다음은 이렇게 수정한 후의 실행 결과입니다. | |||
// TestThread4.java: 운영체제에 무관한 Thread 구현 // 이름을 갖는 Thread 생성 // Thread의 수행작업을 구현 // 프로그램의 제어 } |
| |||
다음으로 앞에서 잠시 언급한Thread 모델에 관해 알아보겠습니다. Thread가 운영체제에서 구현된 방식은 크게협력형과선점형 모델로 나눌 수 있습니다. 각 구현방식의 특징과 장단점에 대해 알아보겠습니다. | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||||||||||||||||||||||||
실제로 이런 Thread 모델은 운영체제별로 Virtual Machine에서 상이한 구현방식을 사용하기 때문에 개발자가 이에 대한 변경을 할 수는 없습니다. 참고로 Windows 기반의 VM에서는선점형 방식을 사용하고, Mac VM은협력형을 사용합니다. Unix의 경우는두가지 방법이 혼합된 경우가 많습니다. | |||||||||||||||||||||||||||||||||||||||||||||||||||||
앞의 TestThread3.java의 예제 결과에서 본대로 운영체제별로 이런 특징 때문에 Thread간에 제어가 자동으로 넘어가기도 하고, 그렇지 못하기도 하다는 것을 이해하시기 바랍니다. 또한 개발자는 이러한 특성을 감안하여운영체제에 종속적이지 않은 Thread 프로그램을 작성해야 한다는 것도 잊지 마세요. | |||||||||||||||||||||||||||||||||||||||||||||||||||||
|
| |||
Thread는 Process보다 적은 리소스를 점유한다고 했는데 그렇다면 과연 Thread는 몇 개까지 만들 수 있을까요? | |||
저도 궁금해서 테스트를 해보니 384Mbyte의 Windows ME 환경에서 약 1200개 정도, 2000 Pro에서는 256Mbyte에서 7000개까지 Thread가 생성되고 OutOfMemoryError가 발생했습니다. 이런 부분에서 확실히 Windows 2000이 이전 버젼보다는 안정되었다는 느낌이 드는군요. | |||
일단 제가 테스트해본 결과로는 결국 메모리라는 시스템 리소스의 한계가 Thread 생성의 한계로 작용했습니다. 실제 7000개 이상의 Thread가 필요할 정도의 시스템이 어디에 쓰일지는 여러분의 상상에 맡기겠습니다. | |||
여러분들의 PC 성능이 궁금하신 분들은 다음의 코드를 실행시켜서 테스트 해 보시기 바랍니다. 아무일도 하지 않고 60초간 sleep하는 Thread를 무한대로 생성시킵니다. | |||
| |||
| |||
// ThreadCntTest.java: Thread의 생성 가능 개수 테스트
public class ThreadCntTest extends Thread {
public void run() {
while(true) {
try {
sleep(60000);
} catch (InterruptedException e) {}
}
}
public static void main(String args[]) {
int i = 0;
while(true) {
new ThreadCntTest().start();
i++;
if(i % 100 == 0)
System.out.println("Thread Number: " + i);
}
}
}
Thread의 개념 중 마지막으로 가장 중요한동기화에 대해 알아보겠습니다. 그동안 학습한 Thread의 상태와 Scheduling과 더불어 가장 중요한 개념이면서 동시에 가장 구현하기 힘든 부분이 바로 동기화입니다. 이제 마지막 고지를 향해 오른다는 각오로 이번절의 학습에 임해주시길 부탁합니다. | |
만약 아무런 통제없이 시스템의 리소스를 점유하기 위해 여러 Thread가 경쟁한다면 어떤 상황이 발생할까요? 아마도 한권의 책을 대출하기 위해 여러명이 동시에 책을 잡고 잡아 당겨 책이 파손되는 것처럼 시스템의 리소스도 정상작동을 할 수 없게 될 것입니다. | |
이런 문제를 해결하기 위해 자바의 언어적 측면에서 지원되는 기능이 바로동기화, 즉synchronized 키워드입니다. 먼저 동기화에 관련된 개념에 대해 정리를 하고 실제 동기화 구문을 작성하는 예제를 만들어 보겠습니다. |
'Programming > JAVA' 카테고리의 다른 글
[펌] [SWING]JFileChooser (0) | 2005.06.29 |
---|---|
[펌] 서버 소켓 예제 (0) | 2005.06.24 |
[펌] 클라이언트 소켓 (0) | 2005.06.24 |
[펌] 13. 쓰레드(Thread) (0) | 2005.06.23 |
[펌] Swing (0) | 2005.06.17 |