티스토리 뷰

728x90

서문



자바의 정석 기초편 챕터 13편을 기재합니다.
목적은 공부한 내용을 기록하는 것에 있기 때문에 완전한 문장이 아닐 수도 있습니다.
또한 모든 내용을 적은 것은 아닙니다.









자바의 정석 (ch.13)



프로세스와 쓰레드(process & thread) (1/3)

  • 프로세스 : 실행 중인 프로그램, 자원(RESOURCE)과 쓰레드로 구성 (공장)

  • 쓰레드 : 프로세스 내에서 실제 작업을 수행. 모든 프로세스는 최소 하나의 쓰레드를 가지고 있음 (노동자)

    • 싱글쓰레드 프로세스 = 공장(resource) + 1명의 노동자
    • 멀티쓰레드 프로세스 = 공장(resource) + N명의 노동자

  • 공장 2 + 노동자 1 VS 공장 1 + 노동자 2 중 무엇이 더 효율적인가?

하나의 새로운 프로세스를 생성하는 것보다 하나의 새로운 쓰레드를 생성하는 것이 더 적은 비용이 든다.



멀티쓰레드의 장단점

장단점 특성
장점 - 시스템자원을 보다 효율적으로 사용가능
- 사용자에 대한 응답성(responseness)이 향상
- 작업이 분리되어 코드가 간결해짐
"여러 모로 좋다"
단점 - 동기화(synchronization)에 주의해야 함
- 교착상태(dead-lock)가 발생하지 않도록 주의
- 각 쓰레드가 효율적으로 고르게 실행될 수 있도록 해야 함
"프로그래밍할 때 고려해야 할 것이 많다"


쓰레드의 구현과 실행

  • Thread 클래스를 상속, 자바는 단일상속
  • Runnable 인터페이스를 구현 , 인터페이스는 다중상속가능
1. Thread 클래스 상속

class MyThread extends Thread{
  public void run(){ 작업할 내용 } // Thread클래스의 run()을 오버라이딩
}

MyThread mt = new MyThread(); // 쓰레드 생성
mt.start(); // 쓰레드 실행
--------------------------

2. Runnable 인터페이스를 구현

class MyThread2 implements Runnable{
  public void run(){ 작업할 내용 } // Runnable인터페이스의 추상메서드 run()을 구현
}

Runnable r = new MyThread2();
Thread t = new Thread(r);  // Thread t = new Thread(new MyThread2());

t.start(); // 쓰레드 실행

--------------------------

3. 예제

class Ex{
  public static void main(String[] args){
    ThreadEx1 te1 = new ThreadEx1();

    te1.start(); // Thread를 상속받은 Thread 실행

    Runnable r = new ThreadEx2();
    Thread te2 = new Thread(r);

    te2.start(); // 인터페이스 구현한 Thread 실행
  }
}

class ThreadEx1 extends Thread{
  public void run(){
    for(int i=0; i<5; i++){
      System.out.println(getName()); // 조상인 Thread의 getName()을 호출
    }
  }
}

class ThreadEx2 implements Runnable{
  public void run(){
    for(int i=0; i<5; i++){
      // Thread.currentThread()는 Thread 객체를 반환
      System.out.println(Thread.currentThread().getName()); // 조상인 Thread의 getName()을 호출
    }
  }
}


> Thread-0 
> Thread-0
> Thread-0
> Thread-0
> Thread-0
> Thread-1
> Thread-1
> Thread-1
> Thread-1
> Thread-1 // 순서가 보장되지 않음


쓰레드의 실행 - start()

  • 쓰레드를 생성 후 start()를 호출해야 쓰레드가 시작
  • 바로 실행되는 것이 아니라 OS의 스케쥴러가 결정
  • 먼저 실행했다고 먼저 실행되는 것도 아님


start(), run()

  • 우리가 구현한 것은 run인데 왜 start를 호출할까?


    1. start()를 호출하면 새로운 콜스택이 생김
    1. 새롭게 생긴 콜스택에 run()이 생김
    1. start()는 종료되고 run()이 실행
    1. 이 때, main과 새로운 콜스택의 실행은 독립적


main쓰레드

  • main메서드의 코드를 실행하는 쓰레드를 main쓰레드라고 함
  • 쓰레드는 사용자쓰레드데몬쓰레드(보조쓰레드) 두 종류가 있음
  • 만약 멀티쓰레드 환경에서 main쓰레드가 종료되도 다른 콜스택에 쓰레드가 있다면 프로그램이 종료되지 않음

실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료됨

class Ex{
  static long startTime = 0;

  public static void main(String[] args){
    ThreadEx1 th1 = new ThreadEx1();
    ThreadEx2 th2 = new ThreadEx2();

    th1.start();
    th2.start();

    startTime = System.currentTimeMillis();

    try{
      th1.join(); // main쓰레드가 th1의 작업이 끝날 때까지 기다림
      th2.join();
    } catch(InterruptedException e){}

    System.out.print("소요시간 : " + (System.currentTimeMillis() - startTime)); 
  }
}


class ThreadEx1 extends Thread{
  public void run(){
    for(int i=0; i<300; i++){
      System.out.print(new String("-"));
    }
  }
}

class ThreadEx2 implements Thread{
  public void run(){
    for(int i=0; i<300; i++){

      System.out.println(new String("|"));
    }
  }
}

>---|--|-||||||----- .... 소요시간 10 // th1.join이 있기 때문에 main쓰레드가 다른 쓰레드를 기다려줌 만약 join이 없다면 소요시간이 먼저 출력


싱글쓰레드 vs 멀티쓰레드

  • 멀티쓰레드할 때 context switching이 발생하기 때문에 시간이 더 걸릴 수 있음
  • 그러나 채팅을 하면서 파일을 주고받는 상황에서 파일을 받을 때 채팅을 할 수 없다면?
  • 시간을 희생하더라도 필요한 상황에서 멀티쓰레드를 사용하는 것은 매우 유용하고 필수적
1. 싱글쓰레드 

class TestThread{
  public static void main(String[] args){

    for(int i = 0; i<50; i++){
      System.out.print("-");
    }

    for(int i = 0; i<50; i++){
      System.out.print("|");
    }
  }
}


> -----|||||

----------------------------------------

2. 멀티쓰레드

class TestThread{
  public static void main(String[] args){

    ThreadEx1 th1 = new ThreadEx1();
    ThreadEx2 th2 = new ThreadEx2();

    th1.start();
    th2.start();

  }
}


class ThreadEx1 extends Thread{
  public void run(){
    for(int i=0; i<300; i++){
      System.out.print(new String("-"));
    }
  }
}

class ThreadEx2 implements Thread{
  public void run(){
    for(int i=0; i<300; i++){

      System.out.println(new String("|"));
    }
  }
}


쓰레드의 I/O 블락킹(blocking)

  • 입출력시 작업이 중지되는 현상
import javax.swing.JOptionPane

class Ex{
  public static void main(String[] args){
    String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
    System.out.println("입력하신 값은 " + input + " 입니다.");

    for(int i=10; i>0; i--){
      System.out.println(i);
      try{
        Thread.sleep(1000); // 1초 지연
      }catch(Exception e){}
    }
  }
}


> 입력을 끝내지 않으면 시간을 카운트하는 함수가 실행되지 않는다
---------------------------------------

class Ex{
  public static void main(String[] args){
    ThreadEx1 th = new ThreadEx1();
    th.start();

    String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
    System.out.println("입력하신 값은 " + input + " 입니다.");
  }
}

class ThreadEx1 extends Thread{
  public void run(){
    for(int i=10; i>0; i--){
      System.out.println(i);
      try{
        Thread.sleep(1000);
      }catch(Exception e){}
    }
  }
}


> 입력이 끝나지도 않았지만 시간을 흐른다.


쓰레드의 우선순위(Priority of Thread)

  • 작업의 중요도에 따라 쓰레드의 우선순위를 다르게 하여 특정 쓰레드가 더 많은 작업시간을 갖게 할 수 있음
  • JVM에서는 우선순위를 (1~10)로 가지고 있고 참고로 WINDOWS OS는 32까지 우선순위가 있음
  • 이는 희망사항에 불가하기 때문에 OS 우선순위가 우선
  • 우선순위를 작을수록 더 중요한 것, 그리고 작다고 모든 작업이 보장될 수 없음, 먼저 끝날 확률이 큰 것
1. 쓰레드 우선순위에 대한 기본 메서드 및 멤버

void setPriority(int newPriority); // 쓰레드의 우선순위를 지정
int getPriority(); // 쓰레드의 우선순위를 반환

public static final int MAX_PRIORITY = 10; // 최대 우선순위
public static final int MIN_PRIORITY = 1; // 최소 우선순위
public static final int NORM_PRIORITY = 5; // 보통 우선순위

----------------------------------------------------------

2. 예제

class Ex{
  public static void main(String[] args){
    ThreadEx1 th1 = new ThreadEx1(); 
    ThreadEx2 th2 = new ThreadEx2();

    th2.setPriority(7); 

    System.out.println("Priority of th1(-) : " + th1.getPriority()); 
    System.out.println("Priority of th2(|) : " + th2.getPriority());
    th1.start(); 
    th2.start(); 

  }
}

class ThreadEx1 extends Thread{
  public void run(){
    for(int i = 0; i < 100; i++){
      System.out.println("-");
      for(int x = 0; i <10000000; x++);
    }
  }
}

class ThreadEx2 extends Thread{
  public void run(){
    for(int i = 0; i < 100; i++){
      System.out.println("|");
      for(int x = 0; i <10000000; x++);
    }
  }
}


> Priority of th1(-) : 5 // 기본적으로 5로 설정되어 있음
> Priority of th2(|) : 7
> ....


쓰레드 그룹

  • 서로 관련된 쓰레드를 그룹으로 묶어서 다루기 위한 것
  • 모든 쓰레드는 반드시 하나의 쓰레드 그룹에 포함되어 있어야 함
  • 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 main쓰레드 그룹에 속함
  • 자신을 생성한 쓰레드(부모 쓰레드)의 그룹과 우선순위를 상속받음
Thread(ThreadGroup group, String name)
Thread(ThreadGroup group, Runnable target)
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name, long stackSize)

ThreadGroup getThreadGroup() // 쓰레드 자신이 속한 그룹을 반환, Thread 클래스
void uncaughtException(Thread t, Throwable e) // 처리되지 않은 예외에 의해 쓰레드 그룹의 쓰레드가 실행이 종료될 때, JVM에 의해 이 메서드가 자동으로 호출됨, ThreadGroup 클래스
메서드 설명
ThreadGroup(String name) 지정된 이름의 새로운 쓰레드 그룹을 생성
ThreadGroup(ThreadGroup parent, String name) 지정된 쓰레드 그룹에 포함되는 새로운 쓰레드 그룹 생성
int activeCount() 쓰레드 그룹에 포함된 활성상태에 있는 쓰레드의 수를 반환(작업을 다 마치지 않은 쓰레드)
int activeGroupCount() 쓰레드 그룹에 포함된 활성상태에 있는 쓰레드 그룹의 수를 반환
void checkAccess() 현재 실행중인 쓰레드가 쓰레드 그룹을 변경할 수 있는 권한이 있는지 체크
int enumerate(Thread[] list)
int enumerate(Thread[] list, boolean recurse)
int enumerate(ThreadGroup[] list)
int enumerate(ThreadGroup[] list, boolean recurse)
쓰레드 그룹에 속한 쓰레드 또는 하위 쓰레드 그룹의 목록을 지정한 배열에 담고 그 수를 반환, 두 번째 매개변수인 recurse의 값이 true로 하면 쓰레드 그룹에 속한 하위 쓰레드 그룹에 쓰레드 또는 쓰레드 그룹까지 배열에 담음
int getMaxPriority() 쓰레드 그룹의 최대우선순위를 반환
String getName() 쓰레드 그룹의 이름을 반환
ThreadGroup getParent() 쓰레드 그룹의 상위 쓰레드 그룹을 반환
void interrupt() 쓰레드 그룹에 속한 모든 쓰레드를 interrupt
boolean isDaemon() 쓰레드 그룹이 데몬 쓰레드인지 확인
boolean isDestroyed() 쓰레드 그룹이 삭제되었는지 확인
void list() 쓰레드 그룹에 속한 쓰레드와 하위 쓰레드 그룹에 대한 정보를 출력
boolean parentOf(ThreadGroup g) 지정된 쓰레드 그룹의 상위 쓰레드 그룹인지 확인
void setDaemon(boolean daemon) 쓰레드 그룹을 데몬 쓰레드 그룹으로 설정/해제
void setMaxPriority(int pri) 쓰레드 그룹의 최대 우선순위를 설정


데몬 쓰레드(Daemon Thread)

  • 일반 쓰레드(non-daemon-thread)의 작업을 돕는 보조적인 역할을 하는 쓰레드
  • 일반 쓰레드가 종료되면 자동적으로 종료
  • 가비지 컬렉터, 자동저장, 화면 자동갱신 등에 사용
  • 무한루프와 조건문을 이용해서 실행 후 대기하다가 특정조건이 만족되면 작업을 수행하고 다시 대기하도록 작성

1. 데몬 쓰레드와 관련된 주요 메서드

boolean isDaemon() - 쓰레드가 데몬 쓰레드인지 확인, 데몬쓰레드이면 true를 반환
void setDaemon(boolean on) - 쓰레드를 데몬 쓰레드로 또는 사용자 쓰레드로 변경, 매개변수 on을 true로 지정하면 데몬 쓰레드가 됨


----------------------------------

2. 데몬 쓰레드의 기본 활용

public void run(){
  while(true){
    try{
      Thread.sleep(3 * 1000); // 3초마다
    } catch(InterruptedException e){}
  }

  if(autoSave){ // autoSave값이 true라면 autoSave를 호출
    autoSave();
  }
}

----------------------------------

3. 예제

class Ex{
  public static void main(String[] args){
    Thread t = new Thread(new Ex()); // Thread(Runnable r)
    t.setDaemon(true); // 이 부분이 없으면 종료되지 않음
    t.start();

    for(int i = 1; <= 10; i++){
      try{
        Thread.sleep(1000);
      }catch(InterruptedException e){}
      System.out.println(i);

      if (i == 5) autoSave = true;

    }

    System.out.println("프로그램을 종료합니다.");
  }

  public void run(){
    while(true){
      try{
        Thread.sleep(3 * 1000);
      }catch(InterruptedException e){}

      if(autoSave) autoSave();
    }
  }

  public void autoSave() {
        System.out.println("작업파일이 자동저장되었습니다.");
    }
}


> 1
> 2
> 3
> 4
> 5
> 작업파일이 자동저장되었습니다.
> 6
> 7
> 8
> 작업파일이 자동저장되었습니다.
> 9
> 10
> 프로그램을 종료합니다. // main 쓰레드가 종료되고 데몬쓰레드도 종료


쓰레드의 상태

상태 설명
NEW 쓰레드가 생성되고 아직 start()가 호출되지 않은 상태
RUNNABLE 실행 중 또는 실행 가능한 상태
BLOCKED 동기화블럭에 의해서 일시정지된 상태(lock이 풀릴 때까지 기다리는 상태)
WAITING
TIMED_WAITING
쓰레드의 작업이 종료되지는 않았지만 실행가능하지 않은(unrunnable) 일시정지상태, TIMED_WAITING은 일시정지시간이 지정된 경우를 의미
TERMINATED 쓰레드 작업이 종료된 상태




쓰레드의 실행제어

  • 쓰레드의 실행을 제어할 수 있는 메서드가 제공
메서드 설명
static void sleep(long millis)
static void sleep(long millis, int nanos)
지정된 시간(천분의 일초 단위)동안 쓰레드를 일시정지, 지정한 시간이 지나고 나면, 자동적으로 다시 실행대기상태로 돌아감
void join()
void join(long millis)
void join(long millis, int nanos)
지정된 시간동안 쓰레드가 실행되도록 함, 지정된 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 다시 돌아와 작업을 계속함
void interrupt() sleep()이나 join()에 의해 일시정지되어 있는 쓰레드를 일깨워 실행대기상태로 만듦, 해당 쓰레드에서는 interrupted Exception이 발생하므로써 일시정지상태를 벗어나게 됨
void stop() 쓰레드를 즉시 종료시킴
void suspend() 쓰레드를 일시정지시킴, resume()을 호출하면 다시 실행대기상태가 됨
void resume() suspend()에 의해 일시정지상태에 있는 쓰레드를 실행대기상태로 만듦
static void yield() 실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보(yield)하고 자신은 실행대기상태로 돌아감


(static)sleep()

  • 현재 쓰레드를 지정된 시간동안 멈추게 함(자기 자신, 남을 멈추게 할 수 없음)
  • sleep()과 yield()는 static이고 자기자신이 사용
  • 예외처리를 해야 함(InterruptedException이 발생하면 깨어남)
    • Thread가 깨워지는 경우
      • 시간이 다 지났을 때
      • InterruptedException이 발생할 때(누군가 깨울 때)
1. sleep의 기본 사용 예시, 

try{
  Thread.sleep(1, 5000000);
}catch(InterruptedException e){} // 그럼 매번 sleep을 할 때 예외처리를 해주어야 하기 때문에 다른 메서드를 만들어서 포함시킴

void delay(long millis){ // 메서드를 만들어서 예외처리까지 
  try{
    Thread.sleep(millis);
  }catch(InterruptedException e){}
}

-------------------------------------------

2. sleep으로 다른 객체에 적용할 때 주의 ( 적용 x, 자기 자신에게 사용하는 꼴 )

try{
  // th1.sleep(10000); // 이렇게 다른 쓰레드에 대해 sleep을 해도 자신이 sleep의 대상이라서 자기 자신에게 sleep하는 경우가 됨
  Thread.sleep(10000); // 이렇게 사용하는 것이 권장
}catch(InterruptedException e){}

-------------------------------------------

3. 예제

class Ex{
  public static void main(String[] args){
    ThreadEx1 th1 = new ThreadEx1(); 
    ThreadEx2 th2 = new ThreadEx2();
    th1.start();
    th2.start();

    try{
      th1.sleep(2000);
    }catch(InterruptedException e){}

    System.out.println("<<main 종료>>");

  }
}

class ThreadEx1 extends Thread{
  public void run(){
    for(int i = 0; i < 100; i++){
      System.out.print("-");
    }
    System.out.println("<<th1 종료>>");
  }
}

class ThreadEx2 extends Thread{
  public void run(){
    for(int i = 0; i < 100; i++){
      System.out.print("|");
    }
    System.out.println("<<th2 종료>>");
  }
}



> .... <<th1 종료>><<th2 종료>><<main 종료>> // th1가 sleep이 되는 것이 아니라 main이 sleep의 대상이 됨
> <<main 종료>> ...... // 만약 sleep을 뺀다면 main쓰레드가 먼저 끝나기 때문에 먼저 출력됨


interrupt()

  • 대기상태(WAITING)인 쓰레드를 실행대기상태(RUNNABLE)로 만듦
  • sleep, join wait으로 대기 상태인 쓰레드를 실행대기로

1. interrupt의 기본 메서드

void interrupt() - 쓰레드의 interrupted상태를 false에서 true로 변경
boolean isInterrupted() - 쓰레드의 interrupted상태를 반환
static boolean interrupted() - 현재 쓰레드의 interrupted상태를 알려주고, false로 초기화

----------------------------------------------------------------------------------------

2. interrupt의 구성과 사용 예시

class Thread{
  ...
  boolean interrupted = false;
  ...
  boolean isInterrupted(){
    return interrupted;
  }

  boolean interrupt(){
    interrupted = true;
  }
}

public static void main(String[] args){
  ThreadEx th = new ThreadEx();
  th.start();
  ...
  th.interrupt();

  System.out.println("isInterruted() : " + th.isInterrupted()); // true;
}

---------------------------------------------------------------------------------------

3. 예제

import javax.swing.JOptionPane;

class Ex{
  public static void main(String[] args){
    ThreadEx th = new ThreadEx();
    th.start();

    String input = JOptionPane.showInputDialog("아무 값이나 입력해주세요.");
    System.out.println("입력하신 값은 " + input + " 입니다.");
    th.interrupt();
    System.out.println("isInterrupted() : " + th.isInterrupted()); // true
    System.out.println("isInterrupted() : " + th.isInterrupted()); // true
    System.out.println("isInterrupted() : " + th.interrupted()); // false, interrupted는 static 메서드이기 때문에 자기 자신의 쓰레드에 해당하기에 th가 아님, main쓰레드가 interrupted되었는가? 확인 
    System.out.println("isInterrupted() : " + Thread.interrupted()); // false, 이렇게 사용해야 함 

  }
}

class ThreadEx extends Thread{
  public void run(){
    int i = 10;

    while(i!=0 && !isInterrupted()){
      System.out.println("-");
      for(long x=0; x<10000000L;x++);
    }
    System.out.println("카운트가 종료되었습니다.")
  }
}


suspend(), resume(), stop()

  • Deprecated 됨 (교착상태dead-lock을 일으킬 가능성이 있기 때문)

1. 메서드 설명

void suspend() - 쓰레드를 일시정지 시킴
void resume() - suspend()에 의해 일시정지된 쓰레드를 실행대기상태로 변경
void stop() - 쓰레드를 즉시 종료

---------------------------------------------------------------------

2. 간단한 사용 예시

class ThreadEx implements Runnable{
  boolean suspend = false;
  boolean stopped = false;

  public void run(){
    while(!stopped){
      if(!suspend){
        // 쓰레드가 수행할 코드를 작성
      }
    }
  }

  public void suspend(){ suspend = true; }
  public void resume(){ suspend = true; }
  public void stop(){ stopped = true; }
}

----------------------------------------------------------------------

3. 예제

class Ex{
  public static void main(String[] args){
    RunImplEx r = new RunImplEx();
    Thread th1 = new Thread(r, "*");
    Thread th2 = new Thread(r, "**");
    Thread th3 = new Thread(r, "***");

    th1.start();
    th2.start();
    th3.start();

    try{
      Thread.sleep(2000);
      th1.suspend();
      Thread.sleep(2000);
      th2.suspend();
      Thread.sleep(2000);
      th1.resume();
      Thread.sleep(2000);
      th1.stop();
      th2.stop();
      Thread.sleep(2000);
      th3.stop();
    } catch(InterruptedException e){}
  }
}

class RunImplEx implements Runnable{
  public void run(){
    while(true){
      System.out.println(Thread.currentThread().getName());
      try{
        Thread.sleep(1000);
      } catch(InterruptedException e){}
    }
  }
}


join()

  • 지정된 시간동안 특정 쓰레드가 작업하는 것을 기다림
void join() - 작업이 모두 끝날 때까지
void join(long millis) - 천분의 일초 동안
void join(long millis, int nanos) - 천분의 일초 + 나노초 동안 

------------------------------------------------------------

class Ex{
  public static void main(String[] args){
    ThreadEx1 th1 = new ThreadEx1(); 
    ThreadEx2 th2 = new ThreadEx2();
    th1.start(); 
    th2.start();
    long startTime = System.currentTimeMillis();

    try{
      th1.join();
      th2.join(); // th1,2를 작업 끝날 때까지 기다림
    } catch(InterruptedExcetion e){}

    System.out.println("소요시간 : " + (System.currentTimeMillis()-startTime())) // 끝난 후 시간을 체크하여 걸린 시간을 연산
  }
}

class ThreadEx1 extends Thread{
  public void run(){
    for(int i = 0; i < 100; i++){
      System.out.print(new String("-"));
    }
  }
}

class ThreadEx2 extends Thread{
  public void run(){
    for(int i = 0; i < 100; i++){
      System.out.print(new String("|"));
    }
  }
}


yield()

  • 남은 시간을 다음 쓰레드에 양보하고, 자기 자신은 실행대기 시킴
  • yield와 interrupt를 적절하게 사용하면 응답성과 효율성을 증대시킬 수 있음
  • 그러나 yield를 사용한다고 반드시 동작한다는 것은 아님, OS의 SCHEDULER가 우선권을 가짐, 통보용
class MyThread implements Runnable{
  boolean suspended = false;
  boolean stopped = false;

  Thread th;
  MyThread(String name){
    th = new Thread(this, name);
  }

  public void run(){
    while(!stopped){
      if(!suspended){
        // 작업 수행

        try{
          Thread.sleep(1000);
        }catch(InterruptedException e){}
      } else{ // 일시정지상태라면 다른 쓰레드에게 양보하는 게 효율적
        Thread.yield();
      }
    }
  }
}


쓰레드의 동기화(Synchronization)

  • 멀티쓰레드 프로세스에서는 다른 쓰레드의 작업에 영향을 주고받을 수 있음
  • 진행중인 작업이 다른 쓰레드에게 간섭을 받지 않게 하려면 동기화가 필요
  • 동기화하려면 간섭받지 않아야 하는 문장들을 "임계영역"으로 설정
  • 임계영역락(lock)을 얻은 단 하나의 쓰레드만 출입가능(객체 1개에 락 1개)

  • 임계영역 설정 방법
    • 메서드 전체를 임계영역으로 지정
    • 특정한 영역을 임계영역으로 지정

1. 메서드 전체를 임계영역으로 지정

public synchronized void calcSum(){ // 임계영역이 많을수록 멀티쓰레드의 장점이 없어지기 때문에 최소화 시켜줌
  ....
}

2. 특정 영역을 임계영역으로 지정
synchronized(객체의 참조변수){ // 권장, 상황에 따라 1,2번을 하지만 최소화시킬 수 있는 방법을 찾아야 함
  ....
}

----------------------------------------------------------------------------------

public synchronized void withdraw(int money){
  if(balance >= money){
    try{
      Thread.sleep(1000);
    }catch(Exception e){}

    balance -= money;
  }
}


public void withdraw(int money){
  synchronized(this){
    if (balance >= money){
      try{
        Thread.sleep(1000);
      } catch(Exception e){}

      balance -= money;
    }
  }
}

----------------------------------------------------------------------------------

class Account2{
  private int balance = 1000; // private로 해야 동기화의 의미가 있음, private를 안하면 다른 곳에서 접근이 가능하므로

  public int getBalance(){
    return balance;
  }

  public synchronized void withdraw(int money){ // 메서드 동기화
    if(balance>=money){
      try{
        Thread.sleep(1000);
      }catch(InterruptedException e){}

      balance -= money;
    }
  }
}

class RunnableEx implements Runnable{
  Account2 acc = new Account2();

  public void run(){
    while(acc.getBalance() > 0){
      // 100,200,300중의 한 값으로 임의로 선택 후 출금
      int money = (int)(Math.random() * 3 + 1) * 100;
      acc.withdraw(money);
      System.out.println("balance : "  + acc.getBalance());
    }
  } // run()
}


wait()과 notify()

  • 동기화의 효율을 높이기 위해 wait() notify()를 사용

  • Object클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있음

    • wait() - 객체의 lock을 풀고 쓰레드를 해당 객체의 waiting pool에 넣음
    • notify() - waiting pool에서 대기중인 쓰레드 중의 하나를 깨움
    • notifyAll() - waiting pool에서 대기중인 모든 쓰레드를 깨움
  • wait와 notify은 대상을 지정할 수 없지만 lock & condition을 사용하면 깨우는, 기다리게 하는 대상을 지정할 수 있음


1. 기본 사용 예시 
class Account {
  int balance = 1000;

  public synchronized void withdraw(int money){
    while(balance < money){
      try{
        wait(); // 대기 - 락을 풀고 기다림, 통지를 받으면 락을 재획득(ReEntrance)
      }catch(InterruptedException e){}
    }
    balance -= money;
  }

  public synchronized void deposit(int money){
    balance += money;
    notify(); // 통지 - 대기중인 쓰레드 중 하나에게 알림
  }
}

--------------------------------------------------------

2. 동기화 예제

import java.util.ArrayList;

class Customer implements Runnable{
  private Table table;
  private String food;

  Customer(Table table, String food){
    this.table = table;
    this.food = food;
  }

  public void run(){
    while(true){
      try{
        Thread.sleep(10);
      } catch(InterruptedException e){}
      String name = Thread.currentThread().getName();

      if(eatFood()){
        System.out.println(name + " ate a " + food);
      } else{
        System.out.println(name + " failed to eat. :(");
      }
    }
  }

  boolean eatFood(){return table.remove(food);}
}

class Cook implements Runnable{
  private Table table;

  Cook(Table table){
    this.table = table;
  }

  public void run(){
    while(true){
      int idx = (int)(Math.random() * table.dishNum());
      table.add(table.dishNames[idx]);
      try{
        Thread.sleep(100);
      }catch(InterruptedException e){}
    }
  }
}

class Table {
  String[] dishNames = {"donut", "donut", "burger", "pizza"};
  final int MAX_FOOD = 6;
  private ArrayList<String> dishes = new ArrayList<>();
  public synchronized void add(String dish){
    if(dishes.size()>= MAX_FOOD){
      return;
    }
    dishes.add(dish);
    System.out.println("Dishes : " + dishes.toString());
  }

  public boolean remove(String dishName){
    synchronized(this){
      while(dishes.size()==0){
        String name = Thread.currentThread().getName();
        System.out.println(name + " is waiting.");
        try{Thread.sleep(500);} catch(InterruptedException e){}
      }

      for(int i = 0; i<dishes.size(); i++){
        if(dishName.equals(dishes.get(i))){
          dishes.remove(i);
          return true;
        }
      }
    }
    return false;
  }
}

----------------------------------------------------------------------

3. wait과 notify를 사용한 동기화 예제

class Customer implements Runnable{
  private Table table;
  private String food;

  Customer(Table table, String food){
    this.table = table;
    this.food = food;
  }

  public void run(){
    while(true){
      try{
        Thread.sleep(100);
      } catch(InterruptedException e){}
      String name = Thread.currentThread().getName();
      table.remove(food);
      System.out.println(name + " ate a " + food);
    }

  }
}

class Cook implements Runnable{
  private Table table;

  Cook(Table table){
    this.table = table;
  }

  public void run(){
    while(true){
      int idx = (int)(Math.random() * table.dishNum());
      table.add(table.dishNames[idx]);
      try{
        Thread.sleep(100);
      }catch(InterruptedException e){}
    }
  }
}

class Table {
  String[] dishNames = {"donut", "donut", "burger", "pizza"}; // donut이 나올 확률을 높임
  final int MAX_FOOD = 6;
  private ArrayList<String> dishes = new ArrayList<>();
  public synchronized void add(String dish){
    while(dishes.size()>=MAX_FOOD){
      String name = Thread.currentThread().getName();
      System.out.println(name+ " is waiting.");

      try{
        wait(); // COOK 쓰레드를 기다리게 함, 요리가 꽉찼기 때문에
        Thread.sleep(500);
      }catch(InterruptedException e){}
    }
    dishes.add(dish); 
    notify(); // 기다리고 있는 손님을 깨우기 위함
    System.out.println("Dishes: " + dishes.toString());
  }

  public void remove(String dishName){
    synchronized(this){
      String name = Thread.currentThread().getName();

      while(dishes.size()==0){
        System.out.println(name+" is waiting.");
        try{
          wait(); // 손님쓰레드를 기다리게 함
          Thread.sleep(500);
        } catch(InterruptedException e){}
      }

      while(true){
        for(int i=0; i<dishes.size(); i++){
          if(dishName.equals(dishes.get(i))){
            dishes.remove(i);
            notify(); // 요리사 쓰레드를 깨우기 위함
            return;
          }
        }

        try{
          System.out.println(name + " is waiting.");
          wait(); // 원하는 음식이 없는 손님쓰레드를 기다리게 함, lock & condition을 사용하면 누구를 기다리게 하는지 대상을 정할 수 있음
          Thread.sleep(500);
        }catch(InterruptedException e){}
      }
    }
  }

  public int dishNum(){return dishNames.length;}
}


class Ex{
  public static void main(String[] args) throws Exception{
    Table table = new Table();

    new Thread(new Cook(table), "Cook").start();
    new Thread(new Customer(table, "donut"), "Customer1").start();
    new Thread(new Customer(table, "burger"), "Customer2").start();
    Thread.sleep(2000);
    System.exit(0);

  }
}





  • 자바의 정석 챕터 13에서는 프로세스와 쓰레드, 단일 쓰레드, 멀티쓰레드, 쓰레드에 중요 메서드와 사용법에 대해서 배웠습니다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함