10 Common Pitfalls in Java Concurrency and How to Avoid Them

This article outlines ten typical concurrency pitfalls in Java—including SimpleDateFormat thread‑safety, double‑checked locking flaws, volatile atomicity limits, deadlocks, lock release issues, HashMap race conditions, default thread‑pool misuse, @Async thread explosion, spin‑lock CPU waste, and ThreadLocal memory leaks—providing explanations, code examples, and practical solutions for each.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
10 Common Pitfalls in Java Concurrency and How to Avoid Them

Java developers often encounter concurrency problems that can cause incorrect results, performance degradation, or even application crashes. This article presents ten common pitfalls and offers concrete solutions.

1. SimpleDateFormat is not thread‑safe

Using a static SimpleDateFormat instance leads to race conditions when multiple threads parse dates. Solutions: create a new instance per method, use ThreadLocal, or switch to DateTimeFormatter (Java 8+).

@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);
    }
}

2. Double‑checked locking bug

The classic lazy‑singleton implementation can suffer from instruction reordering, causing multiple instances. Declaring the instance as volatile fixes the issue.

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

3. Volatile does not guarantee atomicity

While volatile ensures visibility and prevents reordering, operations like count++ remain non‑atomic. Use synchronized or atomic classes such as AtomicInteger to achieve thread‑safe increments.

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

4. Deadlock

Acquiring locks in different orders can cause a deadlock. Reduce lock scope or enforce a consistent lock acquisition order to avoid it.

public class DeadLockTest {
    public static String OBJECT_1 = "OBJECT_1";
    public static String OBJECT_2 = "OBJECT_2";
    // ... lock implementations omitted for brevity ...
}

5. Not releasing locks

When using Lock implementations, always release the lock in a finally block; otherwise the lock may never be freed.

public class LockTest {
    private final ReentrantLock rLock = new ReentrantLock();
    public void fun() {
        rLock.lock();
        try { System.out.println("fun"); }
        finally { rLock.unlock(); }
    }
}

6. HashMap in multithreaded environments

Concurrent modifications of a plain HashMap can cause infinite loops and memory overflow. Replace it with ConcurrentHashMap for thread‑safe access.

@Service
public class HashMapService {
    private Map<Long, Object> hashMap = new HashMap<>();
    public void add(User user) { hashMap.put(user.getId(), user.getName()); }
}

7. Default thread‑pool factories

Spring’s @Async and Executors utilities create unbounded thread pools, which may exhaust resources under high load. Define a custom ThreadPoolExecutor with 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

Without a configured executor, @Async falls back to creating a fresh thread per invocation, leading to OOM in high‑concurrency scenarios. Always supply a bounded executor.

9. Spin‑lock CPU waste

CAS‑based loops (e.g., in AtomicInteger) can spin heavily when contention is high. Introducing a short pause with LockSupport.parkNanos reduces CPU consumption.

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

10. ThreadLocal memory leak

Storing data in ThreadLocal without removing it after use can retain references and cause memory leaks, especially in thread‑pooled environments. Always call remove() in a finally block.

public class CurrentUser {
    private static final ThreadLocal<UserInfo> THREAD_LOCAL = new ThreadLocal<>();
    public static void set(UserInfo userInfo) { THREAD_LOCAL.set(userInfo); }
    public static UserInfo get() { return THREAD_LOCAL.get(); }
    public static void remove() { THREAD_LOCAL.remove(); }
}

By understanding these pitfalls and applying the recommended fixes, developers can write safer, more efficient concurrent Java 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.

Javaconcurrencyspringthread safetymultithreading
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.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.