https://jeong-pro.tistory.com/188
https://ho-log.tistory.com/109
https://engineerinsight.tistory.com/197
https://veneas.tistory.com/entry/Java-Executors-Thread-사용법
https://recordsoflife.tistory.com/1470
https://dev-coco.tistory.com/186
https://xxeol.tistory.com/44
https://yangbox.tistory.com/28
https://velog.io/@dailylifecoding/java-executor-service-weird-exit-methods#🥝-제대로-shutdown-시키려면
## PR 설명
- robotMoveService의 로봇 시뮬레이션 로직 재구성
- Redisson 스레드 풀 설정
- 불필요한 distributeLock 삭제
## 작업사항
- php 서버의 toDestination 사용
- php 서버의 toDestination에서 서빙 나갈 수 있는 로봇이 없으면 400 리턴. try-catch 문으로 예외처리 하는 과정에서 catch 부분에서 BaseException을 던지고 있었음
- 이 BaseException 가 발생하면 while(true) 가 실행되지 않고 정지되었음.
- 기존 php 서버에서 받아오던 toDestination 값을 robotService에 구현 해둔 것으로 사용
- php 서버 응답 사용하지 않으므로 응답 대기를 위한 @Async 삭제
- while(true)와 thread.sleep()을 이용한 로직 수행
- 기존에는 로봇 serving button을 실행한 후 sleep 5초, toDestination 값이 없으면 sleep 7초 수행
- 로봇을 보내고 로봇이 움직이는 시간을 고려해 15초 주기로 toDesitnation-servingButton 로직 실행
- scheduleExecutorService로 스레드 풀을 관리하고 자원 낭비를 하지 않도록 구성. sleep() 을 사용하는 스레드는 그 시간동안 스레드가 일시중지 되어있어 자원을 효율적으로 사용할 수 없음.
- ExecutorService는 스레드 풀을 사용하여 스레드 관리를 추상화하고 최적화하여, 스레드 생성과 소멸에 드는 비용과 복잡성을 줄임.
- 대기중인 스레드
- 메소드 실행 시 thread 생성, scheduleExecutor를 이용해 주기적으로 실행.
- 기본 30분 또는 지정 횟수 이상 로직을 돌리면 메소드 종료하고 thread 반납하도록 구현
- 대기중인 스레드를 사용하는 것도 고려했으나 자주 사용하지 않는 기능에 대기 스레드를 두는 것이 비효율적이라 판단. 매번 새로 만들고 반납하는데 리소스가 들지만 생성/반납 방법 선택
- 스레드 현황 확인하고 대기하는 스레드 없는 것 확인했음
## 연결 이슈 close
<!-- `close #이슈 번호`를 통해 PR 머지와 함께 이슈를 close 할 수 있습니다. -->
close #
scheduled 는 future 객체의 cancel(true) 사용
- true : 즉시 종료 / false : 대기하다 종료
AtomicReference<ScheduledFuture<?>> futureRef = new AtomicReference<>();
Runnable serving = () -> {
if (!redisService.isKeyPresent(redisKey) || count < times.get()) { // key 가 비어있거나 times 가 충족되었을 때
redisService.deleteIntegerData(redisKey);
**executorService.shutdownNow();**
ScheduledFuture<?> scheduledFuture = futureRef.get();
if (scheduledFuture != null) {
**scheduledFuture.cancel(true);**
}
return;
}
리턴 타입 | 메소드명(매개 변수) | 설명 |
---|---|---|
void | shutdown() | 현재 처리 중인 작업 뿐만 아니라 작업 큐에 대기하고 있는모든 작업을 처리한 뒤에 스레드풀을 종료시킨다. |
List | shutdownNow() | 현재 작업 처리 중인 스레드를 interrupt 해서 작업 중지를 시도하고 스레드 풀을 종료 시킨다.리턴값을 작업 큐에 있는 미처리된 작업(Runnable)의 목록이다 |
boolean | awaitTermination(long timeout, TimeUnit unit) | shutdown()메소드 호출 이후 모든 작업처리를 timeout 시간 내에 완료하면 true를 리턴하고, 완료하지 못하면 작업 처리 중인 스레드를 interrupt하고 false를 리턴한다 |
메소드명(매개) | 초기 스레드 수 | 코어 스레드 수 | 최대 스레드 수 |
---|---|---|---|
newCachedThreadPool() | 0 | 0 | Integer.MAX_VALUE |
newFixedThreadPool(int nThread) | 0 | nThreads | nThreads |
초기 스레드 수은 ExecutorService 객체가 생성될 때 기본적으로 생성되는 스레드 수를 말하고,
코어 스레드 수는 스레드 수가 증가된 후 사용되지 않는 스레드를 스레드풀에서 제거할 때
최대 스레드 수는 스레드풀에서 관리하는 최대 스레드 수이다.
ScheduledThread 인 scheduleAtFixedRate를 실행 중 특정 조건에서 종료시키려면 어떻게해?
ChatGPT
ScheduledThreadPoolExecutor
의 scheduleAtFixedRate
메서드를 사용할 때, 특정 조건에 따라 작업을 종료시키고 싶다면 작업 내에서 그 조건을 검사하고 조건이 충족될 때 작업을 적절히 종료시켜야 합니다. 이는 일반적으로 작업 내부에서 조건을 평가하고, 조건이 참이 되면 작업 실행을 멈추는 방식으로 구현됩니다. 다음은 간단한 예제 코드로, 어떻게 작업을 시작하고 특정 조건이 만족되면 중단시킬 수 있는지 보여줍니다:
javaCopy code
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
// Runnable 작업 생성
Runnable task = new Runnable() {
private int count = 0;
@Override
public void run() {
count++;
System.out.println("Running task count: " + count);
// 특정 조건을 검사 (예: count가 10이 됐을 때)
if (count >= 10) {
System.out.println("Condition met, stopping.");
// 작업 취소
executor.shutdown();
}
}
};
// scheduleAtFixedRate를 이용하여 작업을 주기적으로 실행
executor.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS);
}
}
이 예제에서는 ScheduledThreadPoolExecutor
를 사용하여 매초마다 task
를 실행합니다. task
내부에서는 실행 횟수를 계산하고, 이 횟수가 10회 이상이 되면 executor.shutdown()
을 호출하여 모든 스케줄링된 작업을 종료하고 스레드 풀을 정리합니다.
다음과 같은 방법으로도 작업을 종료할 수 있습니다:
Future 객체 사용: scheduleAtFixedRate
는 ScheduledFuture
객체를 반환합니다. 이 객체를 사용하여 실행 중인 작업을 취소할 수 있습니다.
외부 변수 사용: 작업이 외부 변수의 상태를 검사하고 해당 변수에 따라 실행을 계속할지 결정할 수 있습니다.
이러한 방법들은 각 상황에 맞게 조정할 수 있으며, 작업의 주기적 실행을 관리하는 데 매우 유용합니다.
스레드풀을 정리하지 않고 할 수 있어? 저렇게 하면
[2024-04-16 09:30:26][ThreadPoolTaskExecutor-2] ERROR o.s.a.i.SimpleAsyncUncaughtExceptionHandler - Unexpected exception occurred invoking async method: public void com.example.helper_spring_boot.src.robotMove.RobotMoveService.executeSimulator(java.lang.String,java.lang.Integer)
java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.ScheduledThreadPoolExecutorRunnableAdapter@49c92de7[Wrapped task = com.example.helper_spring_boot.src.robotMove.RobotMoveService$$Lambda$2177/0x0000000801f68b50@7b8f63c8]] rejected from java.util.concurrent.ScheduledThreadPoolExecutor@6b133599[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 3]
at java.base/java.util.concurrent.ThreadPoolExecutor$f8d25041.invoke()
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
이 에러나
ChatGPT
제시하신 에러 메시지는 java.util.concurrent.RejectedExecutionException
을 보여주고 있습니다. 이 에러는 작업을 수행하려는 스레드 풀이 이미 종료되었거나 종료 과정에 있을 때 발생합니다. 즉, 스레드 풀이 더 이상 새로운 작업을 수용할 수 없는 상태입니다.
스레드 풀을 종료하지 않고 주기적인 작업을 취소하려면, 스레드 풀 자체를 종료하는 대신 ScheduledFuture<?>
인스턴스를 사용하여 특정 작업만 취소할 수 있습니다. 다음은 scheduleAtFixedRate
로 반환되는 ScheduledFuture
를 사용하여 특정 조건이 만족되면 작업을 취소하고 스레드 풀은 계속 유지하는 방법을 보여주는 예제 코드입니다:
javaCopy code
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
// Runnable 작업 생성
Runnable task = new Runnable() {
private int count = 0;
@Override
public void run() {
count++;
System.out.println("Running task count: " + count);
// 특정 조건을 검사 (예: count가 10이 됐을 때)
if (count >= 10) {
System.out.println("Condition met, cancelling the task.");
// 이 행은 더이상 작업을 수행하지 않도록 작업 취소
thisFuture.cancel(false);
}
}
};
// scheduleAtFixedRate를 이용하여 작업을 주기적으로 실행
ScheduledFuture<?> thisFuture = executor.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS);
}
}
이 코드에서는 thisFuture
변수를 사용하여 작업을 참조하고, 조건이 충족되면 thisFuture.cancel(false)
메서드를 호출하여 특정 작업을 취소합니다. 이 때 false
는 작업이 현재 실행 중이라면 완료될 때까지 기다리라는 의미이고, true
를 사용하면 실행 중인 작업도 즉시 중단됩니다.
이 방법을 사용하면 스레드 풀은 계속 살아있으며, 필요에 따라 다른 작업을 추가하거나 스케줄을 조정할 수 있습니다.
Cannot resolve symbol 'scheduledFuture'
이렇게 뜨는데?
ChatGPT
위에서 보여준 코드 예제에는 scheduledFuture
변수를 적절하게 정의하지 않은 실수가 있었습니다. Runnable
내부에서 scheduledFuture
의 참조를 사용하려고 했으나, 이는 람다 표현식이나 내부 클래스에서 사용할 때 적절히 final 이거나 effectively final이어야 합니다.
여기서 사용할 수 있는 해결책 중 하나는 Runnable
을 구현하는 클래스 안에서 ScheduledFuture
를 저장할 수 있는 필드를 생성하는 것입니다. 그럼 이 필드를 통해 외부에서 ScheduledFuture
를 설정할 수 있으며, 내부에서도 접근이 가능해집니다.
다음은 수정된 코드 예제입니다:
javaCopy code
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
// Runnable 작업을 위한 클래스 정의
class PeriodicTask implements Runnable {
private int count = 0;
private ScheduledFuture<?> future;
public void setFuture(ScheduledFuture<?> future) {
this.future = future;
}
@Override
public void run() {
count++;
System.out.println("Running task count: " + count);
// 특정 조건을 검사 (예: count가 10이 됐을 때)
if (count >= 10) {
System.out.println("Condition met, cancelling the task.");
// 이 행은 더 이상 작업을 수행하지 않도록 작업 취소
if (future != null) {
future.cancel(false);
}
}
}
}
// Runnable 작업 인스턴스 생성
PeriodicTask task = new PeriodicTask();
// scheduleAtFixedRate를 이용하여 작업을 주기적으로 실행
ScheduledFuture<?> future = executor.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS);
// Future 설정
task.setFuture(future);
}
}
이 코드에서는 PeriodicTask
라는 내부 클래스를 정의하고, 이 클래스는 Runnable
인터페이스를 구현합니다. 이 클래스에는 ScheduledFuture<?>
타입의 필드와 이 필드를 설정하는 메서드가 포함되어 있습니다. 메인 메서드에서는 PeriodicTask
인스턴스를 생성하고, 스케줄러로부터 반환받은 ScheduledFuture<?>
인스턴스를 이 인스턴스에 설정합니다. 이렇게 하면 Runnable
내부에서도 스스로를 취소할 수 있습니다.
https://lh3.googleusercontent.com/a/AGNmyxaOICM05O7g-k2so1l6JcfTKXoexlQ_oEOfdszc=s96-c
Runnable serving = () -> {
log.info("times"+String.valueOf(times.get()));
if (!redisService.isKeyAbsent(redisKey) || count < times.get()) { // key 가 비어있거나 times 가 충족되었을 떄
log.info("simulator end .. ");
redisService.deleteIntegerData(redisKey);
// executorService.shutdownNow();
shutdownAndAwaitTermination(executorService);
scheduledFuture.cancel(false);
return ;
}
int randomIndex = (int) (Math.random() * tablesList.size());
String destinationTable = tablesList.get(randomIndex).getTableName();
Integer destinationNode = tablesList.get(randomIndex).getNodeId();
try {
RobotsDestinationResDto resDto = robotsService.toDestinationV2(userId);
if (!resDto.isError()) {
String topic = resDto.getMapName() + "/" + resDto.getRobotId() + "/server/button";
MqttButtonReqDto buttonReqDto = new MqttButtonReqDto(destinationNode, destinationTable, "serving");
mqttPublishService.publishData(buttonReqDto, topic);
times.getAndIncrement();
redisService.updateValueRemainingExpiration(redisKey, times.get());
} else {
redisService.deleteIntegerData(redisKey);
}
} catch (BaseException e) {
// to-desination 없음. 대기
}
};
ScheduledFuture<?> scheduledFuture = executorService.scheduleAtFixedRate(serving, 10, 10, TimeUnit.SECONDS);
log.info("future is cancelled :: "+ scheduledFuture.isCancelled());
// scheduledFuture.cancel(true);
이렇게 되어있는데 해결해줘
ChatGPT
주어진 코드에서 scheduledFuture
를 사용하려고 하지만 Runnable
람다 표현식 내에서는 scheduledFuture
가 아직 초기화되지 않았기 때문에 "Cannot resolve symbol 'scheduledFuture'" 오류가 발생합니다. 람다 표현식 내부에서는 외부의 로컬 변수에 접근할 때 그 변수가 final
이거나 사실상 final
(effectively final)인 경우에만 접근할 수 있습니다. 여기서 문제는 scheduledFuture
가 람다 표현식 이후에 선언되고 초기화되기 때문에 람다 내에서 사용할 수 없는 것입니다.
이 문제를 해결하기 위한 한 가지 방법은 Runnable
을 별도의 클래스로 정의하는 것입니다. 이렇게 하면 Runnable
의 인스턴스를 생성하기 전에 scheduledFuture
를 저장할 필드를 설정할 수 있습니다. 다음은 코드를 수정한 예제입니다:
이 접근 방식은 future
를 Runnable
구현 내부에서 참조하면서도 future
가 final
또는 effectively final
을 만족하도록 해줍니다. ServingTask
클래스를 별도로 정의함으로써 더 많은 컨트롤을 할 수 있고, Runnable
내에서 외부 변수를 참조하는 문제를 해결할 수 있습니다.
람다식을 사용할 수 있는 다른 방법은 없어?
ChatGPT
람다 표현식을 사용하면서 scheduledFuture
변수를 참조할 수 있는 한 가지 방법은 AtomicReference
를 사용하는 것입니다. AtomicReference
는 final
또는 사실상 final
(effectively final) 상태를 유지하면서, 참조하는 객체를 변경할 수 있도록 해줍니다. 이 방법을 사용하면 Runnable
내에서 scheduledFuture
를 참조할 수 있게 됩니다.
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(0);
이걸 0으로 설정했을 때랑 1로 설정했을 때랑 차이가 뭐야?
ChatGPT
**`ScheduledExecutorService`**의 **`newScheduledThreadPool`** 메소드를 이용하여 스레드 풀을 생성할 때 코어 스레드 수를 설정하는 것은 중요한 설정 중 하나입니다. 이 수는 스레드 풀이 유지할 수 있는 최소 스레드 수를 결정합니다. 여기서 **`0`**과 **`1`**로 설정하는 것의 차이를 살펴보겠습니다:
### **코어 스레드 수가 0일 때**
- **스레드 풀 생성**: **`newScheduledThreadPool(0)`**을 호출하면 코어 스레드 수가 0인 스레드 풀이 생성됩니다. 이는 스레드 풀이 비활성 상태일 때 어떠한 스레드도 유지하지 않겠다는 것을 의미합니다. 즉, 스케줄된 작업이 없을 경우 스레드 풀 내에 스레드가 존재하지 않습니다.
- **작업 실행**: 작업이 스케줄될 때, 필요한 스레드가 동적으로 생성됩니다. 작업이 완료된 후 일정 시간(기본적으로 60초) 동안 다른 작업이 발생하지 않으면 스레드는 종료되고 스레드 풀은 다시 스레드가 없는 상태가 됩니다.
### **코어 스레드 수가 1일 때**
- **스레드 풀 생성**: **`newScheduledThreadPool(1)`**을 호출하면 코어 스레드 수가 1인 스레드 풀이 생성됩니다. 이는 스레드 풀이 항상 최소한 한 개의 스레드를 유지하도록 보장합니다.
- **작업 실행**: 스레드 풀에 상시로 하나의 스레드가 존재하기 때문에, 스케줄된 작업은 이 스레드에서 실행됩니다. 만약 해당 스레드가 작업 중이 아닐 때 추가적인 작업이 스케줄된다면, 이 스레드에서 즉시 실행될 수 있습니다. 코어 스레드가 계속 유지되기 때문에, 작업 간의 전환 시간이 단축될 수 있습니다.
### **차이점**
- **스레드 유지**: 0으로 설정하면 스레드가 필요할 때만 생성되고, 그 외의 시간에는 스레드를 유지하지 않습니다. 반면, 1로 설정하면 항상 최소 한 개의 스레드가 유지되어 빠른 작업 시작이 가능합니다.
- **자원 사용**: 0으로 설정한 경우 자원 사용을 최소화할 수 있지만, 작업 시작 시 스레드 생성에 드는 비용이 발생합니다. 1로 설정한 경우 상시적인 자원 사용이 있지만, 작업 대기 시간이 줄어들 수 있습니다.
스레드 풀의 크기를 0으로 설정하는 것은 일반적으로 추천되지 않습니다. 이는 스레드 풀을 효율적으로 사용하지 못하게 만들 수 있으며, 작업 처리 시 지연을 발생시킬 수 있습니다. 최소한 하나 이상의 스레드를 유지하는 것이 좋은 관행으로 간주됩니다.
while(true)
루프와 Thread.sleep
을 사용하는 방식은 스레드를 직접 관리해야 하며, 이는 종종 비효율적이고 오류를 일으키기 쉬울 수 있습니다. 반면, ExecutorService
는 스레드 풀을 사용하여 스레드 관리를 추상화하고 최적화하여, 스레드 생성과 소멸에 드는 비용과 복잡성을 줄여줍니다.Thread.sleep
은 지정된 시간 동안 스레드를 일시 중지합니다만, 실행 준비가 되었을 때 스레드가 즉시 실행되는 것은 보장되지 않습니다. 스케줄이 지연될 수 있으며, sleep
후의 실제 실행 시간은 예측하기 어려울 수 있습니다. 반면, scheduledFixedRate
는 지정된 주기에 따라 작업을 실행하도록 예약하며, 주어진 주기가 일정하게 유지되도록 노력합니다. 이는 특히 주기적인 작업 실행이 중요한 어플리케이션에 유리합니다.scheduledFixedRate
는 이전 작업의 실행이 지연되더라도 정해진 주기를 유지하려고 시도합니다. 만약 작업이 예상보다 오래 걸리면, ExecutorService
는 다음 작업 시작 시간을 조정하여 빠진 시간을 보충하려고 합니다. while
루프와 sleep
을 사용하는 경우, 각 반복 사이의 지연은 처리 시간에 더해져서 전체 스케줄에 영향을 미칠 수 있습니다.ExecutorService
는 여러 작업을 스레드 풀에 효율적으로 할당하여 자원을 더욱 효과적으로 활용할 수 있습니다. 이는 특히 여러 작업을 동시에 실행해야 하는 경우 유용합니다. 를 클래스단에 선언하고 {실행 → stop → 다시 실행} 하면 **
java.util.concurrent.RejectedExecutionException`** 예외 발생ScheduledExecutorService
를 사용하여 정해진 간격으로 작업을 반복 실행합니다. 특정 조건이 충족되거나 오류가 발생하여 더 이상 작업을 실행할 필요가 없을 경우, 예약된 작업을 명시적으로 취소하는 것이 시스템 자원을 해방하는 데 도움이 됩니다.ExecutorService
의 실행을 점진적으로 종료하도록 요청합니다. 이 메소드를 호출하면 더 이상 새로운 작업을 수락하지 않으며, 이미 제출된 작업들은 완료될 때까지 계속 실행됩니다. 하지만 새로운 작업을 시작할 수는 없습니다.ScheduledExecutorService
의 경우 이미 예약된 반복 작업도 완료될 때까지 계속 실행될 수 있습니다.특정 ScheduledFuture
작업을 취소하려고 시도합니다. 이 메소드에 true
를 인자로 전달하면, 실행 중인 작업이 있을 경우 강제로 중단을 시도합니다 (즉, 스레드를 interrupt 시킵니다).
이 작업은 해당 Future
에 의해 나타내진 하나의 작업에만 영향을 미치며, 다른 스케줄된 작업이나 실행자 서비스 전체에는 영향을 주지 않습니다.
/** 로봇 시뮬레이션 로직 */
public void executeSimulator(String userId, Integer count) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(0);
Runnable serving = () -> {
if (@@@@) { // key 가 비어있거나 times 가 충족되었을 때
executorService.shutdownNow();
ScheduledFuture<?> scheduledFuture = futureRef.get();
if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}
return;
}
@@@
};
ScheduledFuture<?> scheduledFuture = executorService.scheduleAtFixedRate(serving, 1, 15, TimeUnit.SECONDS);
futureRef.set(scheduledFuture);
}
@Scheduled
를 사용하는 것이 일반적으로 더 편리합니다. 스프링의 구성 요소와 잘 통합되고, 개발자가 스프링의 다른 기능과 함께 쉽게 사용할 수 있도록 설계되었습니다.ScheduledExecutorService
가 더 적합할 수 있습니다. 복잡한 스레드 관리 요구사항이 있거나 특정 작업에 대한 매우 세밀한 스케줄링 조정이 필요한 경우 이를 사용하는 것이 좋습니다.