How to Achieve Thread‑Safe Random Selection from a Dynamically Changing List in Java

This article explains how to implement a thread‑safe random element picker for a list whose size changes during a performance test, covering the pitfalls of using list.size(), the use of a cached size with AtomicInteger, and a complete asynchronous example.

FunTester
FunTester
FunTester
How to Achieve Thread‑Safe Random Selection from a Dynamically Changing List in Java

Problem Statement

When a list of API endpoints is modified dynamically during a performance test (e.g., a QPS model that periodically changes scenario weights), the usual static‑ratio random selection becomes unsafe. The typical implementation directly calls list.size() to bound the random index, which can produce an IndexOutOfBoundsException or return null if the list is altered concurrently.

public static <F> F random(List<F> list) {
    if (list == null || list.isEmpty())
        ParamException.fail("Array cannot be empty!");
    return list.get(getRandomIntZero(list.size()));
}

Thread‑Safe Solution

1. Cache the list size in a thread‑safe container

Use java.util.concurrent.atomic.AtomicInteger (or a plain int with proper synchronization) to store the current size of the list. The cache is updated atomically whenever elements are added or removed.

static AtomicInteger size = new AtomicInteger();

2. Modify the list asynchronously

Run a background thread that periodically adds or removes elements. After each modification the cached size is adjusted with size.getAndAdd(delta). The order of operations matters:

When increasing the list, add elements to the list first, then increase the cached size.

When decreasing the list, decrease the cached size first, then remove elements from the list.

boolean upKey = false;
while (FunQpsConcurrent.key) {
    if (upKey) {
        // increase phase – repeat 10 times
        10.times {
            sleep(10.0);
            // remove "reduce" elements
            size.getAndAdd(-reduce);
            reduce.times { PriapiWriteApiQpsConfig.apiList.remove(13 as Integer) }
            // add "add" elements
            add.times { PriapiWriteApiQpsConfig.apiList.add(10) }
            size.getAndAdd(add);
        }
    } else {
        // decrease phase – repeat 10 times
        10.times {
            sleep(10.0);
            // add "reduce" elements first
            reduce.times { PriapiWriteApiQpsConfig.apiList.add(13 as Integer) }
            size.getAndAdd(reduce);
            // then remove "add" elements
            size.getAndAdd(-add);
            add.times { PriapiWriteApiQpsConfig.apiList.remove(10 as Integer) }
        }
    }
    upKey = !upKey; // flip direction after each interval
}

3. Random selection using the cached size

All threads that need a random endpoint call the method below. Because the size is read from the atomic cache, the index is always within the current bounds of the list.

PriapiWriteApiQpsConfig.apiList.get(getRandomIntZero(size.get()))

Key Observations

The cached size must be updated atomically to avoid race conditions.

Using AtomicInteger simplifies the implementation, but a synchronized int would also work.

The approach works under very high request rates; in tests up to 100 000 calls per second no null values or out‑of‑range indices were observed.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javarandom selection
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.