OpenJDK 11中ConcurrentHashMap putVal方法高命中次数原因及底层流程解析请求
ConcurrentHashMap.putVal() Get Called 129+ Times for Just One put()? Great question—let’s unpack this, because ConcurrentHashMap’s internals are already complex, and debugging tools add their own hidden behavior that can throw off your counts.
First: The Normal put() → putVal() Flow (Simplified)
First, let’s clear up the baseline: your manual put() calls shouldn’t be triggering hundreds of putVal() invocations on their own. Here’s the stripped-down version of what happens when you call map.put(key, value):
- The public
put()method is just a thin wrapper that callsputVal(key, value, false)(thefalsemeans we allow overwriting existing values). - Step 1: Hash Calculation: Compute the hash of the key to find its position in the underlying
tablearray. - Step 2: Lazy Initialization: If the
tablehasn’t been created yet (firstput()), initialize it using CAS operations to avoid locks. - Step 3: Bucket Check:
- If the target bucket is empty, use CAS to insert the new node directly—done!
- If the bucket’s head is a
ForwardingNode(signaling an ongoing resize), help with the resize and retry theput(this might trigger an extraputVal()call, but only once or twice perput).
- Step 4: Handle Collisions:
- If the bucket uses a linked list, traverse it to find the key: replace the value if found, or add a new node to the end (locking the bucket briefly with
synchronized). - If the bucket is a red-black tree, insert/update the node using tree logic (again, locking the bucket).
- If the bucket uses a linked list, traverse it to find the key: replace the value if found, or add a new node to the end (locking the bucket briefly with
- Step 5: Resize Check: If the bucket’s size hits the threshold, convert the list to a tree or trigger a full resize.
Under normal circumstances, one put() will trigger 1-3 putVal() calls at most (only extra calls if resizing requires retries).
So Why Are You Seeing 129+ Calls?
The massive number of putVal() hits you’re seeing is almost entirely due to IntelliJ’s debugger itself, not your code. Here’s why:
- The debugger runs an agent (the
debugger-agent.jarin your launch command) inside your JVM process. This agent does a lot of background work to track variables, display data in the Variables window, and collect performance metrics for the Overhead tab. - This agent likely uses
ConcurrentHashMapinternally for its own bookkeeping—every time it puts data into its own maps, that triggersputVal()calls that get counted in your method stats. - Additionally, when the debugger displays the state of your
map2instance (e.g., showing entries in the Variables window), it may trigger internalConcurrentHashMapmethods that indirectly lead to extraputVal()invocations (though this is less likely than the agent’s own usage). - Even if you only call
put()once, the debugger’s internal activity is responsible for the other 128+putVal()hits. The difference between 129 and 131 calls is just the two extraput()s in your original code—those add 2 real calls on top of the debugger’s noise.
How to See the Real Call Count
If you want to verify the actual number of putVal() calls from your code:
- Run the program without debugging (use the "Run" button instead of "Debug").
- Add manual logging or use a lightweight profiling tool (like JDK’s built-in
jstackor minimal-overhead profilers) to track method invocations, avoiding IntelliJ’s heavy debugging agent.
内容的提问来源于stack exchange,提问作者ravibeli

