인생을 코딩하다.

[Java] Mulit-thread 본문

Java

[Java] Mulit-thread

Hyung1 2020. 11. 18. 14:33
728x90
반응형

Multi-thread 프로그래밍

  • 동시에 여러 개의 Thread가 수행되는 프로그래밍
  • Thread는 각각의 작업공간 (context)를 가짐
  • 공유 자원이 있는 경우 race condition이 발생
  • critical section에 대한 동기화(synchronization)의 구현이 필요

쓰레드들이 공유하는 자원 = 쉐어드 리소스

이런 자원의 영역을 critical section 이라고 한다. 이 critical section을 2개의 쓰레드가 접근하게 되었을 때,

만약 critical section 에서 어떤값을 가져와서 더하고 뺀다라고 하면 나중에 더해서 오버라이드 한 값이 무시가 될 수 있다. 그래서 더한 다음에 빼야한다. 결국엔 이런 영역에 관해 순서를 지켜주자는게 동기화다.

이 critical section언애 들어갈 수 있는 순서는 한 번에 하나만 있어야 한다.

critical section에 락을 건 후에 다른 쓰레드는 접근 할 수 없게 해결 -> 메서드 블락 방식

 

Thread status

 

쓰레드가 start가 되면 Runnable한 상태로 된다. Runnable 상태여야 CPU를 점유할 수 있다.

스케쥴러가 씨피유를 배분해준다. 이렇게 쓰레드가 돌다가 끝나면 Dead상태가 된다

 

쓰레드가 Not Runnable 상태로 빠지는 경우가 있다.

3가지 메서드에 의해서 빠지게 된다.

 

sleep(시간) ->  1000을 넣으면 그 시간동안 Not Runnable로 빠진다 -> 시간이 지나면 Runnable로 돌아온다.

wait() -> 쉐어드 리소스를 기다리기 위해서 쓰는 메서드 -> notify()가 호출되면 Runnable로 돌아온다.

join() -> 두 개의 쓰레드가 동시에 돌아간다고 하였을 때, 한 쓰레드가 다른 쓰레드에 조인을 걸면 다른 쓰레드가 끝날때까지 조인을 건 쓰레드가 Not Runnable 상태로 빠진다. -> other thread exits일때 Runnable로 돌아온다.

 

CPU는 Not Runnable에서는 실행 될 수 없다. Not Runnable에서 Runnable로 와야 CPU에서 실행이 된다.

 

근데 이런 Thread들이 Runnable 상태로 못 오게 될 때, (Not Runnable에 머물게 될 때,) 인터럽트 익셉션을 날리게 되면,

인터럽트에 의해서 익셉션으로 예외처리 되고 끝이난다. 

Thread는 우선순위

Thread는 우선순위를 가지게 된다.

 

Thread.MIN_PRIORITY(=1) ~ Thread.MAX_PRIORITY(=10)

디폴트 우선 순위 : Thread.NORM_PRIORITY(=5)

(보통 Thread를 처음 돌렸을 때, 기본 값은 Thread.NORM_PRIORITY(=5) 를 가지게 된다.)

 

setPriority(int newPriority) -> setPriority를 통해 Priority를 지정 할 수도 있다.

int getPriority() -> get을 통해 Priority를 가져올 수도 있다.

 

우선 순위가 높은 Thread는 CPU를 배분 받을 확률이 높음

 

 

Thread.currentThread() -> (현재 main이 돌고 있는 쓰레드를 가지고 올 수 있고 static method 이다.)

ㅁ    public static void main(String[] args) {

        System.out.println("start");
//        MyThread th1 = new MyThread();
//        MyThread th2 = new MyThread();
//
//        th1.start();
//        th2.start();

//        MyThread runner1 = new MyThread();
//        Thread th1 = new Thread(runner1);
//        th1.start();
//
//        MyThread runner2 = new MyThread();
//        Thread th2 = new Thread(runner2);
//        th2.start();
		
        // static method, 현재 main이 돌고 있는 쓰레드를 가지고 올 수 있다.
        Thread t = Thread.currentThread(); 
        System.out.println(t);

        System.out.println("end");
    }
}       
// 출력
start
Thread[main,5,main]
end

Thread[main (Thread name) ,5 (Thread 우선순위), main (Thread가 어느 그룹에 속해 있는지)] 

(특별히 우선순위를 지정해 주지 않으면 5가 된다)

Join() 메서드

하나의 thread가 다른 thread의 결과를 보고 진행해야 하는 일이 있는 경우에 join() 메서드를 활용

join() 메서드를 호출한 thread가 non-runnable 상태가 됨.

 

 

package thread;

public class JoinTest extends Thread{

    int start;
    int end;
    int total;

    public JoinTest(int start, int end) { // 어디서부터 어디까지 더할건지
        this.start = start;
        this.end = end;
    }

    public void run() {
        int i;
        for (i = start; i <= end; i++) {
            total += i;
        }
    }

    public static void main(String[] args) {

        JoinTest jt1 = new JoinTest(1, 50);
        JoinTest jt2 = new JoinTest(51, 100);

        jt1.start();
        jt2.start();

//        try {
//            jt1.join();
//            jt2.join();
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }

        int total = jt1.total + jt2.total;
        System.out.println("jt1.total = " + jt1.total);
        System.out.println("jt2.total = " + jt2.total);

        System.out.println(total);


    }
}
jt1.total = 0
jt2.total = 3775
0

위 예제는 중복 연산으로 인해 jt1값이 무시가 되어버린다. 쉐어드 리소스가 발생하기 때문에 jt1에 값도 들어가지 않을 뿐더러 total값도 0이 된다. 실행을 할 때 마다 total에 0이 들어올때도, 3775가 들어올떄도 있지만,  쉐어드 리소스가 발생하기 때문에 정확한 값이 들어오지 않는다. (원래는 jt1 = 1275 , jt2 = 3775, total = 5050이 되어야 한다.)

 

아래는 이 문제를 해결하기 위해 join()메서드를 사용하여 올바른 값을 얻은 예제이다.

package thread;

public class JoinTest extends Thread{

    int start;
    int end;
    int total;

    public JoinTest(int start, int end) { // 어디서부터 어디까지 더할건지
        this.start = start;
        this.end = end;
    }

    public void run() {
        int i;
        for (i = start; i <= end; i++) {
            total += i;
        }
    }

    public static void main(String[] args) {

        JoinTest jt1 = new JoinTest(1, 50);
        JoinTest jt2 = new JoinTest(51, 100);

        jt1.start();
        jt2.start();

        try {
            jt1.join();
            jt2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        int total = jt1.total + jt2.total;

        System.out.println("jt1.total = " + jt1.total);
        System.out.println("jt2.total = " + jt2.total);

        System.out.println(total);


    }
}
jt1.total = 1275
jt2.total = 3775
5050

위에서 말했듯이 Thread는 t1, t2, main 3개가 수행이 되는데

jt1, jt2에 join()을 걸게되면 얘네는 하나씩 수행이되고, 메인은 잠시 기다리게 된다.

그래서 수행이 끝나고main이 실행되면서 total이 정상적으로 실행이 된다.

interrupt() 메서드

  • 다른 thread에 예외를 발생시키는 interrupt를 보냄
  • thread가 join(), sleep(), wait() 메서드에 의해 블럭킹 되었디면 interrupt에 의해 다시 runnable 상태가 될 수 있음
package thread;

public class InterruptTest extends Thread{

    public void run() {
        int i;
        for(i = 0; i < 100; i++) {
            System.out.println(i);
        }
        try {
            sleep(5000);
        } catch (InterruptedException e) {
            System.out.println(e);
            System.out.println("Wake!!!");
        }
    }

    public static void main(String[] args) {
        InterruptTest test = new InterruptTest();
        test.start();
        // test.interrupt();

        System.out.println("end");
    }
}

이 클래스를 실행하면 콘솔창 첫 줄에 end가 찍히고, 그 후 0 ~ 99 순으로 찍힌다. 그리고 sleep(5000)을 해주었기 때문에 5초후에 프로그램이 종료된다.

 

하지만 주석처리한 test.interrupt(); 로직을 주석을 해제한 후, 실행시키면 

end가 찍히고 0 ~ 99가 찍힌 후 interrupt 메서드를 호출하기 때문에 익셉션이 발생하게 되어,

5초 후에 프로그램이 종료되지 않고, 99가 찍힌 후에 바로 

java.lang.InterruptedException: sleep interrupted
Wake!!!

가 콘솔창에 찍히고 프로그램이 종료가 된다.

 

Thread 종료하기

  • 데몬등 무한 반복하는 thread가 종료될 수 있도록 run() 메서드 내의 while 문을 활용, Thread.stop()은 사용하지 않음
  • Thread.stop()으로 종료하기 보다는 서비스를 만들어서 종료하는게 바람직하다.
  • 대부분 while문 안에 flag를 써서 (true, false) 반복 및 종료한다.

 

package thread;

import java.io.IOException;

public class TerminateThread extends Thread{

    private boolean flag = false;
    int i;

    public TerminateThread(String name) { // Thread에 name을 붙여준다.
        super(name);  // thread construct 중에 Thread name을 받을 수 있는 생성자가 있다.
    }
    public void run() {
        while(!flag) {

            try {
                sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(getName() + " end");
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public static void main(String[] args) throws IOException {
        TerminateThread threadA = new TerminateThread("A");
        TerminateThread threadB = new TerminateThread("B");

        threadA.start();
        threadB.start();

        int in;
        while(true) {
            in = System.in.read();
            if (in == 'A') {
                threadA.setFlag(true);
            }else if(in == 'B'){
                threadB.setFlag(true);
            }else if(in == 'M') {
                threadA.setFlag(true);
                threadB.setFlag(true);
                break;
            }else {
                System.out.println("try again"); // A를 했을 때, /n에 걸려 "try again"이 출력된다.
            }
        }
        System.out.println("main end");
    }
}
// 출력
A
try again
A end
B
try again
B end
d
try again
try again
M
main end

Process finished with exit code 0

 위 예제는 while문에 boolean형 변수 flag를 이용하여 Thread를 반복 및 종료 하는 예제이다.

 

임계 영역(critical section)

- 두 개 이상의 thread가 동시에 접근하게 되는 리소스

- critical section에 동시에 thread가 접근하게 되면 실행 결과를 보장할 수 없음

- thread간의 순서를 맞추는 동기화(synchronization)이 필요

 

동기화(Synchronization)

- 임계 영역에 여러 thread가 접근 하는 경우 한 thread가 수행 하는 동안 공유 자원을 lock 하려 다른 thread의 접근을    막음

- 동기화를 잘못 구현하면 deadlcok에 빠질 수 있음

 

Critical section과 동기화

자바에서 동기화 구현

synchronized 수행문과 synchronized 메서드를 이용

 

synchronized 수행문

          synchronized(참조형 수식) {

          }

         참조형 수식에 해당되는 객체에 lock를 건다

 

synchronized 메서드

- 현재 이 메서드가 속해 있는 객체에 lock을 건다.

- synchronized 메서드 내에서 다른 synchronized 메서드를 호출하지 않는다.

(deadlock 방지를 위해)

 

왜 동기화가 필요한지 살펴볼까?

package thread;

class Bank {
    private int money = 10000;

    public void saveMoney(int save) {
        int m = this.getMoney();

        try {
            Thread.sleep(3000);      // 저축이 되는데 3초의 시간이 걸린다.
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        setMoney(m + save);
    }

    public void minusMoney(int minus) {   // 돈이 줄어드는데 0.2초의 시간이 걸린다.
        int m = this.getMoney();

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        setMoney(m - minus);
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }
}

// 한 사람은 적금을 하고 한사람은 출금
class Jung extends Thread {
    public void run() {
        System.out.println("start save");
        SyncTest.myBank.saveMoney(3000);
        System.out.println("save money: " + SyncTest.myBank.getMoney());
    }
}

class JungWife extends Thread{
    public void run() {
        System.out.println("start minus");
        SyncTest.myBank.minusMoney(1000);
        System.out.println("minus money: " + SyncTest.myBank.getMoney());
    }
}


public class SyncTest {
    // 한 사람은 적금을 하고 한사람은 출금
    public static Bank myBank = new Bank();

    public static void main(String[] args) throws InterruptedException{
        // Thread 호출
        Jung j = new Jung();
        j.start();

        Thread.sleep(200);
        JungWife jw = new JungWife();
        jw.start();
    }
}
// 출력
start save
start minus
minus money: 9000
save money: 13000

 

 우리가 기대하는 정상적인 값은 minus money가 12000이고 

 

왜 그랬을까?

 

정씨가 시작하고 나서 0.2초 후에 정씨 와이프가 시작이 되었다. 그런데 이제 정씨의 과정을 보면

세이브 머니를 하게 되는데 3초동안 쉰다. 쉬는동안 정씨 와이프가 시작(minusMoney)을 한다. 시작하고 나서

돈을 가져왔는데 세이브된 머니를 가져온게 아니라 그냥 만원을 가져왔다.

즉, 처음에 둘다 만원을 가지고 있다. (m에 getMoney를 했기 때문에) 그리고 0.2초 후에 정씨 와이파는 1000을 마이너스 해서 9000이라고 업데이트를 했지만, 3초후에 정씨는 그냥 m에다가 3000을 save했다.

 

출력문을 save를 하고 결과가 적용을 하기전에 minus를 하고 save money가 된다. 동기화가 적용이 안된 것이다.

critical section이 Bank가 되는데 Bank에 대한 자원이 정씨와 정씨 와이프에게 공유가 됬다. 공유가 된 상태에서 서로에 관해 순서가 안 맞았기 때문에 중간에 데이터 값이 무시가 되고 처음에 가져왔던 만원에서 3000만 더했다.

 

그럼 어떻게 해야할까? 동기화(synchronized)를 해주면 된다. (아래는 synchronized 수행문 방식)

 

seveMoney(), minusMoney() 메서드에 lock를 건다. 이렇게 되면, 두 메서드가 속해있는 Bank에 lock가 걸린다.

그럼 먼저 시작된 메서드가 수행이 될 때 다른 메서드는 Bank에 접근 할 수가 없다. (Jung씨가 실행을 할때 Jung씨 wife는 Bank에 접근을 할 수 없음)

package thread;

class Bank {
    private int money = 10000;

    public synchronized void saveMoney(int save) {
        int m = this.getMoney();

        try {
            Thread.sleep(3000);      // 저축이 되는데 3초의 시간이 걸린다.
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        setMoney(m + save);
    }

    public synchronized void minusMoney(int minus) {   // 돈이 줄어드는데 0.2초의 시간이 걸린다.
        int m = this.getMoney();

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        setMoney(m - minus);
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }
}

// 한 사람은 적금을 하고 한사람은 출금
class Jung extends Thread {
    public void run() {
        System.out.println("start save");
        SyncTest.myBank.saveMoney(3000);
        System.out.println("save money: " + SyncTest.myBank.getMoney());
    }
}

class JungWife extends Thread{
    public void run() {
        System.out.println("start minus");
        SyncTest.myBank.minusMoney(1000);
        System.out.println("minus money: " + SyncTest.myBank.getMoney());
    }
}


public class SyncTest {
    // 한 사람은 적금을 하고 한사람은 출금
    public static Bank myBank = new Bank();

    public static void main(String[] args) throws InterruptedException{
        // Thread 호출
        Jung j = new Jung();
        j.start();

        Thread.sleep(200);
        JungWife jw = new JungWife();
        jw.start();
    }
}
// 출력
start save
start minus
save money: 13000
minus money: 12000

돈이 save, minus 되고나서 기다렸다가 save money, minus money가 출력이 된다.

 

synchronized 블락 방식 (아래는 synchronized 메서드 방식)

어느 객체에 lock를 걸지?

package thread;

class Bank {
    private int money = 10000;

    public void saveMoney(int save) {
        synchronized (this) {
            int m = this.getMoney();

            try {
                Thread.sleep(3000);      // 저축이 되는데 3초의 시간이 걸린다.
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            setMoney(m + save);
        }
    }

    public synchronized void minusMoney(int minus) {   // 돈이 줄어드는데 0.2초의 시간이 걸린다.
        int m = this.getMoney();

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        setMoney(m - minus);
    }

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }
}

// 한 사람은 적금을 하고 한사람은 출금
class Jung extends Thread {
    public void run() {
        System.out.println("start save");
        SyncTest.myBank.saveMoney(3000);
        System.out.println("save money: " + SyncTest.myBank.getMoney());
    }
}

class JungWife extends Thread{
    public void run() {
        System.out.println("start minus");
        SyncTest.myBank.minusMoney(1000);
        System.out.println("minus money: " + SyncTest.myBank.getMoney());
    }
}


public class SyncTest {
    // 한 사람은 적금을 하고 한사람은 출금
    public static Bank myBank = new Bank();

    public static void main(String[] args) throws InterruptedException{
        // Thread 호출
        Jung j = new Jung();
        j.start();

        Thread.sleep(200);
        JungWife jw = new JungWife();
        jw.start();
    }
}

결과 값은 똑같다.

synchronized 메서드 방식보다는 synchronized 블락방식이 조금더 선호 될 수 있다.

 

만약 Bank클래스 안에 메서드에 synchronized 메서드를 이용해 lock 거는 것이 아니라  Jung 클래스에 run()에 걸게 되면

class Jung extends Thread {
    public synchronized void run() {
        System.out.println("start save");
        SyncTest.myBank.saveMoney(3000);
        System.out.println("save money: " + SyncTest.myBank.getMoney());
    }
}

Jung클래스 리소스에 lock을 거는 것이라 의미가 없다. Thread에 lock을 걸게 되는 것이다

 

쉐어드 리소스는 

public static Bank myBank = new Bank();

이기 때문에 Bank 클래스에서 걸어주어야 한다.

 

만약 Jung 클래스에 lock을 걸고 싶다면, synchronized 메서드 방식 말고 synchronized 블락 방식을 이용해야한다.

class Jung extends Thread{
    public void run() {
        synchronized (SyncTest.myBank) {
            System.out.println("start save");
            SyncTest.myBank.minusMoney(3000);
            System.out.println("save money: " + SyncTest.myBank.getMoney());
        }
    }
}

이렇게 synchronized 블락 방식을 이용해주면 정상적으로 결과값이 출력이 된다.

 

deadlcok

wait() / notify()

wait(), notify()

 

wait() : 리소스가 더 이상 유효하지 않은 경우 리소스가 사용 가능할 때 까지 위해 thread를 non-runnable 상태로 전환

          wait() 상태가 된 thread는 notify() 가 호출 될 때까지 기다린다.

 

notify() : wait() 하고 있는 thread 중 한 thread를 runnable 한 상태로 깨움 (thread 메서드는 아니고 object 메서드다.)

            (오래된 쓰레드를 깨우는게 아니라 아무거나 하나 깨운다.)

 

notifyAll() : wait() 하고 있는 모든 thread가 runnable 한 상태가 되도록 함

               notify() 보다 notifyAll()을 사용하기를 권장

               특정 thread가 통지를 받도록 제어 할 수 없으므로 모두 깨운 후

               scheduler에 CPU를 점유하는 것이 좀 더 공평하다고 함

 

notify()와 notifyAll()의 차이는 하나만 깨운다, 전부 다 깨운다의 차이

 

notify()보다는 notifyAll()을 쓰는 것을 권장한다. notify()는 아무거나 깨우기 때문에,

notifyAll()을 이용해서 자고 있는 쓰레드를 모두 깨우고 그 쓰레드들이 경쟁을 해서 어떤 쓰레드가 CPU를 점유를 하면 나머지는 다시 Wait를 하는 것이 더 효율적이다.

 

도서관 예제를 통해 살펴보겠다.

책은 한정적이고 학생들은 많을 때, 어떻게 기다리는지 notify()와 notifyAll()을 이용해 살펴보겠다.

 

package thread;

import java.util.ArrayList;

class AnyangLibrary {
    // 책들이 있다.
    public ArrayList<String> books = new ArrayList<String>();

    public AnyangLibrary() {
        books.add("자바의 정석 1");
        books.add("자바의 정석 2");
        books.add("자바의 정석 3");

    }

    public synchronized String lendBook() {

        Thread t = Thread.currentThread();

        String title = books.remove(0);
        System.out.println(t.getName() + ": " + title + " lend");
        return title;
    }

    public synchronized void returnBook(String title) {
        Thread t = Thread.currentThread();
        books.add(title);

        System.out.println(t.getName() + ": " + title + " return");
    }
}

class Student extends Thread {

    public void run() {

        try {
            String title = LibraryMain.library.lendBook();
            sleep(5000); // 빌려오면 5초동안 책을 읽는다.
            LibraryMain.library.returnBook(title); // 반납을 한다.
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class LibraryMain {
    public static AnyangLibrary library = new AnyangLibrary();
    public static void main(String[] args) {

        Student std1 = new Student();
        Student std2 = new Student();
        Student std3 = new Student();
        Student std4 = new Student();
        Student std5 = new Student();
        Student std6 = new Student();

        std1.start();
        std2.start();
        std3.start();
        std4.start();
        std5.start();
        std6.start();

    }
}

위 예제는 리소스는 한정적인데 그 리소스를 사용하려고 하는 쓰레드는 여러개인 경우이다. 이럴 경우에

먼저 빌린애는 빌렸는데, 나머지 애들은 없는데서 빌리려고 하기 때문에 IndexOutOfBoundsException 오류가 발생하게 된다.

 

이런 경우에 우리가 할 수 있는 것은, 우선 리소스가 가능하지 않으면 못 빌리게 해주어야 한다.

package thread;

import java.util.ArrayList;

class AnyangLibrary {
    // 책들이 있다.
    public ArrayList<String> books = new ArrayList<String>();

    public AnyangLibrary() {
        books.add("자바의 정석 1");
        books.add("자바의 정석 2");
        books.add("자바의 정석 3");

    }

    public synchronized String lendBook() {

        Thread t = Thread.currentThread();
        if(books.size() == 0) {
            return null;
        }
        String title = books.remove(0);
        System.out.println(t.getName() + ": " + title + " lend");
        return title;
    }

    public synchronized void returnBook(String title) {
        Thread t = Thread.currentThread();
        books.add(title);

        System.out.println(t.getName() + ": " + title + " return");
    }
}

class Student extends Thread {

    public void run() {
        try {
            String title = LibraryMain.library.lendBook();
            if(title == null) {
                return;
            }
            sleep(5000); // 빌려오면 5초동안 책을 읽는다.
            LibraryMain.library.returnBook(title); // 반납을 한다.
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class LibraryMain {
    public static AnyangLibrary library = new AnyangLibrary();
    public static void main(String[] args) {

        Student std1 = new Student();
        Student std2 = new Student();
        Student std3 = new Student();
        Student std4 = new Student();
        Student std5 = new Student();
        Student std6 = new Student();

        std1.start();
        std2.start();
        std3.start();
        std4.start();
        std5.start();
        std6.start();

    }
}
Thread-0: 자바의 정석 1 lend
Thread-5: 자바의 정석 2 lend
Thread-4: 자바의 정석 3 lend
Thread-0: 자바의 정석 1 return
Thread-5: 자바의 정석 2 return
Thread-4: 자바의 정석 3 return

하지만 이럴경우 그냥 못 빌린 것으로 끝난다. thread 0, 5, 4는 빌렸고 나머지는 못 빌린 것으로 끝난다. 도서관에 갔는데 책이 없어서 못 빌린..

 

하지만 저 책을 꼭 읽어야겠고, 꼭 빌려야 겠다 한다면 '책이 오면 연락해주세요' 라고 하고 책이 오면 연락하게끔 wait()를 이용하면 된다. 그 후 책이 오면, '책이 왔어요' 라고 notify()로 알려준다. 

 

package thread;

import java.util.ArrayList;

class AnyangLibrary {
    // 책들이 있다.
    public ArrayList<String> books = new ArrayList<String>();

    public AnyangLibrary() {
        books.add("자바의 정석 1");
        books.add("자바의 정석 2");
        books.add("자바의 정석 3");

    }

    public synchronized String lendBook() throws InterruptedException {

        Thread t = Thread.currentThread();
        if(books.size() == 0) {
            System.out.println(t.getName() + " waiting start"); // 어떤 thread가 기다리는지
            wait();  // Thread.currentThread();를 기다리게 한다
            System.out.println(t.getName() + " waiting end"); // wait()이 끝나고 어느게 기다렸다가 끝났는지
        }
        String title = books.remove(0);
        System.out.println(t.getName() + ": " + title + " lend");
        return title;
    }

    public synchronized void returnBook(String title) {
        Thread t = Thread.currentThread();
        books.add(title);
        notify(); // 책이 왔다.
        System.out.println(t.getName() + ": " + title + " return");
    }
}

class Student extends Thread {

    public void run() {
        try {
            String title = LibraryMain.library.lendBook();
            if(title == null) {
                return;
            }
            sleep(5000); // 빌려오면 5초동안 책을 읽는다.
            LibraryMain.library.returnBook(title); // 반납을 한다.
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class LibraryMain {
    public static AnyangLibrary library = new AnyangLibrary();
    public static void main(String[] args) {

        Student std1 = new Student();
        Student std2 = new Student();
        Student std3 = new Student();
        Student std4 = new Student();
        Student std5 = new Student();
        Student std6 = new Student();

        std1.start();
        std2.start();
        std3.start();
        std4.start();
        std5.start();
        std6.start();

    }
}
// 출력
Thread-0: 자바의 정석 1 lend
Thread-5: 자바의 정석 2 lend
Thread-1: 자바의 정석 3 lend
Thread-2 waiting start
Thread-4 waiting start
Thread-3 waiting start
Thread-1: 자바의 정석 3 return  // 1이 하나 반납해서
Thread-2 waiting end           // 2가 waitting end가 되어
Thread-2: 자바의 정석 3 lend    // 가 빌렸다.
Thread-5: 자바의 정석 2 return
Thread-0: 자바의 정석 1 return
Thread-3 waiting end
Thread-3: 자바의 정석 2 lend
Thread-4 waiting end
Thread-4: 자바의 정석 1 lend
Thread-2: 자바의 정석 3 return
Thread-4: 자바의 정석 1 return
Thread-3: 자바의 정석 2 return

근데 위에서 말한 것 처럼 이렇게 notify()에 의해 아무나 한 명씩 깨어나게 되면 문제가 발생할 수 있다. 

그래서 notifyAll()를 이용하게 되는데 notifyAll()을 이용하게 되면 모든 쓰레드가 일어난다.

한 명만 반납을 하는데 모두 일어나면, 문제가 될 수 있다. 이를 해결하기 위해 못 빌린 애들은 다시 wait() 상태로 빠뜨린다. lendbook() 메서드에서 if문을 while문으로 바꾸어주면

package thread;

import java.util.ArrayList;

class AnyangLibrary {
    // 책들이 있다.
    public ArrayList<String> books = new ArrayList<String>();

    public AnyangLibrary() {
        books.add("자바의 정석 1");
        books.add("자바의 정석 2");
        books.add("자바의 정석 3");

    }

    public synchronized String lendBook() throws InterruptedException {

        Thread t = Thread.currentThread();
        while(books.size() == 0) {                              // 못빌린 애들은 다시 wait() 상태로 빠뜨린다.
            System.out.println(t.getName() + " waiting start"); // 어떤 thread가 기다리는지
            wait();  // Thread.currentThread();를 기다리게 한다
            System.out.println(t.getName() + " waiting end"); // wait()이 끝나고 어느게 기다렸다가 끝났는지
        }
        String title = books.remove(0);
        System.out.println(t.getName() + ": " + title + " lend");
        return title;
    }

    public synchronized void returnBook(String title) {
        Thread t = Thread.currentThread();
        books.add(title);
        notify(); // 책이 왔다.
        System.out.println(t.getName() + ": " + title + " return");
    }
}

class Student extends Thread {

    public void run() {
        try {
            String title = LibraryMain.library.lendBook();
            if(title == null) {
                return;
            }
            sleep(5000); // 빌려오면 5초동안 책을 읽는다.
            LibraryMain.library.returnBook(title); // 반납을 한다.
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class LibraryMain {
    public static AnyangLibrary library = new AnyangLibrary();
    public static void main(String[] args) {

        Student std1 = new Student();
        Student std2 = new Student();
        Student std3 = new Student();
        Student std4 = new Student();
        Student std5 = new Student();
        Student std6 = new Student();

        std1.start();
        std2.start();
        std3.start();
        std4.start();
        std5.start();
        std6.start();

    }
}
Thread-0: 자바의 정석 1 lend
Thread-5: 자바의 정석 2 lend
Thread-3: 자바의 정석 3 lend
Thread-4 waiting start
Thread-2 waiting start
Thread-1 waiting start
Thread-3: 자바의 정석 3 return
Thread-5: 자바의 정석 2 return
Thread-0: 자바의 정석 1 return
Thread-4 waiting end
Thread-4: 자바의 정석 3 lend
Thread-1 waiting end
Thread-1: 자바의 정석 2 lend
Thread-2 waiting end
Thread-2: 자바의 정석 1 lend
Thread-1: 자바의 정석 2 return
Thread-2: 자바의 정석 1 return
Thread-4: 자바의 정석 3 return

출력을 보면 다 깨어났다가 하나만 불리게 되면 다시 wait()에 빠진다.

728x90
반응형
Comments