1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.lang3.concurrent;
19
20 import static org.junit.jupiter.api.Assertions.assertFalse;
21 import static org.junit.jupiter.api.Assertions.assertNotNull;
22 import static org.junit.jupiter.api.Assertions.assertTimeout;
23
24 import java.lang.management.ManagementFactory;
25 import java.lang.management.ThreadMXBean;
26 import java.time.Duration;
27 import java.util.ArrayList;
28 import java.util.List;
29 import java.util.concurrent.CountDownLatch;
30 import java.util.concurrent.ExecutorService;
31 import java.util.concurrent.Executors;
32 import java.util.concurrent.Future;
33 import java.util.concurrent.atomic.AtomicLong;
34
35 import org.junit.jupiter.api.Test;
36
37 /**
38 * AtomicSafeInitializer.get() spins in a while-loop without Thread.yield() or LockSupport.parkNanos() when the CAS fails (another thread is initializing).
39 *
40 * <p>
41 * Concurrent callers who lose the CAS busy-wait for the duration of initialize(), burning CPU proportional to init latency * thread count. A slow initializer
42 * combined with many concurrent callers use more CPU than it can.
43 * </p>
44 *
45 * <p>
46 * 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
47 * yield, keeping CPU near zero while waiting.
48 * </p>
49 */
50 class AtomicSafeInitializerInitTest {
51
52 /** Slow initializer: sleeps 100 ms to widen the spin window. */
53 private static final int INIT_MS = 100;
54 private static final int SPINNER_THREADS = 8;
55
56 private static long threadCpuTimeNanos() {
57 final ThreadMXBean mx = ManagementFactory.getThreadMXBean();
58 return mx.isCurrentThreadCpuTimeSupported() ? mx.getCurrentThreadCpuTime() : 0;
59 }
60
61 @Test
62 void testSpinningThreadsYieldDuringSlowInit() throws Exception {
63 final CountDownLatch startLatch = new CountDownLatch(1);
64 final AtomicLong totalCpuNanos = new AtomicLong();
65 final AtomicSafeInitializer<String> initializer = AtomicSafeInitializer.<String>builder().setInitializer(() -> {
66 Thread.sleep(INIT_MS);
67 return "done";
68 }).get();
69 final ExecutorService exec = Executors.newFixedThreadPool(SPINNER_THREADS + 1);
70 try {
71 final List<Future<?>> futures = new ArrayList<>();
72 for (int i = 0; i < SPINNER_THREADS; i++) {
73 futures.add(exec.submit(() -> {
74 try {
75 startLatch.await();
76 final long cpuBeforeNanos = threadCpuTimeNanos();
77 initializer.get();
78 totalCpuNanos.addAndGet(threadCpuTimeNanos() - cpuBeforeNanos);
79 } catch (final Exception e) {
80 Thread.currentThread().interrupt();
81 }
82 }));
83 }
84 startLatch.countDown();
85 for (final Future<?> f : futures) {
86 f.get();
87 }
88 } finally {
89 exec.shutdown();
90 }
91 assertNotNull(initializer.get());
92 // Post-patch: CPU consumed by spinner threads during 100 ms init must be
93 // significantly less than INIT_MS per thread. We allow 50 ms total CPU
94 // across all spinner threads (vs. ~800 ms if spinning at 100%).
95 // This threshold is conservative, a yielding implementation uses ~0 ms.
96 final long cpuMs = totalCpuNanos.get() / 1_000_000;
97 // Re-express as a blocking assertion: if cpuMs > threshold, fail.
98 assertTimeout(Duration.ofMillis(INIT_MS * SPINNER_THREADS / 4), () -> assertFalse(cpuMs > INIT_MS * SPINNER_THREADS / 4,
99 () -> "Spinner threads consumed " + cpuMs + " ms CPU during " + INIT_MS + " ms init â missing Thread.yield() in get() spin loop"));
100 }
101 }