From 1bf1eb58b175093d45b7aef211d5f1a30879167d Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Fri, 3 Jul 2026 16:43:23 +0300 Subject: [PATCH 1/4] Replace per-operation backend read lock with a scalable shared-access gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every operation enters the pluggable backend through accessBegin() and EntryContainer.sharedLock: one AtomicInteger increment/decrement pair plus a ReentrantReadWriteLock read acquire/release per operation. Read locks do not block each other, but every acquire/release CAS-es the single lock state word, and at tens of thousands of operations per second across all worker threads this cache line becomes the hottest shared write in the server. Hot paths in BackendImpl now register through a striped LongAdder gate (beginSharedAccess/endSharedAccess) and only wait when an exclusive locker has closed the gate. Exclusive lockers (index removal, backend configuration changes, close — all rare structural operations) close the gate via the existing lock()/unlock() helpers, drain in-flight accesses, and still take the write lock, which also excludes the remaining legacy sharedLock readers (export, verify, tree listing). threadTotalCount becomes a LongAdder (read only by waitUntilQuiescent). Interleaved A/B under the compare benchmark: +13% throughput on the cleanest adjacent pair (39.2k -> 44.3k ops/s) and consistently better p99.9 (26 -> 19 ms; 135 -> 54 ms late in the series), every pair favoring the gate. 1,443 tests pass, including JETestCase and the import/export suites exercising the exclusive paths. --- .../backends/pluggable/BackendImpl.java | 56 +++++----- .../backends/pluggable/EntryContainer.java | 101 ++++++++++++++++-- .../backends/pluggable/RootContainer.java | 5 +- 3 files changed, 126 insertions(+), 36 deletions(-) diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendImpl.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendImpl.java index 50506e33db..94acb7aba8 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendImpl.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendImpl.java @@ -29,7 +29,7 @@ import java.util.Set; import java.util.SortedSet; import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.LongAdder; import org.forgerock.i18n.LocalizableException; import org.forgerock.i18n.LocalizableMessage; @@ -93,9 +93,13 @@ public abstract class BackendImpl extends LocalBa /** The root container to use for this backend. */ private RootContainer rootContainer; - // FIXME: this is broken. Replace with read-write lock. - /** A count of the total operation threads currently in the backend. */ - private final AtomicInteger threadTotalCount = new AtomicInteger(0); + /** + * A count of the total operation threads currently in the backend. Bumped + * twice per operation by all worker threads, so it uses LongAdder to avoid + * contending on a single cache line; it is only read when waiting for the + * backend to become quiescent. + */ + private final LongAdder threadTotalCount = new LongAdder(); /** The base DNs defined for this backend instance. */ private Set baseDNs; @@ -130,14 +134,14 @@ private EntryContainer accessBegin(Operation operation, DN entryDN) throws Direc { throw new DirectoryException(ResultCode.UNDEFINED, ERR_BACKEND_ENTRY_DOESNT_EXIST.get(entryDN, getBackendID())); } - threadTotalCount.getAndIncrement(); + threadTotalCount.increment(); return ec; } /** End a Backend API method that accesses the EntryContainer. */ private void accessEnd() { - threadTotalCount.getAndDecrement(); + threadTotalCount.decrement(); } /** @@ -147,7 +151,7 @@ private void accessEnd() */ private void waitUntilQuiescent() { - while (threadTotalCount.get() > 0) + while (threadTotalCount.sum() > 0) { // Still have threads accessing the storage so sleep a little try @@ -252,7 +256,7 @@ public void closeBackend() } // Make sure the thread counts are zero for next initialization. - threadTotalCount.set(0); + threadTotalCount.reset(); // Log an informational message. logger.info(NOTE_BACKEND_OFFLINE, cfg.getBackendId()); @@ -362,7 +366,7 @@ public long getNumberOfEntriesInBaseDN(DN baseDN) throws DirectoryException checkNotNull(baseDN, "baseDN must not be null"); final EntryContainer ec = accessBegin(null, baseDN); - ec.sharedLock.lock(); + ec.beginSharedAccess(); try { return ec.getNumberOfEntriesInBaseDN(); @@ -374,7 +378,7 @@ public long getNumberOfEntriesInBaseDN(DN baseDN) throws DirectoryException } finally { - ec.sharedLock.unlock(); + ec.endSharedAccess(); accessEnd(); } } @@ -401,7 +405,7 @@ public long getNumberOfChildren(DN parentDN) throws DirectoryException throw de; } - ec.sharedLock.lock(); + ec.beginSharedAccess(); try { return ec.getNumberOfChildren(parentDN); @@ -412,7 +416,7 @@ public long getNumberOfChildren(DN parentDN) throws DirectoryException } finally { - ec.sharedLock.unlock(); + ec.endSharedAccess(); accessEnd(); } } @@ -421,7 +425,7 @@ public long getNumberOfChildren(DN parentDN) throws DirectoryException public boolean entryExists(final DN entryDN) throws DirectoryException { EntryContainer ec = accessBegin(null, entryDN); - ec.sharedLock.lock(); + ec.beginSharedAccess(); try { return ec.entryExists(entryDN); @@ -432,7 +436,7 @@ public boolean entryExists(final DN entryDN) throws DirectoryException } finally { - ec.sharedLock.unlock(); + ec.endSharedAccess(); accessEnd(); } } @@ -441,7 +445,7 @@ public boolean entryExists(final DN entryDN) throws DirectoryException public Entry getEntry(DN entryDN) throws DirectoryException { EntryContainer ec = accessBegin(null, entryDN); - ec.sharedLock.lock(); + ec.beginSharedAccess(); try { return ec.getEntry(entryDN); @@ -452,7 +456,7 @@ public Entry getEntry(DN entryDN) throws DirectoryException } finally { - ec.sharedLock.unlock(); + ec.endSharedAccess(); accessEnd(); } } @@ -462,7 +466,7 @@ public void addEntry(Entry entry, AddOperation addOperation) throws DirectoryExc { EntryContainer ec = accessBegin(addOperation, entry.getName()); - ec.sharedLock.lock(); + ec.beginSharedAccess(); try { ec.addEntry(entry, addOperation); @@ -473,7 +477,7 @@ public void addEntry(Entry entry, AddOperation addOperation) throws DirectoryExc } finally { - ec.sharedLock.unlock(); + ec.endSharedAccess(); accessEnd(); } } @@ -484,7 +488,7 @@ public void deleteEntry(DN entryDN, DeleteOperation deleteOperation) { EntryContainer ec = accessBegin(deleteOperation, entryDN); - ec.sharedLock.lock(); + ec.beginSharedAccess(); try { ec.deleteEntry(entryDN, deleteOperation); @@ -495,7 +499,7 @@ public void deleteEntry(DN entryDN, DeleteOperation deleteOperation) } finally { - ec.sharedLock.unlock(); + ec.endSharedAccess(); accessEnd(); } } @@ -506,7 +510,7 @@ public void replaceEntry(Entry oldEntry, Entry newEntry, ModifyOperation modifyO { EntryContainer ec = accessBegin(modifyOperation, newEntry.getName()); - ec.sharedLock.lock(); + ec.beginSharedAccess(); try { @@ -518,7 +522,7 @@ public void replaceEntry(Entry oldEntry, Entry newEntry, ModifyOperation modifyO } finally { - ec.sharedLock.unlock(); + ec.endSharedAccess(); accessEnd(); } } @@ -538,7 +542,7 @@ public void renameEntry(DN currentDN, Entry entry, ModifyDNOperation modifyDNOpe throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, WARN_FUNCTION_NOT_SUPPORTED.get()); } - currentContainer.sharedLock.lock(); + currentContainer.beginSharedAccess(); try { currentContainer.renameEntry(currentDN, entry, modifyDNOperation); @@ -549,7 +553,7 @@ public void renameEntry(DN currentDN, Entry entry, ModifyDNOperation modifyDNOpe } finally { - currentContainer.sharedLock.unlock(); + currentContainer.endSharedAccess(); accessEnd(); } } @@ -559,7 +563,7 @@ public void search(SearchOperation searchOperation) throws DirectoryException, C { EntryContainer ec = accessBegin(searchOperation, searchOperation.getBaseDN()); - ec.sharedLock.lock(); + ec.beginSharedAccess(); try { @@ -571,7 +575,7 @@ public void search(SearchOperation searchOperation) throws DirectoryException, C } finally { - ec.sharedLock.unlock(); + ec.endSharedAccess(); accessEnd(); } } diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java index a4a7eb2c5b..b6b3b7133e 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java @@ -14,6 +14,7 @@ * Copyright 2006-2010 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. * Portions copyright 2013 Manuel Gaupp + * Portions Copyright 2025 3A Systems, LLC */ package org.opends.server.backends.pluggable; @@ -37,6 +38,7 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.TreeMap; +import java.util.concurrent.atomic.LongAdder; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -235,7 +237,7 @@ public ConfigChangeResult applyConfigurationDelete(final BackendIndexCfg cfg) { final ConfigChangeResult ccr = new ConfigChangeResult(); - exclusiveLock.lock(); + EntryContainer.this.lock(); try { storage.write(new WriteOperation() @@ -255,7 +257,7 @@ public void run(WriteableTransaction txn) throws Exception } finally { - exclusiveLock.unlock(); + EntryContainer.this.unlock(); } return ccr; @@ -317,7 +319,7 @@ public boolean isConfigurationDeleteAcceptable(BackendVLVIndexCfg cfg, List Date: Fri, 3 Jul 2026 18:12:20 +0300 Subject: [PATCH 2/4] Convert the remaining hasSubordinates shared-lock site to the access gate Review follow-up: hasSubordinates() still acquired EntryContainer.sharedLock directly and was missed by the original conversion because its local variable is named differently. All hot BackendImpl paths now go through beginSharedAccess()/endSharedAccess(). --- .../org/opends/server/backends/pluggable/BackendImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendImpl.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendImpl.java index 94acb7aba8..1ef5c44c50 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendImpl.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendImpl.java @@ -13,7 +13,7 @@ * * Copyright 2007-2010 Sun Microsystems, Inc. * Portions Copyright 2013-2016 ForgeRock AS. - * Portions Copyright 2025 3A Systems, LLC + * Portions Copyright 2025-2026 3A Systems, LLC */ package org.opends.server.backends.pluggable; @@ -344,7 +344,7 @@ public ConditionResult hasSubordinates(DN entryDN) throws DirectoryException throw de; } - container.sharedLock.lock(); + container.beginSharedAccess(); try { return ConditionResult.valueOf(container.hasSubordinates(entryDN)); @@ -355,7 +355,7 @@ public ConditionResult hasSubordinates(DN entryDN) throws DirectoryException } finally { - container.sharedLock.unlock(); + container.endSharedAccess(); accessEnd(); } } From e540fb61ab9fef32e2395b4750cae9b77c6a8276 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Sat, 4 Jul 2026 09:46:12 +0300 Subject: [PATCH 3/4] Keep draining shared accesses when the exclusive locker is interrupted lock() used to break out of the drain loop on InterruptedException, allowing the exclusive caller (index removal, configuration change) to run concurrently with in-flight shared accesses. Defer the interrupt like beginSharedAccess() does: keep draining and restore the interrupt status before returning. Add a regression test that fails against the previous behavior. --- .../backends/pluggable/EntryContainer.java | 11 ++++- .../PluggableBackendImplTestCase.java | 49 ++++++++++++++++++- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java index b6b3b7133e..398d937a7a 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java @@ -2794,6 +2794,10 @@ void lock() { exclusiveLock.lock(); exclusiveAccessPending = true; + // The drain must not be abandoned on interrupt: returning early would let + // the exclusive caller run concurrently with in-flight shared accesses. + // Exclusive lockers are rare, so sleep-polling is an acceptable trade-off. + boolean interrupted = false; while (sharedAccessCount.sum() != 0) { try @@ -2802,10 +2806,13 @@ void lock() } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - break; + interrupted = true; } } + if (interrupted) + { + Thread.currentThread().interrupt(); + } } /** Unlock the exclusive lock and reopen the gate for lock-free shared accessors. */ diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/PluggableBackendImplTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/PluggableBackendImplTestCase.java index e226c5ff91..9aab97acfd 100644 --- a/opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/PluggableBackendImplTestCase.java +++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/PluggableBackendImplTestCase.java @@ -12,7 +12,7 @@ * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2015-2016 ForgeRock AS. - * Portions Copyright 2023-2025 3A Systems, LLC. + * Portions Copyright 2023-2026 3A Systems, LLC. */ package org.opends.server.backends.pluggable; @@ -31,9 +31,12 @@ import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import com.google.common.io.Resources; import org.forgerock.opendj.ldap.*; @@ -629,6 +632,50 @@ private int getTotalNumberOfLDIFEntries() return topEntries.size() + entries.size() + workEntries.size(); } + @Test(timeOut = 30000) + public void testExclusiveLockDrainsSharedAccessDespiteInterrupt() throws Exception + { + final EntryContainer ec = backend.getRootContainer().getEntryContainer(testBaseDN); + final CountDownLatch lockAcquired = new CountDownLatch(1); + final AtomicBoolean interruptPreserved = new AtomicBoolean(); + + ec.beginSharedAccess(); + final Thread exclusiveLocker = new Thread("Test exclusive locker") + { + @Override + public void run() + { + ec.lock(); + try + { + interruptPreserved.set(Thread.currentThread().isInterrupted()); + lockAcquired.countDown(); + } + finally + { + ec.unlock(); + } + } + }; + + try + { + exclusiveLocker.start(); + exclusiveLocker.interrupt(); + assertFalse(lockAcquired.await(200, TimeUnit.MILLISECONDS), + "lock() returned while a shared access was still in flight"); + } + finally + { + ec.endSharedAccess(); + } + + assertTrue(lockAcquired.await(10, TimeUnit.SECONDS), + "lock() did not complete after the shared access ended"); + assertTrue(interruptPreserved.get(), "lock() must preserve the caller's interrupt status"); + exclusiveLocker.join(10000); + } + @Test public void testHasSubordinates() throws Exception { From a9f7ecc4b6525a0fb8fcb1c9bf2f537b62b5d615 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Sat, 4 Jul 2026 10:46:28 +0300 Subject: [PATCH 4/4] Make the shared-access gate drain provably exact via a same-slot striped counter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LongAdder gives no guarantee that the increment and the backout decrement of one beginSharedAccess() call land in the same cell: the probe is rehashed after CAS contention and the cell table grows. A non-atomic sum() scan may then observe the decrement while missing the paired increment, under-count and return a false zero while a shared access is still in flight, letting lock() start a structural operation concurrently. Replace both counters (sharedAccessCount, threadTotalCount) with a striped counter whose slot is a pure function of the thread, so a scan observes a prefix of each slot's history and can only over-estimate — the same-slot guarantee percpu-rwsem gets from per-CPU counters. --- .../backends/pluggable/BackendImpl.java | 10 +- .../backends/pluggable/EntryContainer.java | 23 +++-- .../backends/pluggable/StripedCounter.java | 95 +++++++++++++++++++ 3 files changed, 114 insertions(+), 14 deletions(-) create mode 100644 opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/StripedCounter.java diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendImpl.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendImpl.java index 1ef5c44c50..028ca38b56 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendImpl.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendImpl.java @@ -29,7 +29,6 @@ import java.util.Set; import java.util.SortedSet; import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.LongAdder; import org.forgerock.i18n.LocalizableException; import org.forgerock.i18n.LocalizableMessage; @@ -95,11 +94,12 @@ public abstract class BackendImpl extends LocalBa /** * A count of the total operation threads currently in the backend. Bumped - * twice per operation by all worker threads, so it uses LongAdder to avoid - * contending on a single cache line; it is only read when waiting for the - * backend to become quiescent. + * twice per operation by all worker threads, so it uses a striped counter + * to avoid contending on a single cache line; it is only read when waiting + * for the backend to become quiescent, which is why it is not a LongAdder — + * see {@link StripedCounter}. */ - private final LongAdder threadTotalCount = new LongAdder(); + private final StripedCounter threadTotalCount = new StripedCounter(); /** The base DNs defined for this backend instance. */ private Set baseDNs; diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java index 398d937a7a..1a54adf1a7 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java @@ -38,7 +38,6 @@ import java.util.NoSuchElementException; import java.util.Objects; import java.util.TreeMap; -import java.util.concurrent.atomic.LongAdder; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; @@ -355,12 +354,14 @@ public void run(WriteableTransaction txn) throws Exception * {@link #beginSharedAccess()}, so acquiring even the read side of the * ReentrantReadWriteLock becomes a cross-core hotspot under load: each * acquire and release CAS-es the single lock state word. The hot paths - * register through this LongAdder instead and only fall back to waiting - * when an exclusive locker has closed the gate; exclusive lockers (rare - * structural changes: index removal, configuration changes, close) close - * the gate through {@link #lock()} and drain in-flight accesses. + * register through this striped counter instead and only fall back to + * waiting when an exclusive locker has closed the gate; exclusive lockers + * (rare structural changes: index removal, configuration changes, close) + * close the gate through {@link #lock()} and drain in-flight accesses. The + * drain relies on {@link StripedCounter#sum()} never under-counting to a + * false zero, which is why this is not a LongAdder — see StripedCounter. */ - private final LongAdder sharedAccessCount = new LongAdder(); + private final StripedCounter sharedAccessCount = new StripedCounter(); /** True while an exclusive locker has closed the gate for lock-free shared access. */ private volatile boolean exclusiveAccessPending; /** Monitor used to park shared accessors while the gate is closed. */ @@ -368,8 +369,9 @@ public void run(WriteableTransaction txn) throws Exception /** * Begins a lock-free shared access to this entry container. Must be paired - * with {@link #endSharedAccess()} in a finally block. Equivalent to - * acquiring {@link #sharedLock}, but scales with the number of cores. + * with {@link #endSharedAccess()} in a finally block on the same thread. + * Equivalent to acquiring {@link #sharedLock}, but scales with the number + * of cores. */ void beginSharedAccess() { @@ -404,7 +406,10 @@ void beginSharedAccess() } } - /** Ends a lock-free shared access to this entry container. */ + /** + * Ends a lock-free shared access to this entry container. Must be called by + * the thread that did the paired {@link #beginSharedAccess()}. + */ void endSharedAccess() { sharedAccessCount.decrement(); diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/StripedCounter.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/StripedCounter.java new file mode 100644 index 0000000000..93e6113fac --- /dev/null +++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/StripedCounter.java @@ -0,0 +1,95 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions Copyright [year] [name of copyright owner]". + * + * Copyright 2026 3A Systems, LLC + */ +package org.opends.server.backends.pluggable; + +import java.util.concurrent.atomic.AtomicLongArray; + +/** + * A striped counter of in-flight accesses whose non-atomic {@link #sum()} scan + * is safe for quiescence detection: it may transiently over-estimate, but can + * never return zero while an access is still in flight. + *

+ * The increment and the matching decrement of one logical access must be + * performed by the same thread; the stripe is a pure function of the thread, + * so both land in the same slot. A scan reads each slot once, i.e. observes a + * prefix of each slot's modification history, and within one slot a decrement + * can never be observed without the increment that preceded it, so every + * per-slot subtotal is non-negative. {@link java.util.concurrent.atomic.LongAdder} + * does not provide this: the two halves of a pair may land in different cells + * (probe rehash after CAS contention, cell table growth), letting a scan + * observe the decrement while missing the increment and under-count to a + * false zero. + */ +final class StripedCounter +{ + /** 16 longs = 128 bytes between slots, to keep them on distinct cache lines. */ + private static final int SPACING = 16; + private static final int STRIPES = nextPowerOfTwo(Runtime.getRuntime().availableProcessors()); + + private final AtomicLongArray counts = new AtomicLongArray(STRIPES * SPACING); + + private static int nextPowerOfTwo(int n) + { + int p = 1; + while (p < n) + { + p <<= 1; + } + return p; + } + + private static int slot() + { + final long id = Thread.currentThread().getId(); + return (((int) ((id * 0x9E3779B97F4A7C15L) >>> 32)) & (STRIPES - 1)) * SPACING; + } + + void increment() + { + counts.getAndIncrement(slot()); + } + + /** Must be called by the same thread that did the paired {@link #increment()}. */ + void decrement() + { + counts.getAndDecrement(slot()); + } + + /** + * Returns the current count. Concurrent updates may cause over-estimation, + * but a paired increment/decrement is never observed half-way in the + * decrement-only direction, so the result is zero only if every access + * whose increment is visible has completed. + */ + long sum() + { + long s = 0; + for (int i = 0; i < counts.length(); i += SPACING) + { + s += counts.get(i); + } + return s; + } + + /** Resets the count to zero. Only safe when no accesses are in flight. */ + void reset() + { + for (int i = 0; i < counts.length(); i += SPACING) + { + counts.set(i, 0); + } + } +}