@@ -21,7 +21,8 @@ execution order at the same time.
2121
2222## Prerequisite
2323
24- Java 8 or better
24+ - Java 8 or better for versions before 20230922.20230925.0 (exclusive)
25+ - Java 21 or better for versions after 20230922.20230925.0 (inclusive)
2526
2627## Get it...
2728
@@ -31,21 +32,21 @@ Install as a compile-scope dependency in Maven or other build tools alike.
3132
3233## Use it...
3334
34- ### General notes
35+ Some general notes:
3536
36- #### Sequence keys
37+ - Sequence keys
3738
3839A sequence key cannot be ` null ` . Any two keys, ` sequenceKey1 ` and ` sequenceKey2 ` , are considered "the same sequence key"
3940if and only if ` Objects.equals(sequenceKey1, sequenceKey2) ` returns ` true ` .
4041
41- #### Thread safety
42+ - Thread safety
4243
4344A conseq4j instance is thread-safe in and of itself. The usual thread-safety rules and concerns, however, still apply
4445when programming the executable tasks. Moreover, in the context of concurrency and sequencing, the thread-safety concern
4546goes beyond concurrent modification of individual-task data, into that of meaningful execution order among multiple
4647related tasks.
4748
48- #### Concurrency and sequencing
49+ - Concurrency and sequencing
4950
5051First of all, by definition, there is no such thing as order or sequence among tasks submitted concurrently by different
5152threads. No particular execution order is guaranteed on those concurrent tasks, regardless of their sequence keys. The
@@ -63,10 +64,10 @@ etc...
6364
6465### Style 1: summon a sequential executor by its sequence key, then use the executor as with a JDK ` ExecutorService `
6566
66- #### API
67+ - API
6768
6869``` java
69- public interface SequentialExecutorServiceFactory extends Terminable {
70+ public interface SequentialExecutorServiceFactory {
7071 /**
7172 * @param sequenceKey
7273 * an {@link Object} instance whose hash code is used to summon the corresponding executor.
@@ -77,40 +78,6 @@ public interface SequentialExecutorServiceFactory extends Terminable {
7778}
7879```
7980
80- where ``` Terminable ``` is defined as
81-
82- ``` java
83- public interface Terminable {
84- /**
85- * Initiates an orderly shutdown of all managed thread resources. Previously submitted tasks are executed, but no
86- * new tasks will be accepted. Invocation has no additional effect if already shut down.
87- * <p >
88- * This method does not wait for the previously submitted tasks to complete execution. Use an external awaiting
89- * mechanism to do that, with the help of {@link #isTerminated()}.
90- */
91- void shutdown ();
92-
93- /**
94- * Non-blocking
95- *
96- * @return true if all tasks of all managed executors have completed following shut down. Note that isTerminated is
97- * never true unless shutdown was called first.
98- */
99- boolean isTerminated ();
100-
101- /**
102- * Attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the
103- * tasks that were awaiting execution.
104- * <p >
105- * This method does not wait for the previously submitted tasks to complete execution. Use an external awaiting
106- * mechanism to do that, with the help of {@link #isTerminated()}.
107- *
108- * @return Tasks submitted but never started executing
109- */
110- List<Runnable > shutdownNow ();
111- }
112- ```
113-
11481This API style loosely takes the form of "thread affinity". Sequence keys are used to summon executors of JDK
11582type [ ExecutorService] ( https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html ) . The same
11683sequence key always gets back the same sequential executor. All tasks of that sequence key can then be "affined" to and
@@ -123,7 +90,7 @@ Consider using this style when the summoned executor needs to provide
12390the [ syntax and semantic richness] ( https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html#method.summary )
12491of the JDK ` ExecutorService ` API.
12592
126- #### Sample usage
93+ - Sample usage
12794
12895``` java
12996public class MessageConsumer {
@@ -137,7 +104,8 @@ public class MessageConsumer {
137104 */
138105 private final SequentialExecutorServiceFactory conseqServiceFactory = ConseqServiceFactory . instance();
139106
140- @Autowired private ShoppingEventProcessor shoppingEventProcessor;
107+ @Autowired
108+ private ShoppingEventProcessor shoppingEventProcessor;
141109
142110 /**
143111 * Suppose run-time invocation of this method is managed by the messaging provider. This is usually via a single
@@ -154,42 +122,46 @@ public class MessageConsumer {
154122}
155123```
156124
157- - The implementation of this thread-affinity style relies on hashing of the sequence keys into a fixed number of
158- "buckets". These buckets are each associated with a sequential executor. The same sequence key is always hashed to and
159- summons back the same executor. Single-threaded, each executor ensures the execution order of all its tasks is the
160- same as they are submitted; excessive tasks pending execution are buffered in a FIFO task queue. Thus, the total
161- number of buckets (i.e. the max number of available executors and the general concurrency) is the maximum number of
162- tasks that can be executed in parallel at any given time.
163- - As with hashing, collision may occur among different sequence keys. When hash collision happens, tasks of different
164- sequence keys are assigned to the same executor. Due to the single-thread setup, the executor still ensures the local
165- sequential execution order for each individual sequence key's tasks. However, unrelated tasks of different sequence
166- keys now assigned to the same bucket/executor may delay each other's execution inadvertently while waiting in the
167- executor's task queue. Consider this a trade-off of the executor's having the same syntax and semantic richness as a
168- JDK [ ExecutorService] ( https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html ) .
169- - To account for hash collision, conseq4j does not support any shutdown action on the API-provided
170- executor ([ ExecutorService] ( https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html ) )
171- instance. That is to prevent unintended task cancellation across different sequence keys.
172- The [ Future] ( https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Future.html ) instance(s) subsequently
173- returned by the executor, though, is still cancellable. The hash collision may not be an issue for workloads that are
174- asynchronous and focused on overall through-put, but is something to be aware of.
175- - The default general concurrency is either 16 or the JVM
176- run-time's [ availableProcessors] ( https://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html#availableProcessors-- ) ,
177- which ever is larger:
125+ The implementation of this thread-affinity style relies on hashing of the sequence keys into a fixed number of
126+ "buckets". These buckets are each associated with a sequential executor. The same sequence key is always hashed to and
127+ summons back the same executor. Single-threaded, each executor ensures the execution order of all its tasks is the
128+ same as they are submitted; excessive tasks pending execution are buffered in a FIFO task queue. Thus, the total
129+ number of buckets (i.e. the max number of available executors and the general concurrency) is the maximum number of
130+ tasks that can be executed in parallel at any given time.
131+
132+ As with hashing, collision may occur among different sequence keys. When hash collision happens, tasks of different
133+ sequence keys are assigned to the same executor. Due to the single-thread setup, the executor still ensures the local
134+ sequential execution order for each individual sequence key's tasks. However, unrelated tasks of different sequence
135+ keys now assigned to the same bucket/executor may delay each other's execution inadvertently while waiting in the
136+ executor's task queue. Consider this a trade-off of the executor's having the same syntax and semantic richness as a
137+ JDK [ ExecutorService] ( https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html ) .
138+
139+ To account for hash collision, conseq4j Style 1 does not support any shutdown action on the API-provided
140+ executor ([ ExecutorService] ( https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html ) )
141+ instance. That is to prevent unintended task cancellation across different sequence keys.
142+ The [ Future] ( https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Future.html ) instance(s) subsequently
143+ returned by the executor, though, is still cancellable. The hash collision may not be an issue for workloads that are
144+ asynchronous and focused on overall through-put, but is something to be aware of.
145+
146+ The default general concurrency is the JVM
147+ run-time's [ availableProcessors] ( https://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html#availableProcessors-- ) :
148+
178149 ``` jshelllanguage
179150 ConseqServiceFactory.instance();
180151 ```
181152
182- The concurrency can be customized:
153+ The concurrency can be customized:
154+
183155 ``` jshelllanguage
184156 ConseqServiceFactory.instance(10)
185157 ```
186158
187159### Style 2: submit each task directly for execution, together with its sequence key
188160
189- #### API
161+ - API
190162
191163``` java
192- public interface SequentialExecutor extends Terminable {
164+ public interface SequentialExecutor {
193165 /**
194166 * @param command
195167 * the Runnable task to run sequentially with others under the same sequence key
@@ -216,33 +188,32 @@ This API style is more concise. Bypassing the JDK ExecutorService API, it servic
216188execution semantics holds: Tasks of the same sequence key are executed in the same submission order; tasks of different
217189sequence keys are managed to execute in parallel.
218190
219- Prefer this style when the full-blown syntax and semantic support of
191+ For versions requiring Java 21+, conseq4j Style 2 defaults to have no preset limit on the overall concurrency when
192+ executing tasks; for other versions, this style's default concurrency is the number of JVM run-time'
193+ s [ availableProcessors] ( https://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html#availableProcessors-- ) . Prefer
194+ this style when the full-blown syntax and semantic support of
220195JDK [ ExecutorService] ( https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html ) is not
221196required.
222197
223- #### Sample usage
198+ - Sample usage
224199
225200``` java
226201public class MessageConsumer {
227202
228203 /**
229- * Default executor concurrency is java.lang.Runtime.availableProcessors.
230- * <p >
231- * Or to provide a custom concurrency of 10, for example:
232- * <code >
233- * private SequentialExecutor conseqExecutor = ConseqExecutor.instance(10));
234- * </code>
204+ * Default executor has no preset limit on overall concurrency, running on virtual thread per task.
235205 */
236206 private final SequentialExecutor conseqExecutor = ConseqExectuor . instance();
237207
238- @Autowired private ShoppingEventProcessor shoppingEventProcessor;
208+ @Autowired
209+ private ShoppingEventProcessor shoppingEventProcessor;
239210
240211 /**
241- * Suppose run-time invocation of this method is managed by the messaging provider. This is usually via a single
212+ * Suppose run-time invocation of this method is managed by the messaging provider. This is usually via a single
242213 * caller thread.
243214 * <p >
244- * Concurrency is achieved when shopping events of different shopping cart IDs are processed in parallel by
245- * different backing threads. Sequence is maintained for all shopping events of the same shopping cart ID, via
215+ * Concurrency is achieved when shopping events of different shopping cart IDs are processed in parallel by
216+ * different backing threads. Sequence is maintained for all shopping events of the same shopping cart ID, via
246217 * linear progression of execution stages with {@link java.util.concurrent.CompletableFuture}.
247218 */
248219 public void onMessage (Message shoppingEvent ) {
@@ -251,32 +222,39 @@ public class MessageConsumer {
251222}
252223```
253224
254- - The interface of this direct-execute style uses ` Future ` as the return type, mainly to reduce conceptual weight of the
255- API. The implementation actually returns ` CompletableFuture ` , and can be used/cast as such if need be.
256- - The implementation relies on
257- JDK's [ CompletableFuture] ( https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html ) to
258- achieve sequential execution of related tasks. One single pool of threads is used to facilitate the overall
259- asynchronous execution. The concurrency to execute unrelated tasks is generally limited only by the backing work
260- thread pool's capacity.
261- - Instead of thread-affinity or bucket hashing, tasks are decoupled from their execution threads. All pooled threads are
262- anonymous and interchangeable to execute any tasks. Even sequential tasks of the same sequence key may be executed by
263- different threads, albeit in sequential order. When the work thread pool has idle threads available, a task awaiting
264- execution must have been blocked only by its own related task(s) of the same sequence key - as is necessary, and not
265- by unrelated tasks of different sequence keys in the same "bucket" - as is unnecessary. This can be a desired
266- advantage over the thread-affinity API style, at the trade-off of lesser syntax and semantic richness than the
267- JDK [ ExecutorService] ( https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html ) .
268- - The default general concurrency (i.e. the execution work thread pool capacity is the JVM
269- run-time's [ availableProcessors] ( https://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html#availableProcessors-- )
225+ The implementation relies on
226+ JDK's [ CompletableFuture] ( https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html ) to
227+ achieve sequential execution of related tasks. One single pool of threads is used to facilitate the overall
228+ asynchronous execution. The concurrency to execute unrelated tasks is generally limited only by the backing work
229+ thread pool's capacity, or unlimited in the case of default/virtual thread mode.
230+
231+ Instead of thread-affinity or bucket hashing, tasks are decoupled from their execution threads. All pooled threads are
232+ anonymous and interchangeable to execute any tasks. Even sequential tasks of the same sequence key may be executed by
233+ different threads, albeit in sequential order. When the work thread pool has idle threads available, a task awaiting
234+ execution must have been blocked only by its own related task(s) of the same sequence key - as is necessary, and not
235+ by unrelated tasks of different sequence keys in the same "bucket" - as is unnecessary. This can be a desired
236+ advantage over the thread-affinity API style, at the trade-off of lesser syntax and semantic richness than the
237+ JDK [ ExecutorService] ( https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ExecutorService.html ) .
238+
239+ For versions requiring Java 21+, the default ConseqExecutor instance uses Virtual thread per task, and has no preset
240+ limit on overall concurrency. For other versions, the default concurrency is the number of JVM's available processors.
241+
270242 ``` jshelllanguage
271243 ConseqExecutor.instance()
272244 ```
273- The concurrency can be customized:
245+
246+ The concurrency/parallelism can be customized to use a Platform thread ` ForkJoinPool ` of the specified capacity:
247+
274248 ``` jshelllanguage
275249 ConseqExecutor.instance(10)
276250 ```
277- - The ` ConseqExecutor ` instance can also be powered by a fully-customized ` ExecutorService ` :
278251
279- ` ConseqExecutor.instance(ExecutorService workerExecutorService) `
252+ The ` ConseqExecutor ` instance can also use a fully-customized (Virtual or Platform thread-based) ` ExecutorService ` to
253+ power its async operations, e.g. with a fixed sized platform-thread pool:
254+
255+ ``` jshelllanguage
256+ ConseqExecutor.instance(Executors.newFixedThreadPool(10))
257+ ```
280258
281259## Full disclosure - Asynchronous Conundrum
282260
0 commit comments