기초단계/JAVA

2022.11.30-1 JAVA 멀티 스레드

춘핑이 2022. 11. 30. 17:49

14. 멀티 스레드

14.8 데몬스레드

데몬스레드는 주 스레드의 작업을 돕는 보조적인 역할을 수행하는 스레드
주스레드 = 주인이 되는 스레드 그 스레드가 데몬스레드를 만들수잇다.
주스레드 종료시 데몬스레드도 종료한다.
워드프로세스 자동저장, 미디어플레이어의 동영상 및 음악재생, 가비지 컬렉터 등이 이와 같은 종류라 할 수 있다.

주 스레드가 데몬이 될 스레드의 setDaemon(true)를 호출하면 된다.

package ch14.sec08;

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

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

 package ch14.sec08;

public class DaemonExample {
public static void main(String[] args) {
    AutoSaveThread autoSaveThread = new AutoSaveThread();
    autoSaveThread.setDaemon(true); // AutoSaveThread()를 데몬 스레드로 만듬
    autoSaveThread.start();

    try {
        Thread.sleep(3000);
    } catch(Exception e) {}
    System.out.println("메인스레드 종료");
}}

14.9 스레드풀

매우 중요!
그때 그때 만들어서 병렬적으로 처리가능하지만 미리만들어놓는게 좋다.
스레드를 만드는 시간도 필요하다 생성시간을 줄여주자
스레드풀은 작업 처리에 사용되는 스레드를 제한 된 개수 만큼 정해놓고 작업 큐에 들어오는 작업들을 스레드가 하나씩 맡아 처리하는 것이다.
작업처리가 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리한다.
이렇게하면 작업량이 증가해도 스레드 개수가 늘어나지 않아 애플리켄이션의 성능이 급격히 저하되지 않는다.
예를들어 for문 100번 스레드 생성 -> 100개 만드는게 아니라 일끝난거 재사용해서 10개만으로 해결할수도 잇다. 스레드 낭비 막음.


풀이란? 여러개의 객체를 관리한다라고 이해하면 좋음 큐? 먼저 들어온놈을 먼저 해결하는 것
스레드 개수는 경험에 따라 개수를 관리

14.9.1 스레드풀 생성

java.util.concurrent 패키지에서 ExecutorService인터페이스와 Executors클래스를 사용한다.
Executors의 정적메소드를 이용하면 간단하게 스레드풀인 ExecutorService 구현객체를 만들 수 있다.

newCachedThreadPool() 초기수 0 코어수 0 최대수 integer.MAX_VALUE(최대 21억갠데 불가능)
작업량에 따라 자동으로 만들어지는데 작업량이 많아지면 스레드가 많아져서 그다지 사용x

newFixedThreadPool(int nThreads) 초기수0 코어수 생성된수 최대수 nThreads
()사이의 주어진 수만큼만 만듬.

초기수는 스레드풀이 생성될때 기본적으로 생성되는 스레드수
코어수는 스레드가 증가된 후 사용되지 않는 스레드를 제거할때 최소한 풀에서 유지하는 스레드수
최대수는 증가되는 스레드의 한도 수이다.

newCachedThreadPool()는 작업 개수가 많아지면 새스레드를 생성시켜 작업을 처리, 60초동안 스레드가 아무작업을 안하면 제거된다.

newFixedThreadPool(int nThreads)는 정해진 수만큼의 최대로 만들고 생성된 스레드는 제거안함.

위 두 메소드를 사용하지 않고 직접 ThreadPoolExecutor로 스레드풀을 생성할수도있다.

ExecutorService threadPool = new ThreadPoolExecutor(
        3,                                 //코어 스레드 개수
        100,                             //최대 스레드 개수
        120L,                             //놀고있는시간 120초동안 놀고있으면 종료
        TimeUnit.SECONDS,                 //놀고있는시간단위 
        new SynchronousQueue<Runnable>() //작업큐    
        // 작업큐에 작업을 넣는거도 스레드임 한번에 하나의 스레드에만 작업을 넣겟다
        );

14.9.2 스레드 풀 종료

스레드풀의 스레드는 기본적으로 데몬 스레드가 아니기때문에 바로 종료 안됨.
스레드풀의 모든 스레드를 종료하려면 ExecutorSercive의 메소드를 실행해야한다.
void shutdown() 현재 처리중인 작업뿐만아니라 작업 큐에 대기하고 있는 모든 작업을 처리한뒤 스레드풀 종료
List<Runnable> shutdownNow() 현재 작업 처리중인 스레드를 interrupt해서 작업을 중지시키ㅗㄱ 스레드풀을 종료한다.
리턴값은 작업큐에 있는 미처리된 작업의 목록이다.
가능하면 shutdown()을 사용하는게 좋다.

Runnable이 뭐냐? Runnable=작업내용을 가지고잇는객체, 한마디로 작업객체 스레드가 처리해야할 작업내용을 가지고있는 객체
스레드가 처리할것을 앞으로 Runnable 구현 객체로 만들어야함.
위의 SynchronousQueue<Runnable>() 이녀석 제네릭 타입임. 결국 작업객체의 종류가 들어와야함.

14.9.3 작업생성과 처리요청

즉 Runnable객체 혹은 Callable객체 생성하고 -> 스레드풀에 넣기라는 것
어떤스레드에서 처리한다느 내부객체에서 결정하는것임.
Runnable객체와 Callable객체의 차이점은 작업처리후 리턴값이 있느냐 없느냐의 차이이다.
없으면 전자 있으면 후자 Callable은 throws Exception 필요
그러나 대부분의 작업은 리턴값이 없어서 전자를 사용한다.
void execute(Runnable command) / Runnable을 작업큐에 저장 작업처리결과를 리턴하지 않음.
Future<T> submit(Callable<T> task) /Callable을 작업큐에 저장 작업처리결과를 받을수잇도록 Future를 리턴
리턴타입이 미래인이유? 바로 실행이아니라 작업큐에 넣으라 이기때문에 아직 처리가 안된거임. 앞에서부터 처리임.즉 미래에 처리되기때문이다.
Runnable객체와 Callable객체가 ExecutorService의 작업큐에 들어가면 처리할 스레드가 잇는지 보고 없으면 스래도를 새로 생성시킨다.
run() 또는 call()메소드를 실행하면서 작업을 처리한다.

14.9.3.1 리턴값이 있는 실습

이메일을 보내는 작업으로 1000개의 runnable 을 생성한다음 execute()메소드로 작업큐에 넣는다.

package ch14.sec09;

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

public class RunnableExecuteExample {
public static void main(String[] args) {
    String[][] mails = new String[1000][3]; // 2차원함수
    for(int i = 0 ; i< mails.length; i++ ) { // 1000번돌림
        mails[i][0] = "admin@my.com";
        mails[i][1] = "member" + i + "@my.com";
        mails[i][2] = "신상품 입고";
    }
    //ExecutorService 생성
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    //1000개의 메일 5개 스레드로 하겟다.

    //이메일을 보내는 작업 생성 및 처리모듈
    for (int i =0; i<1000; i++ ) {
        final int idx = i; //final 값저장 변경불가 final      
        //final을 넣은이유? 로컬변수를 로컬클래스안에서 사용하면 final특성을 가짐 
        //밑에 run()메소드의 익명객체에서 선언되면 그러면 i변경이 안됨. 여기션 익명객체 이니가 미리 for문에서 정의해두기
        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);
            //execute로 스레드에 넣어서 천번 돌림.
            }
        });
    }
    //ExecutorService종료
    executorService.shutdown();
    }}

14.9.3.2 리턴값이 없느 ㄴ실습

자연수를 덧셈하는 작업 으로 100개의 Callable을 생성하고 submit()메소드로 작업큐에 넣는다.
최대 5개의 스레드 작업큐에서 하나씩 꺼내어 call()메소드를 실행하면서 처리한다.
Future의 get()메소드는 작업이 끝날때까지 기다렷다가 call()메소드가 리턴한값을 리턴한다.

package ch14.sec09;

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

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

    //계산 작업 생성 및 처리요청
    for(int i = 1 ; i<=100 ; i++) {
        final int idx = i;
        //스레드풀에 submit로 넣기 자연수 넣기이니 정수인 Integer사용
        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; //call()메소드는 리턴값이 잇어서 리턴값을 해줘야함.
            }

        });
    //submit의 리턴값이 future임 future.get() 퓨처 리턴값을 얻어라 
    try {
        int result = future.get(); 
        //Callabe의 call() 메소드가 리턴한값얻기
        //리턴값얻으면 일시정지 된다. 리턴값 다얻으면 일시정지가 풀리고 출력.
        System.out.println("\t리턴값: " + result);
    } catch (Exception e) {
        e.printStackTrace();
    }

    }
//ExecutorService 종료
executorService.shutdown();
}}