Nine Common Pitfalls of Using Multithreading in Java Applications
Switching from single‑threaded synchronous code to multithreaded asynchronous execution can improve performance, but introduces nine major issues—including missing return values, data loss, ordering problems, thread‑safety, ThreadLocal anomalies, OOM risks, high CPU usage, transaction failures, and service crashes—each explained with Java examples and solutions.
1. Unable to Get Return Value
When a thread is created by extending Thread or implementing Runnable, the method executed in the thread cannot directly return a value. If the business logic requires the result, you must use Callable with Future (pre‑Java 8) or CompletableFuture (Java 8+).
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();
return userInfo;
}Remember to use a thread pool (e.g., executor) to avoid creating too many native threads.
2. Data Loss
When part of a request is split into asynchronous tasks (e.g., configuring user navigation or sending notifications), failures in those tasks can be lost if the main thread has already returned success. Solutions include using MQ for asynchronous processing or a job scheduler that retries failed tasks.
3. Order Problem
Multithreading can change the execution order of operations, leading to inconsistent results. To enforce order you can use Thread.join(), a single‑thread executor, or synchronization utilities like CountDownLatch.
3.1 join
public static void main(String[] args) throws InterruptedException {
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
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
CountDownLatch latch1 = new CountDownLatch(0);
CountDownLatch latch2 = new CountDownLatch(1);
CountDownLatch latch3 = new CountDownLatch(1);
// threads use latch.await() and latch.countDown() to enforce a‑b‑c order4. Thread‑Safety Issue
Adding results from concurrent tasks into a non‑thread‑safe ArrayList can cause missing data. Replace it with CopyOnWriteArrayList or use proper synchronization.
List<User> list = Lists.newCopyOnWriteArrayList();5. ThreadLocal Data Exception
Standard ThreadLocal values are not propagated to threads obtained from a pool. InheritableThreadLocal works only when a new thread is created. For thread‑pool reuse, use Alibaba's TransmittableThreadLocal together with TtlExecutors.getTtlExecutorService.
TransmittableThreadLocal<Integer> threadLocal = new TransmittableThreadLocal<>();
threadLocal.set(6);
ExecutorService ttlExec = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
ttlExec.submit(() -> System.out.println("First: " + threadLocal.get()));
threadLocal.set(7);
ttlExec.submit(() -> System.out.println("Second: " + threadLocal.get()));6. OOM Problem
Creating too many native threads (each ~1 MB) or using a fixed‑size thread pool with an unbounded queue can exhaust memory, leading to OutOfMemoryError: unable to create new native thread. Tune pool size and queue capacity.
7. CPU Usage Spike
Heavy multithreaded processing (e.g., bulk Excel import) can keep CPUs busy. Introducing short sleeps (e.g., Thread.sleep(10)) between tasks can alleviate the spike, though the root cause may also be tight loops, frequent GC, or intensive regex.
8. Transaction Issue
Spring transactions are bound to the thread’s database connection. If a transactional method spawns a new thread, the child thread gets a different connection, breaking the transaction. Do not start new threads inside a @Transactional method; instead, use asynchronous execution that propagates the transaction context.
9. Service Crash
Scaling a consumer to many threads can overwhelm downstream services, causing crashes. Always evaluate the target service’s capacity before increasing concurrency, and consider back‑pressure mechanisms or rate limiting.
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.
IT Services Circle
Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.
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.
