들어가기 전에 이 내용과 소스는 오직 www.okjsp.pe.kr 과 저자의 허락을 받은 곳에서만 사용할 수 있음을 알립니다.
이를 어길시에는 제가 인상을 써야할 일이 생길수도 있습니다.
현재 이 페이지에 대한 링크만은 허용하며 무단 전제는 철저히 응징하겠습니다.
별것 아닌 내용이지만 작은것이라도 저작자의 권리를 인정하며 서로에게 도움을 줄수 있는 사회를 위해서 입니다.
만일 이 글을 읽기 힘드신 분은 첨부 파일에 이 글 내용과 똑같은 ThreadPool 강좌.txt 파일이 있습니다.. 이것을 통해 읽기 바랍니다.
Thread Pool 사용하기..
Thread란 자원은 시스템의 리소스를 많이 차지하는 자원이다.
하나의 Thread를 생성하기 위해서는 OS는 많은것을 할당해주어야 하며 많은 보살핌이 필요하다.
또 생성된 Thread를 종료하는 것 또한 OS가 많은것을 정리해 주어야 한다.
무분별한 Thread의 생성은 시스템의 결함을 초래하는 계기가 될수 있다.
이번 컬럼에서는 Thread Pool에 대한 것이기에 Thread의 자세한 내용은 언급하지 않겠다..
자바 관련 책이나 인터넷 또는 다른 언어나 운영체제 책에 Thread에 대하여 많은 내용이 있으니 참고하길 바란다.(이런 무책임한.. ㅡ.ㅡ)
위에서 언급했듯이 Thread를 효율적으로 사용한다면 시스템에 많은 도움을 줄수 있다..
그러기 위해서는 일정 갯수만큼을 Pooling 하여 사용하는 것은 효율성을 위한 하나의 선택이라 할 수 있다.
이번 예제에서는 pool rebalancing을 하지 않았다.. (그것은 여러분의 숙제로 남기겠다... 역시 무책임한)
pool rebalancing이란 pool에 pooling된 객체가 사용상 필요 없음에도 불구하고 많이 있어 시스템 리소스를 다시 반납하는 것을 말한다.
여기 Thread pool에서는 일정 시점마다 Thread의 갯수를 체크하여 현재 사용중인 Thread와 비교하여 일정 %가 안된다면 이를 시스템에 반납하는 것을 뜻할수 있다.
이러한 Thread Pool은 어떨때 쓰일까 하는 궁금증이 혹실 있는 사람을 위해서 대표적인 예로 우리가 지금 공부하는 서블릿 엔진들이 내부적으로는 Thread Pool을 쓸것이다.
수많은 사용자의 요청을 위해 매번 Thread를 생성하면 그에 따른 처리시간이 늘어나며 동시 처리의 가용성이 떨어질것입니다.
따라서 더 좋은 성능을 위해서 하나의 방편으로 서블릿 엔진들은 Thread Pool을 사용할 것이다.
참고로 많은 분들이 사용하시는 DB Pooling 또한 이와 유사한 것이라 생각하면 된다.
DB의 가장 많은 부하는 DMBS에 처음 접속하는 Connection 시간이 잡아 먹는다.
즉 TCP/IP 기반으로 접속을 하고 사용자 인증등을 거치는 것이 하나의 쿼리를 수행하는 시간에 비하여(시간이 수십분씩 걸리는 무식한 쿼리 말고 이러한 작업은 늦은 시간에 배치를 돌려야 할것이다.) 이러한 연결 시간은 엄청난 것이다.
또한 사용한후 바로 접속을 끊어 버리면 OS는 이러한 소켓 자원을 위해 추가 작업을 해주어야 한다.
이러한 연결을 미리 여러개를 접속하여 필요할때 마다 사용하고 다 쓰면 반납하고 하여 효율적인 DBMS의 사용을 위한 것이 DB Pooling이라 할 수 있다..
실제 클라이언트 프로그램에서는 이러한 DB Pooling을 하지는 않는다.. 많은 사용자를 처리하는 서버에서 이러한 것을 사용한다.
마찬가지로 Thread Pooling 또한 서버에서 많이 사용한다.
특히 웹과 같이 한번 접속하고 작업이 끝나면 연결을 끊고 하는 비 연결성일 경우 더욱 자원의 효율성이 중요하다.
또 한가지 Pool을 썼을 경우 자기 방어적인 프로그램을 만들수 있다. Pool의 최대값은 이론적 근거와 충분한 테스트후 설정해 주면 안정적인 서비스를 제공할 수 있다.
필자가 예제를 만든것은 채팅을 위한 것이었지만 불필요한 군더더기는 모두 빼구 개념을 이해하기 위한 아주 간략한 부분만을 간추렸다.
아직 필자 또한 상당한 실력이 아니기에 콘크리트 코딩이 들어 간것을 양해 하기 바란다.
또한 잘못된 개념을 가지고 설명을 했을 수도 있으며 잘못된 코드가 있을 수도 있다...
만일 이러한 잘못된 사항이 있으면 조언을 바란다.. (사람인 지라.. 아직 경력두 얼마 안 되었구...)
하지만 제공하는 예제를 통해 많은 변화와 파생을 줄수 있을거라 기대한다.
먼저 Thread Pool을 관리할 Manager를 만드는 것이다.
Manager의 역활은 Thread를 설정 값에 따라 생성하고 관리하는 역활이다.
필요한 역활을 생각하면 설정값을 얻어 이에 따른 초기화 작업을 해 주어야 할것이다.
사용자의 요구에 따른 Thread 자원의 대여와 반납을 처리 해 주어야 한다. (필수)
위에서 숙제를 내준 rebalancing을 신경 써야 할것이다.
위의 요구 사항을 보면 별것이 아닐것이다.
하지만 여기서 중요한 것이 동기화 처리이다...
1개의 Thread 자원이 있을때 동시에 여러 대여 요청이 왔다..
시간적으로는 순서가 있지만 무시할 수 있는 시간 사이의 값일때 대여를 해주고 현재 유휴 Thread 자원의 갯수를 설정중에 다른 대여 요청이 들어 오거나 반납 요청이 오면 상태 관리가 엉망이게 될것이다.
따라서 하나의 일에 대한 Atomic한 단위를 나누어 동기화를 해야 할것이다.
즉 사용자의 대여 요청과 반납 요청은 각각 동기화 해주어야 할것이다.
또한 rebalancing을 하게 된다면 rebalancing 하는 동안은 대여와 반납 요청을 정지 시켜야 할것이다.
동기화 문제는 많은 추가적인 보살핌이 필요하기에 함부로 남발하게 되면 효율성에서 치명적인 결함은 생기게 하기 때문에 꼭 필요한 곳이 아니라면 사용하지 않아야 한다.
자.. 마지막으로 또 하나의 역활... 사용자의 요청을 무한 기다림으로 하면 안된다. 일정 시간동안 기다리다가 자원이 없으면 사용자에게 자원이 없음을 알려야 한다.
즉 사용자의 대여 요청이 들어 왔을 경우 최대값인 상태에서 풀에 하나도 없다면 줄것이 없기에 기다려야 하는데 이때 무한적으로 기다린다면 해당 요청은 반납이 될때 까지 기다려야 하는데 만일 이 시간이 10분 20분 이라면 이것은 실시간 처리라 할 수 없다.
따라서 사용자에게 자원이 없음을 알리고 이에 따른 행동을 유도하게 하여야 한다.
이상으로 기본적인 Pool Manager의 역활이고 추가할것이 있다면 여러분께서 해보길 바란다.
이번에는 Pool Manager가 관리할 객체에 대한 것이 제공할 기본 사항을 생각해 보자...
Pool Manager가 관리하기 위해서는 여러개의 관리용 메소드가 필요할 것이다.
필수적인 것으로 하나는 자기 자신의 생성을 위한 것이며 또하나는 자기 자신의 파괴를 위한 것이다.
부수적으로 효율적인 Thread 관리를 위해서 wait과 notify를 사용한 대기 상태와 running 상태를 할 것들이다.
이러한 것들은 최종 Thread에 따라 내용이 다른기 때문에 abstract class를 사용하여 만들었다.
이는 다음과 같이 선언만 하면 된다.
package rushipel.util.threadpool;
public abstract class ThreadObject {
abstract public void create() throws NotCreateException;
abstract public void destroy();
abstract public void waitThread();
abstract public void notifyThread();
} // end class ThreadObject
이를 상속 받은 클래서는 저런것을 처리해 주어야 한다.
이런 방식을 통해 다양한 변화를 줄수 있다...
자 이번에는 Pool Manager의 내용을 보면서 설명을 해 보겠다..
package rushipel.util.threadpool;
import java.io.*;
import java.net.*;
import java.util.*;
/*
* Park Kyoung Tae
* rushipel@hanmail.net , rushipel@thrunet.com
* 2002.03.25
*
* ThreadObject Pool을 관리한다.
*/
public abstract class PoolManager {
protected int initSize = 2; // 초기 Pool Size
protected int maxSize = 2; // 최대 Pool Size
protected int currentSize; // 현재 Pool Size
protected int usedCount = 0; // 현재 사용중인 갯수
protected int waitingTime = 2000; // Pool을 다 썼을 경우 대기 시간
protected java.util.Vector readyPool; // 사용 가능한 ThreadObject 관리
protected java.util.Vector pool; // 전체 ThreadObject 관리
private static PoolManager manager = null;
protected PoolManager() throws NotCreateException {
readyPool = new java.util.Vector();
pool = new java.util.Vector();
readProperties();
initPool();
System.out.println("현재 전체 " + currentSize + "개 Pooling 되어졌으며 " + usedCount + "개 사용중입니다.");
} // end private PoolManager()
/**
* 환경 설정 파일을 읽어 설정한다.
*/
private void readProperties() {
// Properties 파일을 얻어온다.
InputStream is = getClass().getResourceAsStream("/pool.properties");
Properties properties = new Properties();
int tmpInitSize;
int tmpMaxSize;
int tmpWaitingTime;
try {
// Properties 값을 통해 값을 설정해 준다.
properties.load(is);
tmpInitSize = Integer.parseInt(properties.getProperty("init_size", Integer.toString(initSize)));
tmpMaxSize = Integer.parseInt(properties.getProperty("max_size", Integer.toString(maxSize)));
tmpWaitingTime = Integer.parseInt(properties.getProperty("waiting_time", Integer.toString(waitingTime)));
initSize = tmpInitSize;
maxSize = tmpMaxSize;
waitingTime = tmpWaitingTime;
System.out.println("초기화 : init_size = " + initSize + ", max_size = " + maxSize + ", waiting_time = " + waitingTime);
} catch (Exception e) {
//e.printStackTrace();
System.out.println("properties file을 찾을수 없습니다.");
System.out.println("'pool.properties'파일을 CLASSPATH안에 만들어 주세요.");
System.out.println("Default 설정값으로 초기화 : init_size = " + initSize + ", max_size = " + maxSize + ", waiting_time = " + waitingTime);
} // end try
} // end private void readProperties()
/**
* 각종 설정값을 초기화 한다.
*/
private void initPool() throws NotCreateException {
// 초기값 만큼 풀을 생성한다.
ThreadObject obj = null;
currentSize = 0;
usedCount = 0;
for (int i = 0; i < initSize; i++) {
try {
obj = createThreadObject();
obj.create();
readyPool.add( obj );
pool.add( obj );
} catch (NotCreateException e) {} // end try
} // end for
if (currentSize == 0) {
// 하나도 생성하지 못했으면 예외를 던진다.
throw new NotCreateException("ThreadObject 객체를 하나도 생성하지 못했습니다.");
} // end if (currentSize == 0)
} // end private void initPool() throws NotCreateException
/**
* Thread Object를 생성한다.
*/
protected abstract ThreadObject createThreadObject() throws NotCreateException;
/**
* 전체 ThreadObject을 닫는다.
*/
private void destroyAll() {
System.out.println("전체 ThreadObject을 닫습니다.");
ThreadObject obj;
int length = pool.size();
for (int i = 0; i < length; i++) {
obj = (ThreadObject) pool.elementAt(i);
obj.destroy();
} // end for
} // end private destroyAll()
/**
* ThreadObject을 새로 연결한다.
*
* @throws NotCreateException
*/
public synchronized void reset() throws NotCreateException {
System.out.println("전체 ThreadObject을 재 설정 합니다.");
// 전체 ThreadObject을 닫는다.
destroyAll();
// 기본 풀들을 초기화 한다.
readyPool.clear();
pool.clear();
// 새롭게 풀들을 설정한다.
initPool();
} // end public void reset() throws NotCreateException
/**
* Pool에 있는 ThreadObject 하나를 얻어 온다.
* (만일 없으면 null을 리턴)
*
* @return Pool에 있는 ThreadObject
* @throws NotCreateException
*/
private ThreadObject getObject() throws NotCreateException {
ThreadObject obj = null;
if (readyPool.size() > 0) {
// readyPool에 남는 것이 있다면 할당한다.
obj = (ThreadObject) readyPool.firstElement();
readyPool.remove(0);
} // end if (readyPool.size() > 0)
else if (maxSize == 0 || currentSize < maxSize) {
// 남는것이 없고 현재 최대 개수만큼 생성되지 않았다면 새로 생성한다.
obj = createThreadObject();
} // end else if (maxSize == 0 || currentSize < maxSize)
if (obj != null) {
usedCount++;
} // end if (obj != null)
return obj;
} // end private ThreadObject getObject() throws NotCreateException
/**
* Pool에 있는 ThreadObject 하나를 얻어 온다.
* (만일 waitingTime값 만큼 기다린 다음 반납 된것이 없으면 null을 리턴)
*
* @return
* @throws NotCreateException
*/
public synchronized ThreadObject getThreadObject() throws NotCreateException {
ThreadObject obj = null;
long startTime = System.currentTimeMillis();
while ((obj = getObject()) == null) {
try {
// 기다리는 시간동안 wait() 한다.
// 하지만 기다리는 시간내에 반납되면 notify 하면 깨어난다.
// 따라서 기다리는 시간 이내에 깨어 날 수도 있다.
wait(waitingTime);
} catch (InterruptedException e) {}
if ((System.currentTimeMillis() - startTime) >= waitingTime) {
// 기다리는 시간이 넘었다면 null을 리턴
return null;
} // end if ((System.currentTimeMillis() - startTime) >= timeout)
} // end while ((obj = getObject()) == null)
System.out.println("현재 전체 " + currentSize + "개 Pooling 되어졌으며 " + usedCount + "개 사용중입니다.");
return obj;
} // end public synchronized ThreadObject getThreadObject()
/**
* ThreadObject를 Pool에 반납한다.
*
* @param obj 반납한 객체
*/
public synchronized void release(ThreadObject obj) {
readyPool.add(obj);
usedCount--;
//notify();
notifyAll();
System.out.println("반납 성공 : 현재 사용 개수 " + usedCount);
} // end public synchronized void release(ThreadObject obj)
} // end public abstract class PoolManager
생성자에서는 환경 설정 파일을 읽어서 설정 값들을 얻어오는 것이며 각종 객체의 생성과 초기화를 해주는 것이다.
이상한 점이 있다.. 이 클래스 또한 abstract class이다..
왜 그럴까...
protected abstract ThreadObject createThreadObject() throws NotCreateException;
이부분 때문에 그렇다.. 왜 abstract로 했을까....
실제 생성해야 할 Thread는 Extends ThreadObject implements Runnable를 한 것 일것이다. (거 말이상하네 '것 일것이다')
다형성을 위해서 실제 생성은 Runtime에 결정할 수 있게 이를 이렇게 설계한 것이다.
채팅을 처리하는 Thread Pool이 될수도 있고... 서블릿 엔진이 http 요청을 위한 Thread Pool일 수도 있고 FTP나 다양한 Pool로 사용될 수 있기에 이를 위해 필수 기능만을 코딩하고 변화로 될 수 있는 부분을 상속받아 처리하게 만든 것이다.
이러한 예제는 아래 나올것이다. 어떻게 상속받아 처리를 하는지에 대하여서는....
여기서 가장 중요한 것은 다음 2개 이다.
public synchronized ThreadObject getThreadObject()
public synchronized void release(ThreadObject obj)
둘다 동기화 처리를 해준다. 더 중요하다면 역시 대여를 해주는 getThreadObject라 할 수 있다..
Source를 봐 보자...
public synchronized ThreadObject getThreadObject() throws NotCreateException {
ThreadObject obj = null;
long startTime = System.currentTimeMillis();
long tmpTime
long time = waitingTime;
while ((obj = getObject()) == null) {
try {
// 기다리는 시간동안 wait() 한다.
// 하지만 기다리는 시간내에 반납되면 notify 하면 깨어난다.
// 따라서 기다리는 시간 이내에 깨어 날 수도 있다.
wait(time);
} catch (InterruptedException e) {}
tmpTime = System.currentTimeMillis();
if ((tmpTime - startTime) >= waitingTime) {
// 기다리는 시간이 넘었다면 null을 리턴
return null;
} // end if ((tmpTime - startTime) >= waitingTime)
time = tmpTime - startTime;
} // end while ((obj = getObject()) == null)
System.out.println("현재 전체 " + currentSize + "개 Pooling 되어졌으며 " + usedCount + "개 사용중입니다.");
return obj;
} // end public synchronized ThreadObject getThreadObject()
소스를 보면 while 문을 도는데 getObject()를 호출하여 null이 아닐때 까지 루프를 돈다...
그런데 무조건 루프를 도는 것이 아니다. 중간에 time 동안 wait()하고 있다..
주석을 보듯이 time동안 무조건 기다리는 것이 아니라 release()에서 notifyAll() 해 주면 깨어 나다.
깨어 날때 무조건적으로 getObject()를 한다고 그 반납된 자원을 가져 가는것이 아니다. 즉 다른 경쟁자들고 기다릴 수 있다..
그렇기 때문에 또 기다릴 수가 있다.. 그러다가 시간이 넘으면 null을 리턴하게 될 것이다.
좀더 좋은 방법은 예외를 발생시켜 예외 처리를 하면 될것이다. 이것두 여러분의 숙제로 내줄것이다. (정말 넘하네... 지가 좀 할것이지)
이것을 이해 하고 나머지 소스를 본다면 그리 어려울것이 없다...
중요한 것은 동기화 처리와 대여 반납에 대한 구현의 이해이다...
자 이제 실제 이것을 상속받아서 구현한 것들을 보자...
필자는 앞서 말한것 처럼 필자가 구현한 채팅 프로그램에서 불필요한 모든것을 다 버리고 필요한 부분만을 남겼다...
이것에 살을 붙혀 나간다면 효율적인 채팅 프로그램을 만들수 있을 것이다. (다음번에 시간이 난 다면 채팅 프로그램에 대한 강좌를 열수 있으나 장담을 할 수 없다.. 왜냐면 피곤하니까.... ㅡ.ㅡ)
먼저 위에 2개의 abstract class들을 상속 받은 넘들을 봐보자...
우선 ThreadObject를 상속 받은 ServerThread를 보자...
package rushipel.chat.server;
import java.io.*;
import java.net.*;
import rushipel.chat.common.*;
public class ServerThread extends rushipel.util.threadpool.ThreadObject implements Runnable {
private Socket mySocket; // 나의 소켓
private ObjectInputStream reciver; // 클라인트로 부터 오는 바이트 스트림
private ObjectOutputStream sender; // 클라이트로 보낼 바이트 스트림
private Thread runner; // 사용자 접속을 처리하는 Thread
private boolean isRun; // 현재 Thread의 running 여부
private Object lockObject; // 동기화 처리를 위해
// 생성자
ServerThread() {
lockObject = new Object();
} // end ServerThread()
/**
* 통신및 사용자와 관리할 각종 객체를 설정한다.
*
* @param socket Client와 통신을 할 Socket
* @throws Exception
*/
public void setData(Socket socket) throws Exception {
mySocket = socket;
reciver = new ObjectInputStream(mySocket.getInputStream());
sender = new ObjectOutputStream(mySocket.getOutputStream());
} // end public void setData(Socket socket) throw Exception
/**
* Thread를 초기화 한다.
*
* @throws NotCreateException
*/
public void create() throws rushipel.util.threadpool.NotCreateException {
waitThread();
} // end public void create() throws rushipel.util.threadpool.NotCreateException
/**
* Thread의 상태를 멈춘다.
*/
public void waitThread() {
try {
lockObject.wait();
} catch(Exception e) {}
} // end public void waitThread()
/**
* Thread의 상태를 활성화 한다.
*/
public void notifyThread() {
synchronized(lockObject) {
// lock을 푼다.
lockObject.notify();
} // end synchronized(lockObject)
if (runner == null) {
// Thread가 null이 라면 시작한다.
start();
} // end if (runner == null)
} // end public void notifyThread()
/**
* 최종화 처리를 한다.
*/
public void destroy() {
disconnect();
stop();
} // end public void destroy()
/**
* Thread를 시작한다.
*/
public void start() {
if (runner == null) {
// 새로 Thread를 생성하여 실행한다.
runner = new Thread(this, "Server Receiver");
isRun = true;
runner.start();
} // end if (runner == null)
else {
stop();
start();
} // end else
} // end public void start()
/**
* Thread를 종료한다.
*/
public void stop() {
if (runner != null) {
isRun = false;
runner.interrupt();
// Thread가 종료할때까지 대기한다.
try {
runner.join(1000);
} catch(Exception e) {}
// stop을 안불러 줘도 괜찮지만 확인 사살이다.
runner.stop();
} // end if (runner != null)
} // end public void stop()
/**
* 서버측 쓰레드 프로토콜로 정의된 것을 가지고 사용자와 통신
*/
public void run() {
TransObject aTransObject = null;
int protocol = -1;
while (isRun) {
synchronized (lockObject) {
// synchronized를 try 문 위에서 해주어야 한다.
// 순서가 바뀌면 lock에서 풀리지 못한다.
// 주의해야할 사항이다.
try {
aTransObject = (TransObject) reciver.readObject();
// 사용자의 명령이 무엇인지 알아 낸다.
protocol = aTransObject.getProtocol();
System.out.println(protocol);
switch (protocol) {
case 1000:
// 사용자 로그인 처리
send(1001, "접속 성공.");
break;
case 1040:
// 간단한 메시지 전달
send(1041, "받으시요... 받으시요.");
break;
} // end swtich (protocol)
// 자신의 자원을 양보하여 동시 사용을 높인다.
// 실제로 IO 부분에서 synchonize를 처리해 주지만 확인 사살 차원이다.
Thread.yield();
} catch(SocketException e) {
// 접속 종료
disconnect();
// break를 부르면 Thread가 종료한다.
//break;
} catch(IOException e) {
// 접속 종료
disconnect();
// break를 부르면 Thread가 종료한다.
//break;
} catch (Exception e) {
} // end try
} // end synchronized(lockObject)
} // end while(true)
} // end public void run()
/**
* 접속 종료
*/
private void disconnect() {
try {
// 소켓을 닫고 쓰레드를 종료한다.
try { if (reciver != null) reciver.close(); } catch (Exception e) {}
try { if (sender != null) sender.close(); } catch (Exception e) {}
try { if (mySocket != null) mySocket.close(); } catch (Exception e) {}
} catch(Exception e) {
} finally {
try {
// 순서에 주의
// 반납후 wait 하여 멈추게 한다.
ChatThreadPoolManager.getInstance().release(this);
waitThread();
} catch (Exception e) {}
} // end try
} // end private void disconnect()
/**
* 자신에게 메시지 전달
*
* @param protocol 전송할 Protocol
* @param sendObject 전송할 Data
*/
public void send(int protocol, Object sendObject) {
send( new TransObject(protocol, sendObject) );
} // end public void send(int protocol, Object sendObject)
/**
* 자신에게 메시지 전달
*
* @param aTransObject 전송할 객체
*/
public void send(TransObject aTransObject) {
try {
sender.reset();
sender.writeObject(aTransObject);
} catch(IOException e) {
} // end try
} // end public void send(TransObject aTransObject)
} // end class ServerThread
역시 Pool Manager의 관리를 위해 extends ThreadObject를 하였다..
또한 Thread의 역활을 위해서 implements Runnable을 하였다...
그럼 이 넘은 ThreadObject의 abstract 메소드들을 어떻게 구현 했는지 알아보자...
create()는 내부에서 단순히 waitThread()를 호출하였다.
즉 생성기 대기 상태라 있으라는 것이다.
채팅을 위해서는 사용자의 접속을 해서 메시지가 왔다 갔다 해야 하는데 아직 접속도 안한 상태이기 때문에 그냥 기다리는 것이다.
다음은 waitThread()을 보면 lockObject.wait() 하였다.. lockObject는 효율적인 Thread의 사용을 위해 lock 객체를 만들고 이를 run() 메소드에서 동기화 처리를 하면서 무한 루프를 도는 것이다.
하지만 무조건 루프를 돌면 CPU 자원을 잡아 먹게 된다. 따라서 lockObject가 wait을 걸어 놓으면 해당 Thread는 잠겨 있다.. 따라서 cpu 자원을 사용하지 않느다.
이렇게 해서 생성해 놓은 Thread는 잠을 자고 lockObject.notify() 해주면 깨어날 것이다.
이번에는 notifyThread()인데 내부를 보니 lockObject.notify() 이것을 불러 Thread가 움직이게 한다. 위의 PoolManager에서는 notifyAll() 해주었다.
PoolManager는 동시에 여러 요청을 처리하는 것이지만 이 ServerThread는 자기 자신만 책임 지면 되기 때문에 자기 자신만 깨우면 된다.
마지막으로 destroy()는 disconnect()를 불러 접속을 종료하고 stop()을 불러 Thread를 종료 시켜 주는 것이다.
다음 중요한 실제 수행중인 run() 메소드를 보자..
중요한 것은 순서이다.
먼저 while이 오고 synchronized (lockObject) 그 다음이 try 이다..
루프는 종료 될때 까지 돌아야 한다. 루프를 빠져 나가면 해당 Thread는 종료하는 것이다.
종료는 단지 destroy()에서만 해주어야 한다. 임의적으로 루프를 종료 하지 못하게 만들어야 한다.
그다음 lock을 걸구 풀기 위해 synchronized (lockObject)를 루프 전 범위에 걸어 주어야 한다.
만일 try 구문 밑에 걸어 놓으면 예외가 발생하면 synchronized 블럭을 벗어 날것이다.
이렇게 되면 이 Thread를 관리할 수가 없다...
이렇기 때문에 순서에 유의 하라는 것이었다..
자 마지막으로 반납을 할 시기이다.. 반납은 접속 종료를 할때 반납한다.
중요한 것은 finally 절에서 무조건 반납을 할 수 있게 한다. 역시 순서에 유의 하라..
ChatThreadPoolManager.getInstance().release(this)
waitThread()
먼저 반납을 하고 자기 자신을 멈추는 것이다...
자신을 멈추고 반납을 할 수 없다...
나머지 소스에 대해서는 살펴 보길 바란다.. 이를 응용하면 채팅 프로그램을 완료할 수 있다.
다음으로 PoolManager를 상속 받은 ChatThreadPoolManager를 보자...
import rushipel.util.threadpool.*;
public class ChatThreadPoolManager extends PoolManager {
/** Singleton 기법을 위해 */
static private ChatThreadPoolManager aChatThreadPoolManager;
/**
* 생성자
* @throws NotCreateException
*/
private ChatThreadPoolManager() throws NotCreateException {
super();
} // end private ChatThreadPoolManager() throws NotCreateException
/**
* 자기자신의 참조 하나 만을 만든다.
* @return 자기자신의 Singlton 객체
* @throws NotCreateException
*/
public static synchronized ChatThreadPoolManager getInstance() throws NotCreateException {
if (aChatThreadPoolManager == null) {
aChatThreadPoolManager = new ChatThreadPoolManager();
} // end public static synchronized ChatThreadPoolManager getInstance() throws NotCreateException
return aChatThreadPoolManager;
} // end public static synchronized ChatThreadPoolManager getInstance() throws NotCreateException
/**
* 새로운 ThreadObject 객체를 얻어온다.
*
* @return 생성된 ThreadObject 객체
* @throws NotCreateException
*/
protected synchronized ThreadObject createThreadObject() throws NotCreateException {
ThreadObject obj = new ServerThread();
obj.create();
pool.add( obj );
currentSize++;
return obj;
} // end private ThreadObject createThreadObject() throws NotCreateException
}
여기서 중요한 것은 createThreadObject() 이다...
RunTime에 자신에 맞는 ThreadObject를 생성하는 것이다...
ThreadObject obj = new ServerThread();
그 외에는 Singleton 기법을 사용하는 것에 유념하자...
여기까지 왔으면 한가지 의문점이 생길것이다.. 분명히 create() 하면 단지 waitThread()을 호출해 Thread를 대기 상태로 만드는데... 어디서 notifyThread()을 해 줄까....
ServerThread class를 보면 Socket을 통해 데이터를 주고 받는 것이다.. 그럼 이러한 Socket이 필요하다.. 즉 사용자의 접속이 있어야만 해당 Thread가 작업을 할 수 있을것이다.
따라서 사용자의 접속 요청이 있을경우 Pool Manager에서 Thread을 얻어 온 뒤 사용자의 Socket을 설정해 주면서 notifyThread() 할 것이다.
실제 서버의 진입 클래스인 ChatServer class를 봐 보자..
package rushipel.chat.server;
import java.io.*;
import java.net.*;
import rushipel.util.threadpool.*;
public class ChatServer implements Runnable {
private ChatThreadPoolManager aChatThreadPoolManager; // ThreadPool을 관리하는 관리자
private ServerSocket aServerSocket; // 사용자 접속 소켓
private Thread runner; // 사용자 접속을 처리하는 Thread
private boolean isRun; // 현재 Thread의 running 여부
/**
* 생성자
*/
public ChatServer() {
try {
aChatThreadPoolManager = ChatThreadPoolManager.getInstance();
aServerSocket = new ServerSocket(9452);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
} // end try
} // end public ChatServer()
/**
* Thread를 시작한다.
*/
public void start() {
if (runner == null) {
runner = new Thread(this, "Chat Server");
isRun = true;
runner.start();
} // end if (runner == null)
else {
stop();
start();
} // end else
} // end public void start()
/**
* Thread를 종료한다.
*/
public void stop() {
if (runner != null) {
isRun = false;
runner.interrupt();
try { runner.join(); } catch (Exception e) {}
runner = null;
} // end if (runner != null)
} // end public void stop()
/**
* 사용자 접속을 처리한다.
*/
public void run() {
ServerThread aServerThread = null; // 사용자를 처리하는 서버 Thread
Socket aSocket = null; // 사용자의 소켓
while (isRun == true) {
try {
aSocket = aServerSocket.accept();
aServerThread = (ServerThread) aChatThreadPoolManager.getThreadObject();
System.out.println("User Accept");
if (aServerThread != null) {
// Pool에서 얻어 왔으면
aServerThread.setData(aSocket);
aServerThread.notifyThread();
} // end if (aServerThread != null)
else {
sendFullMessage(aSocket);
} // end else
runner.yield();
} catch (Exception e) {
e.printStackTrace();
} // end try
} // end while (isRun == true)
if (aServerSocket != null) {
try { aServerSocket.close(); } catch (IOException e) {}
} // end if (aServerSocket != null)
} // end public void run()
/**
* 사용자가 가득차 있다는 메시지를 전달한다.
*
* @param socket 전송할 사용자의 Sockt 정보
*/
public void sendFullMessage(Socket socket) {
ObjectOutputStream sender = null;
try {
sender = new ObjectOutputStream(socket.getOutputStream());
sender.reset();
sender.writeObject(new rushipel.chat.common.TransObject(1051, "최대 접속량을 넘어섰습니다.\n잠시후 다시 접속해 주세요."));
System.out.println("Server Busy");
} catch (Exception e) {
} finally {
try { if (sender != null) sender.close(); } catch (Exception e) {}
try { if (socket != null) socket.close(); } catch (Exception e) {}
} // end finally
} // end public void sendFullMessage(Socket socket)
public static void main(String[] arg) {
ChatServer aChatServer = new ChatServer();
aChatServer.start();
System.out.println("Server Start");
} // end public static void main(String[] arg)
} // end class ChatServer
위의 소스에서 중요하게 볼 부분은 run() 메소드 이다..
aSocket = aServerSocket.accept();
aServerThread = (ServerThread) aChatThreadPoolManager.getThreadObject();
if (aServerThread != null) {
// Pool에서 얻어 왔으면
aServerThread.setData(aSocket);
aServerThread.notifyThread();
} // end if (aServerThread != null)
else {
sendFullMessage(aSocket);
} // end else
먼저 사용자가 접속을 하면 Socket을 얻는다.. ThreadPool로 부터 Thread를 얻는다..
만일 Thread를 얻어오면 소켓 정보를 설정해 주고 실제 Thread를 돌린다.
그렇지 않고 Thread를 얻어 오지 못했다면 사용자에게 안내문을 보낸다..
이것으로 허접하지만 Thread Pool에 대한 강좌를 끝내기로 한다...