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