9 Hidden Pitfalls When Converting Synchronous Code to Multithreaded Execution
Switching from single‑threaded synchronous calls to multithreaded asynchronous execution can boost performance, but it also introduces nine common problems—including missing return values, data loss, ordering issues, thread‑safety bugs, ThreadLocal anomalies, OOM, high CPU usage, transaction failures, and service crashes—that developers must understand and mitigate.
Introduction
To improve interface performance, many projects replace single‑threaded synchronous code with multithreaded asynchronous execution. For example, a user‑info API that must fetch basic info, points, and growth values from three different services can become much faster by calling these services concurrently and aggregating the results.
1. Cannot Obtain Return Value
If a thread is created by extending Thread or implementing Runnable, its method’s return value cannot be retrieved directly. Two scenarios exist: the caller does not need the return value, or it does. For the latter, Java 8 introduced CompletableFuture (or Callable before Java 8) to capture results.
public UserInfo getUserInfo(Long id) throws InterruptedException, ExecutionException {
final UserInfo userInfo = new UserInfo();
CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> {
getRemoteUserAndFill(id, userInfo);
return Boolean.TRUE;
}, executor);
// similar futures for bonus and growth
CompletableFuture.allOf(userFuture, bonusFuture, growthFuture).join();
userFuture.get();
bonusFuture.get();
growthFuture.get();
return userInfo;
}Remember to use a thread pool; otherwise you may create too many threads under high concurrency.
2. Data Loss
When a registration API performs critical operations (write user table, assign permissions) synchronously and non‑critical operations (configure navigation, send notifications) asynchronously, the API may return success before the asynchronous tasks finish. If those tasks fail, data can be lost. Solutions include:
Use MQ to send a message after the critical steps; a consumer processes the remaining tasks and retries on failure.
Use a scheduled job that scans a task table and retries failed entries.
3. Ordering Issues
Multithreading can change the execution order (e.g., a, b, c becomes a, c, b). To enforce order you can:
3.1 join
Calling Thread.join() makes the main thread wait for each child thread to finish before starting the next.
Thread t1 = new Thread(() -> System.out.println("a"));
Thread t2 = new Thread(() -> System.out.println("b"));
Thread t3 = new Thread(() -> System.out.println("c"));
t1.start();
t1.join();
t2.start();
t2.join();
t3.start();3.2 newSingleThreadExecutor
Submitting tasks to a single‑thread executor guarantees FIFO execution.
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("a"));
executor.submit(() -> System.out.println("b"));
executor.submit(() -> System.out.println("c"));
executor.shutdown();3.3 CountDownLatch
Use a latch to coordinate threads so that they proceed only after previous steps complete.
CountDownLatch latch1 = new CountDownLatch(0);
CountDownLatch latch2 = new CountDownLatch(1);
CountDownLatch latch3 = new CountDownLatch(1);
// threads use latch.await() and latch.countDown()Alternatively, CompletableFuture.thenRun() can enforce ordering without explicit synchronization.
4. Thread‑Safety Problems
Adding results to a shared ArrayList from multiple threads can cause missing data because ArrayList is not thread‑safe. Replace it with CopyOnWriteArrayList or synchronize access.
List<User> list = new CopyOnWriteArrayList<>();Here we use Google Guava’s Lists.newCopyOnWriteArrayList() for brevity.
5. ThreadLocal Data Anomalies
Standard ThreadLocal works only for the thread that created it. In a thread pool, reused threads do not inherit updated values. InheritableThreadLocal copies the parent value only when the thread is created, so subsequent changes are not seen.
InheritableThreadLocal<Integer> tl = new InheritableThreadLocal<>();
tl.set(6);
ExecutorService es = Executors.newSingleThreadExecutor();
// first task sees 6, second task still sees 6 even after tl.set(7)Solution: use Alibaba’s TransmittableThreadLocal together with TtlExecutors.getTtlExecutorService so that each task receives the latest value.
TransmittableThreadLocal<Integer> ttl = new TransmittableThreadLocal<>();
ttl.set(6);
ExecutorService ttlEs = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
// first task prints 6, second task prints 76. OOM Issues
Each native thread consumes about 1 MB of memory. Creating too many threads or using a fixed‑size thread pool with an unbounded queue ( LinkedBlockingQueue) can exhaust heap space and trigger OutOfMemoryError.
7. High CPU Utilization
Massive multithreaded data import (e.g., Excel) can keep CPUs busy for long periods. Introducing a short sleep (e.g., Thread.sleep(10)) between processing items can lower CPU usage.
8. Transaction Problems
Spring transactions are bound to the database connection stored in a thread‑local map. If a transactional method spawns a new thread, the child thread gets a different connection, breaking the transaction. Therefore, never start new threads inside a @Transactional method.
Otherwise, the outer transaction cannot roll back when the inner thread throws an exception.
9. Service Crash
Using a thread pool with a high maximum thread count to parallelize MQ consumer work can overload downstream services (e.g., order query API) and cause them to crash. Proper capacity planning and throttling are required.
Evaluate the maximum request rate an API can sustain before scaling the consumer’s thread pool.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
