10 Common Java Concurrency Pitfalls and How to Avoid Them

This article outlines ten frequent Java concurrency pitfalls—including unsafe SimpleDateFormat usage, double‑checked locking flaws, volatile limitations, deadlocks, HashMap memory leaks, thread‑pool misuse, @Async traps, spin‑lock inefficiencies, and ThreadLocal memory leaks—providing explanations, code examples, and practical solutions for each.

IT Services Circle
IT Services Circle
IT Services Circle
10 Common Java Concurrency Pitfalls and How to Avoid Them

Java developers often encounter subtle concurrency bugs that can lead to incorrect results, performance degradation, or even application crashes. The following ten pitfalls illustrate typical mistakes and how to fix them.

1. SimpleDateFormat is not thread‑safe

Using a shared SimpleDateFormat instance across threads can cause corrupted date parsing. The unsafe pattern is shown below:

@Service
public class SimpleDateFormatService {
    public Date time(String time) throws ParseException {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return dateFormat.parse(time);
    }
}

When the formatter is made static, multiple threads may modify its internal Calendar simultaneously, producing wrong timestamps. Solutions:

Declare the formatter as a local variable.

Use ThreadLocal<SimpleDateFormat>.

Switch to Java 8 DateTimeFormatter, which is immutable.

2. Double‑checked locking bug

The classic lazy‑singleton implementation suffers from instruction reordering, allowing multiple instances to be created:

public class SimpleSingleton4 {
    private static SimpleSingleton4 INSTANCE;
    private SimpleSingleton4() {}
    public static SimpleSingleton4 getInstance() {
        if (INSTANCE == null) {
            synchronized (SimpleSingleton4.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SimpleSingleton4();
                }
            }
        }
        return INSTANCE;
    }
}

Marking INSTANCE as volatile prevents reordering and guarantees visibility:

private volatile static SimpleSingleton7 INSTANCE;

3. volatile does not guarantee atomicity

Although volatile ensures visibility and prevents reordering, operations like count++ remain non‑atomic. Example:

public class VolatileTest {
    public volatile int count = 0;
    public void add() { count++; }
}

Running many threads shows the final count is often less than the expected total. Use synchronized or atomic classes (e.g., AtomicInteger) to achieve atomic updates.

4. Deadlock

Acquiring locks in opposite order creates a classic deadlock:

public class DeadLockTest {
    public static String OBJECT_1 = "OBJECT_1";
    public static String OBJECT_2 = "OBJECT_2";
    // Thread A locks OBJECT_1 then OBJECT_2
    // Thread B locks OBJECT_2 then OBJECT_1
}

Solutions include reducing lock scope and enforcing a consistent lock acquisition order.

5. Not releasing a Lock

When using java.util.concurrent.locks.Lock, forgetting to call unlock() in a finally block leaves the lock held forever:

public void fun() {
    rLock.lock();
    try {
        System.out.println("fun");
    } finally {
        rLock.unlock();
    }
}

6. HashMap can cause memory overflow

In a multithreaded environment, concurrent writes to a plain HashMap may trigger a resize loop that creates a circular linked list, eventually leading to OOM. Use ConcurrentHashMap instead.

7. Default thread‑pool factories are risky

Factory methods such as Executors.newFixedThreadPool or newCachedThreadPool can create unbounded queues or threads, causing OOM under high load. Prefer a custom ThreadPoolExecutor with bounded queue and sensible limits:

ExecutorService threadPool = new ThreadPoolExecutor(
    8, 10,
    60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(500),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

8. @Async creates a new thread each call

Spring's @Async without a configured executor falls back to AsyncExecutionAspectSupport.doExecute, which spawns a fresh thread per invocation, risking OOM in high‑concurrency scenarios. Define a dedicated Executor bean and reference it via @Async("myExecutor").

9. Spin‑lock wastes CPU

CAS loops in classes like AtomicInteger spin aggressively when contention is high. Introducing a short pause reduces CPU waste:

private boolean compareAndSwapInt2(Object o, long offset, int expected, int newVal) {
    if (compareAndSwapInt(o, offset, expected, newVal)) {
        return true;
    } else {
        LockSupport.parkNanos(10);
        return false;
    }
}

10. ThreadLocal not cleared leads to memory leaks

Storing request‑scoped data in a ThreadLocal without removing it after use can retain references indefinitely, especially in thread‑pool environments. Always clean up in a finally block:

public void doSomething(UserDto dto) {
    UserInfo info = convert(dto);
    try {
        CurrentUser.set(info);
        // business logic using CurrentUser.get()
    } finally {
        CurrentUser.remove();
    }
}

By understanding and applying these fixes, developers can write safer, more efficient Java backend code.

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.

performanceconcurrencybest practicesthread safety
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.