자바의 정석 3판 (13) 연습문제 : 쓰레드

NtoZ·2023년 4월 17일
0

Java

목록 보기
23/23
post-thumbnail

쓰레드 연습문제


💡13-1. Thread 상속 -> Runnable 구현으로 쓰레드 생성하기

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

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

        th1.start();
    }
}

class Thread1 extends Thread {
    public void run() {
        for (int i = 0; i < 300; i++) {
            System.out.print('-');
        }
    }
}
  • 문제 접근:
    Thread를 생성하는 방법에는 두 가지 방법이 있다.
    1. Thread 상속하여 run() 오버라이딩하기
    2. Runnable 구현하여 run() 구현하기
    ➡️ 최종적으로는 Thread th = new Thread();를 통해 쓰레드 객체를 생성 후 참조변수를 통해 .start()를 구현해야 한다.

    이 때, 2번의 경우 Runnable r = new 구현클래스();를 통해 Runnable 객체를 생성한 다음, Thread th = new Thread(Runnable target);으로 받아서 .start()를 동작시켜준다.

    💡또는 다형성을 적용하여 Thread th = new Thread(new Thread1());을 사용할 수 있다. (왜냐하면 Thread1 인스턴스가 결국 Runnable로 업캐스팅 될 수 있기 때문이다.)

  • 내 풀이:
class Sol_Exercise13_1 {
    public static void main(String args[]) {
//        Runnable r = new Thread1();
//        Thread th1 = new Thread(r);
        Thread th1 = new Thread(new Thread1());

        th1.start();
    }
}

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

class Thread1 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 300; i++) {
            System.out.println('-');
        }
    }
}
  • 정답:

13-2. thread.run()의 실행결과

  • 문제
//[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이 발생한다.
 */
  • 🔥 풀이 접근:
    해당 소스코드에서 쓰레드는 main 쓰레드th1 쓰레드로 각각 2개이다.
    동기화되지 않은 단일 자원(데이터, 출력)에 대한 멀티 쓰레딩은 두 쓰레드가 빠른 속도로 번갈아가며 수행되므로,
    a가 옳다.

  • 🔥 오답 복기:
    자세히 보니 ⭐t1.start()를 호출한 것이 아니라 t1.run()을 호출했다.
    t1.start()는 새 호출 스택을 만들어 쓰레드를 통한 run()을 동작시키는데 반면,
    t1.run()은 새 호출 스택을 만들지 않고 main쓰레드 안에서 run()을 구동시키므로,
    해당 run()이 끝나고 난 이후에 main 쓰레드의 for문이 동작한다.

    ➡️ 따라서 b가 옳다.

  • 정답 :
    [정답] b
    [해설]
    Thread2클래스의 인스턴스를 생성하긴 했지만, start()가 아닌 run()을 호출함으 로써 쓰레드를 실행시킨 것이 아니라 단순히 Thread2클래스의 메서드를 호출한 셈이 되었 다. 만일 run()이 아닌 start()를 호출하였다면, 숫자가 섞여서 출력되었을 것이다.


13-3. 쓰레드의 상태 : WAITING

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

//        a.	suspend()
//        b.	resume()
//        c.	join()
//        d.	sleep()
//        e.	wait()
//        f.	notify()
  • 풀이접근:
a. suspend()는 WAITING 상태로 만든다.
✔️b. resume()suspend()에 대응하여 WAITING 상태에서 깨운다.
c. join()은 다른 쓰레드가 작업을 마칠 때까지 쓰레드를 WAITING 상태로 만든다.
d. sleep()은 스태틱 메서드로 현재 쓰레드를 WAITING 상태로 만든다.
e. wait()은 실행 중이던 쓰레드를 해당 객체의 WAITING POOL에서 기다리게 한다.
✔️f. notify()는 해당 객체의 WAITING POOL에서 임의의 쓰레드를 깨운다.
  • 답:
<정답>
[정답] b, f
[해설] resume()suspend()의 호출로 인해 일시정지 상태가 된 쓰레드를 실행대기상태로 바꿔준다.
notify()역시 wait()의 호출로 인해 일시정지 상태가 된 쓰레드를 다시 실행대기 상태로 바꿔준다.
join()은 현재 실행 중인 쓰레드를 멈추고 다른 쓰레드가 실행 되도록 한다.

💡13-4. 쓰레드 제어 : interrupt()

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

       /* a.	sleep()에 의해서 일시정지 상태인 쓰레드
        b.	join()에 의해서 일시정지 상태인 쓰레드
        c.	wait()에 의해서 일시정지 상태인 쓰레드
        d.	suspend()에 의해서 일시정지 상태인 쓰레드*/

/*
  • 풀이접근
sleep(), join(), suspend()는 모두 InterruptedException 을 던지는 예외로,
interrupt()를 통해 WAITING 상태에서 벗어나게 된다.

그러나 `wait()`은 InterruptedException를 던지는 예외가 아니므로,
interrupt()로 인해서 WAITING POOL에서 깨어나지 않는다.
wait()을 깨우는 방법으로 notify()notifyAll()이 존재한다.
  • 정답
[정답] d
[해설] suspend()를 제외한 나머지 메서드들은 interrupt()가 호출되면 InterruptedException이 발생하여 일시정지 상태에서 벗어나 실행대기 상태가 된다.(try-catch문으로 InterruptedException을 처리해주어야 한다.)
  • 💡 오답 개념 정리:
    suspend()를 제외한 나머지 메서드들은 interrupt()가 호출되면 interruptedException이 발생하여 일시정지 상태에서 벗어나 실행대기 상태가된다. (try-catch문으로 InterruptedException을 처리해주어야 한다.)
  • 이 중 wait()은 Object 클래스의 메서드이다.

⭐13-5. 멀티 쓰레딩 동작 원리 알기

  • 문제:
//[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) {
            }
        }
    } // run()
}
  • 🔥풀이 접근:
<풀이접근>
Thread3run()0부터 9까지 숫자를 매 초마다 출력하는 메서드이다.
main 쓰레드에서 Thread3에 대한 쓰레드를 생성하여 (Thread3 th1 = new Thread3();)
th1.start();로 호출 스택 하나를 더 만들어 멀티쓰레딩이 구동되도록 했다.
join()이나 wait()이 메인 쓰레드에 존재하지 않으므로 main 쓰레드 역시 계속 동작한다.

main쓰레드는 5초를 쉬고,
새 예외를 고의 발생시킨다. (throw new Exception("꽝~!!!");)
main쓰레드 단의 예외는 JVM이 받는 것으로 생각된다.

th1 쓰레드는 동작을 멈출까? 아니다. 별도의 호출스택에서 동작하므로,
계속해서 9까지 숫자를 출력할 것이다.

<실행결과>
0
1
2
3
4
Exception in thread "main" java.lang.Exception:~!!!
	at Exercise13_5.main(Sol_Exercise13_5.java:14)
5
6
7
8
9
  • 정답:
    [해설] main과 th1 두 개의 쓰레드는 별도의 호출스택(call stack)에서 실행된다. 그래서 ⭐main에서 예외가 발생하여 종료되고 호출스택이 없어져도, 쓰레드 th1이 실행되는 호출스 택에는 아무런 영향을 미치지 못한다.

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) {
            }
        }
    } // run()
}
  • 🔥내 풀이:
Thread4run()0에서 9까지의 숫자를 매 초마다 출력하는 메서드이다.
Thread4 th1 = new Thread4();로 쓰레드를 생성했고,
th1.setDaemon(true);로 이 쓰레드를 데몬 쓰레드로 만들었다.Daemon 쓰레드는 다른 쓰레드에 대한 보조 쓰레드이므로,
여기서는 main 쓰레드에 대한 보조 쓰레드가 된다.
Daemon 쓰레드는 main 쓰레드의 실행이 종료되면 동작을 멈춘다.
따라서 main 쓰레드에 예외가 발생하면 Daemon도 동작을 멈춘다.
  • 정답:
0
1
2
3
4
Exception in thread "main" java.lang.Exception:~!!!
	at Exercise13_6.main(Sol_Exercise13_6.java:15)
  • 해설:
    [해설] 문제13-6에 th1.setDaemon(true); 한 문장을 추가해서 쓰레드 th1을 데몬 쓰레드로(daemon thread) 설정하였다.
    데몬 쓰레드는 일반 쓰레드(데몬 쓰레드가 아닌 쓰레드)가 모두 종료되면 자동 종료되므로, main쓰레드(일반쓰레드)가 종료됨과 동시에 자동 종료된다.
    그래서 문제13-6과는 달리 쓰레드th1이 main메서드의 종료와 동시에 종료되었다.

✔️💡13-7. 쓰레드 상태 정지시키기 리팩토링

  • 문제
//[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) {
            }
        }
    } // run()
}


/*
<실행결과>
0
1
2
stopped
 */
  • 🔥내 풀이:
<풀이접근❌>
main 쓰레드에서
try {
            Thread.sleep(6 * 1000);
        } catch (Exception e) {
        }

        stopped = true; // 쓰레드를 정지시킨다.
를 했음에도 불구하고 몇 초 뒤에야 th1 쓰레드가 멈추는 이유는
➡️ 6초 뒤에 인스턴스 변수 stopped를 true로 변경했음에도 불구하고,
그 직전에 Thread5run()에서 for문 조건 !Exercise13_7.stopped;를 체크했기 때문에,
Thread.sleep(3 * 1000); (3초 텀이 적용되고 나서야 멈추게 되는 것이다.) ❌❌
(물론 OS 스케쥴러에 따라 아래와 같이 바로 멈출수도 있다.)
<⬇️OS 스케줄러의 컨디션에 따라 main 쓰레드가 먼저 실행되었을 경우 실행결과>
0
1
stopped

⭐OS 컨트롤러의 스케줄을 직접 조정하는 것은 매우 큰 부담이기 때문에 적절한 답이 아니다.
❓어떻게 해결해야 할까?
  • 모범답안:
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");
    }
}

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) {
        }
    }
} // run()
}
  • 모범 해설:
    [정답] Exercise13_7.stopped의 값이 바뀌어도 for문내의
    Thread.sleep(3*1000);문장에 의해 일시정지 상태에 있는 경우
    , 시간이 지나서 일시정지 상태를 벗어날 때까지 for문을 벗어날 수 없기 때문에 이런 현상이 발생한다. 그래서 interrupt()를 호출해서 자고 있는 (sleep()에 의해 일시정지 상태에 있는) 쓰레드를 깨워야 즉시 정지하게 된다.

    [해설] 쓰레드 th1은 아래의 반복문을 수행하다가 main메서드에서
    Exercise13_7.stopped의 값을 true로 바꾸면 반복문을 빠져나와 수행을 종료하게 된다. ⭐반복문 안에 쓰레드를 3초 동안 일시정지 상태로 하는 ‘Thread.sleep(3*1000)’이 있기 때문에 Exercise13_7.stopped의 값이 바뀌었다 하더라도 일시정지 상태에 있다면, 일시정지 상태가 끝나야만 반복문을 빠져나오게 된다.
  public void run(){
      // Exercise13_7.stopped의 값이 false인 동안 반복한다.
      for(int i=0;!Exercise13_7.stopped; i++) {
          System.out.println(i);

              try{
                  Thread.sleep(3*1000); // ⭐3초간 쉰다.
              }catch(Exception e){}
          }
      } // run()
  • 그래서 쓰레드의 실행을 바로 종료시키려면 Exercise13_7.stopped의 값을 true로 바꾸는 것만으로는 부족하다. 그 외에 다른 방법이 더 필요하다. 그것은 바로 interrupt()를 호출하는 것이다.
    interrupt()는 InterruptedException을 발생시킴으로써 Thread.sleep()에 의해 일시정지 상태에 있던 쓰레드를 즉시 깨운다. 그래서 ⭐Exercise13_7.stopped의 값을 true로 바꾸고, interrupt()를 호출하면 지연 없이 즉시 쓰레드를 멈추게 할 수 있다.

쓰레드의 run() 구현하기

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

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_9 game = new Exercise13_9();
        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);
            if (index != -1) {
                words.remove(index);
            }
        }
    } // main

    class WordGenerator extends Thread {
        public void run() {
        /*
        (1)	아래의 로직에 맞게 코드를 작성하시오.
        1.	interval(2초)마다 배열 data의 값 중 하나를 임의로 선택해서
        2.	words에 저장한다.
        */
        } // end of run()
    } // class WordGenerator
} // Exercise13_9

/*
<실행결과>
[]
>>
[서현]
>>서현 [수영, 윤아]
>>수영 [윤아, 유리]
>>유리
[윤아, 티파니]
>>티파니
[윤아, 윤아, 유리]
>>윤아 [윤아, 유리]
>>유리 [윤아, 효연]
>>효연
[윤아, 티파니]
>>윤아 [티파니, 윤아]
>>티파니
[윤아, 수영, 써니]
>>
 */
  • 내 풀이:
//[13-8] 다음의 코드는 텍스트기반의 타자연습게임인데 WordGenerator라는 쓰레드가
//        Vector에 2초마다 단어를 하나씩 추가하고, 사용자가 단어를 입력하면 Vector에서 일치하는 단어를 삭제하도록 되어 있다.
//        WordGenerator의 run()을 완성하시오.

import java.util.Scanner;
import java.util.Vector;

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

    int interval = 2 * 1000; // 2초

    //⭐ has a 관계로 WordGenerator 객체를 인스턴스 변수로 가진다.
    WordGenerator wg = new WordGenerator();

    public static void main(String args[]) {
        Sol_Exercise13_8 game = new Sol_Exercise13_8();
        //⭐객체가 가지고 있는 쓰레드 객체 wg를 통해서 쓰레드 start()
        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);
            if (index != -1) {
                words.remove(index);
            }
        }
    } // main

    class WordGenerator extends Thread {
        public void run() {
        /*
        (1)	아래의 로직에 맞게 코드를 작성하시오.
        1.	interval(2초)마다 배열 data의 값 중 하나를 임의로 선택해서
        2.	words에 저장한다.
        */
            //⭐ 1.&2. interval(2초)마다 배열 data의 값 중 하나를 임의로 선택해서 words에 저장
            int ridx;
            while(true) {
                ridx = (int)(Math.random()*data.length);
                words.add(data[ridx]);

                try {
                    //⭐ 2초
                    Thread.sleep(interval);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        } // end of run()
    } // class WordGenerator
} // Exercise13_9

/*
<내 실행결과>

[]
>>서현
[제시카, 제시카]
>>제시카
[제시카, 수영, 서현]
>>제시카
[수영, 서현, 제시카]
>>제시카
[수영, 서현, 효연, 태연]
>>태연
[수영, 서현, 효연, 유리]
>>수영
[서현, 효연, 유리, 유리, 써니]
>>써니
[서현, 효연, 유리, 유리, 윤아, 제시카, 티파니, 서현]
>>효연
[서현, 유리, 유리, 윤아, 제시카, 티파니, 서현, 효연, 티파니]
>>
Process finished with exit code 130

 */
  • 정답:

  • 해설:
    [해설] 쉬운 문제라서 별로 설명할 것이 없다. 쓰레드가 그렇게 어려운 것만은 아니라고 느낄 수 있으면 좋겠다. 이 예제를 발전시켜서 GUI를 갖춘 타자게임을 만들어 보면 재미 있을 것이다.(카페의 'java1000제'게시판에 개발단계별로 공개되어 있음)


💡⭐13-9. 쓰레드 제어 리팩토링 (interrupt())

  • 문제
//[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("카운트가 종료되었습니다.");
    } // main
}

/*
<실행결과>
(swing에 abcd를 입력)
10
9
8
입력하신 값은 abcd입니다.
7
6
5
4
3
2
1
카운트가 종료되었습니다.
 */
  • 모범답안:
import javax.swing.JOptionPane;

class Sol_Exercise13_9 {
    public static void main(String[] args) throws Exception {
        //⭐run()이 구현되어 있는 쓰레드 객체 생성
        Sol_Exercise13_9_1 th1 =new Sol_Exercise13_9_1();
        //⭐호출 스택 하나 더 만들어 그곳에서 th1.run()을 동작시킴.
        th1.start();

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

class Sol_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) {
                //💡⭐sleep()에 의해 쓰레드가 잠시 멈춰있을 때, `interrupt()`를 호출하면 `InterruptedException`이 발생되고
                // 쓰레드의 interrupted상태는 false로 자동 초기화된다. ➡️ 쓰레드의 interrupted상태를 true로 다시 바꿔줘야 한다.
                interrupt();
            }
        }

        System.out.println("카운트가 종료되었습니다.");
    } // main
}
  • 💡해설:
    💡⭐sleep()에 의해 쓰레드가 잠시 멈춰있을 때, interrupt()를 호출하면 InterruptedException이 발생되고 쓰레드의 interrupted상태false로 자동 초기화된다.

    ➡️ 그래서, 위와 같이 catch블럭에 interrupt()를 추가로 넣어줘서 쓰레드의 interrupted상태true로 다시 바꿔줘야 한다.
    보다 자세한 내용은 p.754를 참고하자.
profile
9에서 0으로, 백엔드 개발블로그

0개의 댓글