13장쓰레드

 

▪ 멀티쓰레딩(Multi-Threading)의 이해

 

- 멀티태스킹(Milti-Tasking): 하나의 운영 체제가 여러 개의 프로그램을 동시에 실행하는 것.

- 선점형(Pre-emptive) 멀티태스킹: 운영 체제가 제어권을 갖고 CPU의 시간을 분할하여(Time-Slice) 여러 개의프로그램을 동시에 실행하는 것으로 Windows 95 이후에 채택된 방식.

 

- 프로세스(Process): 하나의 응용 프로그램이 실행되는 동안 사용하는 독립적인 메모리 분할.

 * TLS(Thread Local Storage): 쓰레드 자신의 마지막 작업 내용을 저장하는 곳.

  * Call Stack: 쓰레드 내부의 메소드 호출 순서 및 메소드에서 사용하는정보를 저장하는 곳.

  * Context Switching: 쓰레드가 TLS에 정보를 저장하거나 TLS로부터 정보를복구하는 일.

 

 

 

- 쓰레드(Thread): 프로세스 내의 작업 처리 단위로 하나의 프로세스는 최소한 하나 이상의 쓰레드를 갖는다.

- 멀티쓰레딩(Multi-Threading):하나의 프로세스 내에서 여러 개의 쓰레드를 동시에 수행하는 것.

 

- .NET Framework에서는 하나의 프로세스 내에 여러 개의 Application Domain을 포함할 수 있어 응용 프로그램간의통신이 쉬워졌고 메모리를 보다 효율적으로 사용할 수가 있다.

 

     

▪ 자바의 쓰레드

 

- 언어 차원에서 쓰레드를 지원한다.

-java.lang.Thread 클래스를 사용하는 방식(일반적인 경우)과 java.lang.Runnable 인터페이스를 구현하는 방식(다중상속이 필요한 경우)으로 나뉜다.

 

Thread 클래스 사용법

 

Thread 클래스를 상속받아run() 메소드를 오버라이드한다.

자식 클래스 객체를 생성해서start() 메소드를 호출한다.run() 메소드를 직접 호출할 수는 없다.

 

  Test t = new Test();

  t.start();

 

run() 메소드와 start() 메소드의 관계

 

- start() 메소드를 호출하면 VM은 해당 쓰레드를 준비(Ready) 상태로 바꾸고, VM 내의 쓰레드 스케줄러가 run()메소드를 호출한다(교재 392쪽 그림).

- 프로그래머가 run() 메소드를 직접 호출할 수는 없다.

 

 

 

 

Runnable 인터페이스 사용법

 

Runnable 인터페이스를 구현해서 run() 메소드를 오버라이드 한다.

② 자식 클래스 객체를 생성해Thread 클래스 생성자에 전달하고, 그때 만들어진 Thread 객체를 사용해 start() 메소드를호출한다. run() 메소드를 직접 호출할 수는 없다.

 

  Test t = new Test();

  Thread th = new Thread(t);

  th.start();

 

 

▪ 쓰레드의 제어

 

- 쓰레드는 4가지 상태로 구분(교재 398쪽 그림).

 

① 준비(Ready):실행(Run) 상태에 들어가기 전 단계로 start() 메소드에 의해 쓰레드는 준비 상태가 된다.

② 실행(Run): 준비 상태에 있는 쓰레드가 쓰레드 스케줄러에 의해 선택되면 run() 메소드가 호출되고 쓰레드는 실행상태가 된다.

③ 종료(Done):run() 메소드가 끝나면 쓰레드는 종료 상태가 된다.

  한번 종료된 쓰레드는 다시 실행 상태로 되돌아 갈 수 없으며, 쓰레드가 종료되었다고 쓰레드 객체가 메모리에서 사라지는 것은 아니다.

④ 실행정지(Non-Runnable): 실행이 잠시 멈춰진 상태로 대기(Waiting), 슬립(Sleeping), 지연(Blocked) 상태가 있다.

 

 • 대기(Waiting): 동기화(Synchronized) 블록 내에서 wait() 메소드에 의해 쓰레드가 멈춰진 상태.

   멈춰진 쓰레드는 다른 쓰레드(제3의 쓰레드)가 nofity() 메소드나 notifyAll() 메소드를 호출해 줘야 원래 상태로되돌아 갈 수 있다.

 • 슬립(Sleeping): 지정된 시간 동안 쓰레드가 멈춰진 상태.

 • 지연(Blocked): 입출력 관련 메소드가 사용되는 동안 쓰레드가 멈춰진 상태.

 

- 실행정지 상태가 끝나면 쓰레드는 실행(Run) 상태가 아니라 준비(Ready) 상태로 되돌아 간다.

 

▪ 메소드 호출에 의한 쓰레드 제어

 

- start() 메소드: 쓰레드를 준비(Ready) 상태로 만든 후 쓰레드 스케줄러에 의해 선택되기를 기다린다.

- run() 메소드: 쓰레드에서 실질적으로 수행하는 코드를 정의한다(오버라이드).

- yield() 메소드: 다른 쓰레드에게실행을 양보하고 자신을 대기(Waiting)상태로 만든다.

- sleep() 메소드: 지정된 시간 동안 쓰레드를 슬립(Sleep) 상태로 만든다.

 

▪ 지연(Blocked)

 

- 입출력 관련 메소드가 사용되는 동안 쓰레드가 멈춰진 상태.

 

▪ 대기(Waiting)

 

- 동기화(Synchronized) 블록 내에서 wait() 메소드에 의해 쓰레드가 멈춰진 상태.

 멈춰진 쓰레드는 다른 쓰레드(제3의 쓰레드)가 nofity() 메소드나 notifyAll() 메소드를 호출해 줘야 원래 상태로되돌아 갈 수 있다(실행(Run) 상태가 아니라 준비(Ready) 상태로 되돌아 감).

 

▪ 동기화 메소드

 

- 동기화(Synchronized):하나의 리소스를 여러 개의 쓰레드가 동시에 사용할 때 특정 시점에서는 한 개의 쓰레드만이리소스에 접근할 수 있도록 해 주는 것이다. 이때 다른 쓰레드들은 지연(Blocked) 상태가 된다.

- 메소드에synchronized 예약어를 붙여 준다.

- Deadlock: 두 개의 쓰레드가 서로에 의해 영원히 잠겨지는 상태. 이런 경우에는 윈도 작업관리자를 통해 프로세스를강제 종료해 줘야 한다.

- Race Condition: 여러 개의 쓰레드가 동일한 데이터를 조작하는 경우 발생할 수 있는 오류.

 

▪ 동기화 메소드와 모니터

 

- 모니터(Monitor): 특정 객체를 독점해 사용할 수 있는 권한을 말하며 모든 객체는 모니터를 하나씩 가지고 있다.

- 쓰레드가 동기화 메소드를 사용하기 위해서는 우선 해당 객체에 대한 모니터를 얻어야 한다.

- 쓰레드가 해당 객체에 대한 모니터를 얻었다면 다른 쓰레드들은 해당 객체의 동기화 메소드를 사용할 수 없다(다른일반 메소드는 얼마든지 사용 가능).

 

◊ 코드3

 

- 교재의 실행 결과와 Eclipse의 실행 결과는 서로 다를 수 있고, 이 코드에서는 synchronized 예약어를 붙이나 붙이지 않으나 결과에는 차이가 없다(홈페이지 예제로 보충).

 

<MainData.java 파일>

- 5~10번 행: up() 메소드를 동기화 메소드로 정의한다.

- 8번 행: data 필드의 값을 하나 증가.

- 12~17번 행: down() 메소드를 동기화 메소드로 정의한다.

- 15번 행: down 필드의 값을 하나 감소.

- 25,26번 행: Thread 클래스를 상속받은 IncThread 클래스 객체와 DecThread 클래스 객체를 생성한다.

 두번째 파라미터에 지정된 2와 3 값은 동기화 메소드의 호출 회수(up() 메소드 2번 호출, down() 메소드 3번 호출).

- 28,29번 행: start() 메소드를 호출해 쓰레드를 실행한다.

 

<DecThread.java 파일>

- 1번 행: Thread 클래스를 상속.

- 7~12번 행: DecThread 오버로드 생성자.

- 14~25번 행: 쓰레드에서 실질적으로 수행하는 코드를 run() 메소드에 정의(오버라이드)한다.

- 23번 행: MainData 객체의 down() 동기화 메소드 호출.

 

<IncThread.java 파일>

- 23번 행: MainData 객체의 up() 동기화 메소드 호출.

 

class MainData
{
 int data;

 public synchronized void up(String name) //예약어 동기화(synchronized) void up메소드정의, 순차적으로 하겠다.
 {
  System.out.print(name + "가" + data);
  data++;
  System.out.println("를" + data + "로 증가시킴");
 }

 public synchronized void down(String name)
 {
  System.out.print(name + "가" + data);
  data--;
  System.out.println("를" + data + "로 감소시킴");
 }

 public static void main(String[] args)
 {
  MainData m = new MainData();
  IncThread t1;
  DecThread t2;

  t1 = new IncThread(m,2,"증가 쓰레드"); //for 문 호출2
  t2 = new DecThread(m,3,"감소 쓰레드");

  t1.start(); //IncThread 이거부터 실행해라~ 근데 먼저실행시켜다구 먼저 실행안할수두 있다.
  t2.start();
 }
}
  
// DecThread.java
class DecThread extends Thread { //run오버라이딩해야한다.
 
 MainData m;
 int toAdd;
 String name;

 DecThread(MainData md,int to,String n)
 {
  m = md;
  toAdd = to;
  name = n;
 }

 public void run()
 {
  for(int i = 0; i<toAdd; i++)
  {
   try{
    sleep(5);
   }catch (Exception e) {
   }

   m.down(name);
  }
 }
}

//IncThread.java
class IncThread extends Thread { //run 기억하기!!!
  
 MainData m;
 int toAdd;
 String name;

 IncThread(MainData md,int to,String n)
 {
  m = md;
  toAdd = to;
  name = n;
 }

 public void run()
 {
  for(int i = 0; i<toAdd; i++)
  {
   try{
    sleep(5);
   }catch (Exception e) {
   }

   m.up(name);
  }
 }
}

/*
증가 쓰레드가0를1로 증가시킴
감소 쓰레드가1를0로 감소시킴
증가 쓰레드가0를1로 증가시킴
감소 쓰레드가1를0로 감소시킴
감소 쓰레드가0를-1로 감소시킴
*/

 

▪ 동기화 코드 블록의 사용

 

- 동기화 메소드는 객체의 메소드를 동기화 하는 것이고, 동기화 코드 블록은 특정 코드 및 객체를 동기화 하는 것이다.

- 동기화 메소드에 비해 세밀한 제어가 가능하다.

-synchronized(객체에 대한 레퍼런스) { ... } 구문을 사용한다.

 

 

wait(), notify(), notifyAll() 메소드의 사용

 

- 실행 중인 쓰레드가 wait() 메소드를 호출해 대기(Waiting) 상태로 들어가면 다른 쓰레드(제3의 쓰레드)가 notify()메소드나 notifyAll() 메소드를 호출해 대기 중인 쓰레드를 원래 상태로 되돌려 준다(실행(Run) 상태가 아니라 준비(Ready) 상태로 되돌려 줌).

 

◊ 코드5

 

- 생산자/소비자 패턴을 구현한 예제.

 

import java.util.Random;

class ConThread extends Thread
{
 String name;
 Que que;

 public ConThread(Que q,String str)
 {
  name = str;
  que = q;
 }

 public void run()
 {
  String str;
  Random r = new Random();

  for(int i=0; i<3; i++)
  {
   try{
    sleep(r.nextInt(100));
   } catch (InterruptedException e) {
   }

   str = que.get();
   System.out.println(name + " : " + str);
  }
 }

 public static void main(String[] args)
 {
  Que q = new Que();

  ProThread p1 = new ProThread(q,"생산자1");
  ConThread c1 = new ConThread(q,"소비자1");

  p1.start();
  c1.start();
 }
}

// Que.java 중간다리역활..생산자와 소비자!!!
class Que
{
 String msg; //문자열저장필드
 boolean bMsg = false; //문자열이 전달되는지 확인하는 필드

 public synchronized String get() //get()는 Q객체로부터 갖져오는거
 {
  while(bMsg == false) { //아직문자열을 갖고있지않다면...
   try{
   wait();
   } catch (InterruptedException e) {
   }
  }
  bMsg = false; //미리

  notifyAll(); //현재대기중인 쓰레드 깨워주는거!!! 소비자쓰레드~~~
  return msg; //소비자에게 전달
 }

 public synchronized void put(String msg) //put()는 Q객체에 넣은거...
 {
  while ( bMsg == true) { //문자열 넣을려구 했는데...이미 셋팅된상태..기둘려라.
   try{
    wait();
   }catch (InterruptedException e) {
   }
  }
  this.msg = msg;
  bMsg = true;

  notifyAll();
 }
}

// ProThread.java 생산자
class ProThread extends Thread { //run()오버라이딩

 String name;
 Que que;

 public ProThread(Que q, String str)
 {
  name = str;
  que = q;
 }
 public void run()
 {
  String str;
  Random r = new Random();

  for(int i = 0; i<3; i++)
  {
   try{
    sleep(r.nextInt(100));
   }catch (InterruptedException e) {
   }

   que.put("넣어준값 " + i);
   System.out.println(name + " : " + i);
  }
 }
}


/*
생산자1 : 0
소비자1 : 넣어준값 0
생산자1 : 1
소비자1 : 넣어준값 1
생산자1 : 2
소비자1 : 넣어준값 2
*/

 

<Que.java 파일>

- 생산자 객체와 소비자 객체를 이어주는 역할을 하는 객체로 생산자 객체로부터 문자열을 전달받아 저장하고 있다가

 소비자 객체에 전달한다.

- 4번 행: 생산자 객체로부터 Que 객체로 문자열이 전달되었는지를 확인하기 위한 필드. 문자열이 전달되었으면 true,

 문자열이 전달되지 않았거나 Que 객체가 가지고 있던 문자열이 빠져나갔으면 false(처음에는 문자열이 존재하지

 않으므로 false).

- 6~18번 행: get() 메소드를 동기화 메소드로 정의.

- 8~13번 행: Que 객체가 문자열을 가지고 있지 않다면 10번 행에서 wait() 메소드를 호출해 대기(Waiting) 상태로

 들어간다.

- 14~17번 행: 하지만 Que 객체가 문자열을 가지고 있다면 bMsg 필드를 false로 설정하고, notifyAll() 메소드를 호출해

 현재 대기(Waiting) 중인 쓰레드를 준비(Ready) 상태로 되돌려 준 후 소비자 객체에 문자열을 전달한다.

- 20~32번 행: put() 메소드를 동기화 메소드로 정의.

- 22~27번 행: Que 객체가 문자열을 가지고 있다면 24번 행에서 wait() 메소드를 호출해 대기(Waiting) 상태로 들어간다.

- 28~31번 행: 하지만 Que 객체가 문자열을 가지고 있지 않다면 생산자 객체로부터 문자열을 전달받아 msg 필드에

 저장하고, bMsg 필드를 true로 설정한 후 notifyAll() 메소드를 호출해 현재 대기(Waiting) 중인 쓰레드를 준비(Ready)

 상태로 되돌려 준다.

 

<ProThread.java 파일>

- 생산자에 해당하는 객체.

- 14~29번 행: 쓰레드에서 실질적으로 수행하는 코드를 run() 메소드에 정의(오버라이드).

- 22번 행: 임의의 시간만큼 Sleep.

- 26번 행: Que 객체의 put() 메소드를 호출해 문자열을 전달한다.

 

<ConThread.java 파일>

- 소비자에 해당하는 객체.

- 14~29번 행: 쓰레드에서 실질적으로 수행하는 코드를 run() 메소드에 정의(오버라이드).

- 22번 행: 임의의 시간만큼 Sleep.

- 26번 행: Que 객체의 get() 메소드를 호출해 문자열을 전달받는다.

- 35,36번 행: 책에 오타. "생산자","소비자"로 수정.

 

▪ 우선권 제어

 

- 쓰레드의 실행 순서는 쓰레드 스케줄러가 상황에 따라 결정한다.

- 먼저 실행한 쓰레드라 해서 먼저 끝난다는 보장은 하지 못한다.

- setPriority() 메소드를 사용해 쓰레드의 우선순위를 명시적으로 지정할 수 있다.

 

 

public class PriThread extends Thread  
{
 String name;

 public PriThread(String n)
 {
  name = n;
 }

 public void run()
 {
  for(int i=0;i<3;i++)
   System.out.println(name + "가 출력합니다");
 }

 public static void main(String[] args)
 {
  PriThread p1,p2,p3;

  p1 = new PriThread("제일 높은 쓰레드");
  p2 = new PriThread("보통 쓰레드");
  p3 = new PriThread("제일 낮은 쓰레드");

  p1.setPriority(p1.MAX_PRIORITY);
  p2.setPriority(p1.NORM_PRIORITY);
  p3.setPriority(p1.MIN_PRIORITY);

  p3.start();
  p2.start();
  p1.start();
 }
}

/*
제일 높은 쓰레드가 출력합니다
보통 쓰레드가 출력합니다
제일 높은 쓰레드가 출력합니다
제일 낮은 쓰레드가 출력합니다
제일 높은 쓰레드가 출력합니다
보통 쓰레드가 출력합니다
보통 쓰레드가 출력합니다
제일 낮은 쓰레드가 출력합니다
제일 낮은 쓰레드가 출력합니다
*/

◊ 코드6

 

- 24~26번 행: setPriority() 메소드를 사용해 쓰레드의 우선순위를 지정. p1 객체의 우선순위가 제일 높다.

- 28~30번 행: 가장 우선순위가 낮은 p3 객체의 쓰레드부터 실행하지만 결과는 가장 우선순위가 높은 p1 객체의

 쓰레드부터 실행된다.

 

========================================================================================

 

쓰레드(Thread)

1. Thread 클래스 

 class MyThread extends Thread {
     String str;
     long msec;
 
     MyThread(String str, long msec) {
         this.str = str;
         this.msec = msec;
     }
 
     public void run() {
         try {
             while (true) {
                 Thread.sleep(msec);
                 System.out.println(str);
             }
         }
         catch (Exception ex) {}
     }
 }
 
 class Test {
     public static void main(String args[]) {
         MyThread ta = new MyThread("A", 1000); // 1초마다A출력.
         MyThread tb = new MyThread("B", 3000); // 3초마다B출력.
 
         ta.start();
         tb.start();
     }
 }



2. Runnable 인터페이스 

 class MyThread implements Runnable {
     String str;
     long msec;
 
     MyThread(String str, long msec) {
         this.str = str;
         this.msec = msec;
     }
 
     public void run() {
         try {
             while (true) {
                 Thread.sleep(msec);
                 System.out.println(str);
             }
         }
         catch (Exception ex) {}
     }
 }
 
 class Test {
     public static void main(String args[]) {
         MyThread ta = new MyThread("A", 1000); // 1초마다A출력.
         MyThread tb = new MyThread("B", 3000); // 3초마다B출력.
 
         Thread th1 = new Thread(ta);
         Thread th2 = new Thread(tb);
 
         th1.start();
         th2.start();
     }
 }



3. 동기화 메소드 1

 class A extends Thread {
     Test t;
     int j;
     String name;
 
     A(Test td, int k, String s) {
         t = td;
         j = k;
         name = s;
     }
 
     public void run() {
         for (int i=0; i<j; i++) {
             try {
                 Thread.sleep(1000);
             } catch (Exception e) {}
 
             t.f(name);
         }
     }
 }
 
 class B extends Thread {
     Test t;
     int j;
     String name;
 
     B(Test td, int k, String s) {
         t = td;
         j = k;
         name = s;
     }
 
     public void run() {
         for (int i=0; i<j; i++) {
             try {
                 Thread.sleep(1);
             } catch (Exception e) {}
 
             t.f(name);
         }
     }
 }
 
 class Test {
     int data;
 
     public synchronized void f(String name) {
         System.out.print(name + "" + data);
         data++;
         System.out.println("() " + data + "증가");
 
         try {
             Thread.sleep(1000);
         } catch (Exception e) {}
     }
 
     public static void main(String[] args) {
         Test t = new Test();
         A th1;
         B th2;
 
         th1 = new A(t, 10, "A쓰레드");
         th2 = new B(t, 10, "B쓰레드");
 
         th1.start();
         th2.start();
     }
 }



4. 동기화 메소드 2

 class Account {
     private int balance = 0;
 
     synchronized void deposit(int amount) { //입금deposit
         balance += amount;
     }
 
     int getBalance() {
         return balance; //현재잔고리턴
     }
 }
 
 class Customer extends Thread {
     Account account;
 
     Customer(Account account) {
         this.account = account;
     }
 
     public void run() {
         try {
             for (int i=0; i<10; i++) {
                 account.deposit(100); // 100원씩10입금.
             }
         }
         catch (Exception e) {}
     }
 }
 
 class Test {
     public static void main(String args[]) {
         Account account = new Account(); //계좌생성.
         Customer[] customers = new Customer[5]; //쓰레드5생성.
 
         for (int i=0; i<5; i++) {
             customers[i] = new Customer(account);
             customers[i].start(); //5개의쓰레드시작.
         }
 
         for (int i=0; i<5; i++) {
             try {
                 customers[i].join();
                 // 5개의쓰레드가모두종료되기를기다린다.
             }
             catch (Exception e) {}
         }
 
         System.out.println(account.getBalance()); //계좌의잔액표시.
     }
 }



5. 동기화 블록 

 class Account {
     private int balance = 0;
 
     void deposit(int amount) {
         synchronized (this) {
             balance += amount;
         }
     }
 
     int getBalance() {
         return balance;
     }
 }
 
 class Customer extends Thread {
     Account account;
 
     Customer(Account account) {
         this.account = account;
     }
 
     public void run() {
         try {
             for (int i=0; i<10; i++) {
                 account.deposit(100); // 100원씩10입금.
             }
         }
         catch (Exception e) {}
     }
 }
 
 class Test {
     public static void main(String args[]) {
         Account account = new Account();
         Customer[] customers = new Customer[5]; //쓰레드5생성.
 
         for (int i=0; i<5; i++) {
             customers[i] = new Customer(account);
             customers[i].start(); // 5개의쓰레드시작.
         }
 
         for (int i=0; i<5; i++) {
             try {
                 customers[i].join();
             }
             catch (Exception e) {}
         }
 
         System.out.println(account.getBalance());
     }
 }



6. join() 메소드 

 class ThreadAA extends Thread {
     public void run() {
         try {
             for (int i=0; i<3; i++) {
                 Thread.sleep(1000);
                 System.out.println("ThreadAA");
             }
         }
         catch (InterruptedException ex) {
             ex.printStackTrace();
         }
     }
 }
 
 class ThreadBB extends Thread {
     public void run() {
         try {
             for (int i=0; i<10; i++) {
                 Thread.sleep(1000);
                 System.out.println("ThreadBB");
             }
         }
         catch (InterruptedException ex) {
             ex.printStackTrace();
         }
     }
 }
 
 class Test {
     public static void main(String args[]) {
         ThreadAA ta = new ThreadAA();
         ta.start();
         ThreadBB tb = new ThreadBB();
         tb.start();
 
         try {
             ta.join();
             tb.join();
//쓰레드가모두종료되기를기다린다.
//
쓰레드여러개사용..한꺼번에끝낼!!!
 
             System.out.println("쓰레드가모두종료되었습니다.");
         }
         catch (Exception e) {
             e.printStackTrace();
         }
     }
 }

/*
ThreadAA

ThreadBB

ThreadAA

ThreadBB

ThreadAA

ThreadBB

ThreadBB

ThreadBB

ThreadBB

ThreadBB

ThreadBB

ThreadBB

ThreadBB

두 쓰레드가 모두 종료되었습니다.
*/


7. yield() 메소드 

 class Temp extends Thread {
     public void run() {
         for (int i=0; i<5; i++) {
             System.out.println("나도끼워줘");
         }
     }
 }
 
 class Test extends Thread {
     public void run() {
         for (int i=0; i<10; i++) {
             for (int j=0; j<10; j++) {
                 System.out.println(i + ":" + j);
                 yield(); //다른쓰레드에게실행을양보하고잠깐동안대기.
             }
         }
     }
 
     public static void main(String[] args) {
         Test t1 = new Test();
         Temp t2 = new Temp();
 
         t1.start();
         t2.start();
     }
 }

//이론은 0:0~0:9 다음에 문자열(나도 끼워줘)나와야 한다. 근데 쓰레드는 지멋대로다..ㅡ,.ㅡ;

/*
0:0

나도 끼워줘

0:1

나도 끼워줘

나도 끼워줘

나도 끼워줘

0:2

나도 끼워줘

0:3

0:4

0:5
..
*/

 

 

'Programming > JAVA' 카테고리의 다른 글

[펌] [SWING]JFileChooser  (0) 2005.06.29
[펌] 서버 소켓 예제  (0) 2005.06.24
[펌] 클라이언트 소켓  (0) 2005.06.24
[펌] 통신을 위한 THREAD 프로그래밍  (0) 2005.06.23
[펌] Swing  (0) 2005.06.17

Posted by 영웅기삼
,