Skip to main content

🧠 Java Production Trap I’ll Never Forget – Let’s Talk About ConcurrentModificationException

 

πŸ’¬ “When I was a fresher, I used to wonder: what things should I never do in Java?”

That curiosity led me to explore tons of weird bugs and war stories people had faced in production. Some were scary, some silly — but all of them were super valuable to understand deeply.

Today, I’m sharing one such issue I came across.

⚠️ P.S. I didn’t write this code — but I sure learned a lot digging into what went wrong and how it was fixed. πŸ˜…

This post is for the curious devs out there — especially freshers who want to learn the "why" behind Java's behaviors, not just memorize "what to do".


πŸ’₯ The Problem: ConcurrentModificationException in a Simple Loop

πŸ“Environment:
This happened in a Spring Boot microservice deployed on OpenShift (Linux-based). Interestingly, it didn’t throw any error in the developer's local Windows machine during testing. But in prod? BOOM. πŸ’£

Here’s the actual buggy code snippet:


List<String> users = getActiveUsers(); // Fetch from DB for (String user : users) { if (user.startsWith("temp_")) { users.remove(user); // 🚨 This caused the exception } }

And here’s the ugly surprise from the logs:


Exception in thread "main" java.util.ConcurrentModificationException at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1042)

❓My Doubt: “Why ‘Concurrent’ if I'm Not Using Threads?”

That word "Concurrent" was super misleading to me back then.
I wasn’t using any threads! It was just a simple for-loop and a list.

So I started digging:
What does this error really mean? When and why does it happen?


πŸ”¬ Root Cause Analysis (RCA)

Here’s the twist: even though it looks like you’re looping normally, Java’s for-each loop actually uses an Iterator under the hood.

Let’s break it down:

  • Java’s ArrayList maintains an internal mod count (modCount) to track structural changes.

  • When you iterate using for (item : list), it’s essentially doing this:


Iterator<String> it = users.iterator(); while (it.hasNext()) { String user = it.next(); // Your logic here }
  • Now, when you do users.remove(user), you're modifying the list directly, while the iterator is still running.

  • The iterator notices that the list changed without using its own remove() method — and throws ConcurrentModificationException as a fail-fast mechanism.


πŸ› ️ The Fix: Remove Safely Without Angering the Iterator Gods πŸ˜…

✅ Approach 1: Use Iterator Properly


List<String> users = getActiveUsers(); Iterator<String> it = users.iterator(); while (it.hasNext()) { String user = it.next(); if (user.startsWith("temp_")) { it.remove(); // ✅ Safe removal } }

✅ Approach 2: Use Java Streams to Filter


List<String> users = getActiveUsers() .stream() .filter(u -> !u.startsWith("temp_")) .collect(Collectors.toList());

🧠 Why Does This Work?

Using it.remove() is safe because the iterator knows about the removal and updates its internal state. It’s like you politely told the iterator:

“Hey, I’m removing this one — please adjust your internal counter accordingly.” ☕

In the stream version, you’re not modifying the original list at all — you’re creating a new filtered list, which avoids the mutation problem entirely.



"Whaaatttt, then what does iterator.remove() do internally, and why does that work while list.remove() doesn’t?"


πŸ”¬ What Happens Internally When iterator.remove() is Called?

Time to go inside Java’s ArrayList and Iterator πŸš€

πŸ“Œ Internals of ArrayList.iterator()

Java’s ArrayList uses an internal class called Itr as its iterator.

Let’s see its key variables:


int cursor; // index of next element to return int lastRet = -1; // index of last element returned, -1 if none int expectedModCount = modCount; // used for CME detection

πŸ“Œ What .remove() does:

When you call iterator.remove(), it internally calls the parent list’s remove method, but also updates internal state safely.


public void remove() { if (lastRet < 0) throw new IllegalStateException(); ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; // ✅ sync back the modCount }

πŸ’‘ So it does 3 crucial things:

  1. Removes element at correct position (lastRet)

  2. Moves the cursor properly so next iteration continues smoothly

  3. Updates expectedModCount to match modCount so that CME is NOT thrown


🧠 So Why list.remove() (outside iterator) fails?

Let’s say:


List<String> list = new ArrayList<>(List.of("A", "B", "C")); Iterator<String> it = list.iterator();
  1. Internally:

    • modCount = 0

    • expectedModCount = 0

  2. When you call list.remove(1) directly:

    • modCount++ → modCount = 1

    • But expectedModCount in the iterator stays 0

  3. On next it.next(), it checks:


if (modCount != expectedModCount) throw new ConcurrentModificationException();

πŸ’₯ BOOM! That's your exception.



🧊 Final Analogy Time

Think of iterator like a security guard:

  • If you try to remove someone from the party (direct list.remove), the guard freaks out 😱 — CME!

  • But if the guard (iterator.remove) removes them himself, all is cool 😎



😡 Why Didn't It Fail in Local?

This is a classic gotcha.

  • Small dataset in local? Iterator might never throw.

  • JVM behavior or optimizations on your machine might not enforce the check immediately.

  • But in production (large dataset + different JVM or OS), the fail-fast behavior gets enforced reliably.

That’s why the same code works locally but crashes in prod — the worst kind of bug. πŸ˜–


🧩 What I Learned

  • ConcurrentModificationException is not about threads — it’s about modifying collections during iteration in an unsafe way.

  • Always respect the Iterator pattern if you’re doing removals inside a loop.

  • And never assume something is safe just because it runs locally!


🎨 Wrapping Up

I didn’t write this buggy code — but man, I’m glad I found it while exploring “what not to do” in Java. It helped me understand how iteration really works under the hood.

If you're just starting your Java journey, I highly recommend doing this too:

Don’t just learn “how” to fix something — learn why it broke in the first place.

It’ll make you 10x stronger as a dev. πŸš€


πŸ”š Next Weird Bug coming soon!


Got questions or thoughts? Drop them in the comments or connect with me — I love geeking out over stuff like this. πŸ˜„

Comments

Popular posts from this blog

🐱 Tomcat vs ⚡ Netty – Which One Should You Use?

🐱 Tomcat vs ⚡ Netty – Which One Should You Use? So recently I got curious about this too πŸ€”. Everywhere in Spring Boot tutorials we see Tomcat . Then suddenly while exploring Spring WebFlux , the name Netty pops up. And I was like – “Wait, who’s this Netty guy trying to replace Tomcat?” πŸ˜… Let’s break it down with real-time examples , icons , and fun comparisons . 🐱 Tomcat – The Traditional Web Server Type: Servlet Container (blocking I/O) World: Used with Spring MVC Style: Thread-per-request model πŸ‘©‍πŸ’» Pros: Stable, widely used, battle-tested Cons: Struggles with huge concurrent connections πŸ‘‰ Example in real life: Tomcat is like a restaurant with fixed waiters 🍴. - Each customer = one thread/waiter - If too many customers come in at once → waiters run out → customers wait outside πŸšͺ ⚡ Netty – The Reactive Rockstar Type: Asynchronous Event-Driven Network Framework World: Default for Spring WebFlux Style: Event-lo...

🎭 Spring’s Secret: Why @Transactional & Friends Betray You Silently

πŸ’‘ Lesson Learned — Not a Prod Bug, But a Real Pain No, this wasn’t a production outage. Nobody screamed at me. But I sat for 3 hours wondering: “Why the heck is my @Transactional not rolling back!?” 😡‍πŸ’« “Why is Redis cache not working?” 🀯 Turned out, the issue was one silent villain: 🧱 Self-invocation 🀷 What Is @Transactional ? If you're new: @Transactional = Tells Spring to start a DB transaction when a method is called. It’ll commit if everything’s okay. It’ll rollback if something fails. 🧠 Think of it like wrapping your code in: try { beginTransaction(); // your logic commit(); } catch(Exception e) { rollback(); } πŸ•΅️ Real-Life Analogy — The Gateway Community 🏘️ Let me tell you about my society — it has a strict watchman at the gate. Here’s how it works: πŸ›‚ Watchman = Spring Proxy 🏠 Your apartment = Your service class πŸšͺ Your room = A method inside that class πŸƒ Scenario 1: Outsider Visits Your friend from outside...

🧡 Virtual Threads in Java — The Ultimate Guide with Diagrams, Code & Interview Qs!

πŸš€ “How are Virtual Threads different from Thread Pools?” 😡 “Are they OS threads or JVM threads?” πŸ™ƒ “Should I still use CompletableFuture?” 🀯 “How do I even use them in real-time microservices?” 🧠 What are Virtual Threads? Virtual Threads (introduced in Java 21 as stable πŸŽ‰) are lightweight threads managed by the JVM instead of the OS kernel. πŸ‘‰ They look like normal threads, but don’t hog OS resources like traditional threads. 🧠 What is the OS Kernel? πŸ›️ OS Kernel = The Brain of the Operating System It’s the core part of your OS (Windows, Linux, Mac) that: Manages memory 🧠 Schedules threads πŸ•’ Talks to hardware πŸ’» Handles I/O operations πŸ“¨ When you create a traditional thread in Java, the JVM asks the OS Kernel to create a real OS-level thread. πŸ–Ό️ Imagine This... ┌───────────────────────────┐ │ Your Java Application │ └────────────┬──────────────┘ │ ...