View Javadoc
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 }