쓰레드

박민수·2023년 2월 4일
0

자바의 정석

목록 보기
14/17
post-thumbnail

1. 쓰레드란

쓰레드를 설명하기 이전에 먼저 프로세스를 알아야 한다.

프로세스란 실행 중인 프로그램을 의미한다.
프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)를 할당받아 프로세스가 된다.
이런 프로세스는 프로그램을 수행하는 데 필요한 데이터, 메모리 등의 자원 그리고 쓰레드로 구성되어 있다.

OS에서 실행 중인 하나의 어플리케이션 즉 ctrl + alt + del창 작업 관리자에서 프로세스 탭에 올라와 있는 어플리케이션 하나를 하나의 프로세스라고 부른다. ex) Chrome

만약 우리가 크롬창을 더블클릭 누른다면(실행) 운영체제로부터 필요한 메모리를 할당받아 어플리케이션의 코드를 실행하는 것이 프로세스이다.

크롬을 2개 띄웠다면 두 개의 프로세스가 생성된 것이다.

프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 쓰레드이다.

프로그램 코드를 한 줄씩 실행하는 것이 쓰레드의 역할이다(=실행제어)

한 프로그램에 여러개의 쓰레드가 존재할 수 있다.

쓰레드가 작업을 수행하는데 개별적인 메모리 공간(호출스택)을 필요로 하기 때문에 프로세스의 메모리 한계에 따라 생성할 수 있는 쓰레드의 수가 결정된다.

쓰레드가 1개라면 단일스레드, 2개이상이라면 다중쓰레드

2. 멀티태스킹과 멀티쓰레딩

우리가 사용하는 원도우나 유닉스를 포함한 대부분의 OS는 멀티태스킹(다중작업)을 지원하기 때문에 여러 개의 프로세스가 동시에 실행될 수 있다.

멀티 태스킹이란 두 가지 이상의 작업을 동시에 처리하는 것을 말한다. 예를 들어 워드로 문서작업을 하는 동시에 음악을 듣는 것은 OS가 프로세스마다 작업을 병렬로 처리하기에 가능하다.

멀티 쓰레딩은 하나의 프로세스 내에서 여러 쓰레드가 동시에 작업을 수행하는 것이다.
실제로 동시에 처리되는 작업의 개수는 코어의 개수와 일치한다. (1개의 코어는 한 개의 프로세스를 담당)

예를 들어 메신져 프로세스 같은 경우 채팅 기능을 제공하면서 동시에 파일 업로드 기능을 수행할 수 있다.
이처럼 한 프로세스에서 멀티 태스킹이 가능한 이유는 멀티 스레드(Multi Thread) 때문이다.

쓰레드의 수는 언제나 코어의 개수보다 훨씬 많기 때문에 각 코어가 아주 짧은 시간 동안 여러 작업을 번갈아 가며 수행함으로써 여러 작업들이 모두 동시에 수행되는 것처럼 보이게 한다.

*단 프로세스의 성능이 단순히 쓰레드의 개수에 비례하는 것은 아니며, 하나의 쓰레드를 가진 프로세스가 오히려 더 좋은 성능을 보일수도 있다.

싱글쓰레드와 멀티쓰레드
싱글쓰레드는 한 작업을 마친 후에 다른 작업을 시작한다.
멀티쓰레드(쓰레드가 2개라고 가정)는 짧은 시간동안 2개의 쓰레드를 번갈아가며 작업을 수행한다.
여기서 단순작업(계산 작업 등)을 한다면 싱글쓰레드가 더 효율적이다.
왜냐하면 멀티쓰레드는 작업간의 전환(컨텍스트 스위칭)을 할 때 현재 진행 중인 작업의 상태, 실행해야할 위치 등의 정보를 읽어와야 하기 때문에 더 많은 시간이 걸린다.
그러나 두 쓰레드가 서로 다른 자원을 사용하는 경우에는 멀티쓰레드의 효율이 더 좋다.
예를 들어 데이터를 입려받는 작업과 프린트하는 작업이 필요할 경우, 멀티쓰레드는 사용자로부터 입력을 기다리는 시간을 활용하여 다른 작업을 진행할 수 있기 때문에 더 효율적이다.

여러 개의 프로세스 처리 방법

하나의 *프로세서(CPU, 코어)는 한번에 프로세스 1개밖에 실행시키지 못한다.

여러 개의 프로세스를 처리하는 방법은 크게 두 가지가 있다.

(1) 병렬 처리 방법

2개 이상의 코어가 각기 다른 프로세스의 명령을 실행해서 각 프로세스가 같은 순간에 실행되도록 하는 방법이다.

멀티 코어 환경에 맞게 프로그래밍만 잘 되어 있다면 코어의 개수만큼 빠른 일 처리가 가능하다.

그러나 프로세스가 4개지만 코어는 2개인 경우 2개만 동시에 작업이 진헹되고 나머지는 대기상태에 놓인다.

(2) 병행 처리 방법(멀티프로세싱)
하나의 코어가 여러 프로세스를 돌아가면서 조금씩 처리하는 것을 말한다.
(여러 개의 코어가 여러 프로세스를 돌아다니는 것도 가능)

병행 처리란 작업을 조금씩 나누어 실행하면서 컴퓨터가 마치 여러 작업을 동시에 실행하는 것처럼 보이게 하는 처리 방법이다.

일정한 시간 간격으로 수행해야하는 스레드를 전환한다.

스레드를 전환할 때는 운영체제의 스케줄러의 기준에 따라 순서가 정해지게 된다.

여러 스레드를 번갈아 처리하기 때문에 엄밀히 말하면 한번에 한가지를 처리하지만 동시에 작업하는 듯한 효과를 준다.

프로세스 또는 쓰레드 간의 작업 전환을 '컨텍스트 스위칭(context switching)'이라고 한다.

여러 개의 프로세스를 함께 진행하는 것을 멀티 프로세싱이라고 한다.

프로세스와 프로세서의 차이
Processor (프로세서)는 하드웨어적인 측면에서 "컴퓨터 내에서 프로그램을 수행하는 하드웨어 유닛"이다. 이는 중앙처리장치(Central Processing Unit)를 뜻한다.
반면 프로세스는 특정 목적을 수행하기 위해 나열된 작업의 목록, 즉 프로그램이다.
작업의 과정이 파일로 저장되어 있으면 그것을 "프로그램"이라고 부르고 메모리에 적재되어 실행 중 이거나 실행 대기 중일 땐 "프로세스"라고 구별하여 부른다.
즉 프로세스는 "메모리에 적재되어 프로세서에 의해 실행중인 프로그램" 이라고 정의한다.
프로세스와 프로세서의 차이

멀티쓰레딩의 장단점

장점으로는

CPU의 사용률을 향상시킨다.
자원을 보다 효율적으로 사용할 수 있다.
사용자에 대한 응답성이 향상된다.
작업이 분리되어 코드가 간결해진다.

만약 싱글쓰레도로 작성되어 있다면, 파일을 다운받는 동안 채팅을 할 수 없는 프로그램이 될 것이다.

단점으로는

여러 쓰레드가 같은 프로세스 내에서 자원을 공유하면서 작업하기 때문에 동기화, 교착상태와 같은 문제가 발생할 수 있다는 것이다.

교착상태란
두 쓰레드가 자원을 점유한 상태에서 서로 상대편이 점유한 자원을 사용하려고 기다리느라 진행이 멈춰있는 상태를 말한다.

동기화란
멀티쓰레드 프로세스의 경우 여러 쓰레드가 자원을 공유해서 작업하기 때문에 서로에게 영향을 줄 수 있다.
이러한 일을 방지하기 위해 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 '쓰레드 동기화(Synchronization)'라고 한다. (뒤에 자세한 설명)

3. 쓰레드의 구현

쓰레드를 구현하는 방법은 2가지가 있다.

(1) Thread클래스 상속

class thread_1 extends Thread{
public void run() {내용}

thread_1 t1 = new thread_1();
t1.start();

(2) Runnable인터페이스 구현

class thread_2 implements Runnable{
public void run() {내용}

Runnable r = new thread_2(); //Runnable을 구현한 클래스의 인스턴스 생성
thread t2 = new thread(r); //생성자(Thread(Runnable target), (thread t2 = new thread(new Thread_2()))로 요약 가능
t2.start();

보통 Thread클래스를 상속받으면 다른 클래스를 상속받을 수 없기때문에, Runnable인터페이스를 구현하는 것이 일반적이다.

Thread클래스를 상속받은 경우와 Runnable인터페이스를 구현한 경우의 인스턴스 생성방법이 다르다.

쓰레드를 구현한다는 것은 run()의 몸통을 채운다는 것이다.

Thread클래스를 상속받으면 자손 클래스에서 조상인 Thread클래스의 메서들르 직접 호출할 수 있지만,
Runnable인터페이스를 구현하면 Thread클래스의 static메서드인 currentThread()를 호출하여 쓰레드에 대한 참조를 얻어와야 호출이 가능하다.

  class thread_1 extends Thread{
            public void run() {
                System.out.println(getName()); // 조상Thread의 이름 호출
            }
        }

        class thread_2 implements Runnable{
            public void run() {
                System.out.println(Thread.currentThread().getName()); // 조상Thread의 이름 호출
            }
        }

쓰레드를 생성했다고 해서 자동으로 실행되는 것은 아니다.
start()를 호출해야만 쓰레드가 실행된다.

단 start()가 호출되었다고 해서 바로 실행되는 것이 아니라, 실행대기 상태가 되어 자신의 차례를 기다리게 된다.

쓰레드의 실행순서는 OS의 스케쥴러에 의해 결정된다.

주의할 점으로 한 번 실행이 종료된 쓰레드는 다시 실행할 수 없다.

즉 하나의 쓰레드에 대해 start()가 한 번만 호출될 수 있다는 것이다.

thread_1 t1 = new thread_1();
t1.start(); 
t1.start(); // 예외 발생

다시 실행하고 싶다면
t1 = new thread_1(); // 다시 생성 후에 실행
t1.start();

쓰레드 생성 후 호출스택의 변화

run()을 호출하는 것은 쓰레드를 실행하는 것이 아닌 단순히 클래스에 선언된 메서드를 호출하는 것 뿐이다.

반면에 start()는 새로운 쓰레드가 작업을 실행하는데 필요한 호출스택을 생성한 다음에 run()을 호출해서, 생성된 호출스택에 run()이 첫 번째로 올라가게 한다.

모든 쓰레드는 독립적인 작업을 수행하기 위해 자신만의 호출스택이 필요하다.

새로운 쓰레드를 생성하고 실행시킬 때마다 새로운 호출스택이 생성되고 쓰레드가 종료되면 작업에 사용된 호출스택은 소멸한다.

아래는 start()를 호출한 후 호출스택의 변화이다.

(1) main메서드에서 쓰레드의 start()를 호출 한다.

(2) start()는 새로운 쓰레드를 생성하고, 쓰레드가 작업하는데 사용될 호출 스택을 생성한다.

(3) 새로 생성된 호출스택에 run()이 호출되어, 쓰레드가 독립된 공간에서 작업을 수행한다.

(4) 이제는 호출스택이 2개이므로 스케줄러가 정한 순서에 의해서 반갈아 가면서 실행된다.

4. 쓰레드의 메서드

아래는 쓰레드의 메서드를 나타낸다.

setPriority()
쓰레드의 우선순위를 지정한다(10이 최고, 1이 최하, main은 5)

getPriority()
쓰레드의 우선순위를 반환

getState()
쓰레드의 상태 확인

sleep(millis)
지정된 시간동안 쓰레드를 일시정지, 지정한 시간이 지나고 나면, 자동적으로 다시 실행대기상태가 된다.

join()
join(millis)
지정된 시간동안 쓰레드가 실행되도록 한다. 지정한 시간이 지나거나 작업이 종료되면 join()을 호출한 쓰레드로 돌아와 실행을 계속한다.

interrupt()
sleep()이나 join()에 의해 일시정지 상태인 쓰레드를 깨워서 실행대기상태로 만든다. 해당 쓰레드에서는 
InterruptedExcetion이 발생함으로써 일시정지를 벗어남(try-catch문으로 예외처리 필수)

boolean interrupted()
쓰레드에 interrupt가 호출되었는지 알려준다.

stop()
쓰레드를 즉시 종료시킨다.

suspend()
쓰레드를 일시정지 시킨다.

resume()
suspend()에 의해 일시정지 상태에 있는 쓰레드를 실행대기상태로 만든다.

yield()
자신에게 주어진 실행시간을 다른 쓰레드에게 양보하고 자신은 실행대기상태가 된다.

- stop(), suspend(), resume()은 교착상태를 만들기 쉽기 때문에 deprecated가 되었다.

아래는 쓰레드의 상태를 나타낸다.

NEW
쓰레드가 생성되고 아직 start()가 호출되지 않은 상태

RUNNABLE(실행대기)
실행 중 또는 실행 가능한 상태

BLOCKED(일시정지)
동기화 블록에 의해서 일시정지된 상태

WAITING
TIMED_WAITING(일시정지)
쓰레드의 작업이 종료되지는 않았지만 일시정지상태
일시정지시간이 지정된 경우

TERMINATED(종료)
쓰레드의 작업이 종료된 형태

전체적으로 요약하자면 이렇다.

(1) sleep() 메서드

static void sleep(long millis)
static void sleep(long millis, int nanos)

sleep()은 지정된 시간동안 쓰레드를 멈추게 한다.

TIMED_WAITING(일시정지)상태로 전이된다.

sleep()에 의해 일시정지 상태가 된 쓰레드는 지정된 시간이 다 되거나 interrupt()가 호출되면 잠에서 깨어나 실행대기 상태가 된다. (RUNNABLE 상태)

 try {
     Thread.sleep(2000); // 2초동안 멈추게 한다.
       } catch (InterruptedException e) {
 }

interrupt()를 호출한다는 것은 InterruptedException을 발생시킨다는 뜻(catch문을 실행시킨다는 뜻)으로
sleep()를 구현할 때는 꼭 try-catch문으로 구현해야한다.

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

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

        thread_1 t1 = new thread_1();
        t1.start(); 

        thread_2 t2 = new thread_2();
        t2.start(); 

        try {
            t1.sleep(2000); // 2초동안 멈추게 한다.
            // Thread.sleep(2000)
        } catch (InterruptedException e) {
        }

        System.out.print("main 종료");


    }
    
    //결과 ... th1 종료 th2 종료 main 종료

결과로 t1이 2초 늦게 출력되어서 가장 마지막에 출력되길 기대하지만 결과는 t1이 가장 먼저 종료 되었다.

그 이유는 sleep()은 항상 현재 실행 중인 쓰레드에 대해 작동하기 때문에 t1.sleep(2000)과 같이 호출하였어도 main메서드에 적용되어 main이 가장 늦게 종료된 것이다.

따라서 참조변수를 이용해서 호출하기 보다는 Thread.sleep(2000)처럼 호출을 해야한다.

뒤에 나오지만 yield()도 동일하다.

(2) interrupt()와 interrupted()

진행 중인 쓰레드의 작업을 취소하는 메서드 이다.

interrupt()가 호출되면 쓰레드에게 작업을 멈추라고 요청하며, interrupted상태를 변경한다.(false <-> true, false일때는 쓰레드가 실행되고 true일때는 정지한다.)

void interrupt() 쓰레드의 interrupted상태를 false -> true로
boolean isInterrupted() 쓰레드의 interrupted상태를 반환
static boolean interrupted() 현재 쓰레드의 interrupted상태를 반환 후, false로 변경

쓰레드가 sleep(), wait(), join()에 의해 '일시정지 상태'에 있을 때, interrupt()를 호출하면 InterruptedException이 발생하고 쓰레드는 '실행대기 상태(RUNNABLE)'로 바뀐다.

즉 멈춰있던 쓰레드를 깨워 실행가능한 상태로 만든다.

interrupt() 사용예제

class thread_1 extends Thread{
    public void run() {
       int i = 10;
       while(i != 0 && !isInterrupted()){
        System.out.println(i--);
        for(long x=0; x<2500000L; x++); //시간 지연
       }
       System.out.println("카운트 종료");
    }
}

thread_1 t1 = new thread_1();
        t1.start(); 

        String input = JOptionPane.showInputDialog("입력");
        System.out.println("입력 값은" + input );
        t1.interrupt(); // interrupted상태가 true가 된다.
        System.out.println(t1.isInterrupted());

사용자 입력이 끝나면 interrupt()에 의해 카운트다운이 중간에 멈춘다.

(3) yield()

yield()는 쓰레드 자신에게 주어진 실행시간을 다음 차례의 쓰레드에게 양보(yield)한다.

우선순위가 낮은 코드의 경우 다른 스레드에 자원 할당을 위해 사용한다.

public class ThreadA extends Thread{

    boolean stop = false;
    boolean work = true;
    
    @Override
    public void run() {
        while (!stop){
            if(work){
             System.out.println("ThreadA -AAA");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException ie){}
            } else{
                //다른 스레드에 실행 양보
                Thread.yield();
            }
        }
        System.out.println("ThreadA 종료");
    }
}

public class ThreadB extends Thread {

    boolean stop = false;
    boolean work = true;

    @Override
    public void run() {
        while (!stop){
            if(work){
        System.out.println("ThreadB - BBB");
                try {
                   Thread.sleep(500);
                } catch (InterruptedException ie){}
            } else{
                //다른 스레드에 실행 양보
                Thread.yield();
            }
        }
        System.out.println("ThreadB 종료");
    }
}

ThreadA a = new ThreadA();
ThreadB b = new ThreadB();

        a.start();
        b.start();

        try{
            Thread.sleep(2000);
        } catch (InterruptedException ie){ ie.getMessage(); }

//work값에 따라 다른 스레드에 실행을 양보하며 번갈아 실행된다.

        //a 일시정지
        a.work = false;

        try{
            Thread.sleep(1000);
        } catch (InterruptedException ie){ ie.getMessage(); }

        //a 실행 재개
        a.work = true;

        try{
            Thread.sleep(2000);
        } catch (InterruptedException ie){ ie.getMessage(); }

        //b 일시정지
        b.work = false;

        //a, b 실행종료
        a.stop = true;
        b.stop = true;

(4) join()

쓰레드 자신이 하던 작업을 잠시 멈추고 다른 쓰레드가 지정된 시간동안 작업을 수행하도록 할 때 사용

void join()
void join(long millis)
void join(long millis, int nanos)

시간을 지정하지 않으면, 해당 쓰레드가 작업을 모두 마칠때까지 기다리게 된다.

주로 특정스레드의 실행값을 가져오기 위한 목적으로 사용한다.

예를 들어 특정 스레드에서 계산 작업을 맡고 있다면, 다른 스레드에서는 특정 스레드가 종료된 후에 접근하는 것이 안전하다.

join()sleep()처럼 interrupt()에 의해 대기상태에서 벗어날 수 있으므로, try-catch문을 사용해야 한다.

 static long startTime = 0; // static변수 설정

thread_1 t1 = new thread_1();
t1.start(); 

thread_2 t2 = new thread_2();
t2.start(); 

startTime = System.currentTimeMillis();

        try {
            t1.join(); //t1 작업이 끝날때까지 main쓰레드가 기다린다.
            t2.join(); //t2 작업이 끝날때까지 main쓰레드가 기다린다.
        } catch (InterruptedException e) {}

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

}

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

class thread_2 extends Thread{
    public void run() {
        for(int i = 0; i< 300; i++){
            System.out.print(new String("|"));
            
        }
    }
    
    //결과
  ...(생략)소요시간:32

join()이 없었다면 main쓰레드는 바로 종료되었겠지만 작업을 마칠때까지 기다리게 하여, 마지막에 소요시간을 출력할 수 있게 했다.

5. 쓰레드의 동기화

위에 언급했듯이 쓰레드의 동기화란 "한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것"이다.

동기화의 방법으로 한 쓰레드가 특정 작업을 마치기 전까지 방해받지 않도록 임계 영역(critical section)잠금(락, lock)을 설정하는 방법이 있다.

공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정하고, 공유 데이터(객체)가 가지고 있는 lock을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한다.

모든 코드를 수행한 후 lock을 반납하여 다른 쓰레드가 임계 영역에서 코드를 수행할 수 있게 한다.

(1) synchronized를 이용한 동기화

synchronized는 임계 영역을 설정하는데 사용된다.

1. 메서드 전체를 임계 영역으로 지정
	public synchronized void test1() {
    	...
   }

2. 특정한 영역을 임계 영역으로 지정
	synchronized(객체의 참조변수) {
    	...
    }

첫 번째 방법에서 쓰레드는 synchronized메서드가 호출된 시점부터 해당 메서드가 포함된 객체의 lock을 얻어 작업을 수행하다가 메서드가 종료되면 lock을 반환한다.

두 번째 방법은 메서드 내의 코드 일부를 블럭{}으로 감싸고 블럭 앞에 synchronized(참조변수)를 붙이는 것이다. 이때 참조변수는 락을 걸고자하는 객체를 참조하는 것이어야 한다. 이를 synchronized블럭이라 부르며, 블럭의 영역 안에서는 lock을 얻고 벗어나면 lock을 반환한다.

Runnable r = new thread_1();
new Thread(r).start();
new Thread(r).start();
       
}
}
class Account {
    private int balance = 1000;

    public int getBalance() {
        return balance;
    }

    public void withdraw(int money){
        if(balance >= money){
            try {
                Thread.sleep(1000); // 상황을 만들기위해 
                일부로 다른 쓰레드에게 
                제어권을 넘겨주도록 설계
            } catch (InterruptedException e) {}
            balance -= money;
        }
    }
}

class thread_1 implements Runnable{
    Account acc = new Account();
    public void run() {
       while(acc.getBalance() > 0){
        int money = (int)(Math.random() * 3 +1) * 100;
        acc.withdraw(money);
        System.out.println("balance:" + acc.getBalance());
       }
       
      
    }
}

//결과
balance:600
balance:300
balance:200
balance:0
balance:-100

은행계좌(account)에서 잔고(balance)를 확인하고 임의의 금액을 출금(withdraw)하는 예제에서 잔고가 출금하려는 금액보다 큰 경우에만 출금이 되도록 구현되어 있다.

그러나 실행결과에서는 잔고가 음수가 찍히게 되는데, 이는 한 쓰레드가 if문의 조건식을 통과하고 출금하기 바로 직전에 다른 쓰레드가 끼어들어 출금을 먼저 했기 때문이다.

때문에 잔고를 확인하는 if문과 출금하는 문장은 임계 영역으로 묶어져야 한다.

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

(2) wait()와 notify()

특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않게 하기 위한 메서드들이다.

동기화된 임계영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, wait()를 호출하여 쓰레드가 락을 반납하고 기다리게 한다.

그럼 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 수행 할 수 있게 되고, 추후에 작업을 진행할 수 있는 상황이 되면 notify()를 호출해서, 작업을 중단했던 쓰레드가 다시 락을 얻어 진행할 수 있게 된다.

wait()이 호출되면, 실행 중이던 쓰레드는 해당 객체의 대기실(waiting pool)에서 통지를 기다린다.

notify()가 호출되면, 해당 객체의 대기실에 있던 모든 쓰레드 중에서 임의의 쓰레드만 통지를 받는다.

notifyAll()은 모든 쓰레드에게 통보를 하지만, 그래도 lock을 얻어 나오는 쓰레드는 하나이다.

wait()notify() 또는 notifyAll()이 호출될 때까지 기다린다.

매개변수가 있는 wait()는 지정된 시간이 지난후에 자동적으로 notify()가 호출되는 것과 같이 행동한다.

waiting pool은 객체마다 존재하는 것으로 notifyAll()을 한다고 모든 객체의 waiting pool에 있는 쓰레드를 깨우는 것은 아니다.

wait(), notify(), notifyAll()
Object에 정의되어 있다.
동기화 블록(synchronized블록)내에서만 사용할 수 있다.
보다 효율적인 동기화를 가능하게 한다.

다음 예제는 식당에서 음식(Dish)을 만들어서 테이블(Table)에 추가(add)하는 요리사(Cook)와 테이블의 음식을 소비(remove)하는 손님(Customer)을 쓰레도로 구현한 것이다.

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);
		} // while
	}
}

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(10);} catch(InterruptedException e) {}
		} // while
	}
}

class Table {
	String[] dishNames = { "donut","donut","burger" }; // 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();  // 기다리고 있는 CUST를 깨우기 위함.
		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(); // CUST쓰레드를 기다리게 한다.
						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(); // 잠자고 있는 COOK을 깨우기 위함 
						return;
					}
				} // for문의 끝

				try {
					System.out.println(name+" is waiting.");
					wait(); // 원하는 음식이 없는 CUST쓰레드를 기다리게 한다.
					Thread.sleep(500);
				} catch(InterruptedException e) {}	
			} // while(true)
		} // synchronized
	}

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

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

		new Thread(new Cook(table), "COOK1").start();
		new Thread(new Customer(table, "donut"),  "CUST1").start();
		new Thread(new Customer(table, "burger"), "CUST2").start();
	
		Thread.sleep(2000);
		System.exit(0);
	}
}

6. Lock과 Condition을 이용한 동기화

동기화할 수 있는 방법은 synchronized블럭 외에도 "java.util.concurrent.locks" 패키지가 제공하는 lock클래스들을 이용하는 방법이 있다.

synchronized블럭으로 동기화하면 자동적으로 lock이 걸리고 풀려서 편하다. synchronized블럭 내에서 예외가 발생해도 lock은 자동적으로 풀린다.

그러나 같은 메서드 내에서만 lock을 걸 수 있다는 제약이 있다.

이럴 때 lock 클래스를 사용한다.

ReentrantLock : 재진입이 가능한 lock. 가장 일반적인 배타 lock
ReentrantReadWriteLock : 읽기에는 공유적이고, 쓰기에는 배타적인 lock
StampedLock : ReentrantReadWriteLock에 낙관적인 lock의 기능을 추가

JAVA에서 멀티쓰레드 사용하기
프로세스와 쓰레드
스레드 제어하기
쓰레드 동기화

profile
쉽게 쉽게

0개의 댓글