preamble
Projects often encounter non-distributed scheduling tasks that need to be executed periodically at some point in the future. There are several ways to realize such a feature:
-
Timer
class, introduced in jdk1.3, is not recommended.
All of its tasks are executed serially, only one task can be executing at the same time, and any delay or exception in the previous task will affect the subsequent tasks. It is possible that the execution time of the tasks is too long and the tasks block each other.
- Spring
@Scheduled
Annotation, not really recommended
Although the bottom of this approach is to use the thread pool to achieve, but there is a big problem, all the tasks are used in the same thread pool, may lead to a long period of the task running the impact of short-cycle task running, resulting in thread pool "starvation", more recommended practice is to use the same type of task with the same thread pool.
- customizable
ScheduledThreadPoolExecutor
Implementation of scheduling tasks
This is the way this article focuses on, by customizing theScheduledThreadPoolExecutor
Scheduling thread pools and submitting scheduling tasks is the optimal solution.
Basic Introduction
ScheduledThreadPoolExecutor inherits from ThreadPoolExecutor and provides delayed or periodic execution of tasks, which is a kind of thread pool. Compared to ThreadPoolExecutor, it also has the following features.
-
Use the specialized task type - ScheduledFutureTask to execute periodic tasks, and also receive tasks that do not require time scheduling (these tasks are executed via ExecutorService).
-
Use a specialized storage queue - DelayedWorkQueue to store tasks, DelayedWorkQueue is a kind of unbounded delay queue DelayQueue. Compared to ThreadPoolExecutor also simplifies the execution mechanism (delayedExecute method, analyzed separately later).
-
The optional run-after-shutdown parameter is supported, and optional logic is supported to decide whether to continue the run cycle or delay the task after the pool has been shut down (shutdown). And the review logic is different when the task (re)submission operation overlaps with the shutdown operation.
Basic use
The most common application scenario for ScheduledThreadPoolExecutor is to implement a scheduling task.
Creation method
establishScheduledThreadPoolExecutor
There are a total of two ways to do this, the first through a custom parameter and the second through theExecutors
Factory method creation. The factory method is created according to theIn the Alibaba code specificationof recommendations, it is more recommended to create it using the first method.
- Custom Parameter Creation
ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory, RejectedExecutionHandler handler)
-
corePoolSize
: Number of threads for core work -
threadFactory
: Thread factory, used to create threads -
handler
:: Rejection tactics, saturation tactics
-
Executors
Factory Method Creation
-
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
: Create a pool of scheduling threads based on the number of core threads. -
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
: Create a pool of scheduling threads based on the number of core threads and the thread factory.
Core API
-
schedule(Runnable command, long delay, TimeUnit unit)
: Creates and executes a one-time operation that is enabled after a given delay
-
command
:: Tasks performed -
delay
: Time delayed -
unit
:: Units
-
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
: Timed execution of a periodic task, with a delayed delay after the task has been executed
-
command
:: Tasks performed -
initialDelay
: Time of initial delay -
delay
:: The end of the last execution and how long the execution is delayed -
unit
:: Units
@Test
public void testScheduleWithFixedDelay() throws InterruptedException {
// Creating a thread pool for scheduling tasks
ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
// Scheduled according to a fixed delay time since the completion of the last execution
(() -> {
try {
("scheduleWithFixedDelay ...");
(1000);
} catch (InterruptedException e) {
();
}
}, 1, 2, );
(10000);
}
-
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
: Execute periodic tasks at regular intervals according to a fixed rating, independent of the task runtime.
-
command
:: Tasks performed -
initialDelay
: Time of initial delay -
period
:: Periodicity -
unit
:: Units
@Test
public void testScheduleAtFixedRate() throws InterruptedException {
// Creating a thread pool for scheduling tasks
ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
// at fixed prices2Seconds to execute
(() -> {
try {
("scheduleWithFixedDelay ...");
(1000);
} catch (InterruptedException e) {
();
}
}, 1, 2, );
(10000);
}
Comprehensive Case
pass (a bill or inspection etc)ScheduledThreadPoolExecutor
Implementation of a timed task every Thursday at 18:00:00.
// ScheduledThreadPoolExecutor for Thursdays at 18:00:00
@Test
public void test() {
// Get the current time
LocalDateTime now = ();
(now);
// Get the Thursday time
LocalDateTime time = (18).withMinute(0).withSecond(0).withNano(0).with(); // Get the current time.
// if Current time > Thursday of this week, must find Thursday of next week
if((time) > 0) {
time = (1);
}
(time); // if((time) > 0) { time = (1); }
// initailDelay represents the time difference between the current time and Thursday.
// period the time between weeks
long initailDelay = (now, time).toMillis(); long period = 1000 * 60 * 60 * 24 * 7; // The time difference between the current time and Thursday.
long period = 1000 * 60 * 60 * 24 * 7; // period is the difference between the current time and Thursday's time.
ScheduledExecutorService pool = (1); (() ->;)
(() -> {
("running...") ;
}, initailDelay, period, ); }
}
underlying source code analysis
Let's take a look at the underlying source code of ScheduledThreadPool.
data structure
ScheduledThreadPoolExecutorinherited from ThreadPoolExecutor:
ScheduledThreadPoolExecutor has two internal classes ScheduledFutureTask and DelayedWorkQueue.
-
ScheduledFutureTask: inherits FutureTask, indicating that it is an asynchronous computing task; the topmost layer implements Runnable, Future, Delayed interfaces respectively, indicating that it is an asynchronous computing task that can be delayed in execution.
-
DelayedWorkQueue: This is a delayed queue defined by ScheduledThreadPoolExecutor for storing recurring or delayed tasks. It inherits from AbstractQueue and implements the BlockingQueue interface in order to fit ThreadPoolExecutor. Internally it only allows storing tasks of type RunnableScheduledFuture. The difference with DelayQueue is that it only allows storing RunnableScheduledFuture objects and implements a binary heap (DelayQueue utilizes PriorityQueue's binary heap structure).
Internal class ScheduledFutureTask
causality
//Sequence number for the same delay task
private final long sequenceNumber;
//The time the task can be executed, in nanoseconds.
private long time.
// The cycle time in which the repeating task can be executed, in nanoseconds.
private final long period;
//The reentrant task
RunnableScheduledFuture<V> outerTask = this.
// index of the deferred queue to support faster cancel operations
int heapIndex;
-
sequenceNumber: When two tasks have the same latency, they are queued in the order of the FIFO. sequenceNumber is the sequence number provided for tasks with the same latency.
-
time: the time, in nanoseconds, when the task can be executed, as calculated by the triggerTime method.
-
period: the cycle time of the task execution, in nanoseconds. Positive numbers indicate fixed-rate execution (provided for scheduleAtFixedRate), negative numbers indicate fixed-delay execution (provided for scheduleWithFixedDelay), and 0 indicates that the task is not repeated.
-
outerTask: requeued task, reordered by reExecutePeriodic method.
Core method run()
public void run() {
boolean periodic = isPeriodic();//whether the task is periodic or not
if (!canRunInCurrentRunState(periodic))//Is the current state executable?
cancel(false);
else if (!periodic)
//It's not a periodic task, execute it directly
(); else if (() {())
else if (()) {
setNextRunTime(); // set next run time
reExecutePeriodic(outerTask);//reorder a periodic task
}
}
Description: The run method of ScheduledFutureTask overrides the FutureTask version in order to reset/reorder the task when executing a periodic task. The execution of the task is realized through the run of the parent class FutureTask. Internally, there are two methods for periodic tasks.
- setNextRunTime(): Used to set the next run time, source code is as follows.
// Set the time for the next run of the task
private void setNextRunTime() {
long p = period.
if (p > 0) //fixed rate execution, scheduleAtFixedRate
time += p;
time += p; else
time = triggerTime(-p); //fixed delay execution, scheduleWithFixedDelay
}
// Calculate the execution time of the fixed delay task
long triggerTime(long delay) {
return now() +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay)));
}
- reExecutePeriodic(): the periodic task is re-queued for the next execution, with the following source code.
// reorder a periodic task
void reExecutePeriodic(RunnableScheduledFuture<? > task) {
if (canRunInCurrentRunState(true)) { //execution can continue after the pool is closed
().add(task);//task into the queue
// Recheck the run-after-shutdown parameter, remove the queued task if it can't continue, and cancel the task's execution
if (!canRunInCurrentRunState(true) && remove(task))
(false).
remove(task)) (false); else
ensurePrestart();//start a new thread waiting for the task
}
}
reExecutePeriodic has the same execution policy as delayedExecute, except that reExecutePeriodic doesn't enforce the denial policy but simply drops the task.
The cancel method
public boolean cancel(boolean mayInterruptIfRunning) {
boolean cancelled = (mayInterruptIfRunning);
if (cancelled && removeOnCancel && heapIndex >= 0)
remove(this);
return cancelled;
}
Essentially implemented by its parent class. When a task is canceled successfully, the removeOnCancel parameter determines whether to remove the task from the queue.
Core Properties
// continue executing existing periodic tasks after shutdown
private volatile boolean continueExistingPeriodicTasksAfterShutdown;
// continue executing existing delayed tasks after shutdown
private volatile boolean executeExistingDelayedTasksAfterShutdown = true;
//remove the task after canceling it
private volatile boolean removeOnCancel = false;
// Sequential numbering for tasks with the same delay, ensuring FIFO order between tasks
private static final AtomicLong sequencer = new AtomicLong();
-
continueExistingPeriodicTasksAfterShutdown: and executeExistingDelayedTasksAfterShutdown are the run-after- shutdown parameter to control the task execution logic after the pool is shut down.
-
removeOnCancel: used to control whether a task is removed from the queue after it is canceled. When a submitted periodic or deferred task is canceled before it is run, then it will not be run afterwards. In the default configuration, such canceled tasks are not removed before the session. This mechanism makes it easy to check and monitor the thread pool state, but it can also cause canceled tasks to linger indefinitely. In order to avoid this situation, we can set the removal policy through the setRemoveOnCancelPolicy method, and set the parameter removeOnCancel to true to remove the task from the queue as soon as it is canceled.
-
sequencer: the sequence number provided for tasks with the same delay, to ensure the FIFO order between tasks. It is the same as the sequenceNumber parameter inside ScheduledFutureTask.
constructor
First look at the constructor, ScheduledThreadPoolExecutor has four internal constructors, here we only look at the maximum constructor flexibility of this.
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}
The constructors all call the ThreadPoolExecutor construct via super and use the specific wait queue DelayedWorkQueue.
Schedule
public <V> ScheduledFuture<V> schedule(Callable<V> callable,
long delay,
TimeUnit unit) {
if (callable == null || unit == null)
throw new NullPointerException();
RunnableScheduledFuture<V> t = decorateTask(callable,
new ScheduledFutureTask<V>(callable, triggerTime(delay, unit)));//tectonic (geology)ScheduledFutureTaskmandates
delayedExecute(t);//mandates执行主方法
return t;
}
Description: schedule is mainly used to perform one-time (delayed) tasks. The function's execution logic consists of two steps.
- Encapsulation Callable/Runnable
protected <V> RunnableScheduledFuture<V> decorateTask(
Runnable runnable, RunnableScheduledFuture<V> task) {
return task;
}
- operate
private void delayedExecute(RunnableScheduledFuture<? > task) {
if (isShutdown())
reject(task);// pool is shutdown, execute reject strategy
else {
().add(task);//task into queue
if (isShutdown() &&.
!canRunInCurrentRunState(()) && // judge run-after-shutdown parameter
remove(task))//remove the task
(false);
else
ensurePrestart();//start a new thread waiting for the task
}
}
Description: delayedExecute is the main method that executes the task, the method execution logic is as follows.
-
If the pool is closed (ctl >= SHUTDOWN), enforce the task rejection policy;
-
The pool is running, first sort the tasks into the queue; then recheck the pool's shutdown state by executing the following logic.
A: If the pool is running, or the run-after-shutdown parameter has a value of true, the parent method ensurePrestart is called to start a new thread waiting to execute the task. ensurePrestart source code is as follows.
void ensurePrestart() {
int wc = workerCountOf(());
if (wc < corePoolSize)
addWorker(null, true);
else if (wc == 0)
addWorker(null, false);
}
ensurePrestart is a method of the parent class ThreadPoolExecutor that is used to start a new worker thread waiting for the execution of a task, and schedules a new thread even if corePoolSize is zero.
B: If the pool has been shut down and the run-after-shutdown parameter is false, the parent class (ThreadPoolExecutor) method remove removes the specified task from the queue, and the cancel task is called after successful removal.
scheduleAtFixedRate cap (a poem) scheduleWithFixedDelay
/**
* :: Create a periodic task, with the first execution delayed by an initialDelay.
* and then every PERIOD after that, without waiting for the first execution to complete to start the clock
*/
public ScheduledFuture<? > scheduleAtFixedRate(Runnable command,
long initialDelay, long period, scheduleAtFixedRate(Runnable command, long
long period, TimeUnit unit) {
Runnable command, long initialDelay, long period, TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (period <= 0)
throw new IllegalArgumentException();
// Build the RunnableScheduledFuture task type
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),//calculate the delay time of the task
(period));//calculate the execution period of the task
RunnableScheduledFuture<Void> t = decorateTask(command, sft);//execute user-defined logic
= t;//assign to outerTask, ready to be re-queued for next execution
delayedExecute(t);//execute the task
return t; // Assign to outerTask, ready to be re-queued for the next execution.
}
/**
* Create a task that executes periodically, with the first execution delayed by initialDelay.
* Start the next execution after a delay of delayDelay after the first execution.
*/
public ScheduledFuture<? > scheduleWithFixedDelay(Runnable command,
long initialDelay, long delay, scheduleWithFixedDelay(Runnable command, long
timeUnit unit) {
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (delay <= 0)
throw new IllegalArgumentException();
// Build the RunnableScheduledFuture task type
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),//calculate the delay of the task
(-delay));//calculate the execution period of the task
RunnableScheduledFuture<Void> t = decorateTask(command, sft);//execute user-defined logic
= t;//assign to outerTask, ready to be re-queued for next execution
delayedExecute(t);//execute the task
return t; // Assign to outerTask, ready to be re-queued for the next execution.
}
Description: The logic of the scheduleAtFixedRate and scheduleWithFixedDelay methods is similar to schedule.
Note the difference between scheduleAtFixedRate and scheduleWithFixedDelay!: At first glance the two methods look exactly the same, but in fact, there is a difference in this line of code. That's right, scheduleAtFixedRate passes a positive value, while scheduleWithFixedDelay passes a negative value, which is the period attribute of ScheduledFutureTask.
shutdown()
public void shutdown() {
();
}
//Cancel and clear all tasks that should not run due to the shutdown policy
@Override void onShutdown() {
BlockingQueue<Runnable> q = ();
//gainrun-after-shutdownparameters
boolean keepDelayed =
getExecuteExistingDelayedTasksAfterShutdownPolicy();
boolean keepPeriodic =
getContinueExistingPeriodicTasksAfterShutdownPolicy();
if (!keepDelayed && !keepPeriodic) {//Tasks not retained after pool closure
//Cancellation of tasks in sequence
for (Object e : ())
if (e instanceof RunnableScheduledFuture<?>)
((RunnableScheduledFuture<?>) e).cancel(false);
();//Clearing the Waiting Queue
}
else {//Retention of tasks after pool closure
// Traverse snapshot to avoid iterator exceptions
//Traversing snapshots to avoid iterator exceptions
for (Object e : ()) {
if (e instanceof RunnableScheduledFuture) {
RunnableScheduledFuture<?> t =
(RunnableScheduledFuture<?>)e;
if ((() ? !keepPeriodic : !keepDelayed) ||
()) { // also remove if already cancelled
//If the task has been canceled,Removing tasks from the queue
if ((t))
(false);
}
}
}
}
tryTerminate(); //Terminating the thread pool
}
Description: The pool shutdown method calls the parent class ThreadPoolExecutor's shutdown, which is analyzed in the ThreadPoolExecutor section. Here we introduce the following shutdown hook onShutdown method which is called in the shutdown method. Its main purpose is to cancel and clear all the tasks that should not be run due to the shutdown policy after shutting down the thread pool, based on the run-after-shutdown parameter ( The run-after-shutdown parameters ( continueExistingPeriodicTasksAfterShutdown and executeExistingDelayedTasksAfterShutdown) are used to decide whether to shut down the existing tasks after the thread pool is closed.
ScheduledThreadPoolExecutor swallows exception
in the event thatScheduledThreadPoolExecutor
When an exception is thrown on an error in a task, not only will the exception stack not be printed, but the scheduling will be canceled, so let's look at the example.
@Test
public void testException() throws InterruptedException {
// establish1Thread pool for scheduling tasks with multiple threads
ScheduledExecutorService scheduledExecutorService = ();
// establish一个任务
Runnable runnable = new Runnable() {
volatile int num = 0;
@Override
public void run() {
num ++;
// Simulated execution error
if(num > 5) {
throw new RuntimeException("implementation error");
}
("exec num: [{}].....", num);
}
};
// at intervals of1One mission per second.
(runnable, 0, 1, );
(10000);
}
- After only 5 executions, it doesn't print and doesn't execute because of an error report
- The task reported an error and didn't print the stack once, which moreover caused the scheduling task to be canceled, with serious consequences.
The solution is also very simple, as long as the exception is caught by try catch.
public void run() {
try {
num++;
// Simulated execution error
if (num > 5) {
throw new RuntimeException("implementation error");
}
("exec num: [{}].....", num);
} catch (Exception e) {
();
}
}
Exploring the Principles
So have you ever wondered why an error in a task would cause the exception to not print, or even the scheduling to be canceled? Take a look at the source code to find out.
overheaddelayedExecute
As you can see, the main execution method for a delayed or periodic task is to drop the task into a queue and have it fetched by the worker thread for subsequent execution.
- After the task is in the queue, it is time to execute the task content, which is actually in the run method of the inherited Runnable class.
// ScheduledFutureTask#run method
public void run() {
// Is the task periodic
boolean periodic = isPeriodic();
if (!canRunInCurrentRunState(periodic))
cancel(false); // If it's not a periodic task, it's not a periodic task.
// If the task is not periodic, call the following run once.
else if (!periodic)
(); // If it's a periodic task, call the following run else if (!periodic).
// If it's a periodic task, call runAndReset, and if it returns true, continue execution.
else if (()) {
// Set the next run time
setNextRunTime(); // Re-execute the scheduled task.
// Re-execute the scheduled task
reExecutePeriodic(outerTask);
}
}
- The key here is to look at
()
Whether the method returns true, and if so continue scheduling.
- The runAndReset method is also very simple, the key is to see how the reported exception is handled.
// FutureTask#runAndReset
protected boolean runAndReset() {
if (state != NEW ||
!(this, runnerOffset,
null, ()))
return false;
// Whether to proceed to the next dispatch,default (setting)false
boolean ran = false;
int s = state;
try {
Callable<V> c = callable;
if (c != null && s == NEW) {
try {
// operate
();
// If the execution is successful,set totrue
ran = true;
// Exception handling,crux
} catch (Throwable ex) {
// No modifications will be made.ranvalue of,endfalse,It also does not print the exception stack
setException(ex);
}
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
// Return results
return ran && s == NEW;
}
- The key point ran variable that ultimately returns is not the next time to continue scheduling execution
- If an exception is thrown, you can see that it will not modify ran to true.
wrap-up
Java's ScheduledThreadPoolExecutor timed task thread pool scheduled task if an exception is thrown, and the exception is not caught and thrown directly to the framework, will lead to ScheduledThreadPoolExecutor timed task is not scheduled.
Encapsulate wrapper classes to unify scheduling
In the actual project use, you can encapsulate a wrapper class in your own project, requiring all scheduling are submitted through a unified wrapper class, thus standardizing the code, the following code:
@Slf4j
public class RunnableWrapper implements Runnable {
// The actual thread task to be executed
private Runnable task; // The time at which the thread task is created.
// The time at which the thread task was created
private long createTime; // the time when the threaded task was created by the thread pool.
// The time when the threaded task will be run by the thread pool.
private long startTime; // The time the threaded task was created by the thread pool.
// The time when the threaded task will be run by the thread pool.
private long endTime; // Thread information.
// Thread information
private String taskInfo; // Thread information.
private boolean showWaitLog; private boolean showWaitLog; private String
/**
* How long between executions, print the log
*/
private long durMs = 1000L;
// When the task is created, it sets its creation time.
// But then it's possible that the task is submitted to the thread pool and queued up in the thread pool's queue.
public RunnableWrapper(Runnable task, String taskInfo) {
= task; = taskInfo; String taskInfo
= taskInfo.
= ();
}
public void setShowWaitLog(boolean showWaitLog) {
= showWaitLog; }
}
public void setDurMs(long durMs) {
= durMs; } public void setDurMs(long durMs) { = durMs; }
}
// This run method is not run when the task is queued in the thread pool.
// But it will be called when the task has finished queuing and is ready to be run by the thread pool
// At this point, you can set the start time of the threaded task.
@Override
public void run() {
= ();
// This is where you can call the monitoring system's API to report monitoring metrics.
// Use the startTime-createTime of the threaded task, which is actually the queuing time of the task.
// This prints the log output, which can also be exported to the monitoring system.
if(showWaitLog) {
("Task info: [{}], task queuing time: [{}]ms", taskInfo, startTime - createTime); }
}
// Then you can call the run method of the actual wrapped task
try {
(); } catch (Exception e) {
} catch (Exception e) {
("run task error", e); }
}
// Once the task has finished running, the time at which the task will end is set
= (); // After the task has finished running, it sets a time for the task to end.
// This can be done by calling the monitoring system's API to report monitoring metrics.
// Use the endTime - startTime of the threaded task, which is actually the task's runtime.
// This prints the execution time of the task, which can also be exported to the monitoring system.
if(endTime - startTime > durMs) {
("Task info: [{}], task execution time: [{}]ms", taskInfo, endTime - startTime); }
}
}
}
Of course, it is also possible to encapsulate various monitoring behaviors inside the wrapper class, such as printing the log execution time in this case.
Other points to note for use
- Why does the tuning strategy for ThreadPoolExecutor not apply to ScheduledThreadPoolExecutor?
Since ScheduledThreadPoolExecutor is a thread pool with a fixed core thread size and uses an unbounded queue, adjusting the maximumPoolSize has no effect on it (so ScheduledThreadPoolExecutor doesn't provide a constructor, the default maximum number of threads is fixed to Integer.MAX_VALUE). In addition, setting corePoolSize to 0 or setting core threads to be cleared after they become idle (allowCoreThreadTimeOut) is also not a good strategy, as it may result in no threads in the pool to handle periodic tasks once they reach a certain runtime cycle.
- Which methods does Executors provide to construct a ScheduledThreadPoolExecutor?
-
newScheduledThreadPool: A thread pool that can specify the number of core threads.
-
newSingleThreadScheduledExecutor: A thread pool with only one worker thread. If the internal worker thread is terminated due to an exception in the execution cycle task, a new thread is created in its place.
Note: newScheduledThreadPool(1, threadFactory) is not equivalent to newSingleThreadScheduledExecutor. newSingleThreadScheduledExecutor creates a thread pool that is guaranteed to have only one thread executing tasks and the number of threads is not scalable. The thread pool created by newSingleThreadScheduledExecutor guarantees that there is only one thread executing the task and that the number of threads is not scalable, whereas the thread pool created by newScheduledThreadPool(1, threadFactory) allows you to modify the number of core threads by using the setCorePoolSize method.
Interview questions column
Java interview questions columnIt's online, so feel free to visit.
- If you don't know how to write a resume, resume projects don't know how to package them;
- If there's something on your resume that you're not sure if you should put on it or not;
- If there are some comprehensive questions you don't know how to answer;
Then feel free to private message me and I will help you in any way I can.