GoogleTalksArchive
AI-assisted summary of Advanced Topics in Programming Languages: A Lock-Free Hash Table — original source. Check the original for full context.
Source published:
Key ideas
- Dr. Cliff Click presented NonBlockingHashMap, a lock-free concurrent hash table implemented using sun.misc.Unsafe CAS operations that eliminates lock-striping bottlenecks under heavy concurrent writes. (0:22)
- Correctness is modeled via an exhaustive slot state machine rather than traditional Java Memory Model happens-before fencing, allowing raw CPU loads on reads and local rather than machine-wide coherence overhead. (12:18)
- On massive multi-core hardware scaling up to 768 to 800 Azul CPUs, the non-blocking hash table achieved 1.2 billion read operations per second alongside 12 million concurrent updates, outperforming striped maps that bottleneck under write contention. (38:58)
- Table resizing utilizes cooperative two-phase commit migration with sentinel markers and primed value wrappers, distributing chunked copy work across active mutating threads without halting operations. (27:40)
Detailed notes
## Background This Google Tech Talk was delivered by Dr. Cliff Click on March 28, 2007, and published on YouTube by the GoogleTalksArchive channel on August 22, 2012. Dr. Click, then Chief JVM Architect at Azul Systems and previously lead architect of Sun's HotSpot Server Compiler, presents a lock-free concurrent hash table designed to scale to massive multi-core architectures. The presentation covers how the implementation achieves exceptional single- and multi-thread performance scaling up to 768 CPUs by modeling correctness through slot state machines rather than standard Java Memory Model happens-before fencing.
## TL;DR Dr. Cliff Click introduces a pure Java, lock-free hash table implementation that serves as a high-performance alternative to striped locking structures like java.util.concurrent.ConcurrentHashMap. Using open addressing with stride-1 linear reprobing and hardware compare-and-swap (CAS) via sun.misc.Unsafe, the data structure eliminates global locking and avoids read-side memory barriers entirely. Table mutations, resizing, and cooperative data migration rely on sentinel markers and primed values, allowing concurrent threads to assist with background table copying without stalling progress. Benchmarked on Azul hardware with up to 768 to 800 cores, the algorithm sustains up to 1.2 billion operations per second and scales near-linearly where traditional striped locks collapse under contention.
## Atmosphere & style The presentation carries the engaging, pragmatic energy of a master compiler engineer speaking directly to fellow systems programmers. Click delivers complex concurrency mechanisms with relaxed humor, candid anecdotes about hardware design quirks, and an intuitive, whiteboard-style breakdown of cache coherency chaos. Rather than getting bogged down in rigid academic formalism, the atmosphere feels exploratory and deeply practical, defined by Click's hard-won insights into CPU caches, memory ordering, and real-world performance tuning.
## Bulletin - [00:22](https://www.youtube.com/watch?v=HJ-719EGIts&t=22s) Dr. Cliff Click presented NonBlockingHashMap, a lock-free concurrent hash table implemented using sun.misc.Unsafe CAS operations that eliminates lock-striping bottlenecks under heavy concurrent writes. - [12:18](https://www.youtube.com/watch?v=HJ-719EGIts&t=738s) Correctness is modeled via an exhaustive slot state machine rather than traditional Java Memory Model happens-before fencing, allowing raw CPU loads on reads and local rather than machine-wide coherence overhead. - [38:58](https://www.youtube.com/watch?v=HJ-719EGIts&t=2338s) On massive multi-core hardware scaling up to 768 to 800 Azul CPUs, the non-blocking hash table achieved 1.2 billion read operations per second alongside 12 million concurrent updates, outperforming striped maps that bottleneck under write contention. - [27:40](https://www.youtube.com/watch?v=HJ-719EGIts&t=1660s) Table resizing utilizes cooperative two-phase commit migration with sentinel markers and primed value wrappers, distributing chunked copy work across active mutating threads without halting operations.
## Key takeaways - [00:22](https://www.youtube.com/watch?v=HJ-719EGIts&t=22s) ConcurrentHashMap's 16-way lock striping creates severe scalability limits as thread counts scale beyond 100 to 768 CPUs under write workloads. - [03:00](https://www.youtube.com/watch?v=HJ-719EGIts&t=180s) A strict lock-free design requires eliminating both locks and spin-locks entirely, ensuring killed threads never cause deadlocks or corrupt shared memory. - [06:29](https://www.youtube.com/watch?v=HJ-719EGIts&t=389s) Open addressing with linear reprobing and power-of-two table sizing maximizes CPU cache-line prefetching and replaces modulo instructions with bitwise masking. - [08:22](https://www.youtube.com/watch?v=HJ-719EGIts&t=502s) Read operations execute via raw CPU loads without acquiring locks, polling barriers, or entering synchronized blocks. - [10:24](https://www.youtube.com/watch?v=HJ-719EGIts&t=624s) All mutating operations—including put, putIfAbsent, and conditional remove—unify into a single execution path resolved by a hardware CAS. - [12:18](https://www.youtube.com/watch?v=HJ-719EGIts&t=738s) Proving correctness through a slot state machine demonstrates that out-of-order CPU prefetches produce benign, legal transient states without requiring fences within a table. - [17:27](https://www.youtube.com/watch?v=HJ-719EGIts&t=1047s) Unsynchronized slot updates use weakCompareAndSet to prevent hardware stalling on decoupled architectures where CAS and fences are independent. - [19:46](https://www.youtube.com/watch?v=HJ-719EGIts&t=1186s) Publishing newly allocated object references requires an explicit happens-before memory fence or volatile write to prevent exposing partially constructed objects. - [22:42](https://www.youtube.com/watch?v=HJ-719EGIts&t=1362s) Concurrent table migration relies on installing sentinel 'X' markers into old slots to close them against trailing writes while threads cooperatively copy bounded chunks. - [27:40](https://www.youtube.com/watch?v=HJ-719EGIts&t=1660s) Copied values are temporarily tagged with Prime wrappers in the new table, preventing race conditions between migrating old values and fresh concurrent puts. - [33:52](https://www.youtube.com/watch?v=HJ-719EGIts&t=2032s) Pure Java portability required wrapping primed values into heap-allocated objects rather than stealing unused pointer bits as could be done in C. - [41:53](https://www.youtube.com/watch?v=HJ-719EGIts&t=2513s) Under write contention and rolling churn workloads, lock-free throughput scales near-linearly, running 5x faster than striped maps where locks flatline. - [41:53](https://www.youtube.com/watch?v=HJ-719EGIts&t=2513s) Sizing hints are deliberately omitted because runtime capacity heuristics self-correct within seconds while preventing multi-gigabyte heap waste from bad developer guesses. - [48:54](https://www.youtube.com/watch?v=HJ-719EGIts&t=2934s) Size accounting avoids shared-counter contention by maintaining striped CAS counters across distinct cache lines, returning a point-in-time snapshot on size() queries. - [52:29](https://www.youtube.com/watch?v=HJ-719EGIts&t=3149s) Duplicate table resize allocations caused by racing threads are dropped immediately and reclaimed cheaply by young-generation garbage collection. - [53:34](https://www.youtube.com/watch?v=HJ-719EGIts&t=3214s) The migration mechanism closely mirrors a concurrent copying garbage collector using forwarding pointers to manage live object movement.
## Detailed walkthrough ### [00:22](https://www.youtube.com/watch?v=HJ-719EGIts&t=22s) Introduction and the Multi-Core Bottleneck
A Google host introduces Dr. Cliff Click, noting that he works "across the street at Azul on computers that have many, many processors" and previously served at Sun Microsystems as one of the chief architects of the HotSpot Server Compiler. Dr. Click begins with a self-deprecating joke about technology adoption, remarking that to prove he is not entirely behind the times, he recently launched a blog a week ago.
```mermaid flowchart TD accTitle: Evolution of Java Hash Table Concurrency accDescr: Progression from legacy single-lock tables to striped locking and finally lock-free implementations.
subgraph Legacy["Legacy Java"] HT["Hashtable / Collections.synchronizedMap<br/>(Single coarse lock: Total contention)"] HM["HashMap<br/>(Zero concurrency safety: Not thread-safe)"] end
subgraph ModernLocking["Striped Locking"] CHM["ConcurrentHashMap<br/>(16-way striped lock on writes; bottleneck on >16 CPUs)"] end
subgraph LockFree["Azul Lock-Free"] NBHM["NonBlockingHashMap<br/>(Pure CAS, zero locks, scales linearly to 768+ CPUs)"] end
HT --> CHM HM --> CHM CHM --> NBHM ```
Dr. Click frames his talk around two topics: hash tables and a fundamentally different conceptual model for thinking about concurrent algorithms. While a hash table is fundamentally a constant-time key-value mapping, Click highlights its broader conceptual power: “from a key-value mapping, you can build arbitrary functions out of it, and they're extendable, they're defined at runtime what the function is.” In modern enterprise computing, hash tables are predominantly used for caching—caching web content, database results, or internal application states.
These massive, highly multi-threaded enterprise systems quickly encounter severe performance cliffs due to synchronization contention. Older standard libraries provided `Hashtable` (which used a single global lock) or unsynchronized `HashMap`. While `java.util.concurrent.ConcurrentHashMap` substantially improved the state of the art by using 16-way lock striping on writes with concurrent reads, Dr. Click stresses that 16 partitions fail when hardware scales out: “it turns out that it's easy to get machines with more than 100 CPUs these days—you go to Azul, you get one with 750 CPUs—and we do have people using all those CPUs all the same time, so rapidly a 16-way striping hash table is like a major bottleneck.”
---
[03:00](https://www.youtube.com/watch?v=HJ-719EGIts&t=180s) Defining Lock-Free Progress and Hardware CAS
Dr. Click clarifies what "lock-free" means in his design. It does not simply mean omitting the Java `synchronized` keyword or avoiding `java.util.concurrent.locks.ReentrantLock`; it strictly forbids spin-locks or any primitive that grabs a shared resource and holds it exclusively:
> “No locks here means nothing that looks like a lock: not just synchronized keywords or `java.util` locking, you can't have spin-locks or anything that basically grabs onto a shared resource and holds it permanently.”
Because of this lock-free guarantee, the system possesses complete resilience against thread failure: threads can be killed mid-operation during table inserts or updates, and remaining threads continue making forward progress without deadlock or memory corruption.
At the hardware foundation, this algorithm relies on compare-and-swap (`CAS`). Surveying the audience to confirm that not everyone is intimately familiar with CAS primitives, Dr. Click defines it as the atomic building block of modern computing architectures. A CAS instruction checks a memory location against an expected value: if they match, it updates the location to a new value; if they differ, the write fails, leaves memory untouched, and signals failure to the caller. While architectures like PowerPC rely on Load-Link/Store-Conditional (LL/SC), standard CAS is native to x86, SPARC, Azul, and Itanium, and can be invoked directly in Java via `sun.misc.Unsafe`.
---
[04:59](https://www.youtube.com/watch?v=HJ-719EGIts&t=299s) Performance Characteristics Across Workloads
Lock-free algorithms frequently suffer from high constant factors because their synchronization handshakes become prohibitively complex. Click emphasizes that his design avoids this pitfall:
| Workload Mix | ConcurrentHashMap (16-way striping) | Click's Lock-Free Hash Table | |:--- |:--- |:--- | | **99% Reads / 1% Writes** | Baseline high speed (concurrent reads) | Matches CHM; ~10% faster on moderate CPU counts | | **High CPU Count (>100 to 750+ CPUs)** | Stalls on stripe contention | Continues scaling up to hundreds of cores | | **95% Reads / 5% Writes** | Write locks introduce measurable latency | Significantly pulls ahead with near-flat performance | | **Write-Heavy / 100% Mutations** | Collapses under stripe lock contention | Scales essentially linearly with minimal degradation |
Dr. Click notes that typical read-dominated caching applications (99% reads) perform comfortably on existing concurrent maps. However, as write ratios tick upward to 5% or into heavy mutation patterns, standard locking structures degrade rapidly, whereas his lock-free implementation maintains near-linear scaling even under 100% write, insert, and delete workloads.
---
[06:29](https://www.youtube.com/watch?v=HJ-719EGIts&t=389s) Hash Table Layout and Cache-Line Engineering
Moving past high-level concurrency into what he terms "uninteresting hash table details"—the mechanical engineering realities required to avoid cache penalties—Dr. Click explains his architectural choices:
* **Closed Addressing (Open Addressing) with Linear Probing:** Instead of open hashing using linked-list bucket chains (which incur pointer chasing), he uses open addressing with a stride-1 reprobe. When a collision occurs, the hardware executes a sequential scan across adjacent array elements, maximizing CPU cache-line prefetching. * **Power-of-Two Table Sizing:** Rather than using prime-sized tables with expensive modulo arithmetic, the table size is restricted to powers of two, allowing index masking via a bitwise `AND`. Click notes that integer division/modulo instructions cost as much as or more than an L1 cache miss / L2 cache hit: “if I'm going to be hitting in my L1, I'd rather not screw with a mod, I lost already; I'd rather just reprobe a few times.” * **Zero Allocation on Insertion:** Unlike conventional hash tables that instantiate a map entry node on every insertion, the table performs zero object allocations during standard `put` operations. * **Automatic Dynamic Resizing:** The table completely ignores initial capacity sizing hints supplied by user code, dynamically self-sizing and reallocating memory on the fly.
---
[08:22](https://www.youtube.com/watch?v=HJ-719EGIts&t=502s) The Core `get()` Loop Walkthrough
```mermaid flowchart TD accTitle: Lock-Free get Operation Flow accDescr: Step-by-step logic inside the get operation showing the reprobe loop, matching logic, and termination conditions.
Start(["Enter get(Object key)"]) --> Hash["Compute hash & mask to table size"] Hash --> LoadK["Load key at slot: getKey(idx)"] LoadK --> CheckNull{"Is key == null?"}
CheckNull -- "Yes" --> Miss(["Return null (Cache Miss)"]) CheckNull -- "No" --> CheckMatch{"Is key.equals(targetKey)?"}
CheckMatch -- "Yes" --> Hit["Load value: getVal(idx)"] --> RetVal(["Return value (Cache Hit)"]) CheckMatch -- "No" --> Stride["idx = (idx + 1) & mask"] --> LoadK ```
Dr. Click walks through the streamlined implementation of the table's lookup routine:
```java // Conceptual flow of the get method int hash = hash(key); int idx = hash & (table.length - 1); while (true) { Object k = getKey(idx); if (k == null) { return null; // Key missed entirely } if (k == key || k.equals(key)) { return getVal(idx); // Found key, return associated value } idx = (idx + 1) & (table.length - 1); // Stride-1 reprobe } ```
He explains that the reason the implementation achieves such blistering speeds is that `getKey(idx)` and `getVal(idx)` compile down to bare CPU load instructions without memory barriers or lock acquisitions. The true algorithmic challenge lies in preserving correctness within `getKey` and `getVal` when other threads concurrently mutate, rehash, or resize the table underneath an active lookup. At this point, Dr. Click pauses the high-level architecture walkthrough and invites questions from the audience regarding basic hash table mechanics before diving into the concurrent proof techniques.
[10:24](https://www.youtube.com/watch?v=HJ-719EGIts&t=624s) Unifying Table Mutations and Size Tracking Under a Single CAS
While Cliff Click's hash table delivers exceptionally fast read performance, he notes that making writes fast was never the original objective: “the original goal was to sustain some interesting number of puts while having a huge count of people doing gets”. Under the hood, any mutation to the table—whether an insert, modifying an existing value, a deletion, a `putIfAbsent`, or a conditional delete based on matching an old value—is handled by the exact same piece of code. At the very end of the execution path, a single compare-and-swap (CAS) instruction inserts the value into the table. Because that single CAS covers every permutation of insert, update, and remove, Click emphasizes that all these operations are semantically equivalent inside the underlying implementation.
An audience member asks about handling operations other than standard puts or inserts, as well as maintaining the size of the table. Click reiterates that because this implementation is intended as a drop-in replacement for standard Java structures like `HashMap` or `ConcurrentHashMap`, maintaining a table size is mandatory. However, concurrent size accounting cannot be done synchronously across threads without sacrificing throughput. Instead, size is maintained lazily: a single thread is guaranteed to observe its own updates immediately, while other threads observe size changes lazily as the underlying hardware catches up on disjoint writes.
- All mutations (`put`, `putIfAbsent`, updates, deletes, conditional removals) route through one core code path ending in a single CAS. - Designed as a drop-in replacement for `java.util.concurrent.ConcurrentHashMap` and `java.util.HashMap`. - Table size is lazily consistent across threads, though thread-local updates remain immediately visible to the updating thread.
---
[12:18](https://www.youtube.com/watch?v=HJ-719EGIts&t=738s) Correctness via State Machines Instead of Memory Fences
Proving the correctness of concurrent data structures is notoriously difficult. Developers typically rely on hardware memory fences or formal language-level memory models (the kind Jeremy Manson discusses) to reason about "happens-before" relationships and strict load/store orderings. Click rejects this methodology for his algorithm: “instead I'm going to prove things via a state machine that is I'm going to define a pile of states and some transitions that together makes a state machine and then I'm just going to show that all states are legal”. By validating that every possible state and transition produces a safe and legal outcome regardless of the starting point, the table completely eliminates the need to enforce memory ordering.
```mermaid flowchart TD accTitle: Single-Slot State Machine accDescr: Transitions between valid slot states for key-value pairs during inserts, updates, and deletes.
subgraph NormalLifecycle ["Standard Lifecycle"] Empty["[null, null]<br/>Initial empty slot"] KeyNull["[Key, null]<br/>Key claimed via CAS"] KeyVal["[Key, Value]<br/>Value published"] KeyTomb["[Key, Tombstone]<br/>Deleted"] end
subgraph OutOfOrderRead ["Hardware Out-of-Order Read"] NullVal["[null, Value]<br/>Reader prefetches Value before Key"] end
Empty -->|"CAS(null, Key)"| KeyNull KeyNull -->|"CAS(null, Val)"| KeyVal KeyVal -->|"CAS(Val, NewVal)"| KeyVal KeyVal -->|"CAS(Val, Tombstone)"| KeyTomb KeyTomb -->|"CAS(Tombstone, Val)"| KeyVal Empty -.->|"Hardware reorder / prefetch"| NullVal ```
Because read and write operations require no fencing constraints, `get` operations are permitted to read keys and values in any arbitrary order. Even if an optimizing compiler avoids reordering the loads or attempts prefetching, modern CPU hardware remains free to reorder them aggressively. Since reads lack strong ordering, writes do not require strong ordering either. Mutations use CAS alone to update state directly in memory. Click observes, “no fencing required for correctness and and I claim the proof as simple although maybe I've stared at it too long and so it's not fair for me to claim that.”
- A key slot starts as `null` and transitions exactly once to a non-null key; once written, a key is never altered or removed. - A value slot holds either `null`, a valid pointer to a value object, or a sentinel `Tombstone` marking the entry as deleted. - Repeated insertions and deletions alternate the value slot between valid values and the `Tombstone` sentinel. - State transitions occur exclusively on a successful CAS operation, preventing data races when multiple threads target the same slot.
---
[15:46](https://www.youtube.com/watch?v=HJ-719EGIts&t=946s) The State Machine Transitions and Out-of-Order Reads
Walking through the state machine diagram, Click highlights that the initial state is `[null, null]`. When inserting an entry, a thread uses a CAS to change the key from `null` to the target key. Winning that CAS guarantees that no race condition occurred; the winning thread owns that slot indefinitely, while the value remains temporarily `null` until a subsequent CAS installs the actual value. In-place mutations modify the value directly, deletions overwrite the value with a `Tombstone`, and re-inserting under that same key swaps the `Tombstone` back to a legitimate value.
An audience member points out an immediate drawback of immutable keys: if an application inserts items, removes them, and expects them to be garbage collected, the table will leak memory because the dead keys remain pinned in the array. Click acknowledges the point, replying that the question is about two slides ahead and that he will explain later how the table cleans itself out. He also points out a seemingly bizarre state in the machine: `[null, Value]`. Writers never write a value into a slot with a `null` key, but readers executing on out-of-order hardware can prefetch a value before loading the corresponding key. When a reader observes a `null` key alongside an arbitrary prefetched value, it simply treats the read as a cache miss:
> “I only found that state because I went and computed all possible combinations built the whole state machine and I had this weird State left over and I tried to deduce what's the meaning of the weird State and the meaning is some gets side read junk and got a Miss in the table”
- An immutable key prevents table corruption, ensuring a lookup never reads a value intended for a different key. - The `[null, Value]` state arises purely from hardware-level speculative execution or read reordering, safely resolving as a miss. - Re-inserting deleted items resurrects existing slots by replacing `Tombstone` with a new reference via CAS.
---
[17:27](https://www.youtube.com/watch?v=HJ-719EGIts&t=1047s) Relaxed Consistency, Weak CAS, and Volatile Semantics
Click clarifies that his lock-free hash table maintains no machine-wide coherent state. Threads in different cores across the machine can observe different intermediate states of key-value pairs depending on their local hardware read ordering. When concurrent threads race to update the same key with different values, the arrival order is non-deterministic, and different threads may observe conflicting values at different moments. Click notes that this behavior is completely compliant with Java's concurrency semantics and offers far more rigor than most other programming languages provide.
To illustrate, Click offers an analogy: imagine a hash table reduced down to a single key. That setup behaves identically to a single, shared global variable accessed simultaneously by many readers and writers without external synchronization. An audience member interrupts, asking whether CAS automatically guarantees `volatile` semantics. Click corrects the misconception: a standard CAS does not inherently supply volatile fences on every architecture. On Azul's custom hardware, a CAS does not execute a fence unless explicitly instructed, making it considerably cheaper. Click deliberately uses Java's non-fencing `weakCompareAndSet` (`weakCAS`) for this reason: “my Hardware on Azul does not fence unless I ask it to and it's much cheaper therefore so I'm happy to have a non-fencing CAS and exactly here I don't need a fence”.
However, raw un-synchronized shared memory is not quite enough for practical application development. Click concedes that users expect a slightly stronger guarantee: a "happens-before" relationship strictly applied to values. When a thread allocates and initializes a new object in memory, it must publish the pointer into the hash table such that another thread reading that reference never observes a partially initialized object.
- The table provides the same semantics as an unsynchronized shared global variable per key across competing threads. - Non-fencing `weakCompareAndSet` operations prevent hardware-level stalls on architectures like Azul that decouple CAS from memory barriers. - A value publication requires a happens-before relationship to guarantee that readers traversing a newly inserted pointer observe fully initialized object state.
[19:46](https://www.youtube.com/watch?v=HJ-719EGIts&t=1186s) Memory Fences, Volatile Semantics, and Double-Checked Locking
Cliff Click explains that establishing a happens-before relationship on values in a concurrent data structure provides the same core guarantees as Java's `volatile` keyword. Without `volatile` or an explicit fence, an object written into shared global memory might be observed in an inconsistent or partially constructed state by another thread reading it. As Click points out, this pitfall is historically well known: “classically it's called the double checked locking idiom and until you add the volatile keyword it's broken you could go Google double checked locking if you don't believe me anyhow”.
To make concurrent updates safe across table boundaries, Click introduces an explicit memory fence into the algorithm. He notes that this is the first point in the implementation where a fence is strictly required. The concrete machine instructions emitted for the fence vary according to the target hardware architecture and its underlying memory model (for example, whether the hardware provides total store ordering or a weaker relaxed-memory model).
```mermaid flowchart LR accTitle: Double-Checked Locking vs Fenced Memory Model accDescr: Diagram contrasting an unfenced write to global shared memory with a fenced write establishing a happens-before relationship.
subgraph Unsafe["Unfenced Shared Write"] W1["Write Object Fields"] --> W2["Publish Object Reference"] W2 -.->|"Reordered / Stale Read"| R1["Reader sees broken state"] end
subgraph Fenced["Fenced / Volatile Publication"] F1["Write Object Fields"] --> Fence["Memory Fence / Volatile Store"] Fence -->|"Guaranteed Happens-Before"| F2["Reader sees fully initialized state"] end ```
---
[20:35](https://www.youtube.com/watch?v=HJ-719EGIts&t=1235s) The Challenge of Concurrent Resizing
Click transitions from standard `get` and `put` operations to the core difficulty of the algorithm: dynamic resizing. Resizing is the crucible where other lock-free hash table algorithms become excessively complicated. As Click puts it, “this is where all the other concurrent hash table algorithms I could find sort of I want to say fell down but got really really ugly this is going to get a little bit ugly but it's not going to get nearly as ugly as the other stuff I've seen”.
Tables begin small (as a small power of two) because most hash tables in practice only ever store a handful of entries. Resizing is triggered under three distinct conditions: 1. **Capacity exhaustion:** The table fills up because the initial allocation was too small. 2. **Hash degradation:** A poor hash function leads to excessive collision chains and reprobing. 3. **Table cleansing:** Deletions leave dead keys (tombstones) scattered throughout the table; the table copies live keys into a new table of the exact same size to purge the dead slots.
The primary difficulty during resizing is not synchronizing inside a single array, but bridging the boundary between the old and new arrays: “the hard bit the thing you have to look out for here is how do you handle picking up the last update to the old table that's racing with your copy into the new table that's the tricky piece”.
Click resolves this by extending the state machine so that being "mid-resize" is a valid, well-defined operational state. Under this design: - Reads can proceed without blocking on the partially copied old table. - Threads can cooperatively assist with copying chunks of the table. - Copying can occur concurrently, incrementally, and in parallel. - Resizing introduces a temporary indirection penalty, so the algorithm actively works to complete migrations cleanly.
---
[22:42](https://www.youtube.com/watch?v=HJ-719EGIts&t=1362s) Resizing Heuristics and the Sentinel State ('X')
When coordinating operations across table boundaries, `get` operations continue looking up keys in the old table unless they encounter a sentinel value. In contrast, any modifying operation (`put`, `remove`) must check whether a new table exists. If a new table is detected, the modifying thread must immediately transition to the new table and execute a memory fence. This inter-table fence ensures Java Memory Model semantics: the final write to the old table is guaranteed to have a happens-before relationship with the resize operation.
Click outlines several heuristic choices for distributing the copy workload: - **Cooperative work:** Every thread that touches the table (whether performing a `get`, `put`, or `delete`) copies a fixed, bounded chunk of entries before completing its own operation. - **Writer-only copying:** Only mutating threads perform copy work, keeping read latency minimal. - **Background helper threads:** Dedicated background threads consume extra CPU cores to churn through table copies asynchronously.
```mermaid flowchart TD accTitle: Sentinel X Routing Mechanism accDescr: Flowchart demonstrating how operations interact with slot states and route to old or new tables based on sentinel X.
Op["Incoming Operation"] --> CheckSlot{"Slot State in Old Table"}
CheckSlot -->|"Null Slot"| CAS_X["CAS null to Sentinel 'X' (Closes slot)"] CheckSlot -->|"Live Key/Value"| CopyVal["Copy to New Table -> CAS slot to 'X'"] CheckSlot -->|"Already 'X'"| RouteNew["Redirect Operation to New Table"]
CAS_X --> DoneOld["Prevents late inserts into Old Table"] CopyVal --> RouteNew ```
To mark entries that have moved, Click introduces a sentinel value designated as `X`. This state serves as an explicit marker that a slot is closed: - **Key Slots:** A key slot transitions from `null` to a valid key, or from `null` to `X`. Once stamped with `X`, late-arriving inserts are prevented from occupying the slot in the old table and are forced into the new table. - **Value Slots:** A value slot transitions from its normal contents (values or tombstones) to `X`, proving that the entry has been successfully migrated to the new table.
---
[25:09](https://www.youtube.com/watch?v=HJ-719EGIts&t=1509s) State Machine Expansion and Handling Chained Resizes
Integrating the `X` sentinel expands the slot state machine into 12 distinct logical states ($3 \times 4$ combinations of key and value states). Click highlights two key states added to the diagram: - An empty slot where the key is stamped directly with `X` to lock out new insertions. - A previously live key-value pair where the value is stamped with `X`, certifying that the key and value have been copied into the new table and any caller checking this slot is now on the wrong table.
Click addresses the scenario of "stacked" or chained resizes, where a new table itself requires resizing before the previous copy has even completed: - If 700 threads simultaneously hammer a brand-new table initialized to a minimum of 32 entries, all 32 slots fill instantly. - The threads trigger a resize to 64 entries, which also saturates immediately, triggering further doublings in rapid succession (e.g., five doublings in a row to swallow 750 concurrent puts). - While the algorithm functionally supports traversing stacked chains of `X` markers across multiple generational tables, Click’s chosen heuristic constrains this behavior: “as a performance thing it was more convenient to force the old table to clean out before making get another new table so I make you do more copy work essentially till the table cleans out”. - To preserve predictable latency, every thread touching the table performs a strictly bounded, constant amount of copy work per operation rather than stalling for the duration of an entire multi-megabyte array copy.
---
[27:40](https://www.youtube.com/watch?v=HJ-719EGIts&t=1660s) Copy Transitions and Racing Updates
Click walks through the specific atomic transitions executed while copying slots from the old table to the new table:
1. **Empty Slot Copy:** If an entry in the old table is empty (`null`), the copier executes a compare-and-swap (CAS) to install `X`. If the CAS succeeds, no key can ever be inserted into that old slot. If the CAS fails, a racing thread has just inserted a key, and the copier re-reads the slot to handle it. 2. **Dead Slot Copy (Tombstones):** If an entry holds a tombstone from a deleted key, the copier overwrites it with an `X`. This closes the dead slot permanently; any subsequent insertion of that same key must take place anew in the new table. 3. **Live Slot Copy with Racing Updates:** When a live key-value pair is copied, it may race against an independent update already taking place directly on the new table. Click notes that happens-before reasoning under the Java Memory Model resolves this cleanly: - Modifying threads check for the existence of the new table before updating. - If a new value ($V_2$) already exists in the new table, it is guaranteed to be strictly newer than the value ($V_1$) lingering in the old table. - Consequently, the copier can safely disregard the old value: “this update this V2 down here that's a newer put and there's a race going on between people doing updates this is a more recent put and I can just ignore the last put I can ignore the guy in the old table I can claim I'm uninterested in his value got stomped by somebody racing to do a put here”.
[29:07](https://www.youtube.com/watch?v=HJ-719EGIts&t=1747s) Copying Slots via a Two-Phase Commit and Primed Values
When migrating entries during a hash table resize, Cliff Click explains that copying is handled through a two-phase commit protocol rather than by simply discarding the old table or locking it. To manage this safely, he introduces "primed" versions of values and tombstones (denoted as $V'$ and $T'$). A primed value in the new table explicitly represents an entry copied over from the old table—meaning it is not a recent, direct update in the new table, but merely a reflection of the past. Conversely, any update placed directly into the new table by a client thread occurs after the resize has begun, guaranteeing that direct updates are strictly newer than any primed value.
```mermaid flowchart TD accTitle: Two-Phase Commit Slot Migration accDescr: Workflow showing how a value transitions from the old table to the new table using primed states and compare-and-swap operations.
subgraph OldTable["Old Table"] direction TB OldVal["Old Value: V"] OldCAS["CAS slot to X"] OldDone["Slot Closed (X)"] OldVal --> OldCAS --> OldDone end
subgraph NewTable["New Table"] direction TB NewNull["Slot: null"] NewCASPrime["CAS in Primed Value: V'"] NewCASUnprime["Strip Prime: CAS to V"] NewNull --> NewCASPrime --> NewCASUnprime end
OldVal -.->|"1. Read old value"| NewCASPrime NewCASPrime -.->|"2. Prime installed"| OldCAS OldDone -.->|"3. Old slot locked"| NewCASUnprime ```
The challenge lies in catching the very last late-arriving write to the old table. The algorithm attempts to copy atomically between tables while the old table might still be bombarded with continuous writes from threads that began their operations before noticing the resize. “I'm going to attempt uh uh uh to copy atomically between the two tables and the old table might be getting continuous uh late arriving updates I need to get the last one out and eventually a new update to the new table might appear and make me uninterested in anything in the old table,” Click notes. Until a newer update lands directly in the new table to supersede the old data, the copier must persist in trying to extract the latest state.
Under this model, the state machine allows transitions from `null` to a primed value ($V'$) in the new table. Once the copier successfully writes an `X` into the corresponding slot of the old table using a Compare-And-Swap (CAS), that old slot is permanently sealed against future updates. With the old slot locked down, the copier can safely strip the prime off the value in the new table, finalizing the transfer.
[31:33](https://www.youtube.com/watch?v=HJ-719EGIts&t=1893s) The Resizing State Machine and Thread Contention
Click zooms into the detailed state transitions governing the migration. Reading the new table acts as the synchronization point—requiring happens-before memory fencing via the Java Memory Model rules. If a thread inspects the new table and sees an unprimed value, it can stop immediately, as an unprimed entry indicates either that another thread has already finished copying or that a brand-new update has landed, making the old table's slot completely obsolete.
If the value in the new table is primed, the thread checks whether it matches the value observed in the old table. If the values differ, the thread uses a CAS to update the new table with a primed version of the newer value ($V_1'$). Once the new table holds the latest copy, the thread attempts to CAS an `X` into the old table. If that CAS succeeds, no more updates can enter the old table; the thread can then strip the prime in the new table, completing the copy atomically.
If any CAS fails along the way, a late-arriving update interfered, requiring the copy operation to roll back and retry in a CAS spin loop: - The spin loop is strictly bounded by the finite number of threads that bypassed the resize check before it became visible but have not yet committed their writes. - “This is a CAS spin loop if I fail, but it's limited by the count of threads that arrive late in the table; in practice, it's almost never, it's once in a blue moon,” Click notes as he glances at the audience. - Observing the mixed reactions in the room, he jokes: “Lot of lot of glazed eyes there's a few people going I got it a lot of people going oh my God.”
[33:52](https://www.youtube.com/watch?v=HJ-719EGIts&t=2032s) Coping with Asynchronous State and Hardware Realities
Click reviews several edge cases that arise during resizing. An entry copied over might turn out to be a tombstone ($T$) if a deletion slipped in after an initial copy attempt failed. In the case of nested resizes, the entry copied may itself already be primed. If a thread finds that another thread has already stripped the prime or performed an unprimed `put`, it simply abandons the copy effort. Similarly, if multiple threads race to copy the same slot and one fails its CAS, it backs off and lets the winning thread complete the work.
```mermaid flowchart LR accTitle: Slot Copy Race Outcomes accDescr: Branching paths when multiple threads race to migrate or update a hash table slot.
Start["Inspect New Table Slot"] --> CheckPrime{"Is Slot Primed?"} CheckPrime -->|"No (Unprimed/Direct Put)"| StopDone["Quit: Newer update exists"] CheckPrime -->|"Yes (V')"| TryOldCAS["CAS 'X' into Old Slot"]
TryOldCAS -->|"CAS Succeeded"| StripPrime["Strip Prime to V (Complete)"] TryOldCAS -->|"CAS Failed (Late Write)"| RetryLoop["Roll back & Retry Copy"] ```
The core insight is that threads cannot rely on classical sequential assumptions when reasoning about system state: - Keys and values can mutate independently and asynchronously underneath an operation due to the cache coherency protocol. - Threads must inspect the observed state and apply the designated transition rule without assuming global stability. - “Mentally it worked for me too but it took me a while to get there to to realize oh I don't care I I can't know what the state really is in memory because there isn't a state defining memory it's floating around the cache coherency protocol in the machine flipping randomly all over the place,” Click explains.
An audience member asks why Azul Systems does not simply implement wider atomic operations in their custom hardware. Click responds: - Azul could build it, but he wants an algorithm that works in standard Java across arbitrary commodity architectures. - “As soon as I can convince Intel to provide me with DCAS I it would it would work right and it's all over with I got DCAS I'll just DCAS key value pairs as a pair and I'm done,” he observes, but the algorithm must function without Double Compare-And-Swap. - When asked how primed values are represented, Click notes that in C code one could steal a bit from a pointer, whereas in Java he wraps the value in a private indirection object and checks it via `instanceof`, which modern JVMs optimize into an extremely cheap check. - Nested resizes do not require multiple prime levels; a single prime marker suffices to denote that a value originates from an older generation table.
[37:18](https://www.youtube.com/watch?v=HJ-719EGIts&t=2238s) Performance Benchmarks and Scaling Characteristics
Click turns to performance benchmarks comparing the lock-free hash table against alternative implementations across several hardware configurations under a workload of 99% reads and 1% inserts/deletes.
| Machine / Architecture | Configuration Details | Benchmark Behavior | |:--- |:--- |:--- | | **x86 (x04)** | Dual hyperthreaded (effectively 2 CPUs) | Achieves ~25 million operations/sec; non-blocking code slightly leads | | **UltraSPARC III (Sun 2)** | 2 CPUs | Baseline dual-processor reference curve | | **Sun 32-way & Niagara** | UltraSPARC / T1 architectures up to 32 cores | Scales upward smoothly on the read-dominated workload | | **Azul Systems** | Older and newer custom Azul multi-core boxes | Shows steady upward throughput scaling | | **java.util.concurrent** | Evaluated on x86 with 4,000-way lock striping | Suffers performance dropouts caused by internal lock contention |
He explains that `ConcurrentHashMap` was specifically configured with 4,000-way striping to keep internal segment locks from immediately choking as thread counts expand on later charts. While the lock-free implementation maintains an uninterrupted upward throughput ramp, the lock-striped concurrent map encounters noticeable dips and dropouts where thread contention on stripe locks disrupts scaling, even with thousands of stripes configured.
[38:58](https://www.youtube.com/watch?v=HJ-719EGIts&t=2338s) Benchmark Scaling: Reaching 1.2 Billion Operations per Second
Cliff Click presents the benchmark comparison between his non-blocking hash table and the standard blocking concurrent hash table across several hardware platforms. On most architectures, the non-blocking version runs roughly 10% faster, with one specific exception: Sun's UltraSPARC T1 ("Niagara"). On Niagara, the concurrent table matches the non-blocking table because the benchmark table size happened to fit into Niagara’s cache for the concurrent version but spilled over for Click's implementation. Adjusting the table size slightly would have shifted those numbers.
```mermaid flowchart TD accTitle: Benchmark Scaling Comparison accDescr: Comparison of operations per second between non-blocking and concurrent striped hash tables up to 768 to 800 CPUs.
subgraph LowCore["Low Core Count / Small Scale"] N1["Niagara / Standard SMPs"] N1 -->|"Cache fit anomaly"| S1["Tied on Niagara; ~10% faster on standard SMPs"] end
subgraph HighThread["High Scale (400 to 800 Threads)"] A1["Azul Appliance (Up to 768-800 CPUs)"] A1 -->|"Read-heavy: 99% reads, 1% writes"| B1["Non-blocking hits 1.2B ops/sec\n(12M updates/sec)"] A1 -->|"High-mutation: 75% reads, 25% writes"| B2["Non-blocking hits 600M reads/sec\n(150M updates/sec)"] A1 -->|"Concurrent striped table"| B3["Flatlines: lock contention,\nexhausted 4,000-way striping, GC pressure"] end ```
When scaling up to hundreds of threads on Azul hardware, the performance gap widens drastically. With 400 threads, the non-blocking algorithm pulls ahead by about 50%, even though the competing concurrent map uses aggressive 4,000-way lock striping. Pushing out to between 768 and 800 CPUs, the non-blocking implementation achieves an astonishing 1.2 billion read operations per second. Because updates constitute 1% of the workload in this test, that corresponds to 12 million concurrent updates per second into the exact same table. Under this load, the concurrent table runs into severe lock contention, thread queueing, and GC issues.
Tests at different read/write ratios demonstrate consistent advantages: - At 95% reads (5% updates), both algorithms shift downward in raw throughput, but the concurrent implementation degrades much faster. At 800 threads, the non-blocking table outperforms the concurrent map by roughly 3×, sustaining 750 million reads per second alongside tens of millions of updates. - At 90% reads (10% updates), the scaling curves reflect the identical downward shift and non-blocking advantage. - At 75% reads (25% updates), the concurrent map completely flatlines because 4,000-way lock striping cannot absorb the write volume. Meanwhile, the non-blocking table sustains 600 million reads per second and 150 million updates per second directly into the shared table. - Looking at older multi-socket Sun hardware (green line on the charts), the non-blocking implementation cleanly outperforms the blocking table.
---
[41:53](https://www.youtube.com/watch?v=HJ-719EGIts&t=2513s) Resizing Overhead and Pathological Workloads
An audience member asks whether resizing occurs during the benchmark runs. Click explains that every benchmark run deliberately starts with a tiny hash table that must rapidly inflate to its target capacity. The benchmark protocol executes seven 2-second trials, discards the highest and lowest results, and averages the remaining five. During these tests, the initial run always suffers noticeably while the table resizes, dropping by a factor of 2× to 3× compared to steady-state throughput. Once the table stabilizes at the correct volume, throughput rebounds and holds steady at roughly double the speed of the concurrent map.
To stress-test resizing behavior, Click created a pathological benchmark maximizing hash table churn. The test repeatedly inserts and deletes elements from a continuous, rolling key set such that only roughly 1,000 keys are alive at any given moment, but 1,000,000 total keys pass through the table. Because the remaining 999,000 slots are tombstones, the table must continuously resize and rehash to purge dead entries.
Even in this non-stop resizing regime, the non-blocking table ran approximately 5× faster than the striped concurrent map. The concurrent map completely seized up trying to acquire locks continuously across its 4,000 stripes. However, the non-blocking table revealed its own architectural bottleneck: because table copying wraps values in prime wrappers, heavy rehashing creates immense allocation churn, saturating the garbage collector at around 500 threads with multi-gigabyte-per-second allocation rates.
When asked why he always starts benchmarks with an undersized table rather than letting the caller pre-size it, Click notes that developer sizing guesses are notoriously unreliable:
> “The answer is that the users think they know and and you know time and time again that's sort of proven correct sometimes they're right and sometimes they're badly wrong and then and if I just do it all the time then they pay a cost to get started except that that cost is always over with in a couple seconds and there's no more cost and then then they're never wrong again right.”
- If a developer needs 1,000,000 entries and over-allocates for 10,000,000 entries, reserving 8 bytes for the key and 8 bytes for the value blows away tens of megabytes of unnecessary heap. - Developers often instantiate tens of thousands of tables configured for 100 to 200 elements, yet only store one or two keys in each, wasting gigabytes of system memory across the fleet. - While initial size hints could eventually be supported, ignoring the hint and auto-sizing quickly avoids catastrophic memory over-allocation.
---
[46:18](https://www.youtube.com/watch?v=HJ-719EGIts&t=2778s) Designing via State Machines, Future Work, and Open Source
Summarizing the project, Click underscores that the real breakthrough is not merely raw speed on massive CPU counts, but the state-machine-driven reasoning that makes lock-free correctness tractable. Rather than trying to reason about execution order via classic Java Memory Model (JMM) fences, `happens-before` relations, and global memory barriers, the algorithm relies on local atomic Compare-And-Swap (CAS) transitions between discrete slot states.
> “And that means that I can um I can dodge the ordering uh and fencing games that go into the JMM turns out that I have to pay attention when I go between tables but within the same table I can ignore that ordering um and that actually helps scaling a lot as well it means I don't have to do uh machine wide coherency on updates I just need to do local coherency on updates.”
Because any thread can observe any slot in any intermediate state at any time, mental models must shift from sequential flows to state transitions. Click drew dozens of state machine diagrams on paper during development, and the final production Java code mirrors those drawings nearly one-to-one: inspect the current state, determine the required target state, and execute the corresponding CAS.
Looking ahead, Click outlines several areas of future exploration: - **Verification and Synthesis Tools:** Building automated tools to verify that all possible states and transitions are handled correctly, or even synthesizing lock-free Java algorithms directly from state transition graphs. - **Broader Data Structures:** Applying state-machine-driven lock-free techniques to other containers, such as building a concurrent non-blocking `java.util.Vector` to support fast concurrent append, delete, and work-list operations. - **Open-Source Release:** Publishing the implementation on his blog. The primary delays are choosing a product name and selecting an open-source license, alongside customer interest driving internal Azul discussions.
---
[48:54](https://www.youtube.com/watch?v=HJ-719EGIts&t=2934s) The Difficulty of Scalable Size Tracking
An audience member asks about race conditions when maintaining the table's total element count (`size()`), wondering if an out-of-date size might delay resizing and cause lookups to traverse long probe sequences fruitlessly. Click clarifies that in a concurrent map, `size()` is never strictly coherent with an in-flight table iteration unless the entire table is quiescent and all update threads have stopped.
The real engineering challenge with `size()` is avoiding a centralized scalability bottleneck. If 750 threads simultaneously attempt to mutate a single shared counter, relying on a naive CAS or synchronization locks causes severe cache line bouncing and drops throughput across the entire chip. Click begins explaining the distributed mechanism required to record increments and decrements scalably across hundreds of cores without serializing the machine.
[49:38](https://www.youtube.com/watch?v=HJ-719EGIts&t=2978s) Striped Counters and the Semantics of `size()`
Cliff explains that maintaining an accurate table size without destroying multi-core throughput required rethinking how mutations are tracked. A single shared counter updated on every insert or delete quickly becomes a centralized choke point on high-CPU machines. To solve this, Cliff implemented striped counters updated via atomic Compare-And-Swap (CAS) operations across multiple independent cache lines, allowing threads running on separate cores to record size increments and decrements in parallel without serializing on one variable.
Because the counter is striped across multiple slots, answering a `size()` query cannot be satisfied by reading a single field. The calling thread must iterate across all the striped counter cells and sum their values. Cliff stresses that in a concurrent data structure, this sum represents only an instantaneous snapshot rather than a strict transactional invariant. If other worker threads are concurrently removing or inserting entries while a caller reads the size and prepares to loop over the table, the actual number of elements may shift immediately.
Addressing audience expectations around precise consistency guarantees, Cliff states: > “There is no guarantee then otherwise on the size that you got back and the contents of the table unless nobody else is updating the table.”
- Striped CAS counters prevent high-core lock contention during frequent insertions and deletions. - Calling `size()` requires walking and summing all striped counter slots rather than a single volatile read. - Concurrent modifications mean `size()` cannot guarantee that an immediate subsequent iteration will encounter that exact number of elements.
---
[50:25](https://www.youtube.com/watch?v=HJ-719EGIts&t=3025s) Resize Triggers and the Argument Against Shrinking
Cliff outlines the two distinct conditions that trigger a table growth phase. The primary condition is slot saturation: keys permanently consume slots, and once the ratio of occupied key slots crosses a set capacity threshold, the table must expand. The secondary trigger is probe depth. If an individual thread reprobes too aggressively during a search—checking slot after slot only to find a long, continuous run of occupied entries—it concludes that clustering has degraded local lookup performance. Rather than spinning indefinitely through long collision chains, that thread initiates an expansion to double the array size and redistribute the keys across a wider hash space.
```mermaid flowchart TD accTitle: Table Growth Triggers accDescr: Flowchart detailing the two conditions under which the hash table triggers an expansion. subgraph Triggers ["Resize Evaluation"] T1["Global Slot Threshold Exceeded"] T2["Single-Thread Reprobe Limit Exceeded"] end subgraph Action ["Expansion Action"] Grow["Trigger Table Growth\n(Double array size to disperse keys)"] end T1 --> Grow T2 --> Grow ```
When an audience member asks whether the table ever shrinks when entries are deleted, Cliff confirms that it currently does not. While shrinking could theoretically be triggered by tracking dead keys and counting live elements during resize passes, Cliff questions the practical utility of doing so in production server environments. An audience member points out that an application running at that scale has already proven it is not memory constrained; Cliff adds that fluctuating workloads playing "ping-pong" by constantly expanding and contracting memory to share space with other processes represents an overly complicated edge case. Furthermore, core Java runtime structures like `ArrayList` and `Vector` share this exact design decision.
Cliff makes his position on table contraction clear: > “I'm convinced that it's an interesting thing to do; not that I don't think it can't be done, I just think it's uninteresting to do it.”
- Resizing is triggered globally by overall slot consumption or locally by any thread experiencing excessive reprobe lengths. - Growing doubles the table capacity, dispersing clustered keys and reopening empty slots for insertions. - The implementation does not contract tables after mass deletions, matching the design philosophy of standard Java collections.
---
[52:29](https://www.youtube.com/watch?v=HJ-719EGIts&t=3149s) Concurrent Table Growth, CAS Races, and Cooperative Copying
An audience member asks how the algorithm handles situations where two distinct threads simultaneously decide that the table is full and must grow. Cliff confirms that multiple threads frequently trigger resizing concurrently. The data structure manages this race through a state machine centered around a single reference field pointing to the next table. When threads decide to resize, they each allocate a new, empty array independently and attempt a CAS operation to transition that pointer from `null` to their newly allocated array.
```mermaid flowchart TD accTitle: Cooperative Resizing State Machine accDescr: Flowchart showing how threads race to install a new array and then cooperatively copy entries. subgraph Alloc ["1. Allocation Race"] T1["Thread A allocates new array"] T2["Thread B allocates new array"] CAS{"CAS next-table pointer\n(null -> new table)"} T1 --> CAS T2 --> CAS CAS -->|Winner| Inst["Table installed"] CAS -->|Loser| Drop["Discard array\n(collected by young GC)"] end subgraph Copy ["2. Cooperative Migration"] Inst --> Migrate["Threads cooperatively\ncopy slots old -> new"] Migrate --> Strip["Strip Prime wrappers in new table"] end subgraph Promo ["3. Promotion"] Strip --> Root["Promote new table to root"] Root --> OldGC["Old table reclaimed by GC"] end ```
The thread that successfully executes the CAS installs its array as the official next table. The losing thread immediately discards its allocated array without copying anything into it. Once the target array is installed, the operation enters a cooperative migration phase: all threads participating in table operations help copy individual key-value slots from the old table to the new one, stripping temporary `Prime` wrapper markers as items land in the expanded table. When all slots have migrated, the new table is promoted to become the root table, leaving the orphaned old table to be reclaimed by the garbage collector.
Describing the race to install the new array, Cliff explains: > “One guy wins the CAS, the other guy loses; the loser throws his table away.”
---
[53:34](https://www.youtube.com/watch?v=HJ-719EGIts&t=3214s) Copying GC Analogy, Portability, and Prime Bits
An audience member remarks that Cliff's slot-copying protocol behaves almost identically to a concurrent copying garbage collector, with the `Prime` marker wrapper functioning as a forwarding pointer. Cliff agrees completely with the observation. Azul had previously evaluated binding the hash table directly into their JVM's internal runtime so that the garbage collector could manage the entire copying process behind read barriers. However, Cliff intentionally chose pure Java semantics to guarantee absolute portability across standard, unmodified JVMs.
Had portability not been a primary objective, Cliff notes he would have abandoned the `Prime` object wrapper entirely. In a language like C or within custom VM internals, one would simply steal an unused pointer bit to mark a slot as primed or forwarded, avoiding the memory allocation overhead of wrapper objects altogether. When an attendee points out that Azul's specialized hardware already features read barriers that could handle this natively, Cliff reiterates his strategic goal: he wants this lock-free hash table freely available in the public domain so enterprise customers adopt it before deploying on Azul machines, eliminating the need to rewrite bottlenecks after the fact.
Emphasizing the architectural trade-off required for portable Java execution, Cliff says: > “If I didn't want to be portable, the first thing I'd do is I'd get rid of the Prime wrapper and use a prime bit, and that was the sort of the major cost in any of the table copy operations is messing with that extra bit by doing a wrapper.”
- The algorithm's data migration directly mirrors a copying collector using forwarding pointers. - Wrapping values in a `Prime` object is the single largest overhead during table resizing, required only because pure Java cannot steal raw pointer bits. - Azul deliberately released the code into the public domain so multi-threaded enterprise software scales out of the box without requiring manual re-engineering on massive hardware.
---
[54:52](https://www.youtube.com/watch?v=HJ-719EGIts&t=3292s) Collaboration with Doug Lea and Standard Library Prospects
Cliff discusses his extensive technical debates with Doug Lea regarding the potential inclusion of this lock-free algorithm in the official `java.util.concurrent` package. In earlier discussions several months prior, Doug was supportive of incorporating an `IdentityHashMap` variant based on this approach, but expressed reservations about a general-purpose map relying on object `.equals()` and `.hashCode()` implementations—debating whether hash codes should be aggressively memoized or handled differently during probing cascades.
Since those earlier discussions, Cliff refined the algorithm considerably, establishing firmer correctness invariants and verifying performance against `java.util.concurrent.ConcurrentHashMap`. Even when `ConcurrentHashMap` was configured with large striping concurrency levels, Cliff's lock-free design demonstrated superior scaling across heavy thread counts. Given these benchmarks and structural improvements, Cliff anticipates that some variant of the algorithm will ultimately make its way into standard Java runtime libraries.
Highlighting the trajectory of the implementation into standard Java, Cliff notes: > “Some version of this is likely to hit java.util.concurrent at some point.”
---
[56:00](https://www.youtube.com/watch?v=HJ-719EGIts&t=3360s) Memory Bounds During Resizing and Azul Zeroing Hardware
An audience member asks about worst-case memory consumption during resizing, suggesting that if a 1 GB table begins doubling to 2 GB, multiple threads allocating concurrent 2 GB buffers could consume memory proportional to the table size multiplied by the thread count. Cliff explains that while that scenario represents the theoretical upper bound if hundreds of threads entered allocation at the exact same instant, the practical reality is completely different. Threads race to install the table pointer immediately after allocation before doing any slot processing; in practice, even across 700 active CPUs resizing aggressively, only a tiny handful of duplicate arrays are ever allocated before the pointer is CAS-installed and other threads back off.
Furthermore, Cliff notes that the operation is fully fault-tolerant: if an allocation fails with an `OutOfMemoryError`, the thread catches or handles it, abandons the resize attempt, and continues servicing operations out of the old table rather than crashing. Because unused loser arrays are discarded immediately before any data is copied into them, they never escape into old generation heaps; instead, they are swept up cheaply in the next young-generation nursery collection without incurring significant GC overhead. When an audience member asks about the CPU cost of zeroing massive newly allocated arrays, Cliff reveals an Azul architectural advantage: the Vega processors feature dedicated hardware zeroing engines that wipe allocated memory lines at high speed, minimizing the CPU penalty of array initialization.
On the fate of discarded candidate arrays during a resize collision, Cliff concludes: > “If you're not going to use the array you just allocated because somebody beat you to the punch, you drop it on the floor; the next young generation GC cycle will reclaim it for no work.”
- The theoretical worst-case memory spike is bounded by active thread count, but the practical overhead is limited to a few transient arrays because winning CAS installations shut down subsequent allocation attempts quickly. - Unused table allocations die immediately in young generation nursery space, making reclamation nearly free for modern generational collectors. - Azul's custom hardware incorporates specialized zeroing hardware that accelerates raw array allocation times compared to standard x86 architectures.
## Notable quotes > [03:00](https://www.youtube.com/watch?v=HJ-719EGIts&t=180s) No locks here means nothing that looks like a lock: not just synchronized keywords or java.util locking, you can't have spin-locks or anything that basically grabs onto a shared resource and holds it permanently. — Dr. Cliff Click
> [56:00](https://www.youtube.com/watch?v=HJ-719EGIts&t=3360s) If you're not going to use the array you just allocated because somebody beat you to the punch, you drop it on the floor; the next young generation GC cycle will reclaim it for no work. — Dr. Cliff Click
> [52:29](https://www.youtube.com/watch?v=HJ-719EGIts&t=3149s) One guy wins the CAS, the other guy loses; the loser throws his table away. — Dr. Cliff Click
## Mentioned Dr. Cliff Click, Azul Systems, Sun Microsystems, Google, Doug Lea, Jeremy Manson, HotSpot Server Compiler, NonBlockingHashMap, ConcurrentHashMap, HashMap, Hashtable, IdentityHashMap, ArrayList, Vector, java.util.concurrent, Java, sun.misc.Unsafe, x86, SPARC, UltraSPARC III, UltraSPARC T1 (Niagara), Vega, PowerPC, Itanium, Intel
## Video description Google Tech Talks March 28, 2007
ABSTRACT
I present a lock-free concurrent Hash Table implementation with better single-thread performance than most Hash Tables, and better multi-thread performance than all other implementations I tried. I demonstrate scaling up to 768 CPUs even with high mutation rates. I show correctness by looking at the problem in a very different light than the usual "happens-before" / memory-order / fencing style of thinking.
Speaker: Dr. Cliff Click
Dr. Cliff Click, Azul Systems. Chief JVM Architect, Distinguished Engineer With more than twenty-five years experience developing compilers Cliff serves as Azul Systems' Chief JVM Architect. Cliff joined Azul in 2002 from Sun Microsystems where he was the architect and lead developer of the HotSpot Server Compiler, a technology that has delivered dramatic improvements in Java performance since its inception. Previously he was with Motorola where he helped deliver industry leading SpecInt2000 scores on PowerPC chips, and before that he researched compiler technology at HP Labs. Cliff has been writing optimizing compilers and JITs for over 15 years. He is invited to speak regularly at industry and academic conferences, and has published many papers about HotSpot technology. Cliff holds a PhD in Computer Science from Rice University.
Google engEDU Speaker: Cliff Click
[Source](https://www.youtube.com/watch?v=HJ-719EGIts)