Thread-3

Moom2n·2024년 3월 13일
0

Java

목록 보기
19/26

Thread Pool

ex). 손님 100명이 있다. 한번에 매장에 10명만 들어갈 수 있다면 손님들에게 쓰레드 풀을 할당하고, 자동으로 알아서 들어갔다 나왔다 설계할 수 있다.

스레드 풀(Thread Pool)은 많은 수의 스레드를 생성 및 소멸시키는 것에 따른 오버헤드를 줄이기 위해 사용되는 기술입니다. 스레드 풀을 이용하면 작업 처리에 사용되는 스레드의 수를 제한하고, 미리 생성된 스레드를 재사용함으로써 시스템의 자원을 효율적으로 관리할 수 있습니다.

Java thread pool은 여러 개의 thread를 만들어 두고 만들어 여러 번 재사용되는 작업자 thread 그룹을 나타낸다.

Thread pool은 고정 크기로 생성된 thread pool에 Runnable object를 주고 실행을 요청하면, thread pool에서 thread를 가져와 실행시킨다.

만약 thread pool에 남아 있는 thread가 없다면, 유휴 thread가 생길 때까지 해당 작업은 실행되지 못하고 대기상태에 있게 된다.

실행 중이던 thread는 작업이 완료되면 다시 thread pool로 돌아오게 되고, 대기 중인 작업이 있는 경우 다시 실행하게 된다.

- 실습 1

자바에서는 java.util.concurrent 패키지를 통해 스레드 풀을 쉽게 사용할 수 있습니다. 가장 널리 사용되는 스레드 풀 구현체로는 Executors 클래스가 있습니다. Executors 클래스는 다양한 종류의 스레드 풀을 생성하는 정적 메소드를 제공합니다.

예를 들어, 고정된 수의 스레드를 갖는 스레드 풀을 생성하려면 Executors.newFixedThreadPool(int nThreads) 메소드를 사용할 수 있습니다. 이 메소드는 지정된 수의 스레드를 갖는 스레드 풀을 생성하며, 모든 스레드가 바쁘게 작업을 처리하고 있을 때 추가 작업은 큐에 대기하게 됩니다.

  • 10개의 thread가 시작 메시지를 출력하고, 2초간 대기 후 종료 메시지를 출력하도록 thread pool을 구성해보자.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Worker implements Runnable {
    String name;

    public Worker(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public void run() {
        System.out.println(getName() + " started ");
        try {
            Thread.sleep(1999);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(getName() + " finished");
    }

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

        for (int i = 0; i < 10; i++) {
            executor.submit(new Worker("Worker" + i));
        }

        executor.shutdown();
        while (!executor.isTerminated()) {
        }

        System.out.println("모든 작업이 완료되었습니다.");
    }
}

실행결과

Worker0 started 
Worker1 started 
Worker3 started 
Worker2 started 
Worker4 started 
Worker1 finished
Worker0 finished
Worker4 finished
Worker7 started 
Worker2 finished
Worker8 started 
Worker6 started 
Worker5 started 
Worker3 finished
Worker9 started 
Worker7 finished
Worker6 finished
Worker9 finished
Worker8 finished
Worker5 finished
모든 작업이 완료되었습니다.

- 실습 2

  • Thread pool를 이용해 한정된 자원에서 많은 작업을 등록하여 수행하도록 구성해 보자
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class DownloadWorker implements Runnable {

    private String url;
    static String[] urls = { 
            "https://nhnacademy.dooray.com/share/drive-files/ocfkrcbb5vui.go2pyBWXRZ6IXN3whxDFtg",
            "https://nhnacademy.dooray.com/share/drive-files/ocfkrcbb5vui.YQloTWfJRz24Xhq2aVSGgw",
            "https://nhnacademy.dooray.com/share/drive-files/ocfkrcbb5vui.DwdVMtMaTmOFS_mQebo56w",
            "https://nhnacademy.dooray.com/share/drive-files/ocfkrcbb5vui.e2pbYnmHT_mRPWZZ3Z511Q",
            "https://nhnacademy.dooray.com/share/drive-files/ocfkrcbb5vui.p0sB3Ke2Tt64uXFPa1sU5A",
            "https://nhnacademy.dooray.com/share/drive-files/ocfkrcbb5vui.gYevNY21TIOphjrtV1Bznw"
        };

    public DownloadWorker(String url) {
        this.url = url;
    }

    @SuppressWarnings("deprecation")
    @Override
    public void run() {
        String fileName = url.substring(url.lastIndexOf("/") + 1);
        try (InputStream inputStream = new URL(url).openStream();
                FileOutputStream outputStream = new FileOutputStream(fileName)) {

            byte[] buffer = new byte[2048];
            int length;
            while ((length = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, length);
            }

            inputStream.close();
            outputStream.close();

            System.out.println(fileName + " 다운로드 완료");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (String url : urls) {
            Runnable worker = new DownloadWorker(url);
            executor.execute(worker);
        }

        executor.shutdown();
        while (!executor.isTerminated()) {
        }
    }
}

실행 결과

ocfkrcbb5vui.DwdVMtMaTmOFS_mQebo56w 다운로드 완료
ocfkrcbb5vui.YQloTWfJRz24Xhq2aVSGgw 다운로드 완료
ocfkrcbb5vui.go2pyBWXRZ6IXN3whxDFtg 다운로드 완료
ocfkrcbb5vui.gYevNY21TIOphjrtV1Bznw 다운로드 완료
ocfkrcbb5vui.p0sB3Ke2Tt64uXFPa1sU5A 다운로드 완료
ocfkrcbb5vui.e2pbYnmHT_mRPWZZ3Z511Q 다운로드 완료

- 주요 구성요소

스레드 풀의 주요 구성 요소:
1. 스레드 풀 매니저(Thread Pool Manager): 작업 큐에서 대기 중인 작업을 스레드에 할당하고, 스레드 풀의 상태를 관리합니다.
2. 작업 큐(Work Queue): 처리해야 할 작업들을 보관하는 대기열입니다. 스레드 풀에 있는 스레드들은 이 큐에서 작업을 가져와 처리합니다.
3. 워커 스레드(Worker Threads): 실제 작업을 수행하는 스레드들입니다. 미리 생성되어 작업 큐에서 작업을 가져와 처리합니다.

- 이점

Thread 생성과 삭제에 따른 시간과 resource를 절약할 수 있다.

  • 자원의 효율적 사용: 스레드 생성 및 소멸에 따른 오버헤드가 줄어들어, 시스템 자원을 효율적으로 사용할 수 있습니다.
  • 성능 향상: 미리 생성된 스레드를 재사용하기 때문에, 스레드 생성에 소요되는 시간이 절약되어 성능이 향상됩니다.
  • 안정성 향상: 스레드의 최대 개수를 제한함으로써, 동시에 실행되는 스레드의 수를 제어할 수 있으며, 이는 시스템의 안정성을 향상시킵니다.

- 주의점

Deadlock

  • Multi-thread와 관련된 모든 프로그램에서 교착 상태가 발생할 수 있다.
  • 작업 간 데이터 교환이 필요한 경우, 실행 작업과 대기 작업의 교착 상태가 발생할 수 있다.

Thread Leakage

  • 비정상 종료에 의한 실행 종료

Resource Thrashing

  • 지나치게 큰 thread pool을 사용할 경우, thread 간 context 전환 시 느려지는 문제가 발생할 수 있다.

Thread Group

  • Java는 단일 object에서 여러 thread를 그룹화하는 편리한 방법을 제공한다.
  • 단일 함수 호출로 thread group을 pause, resume 또는 stop할 수 있다.
  • pause, resume, stop 등의 안전성의 이유로 사용하지 말것을 권장함으로 ThreadGroup 역시 사용하지 않는 것이 좋다.

Daemon Thread

- User Thread vs Daemon Thread

  • Java에서의 thread는 user thread와 daemon thread로 나뉜다.

  • User thread는 특별히 daemon thread로 설정하지 않은 thread로서 일반적으로 생성해서 사용되는 thread이다.

  • JVM은 모든 user thread가 종료될 때까지 프로그램을 실행한다.

  • Daemon thread는 백그라운드에서 동작하는 thread로, user thread가 종료될 때 자동으로 종료된다.

  • 주로 main thread나 다른 user thread의 보조 역할을 수행하거나, 특정 작업을 주기적으로 처리하는 thread 등에 사용된다.

  • JVM은 모든 user thread가 종료되면 daemon thread를 강제로 종료합니다.

  • 모든 thread의 생성과 종료를 직접 관리하지 않을 수도 있다.

  • Deamon thread의 우선 순위는 낮으나 조정 가능하다.

- 실습

  • 사용자에게 현재 시간 표시, 로그 파일에 시간 기록 백그라운드 작업으로 수행
public class ClockApplication {

    public static void main(String[] args) {
        // 사용자 스레드 생성 및 시작: 시간 표시 기능
        Thread userThread = new Thread(new Runnable() {
            public void run() {
                while (true) {
                    System.out.println("현재 시간: " + System.currentTimeMillis());
                    try {
                        Thread.sleep(1000); // 1초마다 시간 표시
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        userThread.start(); // 사용자 스레드 시작

        // 데몬 스레드 생성 및 시작: 로그 기록 기능
        Thread daemonThread = new Thread(new Runnable() {
            public void run() {
                while (true) {
                    // 현재 시간을 로그 파일에 기록하는 코드 (가상 코드)
                    logTime(System.currentTimeMillis());
                    try {
                        Thread.sleep(5000); // 5초마다 로그 기록
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

            private void logTime(long time) {
                // 로그 파일에 시간을 기록하는 로직 (가상 코드)
                System.out.println("로그 기록: " + time);
            }
        });
        daemonThread.setDaemon(true); // 이 스레드를 데몬 스레드로 설정
        daemonThread.start(); // 데몬 스레드 시작
    }
}

이 예시에서 daemonThreadsetDaemon(true) 메소드 호출을 통해 데몬 스레드로 설정되었습니다. 이는 해당 스레드가 주 애플리케이션의 생명주기에 영향을 주지 않음을 의미합니다. 사용자가 애플리케이션을 종료하면, userThread는 종료되며, 이후 JVMdaemonThread가 아직 실행 중이라도 애플리케이션을 종료할 수 있습니다.

0개의 댓글

관련 채용 정보