본문 바로가기

Java의 정석 : 3rd Edition

[Java의 정석 - 연습문제] Chapter13. 쓰레드(thread)

Java의 정석 : 3rd Edition, 2016을 개인 학습용으로 정리한 내용입니다.
"ppt 파일 자료, 연습문제"를 학습 용도로 배포할 수 있음을 고지하셨습니다.
저자님께 감사드립니다.

 

 

 

 

[13-1] 쓰레드를 구현하는 방법에는 Thread클래스로부터 상속받는 것과 Runnable인터페이스를 구현하는 것 두 가지가 있는데, 다음의 코드는 Thread클래스를 상속받아서 쓰레드를 구현한 것이다.

이 코드를 Runnable인터페이스를 구현하도록 변경하시오.

class Exercise13_1 {
    public static void main(String args[]) {
        Thread th1 = new Thread1();
        
        th1.start();
    }
}

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

 

답 :

Thread(Runnable r) run()의 구현체(Thread1)를 인자로 전달하면 쓰레드가 구현한 run() 호출한다.

Runnable인터페이스를 구현한 클래스의 인스턴스를 생성한다.

② 생성한 인스턴스를 Thread(Runnable r) 생성자의 인자로 제공한다.

class Exercise13_1 {
    public static void main(String args[]) {
        Runnable r = new Thread1();  // Runnable 인터페이스를 구현한 클래스 생성
        Thread th1 = new Thread(r);  // Thread(Runnable r)를 이용하여 쓰레드 생성
        // Thread th1 = new Thread(new Thread1());

        th1.start();
    }
}

class Thread1 implements Runnable {  // Runnable인터페이스를 구현하여 쓰레드를 구현
    public void run() {  // 쓰레드가 수행할 작업
        for(int i = 0; i < 300; i++) {
            System.out.print('-');
        }
    }
}

 

 

 

 

[13-2] 다음 코드의 실행 결과로 옳은 것은?

class Exercise13_2 {
    public static void main(String[] args) {
        Thread2 t1 = new Thread2();
        t1.run();

        for(int i = 0; i < 10; i++)
            System.out.print(i);
    }
}

class Thread2 extends Thread {
    public void run() {
        for(int i = 0; i < 10; i++)
            System.out.print(i);
    }
}

a. 01021233454567689789처럼 0부터 9까지의 숫자가 섞여서 출력된다.

b. 01234567890123456789처럼 0부터 9까지의 숫자가 순서대로 출력된다.

c. IllegalThreadStateException이 발생한다.

 

답 : b

b. run() : 구현한 run()를 직접 호출하면 Thread2클래스에 선언된 run() 호출된다(일반적인 메서드 호출)

a. start() : start()를 호출해야 새로 생성되는 호출 스택에 run()가 올라가게 된다(실행 대기 상태)

              : 실행 대기 상태인 쓰레드는 OS 스케줄러에 의해 실행된다(어떤 쓰레드가? 얼마동안 실행?될지를 결정)

              : 모든 프로세스와 쓰레드의 상황을 고려하여 스케줄링하므로 실행 순서가 실행할 때마다 다르다(불확실성)

              : main ↔ run가 번갈아가며 작업을 수행하기 때문에 숫자가 섞여 출력된다.

a의 호출 스택(call stack)
a의 호출 스택(call stack)
b의 호출 스택(call stack)
b의 호출 스택(call stack)

 

[13-3] 다음 중 쓰레드를 일시 정지 상태(WAITING)로 만드는 것이 아닌 것은? (모두 고르시오)

a. suspend()

b. resume()

c. join()

d. sleep()

e. wait()

f. notify()

 

답 : b, f

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

f. notify() : wait()에 의해 일시 정지 상태가 된 쓰레드를 실행 대기 상태로 만든다.


a. suspend() : 지정된 시간동안 쓰레드를 일시 정지시킨다(non-static : 특정 쓰레드에 적용 가능)

                     : 지정된 시간이 지나거나 resume()를 호출하면 실행 대기 상태가 된다.

 

c. join() : 지정된 시간동안 진행하던 작업을 잠시 멈추고, 다른 쓰레드의 작업을 먼저 수행한다.

             : 시간을 지정하지 않으면, 다른 쓰레드의 작업이 끝날 때까지 기다린다.

             : 지정된 시간이 지나거나 다른 쓰레드의 작업이 끝나면 join()를 호출한 쓰레드로 돌아와 남은 작업을 수행한다.

             : interrupt()를 호출(interruptedException 발생)하면 실행 대기 상태가 된다.

               
d. sleep() : 지정된 시간동안 쓰레드를 일시 정지시킨다(static : 현재(this) 쓰레드에 적용 가능)

                : 지정된 시간이 지나거나 interrupt()를 호출(interruptedException 발생)하면 실행 대기 상태가 된다.

 

e. wait() : 작업을 진행할 수 없는 상황일 때 지정된 시간동안 락을 반납하여 다른 쓰레드의 작업이 끝날 때까지 기다린다.

                예) 출금을 해야 하는데 통장 잔고가 부족한 경우

                      A(출금) : 락을 해제하여 입금 될 때까지 기다림 wait() → B에게 입금해달라 요청 notify()

                      B(입금) : 통장 잔고에 입금 → 입금 했음을 A에게 알림 notify() → A가 출금 작업 수행

              : 다른 Thread가 임계 영역의 락을 얻어 해당 객체에 대한 작업을 수행한다.

              : 지정된 시간이 지나면 자동적으로 notify()가 호출된다.

              : 지정된 시간이 지나거나 notify() or notifyAll()를 호출하여 임계 영역의 락을 다시 얻는다(남은 작업 수행)

 

 

 

 

[13-4] 다음 중 interrupt()에 의해 실행 대기 상태(RUNNABLE)가 되지 않는 경우는? (모두 고르시오)

a. sleep()에 의해 일시 정지 상태인 쓰레드

b. join()에 의해 일시 정지 상태인 쓰레드

c. wait()에 의해 일시 정지 상태인 쓰레드

d. suspend()에 의해 일시 정지 상태인 쓰레드

 

답 : d

interrupt()는 진행 중인 쓰레드의 작업을 중단시키거나, 중단된 쓰레드를 재개할 때 사용한다.

* suspend()를 제외한 나머지 메서드들은 interrupt()가 호출되면 InterruptedException가 발생한다.

* InterruptedException 발생으로 인해 일시 정지 상태에서 벗어나게 되어 실행 대기 상태가 된다.

* InterruptedExceptionException의 자손(예외 처리 필수)이므로 반드시 try-catch문으로 예외 처리를 해줘야 한다.

suspend()는 왜 해당되지 않을까?

 

 

 

 

[13-5] 다음의 코드를 실행한 결과를 예측하고, 직접 실행한 결과와 비교하라.

만일 예측한 결과와 실행한 결과의 차이가 있다면 그 이유를 설명하라.

class Exercise13_5 {
    public static void main(String[] args) throws Exception {
        Thread3 th1 = new Thread3();
        th1.start();

        try {
            Thread.sleep(5 * 1000);
        } catch(Exception e) {}

        throw new Exception("꽝~!!!");
    }
}

class Thread3 extends Thread {
    public void run() {
        for(int i = 0; i < 10; i++) {
            System.out.println(i);

            try {
                Thread.sleep(1000);
            } catch(Exception e) {}
        }
    }
}

 

답 : 예외가 발생된 main 쓰레드는 종료되고, th1 쓰레드는 진행중인 작업을 다 끝마친 후에 종료된다.

     : main 쓰레드th1 쓰레드는 별도의 호출 스택(call stack)에서 실행된다.

     : 한 쓰레드가 예외 발생으로 인해 종료되어도 다른 쓰레드의 실행에 영향을 미치지 않는다.

     : 예외가 발생하여 종료된 main의 호출 스택이 없어져도, 쓰레드 th1이 실행되는 호출 스택에 영향을 미치지 않는다.

main과 th1의 호출 스택(call stack)
main과 th1의 호출 스택(call stack)

0
1
2
3
4
Exception in thread "main" java.lang.Exception: 꽝~!!!
	at Exercise.Exercise13.Exercise13_5.main(Exercise13_5.java:16)
5
6
7
8
9

 

 

 

 

[13-6] 다음의 코드를 실행한 결과를 예측하고, 직접 실행한 결과와 비교하라.

만일 예측한 결과와 실행한 결과의 차이가 있다면 그 이유를 설명하라.

class Exercise13_6 {
    public static void main(String[] args) throws Exception {
        Thread4 th1 = new Thread4();
        th1.setDaemon(true);  // 데몬 쓰레드로 설정
        th1.start();

        try {
            th1.sleep(5 * 1000);
        } catch(Exception e) {}

        throw new Exception("꽝~!!!");
    }
}

class Thread4 extends Thread {
    public void run() {
        for(int i = 0; i < 10; i++) {
            System.out.println(i);

            try {
                Thread.sleep(1000);
            } catch(Exception e) {}
        }
    }
}

 

답 : 예외가 발생하여 main 쓰레드가 종료될 때 Thread4(th1) 쓰레드가 자동 종료되어 프로그램이 종료된다.

     : main 쓰레드(일반 쓰레드)가 종료됨과 동시에 쓰레드 th1가 자동 종료된다.
     : 데몬 쓰레드는 일반 쓰레드(데몬 쓰레드가 아닌 쓰레드)가 모두 종료되면 자동 종료된다.

0
1
2
3
4
Exception in thread "main" java.lang.Exception: 꽝~!!!
	at Exercise.Exercise13.Exercise13_6.main(Exercise13_6.java:17)

 

 

 

 

[13-7] 다음의 코드는 쓰레드 th1을 생성해서 실행시킨 다음 6초 후에 정지시키는 코드이다.

그러나 실제로 실행시켜보면 쓰레드를 정지시킨 다음에도 몇 초가 지난 후에서야 멈춘다.

그 이유를 설명하고, 쓰레드를 정지시키면 바로 정지되도록 코드를 개선하시오.

class Exercise13_7 {
    static boolean stopped = false;

    public static void main(String[] args) {
        Thread5 th1 = new Thread5();
        th1.start();

        try {
            Thread.sleep(6 * 1000);
        } catch(Exception e) {}

        stopped = true;  // 쓰레드를 정지시킨다.
        System.out.println("stopped");
    }
}

class Thread5 extends Thread {
    public void run() {
        // Exercise13_7.stopped의 값이 false인 동안 반복한다.
        for(int i = 0; !Exercise13_7.stopped; i++) {
            System.out.println(i);

            try {
                Thread.sleep(3 * 1000);
            } catch(Exception e) {}
        }
    }
}
// 0
// 1
// 2
// stopped

 

답 : stopped의 값을 true로 바꾸고, interrupt()를 호출하면 지연 없이 즉시 쓰레드를 종료시킬 수 있다.

 

th1은 반복문을 수행하다 main 쓰레드에서 stopped의 값을 true로 바꾸면 반복문을 빠져나와 수행을 종료하게 된다.

: 하지만, stopped의 값을 true로 바꾸는 것만으로는 쓰레드의 실행을 바로 종료시킬 수 없다.

: stopped의 값이 바뀌어도 th1 Thread.sleep(3 * 1000)에 의해 일시 정지 상태에 있다면,

  시간이 지나 일시 정지 상태에서 벗어날 때까지 반복문을 빠져나올 수 없다.

: 그러므로 interrupt()를 호출하여 자고 있는(sleep()에 의해 일시 정지 상태인) 쓰레드를 깨워야 즉시 정지하게 된다.

 

interrupt() : InterruptedException를 발생시킴으로써 Thread.sleep()에 의해 일시 정지 상태에 있던 쓰레드를 즉시 깨운다.

 

 

 

class Exercise13_7 {
    static boolean stopped = false;

    public static void main(String[] args) {
        Thread5 th1 = new Thread5();
        th1.start();

        try {
            Thread.sleep(6 * 1000);
        } catch(Exception e) {}

        stopped = true;   // 쓰레드를 정지시킨다.
        th1.interrupt();  // 일시 정지 상태에 있는 쓰레드를 깨운다.
        System.out.println("stopped");
    }
}

 

 

 

 

[13-8] 다음의 코드는 텍스트기반의 타자연습게임인데 WordGenerator라는 쓰레드가 Vector에 2초마다 단어를 하나씩 추가하고, 사용자가 단어를 입력하면 Vector에서 일치하는 단어를 삭제하도록 되어 있다.

WordGeneratorrun()을 완성하시오.

import java.util.*;

class Exercise13_8 {
    Vector words = new Vector();
    String[] data = {"태연", "유리", "윤아", "효연", "수영", "서현", "티파니", "써니", "제시카"};

    int interval = 2 * 1000; // 2초

    WordGenerator wg = new WordGenerator();

    public static void main(String args[]) {
        Exercise13_8 game = new Exercise13_8();

        game.wg.start();  // 단어를 생성하는 쓰레드를 실행시킨다.

        Vector words = game.words;

        while(true) {
            System.out.println(words);

            String prompt = ">>";
            System.out.print(prompt);

            // 화면으로부터 라인 단위로 입력받는다.
            Scanner s = new Scanner(System.in);
            String input = s.nextLine().trim();

            int index = words.indexOf(input);  // 입력받은 단어를 words에서 찾는다.

            if(index != -1) {         // 찾으면
                words.remove(index);  // words에서 해당 단어를 제거한다.
            }
        }
    }

    class WordGenerator extends Thread {
        public void run() {
            /*
                (1) 아래의 로직에 맞게 코드를 작성하시오.
                    1. interval(2초)마다 배열 data의 값 중 하나를 임의로 선택해서
                    2. words에 저장한다.
            */
        } // end of run()
    } // class WordGenerator
} // Exercise13_8
[]
>>
[서현]
>>서현
[수영, 윤아]
>>수영
[윤아, 유리]
>>유리
[윤아, 티파니]
>>티파니
[윤아, 윤아, 유리]
>>윤아
[윤아, 유리]
>>유리
[윤아, 효연]
>>효연
[윤아, 티파니]
>>윤아
[티파니, 윤아]
>>티파니
[윤아, 수영, 써니]
>>

 

답 : Math.random()를 이용하여 String[]에 저장된 값들 중 임의의 위치에 있는 요소를 words(Vector)에 저장한다.

class WordGenerator extends Thread {
    public void run() {
        while (true) {
            // 1. interval(2초)마다 배열 data의 값 중 하나를 임의로 선택해서
            int random = (int) (Math.random() * data.length);
            
            // 2. words에 저장한다.
            words.add(data[random]);  // String[] -> Vector에 저장

            try {
                Thread.sleep(interval);
            } catch(Exception e) {}
        }
    } // end of run()
} // class WordGenerator

 

 

 

 

[13-9] 다음은 사용자의 입력을 출력하고 종료하는 프로그램을 작성한 것, 10초 동안 입력이 없으면 자동종료되어야 한다.

그러나 실행결과를 보면, 사용자의 입력이 10초안에 이루어졌음에도 불구하고 프로그램이 즉시 종료되지 않는다.

사용자로부터 입력받는 즉시 프로그램이 종료되도록 수정하시오.

import javax.swing.JOptionPane;

class Exercise13_9 {
    public static void main(String[] args) throws Exception {
        Exercise13_9_1 th1 = new Exercise13_9_1();
        th1.start();

        String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
        System.out.println("입력하신 값은 " + input + "입니다.");
        th1.interrupt();  // 쓰레드에게 작업을 멈추라고 요청한다.
    }
}

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

        while(i != 0 && !isInterrupted()) {
            System.out.println(i--);

            try {
                Thread.sleep(1000);  // 1초 지연
            } catch(InterruptedException e) { }
        }
        System.out.println("카운트가 종료되었습니다.");
    }
}

[13-9] 실행 결과
[13-9] 실행 결과

 

답 : 쓰레드의 catch블럭에 interrupt()를 넣어 쓰레드의 interrupted상태를 다시 true로 바꿔줘야 한다.

 

sleep()에 의해 쓰레드가 잠시 멈춰있을 때, interrupt()가 호출되면 InterruptedException가 발생한다.

예외 발생으로 인해 일시 정지 상태이던 쓰레드가 실행 대기 상태가 되지만, interrupted상태가 false로 자동 초기화된다.

초기화로 인해 반복문을 계속 수행하게 되는 것.

catch블럭에서 interrupt()를 호출하면 interrupted상태가 true가 되므로, 반복문을 벗어나게 되어 쓰레드가 종료된다.

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

        while(i != 0 && !isInterrupted()) {
            System.out.println(i--);

            try {
                Thread.sleep(1000);  // 1초 지연
            } catch(InterruptedException e) {
                interrupt();  // 추가
            }
        }
        System.out.println("카운트가 종료되었습니다.");
    }
}