Hazard Pointers
Memory reclamation scheme for lock-free structures in non-GC languages. Each thread publishes the pointers it's about to dereference (hazard pointers). Reclamation defers freeing a node until no thread's hazard pointer references it. Solves the 'when can I free this?' problem without a GC.
The problem in one sentence
In a lock-free data structure without garbage collection, after a node is popped other threads might still be reading it. Free it too soon and they read garbage. Free it too late and memory leaks.
Hazard pointers are the way to know when "soon enough" is.
A picture of the danger
The stack starts as head → A → B → C. Thread 1 is in the middle of a pop. Thread 2 is reading the same node A through the head. The pop completes correctly, but the timing of the free call is what causes the bug.
The pop itself was correct. The bug is that A was freed while Thread 2 still had a pointer to it. Garbage-collected languages sidestep this because the GC will not free A while any thread holds a reference. C and C++ have no such safety net. Hazard pointers are how that safety net is built explicitly.
The trick
Each thread keeps a small array of "I am about to use this pointer" announcements, called hazard slots. Before reading from a shared atomic pointer, the thread writes the pointer it just loaded into one of its own hazard slots. Before freeing a node that was popped earlier, the reclaimer scans every thread's hazard slots. If any slot contains the node's address, the reclaimer holds off on the free until later.
The reader's protocol is:
- Load the pointer from the shared atomic.
- Publish that pointer to its hazard slot.
- Re-read the shared atomic. If the value changed in between, the announcement was published too late and the node may already have been retired; reload and try again.
- Once the re-read still matches, the pointer is safe to dereference.
- When done, clear the hazard slot.
The double-check at the top is the subtle part. Publishing alone is not enough, because the publish might happen after a parallel reclaimer has already scanned and decided the node is safe to free. Publishing followed by a re-read of the source closes the gap: if the source still equals the pointer, the announcement reached the reclaimer in time; if the source has moved on, the node may already have been retired and the reader must retry.
A picture of the lifecycle
There are two views to keep in mind. The reader's view of one hazard slot, and the reclaimer's view of one node.
Reader's view of a hazard slot. The slot moves between two states: empty when the thread is doing nothing risky, and holding a pointer when the thread is actively reading the node at that pointer.
Reclaimer's view of a node X. The reclaimer never frees X immediately when X is unlinked. X spends time on a per-thread retired list first. The reclaimer periodically scans every thread's hazard slots. As long as X appears in any slot, X stays on the retired list. Once X appears in no slot, the reclaimer is free to release the memory.
Each thread typically has between one and eight hazard slots. Most lock-free structures only need each reader to hold one or two pointers at a time, so eight slots is plenty.
Comparison with other schemes
Reference counting: each node has an atomic refcount. Increment on access, decrement on release. Free at zero. Simple but the atomic ops on the shared counter dominate performance under contention.
Epoch-based reclamation (RCU-like): each thread maintains a current epoch. Reclamation defers until all threads have advanced past the epoch when the node was unlinked. Lower per-access overhead than hazard pointers (just bump the local epoch counter). Higher reclamation latency.
Hazard pointers: per-access overhead (publish + clear), fast reclamation (free as soon as no hazard). Each access needs a bounded number of slots.
For lock-free queues and stacks accessed by many threads, all three work. The right choice depends on the access pattern: if accesses are short, RCU wins; if access depth is bounded, hazard pointers are predictable; if prompt freeing is required, hazard pointers.
Hazard pointers vs epoch-based reclamation, side by side
This is the choice that comes up when picking a reclamation strategy for a real lock-free structure. The two are competing approaches with mirror-image tradeoffs.
| Property | Hazard pointers | Epoch-based reclamation (EBR) |
|---|---|---|
| Per-access overhead | One atomic store + one re-load + one atomic store on clear | One atomic load (the local epoch counter) |
| Reclamation latency | Bounded: O(threads × slots) operations after unlink | Unbounded in theory: a stalled thread stuck in old epoch defers reclamation forever |
| Memory footprint per pending free | Small (bounded retired list per thread) | Can grow large if a thread doesn't advance |
| Scales to many threads | Linearly degrading (hazard scan is O(threads × slots)) | Better (epoch comparisons are constant) |
| Sensitive to thread death | No (slot can be reclaimed by registry) | Yes (a dead thread parked in old epoch wedges reclamation) |
| Used in | Folly, libcds, std::hazard_pointer | crossbeam (Rust), Linux kernel RCU, Go runtime, ParkingLot |
The mental model:
- Hazard pointers: "I'm holding this exact node, please don't free it." Fine-grained, per-access.
- EBR: "Nobody is reading anything from before epoch N." Coarse-grained, per-thread.
When to pick which:
- Many threads with short, bounded accesses (typical lock-free queue): EBR. Lower per-access cost matters more than reclamation latency.
- Few threads with long accesses, or strict memory budgets: hazard pointers. You free promptly and don't accumulate.
- Real-time systems where bounded reclamation latency is required: hazard pointers. EBR has no upper bound.
- Code that already runs in a managed runtime: neither. The GC is the reclamation strategy.
Code-shape comparison for the same operation (pop from a lock-free stack):
// Hazard pointer style
HazardPointer hp = thread_local_hazard_slots[0];
Node* old = head.load(acquire);
do {
hp.protect(old); // publish
Node* recheck = head.load(acquire);
if (recheck != old) { old = recheck; continue; } // retry
if (!head.compare_exchange_strong(old, old->next)) continue;
break;
} while (true);
// dereference old safely
hp.clear();
retire(old); // freed when no hazard
// Epoch-based reclamation style
Guard guard = epoch.pin(); // bump local epoch
Node* old = head.load(acquire);
while (!head.compare_exchange_weak(old, old->next));
// dereference old safely
defer_free(old, guard); // freed when global epoch advances past current
// guard goes out of scope, releases pin
EBR's hot path is one atomic to bump the epoch and one atomic on the actual data structure. Hazard pointers need an extra publish + re-check on every access. EBR wins per-op; hazard pointers win on reclamation predictability.
In practice
Most application code never touches hazard pointers directly. You use a library (folly's hazard_pointer, crossbeam in Rust) that hides the protocol behind a typed API.
For C++26+ code, std::hazard_pointer (proposed) will provide the standard primitive. For now, copy from folly, or use a library that handles reclamation directly.
In Java and Go, hazard pointers aren't needed; the GC handles the equivalent. Garbage collected languages essentially run hazard-pointer-style logic continuously over all references.
The core insight worth keeping: a lock-free pop is not the same as a safe free. Other threads might still hold the popped pointer, so without GC an explicit reclamation scheme is required. Hazard pointers are one of the standard answers (alongside reference counting and epoch-based reclamation), and what they publish is essentially "I am about to use this". And almost no one should be writing them by hand: use folly, libcds, or the proposed standard primitive. Lock-free reclamation is one of the most error-prone areas of systems programming.
Implementations
The JVM GC tracks all reachable objects. As long as a thread holds a reference to a popped node, the GC won't free it. Hazard pointers are essentially what the GC already does, but for specific pointers and without the full GC machinery.
1 // Java: just hold the reference, GC handles the rest
2 public T pop() {
3 Node<T> oldHead, newHead;
4 do {
5 oldHead = head.get(); // local ref keeps oldHead alive
6 if (oldHead == null) return null;
7 newHead = oldHead.next();
8 } while (!head.compareAndSet(oldHead, newHead));
9 return oldHead.value(); // safe; oldHead.value still valid
10 }
11 // After pop, the local oldHead reference goes out of scope; GC can collect later.Key points
- •Each thread maintains a small fixed array of hazard pointers (typically 1-8).
- •Before dereferencing a pointer obtained via shared atomic, publish it as a hazard pointer.
- •On reclamation: collect all hazard pointers across threads, only free nodes not in the set.
- •Bounds on memory reclamation latency: a node can be freed at most O(N * H) operations after being unlinked, where N=threads, H=hazards per thread.
- •Alternative to epoch-based reclamation; lower latency for individual frees, more bookkeeping per access.
Follow-up questions
▸What problem do hazard pointers solve?
▸Hazard pointers vs epoch-based reclamation?
▸How many hazard pointers do I need per thread?
▸Why is C++ getting hazard pointers in the standard library?
Gotchas
- !Publishing hazard pointer AFTER dereferencing = use-after-free
- !Forgetting to clear hazard pointer = node never freed (memory leak)
- !Too few hazard slots per thread = need to retire-and-restart on contention
- !Hazard scan is O(threads × slots); doesn't scale to thousands of threads
- !Mixing hazard pointers with epoch-based reclamation in the same structure: confusing