1996년 3월 19일 첫 출판


앞 페이지 뒷 페이지 색인


쓰레드 클래스와 입출력 클래스

자바에서 잘 정의된 시스템 독립적인 추상 윈도우 시스템 외에 또하나 주목할 부분은 쓰레드를 지원한다는 점이다.
쓰레드는 프로세스 내에서 하나의 연속적인 제어 흐름이다. 이것은 프로그램 내에서 실행될 때 각각의 쓰레드는 하나의 시작점 과 하나의 흐름을 가지고, 런타임의 모든 시점에서 하나의 실행 지점을 가지고 또 하나의 끝점을 가진다는 의미이다. 쓰레드 객 체는 멀티쓰레드 프로그래밍의 기반으로 멀티쓰레드 프로그래밍은 하나의 프로그램이 서로 다른 일을 수행하는 여러 개의 쓰레드 를 동시에 수행할 수 있게 해 준다.

java.lang.Thread 클래스

실행 쓰레드를 새로 하나 만들려면 java.lang.Thread 클래스에서 하위 클래스를 파생하여 run() 도구를 오버라이드하면 된다. 오 버라이드된 run() 도구에는 새로 만들어진 쓰레드가 수행할 코드가 들어가게 된다. 다음 start() 도구를 호출하면 쓰레드 인스턴 스가 생성된다. start() 도구는 쓰레드를 생성하고 run() 도구를 실행하는 역할을 한다.
예를 들어 보면 다음과 같다.

class PrimeThread extends Thread {
            public void run() {
                // PrimeThread의 실행 코드
            }
}

이 쓰레드를 실행시키려면 다음과 같이 쓰레드에서 파생한 클래스의 인스턴스를 생성한 후 start()를 호출한다.

 PrimeThread p = new PrimeThread();
        p.start();
        ...

쓰레드를 생성하는 또하나의 방법은 Runnable 인터페이스를 이용하는 방법이다. Runnable 인터페이스를 포함한 모든 객체는 이 방법으로 하나의 쓰레드로 실행할 수 있다. 예를 들면 다음과 같다.

class Primes implements Runnable {
            public void run() {
                // PrimeThread의 실행 코드
            }
}

이 쓰레드를 실행하려면 다음과 같이 하면 된다.

Primes p = new Primes();
        new Thread(p).start();
        ...

자바 가상 기계(자바의 커널로 볼 수 있지만 기계 독립적으로 추상화된 부분)는 데몬 쓰레드가 아닌 모든 쓰레드가 종료할 때까 지 실행된다. 쓰레드는 run() 도구를 모두 수행하고 반환될 때나 stop() 도구가 호출될 때 종료한다.
새로운 쓰레드가 생성될 때 생성되는 쓰레드는 부모 쓰레드(즉 자신을 생성한 쓰레드)로부터 우선권(priority)과 데몬 플래그를 상속받는다.
다음은 간단한 쓰레드 예제이다. 100개의 쓰레드를 차례로 만들면, 생성된 쓰레드는 자신의 이름을 출력하고 종료한다. 100개의 쓰레드가 생성되는 것은 순서대로이지만 생성된 100개의 쓰레드가 순서대로 자신의 문맥을 수행한다는 보장은 없으며 문맥 수행 중이라는 문자열을 출력하기 전에 System.exit(0)가 호출되어 강제로 종료할 가능성도 있다.

ThreadTest.java

 

ThreadTest 실행 결과

 

동기화

동기화는 멀티 쓰레드 프로그래밍에서 고전적인 문제이다. 이 글의 서두에서 등장했던 철학자들의 만찬 애플릿을 기억하는지? 이 애플릿의 줄거리는 다음과 같다.
생각하는 것과 식사하는 것, 두 가지 일만 하는 철학자 다섯 명이 있다. 이들 철학자 사이에 하나씩의 스틱이 놓여 있다. 식사 를 하려면 양쪽의 스틱을 모두 가져야만 한다.
만약 모든 철학자가 오른쪽의 스틱을 잡고 있고 왼쪽의 스틱을 기다린다면 문제가 발생한다. 그들은 아무도 왼쪽 스틱을 얻을 수 없으므로 모두 굶어죽게 된다. 물론 철학자들은 모두 사기를 칠 줄 모르며 먹는 양도 모두 같다고 가정한다. 이런 상태를 데 드락이라고 한다. 데드락을 방지하려면 어떻게 해야 할까? 이 애플릿에서는 다섯 개의 스틱 중 하나를 표시하여 표시된 스틱을 잡을 경우 반드시 내려놓고 다른 스틱을 시도하도록 하고 있다. 이렇게 하면 위와 같은 데드락 상황이 발생하기 전에 표시된 스 틱의 왼쪽 철학자는 오른쪽의 표시된 스틱을 내려놓고 왼쪽 스틱을 기다리게 되므로 표시된 스틱의 오른쪽 철학자가 왼쪽 스틱을 얻을 수 있게 된다.
[그림] 고전적인 동기화 문제를 다룬 식사하는 철학자들 애플릿

 다음은 데드락 상태를 표현해주는 간단한 애플릿 예제이다.

ThreadTest2.java

 
<ThreadTest2.html>

<html>
<head><title>Thread Test 2</title></head>
<body>
<h2>DeadLock Condition</h2>
<applet code="ThreadTest2" width=300 height=300>
<param name=WorkA value=5> <! value는 1/1000 초 단위로 주어진다.>
<param name=WorkB value=10>
<! 두 개의 변수 값이 같거나 비슷할 경우 결과는 둘 다 실패(데드락)이다.>
</applet>
</body></html>

[그림] 데드락 조건을 보여 주는 애플릿
이 예제 애플릿은 변수로 WorkA와 WorkB를 받는데 이것은 A, B 쓰레드의 각각의 작업 시간이라고 볼 수 있다. 실제로 sleep() 도구의 인자값으로 사용된다. 이 두 값이 비슷하면 (예를 들어 기본값인 5라고 하면) 두 쓰레드의 실행 속도가 비슷하게 되어 서 로 다른쪽이 먼저 수행을 끝내고 그 결과를 기록해 주기를 기다리는 두 쓰레드는 모두 실패하게 된다. 서로가 상대방의 값을 무 한히 기다리는 상황, 이것이 바로 데드락 조건이다.(실제 데드락 상황이 되면 프로세스는 진행을 멈추게 된다. 이 예제는 단순히 데드락 조건만 보여주고 종료하도록 했다.) 인자값이 차이가 나는 경우, 예를 들어 WorkA=5, WorkB=10인 경우에는 보통 하나가 먼저 일을 종료하게 되므로 데드락 상황이 발생하지 않으나 쓰레드의 수행 순서는 상황에 따라 운영 체제의 커널이 결정하는 것 이므로 항상 발생하지 않는다고 보장할 수는 없다.

데드락 외에 식사하는 철학자 애플릿에서 두 명의 철학자가 동시에 스틱을 잡는 경우도 문제가 발생하는데 이것은 스틱을 잡는 것을 묘사하는 도구의 도구 제한자를 synchronized로 사용하면 방지할 수 있다. synchronized로 선언된 도구는 하나의 쓰레드가 이 문맥을 수행하게 되면 다른 쓰레드는 블록되어 기다리다가 먼저 들어간 쓰레드가 수행을 끝내고 리턴하는 순간에 수행을 시작 한다. 참고로 자바는 synchronized 도구를 구현하기 위해서 솔라리스에서는 뮤텍(크기가 1인 세마포어라고 생각할 수 있다.) 오 브젝트, win32에서는 크리티컬 섹션(동일 프로세스 내의 뮤텍이라고 생각할 수 있다.) 오브젝트를 사용한다. 세마포어 등을 사용 하여 동기화하는 것은 C 혹은 C++ 프로그램에선 까다로운 부분일 수밖에 없는데 자바는 이들을 synchronized 예약어 하나로 해결 할 수 있도록 배려한 것이다.
다음은 간단한 동기화 예제이다.

ThreadTest3.java

 이 예제 프로그램은 두 개의 쓰레드가 하나의 파일에 입출력을 할 때 발생하는 문제를 다룬 것이다. SeqTh read 클래스의 static 도구인 doSeq()는 seqnofile이라는 이름의 파일을 읽어 그 내용인 정수를 출력하고 현재 값에 1을 더하여 파일에 쓰는 일을 스무 번 반복한다.
이 프로그램의 실행결과는 다음과 같다.

ThreadTest3 실행 결과 1

 
즉, 두 쓰레드 중 먼저 수행된 쓰레드가 0에서 19까지 값을 쓴 다음 다음 쓰레드는 앞 쓰레드의 수행이 끝 날 때까지 기다리다가 앞 쓰레드가 파일에 쓴 마지막 값인 19를 읽어들여 수행을 하게 된다.
doSeq() 도구가 만약 synchronized로 선언되지 않았다면 (실제로 synchronized를 주석 처리하고 컴파일하여 실행해보길 바란다. ) 두 개의 쓰레드가 경쟁적으로 파일을 읽고 쓰기 때문에 파일 접근 자체가 에러가 나거나 우연히 제대로 실행되더라도 그 값이 전혀 순서를 지키지 않을 것이다. 다음은 synchronized를 주석처리할 때의 한 실행 예이다.

ThreadTest3 실행 결과 2-synchronized를 주석 처리한 경우

 두 개의 쓰레드가 서로 경쟁적으로 파일을 읽고 쓰기 때문에 seqno의 값이 각 쓰레드별로 순서가 맞지 않 고 값이 엉켜 갑자기 seqno 값이 크게 증가하였다.

HERE TO GO

java.io 패키지

위의 ThreadTest3.java 예제 파일을 살펴보면 파일 입출력을 하는 것이 눈에 띌 것이다.
디스크 파일은 파일 포인터를 임의로 조작할 수 있는 임의 접근 파일(Random Access File)이기 때문에 위의 예제처럼 RandomAccessFile 클래스로 파일을 처리하면 읽고 쓰는 것이 간단하다.
포인터를 임의로 조작할 수 없는 일반적인 스트림 입출력의 경우에는 java.io.InputStream이나 java.io.OutputStream 클래스 인스턴스를 생성하여 입출력을 할 수 있다.
다음은 InputStream(OutputStream)에서 파생된 클래스인 FileInputStream(FileOutputStream) 클래스를 사용하는 간단한 파일 입 출력 프로그램이다. 파일 스트림 클래스들은 임의 접근 파일이 아니기 때문에 포인터를 조작하는 seek()과 같은 도구가 없다는 점을 제외하면 RandomAccessFile과 거의 동일한 도구들을 가지고 있다. 스트림을 조작할 때 먼저 FileInputStream 인스턴스를 생 성하고 이를 인자로 DataInputStream 인스턴스를 생성하여 실제 스트림 입력 조작을 행하는 순서를 눈여겨 보아두자. 스트림 출 력의 경우에는 먼저 FileOutputStream 인스턴스를 생성하고 이를 인자로 PrintStream 인스턴스(혹은 DataOutputStream 인스턴스) 를 생성하여 스트림을 조작한다.
[그림] 스트림 입출력 관련 클래스 계층 구조
<그림 > 스트림 입출력 관련 클래스 계층 구조

TextCopy.java

 
위의 예제는 텍스트 원시 파일을 한 줄씩 화면 출력을 하면서 동시에 목적 파일로 복사한다. 하지만 win32와 솔라리스 시스템은 텍스트 파일에서 개행 문자를 다루는 방식이 다르므로 완전히 동일한 파일로 복사되지는 않을 것이다.

자바에서 애플러케이션과 달리 애플릿은 서버로부터의 입출력에 제한이 많다. 애플릿이 클라이언트의 디스크에 쓰기를 할 수 있 는 방법은 클라이언트의 사용자가 해당 클래스 파일을 직접 자신의 디스크에 설치하여 접근 권한을 수정하는 수밖에 없다.  


앞 페이지 뒷 페이지 색인