| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * https://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.commons.lang3.concurrent; |
| |
| import static org.junit.jupiter.api.Assertions.assertFalse; |
| import static org.junit.jupiter.api.Assertions.assertNotNull; |
| import static org.junit.jupiter.api.Assertions.assertTimeout; |
| |
| import java.lang.management.ManagementFactory; |
| import java.lang.management.ThreadMXBean; |
| import java.time.Duration; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| import java.util.concurrent.Future; |
| import java.util.concurrent.atomic.AtomicLong; |
| |
| import org.junit.jupiter.api.Test; |
| |
| /** |
| * AtomicSafeInitializer.get() spins in a while-loop without Thread.yield() or LockSupport.parkNanos() when the CAS fails (another thread is initializing). |
| * |
| * <p> |
| * Concurrent callers who lose the CAS busy-wait for the duration of initialize(), burning CPU proportional to init latency * thread count. A slow initializer |
| * combined with many concurrent callers use more CPU than it can. |
| * </p> |
| * |
| * <p> |
| * This test measures CPU time spent in spinning threads during a 100 ms init. Pre-patch: spinning threads consume significant CPU. Post-patch: spinning threads |
| * yield, keeping CPU near zero while waiting. |
| * </p> |
| */ |
| class AtomicSafeInitializerInitTest { |
| |
| /** Slow initializer: sleeps 100 ms to widen the spin window. */ |
| private static final int INIT_MS = 100; |
| private static final int SPINNER_THREADS = 8; |
| |
| private static long threadCpuTimeNanos() { |
| final ThreadMXBean mx = ManagementFactory.getThreadMXBean(); |
| return mx.isCurrentThreadCpuTimeSupported() ? mx.getCurrentThreadCpuTime() : 0; |
| } |
| |
| @Test |
| void testSpinningThreadsYieldDuringSlowInit() throws Exception { |
| final CountDownLatch startLatch = new CountDownLatch(1); |
| final AtomicLong totalCpuNanos = new AtomicLong(); |
| final AtomicSafeInitializer<String> initializer = AtomicSafeInitializer.<String>builder().setInitializer(() -> { |
| Thread.sleep(INIT_MS); |
| return "done"; |
| }).get(); |
| final ExecutorService exec = Executors.newFixedThreadPool(SPINNER_THREADS + 1); |
| try { |
| final List<Future<?>> futures = new ArrayList<>(); |
| for (int i = 0; i < SPINNER_THREADS; i++) { |
| futures.add(exec.submit(() -> { |
| try { |
| startLatch.await(); |
| final long cpuBeforeNanos = threadCpuTimeNanos(); |
| initializer.get(); |
| totalCpuNanos.addAndGet(threadCpuTimeNanos() - cpuBeforeNanos); |
| } catch (final Exception e) { |
| Thread.currentThread().interrupt(); |
| } |
| })); |
| } |
| startLatch.countDown(); |
| for (final Future<?> f : futures) { |
| f.get(); |
| } |
| } finally { |
| exec.shutdown(); |
| } |
| assertNotNull(initializer.get()); |
| // Post-patch: CPU consumed by spinner threads during 100 ms init must be |
| // significantly less than INIT_MS per thread. We allow 50 ms total CPU |
| // across all spinner threads (vs. ~800 ms if spinning at 100%). |
| // This threshold is conservative, a yielding implementation uses ~0 ms. |
| final long cpuMs = totalCpuNanos.get() / 1_000_000; |
| // Re-express as a blocking assertion: if cpuMs > threshold, fail. |
| assertTimeout(Duration.ofMillis(INIT_MS * SPINNER_THREADS / 4), () -> assertFalse(cpuMs > INIT_MS * SPINNER_THREADS / 4, |
| () -> "Spinner threads consumed " + cpuMs + " ms CPU during " + INIT_MS + " ms init — missing Thread.yield() in get() spin loop")); |
| } |
| } |