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.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
9 Hidden Pitfalls When Converting Synchronous Code to Multithreaded Execution

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 7

6. 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.
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavatransactionconcurrencyThreadPoolmultithreadingThreadLocal
Su San Talks Tech
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.