Skip to content

Commit 9cf462f

Browse files
committed
Improve ThreadUtil APIs. Add image IO and Processing thread pools.
1 parent 28442be commit 9cf462f

File tree

11 files changed

+234
-54
lines changed

11 files changed

+234
-54
lines changed

weasis-acquire/weasis-acquire-explorer/src/main/java/org/weasis/acquire/explorer/gui/control/AcquirePublishPanel.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public class AcquirePublishPanel extends JPanel {
6161
private AuthMethod authMethod;
6262

6363
public static final ExecutorService PUBLISH_DICOM =
64-
ThreadUtil.buildNewSingleThreadExecutor("Publish Dicom"); // NON-NLS
64+
ThreadUtil.newSingleThreadExecutor("AcquirePublishDicom");
6565

6666
public AcquirePublishPanel() {
6767
publishBtn.addActionListener(

weasis-acquire/weasis-acquire-explorer/src/main/java/org/weasis/acquire/explorer/gui/control/ImportPanel.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
public class ImportPanel extends JPanel {
3636

3737
public static final ExecutorService IMPORT_IMAGES =
38-
ThreadUtil.buildNewSingleThreadExecutor("ImportImage");
38+
ThreadUtil.newSingleThreadExecutor("ImportImage");
3939

4040
private final JButton importBtn = new JButton(Messages.getString("ImportPanel.import"));
4141
private final SpinnerProgress progressBar = new SpinnerProgress();

weasis-acquire/weasis-acquire-explorer/src/main/java/org/weasis/acquire/explorer/gui/dialog/AcquirePublishDialog.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,9 @@ private void publishAction(File exportDir) {
335335
}
336336

337337
SwingWorker<File, AcquireMediaInfo> dicomizeTask = setupPublishingTask(toPublish, exportDir);
338-
ThreadUtil.buildNewSingleThreadExecutor("Dicomize").execute(dicomizeTask); // NON-NLS
338+
try (var executor = ThreadUtil.newSingleThreadExecutor("AcquireDicomize")) {
339+
executor.execute(dicomizeTask);
340+
}
339341
}
340342

341343
private SwingWorker<File, AcquireMediaInfo> setupPublishingTask(

weasis-base/weasis-base-explorer/src/main/java/org/weasis/base/explorer/JIThumbnailCache.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,7 @@ public final class JIThumbnailCache {
4040
// ImageElement)
4141
private final ExecutorService qExecutor =
4242
new ThreadPoolExecutor(
43-
1,
44-
1,
45-
0L,
46-
TimeUnit.MILLISECONDS,
47-
queue,
48-
ThreadUtil.getThreadFactory("Thumbnail Cache")); // NON-NLS
43+
1, 1, 0L, TimeUnit.MILLISECONDS, queue, ThreadUtil.namedThreadFactory("ThumbnailCache"));
4944

5045
private final Map<URI, ThumbnailIcon> cachedThumbnails =
5146
Collections.synchronizedMap(

weasis-base/weasis-base-explorer/src/main/java/org/weasis/base/explorer/list/AThumbnailListPane.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ protected AThumbnailListPane(ThumbnailList<E> thumbList) {
3535
thumbList.asComponent(),
3636
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
3737
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
38-
this.pool = ThreadUtil.buildNewSingleThreadExecutor("Thumbnail List"); // NON-NLS
38+
this.pool = ThreadUtil.newSingleThreadExecutor("AcquireThumbnailList");
3939

4040
this.thumbnailList = thumbList;
4141
this.thumbnailList.addListSelectionListener(new JIListSelectionAdapter());

weasis-core/src/main/java/org/weasis/core/api/media/data/Thumbnail.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public class Thumbnail extends JLabel implements Thumbnailable {
5757
AppProperties.buildAccessibleTempDirectory(
5858
AppProperties.FILE_CACHE_DIR.getName(), "thumb"); // NON-NLS
5959
public static final ExecutorService THUMB_LOADER =
60-
ThreadUtil.buildNewSingleThreadExecutor("Thumbnail Loader"); // NON-NLS
60+
ThreadUtil.newSingleThreadExecutor("ThumbnailLoader");
6161

6262
public static final String KEY_SIZE = "explorer.thumbnail.size";
6363
public static final int MIN_SIZE = 48;

weasis-core/src/main/java/org/weasis/core/api/util/ThreadUtil.java

Lines changed: 222 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,60 +9,245 @@
99
*/
1010
package org.weasis.core.api.util;
1111

12-
import java.util.concurrent.ExecutorService;
13-
import java.util.concurrent.Executors;
14-
import java.util.concurrent.ThreadFactory;
12+
import java.util.Objects;
13+
import java.util.concurrent.*;
14+
import java.util.concurrent.atomic.AtomicInteger;
1515

16-
public class ThreadUtil {
16+
/** Utility class for creating thread pools and thread factories with custom naming. */
17+
public final class ThreadUtil {
1718

18-
private ThreadUtil() {
19-
super();
20-
}
19+
private ThreadUtil() {}
2120

2221
/**
23-
* Creates an Executor that uses a single worker thread operating off an unbounded queue, and uses
24-
* the provided ThreadFactory to create a new thread when needed. Unlike the otherwise equivalent
25-
* {@code newFixedThreadPool(1, threadFactory)} the returned executor is guaranteed not to be
26-
* reconfigurable to use additional threads.
22+
* Creates a single-threaded executor with named threads.
2723
*
28-
* @param name the name of the new thread
29-
* @return the newly created single-threaded Executor
30-
* @throws NullPointerException if threadFactory is null
24+
* @param name the name prefix for the thread
25+
* @return a new single-threaded executor
26+
* @throws NullPointerException if name is null
3127
*/
32-
public static ExecutorService buildNewSingleThreadExecutor(final String name) {
33-
return Executors.newSingleThreadExecutor(getThreadFactory(name));
28+
public static ExecutorService newSingleThreadExecutor(String name) {
29+
return Executors.newSingleThreadExecutor(namedThreadFactory(name));
3430
}
3531

3632
/**
37-
* Creates a thread pool that reuses a fixed number of threads operating off a shared unbounded
38-
* queue, using the provided ThreadFactory to create new threads when needed. At any point, at
39-
* most {@code nThreads} threads will be active processing tasks. If additional tasks are
40-
* submitted when all threads are active, they will wait in the queue until a thread is available.
41-
* If any thread terminates due to a failure during execution prior to shutdown, a new one will
42-
* take its place if needed to execute subsequent tasks. The threads in the pool will exist until
43-
* it is explicitly {@link ExecutorService#shutdown shutdown}.
33+
* Creates a fixed thread pool with named threads.
4434
*
4535
* @param nThreads the number of threads in the pool
46-
* @param name the name of the new thread
47-
* @return the newly created thread pool
48-
* @throws NullPointerException if threadFactory is null
49-
* @throws IllegalArgumentException if {@code nThreads <= 0}
36+
* @param name the name prefix for threads
37+
* @return a new fixed thread pool
38+
* @throws IllegalArgumentException if nThreads <= 0
39+
* @throws NullPointerException if name is null
5040
*/
51-
public static ExecutorService buildNewFixedThreadExecutor(int nThreads, final String name) {
52-
return Executors.newFixedThreadPool(nThreads, getThreadFactory(name));
41+
public static ExecutorService newFixedThreadPool(int nThreads, String name) {
42+
return Executors.newFixedThreadPool(nThreads, namedThreadFactory(name));
5343
}
5444

5545
/**
56-
* Based on the default thread factory
46+
* Creates a cached thread pool with named threads.
5747
*
58-
* @param name the name prefix of the new thread
59-
* @return the factory to use when creating new threads
48+
* @param name the name prefix for threads
49+
* @return a new cached thread pool
50+
* @throws NullPointerException if name is null
6051
*/
61-
public static ThreadFactory getThreadFactory(String name) {
62-
return r -> {
63-
Thread t = Executors.defaultThreadFactory().newThread(r);
64-
t.setName(name + "-" + t.getName());
65-
return t;
52+
public static ExecutorService newCachedThreadPool(String name) {
53+
return Executors.newCachedThreadPool(namedThreadFactory(name));
54+
}
55+
56+
/**
57+
* Creates an IO-optimized bounded thread pool for image loading operations. Pool size and queue
58+
* capacity are calculated based on system resources.
59+
*
60+
* @param name the name prefix for threads
61+
* @return a new IO-optimized thread pool
62+
* @throws NullPointerException if name is null
63+
*/
64+
public static ExecutorService newImageIOThreadPool(String name) {
65+
var config = calculateIOPoolConfig();
66+
67+
return new ThreadPoolExecutor(
68+
config.poolSize(),
69+
config.poolSize(),
70+
30L,
71+
TimeUnit.SECONDS,
72+
new LinkedBlockingQueue<>(config.queueCapacity()),
73+
namedDaemonThreadFactory(name, true),
74+
new ThreadPoolExecutor.CallerRunsPolicy());
75+
}
76+
77+
/**
78+
* Creates a bounded thread pool optimized for CPU-intensive image processing. Pool size and queue
79+
* capacity are calculated based on system resources.
80+
*
81+
* @param name the name prefix for threads
82+
* @return a new image processing thread pool
83+
* @throws NullPointerException if name is null
84+
*/
85+
public static ExecutorService newImageProcessingThreadPool(String name) {
86+
var config = calculateProcessingPoolConfig();
87+
88+
return new ThreadPoolExecutor(
89+
config.poolSize(),
90+
config.poolSize(),
91+
30L,
92+
TimeUnit.SECONDS,
93+
new LinkedBlockingQueue<>(config.queueCapacity()),
94+
namedDaemonThreadFactory(name, true),
95+
new ThreadPoolExecutor.CallerRunsPolicy());
96+
}
97+
98+
// Calculates optimal IO pool configuration based on system resources
99+
private static PoolConfig calculateIOPoolConfig() {
100+
int cores = Runtime.getRuntime().availableProcessors();
101+
long totalMemoryMB = Runtime.getRuntime().maxMemory() / (1024 * 1024);
102+
103+
// IO operations benefit from more threads due to blocking nature
104+
// Scale with cores but cap based on memory to avoid resource exhaustion
105+
int poolSize = Math.max(2, Math.min(calculateMaxIOThreads(totalMemoryMB), cores * 2));
106+
107+
// Queue capacity scales with available memory and pool size
108+
// Each queued task represents potential memory usage
109+
int queueCapacity = calculateIOQueueCapacity(totalMemoryMB, poolSize);
110+
111+
return new PoolConfig(poolSize, queueCapacity);
112+
}
113+
114+
// Calculates optimal processing pool configuration based on system resources
115+
private static PoolConfig calculateProcessingPoolConfig() {
116+
int cores = Runtime.getRuntime().availableProcessors();
117+
long totalMemoryMB = Runtime.getRuntime().maxMemory() / (1024 * 1024);
118+
119+
// CPU-intensive tasks: optimal pool size close to core count
120+
// Add one extra thread to handle coordination and avoid starvation
121+
int poolSize = Math.max(1, Math.min(cores + 1, calculateMaxProcessingThreads(totalMemoryMB)));
122+
123+
// Processing tasks typically require more memory per task
124+
// Conservative queue to prevent memory pressure
125+
int queueCapacity = calculateProcessingQueueCapacity(totalMemoryMB, poolSize);
126+
127+
return new PoolConfig(poolSize, queueCapacity);
128+
}
129+
130+
// Calculate maximum IO threads based on available memory
131+
private static int calculateMaxIOThreads(long totalMemoryMB) {
132+
// Rough estimation: each IO thread might use ~100MB for buffers and caching
133+
return Math.max(2, Math.min(16, (int) (totalMemoryMB / 100)));
134+
}
135+
136+
// Calculate maximum processing threads based on available memory
137+
private static int calculateMaxProcessingThreads(long totalMemoryMB) {
138+
// Processing threads need more memory for image data (~300MB per thread)
139+
return Math.max(1, Math.min(12, (int) (totalMemoryMB / 300)));
140+
}
141+
142+
// Calculate IO queue capacity based on memory and pool size
143+
private static int calculateIOQueueCapacity(long totalMemoryMB, int poolSize) {
144+
// Base capacity scales with memory, but consider pool size for balance
145+
int baseCapacity = (int) Math.min(128, totalMemoryMB / 32);
146+
// Ensure reasonable ratio between queue and pool (4:1 to 8:1)
147+
return Math.max(poolSize * 4, Math.min(poolSize * 8, baseCapacity));
148+
}
149+
150+
// Calculate processing queue capacity based on memory and pool size
151+
private static int calculateProcessingQueueCapacity(long totalMemoryMB, int poolSize) {
152+
// More conservative for processing tasks due to higher memory usage
153+
int baseCapacity = (int) Math.min(64, totalMemoryMB / 64);
154+
// Lower ratio for processing tasks (2:1 to 4:1)
155+
return Math.max(poolSize * 2, Math.min(poolSize * 4, baseCapacity));
156+
}
157+
158+
// Configuration record for pool parameters
159+
private record PoolConfig(int poolSize, int queueCapacity) {}
160+
161+
/**
162+
* Creates a thread factory that names threads with the given prefix.
163+
*
164+
* @param name the name prefix for threads
165+
* @return a thread factory with custom naming
166+
* @throws NullPointerException if name is null
167+
*/
168+
public static ThreadFactory namedThreadFactory(String name) {
169+
return namedDaemonThreadFactory(name, false);
170+
}
171+
172+
/**
173+
* Creates a thread factory that names threads with the given prefix and daemon status.
174+
*
175+
* @param name the name prefix for threads
176+
* @param daemon whether threads should be daemon threads
177+
* @return a thread factory with custom naming and daemon status
178+
* @throws NullPointerException if name is null
179+
*/
180+
public static ThreadFactory namedDaemonThreadFactory(String name, boolean daemon) {
181+
Objects.requireNonNull(name, "Thread name cannot be null");
182+
var threadCounter = new AtomicInteger(1);
183+
return runnable -> {
184+
var thread = Executors.defaultThreadFactory().newThread(runnable);
185+
thread.setName(name + "-" + threadCounter.getAndIncrement());
186+
thread.setDaemon(daemon);
187+
return thread;
66188
};
67189
}
190+
191+
/**
192+
* Creates a single-threaded executor with daemon threads.
193+
*
194+
* @param name the name prefix for the thread
195+
* @return a new single-threaded executor with daemon threads
196+
*/
197+
public static ExecutorService newSingleThreadDaemonExecutor(String name) {
198+
return Executors.newSingleThreadExecutor(namedDaemonThreadFactory(name, true));
199+
}
200+
201+
/**
202+
* Creates a fixed thread pool with daemon threads.
203+
*
204+
* @param nThreads the number of threads in the pool
205+
* @param name the name prefix for threads
206+
* @return a new fixed thread pool with daemon threads
207+
*/
208+
public static ExecutorService newFixedDaemonThreadPool(int nThreads, String name) {
209+
return Executors.newFixedThreadPool(nThreads, namedDaemonThreadFactory(name, true));
210+
}
211+
212+
// Creates a shutdown hook for graceful executor shutdown
213+
private static Thread createShutdownHook(
214+
ExecutorService executor, String name, long timeoutSeconds) {
215+
return new Thread(
216+
() -> {
217+
executor.shutdown();
218+
try {
219+
if (!executor.awaitTermination(timeoutSeconds, TimeUnit.SECONDS)) {
220+
executor.shutdownNow();
221+
}
222+
} catch (InterruptedException e) {
223+
Thread.currentThread().interrupt();
224+
executor.shutdownNow();
225+
}
226+
},
227+
name + "-shutdown");
228+
}
229+
230+
/**
231+
* Creates an IO thread pool with automatic shutdown hook registration.
232+
*
233+
* @param name the name prefix for threads
234+
* @return a new IO thread pool with shutdown hook
235+
*/
236+
public static ExecutorService newManagedImageIOThreadPool(String name) {
237+
var executor = newImageIOThreadPool(name);
238+
Runtime.getRuntime().addShutdownHook(createShutdownHook(executor, name, 5));
239+
return executor;
240+
}
241+
242+
/**
243+
* Creates an image processing thread pool with automatic shutdown hook registration.
244+
*
245+
* @param name the name prefix for threads
246+
* @return a new processing thread pool with shutdown hook
247+
*/
248+
public static ExecutorService newManagedImageProcessingThreadPool(String name) {
249+
var executor = newImageProcessingThreadPool(name);
250+
Runtime.getRuntime().addShutdownHook(createShutdownHook(executor, name, 10));
251+
return executor;
252+
}
68253
}

weasis-dicom/weasis-dicom-explorer/src/main/java/org/weasis/dicom/explorer/DicomModel.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public class DicomModel implements TreeModel, DataExplorerModel {
133133
TagW.SubseriesInstanceUID,
134134
new TagView(TagD.getTagFromIDs(Tag.SeriesDescription, Tag.SeriesNumber, Tag.SeriesTime)));
135135
public static final ExecutorService LOADING_EXECUTOR =
136-
ThreadUtil.buildNewSingleThreadExecutor("Dicom Model"); // NON-NLS
136+
ThreadUtil.newSingleThreadExecutor("DicomModelLoader");
137137

138138
private static final List<TreeModelNode> modelStructure =
139139
Arrays.asList(TreeModelNode.ROOT, patient, study, series);

weasis-dicom/weasis-dicom-explorer/src/main/java/org/weasis/dicom/explorer/ExportDicomView.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ public class ExportDicomView extends AbstractItemDialogPage implements ExportDic
4747
protected final File exportDir;
4848
protected final DicomModel dicomModel;
4949
protected final ExportTree exportTree;
50-
protected final ExecutorService executor =
51-
ThreadUtil.buildNewFixedThreadExecutor(3, "Dicom Send task"); // NON-NLS
50+
protected final ExecutorService executor = ThreadUtil.newFixedThreadPool(3, "DicomSend");
5251

5352
public ExportDicomView(
5453
String title, int position, DicomModel dicomModel, CheckTreeModel treeModel) {

weasis-dicom/weasis-dicom-explorer/src/main/java/org/weasis/dicom/explorer/wado/DownloadManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public class DownloadManager {
121121
0L,
122122
TimeUnit.MILLISECONDS,
123123
PRIORITY_QUEUE,
124-
ThreadUtil.getThreadFactory("Series Downloader")); // NON-NLS
124+
ThreadUtil.namedThreadFactory("SeriesDownloader"));
125125

126126
public static class PriorityTaskComparator implements Comparator<Runnable> {
127127

0 commit comments

Comments
 (0)