Threading & Executors
The Right Executor for the Job
Java 21's virtual threads are excellent for I/O-bound work but can hurt performance for CPU-intensive tasks. Brane's BraneExecutors provides properly configured executors for each use case.
import sh.brane.rpc.BraneExecutors;I/O-Bound Work
Use virtual threads for operations that spend most of their time waiting:
- RPC calls
- File I/O
- Database queries
- Network requests
Example: Parallel RPC Calls
import sh.brane.rpc.BraneExecutors;
import sh.brane.rpc.BraneProvider;
import java.util.List;
import java.util.concurrent.Future;
import java.util.ArrayList;
try (var exec = BraneExecutors.newIoBoundExecutor()) {
List<Future<?>> futures = new ArrayList<>();
// Fire 10,000 requests in parallel
for (int i = 0; i < 10_000; i++) {
futures.add(exec.submit(() ->
provider.send("eth_blockNumber", List.of())
));
}
// Wait for all to complete
for (var future : futures) {
future.get();
}
}CPU-Bound Work
Use platform threads for operations that keep the CPU busy:
- Cryptographic operations (signature verification, hashing)
- Heavy JSON/RLP parsing
- Data transformation
Example: Signature Verification
import sh.brane.rpc.BraneExecutors;
import java.util.List;
List<byte[]> signatures = getSignaturesToVerify();
try (var exec = BraneExecutors.newCpuBoundExecutor()) {
var futures = signatures.stream()
.map(sig -> exec.submit(() -> crypto.verify(sig)))
.toList();
for (var future : futures) {
boolean valid = future.get();
// Process result
}
}Custom Thread Count
By default, newCpuBoundExecutor() uses Runtime.availableProcessors() threads. Override if needed:
// Reserve 2 cores for other work
int threads = Runtime.getRuntime().availableProcessors() - 2;
try (var exec = BraneExecutors.newCpuBoundExecutor(threads)) {
// ...
}Decision Guide
| Workload | Executor | Why |
|---|---|---|
RPC calls (send, sendAsync) | newIoBoundExecutor() | Waiting on network I/O |
| File reading (keystores) | newIoBoundExecutor() | Waiting on disk I/O |
| Signature verification | newCpuBoundExecutor() | Pure computation |
| ABI encoding (large batches) | newCpuBoundExecutor() | CPU-intensive |
| Transaction building | Neither - use calling thread | Fast operation |
Thread Naming
Platform threads created by newCpuBoundExecutor() are named brane-cpu-worker-N for easy identification in thread dumps and profilers:
"brane-cpu-worker-0" #23 daemon prio=5 ...
"brane-cpu-worker-1" #24 daemon prio=5 ...All threads are daemon threads, so they won't prevent JVM shutdown.
WebSocket Subscription Executor
By default, WebSocket subscription callbacks run on virtual threads. You can customize this:
import sh.brane.rpc.WebSocketProvider;
import sh.brane.rpc.BraneExecutors;
var provider = WebSocketProvider.create("wss://...");
// Use a bounded pool for callbacks
provider.setSubscriptionExecutor(
BraneExecutors.newCpuBoundExecutor(2)
);Best Practices
-
Always use try-with-resources - Executors implement
AutoCloseableand should be properly shut down. -
Don't mix I/O and CPU work - Use separate executors for different workload types.
-
Virtual threads are the default - Unless you know you have CPU-bound work, virtual threads are the right choice.
-
Thread pools for CPU-bound work - Having more threads than cores just adds scheduling overhead for computational tasks.