Fundamentals 17 min read

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.

IT Services Circle
IT Services Circle
IT Services Circle
Nine Common Pitfalls of Using Multithreading in Java Applications

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 order

4. 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.

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.

Javaconcurrencythread safetymultithreading
IT Services Circle
Written by

IT Services Circle

Delivering cutting-edge internet insights and practical learning resources. We're a passionate and principled IT media platform.

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.