peaches-book-study / effective-java

이펙티브 자바 3/E
0 stars 2 forks source link

Item 80. 스레드보다는 실행자, 태스크, 스트림을 애용하라 #71

Open byunghyunkim0 opened 1 month ago

byunghyunkim0 commented 1 month ago

Chapter : 11. 동시성

Item : 80. 스레드보다는 실행자, 태스크, 스트림을 애용하라

Assignee : byunghyunkim0


🍑 서론

과거에는 단순한 작업 큐(work queue)를 작성하기 위해서 많은 코드를 작성해야 했다.

🍑 본론

java.util.concurrent

// 실행자에 실행할 태스크를 넘기는 방법 exec.execute(runnable);

// 실행자 종료 (이 작업이 실패하면 VM 자체가 종료되지 않을 것이다.) exec.shutdown();


## 실행자 프레임워크 주요 기능
- 특정 태스크가 완료되기를 기다린다.
```java
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        Callable<String> hello = () -> {
            Thread.sleep(2000L);
            return "Hello";
        };

        Future<String> helloFuture = executorService.submit(hello);
        System.out.println("Started!");

        System.out.println(helloFuture.get()); // hello가 완료될때까지 기다림

        System.out.println("End!!");
        executorService.shutdown();
    }
}

public class Main { public static void main(String[] args) throws InterruptedException, ExecutionException { ExecutorService executorService = Executors.newFixedThreadPool(3);

    Callable<String> hello = () -> {
        Thread.sleep(2000L);
        return "Hello";
    };

    Callable<String> java = () -> {
        Thread.sleep(4000L);
        return "Java";
    };

    Callable<String> study = () -> {
        Thread.sleep(1000L);
        return "Study";
    };

    List<Future<String>> allFutures = executorService.invokeAll(Arrays.asList(hello, java, study));

    for (Future<String> f : allFutures) {
        System.out.println(f.get()); // Hello, Java, Study 순서대로 출력된다.
    }

    String anyFutures = executorService.invokeAny(Arrays.asList(hello, java, study));

    System.out.println(anyFutures); // Study
    executorService.shutdown();
}

}


- 실행자 서비스가 종료하기를 기다린다. (awaitTermination 메서드)
```java
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) throws InterruptedException {

        // awaitTermination 를 이용해서 시간측정을 하려고 한다.
        // 일단 시작 시간을 저장한다.
        LocalDateTime startTime = LocalDateTime.now();

        // 쓰레드풀 생성
        ExecutorService executorService = Executors.newFixedThreadPool(4);

        // ? ~ 5 초 사이에 끝나는 Runnable 들을 submit 한다.
        executorService.submit(getRunnable(new Random().nextLong(3000,5000)));
        executorService.submit(getRunnable(new Random().nextLong(2000,5000)));
        executorService.submit(getRunnable(new Random().nextLong(4000,5000)));
        executorService.submit(getRunnable(new Random().nextLong(3000,5000)));
        // ExecutorService shutdown!
        executorService.shutdown();

        // executorService.shutdown(); 를 호출한 main 쓰레드가
        // executorService 가 완전히 종료될 때까지 기다리는(= blocking)
        // 하는 작업을 수행한다. 기다리는 시간을 첫번째 파라미터로 넣는데,
        // 여기서는 무한정 대기인 Long.MAX_VALUE 을 준다.
        System.out.println("Blocking 시작!");
        executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);

        // blocking 하는 데 걸린 시간 측정
        LocalDateTime endTime = LocalDateTime.now();
        System.out.println("걸린 시간: "
                + Duration.between(startTime, endTime).toSeconds() + "초");

        System.out.println("main thread 끝!");
    }

    private static Runnable getRunnable(Long time) {
        return () -> {
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {

                System.out.println("InterruptedException occurred from ... "
                        + Thread.currentThread().getName());

                throw new RuntimeException(e);

            }
            System.out.println(Thread.currentThread().getName());
        };
    }
}

import java.util.concurrent.*;

public class Main { public static void main(String[] args) { // 스레드 풀 생성 ExecutorService executorService = Executors.newFixedThreadPool(3);

    // ExecutorCompletionService 생성
    ExecutorCompletionService<String> completionService = new ExecutorCompletionService<>(executorService);

    // 작업 제출
    completionService.submit(new MyTask("Study"));
    completionService.submit(new MyTask("Hello"));
    completionService.submit(new MyTask("Java"));

    try {
        // 작업 결과 순서대로 받기
        for (int i = 0; i < 3; i++) {
            Future<String> future = completionService.take();
            System.out.println(future.get());
        }
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    } finally {
        // 스레드 풀 종료
        executorService.shutdown();
    }
}

private record MyTask(String name) implements Callable<String> {
    @Override
    public String call() throws Exception {
        Thread.sleep((int) (Math.random() * 3000)); // 작업 처리 시간 지연
        return name;
    }
}

}


- 태스크를 특정 시간에 혹은 주기적으로 실행하게 한다 (ScheduledThreadPoolExecutor 이용)
```java
import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) {
        // ScheduledThreadPoolExecutor 생성
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

        // 1초 후에 작업 실행
        scheduler.schedule(new MyTask("Task 1"), 1, TimeUnit.SECONDS);

        // 2초 후에 작업 실행, 그 후 3초 간격으로 반복 실행
        scheduler.scheduleAtFixedRate(new MyTask("Task 2"), 2, 3, TimeUnit.SECONDS);

        // 3초 후에 작업 실행, 그 후 5초 간격으로 반복 실행
        scheduler.scheduleWithFixedDelay(new MyTask("Task 3"), 3, 5, TimeUnit.SECONDS);

        // 10초 후에 스레드 풀 종료
        scheduler.schedule(() -> scheduler.shutdown(), 10, TimeUnit.SECONDS);
    }

    private record MyTask(String name) implements Runnable {

        @Override
            public void run() {
                System.out.println(name + " started");
                try {
                    Thread.sleep(2000); // 2초 동안 작업 수행
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(name + " finished");
            }
        }
}

ThreadPool 종류

ThreadPoolExecutor

Executors.newCachedThreadPool

Executors.newFixedThreadPool

주의점

포크-조인(fork-join)

Callable & Runable

Callable과 Runnable은 자바에서 비동기 작업을 정의하기 위해 사용되는 두 가지 인터페이스입니다.

  1. Callable

    • Callable 인터페이스는 call() 메서드를 정의하고 있습니다.
    • call() 메서드는 작업 수행 후 결과값을 반환할 수 있습니다.
    • Callable은 작업의 결과를 반환할 수 있기 때문에, 주로 작업의 결과를 활용해야 하는 경우에 사용됩니다.
    • Callable 인터페이스는 Future 객체와 함께 사용되어, 작업 결과를 비동기적으로 가져올 수 있습니다.
  2. Runnable

    • Runnable 인터페이스는 run() 메서드를 정의하고 있습니다.
    • run() 메서드는 작업 수행 후 결과값을 반환할 수 없습니다.
    • Runnable은 작업의 결과를 반환할 수 없기 때문에, 주로 작업 자체만 수행하면 되는 경우에 사용됩니다.
    • Runnable 인터페이스는 Thread 객체와 함께 사용되어, 작업을 비동기적으로 실행할 수 있습니다.

차이점

사용 사례

🍑 결론

실행자 프레임워클르 통해서 스레드를 관리해라


Referenced by

-