<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gaurav Tyagi</title>
    <description>The latest articles on DEV Community by Gaurav Tyagi (@gaurav_tyagi_4d2a33837e04).</description>
    <link>https://dev.to/gaurav_tyagi_4d2a33837e04</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2345212%2Fc8de9e9b-c599-443a-88fb-c94966ae03ea.jpg</url>
      <title>DEV Community: Gaurav Tyagi</title>
      <link>https://dev.to/gaurav_tyagi_4d2a33837e04</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gaurav_tyagi_4d2a33837e04"/>
    <language>en</language>
    <item>
      <title>I Benchmarked CAS vs. a Lock — and Caught Myself Drawing the Wrong Conclusion Halfway Through</title>
      <dc:creator>Gaurav Tyagi</dc:creator>
      <pubDate>Wed, 23 Sep 2026 10:18:25 +0000</pubDate>
      <link>https://dev.to/gaurav_tyagi_4d2a33837e04/i-benchmarked-cas-vs-a-lock-and-caught-myself-drawing-the-wrong-conclusion-halfway-through-304a</link>
      <guid>https://dev.to/gaurav_tyagi_4d2a33837e04/i-benchmarked-cas-vs-a-lock-and-caught-myself-drawing-the-wrong-conclusion-halfway-through-304a</guid>
      <description>&lt;p&gt;"Lock-free beats locks" is close to something of a folk wisdom in Java concurrency circles. Wanting a concrete answer, I implemented both a hand-rolled compare-and-swap (CAS) retry loop and a plain &lt;code&gt;synchronized&lt;/code&gt; counter in a Java project — benchmarked the two against each other under JMH with a 1/2/4/6/8 thread sweep on 4-core hardware. The first result I'd gotten almost led me to a precisely the sort of over generalization that made me want to run the test in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup: two counters, same shape
&lt;/h2&gt;

&lt;p&gt;Both of the benchmarked implementations increment and return in a single atomic call, implemented deliberately via hand-rolled code rather than directly via &lt;code&gt;AtomicInteger.incrementAndGet()&lt;/code&gt;, to expose the retry mechanism:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CASCounter — a hand-rolled CAS retry loop&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;incrementAndGet&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compareAndSet&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// LockCounter — mutual exclusion via a monitor&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;synchronized&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;incrementAndGet&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;++;&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The annotated (&lt;code&gt;@State(Scope.Benchmark)&lt;/code&gt;) instances of these classes are shared between every thread in a given benchmark method, not per-thread — &lt;code&gt;Scope.Thread&lt;/code&gt; would give each thread a distinct instance and make for effectively zero contention, as far as this benchmark is concerned.&lt;/p&gt;

&lt;h2&gt;
  
  
  A JMH trap that was hiding in the annotations
&lt;/h2&gt;

&lt;p&gt;Each benchmark method loops 1,000,000 &lt;code&gt;incrementAndGet()&lt;/code&gt; calls, and both classes thus have &lt;code&gt;@OperationsPerInvocation(1_000_000)&lt;/code&gt;. Here's what that annotation actually buys you, worked out in numbers rather than just asserted.&lt;/p&gt;

&lt;p&gt;Without it, JMH's &lt;code&gt;Mode.Throughput&lt;/code&gt; reports how many times the annotated &lt;code&gt;@Benchmark&lt;/code&gt; method &lt;em&gt;itself&lt;/em&gt; completes per second — not how many &lt;code&gt;incrementAndGet()&lt;/code&gt; calls happen per second. Take the real, correctly-measured CAS number at 1 thread: ~120.1M increments/sec. Each call to &lt;code&gt;call1()&lt;/code&gt; performs 1,000,000 of those increments before returning, so the method itself only completes about 120,100,000 ÷ 1,000,000 = &lt;strong&gt;120.1 times per second&lt;/strong&gt;. Strip the annotation, and that's the number JMH would report as "ops/s" — 120.1, not 120,100,000.&lt;/p&gt;

&lt;p&gt;Now suppose that omission happened on only one side of the comparison — &lt;code&gt;CASCounter&lt;/code&gt;'s annotation missing, &lt;code&gt;LockCounter&lt;/code&gt;'s intact. JMH would report CAS at "120.1 ops/s" (loop completions) against Lock's correctly-annotated "57,200,000 ops/s" (actual per-increment rate). Read at face value, that makes Lock look roughly 57,200,000 ÷ 120.1 ≈ &lt;strong&gt;476,000x faster than CAS&lt;/strong&gt; — the exact opposite of reality, where CAS is actually ~2.1x faster at 1 thread. One missing annotation on one of two classes doesn't just blur the numbers, it can flip the conclusion entirely.&lt;/p&gt;

&lt;p&gt;Even a &lt;em&gt;symmetric&lt;/em&gt; mistake — both classes missing the annotation — wouldn't break the CAS-vs-Lock comparison (both numbers get divided by the same 1,000,000, so the ratio between them survives), but the absolute figures would become meaningless on their own: "120.1 ops/s" reads like a counter incrementing about 120 times a second, six orders of magnitude slower than what's actually happening. That's the specific failure mode the JMH methodology primer warns about — not vague noise, but a benchmark that reports a wrong number with total confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  First look — and the conclusion I almost drew
&lt;/h2&gt;

&lt;p&gt;The first thing I ran benchmarked was &lt;code&gt;CASCounter&lt;/code&gt; on its own, for the aggregate throughput of the &lt;code&gt;threadCount&lt;/code&gt;-thread harness:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threads&lt;/th&gt;
&lt;th&gt;Throughput (relative)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;129.6 (baseline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;14.5 (not ~2x, ~9x worse)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;4.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;4.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Increasing from 1 to 2 threads didn't roughly double the aggregated throughput of the program — it collapsed by ~9x. That would be an intuitively plausible opportunity for someone to conclude "well, obviously a lock would've done better here", having not actually run the other benchmark. Reading a conclusion about &lt;code&gt;LockCounter&lt;/code&gt; out of results for &lt;code&gt;CASCounter&lt;/code&gt; would be no different from concluding that "this program would be faster if it had a different set of inputs". The fact that &lt;code&gt;CASCounter&lt;/code&gt; simply collapses under contention tells us nothing about it by itself, before running the other harness.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real head-to-head
&lt;/h2&gt;

&lt;p&gt;Running both counters through the same harness, normalized again to per-increment throughput (99.9% confidence intervals don't overlap on any thread count — this crossover is real):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threads&lt;/th&gt;
&lt;th&gt;CAS (ops/s)&lt;/th&gt;
&lt;th&gt;Lock (ops/s)&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;~120.1 M&lt;/td&gt;
&lt;td&gt;~57.2 M&lt;/td&gt;
&lt;td&gt;CAS, ~2.1×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;~14.6 M&lt;/td&gt;
&lt;td&gt;~31.2 M&lt;/td&gt;
&lt;td&gt;Lock, ~2.1×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;~6.9 M&lt;/td&gt;
&lt;td&gt;~16.6 M&lt;/td&gt;
&lt;td&gt;Lock, ~2.4×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;~5.6 M&lt;/td&gt;
&lt;td&gt;~11.8 M&lt;/td&gt;
&lt;td&gt;Lock, ~2.1×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;~5.7 M&lt;/td&gt;
&lt;td&gt;~10.2 M&lt;/td&gt;
&lt;td&gt;Lock, ~1.8×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CAS wins uncontended by roughly an order of magnitude, but lock contention negates that advantage almost entirely and hands the victory to the lock by a similar margin. The uncontested CAS has a bare-metal &lt;code&gt;CAS&lt;/code&gt; that has succeeded on the first try and thus is cheaper than even an uncontended &lt;code&gt;synchronized&lt;/code&gt; block. Once there's any contention at all, the CAS's lack of backoff mechanism makes it much slower than the lock — the &lt;code&gt;compareAndSet&lt;/code&gt; will always keep spinning in a tight loop without any pauses, whereas a thread blocked on a &lt;code&gt;synchronized&lt;/code&gt; it doesn't hold waits entirely outside the coherence protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the crossover happens: spin vs. park
&lt;/h2&gt;

&lt;p&gt;Concrete trace of what happens when both Thread A and Thread B read the same value and CAS() fails, due to the cache line being invalidated by A's CAS() having succeeded:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
participant A as Thread A
participant Mem as counter (cache line)
participant B as Thread B

Mem--&amp;gt;&amp;gt;A: read counter = 5
Mem--&amp;gt;&amp;gt;B: read counter = 5
A-&amp;gt;&amp;gt;Mem: CAS(5, 6) — succeeds
Note over Mem: cache line invalidated on every other core
B-&amp;gt;&amp;gt;Mem: CAS(5, 6) — fails (live value is 6)
B-&amp;gt;&amp;gt;Mem: re-read counter = 6
B-&amp;gt;&amp;gt;Mem: CAS(6, 7) — succeeds&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;&lt;code&gt;CASCounter.incrementAndGet()&lt;/code&gt; contains no backoff mechanism, and the moment a &lt;code&gt;compareAndSet&lt;/code&gt; fails, will immediately proceed to re-read counter value and retry as many times as necessary in a spinloop until it manages to CAS() successfully. A thread that iblocked on a &lt;code&gt;synchronized&lt;/code&gt; monitor it cannot acquire will instead enter park() and completely exit the cache-coherence picture until it manages to acquire the lock again.&lt;/p&gt;

&lt;h2&gt;
  
  
  The generalization — and it isn't "use locks"
&lt;/h2&gt;

&lt;p&gt;The accurate generalization is a narrower, more specific one — a CAS retry loop in the absence of backoff mechanism loses to locks under contention. The resolution is an exponentially-increasing backoff with jitter, to prevent threads that collided from restarting at the same point in the spinloop and colliding again — but at this operation's nanosecond timescale, &lt;code&gt;Thread.sleep()&lt;/code&gt; is the wrong level of granularity (nanoseconds vs. milliseconds) to have such a coarse-grained mechanism as &lt;code&gt;sleep()&lt;/code&gt;. For smaller backoffs, &lt;code&gt;Thread.onSpinWait()&lt;/code&gt; is appropriate (a CPU instruction on x86 telling the processor that this spinloop is waiting for an external event to happen and thus should occasionally yield to other threads), and eventually &lt;code&gt;LockSupport.parkNanos()&lt;/code&gt; is more appropriate than &lt;code&gt;sleep()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is precisely the reason &lt;code&gt;LongAdder&lt;/code&gt; exists and is able to outperform a single &lt;code&gt;AtomicLong&lt;/code&gt; under contention: it does not pick a different mutual-exclusion primitive to handle the hot spot, but stripes the counter in such a way that threads collide less often in the first place. The same technique also powers the Disruptor's multi-producer case (though not the high-performance single-producer one): each producer operates on a cell of the array in turn, and each thread only collides with any other thread if they pick the same cell.&lt;/p&gt;

&lt;p&gt;In fact, neither &lt;code&gt;LongAdder&lt;/code&gt; nor &lt;code&gt;Disruptor&lt;/code&gt; eliminates a shared touchpoint altogether, only reducing the frequency with which threads collide on it. The mechanisms described apply whenever threads do collide in spite of such measures and need to compete for access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;A benchmark's first result may not be a comparison. &lt;code&gt;CASCounter&lt;/code&gt;'s preliminary result, where increasing the number of threads from 1 to 2 caused a ~9x drop in throughput, did not say anything about &lt;code&gt;LockCounter&lt;/code&gt;'s behavior until both were actually benchmarked against each other. Once they had, the actual result turned out to be closer to a crossover than an outright rout, and the reason for this difference pointed directly to the means of resolution — backoff, and not a switch in primitives, was the appropriate action in this case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gist.github.com/Tyagi-Gaurav/367765f8ed1cda8d038df1750dcc750e" rel="noopener noreferrer"&gt;Full source: &lt;code&gt;CASCounter.java&lt;/code&gt; / &lt;code&gt;LockCounter.java&lt;/code&gt; — GitHub Gist&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.baeldung.com/java-longadder-and-longaccumulator" rel="noopener noreferrer"&gt;LongAdder and LongAccumulator in Java — Baeldung&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="http://psy-lob-saw.blogspot.com/2014/06/jdk8-update-on-scalable-counters.html" rel="noopener noreferrer"&gt;Advice for the concurrently confused: AtomicLong JDK7/8 vs. LongAdder — psy-lob-saw&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://coffeebeforearch.github.io/2020/11/07/spinlocks-5.html" rel="noopener noreferrer"&gt;Spinlocks — Part 5 (exponential backoff) — CoffeeBeforeArch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>performance</category>
      <category>jmh</category>
    </item>
    <item>
      <title>The hashCode()-to-Array-Index Bug Almost Everyone Writes Once</title>
      <dc:creator>Gaurav Tyagi</dc:creator>
      <pubDate>Sun, 06 Sep 2026 20:57:37 +0000</pubDate>
      <link>https://dev.to/gaurav_tyagi_4d2a33837e04/the-hashcode-to-array-index-bug-almost-everyone-writes-once-4leg</link>
      <guid>https://dev.to/gaurav_tyagi_4d2a33837e04/the-hashcode-to-array-index-bug-almost-everyone-writes-once-4leg</guid>
      <description>&lt;p&gt;&lt;code&gt;"hash % capacity"&lt;/code&gt; looks trivial until you trace it by hand. Two separate bugs hide in that one line — and the fix for one of them has its own hole.&lt;/p&gt;




&lt;p&gt;Building a HashMap from scratch, the "turn a key into a bucket index" line looks like it should be the easy part:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hashCode&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I traced this by hand before writing a single line of &lt;code&gt;MyHashMap&lt;/code&gt;, and it broke immediately — for a reason that has nothing to do with hashing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug one: &lt;code&gt;%&lt;/code&gt; isn't modulo
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;hashCode()&lt;/code&gt; returns a signed 32-bit &lt;code&gt;int&lt;/code&gt; — anything from &lt;code&gt;Integer.MIN_VALUE&lt;/code&gt; to &lt;code&gt;Integer.MAX_VALUE&lt;/code&gt;, including negative numbers. Trace &lt;code&gt;-7 % 16&lt;/code&gt; in Java by hand before reading the next sentence.&lt;/p&gt;

&lt;p&gt;Java's &lt;code&gt;%&lt;/code&gt; is a &lt;em&gt;remainder&lt;/em&gt; operator, not mathematical modulo — the result takes the sign of the &lt;em&gt;dividend&lt;/em&gt;, not the divisor. &lt;code&gt;-7 % 16&lt;/code&gt; is &lt;code&gt;-7&lt;/code&gt;, not &lt;code&gt;9&lt;/code&gt;. Feed that straight into &lt;code&gt;array[bucket]&lt;/code&gt; and you get an &lt;code&gt;ArrayIndexOutOfBoundsException&lt;/code&gt;, not a wrong-but-harmless bucket.&lt;/p&gt;

&lt;h3&gt;
  
  
  The naive fix has its own hole
&lt;/h3&gt;

&lt;p&gt;First instinct: wrap it in &lt;code&gt;Math.abs()&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;abs&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hashCode&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// looks fixed. isn't, always.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works for almost every input — and then silently breaks on exactly one value. &lt;code&gt;Integer.MIN_VALUE&lt;/code&gt; (&lt;code&gt;-2147483648&lt;/code&gt;) has no positive counterpart in 32-bit two's complement — the range is &lt;code&gt;-2147483648&lt;/code&gt; to &lt;code&gt;2147483647&lt;/code&gt;, one more negative number than positive. &lt;code&gt;Math.abs(Integer.MIN_VALUE)&lt;/code&gt; returns &lt;code&gt;Integer.MIN_VALUE&lt;/code&gt; itself, still negative. It's documented behavior, not a JVM bug — and it means &lt;code&gt;Math.abs()&lt;/code&gt; doesn't actually guarantee a non-negative result for &lt;em&gt;every&lt;/em&gt; &lt;code&gt;int&lt;/code&gt;, which defeats the entire point of reaching for it here.&lt;/p&gt;

&lt;h3&gt;
  
  
  The actual fix
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Math.floorMod(hash, capacity)&lt;/code&gt;, added in Java 8, does true floored modulo — always non-negative for a positive divisor, no edge case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;getBucket&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;K&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;hashCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hashCode&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fnvHash&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hashCode&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;floorMod&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Bug two (a design choice, really): bitmask vs. modulo
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;java.util.HashMap&lt;/code&gt; doesn't use modulo at all — it uses &lt;code&gt;hash &amp;amp; (capacity - 1)&lt;/code&gt;, a bitmask. That only works correctly when capacity is a power of two: &lt;code&gt;16 - 1 = 15 = 0b01111&lt;/code&gt;, a clean run of 1-bits that extracts exactly the low 4 bits regardless of the input's sign. Try it with a non-power-of-two capacity and the mask stops behaving like a clean modulo at all. In exchange for that constraint, bitmask AND is measurably faster than a division-based modulo — division/remainder is one of the more expensive basic ALU operations, and benchmarks commonly show bitmask coming out several times faster at scale.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MyHashMap&lt;/code&gt; deliberately went the other way: &lt;code&gt;Math.floorMod&lt;/code&gt; instead of a bitmask, trading that speedup for &lt;strong&gt;not&lt;/strong&gt; forcing capacity to always be a power of two. Worth being honest about which one you're picking and why, rather than defaulting to whichever the JDK does without registering there's a choice at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug three: fixing the sign doesn't fix distribution
&lt;/h2&gt;

&lt;p&gt;Even with a correct, always-non-negative index, there's a separate problem hiding underneath. Bitmask (and, to a lesser extent, modulo) only meaningfully uses the &lt;em&gt;low&lt;/em&gt; bits of the hash. If a class's &lt;code&gt;hashCode()&lt;/code&gt; implementation happens to vary mostly in its &lt;em&gt;high&lt;/em&gt; bits — this is common, not hypothetical (float-derived hashes and sequential object-identity hashes are classic examples) — a small table can collide those distinct hashCodes into the same few buckets, even though the hashCodes themselves are all different.&lt;/p&gt;

&lt;p&gt;This is why &lt;code&gt;java.util.HashMap&lt;/code&gt; doesn't use a key's raw &lt;code&gt;hashCode()&lt;/code&gt; — it runs it through &lt;code&gt;h ^ (h &amp;gt;&amp;gt;&amp;gt; 16)&lt;/code&gt; first: shift the top 16 bits down, XOR them into the bottom 16. Cheap — one shift, one XOR — and it's enough to fold high-bit entropy into the bits that actually get used for indexing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;MyHashMap&lt;/code&gt; goes further: a full FNV-1a mix over all four bytes of the hashCode, not just a single XOR-fold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;fnvHash&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;hashCode&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;STARTING_SEED&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 0x811C9DC5, the FNV offset basis&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;hashCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mh"&gt;0x01000193&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// FNV prime, 32-bit&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a genuine cost/benefit tradeoff, not a strict improvement: four iterations of shift/XOR/multiply per hash call is meaningfully more work than the JDK's single shift-and-XOR. Whether that's worth it depends on how confident you are in your keys' &lt;code&gt;hashCode()&lt;/code&gt; distribution and how hot the code path is — the JDK's choice is "cheapest fix that's good enough for general-purpose use," not "best possible mixing."&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;One line, three separate bugs, none of them about hashing itself: sign handling (and a naive fix for it that has its own edge case), a speed/flexibility tradeoff in how you reduce to a valid range, and a distribution problem that survives fixing the first two. Trace &lt;code&gt;-7 % 16&lt;/code&gt; by hand before you trust any "obviously correct" indexing line again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gist.github.com/Tyagi-Gaurav/a2346b359a60720646dfa44ef12d5b70" rel="noopener noreferrer"&gt;Full Source Code Gist&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://torstencurdt.com/tech/modulo-of-negative-numbers/" rel="noopener noreferrer"&gt;Modulo of Negative Numbers — Torsten Curdt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.javathinking.com/blog/best-way-to-make-java-s-modulus-behave-like-it-should-with-negative-numbers/" rel="noopener noreferrer"&gt;Best Way to Fix Java's Modulus with Negative Numbers — javathinking.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://0equalsto1.medium.com/the-power-of-the-power-of-two-c5720e240411" rel="noopener noreferrer"&gt;The Power of the "Power of Two" — Medium&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://algs4.cs.princeton.edu/34hash/" rel="noopener noreferrer"&gt;Algorithms, 4th ed. — Hash Tables (Sedgewick &amp;amp; Wayne)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>datastructures</category>
      <category>computerscience</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Built My Own Fail-Fast HashMap — Here's Why a Boolean Flag Wasn't Enough</title>
      <dc:creator>Gaurav Tyagi</dc:creator>
      <pubDate>Sun, 30 Aug 2026 20:45:30 +0000</pubDate>
      <link>https://dev.to/gaurav_tyagi_4d2a33837e04/i-built-my-own-fail-fast-hashmap-heres-why-a-boolean-flag-wasnt-enough-5aa9</link>
      <guid>https://dev.to/gaurav_tyagi_4d2a33837e04/i-built-my-own-fail-fast-hashmap-heres-why-a-boolean-flag-wasnt-enough-5aa9</guid>
      <description>&lt;p&gt;If you've done LeetCode's &lt;a href="https://leetcode.com/problems/design-hashmap/" rel="noopener noreferrer"&gt;Design HashMap&lt;/a&gt;, you've implemented &lt;code&gt;put&lt;/code&gt;, &lt;code&gt;get&lt;/code&gt;, and &lt;code&gt;remove&lt;/code&gt;. What that exercise usually skips is the part that actually breaks in production: what happens when someone mutates the map &lt;em&gt;while&lt;/em&gt; another piece of code is iterating over it.&lt;/p&gt;

&lt;p&gt;I ran into this directly while building &lt;code&gt;MyHashMap&lt;/code&gt;, a from-scratch single-threaded HashMap (separate chaining, resize on load factor). Getting &lt;code&gt;put&lt;/code&gt;/&lt;code&gt;get&lt;/code&gt;/&lt;code&gt;remove&lt;/code&gt; right was the easy 80%. Getting &lt;code&gt;entrySet().iterator()&lt;/code&gt; to correctly detect concurrent mutation — including the case where a &lt;em&gt;second, completely separate&lt;/em&gt; iterator is the one that should notice — took three wrong turns before landing on the pattern the JDK actually uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem, concretely
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;Iterator&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Entry&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;K&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="no"&gt;V&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;entrySet&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;iterator&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;someNewKey&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;someValue&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// structural change, mid-iteration&lt;/span&gt;
&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// ??? — undefined behavior if we don't guard against this&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without a guard, &lt;code&gt;next()&lt;/code&gt; might return a stale entry, skip entries entirely, or throw an unrelated exception depending on internal bucket-array state. Java's real collections handle this with &lt;code&gt;ConcurrentModificationException&lt;/code&gt; (CME) — but the interesting part isn't the exception, it's the mechanism that detects when to throw it.&lt;/p&gt;

&lt;h2&gt;
  
  
  First idea: a boolean "dirty" flag
&lt;/h2&gt;

&lt;p&gt;Obvious first attempt: a &lt;code&gt;boolean modified&lt;/code&gt; field on the map, flipped to &lt;code&gt;true&lt;/code&gt; on any &lt;code&gt;put&lt;/code&gt;/&lt;code&gt;remove&lt;/code&gt;, checked by the iterator. This works for exactly one iterator. It falls apart the moment two iterators are alive at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Iterator A calls &lt;code&gt;next()&lt;/code&gt;, sees &lt;code&gt;modified == false&lt;/code&gt;, proceeds.&lt;/li&gt;
&lt;li&gt;Something else mutates the map. &lt;code&gt;modified&lt;/code&gt; flips to &lt;code&gt;true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Iterator B — created &lt;em&gt;after&lt;/em&gt; that mutation — checks the &lt;em&gt;same shared&lt;/em&gt; &lt;code&gt;modified&lt;/code&gt; flag, sees &lt;code&gt;true&lt;/code&gt;, and incorrectly throws, even though nothing has changed since B was created.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single shared boolean can't represent "changed since &lt;em&gt;this specific iterator&lt;/em&gt; was created" for more than one iterator at a time. Resetting it on read doesn't help either — now the &lt;em&gt;other&lt;/em&gt; iterator stops seeing the change it legitimately needed to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Second idea: a timestamp
&lt;/h2&gt;

&lt;p&gt;Next instinct: give the map a &lt;code&gt;lastModified&lt;/code&gt; timestamp instead, and have each iterator capture the current time on creation. Compare timestamps instead of a shared flag — now each iterator has its own baseline.&lt;/p&gt;

&lt;p&gt;This closes the multi-iterator gap, but introduces a different bug: &lt;strong&gt;resolution&lt;/strong&gt;. Two mutations in a tight loop, or a mutation immediately followed by iterator creation, can land in the same millisecond (&lt;code&gt;System.currentTimeMillis()&lt;/code&gt;) — or even the same tick of &lt;code&gt;System.nanoTime()&lt;/code&gt; on some platforms. If a real modification and an iterator's baseline capture ever produce the &lt;em&gt;same&lt;/em&gt; timestamp value, the comparison can't tell who happened first. Worse, &lt;code&gt;currentTimeMillis()&lt;/code&gt; isn't even guaranteed monotonic — it can jump backward on a clock adjustment.&lt;/p&gt;

&lt;p&gt;The deeper issue: this isn't a timing problem. It's a "did &lt;em&gt;anything&lt;/em&gt; change since I looked" problem, and wall-clock time is the wrong tool for a question that has nothing to do with elapsed time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually works: a monotonic counter (modCount)
&lt;/h2&gt;

&lt;p&gt;Every mutation increments a plain &lt;code&gt;long&lt;/code&gt; counter. Every iterator, at creation, snapshots the counter's current value. Every &lt;code&gt;next()&lt;/code&gt; call compares its snapshot against the live value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyEntryIterator&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Iterator&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Entry&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;K&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;V&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;expectedVersion&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;MyEntryIterator&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expectedVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// snapshot at creation&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Entry&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;K&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;V&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expectedVersion&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ConcurrentModificationException&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;// ... advance and return&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No shared mutable flag, no clock. Each iterator carries its &lt;em&gt;own&lt;/em&gt; independent baseline (&lt;code&gt;expectedVersion&lt;/code&gt;), so the multi-iterator case that broke the boolean flag now works automatically — iterator B's snapshot is whatever the counter was when &lt;em&gt;B&lt;/em&gt; was created, completely independent of A's. And since it's a plain increment, not a physical measurement, there's zero resolution/collision risk: every mutation gets a value strictly different from every other, no matter how fast they happen. This is exactly the pattern &lt;code&gt;java.util.HashMap&lt;/code&gt;, &lt;code&gt;ArrayList&lt;/code&gt;, and friends use internally — go read &lt;code&gt;AbstractList&lt;/code&gt;'s &lt;code&gt;modCount&lt;/code&gt; field and the &lt;code&gt;ConcurrentModificationException&lt;/code&gt; javadoc directly; it's short.&lt;/p&gt;

&lt;h2&gt;
  
  
  The subtlety that actually caught me: no "free" first call
&lt;/h2&gt;

&lt;p&gt;Here's the case that exposed a real bug in my own test suite, not just the implementation. Two iterators, A and B, created back to back — &lt;em&gt;before&lt;/em&gt; either has called &lt;code&gt;next()&lt;/code&gt; even once:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant Map
    participant IteratorA
    participant IteratorB

    Note over Map: version = 3 (after 3 puts)
    IteratorA-&amp;gt;&amp;gt;Map: create (snapshot version=3)
    IteratorB-&amp;gt;&amp;gt;Map: create (snapshot version=3)

    IteratorA-&amp;gt;&amp;gt;Map: next()
    IteratorA-&amp;gt;&amp;gt;Map: remove() → version = 4

    IteratorB-&amp;gt;&amp;gt;Map: next()
    Map--&amp;gt;&amp;gt;IteratorB: ConcurrentModificationException
    Note over IteratorB: B never called next() before A's&amp;lt;br/&amp;gt;change — still invalidated on its&amp;lt;br/&amp;gt;very first call&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;I originally wrote a test that let iterator B succeed on its first &lt;code&gt;next()&lt;/code&gt; call before asserting a &lt;em&gt;second&lt;/em&gt; call would throw. That's wrong: B's snapshot predates A's &lt;code&gt;remove()&lt;/code&gt;, so B is already stale the instant A mutates — there's no "one free call" grace period. The check only cares whether a modification happened since the snapshot was taken, not whether the iterator has navigated yet. Fixing that test (not the implementation) was the actual bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The self-invalidation trap
&lt;/h2&gt;

&lt;p&gt;One more sharp edge: an iterator's &lt;em&gt;own&lt;/em&gt; &lt;code&gt;remove()&lt;/code&gt; legitimately bumps the shared counter — so if the iterator doesn't resync its own &lt;code&gt;expectedVersion&lt;/code&gt; immediately afterward, it trips its own check on the very next call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;nextCalled&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;IllegalStateException&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="nc"&gt;MyEntry&lt;/span&gt; &lt;span class="n"&gt;entryToDelete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;currentEntry&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;MyHashMap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entryToDelete&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getKey&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expectedVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// resync — or self-CME on the next call&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nextCalled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One more nuance most write-ups skip: not every write is "structural"
&lt;/h2&gt;

&lt;p&gt;The counter shouldn't bump on &lt;em&gt;every&lt;/em&gt; &lt;code&gt;put()&lt;/code&gt; — only on ones that actually change the map's shape. Overwriting the value of an already-present key isn't structural (same key, same position, same size) and doesn't need to invalidate a live iterator; inserting a genuinely new key is, and does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MyEntry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// new key: structural&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// walk the chain...&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matchFound&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setValue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// existing key, value-only: NOT structural, no bump&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matches &lt;code&gt;java.util.HashMap&lt;/code&gt;'s real behavior, and it's the kind of detail that only shows up once you've actually built the thing rather than read about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Fail-fast iteration looks like a one-line trick (&lt;code&gt;modCount&lt;/code&gt;) until you actually have to defend it against a second iterator, a same-millisecond race, or your own iterator's legal mutation. Building it from scratch — and writing tests that actually exercise the multi-iterator case — surfaced three separate designs before landing on the one that's actually in the JDK, plus a bug in the &lt;em&gt;test&lt;/em&gt; for the final design, not the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.baeldung.com/java-fail-safe-vs-fail-fast-iterator" rel="noopener noreferrer"&gt;Fail-Safe Iterator vs Fail-Fast Iterator — Baeldung&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developers.google.com/j2objc/javadoc/jre/reference/java/util/ConcurrentModificationException" rel="noopener noreferrer"&gt;&lt;code&gt;ConcurrentModificationException&lt;/code&gt; javadoc&lt;/a&gt; — the "best-effort, not a guarantee" language is worth reading verbatim&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mtumilowicz/java-iterator-fail-types" rel="noopener noreferrer"&gt;java-iterator-fail-types — GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>datastructures</category>
      <category>computerscience</category>
    </item>
  </channel>
</rss>
