[자바의 정석 기초편] 쓰레드 1

JEREGIM·2023년 3월 11일
0

자바의 정석 기초편

목록 보기
18/23

📌프로세스와 쓰레드

프로세스 : 실행 중인 프로그램. 자원(resources)과 쓰레드로 구성

  • 자원 : 메모리, CPU, 하드 디스크, 키보드 등

쓰레드 : 프로세스 내에서 실제 작업을 수행. 모든 프로세스는 최소한 하나의 쓰레드를 가지고 있다.

  • 싱글쓰레드 프로세스 = 자원 + 쓰레드
  • 멀티쓰레드 프로세스 = 자원 + 쓰레드 + 쓰레드 +...+ 쓰레드
프로세스 : 쓰레드 = 공장 : 일꾼

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

  • 2 프로세스 1 쓰레드 vs 1 프로세스 2 쓰레드
    -> 1 프로세스 2 쓰레드가 더 효율적

📌멀티쓰레드의 장단점

대부분의 프로그램이 멀티쓰레드로 작성되어 있다. 하지만 멀티쓰레드 프로그래밍이 장점만 있는 것은 아니다.

장점

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

단점

  • 동기화(synchronization)에 주의해야 한다.

    쓰레드 동기화는 여러 쓰레드가 동일한 리소스를 공유하여 사용하게 되면 서로의 결과에 영향을 주기 때문에 이를 방지하는 기법이다.

  • 교착상태(dead-lock)가 발생하지 않도록 주의해야 한다.

    교착상태(dead-lock) : 둘 이상의 작업이 서로 상대방 작업이 끝나기만 기다리고 있어서 서로 다음 단계로 진행하는 못하는 상태로 서로 무한 대기 상태로 빠지게 된다.

  • 기아가 발생하지 않도록 각 쓰레드가 효율적으로 고르게 실행될 수 있도록 해야 한다.

    기아 : 특정 쓰레드보다 우선순위가 높은 쓰레드만 계속 실행되면서 특정 쓰레드가 실행될 기회를 얻지 못하는 상태


📌쓰레드의 구현과 실행

구현

쓰레드를 구현하는 방법은 2가지가 있다.
Thread 클래스를 상속
Runnable 인터페이스를 구현

상속으로 구현을 하게 되면 자바는 단일 상속이기 때문에 다른 클래스를 상속 못한다는 단점이 있다.
그래서 일반적으로 Runnable 인터페이스를 구현 방법을 사용한다.

  1. Thread 클래스를 상속
class Thread1 extends Thread {
	public void run() {
    /*작업내용*/
    }
}
  • run() 메서드는 Thread 클래스의 run() 메서드를 오버라이딩한 것이다.
  1. Runnable 인터페이스를 구현
class Thread2 implements Runnable {
	public void run() {
    /*작업내용*/
    }
}
  • run() 메서드는 Runnable 인터페이스의 run() 추상 메서드를 구현한 것이다.

실행

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

        Runnable r = new Thread2();
        Thread t2 = new Thread(r); // 생성자 Thread(Runnable target)

        t1.start();
        t2.start();
    }
}

class Thread1 extends Thread {
    public void run() { // 쓰레드가 수행할 작업을 작성
        for (int i = 0; i < 5; i++) {
        	// 조상인 Thread의 getName()을 호출
            System.out.printf("(%s)%n", getName());
        }
    }
}

class Thread2 implements Runnable {
    public void run() {
        for (int i = 0; i < 5; i++) {
        	// 현재 실행중인 Thread의 이름을 반환
            System.out.printf("(%s)%n", Thread.currentThread().getName());
        }
    }
}

(Thread-0)
(Thread-0)
(Thread-0)
(Thread-1)
(Thread-1)
(Thread-1)
(Thread-1)
(Thread-1)
(Thread-0)
(Thread-0)

  • t1.start()을 먼저 적어줬다고 해서 t1이 먼저 실행되지는 않는다. 이는 OS의 스케쥴러가 결정해준다.
  • 실행할 때마다 순서는 바뀔 수 있다.
Runnable r = new Thread2();
Thread t2 = new Thread(r); // 생성자 Thread(Runnable target)
  • Runnable을 구현한 쓰레드는 객체를 생성할 때 생성자 Thread(Runnable target)를 이용해줘야 한다.
  • Thread t2 = new Thread(new Thread2()); : 이렇게 한 줄로 바꿔줄 수도 있다.
// 조상인 Thread의 getName()을 호출
System.out.printf("(%s)%n", getName());
...
// 현재 실행중인 Thread의 이름을 반환
System.out.printf("(%s)%n", Thread.currentThread().getName());
  • Thread를 상속한 쓰레드는 조상의 메서드인 getName()을 호출해서 쓰레드의 이름을 출력할 수 있다.
  • Runnable을 구현한 쓰레드는 Thread.currentThread().getName()); 메서드를 통해서 현재 쓰레드의 이름을 반환할 수 있다.

start()

쓰레드를 생성한 후에 start()를 호출해야 쓰레드가 작업 가능한 상태가 된다.

t1.start();
t2.start();
  • start()를 호출하면 쓰레드가 실행 가능한 상태가 되는 것이지 실행이 되는 것이 아니다.
    실제로 언제 실행이 될지는 OS의 스케쥴러가 실행 순서를 결정한다.

  • t1.start(); 먼저 적는다고 해도 먼저 호출되는 것이 아니다.

run() 메서드를 통해 수행할 작업을 작성했지만 호출은 start() 메서드로 하는 이유

  1. 호출 스택에서 start() 메서드가 새로운 호출 스택 생성

  2. start() 메서드는 종료되고 새로운 호출 스택에서 run() 메서드가 동작함으로써 서로 독립적인 작업 수행이 가능(멀티쓰레드)

t1.start();가 아닌 t1.run();을 하게될 경우

  • 멀티쓰레드가 아닌 main쓰레드에서 싱글쓰레드로 동작하게 된다.

📌main쓰레드

: main메서드의 코드를 수행하는 쓰레드

쓰레드는 "사용자 쓰레드"와 "데몬 쓰레드" 두 종류가 있다.

  • 데몬 쓰레드는 보조 쓰레드 개념이다.
  • 실행 중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.(데몬 쓰레드의 실행 여부는 신경X)
public class MainThread {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyThread1());
        Thread t2 = new Thread(new MyThread2());

        t1.start();
        t2.start();

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

class MyThread1 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print(1);
        }
    }
}

class MyThread2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print(2);
        }
    }
}

00002112222222222222222222222220000000000000000000000000000000000000000000000022222222222222222221111111111111222222222222222222220000000000000000000000000000000000000000000000000main쓰레드 종료시점222222222222222222222222222222222222MyThread2 종료시점1111111111111111111111111111111111111111111111111111111111111111111111111111111111111MyThread1 종료시점

  • main, MyThread2, MyThread1 총 3개의 "사용자 쓰레드"가 모두 종료될 때까지 프로그램은 종료되지 않는다.

📌싱글쓰레드와 멀티쓰레드

싱글쓰레드

public class MainThread {
        for (int i = 0; i < 100; i++) {
            System.out.print(0);
        }
        for (int i = 0; i < 100; i++) {
            System.out.print(1);
        }
    }
}

00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111

  • 0을 다 출력하고나서 1이 출력된다.

멀티쓰레드

public class MainThread {
    public static void main(String[] args) {
        Thread t1 = new Thread(new MyThread1());

        t1.start();

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

class MyThread1 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.print(1);
        }
    }
}

01000000000000000000000000000000111111111111110000000000000000000000000000000000000000000000000000010011111111111111111111111111111111111111111111111111111111111111100000000000000111111111111111111111

  • 멀티쓰레드로 작성하면 0과 1이 번갈아가면서 출력된다.

싱글쓰레드와 멀티쓰레드 그래프

싱글쓰레드와 멀티쓰레드를 그래프로 그려보면 다음과 같다.

  • 가로축을 보게 되면 멀티쓰레드가 시간이 약간 더 걸린다는 것을 알 수 있다.
    그 이유는 main쓰레드가 실행되다가 t1쓰레드가 실행되면 쓰레드 간의 작업 전환(context switching)이 발생하면서 시간이 조금씩 소모하기 때문이다.

  • 사실 위의 멀티쓰레드 그래프는 이상적인 그래프이다. 해당 쓰레드가 언제 얼마만큼 실행될지는 OS의 스케쥴러가 결정하기 때문에 실제로는 위의 그래프처럼 일정한 시간동안 두 쓰레드가 번갈아가며 실행되지 않는다.
    OS 스케쥴러는 OS 전체의 프로세스와 쓰레드를 총괄하기 때문에 시시각각 변하는 모든 프로레스와 쓰레드의 상황을 고려해서 실행순서와 실행기간을 정해 스케쥴링을 한다.
    따라서, 우리가 작성한 쓰레드는 실행할 때마다 결과가 달라지는 것이다.

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

싱글쓰레드로 작성

import javax.swing.JOptionPane;

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

        for (int i = 5; i > 0; i--) {
            System.out.println(i);
            try {
                Thread.sleep(500);
            } catch (Exception e) {
            }
        }
    }
}

  • 입력을 받을 때까지 다음 작업은 실행되지 않고 대기상태에 머무르게 된다. 즉, for 문(카운트다운)이 실행되지 않는다. 이것을 I/O Blocking 이라고 한다.

멀티쓰레드로 작성

import javax.swing.JOptionPane;

class Ex13_4 {
    public static void main(String[] args) throws Exception {
        Thread t1 = new ThreadEx5();
        t1.start();

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

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

  • 멀티쓰레드로 작성하게 되면 입력을 받지 않아도 멀티쓰레드인 t1쓰레드가 실행되면서 for 문(카운트다운)이 동작하게 된다.

결론 : 멀티쓰레드를 통해서 I/O Blocking을 해결할 수 있다.


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

: 작업의 중요도에 따라 쓰레드의 우선순위를 다르게 하여 특정 쓰레드가 더 많은 작업 시간을 갖게 할 수 있다.

void setPriority(int newPriority) : 지정한 값으로 우선순위 변경
int getPriority() : 우선순위 반환
우선순위 범위 : 1 (최소) ~ 10 (최대)
우선순위 default 값 : 5
main쓰레드 우선순위 : 5

JVM의 우선순위는 범위가 1 ~ 10으로 지정할 수 있도록 정해져있고 Windows OS의 경우에는 우선순위 범위가 32단계로 나뉘어져 있다.
JVM에서 설정된 쓰레드들의 우선순위를 OS 스케줄러에 전달하는 것이다.

그러나 우리가 쓰레드들의 우선순위를 각자 지정한다고 현재 OS 전체 작업을 다 제끼고 우선적으로 설정되는 것이 아니라 그저 희망 사항을 스케쥴러에 전달하는 것 뿐이다.

OS 스케쥴러는 우리가 전달한 희망 사항을 참고할 뿐 결국 OS 내에서 돌아가는 전체 프로그램들의 작업 효율을 따져 실행 순서를 정한다.


📌쓰레드 그룹

서로 관련된 쓰레드를 그룹으로 묶어서 다루기 위한 것

모든 쓰레드는 반드시 하나의 쓰레드 그룹에 포함되어 있어야 하고 쓰레드 그룹을 지정하지 않고 생성한 쓰레드는 자동으로 main쓰레드 그룹에 속한다.

자신을 생성한 쓰레드(부모 쓰레드)의 그룹과 우선순위를 상속받는다.

Thread 생성자

Thread(ThreadGroup group, String name) // Thread 클래스 상속받아서 만들어진 경우
Thread(ThreadGroup group, Runnable target) // Runnable 인터페이스 구현해서 만들어진 경우 
Thread(ThreadGroup group, Runnable target, String name)
Thread(ThreadGroup group, Runnable target, String name , long stackSize) // 생성할 호출스택 사이즈 설정 

쓰레드 그룹 메서드

Method설명
생성자
ThreadGroup getThreadGroup()자신이 속한 쓰레드 그룹 반환
ThreadGroup(String name)지정된 이름의 새로운 쓰레드 그룹 생성
ThreadGroup(ThreadGroup parent, String name)지정된 쓰레드 그룹에 속하는 지정된 이름의 새로운 쓰레드 그룹 생성
반환
int activeCount()쓰레드 그룹에 포함된 활성상태 쓰레드 수 반환
int activeGroupCount()쓰레드 그룹에 포함된 활성상태 쓰레드 그룹 수 반환
int getMaxPriority()쓰레드 그룹의 최대 우선순위 반환
String getName()쓰레드 그룹의 이름 반환
ThreadGroup getParent()쓰레드 그룹의 상위 쓰레드 그룹 반환
void list()소속 쓰레드와 하위 쓰레드 그룹 정보 출력
조회/확인
boolean isDaemon()해당 쓰레드 그룹이 데몬 쓰레드 그룹인지 확인
boolean isDestroyed()해당 쓰레드 그룹이 삭제되었는지 확인
boolean parentOf(ThreadGroup g)해당 쓰레드 그룹이 지정된 쓰레드 그룹의 상위 쓰레드 그룹인지 확인
void checkAccess()현재 실행 중인 쓰레드가 쓰레드 그룹을 변경할 권한이 있는지 체크
삭제
void destroy()쓰레드 그룹과 하위 쓰레드 그룹까지 전부 삭제
int enumerate(Thread[] list)
int enumerate( Thread[] list, boolean recurse)
int enumerate(ThreadGroup[] list)
int enumerate(ThreadGroup[] list, boolean recurse)
그룹에 속한 쓰레드와하위 쓰레드 그룹의 목록을 지정된 배열에 담고 개수 반환
설정
void setDaemon(boolean daemon)쓰레드 그룹을 데몬 쓰레드 그룹으로 설정/해제
void setMaxPriority(int Priority)그룹의 최대 우선순위 설정
void interrupt()그룹에 속한 모든 쓰레드를 interrupt()

void uncaughtException(Thread th, Throwable e) 메서드

  • 쓰레드 그룹에 있는 쓰레드가 예외처리를 하지 못하고 종료되었을 때 실행될 예외처리 동작을 오버라이딩 해주는 메서드.
    JVM에 의해 자동적으로 호출된다.
class Demo implements Runnable {
    @Override
    public void run() {
        int x = 10 / 0; // ArithmeticException 발생
    }
}

class MyHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("쓰레드 " + t.getName() + "에서 " + e.getMessage() + " 발생");
        // 로깅 작업 등을 할 수 있다.
    }
}

public class GFG {
    public static void main(String[] args) {
        Thread t = new Thread(new Demo());
        t.setUncaughtExceptionHandler(new MyHandler()); // uncaughtException 설정
        t.start();
    }
}

쓰레드 Thread-0에서 / by zero 발생

실습 예제

public class MainThread {
    public static void main(String[] args) {
        ThreadGroup root = new ThreadGroup("rootGroup"); // 루트 그룹 생성
        ThreadGroup childGroup = new ThreadGroup(root, "childGroup"); // 루트 그룹에 속하는 자식 그룹 생성
        rootThread rt = new rootThread(root, "rt"); // 루트 그룹에 속하는 쓰레드 생성
        Thread ch1 = new Thread(childGroup, new childThread1(), "ch1"); // 자식 그룹에 속하는 쓰레드 생성
        Thread ch2 = new Thread(new childThread2(), "ch2"); // 쓰레드 그룹 미지정 -> 자동으로 main쓰레드 그룹에 속함

        rt.start(); // 루트 그룹의 쓰레드 시작
        ch1.start(); // 자식 그룹의 쓰레드 시작
        ch2.start(); // main쓰레드 그룹의 쓰레드 시작

        root.list(); // 루트 그룹과 하위 그룹들의 정보 출력
        System.out.println();
    }
}

class childThread1 implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "가 속한 쓰레드 그룹 : " + Thread.currentThread().getThreadGroup().getName());
    }
}

class childThread2 implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "가 속한 쓰레드 그룹 : " + Thread.currentThread().getThreadGroup().getName());
    }
}

class rootThread extends Thread {
    public rootThread(ThreadGroup group, String name) {
        super(group, name);
    }

    @Override
    public void run() {
        System.out.println(getName() + "가 속한 쓰레드 그룹 : " + getThreadGroup().getName()); // rt의 쓰레드 그룹 이름 출력
    }
}
  • 쓰레드에 어떤 명령을 내릴 때 쓰레드 그룹에 한번에 명령을 내릴 수 있다.

  • rootThread rt = new rootThread(root, "rt"); : Thread(ThreadGroup group, String name) 생성자 사용

  • Thread ch1 = new Thread(childGroup, new childThread1(), "ch1"); : Thread(ThreadGroup group, Runnable target, String name) 생성자 사용

  • 쓰레드를 생성할 땐 생성자를 통해 쓰레드 그룹을 지정해줘야 하지만 지정해주지 않으면 자동으로 main쓰레드 그룹에 속하게 된다. -> Thread ch2 = new Thread(new childThread2(), "ch2");


📌데몬 쓰레드(Daemon thread)

: 일반 쓰레드의 작업을 돕는 보조적인 역할을 수행하는 쓰레드

일반 쓰레드가 모두 종료되면 자동적으로 종료된다.(일반 쓰레드의 보조적인 역할로써 일반 쓰레드가 종료되면 존재의 의미가 없어지기 때문에)

데몬 쓰레드 예시 : 가비지 컬렉터(GC), 자동 저장, 화면 자동갱신 등

데몬 쓰레드 작성 방법

: 무한루프와 조건문을 이용해서 실행 후 대기 상태에 있다가 특정 조건이 만족되면 작업을 수행하고 다시 대기하도록 작성

  • boolean isDaemon() : 쓰레드가 데몬 쓰레드인지 확인. 데몬 쓰레드면 true 반환

  • void setDaemon(boolean on) : 쓰레드를 데몬 쓰레드로 또는 일반 쓰레드로 변경. 매개변수로 true를 지정하면 데몬 쓰레드가 된다.
    void setDaemon(boolean on)는 반드시 start() 메서드를 호출하기 전에 실행되어야 한다. 그렇지 않으면 IllegalThreadStateException 발생

class Ex13_7 implements Runnable {
    static boolean autoSave = false;

    public static void main(String[] args) {
        Thread t = new Thread(new Ex13_7());
        t.setDaemon(true); // 쓰레드 t를 데몬 쓰레드로 변경
        t.start();

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

            if (i == 5) autoSave = true; // 5초가 지나면 autoSave를 true로 설정
        }

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

    public void run() {
        while (true) { // 무한루프로 작성
            try {
                Thread.sleep(3 * 1000);
            } catch (InterruptedException e) {
            }

            if (autoSave) autoSave(); // autoSave가 true 조건을 만족할 때만 작업을 수행
        }
    }

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

1
2
3
4
5
작업 파일이 자동 저장 되었습니다.
6
7
8
작업 파일이 자동 저장 되었습니다.
9
10
프로그램을 종료합니다.

  • 5초 이후 autoSave를 true로 변경하고 이 때부터 데몬 쓰레드의 if 조건문이 true가 되면서 3초마다 자동 저장하는 데몬 쓰레드가 동작한다.

  • 일반 쓰레드가 10초 후 프로그램 종료가 되도록 설정되었기 때문에 10초 후 데몬 쓰레드도 자동적으로 종료된다.

  • t.setDaemon(true); : 쓰레드 t를 데몬 쓰레드로 변경. 반드시 start() 메서드 전에 실행되어야 한다.


📌쓰레드의 상태

상태설명
NEW생성은 되었지만 아직 start() 호출이 안된 상태
RUNNABLE실행 중 또는 실행 가능한 상태
BLOCKED동기화 블럭에 의해 일시 정지된 상태(풀릴 때까지 기다리는 상태)
WAITING / TIMED_WAITING종료는 아니지만 실행 불가능한 일시 정지 상태
(TIMED_WAITING은 시간이 지정된 일시정지)
TERMINATED쓰레드의 작업이 종료된 상태 -> 소멸

  • 쓰레드가 생성되었지만 아직 start() 메서드가 호출되기 전을 NEW 상태라고 한다.

  • 실행 뿐만 아니라 실행 후에 다시 자신 차례가 올 때까지 실행 대기를 하는 상태 또한 RUNNABLE 상태라고 한다.

  • 작업을 다 마치거나 stop() 메서드가 호출되면 TERMINATED 상태가 된다.

  • suspend()resume()

    • suspend() 메서드가 호출되면 쓰레드가 WAITING / BLOCKED 상태가 된다.
    • resume() 메서드를 호출하면 다시 RUNNALBLE 상태가 된다.
  • sleep()time-out, interrupt()

    • sleep() 메서드가 호출되면 쓰레드가 지정한 시간동안 WAITING / BLOCKED 상태가 된다.
    • 지정된 시간이 모두 지나면 time-out이 되고 다시 RUNNALBLE 상태가 된다.
    • 지정된 시간이 모두 지나기 전에 interrupt() 메서드를 호출하면 중간에 바로 RUNNABLE 상태가 된다.
  • wait()notify()

    • wait() 메서드가 호출되면 쓰레드가 WAITING / BLOCKED 상태가 된다.
    • notify() 메서드를 호출하면 다시 RUNNALBLE 상태가 된다.
  • join() : 지정한 다른 쓰레드가 종료될 때까지 기다리는 메서드

    • Ex) main 쓰레드에서 t1.join() 을 호출하면 main 쓰레드는 t1 쓰레드가 종료될 때까지 일시정지 상태에 있게 된다.
  • I/O block : 입출력이 완료될 때까지 대기

suspend() vs wait()

suspend()wait()는 자바에서 쓰레드를 일시 정지시키는 메서드이다.
그러나 두 메소드는 다음과 같은 차이점이 있습니다.

  • suspend()는 쓰레드가 속한 Thread 클래스의 메서드이고,
    wait()는 모든 객체가 상속하는 Object 클래스의 메서드이다.

  • suspend()는 쓰레드를 일시 정지시키면서 락을 해제하지 않는다. 이로 인해 다른 쓰레드가 공유 자원에 접근할 수 없게 되어 교착 상태(dead-lock)가 발생할 수 있다.
    반면 wait()는 쓰레드를 일시 정지시키면서 락을 해제한다. 이로 인해 다른 쓰레드가 공유 자원에 접근하고 통지(notify)를 보낼 수 있다.

  • suspend()resume() 메서드로만 재개될 수 있다. 이 때 resume() 메서드가 호출되기 전에 suspend()된 쓰레드가 종료되면 문제가 발생할 수 있다.
    반면 wait()notify(), notifyAll(), interrupt(), time-out 등의 방법으로 재개될 수 있다.

따라서 suspend()resume()은 권장되지 않는 방법이며, 대신 wait()/notify(), sleep()/interrupt() 등의 방법을 사용하는 것이 더 좋다.


0개의 댓글