자유/대외 활동

[혼공학습단 9기] #7. C언어에 포인터가 있다면 자바엔 스레드다!

Chipmunks 2023. 1. 24.
728x90

 

안녕하세요, 다람쥐 입니다!

 

벌써 한빛미디어 혼공학습단이 절반 지나갔네요!

 

저번 주에 설 연휴에는 쉬고 이제 다시 혼공학습단 9기 혼공자바 완독을 위해 달려가려고 합니다~

 

한빛미디어 혼공학습단 9기 혼공자바 4주차 진도는 스레드입니다!

 

C언어에 포인터가 있다면 자바에는 스레드가 있는데요..!

 

포인터 전 까지 그럭저럭 잘 나가다가

 

포인터부터 머릿 속에 물음표만 가득한 채로 배우셨던 경험이 있으실까요~?

 

자바에선 포인터가 없지만 포인터와 비슷한 난관인 스레드가 있습니다...!

 

객체지향까지 어찌저찌 따라와도 스레드에서 머릿 속에 물음표만 한 가득이었던 경험이 생생하네요. 🤣

 

스레드를 처음부터 차근 차근 정리해봅시다!

 

한빛미디어 혼공학습단 9기 4주차 진도

요즘도 마찬가지지만 예전부터 멀티 스레드가 굉장히 중요했는데요.

 

자바 언어에서는 어떻게 처리되는지 한 번은 꼭 자세히 정리해 보고자 했습니다.

 

멀티 스레드

운영체제에서 실행 중인 하나의 애플리케이션을 프로세스(process) 라고 합니다.

 

컴파일돤 자바 애플리케이션을 실행하면 새로운 JVM 프로세스가 시작됩니다.

 

애플리케이션의 Main 스레드가 생성되어 코드가 실행됩니다.

 

이 때, Main 스레드 뿐 아니라 동적으로 다른 스레드를 생성할 수가 있습니다.

 

스레드는 하나의 프로세스에서 한가지 작업을 실행하기 위해 순차적으로 실행할 코드를 실처럼 이어놓았다고 해서 유래된 이름입니다.

 

멀티 프로세스는  운영체제에서 할당받은 자신의 메모리를 가지고 실행하기에 각 프로세스는 서로 독립적입니다.

 

그러나 멀티 스레드는 하나의 프로세스 내부에서 생성하기에 하나의 스레드를 예외가 발생하면 프로세스 자체가 종료될 수 있어 다른 스레드에 영향을 끼칠 수 있습니다.

 

작업 스레드 생성과 실행

멀티 스레드로 개발하려면 몇 개의 작업을 병렬로 실행할 지 결정하고 각 작업별로 스레드를 생성합니다.

 

메인 스레드 외에 추가적인 작업 수 만큼 스레드를 생성합니다.

 

자바는 작업 스레드도 객체로 관리하므로 클래스가 필요합니다.

 

Thread 클래스로 직접 객체를 생성하는 방법도 있고 하위 클래스로 만드는 방법도 있습니다.

 

1. Thread 클래스로 직접 생성

java.lang 패키지에 있는 Thread 클래스로 작업 스레드 객체를 직접 생성하려면 Runnable 구현 객체를 매개값으로 갖는 생성자를 호출한다.

    /**
     * Allocates a new {@code Thread} object. This constructor has the same
     * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}
     * {@code (null, target, gname)}, where {@code gname} is a newly generated
     * name. Automatically generated names are of the form
     * {@code "Thread-"+}<i>n</i>, where <i>n</i> is an integer.
     *
     * @param  target
     *         the object whose {@code run} method is invoked when this thread
     *         is started. If {@code null}, this classes {@code run} method does
     *         nothing.
     */
    public Thread(Runnable target) {
        this(null, target, "Thread-" + nextThreadNum(), 0);
    }

Runnable 인터페이스는 스레드가 작업을 실행할 때 사용합니다.

run() 메소드가 정의되어 있습니다.

구현 클래스는 run() 메소드에 스레드가 실행할 코드를 재정의합니다.

class Task implements Runnable {
    @Override
    public void run() {
        // 스레드가 실행할 코드
    }
}

Runnable 구현 객체를 생성한 후 Thread 생성자 매개값으로 Runnable 객체를 전달한다.

public class Example {
    public static void main(String[] args) {
        Runnable task = new Task();
        
        Thread thread = new Thread(task);
    }
}

명시적인 Runnable 구현 클래스를 작성하지 않고 Runnable 익명 구현 객체를 매개값으로 사용할 수 있다.

이 방법을 많이 사용한다.

public class Example {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                // 스레드가 실행할 코드0
            }
        });

        thread.start();
    }
}

작업 스레드를 실행하는 start() 메소드를 호출한다.

 

메인 스레드가 start() 메소드를 호출하는 순간 다른 작업 스레드가 코드를 실행한다.

그와 동시에 메인 스레드는 다음 코드를 처리한다.

 

import java.awt.*;

public class Example {
    public static void main(String[] args) {
        Toolkit toolkit = Toolkit.getDefaultToolkit();
        for (int i = 0 ; i < 5; i++) {
            toolkit.beep();
            try { Thread.sleep(500); } catch (Exception e) {}
        }

        for (int i = 0; i < 5; i++) {
            System.out.println("띵");
            try { Thread.sleep(500); } catch (Exception e) {}
        }
    }
}

메인 스레드가 동시에 처리할 수 없기에, 비프음을 올리는 동시에 "띵" 문자열을 출력할 수 없다.

작업 스레드를 만들어 동시에 처리할 수 있게 한다.

 

import java.awt.*;

public class Example {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                Toolkit toolkit = Toolkit.getDefaultToolkit();
                for (int i = 0 ; i < 5; i++) {
                    toolkit.beep();
                    try { Thread.sleep(500); } catch (Exception e) {}
                }
            }
        });

        thread.start();

        for (int i = 0; i < 5; i++) {
            System.out.println("띵");
            try { Thread.sleep(500); } catch (Exception e) {}
        }
    }
}

2. Thread 자식 클래스로 생성

Thread 의 자식 객체로 만들어서 작업 스레드 객체를 생성할 수 있다.

Thread 클래스를 상속한 다음 run() 메소드를 재정의한 객체를 생성한다.

1번과 동일하게 start() 메소드로 작업 스레드를 실행한다.

public class WorkerThread extends Thread {
    @Override
    public void run() {
        // 스레드가 실행할 코드
    }
}

public class Example {
    public static void main(String[] args) {
        Thread thread = new WorkerThread();
        thread.start();
    }
}

스레드 이름

스레드 생성자에서 알 수 있듯이 스레드에 이름을 부여한다.

메인 스레드는 'main' 이라는 이름을 가지고 있고, 작업 스레드는 자동적으로 'Thread-n' 이라는 이름을 가진다.

다른 이름으로 설정하는 메소드는 setName() 메소드를 사용한다.

 

스레드 이름은 디버깅할 때 어떤 스레드가 작업하는지 디버깅 용도로 주로 사용한다.

현재 코드를 어떤 스레드가 실행하고 있는지 확인하려면 정적 메소드 currentThread() 으로 스레드 객체의 참조를 얻은 다음

getName() 메소드로 이름을 출력한다.

Thread thread = Thread.currentThread();
System.out.println(thread.getName());
public class Example {
    public static void main(String[] args) {
        Thread mainThread = Thread.currentThread();
        System.out.println(mainThread.getName() + " 실행");

        for (int i = 0; i < 3; i++) {
            Thread threadA = new Thread() {
                @Override
                public void run() {
                    System.out.println(getName() + " 실행");
                }
            };
            threadA.start();
        }

        Thread chatThread = new Thread() {
            @Override
            public void run() {
                System.out.println(getName() + " 실행");
            }
        };
        chatThread.setName("chat-thread");
        chatThread.start();
    }
}
main 실행
Thread-0 실행
Thread-2 실행
Thread-1 실행
chat-thread 실행

스레드 동기화

멀티 스레드는 하나의 객체를 공유해서 작업할 수 있다.

공유 객체를 사용할 때의 주의해야 한다.

스레드 A가 사용하던 객체를 스레드 B가 상태를 변경할 수 있다.

이 때, 스레드 A가 의도했던 것과는 다른 결과가 나올 수 있다.

 

스레드가 사용 중인 객체를 다른 스레드가 변경할 수 없게 하려면 스레드 작업이 끝날 때까지 객체에 잠금을 걸어야 한다.

멀티 스레드에서 단 하나의 스레드만 실행할 수 있는 코드 영역을 임계 영역(crticial section)이라고 한다.

자바는 임계 영역을 지정하기 위해 동기화(synchronized) 메소드를 제공한다.

 

스레드가 객체 내부의 동기화 메소드를 실행하면 즉시 객체에 잠금을 걸어 다른 스레드가 동기화 메소드를 실행하지 못하게 한다.

동기화 메소드는 메소드 선언에 synchonized 키워드를 붙인다.

인스턴스와 정적 메소드 어디든 붙일 수 있다.

public synchronized void method() {
    // 임계 영역 코드
}
// Calculator.java

public class Calculator {
    private int memory;

    public int getMemory() {
        return memory;
    }

    public void setMemory(int memory) {
        this.memory = memory;
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }
        System.out.println(Thread.currentThread().getName() + ": " + this.memory);
    }
}


// User1.java

public class User1 extends Thread {
    private Calculator calculator;

    public void setCalculator(Calculator calculator) {
        this.setName("User1");
        this.calculator = calculator;
    }

    @Override
    public void run() {
        calculator.setMemory(100);
    }
}


// User2.java

public class User2 extends Thread {
    private Calculator calculator;

    public void setCalculator(Calculator calculator) {
        this.setName("User2");
        this.calculator = calculator;
    }

    @Override
    public void run() {
        calculator.setMemory(50);
    }
}

// Example.java

public class Example {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();

        User1 user1 = new User1();
        user1.setCalculator(calculator);
        user1.start();

        User2 user2 = new User2();
        user2.setCalculator(calculator);
        user2.start();

    }
}
User2: 50
User1: 50

Calculator 클래스의 setMemory() 메소드에 synchronized 키워드를 붙이지 않으면 잠금이 되지 않아

기존에 작업하던 게 덮어 씌워지게 된다.

public synchronized void setMemory(int memory) {
    this.memory = memory;
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
    }
    System.out.println(Thread.currentThread().getName() + ": " + this.memory);
}

synchronized 메소드를 붙이면 의도했던 결과가 나옵니다.

User1: 100
User2: 50

스레드 제어

스레드 객체를 생성하고 start() 메소드를 호출하면 바로 실행되지 않습니다.

실행 대기 상태가 되는데, 언제든지 실행할 준비가 되어 있는 상태입니다.

운영체제가 실행 대기 상태에 있는 스레드 중에서 하나를 선택해 CPU(코어)가 실행 상태로 만듭니다.

 

실행 상태의 스레드는 언제든 다시 실행 대기 상태로 돌아갈 수 있다.

실행 대기 상태에 있는 다른 스레드가 선택되어 실행 상태가 되기도 한다.

 

실행 상태에서 run() 메소드의 내용이 모두 실행되면 스레드의 실행이 멈추고 종료 상태가 된다.

스레드 상태

스레드 상태 제어

실행 중인 스레드의 상태를 변경하는 것을 스레드 상태 제어라고 합니다.

스레드 제어를 정교하게 해야 프로그램이 불안정하고 먹통되지 않습니다.

스레드 제어를 정교하게 하기 위해선 스레드 상태 제어 흐름을 이해해야 합니다.

 

스레드 상태 제어 흐름도

interrupt() 메소드 : 일시 정지 상태의 스레드에서 InterruptException 을 발생시켜, 예외 처리 코드(catch)에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 합니다.

 

notify(), notifyAll() : Object 클래스 메소드로 wait() 메소드로 인해 일시 정지인 스레드를 실행 대기 상태로 만든다.

 

sleep(long millis) 메소드 : 주어진 시간 동안 스레드를 일시 정지 상태로 만듭니다. 주어진 시간이 지나면 자동적으로 실행 대기 상태가 됩니다.

 

join() 메소드 : 호출한 스레드는 일시 정지 상태가 된다. 실행 대기 상태가 되려면, join() 메소드를 가진 스레드가 종료되어야 한다.

 

wait() 메소드 : Object 클래스 메소드로 동기화 블록 내에서 스레드를 일시 정지 상태로 만든다.

 

yield() 메소드 : 실행 상태에서 다른 스레드에게 실행을 양보하고 실행 대기 상태가 된다.

 

stop() 메소드 : 스레드를 즉시 종료합니다. 불안전한 종료를 유발하므로 사용하지 않는 것이 좋습니다.

 

스레드의 안전한 종료

stop() 메서드는 이제 deprecated (중요하지 않음) 되었습니다. 그럼 어떻게 해야 스레드를 안전하게 종료할 수 있을까요?

 

stop 플래그를 이용하는 방법

스레드는 run() 메소드가 끝나면 자동적으로 종료됩니다.

run() 메소드가 정상적으로 종료되도록 유도하는 것이 중요합니다.

public class XXXThread extends Thread {
    private boolean stop; // stop 플래그 필드

    @Override
    public void run() {
        while ( !stop ) {
            // 스레드가 반복 실행되는 코드;
        }
        // 스레드가 사용한 자원 정리
    }
}

setStop() 메소드로 stop 플래그 필드를 조절하면 분기 처리로 스레드를 안전하게 종료할 수 있습니다.

 

interrupt() 메소드를 이용하는 방법

interrupt() 메소드는 스레드가 일시 정지 상태에 있을 때 InterruptedException 을 발생시킵니다.

이를 이용하면 run() 메소드를 정상 종료할 수 있습니다.

// PrintThread.java

public class PrintThread extends Thread {
    @Override
    public void run() {
        try {
            while (true) {
                System.out.println("실행 중");
                Thread.sleep(1);
            }
        } catch (InterruptedException e) {
        }

        System.out.println("자원 정리");
        System.out.println("실행 종료");
    }
}


// Example.java

public class Example {
    public static void main(String[] args) {
        Thread thread = new PrintThread();
        thread.start();

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

        thread.interrupt();
    }
}
실행 중
...
실행 중
자원 정리
실행 종료

Thread 클래스에서 while (true) { } 으로 반복하는 코드를 작성합니다.

그리고 try { } catch (InterruptedException e) { } 으로 예외가 발생하면 빠져나옵니다.

메인 스레드에서 1초 지난 후에 interrupt() 메소드를 호출합니다.

 

주목할 점은 스레드가 실행 대기 또는 실행 상태에 있을 때 interrupt() 메소드가 실행되면 InterruptedException 이 발생하지 않는다.

스레드가 미래에 일시 정지 상태가 되면 InterruptedException 이 발생합니다.

Thread.sleep(1000); 을 한 이유가 예외를 발생시키기 위해 충분한 시간을 주었습니다.

 

일시 정지를 만들지 않고도 interrupt() 메소드의 호출 여부를 알 수 있는 방법이 있습니다.

interrupt() 메소드가 호출된다면, 스레드의 interrupted(), isInterrupted() 메소드는 true 를 리턴합니다.

interrupted() 는 정적 메소드이고, isInterrupted() 메소드는 인스턴스 메소드입니다.

둘 다 현재 스레드가 interrupted() 되었는지 확인합니다.

 

public class PrintThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("실행 중");
            if (Thread.interrupted()) {
                break;
            }
        }

        System.out.println("자원 정리");
        System.out.println("실행 종료");
    }
}

데몬 스레드 ( 기본 미션 )

데몬 (daemon) 스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드입니다.

주 스레드가 종료하면 데몬 스레드는 강제적으로 종료합니다.

데몬 스레드의 예는 워드프로세서의 자동 저장, 미디어 플레이어의 동영상 및 음악 재생, 쓰레기 수집기(GC) 등이 있습니다.

이 기능들은 주 스레드가 종료되면 같이 종료됩니다.

 

스레드를 데몬으로 만들기 위해선 setDaemon(true); 를 호출합니다.

아래 코드는 메인 스레드가 주 스레드가 되고, AutoSaveThread 가 데몬 스레드가 됩니다.

public class Example {
    public static void main(String[] args) {
        AutoSaveThread thread = new AutoSaveThread();
        thread.setDaemon(true);
        thread.start();
        ...
    }
}

현재 스레드가 데몬 스레드인지 확인하려면 isDaemon() 메소드를 호출하면 됩니다. 데몬 스레드면 true 를 반환합니다.

// AutoSaveThread.java

public class AutoSaveThread extends Thread {
    public void save() {
        System.out.println("작업 내용을 저장함");
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
            save();
        }
    }
}


// Example.java

public class Example {
    public static void main(String[] args) {
        AutoSaveThread thread = new AutoSaveThread();
        thread.setDaemon(true);
        thread.start();

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

        System.out.println("메인 스레드 종료");
    }
}

다른 스레드의 종료를 기다림 : join() 메소드

다른 스레드와 독립적으로 실행되지만, 다른 스레드가 종료될 때 까지 기다렸다가 실행을 해야 하는 경우가 있습니다.

계산 스레드의 작업을 마치고 그 결과값을 받아 처리하는 경우가 그 예입니다.

 

ThreadA 가 ThreadB 의 join() 메소드를 호출하면, ThreadA 는 ThreadB 가 종료할 때 까지 일시 정지 상태가 됩니다.

Thread B 의 run() 메소드가 종료되고 나서야 ThreadA 가 일시 정지에서 풀려 다음 코드를 실행합니다.

 

// SumThread.java

public class SumThread extends Thread {
    private long sum;

    public long getSum() {
        return sum;
    }

    public void setNum(long sum) {
        this.sum = sum;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
    }
}

// Example.java

public class Example {
    public static void main(String[] args) {
        SumThread sumThread = new SumThread();
        sumThread.start();

        try {
            sumThread.join();
        } catch (InterruptedException e) {}
        System.out.println("1~100 합: " + sumThread.getSum()); // 1~100 합: 5050
    }
}

다른 스레드에게 실행 양보

스레드가 무의미한 반복을 하는 것보다 다른 스레드에게 실행을 양보하고 실행 대기 상태로 가는 것이 프로그램 성능에 도움이 된다.

yield() 메소드로 기능을 지원한다.

// WorkThread.java

public class WorkThread extends Thread {
    public boolean work = true;

    public WorkThread(String name) {
        setName(name);
    }

    @Override
    public void run() {
        while (true) {
            if (work) {
                System.out.println(getName() + ": 작업처리");
            } else {
                Thread.yield();
            }
        }
    }
}


// Example.java

public class Example {
    public static void main(String[] args) {
        WorkThread workThreadA = new WorkThread("workThreadA");
        WorkThread workThreadB = new WorkThread("workThreadB");

        workThreadA.start();
        workThreadB.start();

        try { Thread.sleep(5000); } catch (InterruptedException e) {}
        workThreadA.work = false;

        try { Thread.sleep(5000); } catch (InterruptedException e) {}
        workThreadA.work = true;

    }
}
workThreadA: 작업처리
...
workThreadB: 작업처리
...
workThreadB: 작업처리
workThreadB: 작업처리
workThreadB: 작업처리
...
workThreadA: 작업처리
...
workThreadB: 작업처리
...

wait() 과 notify() 를 이용한 스레드 제어

경우에 따라서 두 개의 스레드를 교대로 번갈아 가며 실행할 때도 있다.

내 작업이 끝나면 다른 스레드를 일시 정지 상태에서 풀어주고 나는 일시 정지 상태로 들어간다.

공유 객체를 활용하여 두 스레드가 작업할 내용을 각각 동기화 메소드로 만든다.

 

// WorkObject.java

public class WorkObject {
    public synchronized void methodA() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + ": methodA 작업 실행");
        notify(); // 다른 스레드를 실행 대기 상태로 만듦
        try {
            wait(); // 자신의 스레드는 일시 정지 상태로 만듦
        } catch (InterruptedException e) {
        }
    }

    public synchronized void methodB() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName() + ": methodB 작업 실행");
        notify(); // 다른 스레드를 실행 대기 상태로 만듦
        try {
            wait(); // 자신의 스레드는 일시 정지 상태로 만듦
        } catch (InterruptedException e) {
        }
    }
}

// ThreadA.java

public class ThreadA extends Thread {
    private WorkObject workObject;

    public ThreadA(WorkObject workObject) {
        setName("ThreadA");
        this.workObject = workObject;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            workObject.methodA();
        }
    }
}


// ThreadB.java

public class ThreadB extends Thread {
    private WorkObject workObject;

    public ThreadB(WorkObject workObject) {
        setName("ThreadB");
        this.workObject = workObject;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            workObject.methodB();
        }
    }
}

// Example.java

public class Example {
    public static void main(String[] args) {
        WorkObject workObject = new WorkObject();

        ThreadA threadA = new ThreadA(workObject);
        ThreadB threadB = new ThreadB(workObject);

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

스레드풀

병렬 작업 처리가 많아져 스레드의 개수가 많아지면 CPU 점유율이 상승하고 메모리 사용량도 늘어난다.

그에 따라 애플리케이션의 성능도 저하된다.

병렬 작업 증가로 인한 스레드의 폭증을 막기 위해선 스레드풀을 사용하는 게 좋다.

 

스레드풀은 작업 처리에 스레드를 제한된 개수만큼 정해 놓고 작업 큐에 들어오는 작업들을 스레드가 하나씩 맡아 처리하는 방식이다.

작업 처리가 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리한다.

작업량이 증가해도 스레드의 개수가 늘어나지 않아 애플리케이션의 성능을 유지할 수 있다.

 

스레드풀 생성

스레드풀은 자바 언어에서 API 를 제공하고 있다.

java.util.concurrent 패키지에서 ExecutorService 인터페이스와 Executors 클래스를 제공한다.

Excutors 클래스의 두 개의 정적 메소드로 ExecutorService 구현 객체를 만들 수 있다.

 

newCachedThreadPool() 스레드풀은 초기 수와 코어 수는 0개 이다. 작업 개수가 많아지면 새 스레드를 생성시켜 작업을 처리한다.

60초 동안 스레드가 아무 작업을 하지 않으면 스레드를 풀에서 제거한다.

최대로 만드는 스레드 개수는 Integer.MAX_VALUE 만큼이다.

ExecutorService executorService = Executors.newCachedThreadPool();

// Executors.java

/**
 * Creates a thread pool that creates new threads as needed, but
 * will reuse previously constructed threads when they are
 * available.  These pools will typically improve the performance
 * of programs that execute many short-lived asynchronous tasks.
 * Calls to {@code execute} will reuse previously constructed
 * threads if available. If no existing thread is available, a new
 * thread will be created and added to the pool. Threads that have
 * not been used for sixty seconds are terminated and removed from
 * the cache. Thus, a pool that remains idle for long enough will
 * not consume any resources. Note that pools with similar
 * properties but different details (for example, timeout parameters)
 * may be created using {@link ThreadPoolExecutor} constructors.
 *
 * @return the newly created thread pool
 */
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
    }

 

newFixedThreadPool() 으로 생성한 스레드풀의 초기 수는 0개이고, 작업 개수가 많아지면 최대 5개까지 스레드를 생성시켜 작업을 처리한다. 이 스레드풀로 만든 스레드는 제거하지 않는다.

ExecutorService executorService = Executors.newFixedThreadPool(5);

    /**
     * Creates a thread pool that reuses a fixed number of threads
     * operating off a shared unbounded queue.  At any point, at most
     * {@code nThreads} threads will be active processing tasks.
     * If additional tasks are submitted when all threads are active,
     * they will wait in the queue until a thread is available.
     * If any thread terminates due to a failure during execution
     * prior to shutdown, a new one will take its place if needed to
     * execute subsequent tasks.  The threads in the pool will exist
     * until it is explicitly {@link ExecutorService#shutdown shutdown}.
     *
     * @param nThreads the number of threads in the pool
     * @return the newly created thread pool
     * @throws IllegalArgumentException if {@code nThreads <= 0}
     */
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

직접 ThreadPoolExecutor 으로 스레드풀을 생성할 수 있다.

import java.util.concurrent.*;

public class Example {
    public static void main(String[] args) {
        ExecutorService executorService = new ThreadPoolExecutor(
                3,                      	    // 코어 스레드 개수
                100,                                // 최대 스레드 개수
                120L,                               // 놀고 있는 시간
                TimeUnit.SECONDS,                   // 놀고 있는 시간 단위
                new SynchronousQueue<Runnable>()    // 작업 큐
        );
    }
}

스레드풀 종료

스레드풀의 스레드는 기본적으로 데몬 스레드가 아니다.

따라서 main 스레드가 종료되더라도 계속 실행 상태로 남아있다.

스레드풀의 모든 스레드를 종료하려면 ExecutorService 의 다음 두 메소드 중 하나를 실행해야 한다.

 

/**
 * Initiates an orderly shutdown in which previously submitted
 * tasks are executed, but no new tasks will be accepted.
 * Invocation has no additional effect if already shut down.
 *
 * <p>This method does not wait for previously submitted tasks to
 * complete execution.  Use {@link #awaitTermination awaitTermination}
 * to do that.
 *
 * @throws SecurityException {@inheritDoc}
 */
public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(SHUTDOWN);
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

/**
 * Attempts to stop all actively executing tasks, halts the
 * processing of waiting tasks, and returns a list of the tasks
 * that were awaiting execution. These tasks are drained (removed)
 * from the task queue upon return from this method.
 *
 * <p>This method does not wait for actively executing tasks to
 * terminate.  Use {@link #awaitTermination awaitTermination} to
 * do that.
 *
 * <p>There are no guarantees beyond best-effort attempts to stop
 * processing actively executing tasks.  This implementation
 * interrupts tasks via {@link Thread#interrupt}; any task that
 * fails to respond to interrupts may never terminate.
 *
 * @throws SecurityException {@inheritDoc}
 */
public List<Runnable> shutdownNow() {
    List<Runnable> tasks;
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);
        interruptWorkers();
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
    return tasks;
}

두 메소드의 차이는 다음과 같습니다.

 

shutdown() : 현재 처리 중인 작업 뿐만 아니라 작업 큐에 대기하고 있는 모든 작업을 처리한 뒤에 스레드풀을 종료합니다.

 

shutdownNow() : 현재 작업 처리 중인 스레드를 interrupt 해서 작업을 중지시키고 스레드풀을 종료시킨다. 리턴값은 작업 큐에 있는 미처리된 작업(Runnable)의 목록이다.

 

작업 생성과 처리 요청

하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현합니다.

새로운 Callable 클래스가 등장하게 되는데요.

둘의 차이점은 작업 처리 완료 후 리턴값의 유무 차이입니다.

 

Callable 익명 구현 클래스의 경우 제네릭으로 받은 타입을 리턴할 수 있게합니다.

new Callable<T> {
    @Override
    public T call() throws Exception {
    	// 스레드가 처리할 작업 내용
        return T;
    }
}

작업 처리 요청은 ExecutorService 의 작업 큐에 Runnable 또는 Callable 객체를 넣는 행위를 말한다.

ExecutorService 의 execute() 메소드와 submit() 메소드를 제공한다.

 

void execute(Runnable command) : Runnable 을 작업 큐에 저장, 작업 처리 결과를 리턴하지 않음

 

Future<T> submit(Callable<T> task) : Callable 을 작업 큐에 저장, 작업 처리 결과를 얻을 수 있도록 Future 를 리턴합니다.

 

아래는 execute 메소드와 submit 메소드를 사용한 예제입니다.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Example {
    public static void main(String[] args) {
        String[][] mails = new String[1000][3];
        for (int i = 0; i < mails.length; i++) {
            mails[i][0] = "admin@my.com";
            mails[i][1] = "member" + i + "@my.com";
            mails[i][2] = "신상품 입고";
        }

        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 1000; i++) {
            final int idx = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    Thread thread = Thread.currentThread();
                    String from = mails[idx][0];
                    String to = mails[idx][1];
                    String content = mails[idx][2];

                    System.out.println("[" + thread.getName() + "] " + from + " ==> " + to + ": " + content);
                }
            });
        }

        executorService.shutdown();
    }
}

...
[pool-1-thread-2] admin@my.com ==> member937@my.com: 신상품 입고
[pool-1-thread-4] admin@my.com ==> member928@my.com: 신상품 입고
[pool-1-thread-5] admin@my.com ==> member995@my.com: 신상품 입고
[pool-1-thread-1] admin@my.com ==> member993@my.com: 신상품 입고
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Example {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        for (int i = 1; i <= 100; i++) {
            final int idx = i;

            Future<Integer> future = executorService.submit(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    int sum = 0;
                    for (int i = 1; i <= idx; i++) {
                        sum +=i;
                    }
                    Thread thread = Thread.currentThread();
                    System.out.println("[" + thread.getName() + "] 1~" + idx + " 합 계산");
                    return sum;
                }
            });

            try {
                int result = future.get();
                System.out.println("\t리턴값: " + result);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        executorService.shutdown();
    }
}

...
[pool-1-thread-5] 1~99 합 계산
	리턴값: 4950
[pool-1-thread-1] 1~100 합 계산
	리턴값: 5050

선택 미션 : 스레드 문제 풀기

동영상과 음악을 재생하기 위해 두 가지 스레드를 실행하려고 합니다.

코드가 동작하게끔 빈칸을 채웠습니다.

 

첫 번째 빈칸은 new Thread() 생성자에 Runnable 인터페이스를 구현한 클래스를 넣어야 합니다.

따라서 MusicRunnable() 클래스를 만든 다음 생성자의 매개변수로 넣었습니다.

 

두 번째 빈칸은 Thread 클래스를 상속한 MovieThread 의 빈칸을 채워야 합니다.

extends Thread 으로 Thread 클래스를 상속받았습니다.

 

세 번째 빈칸은 Runnable 인터페이스를 구현해야 하므로, implements Runnable 으로 채웠습니다.

// Example.java

public class Example {
    public static void main(String[] args) {
        Thread thread1 = new MovieThread();;
        thread1.start();

        // Thread thread2 = new Thread(빈칸);
        Thread thread2 = new Thread(new MusicRunnable());
        thread2.start();
    }
}

// MovieThread.java

// public class MovieThread (빈칸) {
public class MovieThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("동영상을 재생합니다.");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
    }
}


// MusicRunnable.java

// public class MusicRunnable (빈칸) {
public class MusicRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("음악을 재생합니다.");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
    }
}

동영상을 재생합니다.
음악을 재생합니다.
음악을 재생합니다.
동영상을 재생합니다.
음악을 재생합니다.
동영상을 재생합니다.

마무리

스레드를 한 번 쭉 정리했더니 마음이 후련하네요. ☺️

 

또 추가적으로 공부하고픈 내용이 있다면 다른 포스팅으로 적어보겠습니다!

 

혼공학습단 9기 다른 글 보러가기

2023.01.17 - [자유/대외 활동] - [혼공학습단 9기] #5. 3주차 자바 기본 API 클래스, 이건 몰랐을걸?

 

[혼공학습단 9기] #5. 3주차 자바 기본 API 클래스, 이건 몰랐을걸?

안녕하세요, 다람쥐 입니다! 한빛미디어 혼공학습단 9기 혼공자바 3주차 진도 중 두 번째 진도를 나갔습니다. 혼공학습단 9기 혼공자바 3주차 진도 중 두 번째 진도는 바로 자바 기본 API 클래스입

itchipmunk.tistory.com

2023.01.17 - [자유/대외 활동] - [혼공학습단 9기] #4. 3주차 예외, 200% 이해하기

 

[혼공학습단 9기] #4. 3주차 예외, 200% 이해하기

안녕하세요, 다람쥐 입니다. 벌써 한빛미디어 혼공학습단 9기 혼공자바 3주차 진도를 나가게 됐네요! 혼공 학습단을 하며 혼자 공부하는 자바를 공부하다보니 시간이 참 빠르네요! 연휴가 다가

itchipmunk.tistory.com

2023.01.10 - [자유/대외 활동] - [혼공학습단 9기] #3. 2주차 객체 지향 프로그래밍을 왜 쓸까?

 

[혼공학습단 9기] #3. 2주차 객체 지향 프로그래밍을 왜 쓸까?

안녕하세요! 여러분들의 궁금증을 해결해 줄 다람쥐입니다. 한빛미디어 혼공학습단 9기 혼공자바 2주차 포스팅입니다. 2주차는 혼공자바 Chapter 6 ~ 9 ( 클래스, 상속, 인터페이스, 중첩 클래스와

itchipmunk.tistory.com

2023.01.08 - [자유/대외 활동] - [혼공학습단 9기] #2. 자바 기본 정말 안다고 생각해?

 

[혼공학습단 9기] #2. 자바 기본 정말 안다고 생각해?

안녕하세요! 여러분들의 궁금증을 해결해 줄 다람쥐입니다. 한빛미디어 혼공학습단 9기 혼공자바 1주차 포스팅입니다. 1주차는 혼공자바 Chapter 1 ~ 5 ( 자바 설치, 이클립스 설치, 변수와 타입, 연

itchipmunk.tistory.com

2023.01.08 - [자유/대외 활동] - [혼공학습단 #1] 한빛미디어 혼공학습단 9기 선정

 

[혼공학습단 #1] 한빛미디어 혼공학습단 9기 선정

안녕하세요, 다람쥐입니다. 지난 말에 이메일로 한빛미디어에서 혼공학습단 9기 모집안내를 받았어요! 안그래도 직장을 퇴사하고 자바 공부를 해야하던 터였는데요~ 스스로 목표를 잡고 강제성

itchipmunk.tistory.com

 

댓글