This is an automated email from the ASF dual-hosted git repository.

aherbert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-rng.git

commit 0021eb92a56dd5dc61e63256fb2a30c316c7d8af
Author: Alex Herbert <aherb...@apache.org>
AuthorDate: Wed Sep 7 13:08:56 2022 +0100

    RNG-181: LXM family to support SplittableUniformRandomProvider
    
    Create generic RandomStreams class that can stream objects created with
    a seed and splittable source of randomness. The seed uses the stream
    position mixed with random bits to ensure it is unique within the stream
    (up to a size limit of 2^60). Use this feature to support splits in the
    LXM family.
    
    Add isSplittable method to RandomSource to use to identify supported
    interfaces.
    
    Use of the splittable interface by other modules requires an exception
    in RevAPI for exposing an external class in the API. This is similar to
    exposure of UniformRandomProvider and is allowed. The change is
    non-breaking for binary and source compatibility (see revapi
    java.class.externalClassExposedInAPI).
---
 .../org/apache/commons/rng/core/BaseProvider.java  |   8 +-
 .../commons/rng/core/source32/L32X64Mix.java       |  51 ++-
 .../commons/rng/core/source32/LXMSupport.java      |   8 +
 .../commons/rng/core/source64/L128X1024Mix.java    |  60 ++-
 .../commons/rng/core/source64/L128X128Mix.java     |  52 ++-
 .../commons/rng/core/source64/L128X256Mix.java     |  57 ++-
 .../commons/rng/core/source64/L64X1024Mix.java     |  60 ++-
 .../commons/rng/core/source64/L64X128Mix.java      |  51 ++-
 .../commons/rng/core/source64/L64X128StarStar.java |  43 +-
 .../commons/rng/core/source64/L64X256Mix.java      |  54 ++-
 .../commons/rng/core/source64/LXMSupport.java      |   8 +
 .../commons/rng/core/util/RandomStreams.java       | 278 +++++++++++++
 .../org/apache/commons/rng/core/ProvidersList.java |  18 +-
 .../org/apache/commons/rng/core/RandomAssert.java  |  34 ++
 .../core/SplittableProvidersParametricTest.java    | 359 +++++++++++++++++
 .../commons/rng/core/source32/L32X64MixTest.java   |  28 ++
 .../rng/core/source64/L128X1024MixTest.java        |  29 ++
 .../commons/rng/core/source64/L128X128MixTest.java |  28 ++
 .../commons/rng/core/source64/L128X256MixTest.java |  28 ++
 .../commons/rng/core/source64/L64X1024MixTest.java |  29 ++
 .../commons/rng/core/source64/L64X128MixTest.java  |  28 ++
 .../rng/core/source64/L64X128StarStarTest.java     |  28 ++
 .../commons/rng/core/source64/L64X256MixTest.java  |  28 ++
 .../commons/rng/core/util/RandomStreamsTest.java   | 448 +++++++++++++++++++++
 .../rng/core/util/RandomStreamsTestHelper.java     |  39 ++
 .../apache/commons/rng/simple/RandomSource.java    |  23 ++
 .../rng/simple/ProvidersCommonParametricTest.java  |   4 +
 .../commons/rng/simple/RandomSourceTest.java       |   7 +
 src/main/resources/pmd/pmd-ruleset.xml             |  10 +-
 src/main/resources/revapi/api-changes.json         |   6 +
 30 files changed, 1892 insertions(+), 12 deletions(-)

diff --git 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/BaseProvider.java 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/BaseProvider.java
index e78dbea2..164258fa 100644
--- 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/BaseProvider.java
+++ 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/BaseProvider.java
@@ -404,9 +404,12 @@ public abstract class BaseProvider
      *
      * <p>This is ranked first of the top 14 Stafford mixers.
      *
+     * <p>This function can be used to mix the bits of a {@code long} value to
+     * obtain a better distribution and avoid collisions between similar 
values.
+     *
      * @param x the input value
      * @return the output value
-     * @see <a 
href="http://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html";>Better
+     * @see <a 
href="https://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html";>Better
      *      Bit Mixing - Improving on MurmurHash3&#39;s 64-bit Finalizer.</a>
      */
     private static long stafford13(long x) {
@@ -418,6 +421,9 @@ public abstract class BaseProvider
     /**
      * Perform the finalising 32-bit mix function of Austin Appleby's 
MurmurHash3.
      *
+     * <p>This function can be used to mix the bits of a {@code int} value to
+     * obtain a better distribution and avoid collisions between similar 
values.
+     *
      * @param x the input value
      * @return the output value
      * @see <a href="https://github.com/aappleby/smhasher";>SMHasher</a>
diff --git 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source32/L32X64Mix.java
 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source32/L32X64Mix.java
index 19c6b982..cb21433f 100644
--- 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source32/L32X64Mix.java
+++ 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source32/L32X64Mix.java
@@ -17,10 +17,13 @@
 
 package org.apache.commons.rng.core.source32;
 
+import java.util.stream.Stream;
 import org.apache.commons.rng.JumpableUniformRandomProvider;
 import org.apache.commons.rng.LongJumpableUniformRandomProvider;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
 import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.core.util.NumberFactory;
+import org.apache.commons.rng.core.util.RandomStreams;
 
 /**
  * A 32-bit all purpose generator.
@@ -41,13 +44,22 @@ import org.apache.commons.rng.core.util.NumberFactory;
  * against accidental correlation in a multi-threaded setting. The additive 
parameters must be
  * different in the most significant 31-bits.
  *
+ * <p>This generator implements
+ * {@link org.apache.commons.rng.SplittableUniformRandomProvider 
SplittableUniformRandomProvider}.
+ * The stream of generators created using the {@code splits} methods support 
parallelisation
+ * and are robust against accidental correlation by using unique values for 
the additive parameter
+ * for each instance in the same stream. The primitive streaming methods 
support parallelisation
+ * but with no assurances of accidental correlation; each thread uses a new 
instance with a
+ * randomly initialised state.
+ *
  * @see <a href="https://doi.org/10.1145/3485525";>Steele &amp; Vigna (2021) 
Proc. ACM Programming
  *      Languages 5, 1-31</a>
  * @see <a 
href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/random/package-summary.html";>
  *      JDK 17 java.util.random javadoc</a>
  * @since 1.5
  */
-public final class L32X64Mix extends IntProvider implements 
LongJumpableUniformRandomProvider {
+public final class L32X64Mix extends IntProvider implements 
LongJumpableUniformRandomProvider,
+    SplittableUniformRandomProvider {
     // Implementation note:
     // This does not extend AbstractXoRoShiRo64 as the XBG function is 
re-implemented
     // inline to allow parallel pipelining. Inheritance would provide only the 
XBG state.
@@ -213,4 +225,41 @@ public final class L32X64Mix extends IntProvider 
implements LongJumpableUniformR
         resetCachedState();
         return copy;
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public SplittableUniformRandomProvider split(UniformRandomProvider source) 
{
+        // The upper half of the long seed is discarded so use nextInt
+        return create(source.nextInt(), source);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Stream<SplittableUniformRandomProvider> splits(long streamSize, 
SplittableUniformRandomProvider source) {
+        return RandomStreams.generateWithSeed(streamSize, source, 
L32X64Mix::create);
+    }
+
+    /**
+     * Create a new instance using the given {@code seed} and {@code source} 
of randomness
+     * to initialise the instance.
+     *
+     * @param seed Seed used to initialise the instance.
+     * @param source Source of randomness used to initialise the instance.
+     * @return A new instance.
+     */
+    private static SplittableUniformRandomProvider create(long seed, 
UniformRandomProvider source) {
+        // LCG state. The addition uses the input seed.
+        // The LCG addition parameter is set to odd so left-shift the seed.
+        final int s0 = (int) seed << 1;
+        final int s1 = source.nextInt();
+        // XBG state must not be all zero
+        int x0 = source.nextInt();
+        int x1 = source.nextInt();
+        if ((x0 | x1) == 0) {
+            // SplitMix style seed ensures at least one non-zero value
+            x0 = LXMSupport.lea32(s1);
+            x1 = LXMSupport.lea32(s1 + LXMSupport.GOLDEN_RATIO_32);
+        }
+        return new L32X64Mix(s0, s1, x0, x1);
+    }
 }
diff --git 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source32/LXMSupport.java
 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source32/LXMSupport.java
index 290bb3d9..94e893c8 100644
--- 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source32/LXMSupport.java
+++ 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source32/LXMSupport.java
@@ -48,6 +48,14 @@ final class LXMSupport {
      * </pre>
      */
     static final int C32P = 0x046b0000;
+    /**
+     * The fractional part of the golden ratio, phi, scaled to 32-bits and 
rounded to odd.
+     * <pre>
+     * phi = (sqrt(5) - 1) / 2) * 2^32
+     * </pre>
+     * @see <a href="https://en.wikipedia.org/wiki/Golden_ratio";>Golden 
ratio</a>
+     */
+    static final int GOLDEN_RATIO_32 = 0x9e3779b9;
 
     /** No instances. */
     private LXMSupport() {}
diff --git 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L128X1024Mix.java
 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L128X1024Mix.java
index 0417026c..d9bdeabf 100644
--- 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L128X1024Mix.java
+++ 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L128X1024Mix.java
@@ -17,9 +17,12 @@
 
 package org.apache.commons.rng.core.source64;
 
+import java.util.stream.Stream;
 import org.apache.commons.rng.JumpableUniformRandomProvider;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
 import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.core.util.NumberFactory;
+import org.apache.commons.rng.core.util.RandomStreams;
 
 /**
  * A 64-bit all purpose generator.
@@ -41,17 +44,27 @@ import org.apache.commons.rng.core.util.NumberFactory;
  * against accidental correlation in a multi-threaded setting. The additive 
parameters must be
  * different in the most significant 127-bits.
  *
+ * <p>This generator implements
+ * {@link org.apache.commons.rng.SplittableUniformRandomProvider 
SplittableUniformRandomProvider}.
+ * The stream of generators created using the {@code splits} methods support 
parallelisation
+ * and are robust against accidental correlation by using unique values for 
the additive parameter
+ * for each instance in the same stream. The primitive streaming methods 
support parallelisation
+ * but with no assurances of accidental correlation; each thread uses a new 
instance with a
+ * randomly initialised state.
+ *
  * @see <a href="https://doi.org/10.1145/3485525";>Steele &amp; Vigna (2021) 
Proc. ACM Programming
  *      Languages 5, 1-31</a>
  * @see <a 
href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/random/package-summary.html";>
  *      JDK 17 java.util.random javadoc</a>
  * @since 1.5
  */
-public class L128X1024Mix extends AbstractL128 {
+public class L128X1024Mix extends AbstractL128 implements 
SplittableUniformRandomProvider {
     /** Size of the seed vector. */
     private static final int SEED_SIZE = 20;
     /** Size of the XBG state vector. */
     private static final int XBG_STATE_SIZE = 16;
+    /** Size of the LCG state vector. */
+    private static final int LCG_STATE_SIZE = SEED_SIZE - XBG_STATE_SIZE;
     /** Low half of 128-bit LCG multiplier. */
     private static final long ML = LXMSupport.M128L;
 
@@ -185,4 +198,49 @@ public class L128X1024Mix extends AbstractL128 {
         // the correct class type. It should not be public.
         return new L128X1024Mix(this);
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public SplittableUniformRandomProvider split(UniformRandomProvider source) 
{
+        return create(source.nextLong(), source);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Stream<SplittableUniformRandomProvider> splits(long streamSize, 
SplittableUniformRandomProvider source) {
+        return RandomStreams.generateWithSeed(streamSize, source, 
L128X1024Mix::create);
+    }
+
+    /**
+     * Create a new instance using the given {@code seed} and {@code source} 
of randomness
+     * to initialise the instance.
+     *
+     * @param seed Seed used to initialise the instance.
+     * @param source Source of randomness used to initialise the instance.
+     * @return A new instance.
+     */
+    private static SplittableUniformRandomProvider create(long seed, 
UniformRandomProvider source) {
+        final long[] s = new long[SEED_SIZE];
+        // LCG state. The addition lower-half uses the input seed.
+        // The LCG addition parameter is set to odd so left-shift the seed.
+        s[0] = source.nextLong();
+        s[1] = seed << 1;
+        s[2] = source.nextLong();
+        s[3] = source.nextLong();
+        // XBG state must not be all zero
+        long x = 0;
+        for (int i = LCG_STATE_SIZE; i < s.length; i++) {
+            s[i] = source.nextLong();
+            x |= s[i];
+        }
+        if (x == 0) {
+            // SplitMix style seed ensures at least one non-zero value
+            x = s[LCG_STATE_SIZE - 1];
+            for (int i = LCG_STATE_SIZE; i < s.length; i++) {
+                s[i] = LXMSupport.lea64(x);
+                x += LXMSupport.GOLDEN_RATIO_64;
+            }
+        }
+        return new L128X1024Mix(s);
+    }
 }
diff --git 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L128X128Mix.java
 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L128X128Mix.java
index 8d905f24..8938ba52 100644
--- 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L128X128Mix.java
+++ 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L128X128Mix.java
@@ -17,9 +17,12 @@
 
 package org.apache.commons.rng.core.source64;
 
+import java.util.stream.Stream;
 import org.apache.commons.rng.JumpableUniformRandomProvider;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
 import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.core.util.NumberFactory;
+import org.apache.commons.rng.core.util.RandomStreams;
 
 /**
  * A 64-bit all purpose generator.
@@ -41,13 +44,21 @@ import org.apache.commons.rng.core.util.NumberFactory;
  * against accidental correlation in a multi-threaded setting. The additive 
parameters must be
  * different in the most significant 127-bits.
  *
+ * <p>This generator implements
+ * {@link org.apache.commons.rng.SplittableUniformRandomProvider 
SplittableUniformRandomProvider}.
+ * The stream of generators created using the {@code splits} methods support 
parallelisation
+ * and are robust against accidental correlation by using unique values for 
the additive parameter
+ * for each instance in the same stream. The primitive streaming methods 
support parallelisation
+ * but with no assurances of accidental correlation; each thread uses a new 
instance with a
+ * randomly initialised state.
+ *
  * @see <a href="https://doi.org/10.1145/3485525";>Steele &amp; Vigna (2021) 
Proc. ACM Programming
  *      Languages 5, 1-31</a>
  * @see <a 
href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/random/package-summary.html";>
  *      JDK 17 java.util.random javadoc</a>
  * @since 1.5
  */
-public class L128X128Mix extends AbstractL128 {
+public class L128X128Mix extends AbstractL128 implements 
SplittableUniformRandomProvider {
     /** Size of the seed vector. */
     private static final int SEED_SIZE = 6;
     /** Size of the XBG state vector. */
@@ -202,4 +213,43 @@ public class L128X128Mix extends AbstractL128 {
         // the correct class type. It should not be public.
         return new L128X128Mix(this);
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public SplittableUniformRandomProvider split(UniformRandomProvider source) 
{
+        return create(source.nextLong(), source);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Stream<SplittableUniformRandomProvider> splits(long streamSize, 
SplittableUniformRandomProvider source) {
+        return RandomStreams.generateWithSeed(streamSize, source, 
L128X128Mix::create);
+    }
+
+    /**
+     * Create a new instance using the given {@code seed} and {@code source} 
of randomness
+     * to initialise the instance.
+     *
+     * @param seed Seed used to initialise the instance.
+     * @param source Source of randomness used to initialise the instance.
+     * @return A new instance.
+     */
+    private static SplittableUniformRandomProvider create(long seed, 
UniformRandomProvider source) {
+        // LCG state. The addition lower-half uses the input seed.
+        // The LCG addition parameter is set to odd so left-shift the seed.
+        final long s0 = source.nextLong();
+        final long s1 = seed << 1;
+        final long s2 = source.nextLong();
+        final long s3 = source.nextLong();
+        // XBG state must not be all zero
+        long x0 = source.nextLong();
+        long x1 = source.nextLong();
+        if ((x0 | x1) == 0) {
+            // SplitMix style seed ensures at least one non-zero value
+            final long z = s3;
+            x0 = LXMSupport.lea64(z);
+            x1 = LXMSupport.lea64(z + LXMSupport.GOLDEN_RATIO_64);
+        }
+        return new L128X128Mix(s0, s1, s2, s3, x0, x1);
+    }
 }
diff --git 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L128X256Mix.java
 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L128X256Mix.java
index 16a1b0a3..c860b429 100644
--- 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L128X256Mix.java
+++ 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L128X256Mix.java
@@ -17,9 +17,12 @@
 
 package org.apache.commons.rng.core.source64;
 
+import java.util.stream.Stream;
 import org.apache.commons.rng.JumpableUniformRandomProvider;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
 import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.core.util.NumberFactory;
+import org.apache.commons.rng.core.util.RandomStreams;
 
 /**
  * A 64-bit all purpose generator.
@@ -41,13 +44,21 @@ import org.apache.commons.rng.core.util.NumberFactory;
  * against accidental correlation in a multi-threaded setting. The additive 
parameters must be
  * different in the most significant 127-bits.
  *
+ * <p>This generator implements
+ * {@link org.apache.commons.rng.SplittableUniformRandomProvider 
SplittableUniformRandomProvider}.
+ * The stream of generators created using the {@code splits} methods support 
parallelisation
+ * and are robust against accidental correlation by using unique values for 
the additive parameter
+ * for each instance in the same stream. The primitive streaming methods 
support parallelisation
+ * but with no assurances of accidental correlation; each thread uses a new 
instance with a
+ * randomly initialised state.
+ *
  * @see <a href="https://doi.org/10.1145/3485525";>Steele &amp; Vigna (2021) 
Proc. ACM Programming
  *      Languages 5, 1-31</a>
  * @see <a 
href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/random/package-summary.html";>
  *      JDK 17 java.util.random javadoc</a>
  * @since 1.5
  */
-public class L128X256Mix extends AbstractL128 {
+public class L128X256Mix extends AbstractL128 implements 
SplittableUniformRandomProvider {
     /** Size of the seed vector. */
     private static final int SEED_SIZE = 8;
     /** Size of the XBG state vector. */
@@ -230,4 +241,48 @@ public class L128X256Mix extends AbstractL128 {
         // the correct class type. It should not be public.
         return new L128X256Mix(this);
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public SplittableUniformRandomProvider split(UniformRandomProvider source) 
{
+        return create(source.nextLong(), source);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Stream<SplittableUniformRandomProvider> splits(long streamSize, 
SplittableUniformRandomProvider source) {
+        return RandomStreams.generateWithSeed(streamSize, source, 
L128X256Mix::create);
+    }
+
+    /**
+     * Create a new instance using the given {@code seed} and {@code source} 
of randomness
+     * to initialise the instance.
+     *
+     * @param seed Seed used to initialise the instance.
+     * @param source Source of randomness used to initialise the instance.
+     * @return A new instance.
+     */
+    private static SplittableUniformRandomProvider create(long seed, 
UniformRandomProvider source) {
+        // LCG state. The addition lower-half uses the input seed.
+        // The LCG addition parameter is set to odd so left-shift the seed.
+        final long s0 = source.nextLong();
+        final long s1 = seed << 1;
+        final long s2 = source.nextLong();
+        final long s3 = source.nextLong();
+        // XBG state must not be all zero
+        long x0 = source.nextLong();
+        long x1 = source.nextLong();
+        long x2 = source.nextLong();
+        long x3 = source.nextLong();
+        if ((x0 | x1 | x2 | x3) == 0) {
+            // SplitMix style seed ensures at least one non-zero value
+            long z = s3;
+            x0 = LXMSupport.lea64(z);
+            x1 = LXMSupport.lea64(z += LXMSupport.GOLDEN_RATIO_64);
+            x2 = LXMSupport.lea64(z += LXMSupport.GOLDEN_RATIO_64);
+            x3 = LXMSupport.lea64(z + LXMSupport.GOLDEN_RATIO_64);
+        }
+        // The LCG addition parameter is set to odd so left-shift the seed
+        return new L128X256Mix(s0, s1, s2, s3, x0, x1, x2, x3);
+    }
 }
diff --git 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L64X1024Mix.java
 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L64X1024Mix.java
index d78b0f80..5b4373bb 100644
--- 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L64X1024Mix.java
+++ 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L64X1024Mix.java
@@ -17,9 +17,12 @@
 
 package org.apache.commons.rng.core.source64;
 
+import java.util.stream.Stream;
 import org.apache.commons.rng.JumpableUniformRandomProvider;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
 import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.core.util.NumberFactory;
+import org.apache.commons.rng.core.util.RandomStreams;
 
 /**
  * A 64-bit all purpose generator.
@@ -41,17 +44,27 @@ import org.apache.commons.rng.core.util.NumberFactory;
  * against accidental correlation in a multi-threaded setting. The additive 
parameters must be
  * different in the most significant 63-bits.
  *
+ * <p>This generator implements
+ * {@link org.apache.commons.rng.SplittableUniformRandomProvider 
SplittableUniformRandomProvider}.
+ * The stream of generators created using the {@code splits} methods support 
parallelisation
+ * and are robust against accidental correlation by using unique values for 
the additive parameter
+ * for each instance in the same stream. The primitive streaming methods 
support parallelisation
+ * but with no assurances of accidental correlation; each thread uses a new 
instance with a
+ * randomly initialised state.
+ *
  * @see <a href="https://doi.org/10.1145/3485525";>Steele &amp; Vigna (2021) 
Proc. ACM Programming
  *      Languages 5, 1-31</a>
  * @see <a 
href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/random/package-summary.html";>
  *      JDK 17 java.util.random javadoc</a>
  * @since 1.5
  */
-public class L64X1024Mix extends AbstractL64 {
+public class L64X1024Mix extends AbstractL64 implements 
SplittableUniformRandomProvider {
     /** Size of the seed vector. */
     private static final int SEED_SIZE = 18;
-    /** Size of the state vector. */
+    /** Size of the XBG state vector. */
     private static final int XBG_STATE_SIZE = 16;
+    /** Size of the LCG state vector. */
+    private static final int LCG_STATE_SIZE = SEED_SIZE - XBG_STATE_SIZE;
     /** LCG multiplier. */
     private static final long M = LXMSupport.M64;
 
@@ -176,4 +189,47 @@ public class L64X1024Mix extends AbstractL64 {
         // the correct class type. It should not be public.
         return new L64X1024Mix(this);
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public SplittableUniformRandomProvider split(UniformRandomProvider source) 
{
+        return create(source.nextLong(), source);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Stream<SplittableUniformRandomProvider> splits(long streamSize, 
SplittableUniformRandomProvider source) {
+        return RandomStreams.generateWithSeed(streamSize, source, 
L64X1024Mix::create);
+    }
+
+    /**
+     * Create a new instance using the given {@code seed} and {@code source} 
of randomness
+     * to initialise the instance.
+     *
+     * @param seed Seed used to initialise the instance.
+     * @param source Source of randomness used to initialise the instance.
+     * @return A new instance.
+     */
+    private static SplittableUniformRandomProvider create(long seed, 
UniformRandomProvider source) {
+        final long[] s = new long[SEED_SIZE];
+        // LCG state. The addition uses the input seed.
+        // The LCG addition parameter is set to odd so left-shift the seed.
+        s[0] = seed << 1;
+        s[1] = source.nextLong();
+        // XBG state must not be all zero
+        long x = 0;
+        for (int i = LCG_STATE_SIZE; i < s.length; i++) {
+            s[i] = source.nextLong();
+            x |= s[i];
+        }
+        if (x == 0) {
+            // SplitMix style seed ensures at least one non-zero value
+            x = s[LCG_STATE_SIZE - 1];
+            for (int i = LCG_STATE_SIZE; i < s.length; i++) {
+                s[i] = LXMSupport.lea64(x);
+                x += LXMSupport.GOLDEN_RATIO_64;
+            }
+        }
+        return new L64X1024Mix(s);
+    }
 }
diff --git 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L64X128Mix.java
 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L64X128Mix.java
index 0555f1fd..2ca27d23 100644
--- 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L64X128Mix.java
+++ 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L64X128Mix.java
@@ -17,6 +17,11 @@
 
 package org.apache.commons.rng.core.source64;
 
+import java.util.stream.Stream;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.core.util.RandomStreams;
+
 /**
  * A 64-bit all purpose generator.
  *
@@ -37,13 +42,21 @@ package org.apache.commons.rng.core.source64;
  * against accidental correlation in a multi-threaded setting. The additive 
parameters must be
  * different in the most significant 63-bits.
  *
+ * <p>This generator implements
+ * {@link org.apache.commons.rng.SplittableUniformRandomProvider 
SplittableUniformRandomProvider}.
+ * The stream of generators created using the {@code splits} methods support 
parallelisation
+ * and are robust against accidental correlation by using unique values for 
the additive parameter
+ * for each instance in the same stream. The primitive streaming methods 
support parallelisation
+ * but with no assurances of accidental correlation; each thread uses a new 
instance with a
+ * randomly initialised state.
+ *
  * @see <a href="https://doi.org/10.1145/3485525";>Steele &amp; Vigna (2021) 
Proc. ACM Programming
  *      Languages 5, 1-31</a>
  * @see <a 
href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/random/package-summary.html";>
  *      JDK 17 java.util.random javadoc</a>
  * @since 1.5
  */
-public class L64X128Mix extends AbstractL64X128 {
+public class L64X128Mix extends AbstractL64X128 implements 
SplittableUniformRandomProvider {
     /**
      * Creates a new instance.
      *
@@ -123,4 +136,40 @@ public class L64X128Mix extends AbstractL64X128 {
         // the correct class type. It should not be public.
         return new L64X128Mix(this);
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public SplittableUniformRandomProvider split(UniformRandomProvider source) 
{
+        return create(source.nextLong(), source);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Stream<SplittableUniformRandomProvider> splits(long streamSize, 
SplittableUniformRandomProvider source) {
+        return RandomStreams.generateWithSeed(streamSize, source, 
L64X128Mix::create);
+    }
+
+    /**
+     * Create a new instance using the given {@code seed} and {@code source} 
of randomness
+     * to initialise the instance.
+     *
+     * @param seed Seed used to initialise the instance.
+     * @param source Source of randomness used to initialise the instance.
+     * @return A new instance.
+     */
+    private static SplittableUniformRandomProvider create(long seed, 
UniformRandomProvider source) {
+        // LCG state. The addition uses the input seed.
+        // The LCG addition parameter is set to odd so left-shift the seed.
+        final long s0 = seed << 1;
+        final long s1 = source.nextLong();
+        // XBG state must not be all zero
+        long x0 = source.nextLong();
+        long x1 = source.nextLong();
+        if ((x0 | x1) == 0) {
+            // SplitMix style seed ensures at least one non-zero value
+            x0 = LXMSupport.lea64(s1);
+            x1 = LXMSupport.lea64(s1 + LXMSupport.GOLDEN_RATIO_64);
+        }
+        return new L64X128Mix(s0, s1, x0, x1);
+    }
 }
diff --git 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L64X128StarStar.java
 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L64X128StarStar.java
index e949ff7b..dc56fa2d 100644
--- 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L64X128StarStar.java
+++ 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L64X128StarStar.java
@@ -17,6 +17,11 @@
 
 package org.apache.commons.rng.core.source64;
 
+import java.util.stream.Stream;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.core.util.RandomStreams;
+
 /**
  * A 64-bit all purpose generator.
  *
@@ -43,7 +48,7 @@ package org.apache.commons.rng.core.source64;
  *      JDK 17 java.util.random javadoc</a>
  * @since 1.5
  */
-public class L64X128StarStar extends AbstractL64X128 {
+public class L64X128StarStar extends AbstractL64X128 implements 
SplittableUniformRandomProvider {
     /**
      * Creates a new instance.
      *
@@ -123,4 +128,40 @@ public class L64X128StarStar extends AbstractL64X128 {
         // the correct class type. It should not be public.
         return new L64X128StarStar(this);
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public SplittableUniformRandomProvider split(UniformRandomProvider source) 
{
+        return create(source.nextLong(), source);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Stream<SplittableUniformRandomProvider> splits(long streamSize, 
SplittableUniformRandomProvider source) {
+        return RandomStreams.generateWithSeed(streamSize, source, 
L64X128StarStar::create);
+    }
+
+    /**
+     * Create a new instance using the given {@code seed} and {@code source} 
of randomness
+     * to initialise the instance.
+     *
+     * @param seed Seed used to initialise the instance.
+     * @param source Source of randomness used to initialise the instance.
+     * @return A new instance.
+     */
+    private static SplittableUniformRandomProvider create(long seed, 
UniformRandomProvider source) {
+        // LCG state. The addition uses the input seed.
+        // The LCG addition parameter is set to odd so left-shift the seed.
+        final long s0 = seed << 1;
+        final long s1 = source.nextLong();
+        // XBG state must not be all zero
+        long x0 = source.nextLong();
+        long x1 = source.nextLong();
+        if ((x0 | x1) == 0) {
+            // SplitMix style seed ensures at least one non-zero value
+            x0 = LXMSupport.lea64(s1);
+            x1 = LXMSupport.lea64(s1 + LXMSupport.GOLDEN_RATIO_64);
+        }
+        return new L64X128StarStar(s0, s1, x0, x1);
+    }
 }
diff --git 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L64X256Mix.java
 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L64X256Mix.java
index 61ad84d2..9cc36443 100644
--- 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L64X256Mix.java
+++ 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/L64X256Mix.java
@@ -17,9 +17,12 @@
 
 package org.apache.commons.rng.core.source64;
 
+import java.util.stream.Stream;
 import org.apache.commons.rng.JumpableUniformRandomProvider;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
 import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.core.util.NumberFactory;
+import org.apache.commons.rng.core.util.RandomStreams;
 
 /**
  * A 64-bit all purpose generator.
@@ -41,13 +44,21 @@ import org.apache.commons.rng.core.util.NumberFactory;
  * against accidental correlation in a multi-threaded setting. The additive 
parameters must be
  * different in the most significant 63-bits.
  *
+ * <p>This generator implements
+ * {@link org.apache.commons.rng.SplittableUniformRandomProvider 
SplittableUniformRandomProvider}.
+ * The stream of generators created using the {@code splits} methods support 
parallelisation
+ * and are robust against accidental correlation by using unique values for 
the additive parameter
+ * for each instance in the same stream. The primitive streaming methods 
support parallelisation
+ * but with no assurances of accidental correlation; each thread uses a new 
instance with a
+ * randomly initialised state.
+ *
  * @see <a href="https://doi.org/10.1145/3485525";>Steele &amp; Vigna (2021) 
Proc. ACM Programming
  *      Languages 5, 1-31</a>
  * @see <a 
href="https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/random/package-summary.html";>
  *      JDK 17 java.util.random javadoc</a>
  * @since 1.5
  */
-public class L64X256Mix extends AbstractL64 {
+public class L64X256Mix extends AbstractL64 implements 
SplittableUniformRandomProvider {
     /** Size of the seed vector. */
     private static final int SEED_SIZE = 6;
     /** Size of the XBG state vector. */
@@ -219,4 +230,45 @@ public class L64X256Mix extends AbstractL64 {
         // the correct class type. It should not be public.
         return new L64X256Mix(this);
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public SplittableUniformRandomProvider split(UniformRandomProvider source) 
{
+        return create(source.nextLong(), source);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Stream<SplittableUniformRandomProvider> splits(long streamSize, 
SplittableUniformRandomProvider source) {
+        return RandomStreams.generateWithSeed(streamSize, source, 
L64X256Mix::create);
+    }
+
+    /**
+     * Create a new instance using the given {@code seed} and {@code source} 
of randomness
+     * to initialise the instance.
+     *
+     * @param seed Seed used to initialise the instance.
+     * @param source Source of randomness used to initialise the instance.
+     * @return A new instance.
+     */
+    private static SplittableUniformRandomProvider create(long seed, 
UniformRandomProvider source) {
+        // LCG state. The addition uses the input seed.
+        // The LCG addition parameter is set to odd so left-shift the seed.
+        final long s0 = seed << 1;
+        final long s1 = source.nextLong();
+        // XBG state must not be all zero
+        long x0 = source.nextLong();
+        long x1 = source.nextLong();
+        long x2 = source.nextLong();
+        long x3 = source.nextLong();
+        if ((x0 | x1 | x2 | x3) == 0) {
+            // SplitMix style seed ensures at least one non-zero value
+            long z = s1;
+            x0 = LXMSupport.lea64(z);
+            x1 = LXMSupport.lea64(z += LXMSupport.GOLDEN_RATIO_64);
+            x2 = LXMSupport.lea64(z += LXMSupport.GOLDEN_RATIO_64);
+            x3 = LXMSupport.lea64(z + LXMSupport.GOLDEN_RATIO_64);
+        }
+        return new L64X256Mix(s0, s1, x0, x1, x2, x3);
+    }
 }
diff --git 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/LXMSupport.java
 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/LXMSupport.java
index 540df88d..3e897c86 100644
--- 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/LXMSupport.java
+++ 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/source64/LXMSupport.java
@@ -68,6 +68,14 @@ final class LXMSupport {
      * </pre>
      */
     static final long C128PH = 0x61139b28883277c3L;
+    /**
+     * The fractional part of the golden ratio, phi, scaled to 64-bits and 
rounded to odd.
+     * <pre>
+     * phi = (sqrt(5) - 1) / 2) * 2^64
+     * </pre>
+     * @see <a href="https://en.wikipedia.org/wiki/Golden_ratio";>Golden 
ratio</a>
+     */
+    static final long GOLDEN_RATIO_64 = 0x9e3779b97f4a7c15L;
 
     /** A mask to convert an {@code int} to an unsigned integer stored as a 
{@code long}. */
     private static final long INT_TO_UNSIGNED_BYTE_MASK = 0xffff_ffffL;
diff --git 
a/commons-rng-core/src/main/java/org/apache/commons/rng/core/util/RandomStreams.java
 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/util/RandomStreams.java
new file mode 100644
index 00000000..e6db6fa7
--- /dev/null
+++ 
b/commons-rng-core/src/main/java/org/apache/commons/rng/core/util/RandomStreams.java
@@ -0,0 +1,278 @@
+/*
+ * 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
+ *
+ *      http://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.rng.core.util;
+
+import java.util.Objects;
+import java.util.Spliterator;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
+import org.apache.commons.rng.UniformRandomProvider;
+
+/**
+ * Utility for creating streams using a source of randomness.
+ */
+public final class RandomStreams {
+    /** The number of bits of each random character in the seed.
+     * The generation algorithm will work if this is in the range [2, 30]. */
+    private static final int SEED_CHAR_BITS = 4;
+
+    /**
+     * A factory for creating objects using a seed and a using a source of 
randomness.
+     *
+     * @param <T> the object type
+     */
+    public interface ObjectFactory<T> {
+        /**
+         * Creates the object.
+         *
+         * @param seed Seed used to initialise the instance.
+         * @param source Source of randomness used to initialise the instance.
+         * @return the object
+         */
+        T create(long seed, UniformRandomProvider source);
+    }
+
+    /**
+     * Class contains only static methods.
+     */
+    private RandomStreams() {}
+
+    /**
+     * Returns a stream producing the given {@code streamSize} number of new 
objects
+     * generated using the supplied {@code source} of randomness using the 
{@code factory}.
+     *
+     * <p>A {@code long} seed is provided for each object instance using the 
stream position
+     * and random bits created from the supplied {@code source}.
+     *
+     * <p>The stream supports parallel execution by splitting the provided 
{@code source}
+     * of randomness. Consequently objects in the same position in the stream 
created from
+     * a sequential stream may be created from a different source of 
randomness than a parallel
+     * stream; it is not expected that parallel execution will create the same 
final
+     * collection of objects.
+     *
+     * @param <T> the object type
+     * @param streamSize Number of objects to generate.
+     * @param source A source of randomness used to initialise the new 
instances; this may
+     * be split to provide a source of randomness across a parallel stream.
+     * @param factory Factory to create new instances.
+     * @return a stream of objects; the stream is limited to the given {@code 
streamSize}.
+     * @throws IllegalArgumentException if {@code streamSize} is negative.
+     * @throws NullPointerException if {@code source} or {@code factory} is 
null
+     */
+    public static <T> Stream<T> generateWithSeed(long streamSize,
+                                                 
SplittableUniformRandomProvider source,
+                                                 ObjectFactory<T> factory) {
+        if (streamSize < 0) {
+            throw new IllegalArgumentException("Invalid stream size: " + 
streamSize);
+        }
+        Objects.requireNonNull(source, "source");
+        Objects.requireNonNull(factory, "factory");
+        final long seed = createSeed(source);
+        return StreamSupport
+                .stream(new SeededObjectSpliterator<>(0, streamSize, source, 
factory, seed), false);
+    }
+
+    /**
+     * Creates a seed to prepend to a counter. The seed is created to satisfy 
the following
+     * requirements:
+     * <ul>
+     * <li>The least significant bit is set
+     * <li>The seed is composed of characters from an n-bit alphabet
+     * <li>The character used in the least significant bits is unique
+     * <li>The other characters are sampled uniformly from the remaining (n-1) 
characters
+     * </ul>
+     *
+     * <p>The composed seed is created using {@code ((seed << shift) | count)}
+     * where the shift is applied to ensure non-overlap of the shifted seed and
+     * the count. This is achieved by ensuring the lowest 1-bit of the seed is
+     * above the highest 1-bit of the count. The shift is a multiple of n to 
ensure
+     * the character used in the least significant bits aligns with higher 
characters
+     * after a shift. As higher characters exclude the least significant 
character
+     * no shifted seed can duplicate previously observed composed seeds. This 
holds
+     * until the least significant character itself is shifted out of the 
composed seed.
+     *
+     * <p>The seed generation algorithm starts with a random series of bits 
with the lowest bit
+     * set. Any occurrences of the least significant character in the 
remaining characters are
+     * replaced using {@link UniformRandomProvider#nextInt()}.
+     *
+     * <p>The remaining characters will be rejected at a rate of 
2<sup>-n</sup>. The
+     * character size is a compromise between a low rejection rate and the 
highest supported
+     * count that may receive a prepended seed.
+     *
+     * <p>The JDK's {@code java.util.random} package uses 4-bits for the 
character size when
+     * creating a stream of SplittableGenerator. This achieves a rejection rate
+     * of {@code 1/16}. Using this size will require 1 call to generate a 
{@code long} and
+     * on average 1 call to {@code nextInt(15)}. The maximum supported stream 
size with a unique
+     * seed per object is 2<sup>60</sup>. The algorithm here also uses a 
character size of 4-bits;
+     * this simplifies the implementation as there are exactly 16 characters. 
The algorithm is a
+     * different implementation to the JDK and creates an output seed with 
similar properties.
+     *
+     * @param rng Source of randomness.
+     * @return the seed
+     */
+    static long createSeed(UniformRandomProvider rng) {
+        // Initial random bits. Lowest bit must be set.
+        long bits = rng.nextLong() | 1;
+        // Mask to extract characters.
+        // Can be used to sample from (n-1) n-bit characters.
+        final long n = (1 << SEED_CHAR_BITS) - 1;
+
+        // Extract the unique character.
+        final long unique = bits & n;
+
+        // Check the rest of the characters do not match the unique character.
+        // This loop extracts the remaining characters and replaces if 
required.
+        // This will work if the characters do not evenly divide into 64 as we 
iterate
+        // over the count of remaining bits. The original order is maintained 
so that
+        // if the bits already satisfy the requirements they are unchanged.
+        for (int i = SEED_CHAR_BITS; i < Long.SIZE; i += SEED_CHAR_BITS) {
+            // Next character
+            long c = (bits >>> i) & n;
+            if (c == unique) {
+                // Branch frequency of 2^-bits.
+                // This code is deliberately branchless.
+                // Avoid nextInt(n) using: c = floor(n * ([0, 2^32) / 2^32))
+                // Rejection rate for non-uniformity will be negligible: 2^32 
% 15 == 1
+                // so any rejection algorithm only has to exclude 1 value from 
nextInt().
+                c = (n * Integer.toUnsignedLong(rng.nextInt())) >>> 
Integer.SIZE;
+                // Ensure the sample is uniform in [0, n] excluding the unique 
character
+                c = (unique + c + 1) & n;
+                // Replace by masking out the current character and bitwise 
add the new one
+                bits = (bits & ~(n << i)) | (c << i);
+            }
+        }
+        return bits;
+    }
+
+    /**
+     * Spliterator for streams of a given object type that can be created from 
a seed
+     * and source of randomness. The source of randomness is splittable 
allowing parallel
+     * stream support.
+     *
+     * <p>The seed is mixed with the stream position to ensure each object is 
created using
+     * a unique seed value. As the position increases the seed is left shifted 
until there
+     * is no bit overlap between the seed and the position, i.e the right-most 
1-bit of the seed
+     * is larger than the left-most 1-bit of the position.
+     *s
+     * @param <T> the object type
+     */
+    private static final class SeededObjectSpliterator<T>
+            implements Spliterator<T> {
+        /** Message when the consumer action is null. */
+        private static final String NULL_ACTION = "action must not be null";
+
+        /** The current position in the range. */
+        private long position;
+        /** The upper limit of the range. */
+        private final long end;
+        /** Seed used to initialise the new instances. The least significant 
1-bit of
+         * the seed must be above the most significant bit of the position. 
This is maintained
+         * by left shift when the position is updated. */
+        private long seed;
+        /** Source of randomness used to initialise the new instances. */
+        private final SplittableUniformRandomProvider source;
+        /** Factory to create new instances. */
+        private final ObjectFactory<T> factory;
+
+        /**
+         * @param start Start position of the stream (inclusive).
+         * @param end Upper limit of the stream (exclusive).
+         * @param source Source of randomness used to initialise the new 
instances.
+         * @param factory Factory to create new instances.
+         * @param seed Seed used to initialise the instances. The least 
significant 1-bit of
+         * the seed must be above the most significant bit of the {@code 
start} position.
+         */
+        SeededObjectSpliterator(long start, long end,
+                                SplittableUniformRandomProvider source,
+                                ObjectFactory<T> factory,
+                                long seed) {
+            position = start;
+            this.end = end;
+            this.seed = seed;
+            this.source = source;
+            this.factory = factory;
+        }
+
+        @Override
+        public long estimateSize() {
+            return end - position;
+        }
+
+        @Override
+        public int characteristics() {
+            return Spliterator.SIZED | Spliterator.SUBSIZED | 
Spliterator.IMMUTABLE;
+        }
+
+        @Override
+        public Spliterator<T> trySplit() {
+            final long start = position;
+            final long middle = (start + end) >>> 1;
+            if (middle <= start) {
+                return null;
+            }
+            // The child spliterator can use the same seed as the position 
does not overlap
+            final SeededObjectSpliterator<T> s =
+                new SeededObjectSpliterator<>(start, middle, source.split(), 
factory, seed);
+            // Since the position has increased ensure the seed does not 
overlap
+            position = middle;
+            while (seed != 0 && Long.compareUnsigned(Long.lowestOneBit(seed), 
middle) <= 0) {
+                seed <<= SEED_CHAR_BITS;
+            }
+            return s;
+        }
+
+        @Override
+        public boolean tryAdvance(Consumer<? super T> action) {
+            Objects.requireNonNull(action, NULL_ACTION);
+            final long pos = position;
+            if (pos < end) {
+                // Advance before exceptions from the action are relayed to 
the caller
+                position = pos + 1;
+                action.accept(factory.create(seed | pos, source));
+                // If the position overlaps the seed, shift it by 1 character
+                if ((position & seed) != 0) {
+                    seed <<= SEED_CHAR_BITS;
+                }
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void forEachRemaining(Consumer<? super T> action) {
+            Objects.requireNonNull(action, NULL_ACTION);
+            long pos = position;
+            final long last = end;
+            if (pos < last) {
+                // Ensure forEachRemaining is called only once
+                position = last;
+                final SplittableUniformRandomProvider s = source;
+                final ObjectFactory<T> f = factory;
+                do {
+                    action.accept(f.create(seed | pos, s));
+                    pos++;
+                    // If the position overlaps the seed, shift it by 1 
character
+                    if ((pos & seed) != 0) {
+                        seed <<= SEED_CHAR_BITS;
+                    }
+                } while (pos < last);
+            }
+        }
+    }
+}
diff --git 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/ProvidersList.java 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/ProvidersList.java
index 80eab849..0abf3a51 100644
--- 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/ProvidersList.java
+++ 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/ProvidersList.java
@@ -74,6 +74,7 @@ import org.apache.commons.rng.core.source64.PcgRxsMXs64;
 import org.apache.commons.rng.core.source64.DotyHumphreySmallFastCounting64;
 import org.apache.commons.rng.JumpableUniformRandomProvider;
 import org.apache.commons.rng.RestorableUniformRandomProvider;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
 
 /**
  * The purpose of this class is to provide the list of all generators
@@ -93,6 +94,8 @@ public final class ProvidersList {
     private static final List<RestorableUniformRandomProvider> LIST64 = new 
ArrayList<>();
     /** List of {@link JumpableUniformRandomProvider} RNGs. */
     private static final List<JumpableUniformRandomProvider> LIST_JUMP = new 
ArrayList<>();
+    /** List of {@link SplittableUniformRandomProvider} RNGs. */
+    private static final List<SplittableUniformRandomProvider> LIST_SPLIT = 
new ArrayList<>();
 
     static {
         // External generator for creating a random seed.
@@ -166,10 +169,13 @@ public final class ProvidersList {
             // Complete list.
             LIST.addAll(LIST32);
             LIST.addAll(LIST64);
-            // Dynamically identify the Jumpable RNGs
+            // Dynamically identify the sub-type RNGs
             LIST.stream()
                 .filter(rng -> rng instanceof JumpableUniformRandomProvider)
                 .forEach(rng -> LIST_JUMP.add((JumpableUniformRandomProvider) 
rng));
+            LIST.stream()
+                .filter(rng -> rng instanceof SplittableUniformRandomProvider)
+                .forEach(rng -> 
LIST_SPLIT.add((SplittableUniformRandomProvider) rng));
         } catch (Exception e) {
             // CHECKSTYLE: stop Regexp
             System.err.println("Unexpected exception while creating the list 
of generators: " + e);
@@ -223,4 +229,14 @@ public final class ProvidersList {
     public static Iterable<JumpableUniformRandomProvider> listJumpable() {
         return Collections.unmodifiableList(LIST_JUMP);
     }
+
+    /**
+     * Subclasses that are "parametric" tests can forward the call to
+     * the "@Parameters"-annotated method to this method.
+     *
+     * @return the list of {@link SplittableUniformRandomProvider} generators.
+     */
+    public static Iterable<SplittableUniformRandomProvider> listSplittable() {
+        return Collections.unmodifiableList(LIST_SPLIT);
+    }
 }
diff --git 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/RandomAssert.java 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/RandomAssert.java
index 85462a1e..75cf2f10 100644
--- 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/RandomAssert.java
+++ 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/RandomAssert.java
@@ -243,6 +243,40 @@ public final class RandomAssert {
         }
     }
 
+    /**
+     * Assert that the two random generators produce a different output for
+     * {@link UniformRandomProvider#nextInt()} over the given number of cycles.
+     *
+     * @param cycles Number of cycles.
+     * @param rng1 Random generator 1.
+     * @param rng2 Random generator 2.
+     */
+    public static void assertNextIntNotEquals(int cycles, 
UniformRandomProvider rng1, UniformRandomProvider rng2) {
+        for (int i = 0; i < cycles; i++) {
+            if (rng1.nextInt() != rng2.nextInt()) {
+                return;
+            }
+        }
+        Assertions.fail(() -> cycles + " cycles of nextb has same output");
+    }
+
+    /**
+     * Assert that the two random generators produce a different output for
+     * {@link UniformRandomProvider#nextLong()} over the given number of 
cycles.
+     *
+     * @param cycles Number of cycles.
+     * @param rng1 Random generator 1.
+     * @param rng2 Random generator 2.
+     */
+    public static void assertNextLongNotEquals(int cycles, 
UniformRandomProvider rng1, UniformRandomProvider rng2) {
+        for (int i = 0; i < cycles; i++) {
+            if (rng1.nextLong() != rng2.nextLong()) {
+                return;
+            }
+        }
+        Assertions.fail(() -> cycles + " cycles of nextLong has same output");
+    }
+
     /**
      * Assert that the random generator produces zero output for
      * {@link UniformRandomProvider#nextInt()} over the given number of cycles.
diff --git 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/SplittableProvidersParametricTest.java
 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/SplittableProvidersParametricTest.java
new file mode 100644
index 00000000..bf2721e9
--- /dev/null
+++ 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/SplittableProvidersParametricTest.java
@@ -0,0 +1,359 @@
+/*
+ * 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
+ *
+ *      http://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.rng.core;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.Spliterator;
+import java.util.SplittableRandom;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.Consumer;
+import java.util.function.UnaryOperator;
+import java.util.stream.Stream;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.core.util.RandomStreamsTestHelper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Tests which all {@link SplittableUniformRandomProvider} generators must 
pass.
+ */
+class SplittableProvidersParametricTest {
+    /** The expected characteristics for the spliterator from the splittable 
stream. */
+    private static final int SPLITERATOR_CHARACTERISTICS =
+        Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.IMMUTABLE;
+
+    /**
+     * Dummy class for checking the behavior of the 
SplittableUniformRandomProvider.
+     * All generation and split methods throw an exception. This can be used 
to test
+     * exception conditions for arguments to default stream functions.
+     */
+    private static class DummyGenerator implements 
SplittableUniformRandomProvider {
+        /** An instance. */
+        static final DummyGenerator INSTANCE = new DummyGenerator();
+
+        @Override
+        public long nextLong() {
+            throw new UnsupportedOperationException("The nextLong method 
should not be invoked");
+        }
+
+        @Override
+        public SplittableUniformRandomProvider split(UniformRandomProvider 
source) {
+            throw new UnsupportedOperationException("The split(source) method 
should not be invoked");
+        }
+    }
+
+    /**
+     * Thread-safe class for checking the behavior of the 
SplittableUniformRandomProvider.
+     * Generation methods default to ThreadLocalRandom. Split methods return 
the same instance.
+     * This is a functioning generator that can be used as a source to seed 
splitting.
+     */
+    private static class ThreadLocalGenerator implements 
SplittableUniformRandomProvider {
+        /** An instance. */
+        static final ThreadLocalGenerator INSTANCE = new 
ThreadLocalGenerator();
+
+        @Override
+        public long nextLong() {
+            return ThreadLocalRandom.current().nextLong();
+        }
+
+        @Override
+        public SplittableUniformRandomProvider split(UniformRandomProvider 
source) {
+            return this;
+        }
+    }
+
+    /**
+     * Gets the list of splittable generators.
+     *
+     * @return the list
+     */
+    private static Iterable<SplittableUniformRandomProvider> 
getSplittableProviders() {
+        return ProvidersList.listSplittable();
+    }
+
+    /**
+     * Test that the split methods throw when the source of randomness is null.
+     */
+    @ParameterizedTest
+    @MethodSource("getSplittableProviders")
+    void testSplitThrowsWithNullSource(SplittableUniformRandomProvider 
generator) {
+        Assertions.assertThrows(NullPointerException.class, () -> 
generator.split(null));
+    }
+
+    /**
+     * Test that the random generator returned from the split is a new 
instance of the same class.
+     */
+    @ParameterizedTest
+    @MethodSource("getSplittableProviders")
+    void testSplitReturnsANewInstance(SplittableUniformRandomProvider 
generator) {
+        assertSplitReturnsANewInstance(SplittableUniformRandomProvider::split, 
generator);
+    }
+
+    /**
+     * Test that the random generator returned from the split(source) is a new 
instance of the same class.
+     */
+    @ParameterizedTest
+    @MethodSource("getSplittableProviders")
+    void 
testSplitWithSourceReturnsANewInstance(SplittableUniformRandomProvider 
generator) {
+        assertSplitReturnsANewInstance(s -> 
s.split(ThreadLocalGenerator.INSTANCE), generator);
+    }
+
+    /**
+     * Assert that the random generator returned from the split function is a 
new instance of the same class.
+     *
+     * @param splitFunction Split function to test.
+     * @param generator RNG under test.
+     */
+    private static void 
assertSplitReturnsANewInstance(UnaryOperator<SplittableUniformRandomProvider> 
splitFunction,
+                                                       
SplittableUniformRandomProvider generator) {
+        final UniformRandomProvider child = splitFunction.apply(generator);
+        Assertions.assertNotSame(generator, child, "The child instance should 
be a different object");
+        Assertions.assertEquals(generator.getClass(), child.getClass(), "The 
child instance should be the same class");
+        RandomAssert.assertNextLongNotEquals(10, generator, child);
+    }
+
+    /**
+     * Test that the split method is reproducible when used with the same 
generator source in the
+     * same state.
+     */
+    @ParameterizedTest
+    @MethodSource("getSplittableProviders")
+    void testSplitWithSourceIsReproducible(SplittableUniformRandomProvider 
generator) {
+        final long seed = ThreadLocalRandom.current().nextLong();
+        UniformRandomProvider rng1 = generator.split(new 
SplittableRandom(seed)::nextLong);
+        UniformRandomProvider rng2 = generator.split(new 
SplittableRandom(seed)::nextLong);
+        RandomAssert.assertNextLongEquals(10, rng1, rng2);
+    }
+
+    /**
+     * Test that the other stream splits methods all call the
+     * {@link SplittableUniformRandomProvider#splits(long, 
SplittableUniformRandomProvider)} method.
+     * This is tested by checking the spliterator is the same.
+     *
+     * <p>This test serves to ensure the default implementations in 
SplittableUniformRandomProvider
+     * eventually call the same method. The RNG implementation thus only has 
to override one method.
+     */
+    @ParameterizedTest
+    @MethodSource("getSplittableProviders")
+    void testSplitsMethodsUseSameSpliterator(SplittableUniformRandomProvider 
generator) {
+        final long size = 10;
+        final Spliterator<SplittableUniformRandomProvider> s = 
generator.splits(size, generator).spliterator();
+        Assertions.assertEquals(s.getClass(), 
generator.splits().spliterator().getClass());
+        Assertions.assertEquals(s.getClass(), 
generator.splits(size).spliterator().getClass());
+        Assertions.assertEquals(s.getClass(), 
generator.splits(ThreadLocalGenerator.INSTANCE).spliterator().getClass());
+    }
+
+    @ParameterizedTest
+    @MethodSource("getSplittableProviders")
+    void testSplitsSize(SplittableUniformRandomProvider generator) {
+        for (final long size : new long[] {0, 1, 7, 13}) {
+            Assertions.assertEquals(size, generator.splits(size).count(), 
"splits");
+            Assertions.assertEquals(size, generator.splits(size, 
ThreadLocalGenerator.INSTANCE).count(), "splits with source");
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource("getSplittableProviders")
+    void testSplits(SplittableUniformRandomProvider generator) {
+        assertSplits(generator, false);
+    }
+
+    @ParameterizedTest
+    @MethodSource("getSplittableProviders")
+    void testSplitsParallel(SplittableUniformRandomProvider generator) {
+        assertSplits(generator, true);
+    }
+
+    /**
+     * Test the splits method returns a stream of unique instances. The test 
uses a
+     * fixed source of randomness such that the only randomness is from the 
stream
+     * position.
+     *
+     * @param generator Generator
+     * @param parallel true to use a parallel stream
+     */
+    private static void assertSplits(SplittableUniformRandomProvider 
generator, boolean parallel) {
+        final long size = 13;
+        for (final long seed : new long[] {0, 
RandomStreamsTestHelper.createSeed(ThreadLocalGenerator.INSTANCE)}) {
+            final SplittableUniformRandomProvider source = new 
SplittableUniformRandomProvider() {
+                @Override
+                public long nextLong() {
+                    return seed;
+                }
+
+                @Override
+                public SplittableUniformRandomProvider 
split(UniformRandomProvider source) {
+                    return this;
+                }
+            };
+            // Test the assumption that the seed will be passed through 
(lowest bit is set)
+            Assertions.assertEquals(seed | 1, 
RandomStreamsTestHelper.createSeed(source));
+
+            Stream<SplittableUniformRandomProvider> stream = 
generator.splits(size, source);
+            Assertions.assertFalse(stream.isParallel(), "Initial stream should 
be sequential");
+            if (parallel) {
+                stream = stream.parallel();
+                Assertions.assertTrue(stream.isParallel(), "Stream should be 
parallel");
+            }
+
+            // Check the instance is a new object of the same type.
+            // These will be hashed using the system identity hash code.
+            final Set<SplittableUniformRandomProvider> observed = 
ConcurrentHashMap.newKeySet();
+            observed.add(generator);
+            stream.forEach(r -> {
+                Assertions.assertTrue(observed.add(r), "Instance should be 
unique");
+                Assertions.assertEquals(generator.getClass(), r.getClass());
+            });
+            // Note: observed contains the original generator so subtract 1
+            Assertions.assertEquals(size, observed.size() - 1);
+
+            // Test instances generate different values.
+            // The only randomness is from the stream position.
+            final long[] values = observed.stream().mapToLong(r -> {
+                // Warm up generator with some cycles.
+                // E.g. LXM generators return the first value from the initial 
state.
+                for (int i = 0; i < 10; i++) {
+                    r.nextLong();
+                }
+                return r.nextLong();
+            }).distinct().toArray();
+            // This test is looking for different values.
+            // To avoid the rare case of not all distinct we relax the 
threshold to
+            // half the generators. This will spot errors where all generators 
are
+            // the same.
+            Assertions.assertTrue(values.length > size / 2,
+                () -> "splits did not seed randomness from the stream 
position. Initial seed = " + seed);
+        }
+    }
+
+    // Test adapted from stream tests in commons-rng-client-api module
+
+    /**
+     * Helper method to raise an assertion error inside an action passed to a 
Spliterator
+     * when the action should not be invoked.
+     *
+     * @see Spliterator#tryAdvance(Consumer)
+     * @see Spliterator#forEachRemaining(Consumer)
+     */
+    private static void failSpliteratorShouldBeEmpty() {
+        Assertions.fail("Spliterator should not have any remaining elements");
+    }
+
+    @ParameterizedTest
+    @MethodSource("getSplittableProviders")
+    void testSplitsInvalidStreamSizeThrows(SplittableUniformRandomProvider 
rng) {
+        Assertions.assertThrows(IllegalArgumentException.class, () -> 
rng.splits(-1), "splits(size)");
+        final SplittableUniformRandomProvider source = DummyGenerator.INSTANCE;
+        Assertions.assertThrows(IllegalArgumentException.class, () -> 
rng.splits(-1, source), "splits(size, source)");
+    }
+
+    @ParameterizedTest
+    @MethodSource("getSplittableProviders")
+    void testSplitsUnlimitedStreamSize(SplittableUniformRandomProvider rng) {
+        assertUnlimitedSpliterator(rng.splits().spliterator(), "splits()");
+        final SplittableUniformRandomProvider source = 
ThreadLocalGenerator.INSTANCE;
+        assertUnlimitedSpliterator(rng.splits(source).spliterator(), 
"splits(source)");
+    }
+
+    /**
+     * Assert the spliterator has an unlimited expected size and the 
characteristics for a sized
+     * immutable stream.
+     *
+     * @param spliterator Spliterator.
+     * @param msg Error message.
+     */
+    private static void assertUnlimitedSpliterator(Spliterator<?> spliterator, 
String msg) {
+        Assertions.assertEquals(Long.MAX_VALUE, spliterator.estimateSize(), 
msg);
+        
Assertions.assertTrue(spliterator.hasCharacteristics(SPLITERATOR_CHARACTERISTICS),
+            () -> String.format("%s: characteristics = %s, expected %s", msg,
+                Integer.toBinaryString(spliterator.characteristics()),
+                Integer.toBinaryString(SPLITERATOR_CHARACTERISTICS)
+            ));
+    }
+
+    @ParameterizedTest
+    @MethodSource("getSplittableProviders")
+    void testSplitsNullSourceThrows(SplittableUniformRandomProvider rng) {
+        final SplittableUniformRandomProvider source = null;
+        Assertions.assertThrows(NullPointerException.class, () -> 
rng.splits(source));
+        Assertions.assertThrows(NullPointerException.class, () -> 
rng.splits(1, source));
+    }
+
+    @ParameterizedTest
+    @MethodSource("getSplittableProviders")
+    void testSplitsSpliterator(SplittableUniformRandomProvider rng) {
+        // Split a large spliterator into four smaller ones;
+        // each is used to test different functionality
+        final long size = 41;
+        Spliterator<SplittableUniformRandomProvider> s1 = 
rng.splits(size).spliterator();
+        Assertions.assertEquals(size, s1.estimateSize());
+        final Spliterator<SplittableUniformRandomProvider> s2 = s1.trySplit();
+        final Spliterator<SplittableUniformRandomProvider> s3 = s1.trySplit();
+        final Spliterator<SplittableUniformRandomProvider> s4 = s2.trySplit();
+        Assertions.assertEquals(size, s1.estimateSize() + s2.estimateSize() + 
s3.estimateSize() + s4.estimateSize());
+
+        // s1. Test cannot split indefinitely
+        while (s1.estimateSize() > 1) {
+            final long currentSize = s1.estimateSize();
+            final Spliterator<SplittableUniformRandomProvider> other = 
s1.trySplit();
+            Assertions.assertEquals(currentSize, s1.estimateSize() + 
other.estimateSize());
+            s1 = other;
+        }
+        Assertions.assertNull(s1.trySplit(), "Cannot split when size <= 1");
+
+        // Check the instance is a new object of the same type.
+        // These will be hashed using the system identity hash code.
+        final HashSet<SplittableUniformRandomProvider> observed = new 
HashSet<>();
+        observed.add(rng);
+
+        final Consumer<SplittableUniformRandomProvider> action = r -> {
+            Assertions.assertTrue(observed.add(r), "Instance should be 
unique");
+            Assertions.assertEquals(rng.getClass(), r.getClass());
+        };
+
+        // s2. Test advance
+        for (long newSize = s2.estimateSize(); newSize-- > 0;) {
+            Assertions.assertTrue(s2.tryAdvance(action));
+            Assertions.assertEquals(newSize, s2.estimateSize(), "s2 size 
estimate");
+        }
+        Assertions.assertFalse(s2.tryAdvance(r -> 
failSpliteratorShouldBeEmpty()));
+        s2.forEachRemaining(r -> failSpliteratorShouldBeEmpty());
+
+        // s3. Test forEachRemaining
+        s3.forEachRemaining(action);
+        Assertions.assertEquals(0, s3.estimateSize());
+        s3.forEachRemaining(r -> failSpliteratorShouldBeEmpty());
+
+        // s4. Test tryAdvance and forEachRemaining when the action throws an 
exception
+        final IllegalStateException ex = new IllegalStateException();
+        final Consumer<SplittableUniformRandomProvider> badAction = r -> {
+            throw ex;
+        };
+        final long currentSize = s4.estimateSize();
+        Assertions.assertTrue(currentSize > 1, "Spliterator requires more 
elements to test advance");
+        Assertions.assertSame(ex, 
Assertions.assertThrows(IllegalStateException.class, () -> 
s4.tryAdvance(badAction)));
+        Assertions.assertEquals(currentSize - 1, s4.estimateSize(), 
"Spliterator should be advanced even when action throws");
+
+        Assertions.assertSame(ex, 
Assertions.assertThrows(IllegalStateException.class, () -> 
s4.forEachRemaining(badAction)));
+        Assertions.assertEquals(0, s4.estimateSize(), "Spliterator should be 
finished even when action throws");
+        s4.forEachRemaining(r -> failSpliteratorShouldBeEmpty());
+    }
+}
diff --git 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source32/L32X64MixTest.java
 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source32/L32X64MixTest.java
index a448ca93..b947b52c 100644
--- 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source32/L32X64MixTest.java
+++ 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source32/L32X64MixTest.java
@@ -18,7 +18,10 @@ package org.apache.commons.rng.core.source32;
 
 import java.util.stream.Stream;
 import org.apache.commons.rng.LongJumpableUniformRandomProvider;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
+import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.core.RandomAssert;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -135,4 +138,29 @@ class L32X64MixTest extends AbstractLXMTest {
         final L32X64Mix rng2 = new L32X64Mix(seed[0], seed[1], seed[2], 
seed[3]);
         RandomAssert.assertNextLongEquals(seed.length * 2, rng1, rng2);
     }
+
+    /**
+     * Test split with zero bits from the source. This should be robust to 
escape the state
+     * of all zero bits that will create an invalid state for the xor-based 
generator (XBG).
+     */
+    @Test
+    void testSplitWithZeroBits() {
+        final UniformRandomProvider zeroSource = () -> 0;
+        final int[] seed = new int[Factory.INSTANCE.seedSize()];
+        // Here we copy the split which sets the LCG increment to odd
+        seed[(Factory.INSTANCE.lcgSeedSize() / 2) - 1] = 1;
+        final SplittableUniformRandomProvider rng1 = new L32X64Mix(seed);
+        final SplittableUniformRandomProvider rng2 = rng1.split(zeroSource);
+        RandomAssert.assertNextIntNotEquals(seed.length * 2, rng1, rng2);
+
+        // Since we know how the zero seed is amended
+        int z = 0;
+        for (int i = Factory.INSTANCE.lcgSeedSize(); i < seed.length; i++) {
+            seed[i] = LXMSupport.lea32(z);
+            z += LXMSupport.GOLDEN_RATIO_32;
+        }
+        final SplittableUniformRandomProvider rng3 = new L32X64Mix(seed);
+        final SplittableUniformRandomProvider rng4 = rng1.split(zeroSource);
+        RandomAssert.assertNextIntEquals(seed.length * 2, rng3, rng4);
+    }
 }
diff --git 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L128X1024MixTest.java
 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L128X1024MixTest.java
index 171782bb..276bf54d 100644
--- 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L128X1024MixTest.java
+++ 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L128X1024MixTest.java
@@ -19,6 +19,10 @@ package org.apache.commons.rng.core.source64;
 import java.util.Arrays;
 import java.util.stream.Stream;
 import org.apache.commons.rng.LongJumpableUniformRandomProvider;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.core.RandomAssert;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.provider.Arguments;
 
 /**
@@ -137,4 +141,29 @@ class L128X1024MixTest extends AbstractLXMTest {
                     0x479d66e6c85c98beL, 0xba9516550452d729L, 
0x299e54b50cebe420L, 0x8fde3ca654cd399dL,
                 }));
     }
+
+    /**
+     * Test split with zero bits from the source. This should be robust to 
escape the state
+     * of all zero bits that will create an invalid state for the xor-based 
generator (XBG).
+     */
+    @Test
+    void testSplitWithZeroBits() {
+        final UniformRandomProvider zeroSource = () -> 0;
+        final long[] seed = new long[Factory.INSTANCE.seedSize()];
+        // Here we copy the split which sets the LCG increment to odd
+        seed[(Factory.INSTANCE.lcgSeedSize() / 2) - 1] = 1;
+        final SplittableUniformRandomProvider rng1 = new L128X1024Mix(seed);
+        final SplittableUniformRandomProvider rng2 = rng1.split(zeroSource);
+        RandomAssert.assertNextLongNotEquals(seed.length * 2, rng1, rng2);
+
+        // Since we know how the zero seed is amended
+        long z = 0;
+        for (int i = Factory.INSTANCE.lcgSeedSize(); i < seed.length; i++) {
+            seed[i] = LXMSupport.lea64(z);
+            z += LXMSupport.GOLDEN_RATIO_64;
+        }
+        final SplittableUniformRandomProvider rng3 = new L128X1024Mix(seed);
+        final SplittableUniformRandomProvider rng4 = rng1.split(zeroSource);
+        RandomAssert.assertNextLongEquals(seed.length * 2, rng3, rng4);
+    }
 }
diff --git 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L128X128MixTest.java
 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L128X128MixTest.java
index 102fc15a..24bad789 100644
--- 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L128X128MixTest.java
+++ 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L128X128MixTest.java
@@ -18,7 +18,10 @@ package org.apache.commons.rng.core.source64;
 
 import java.util.stream.Stream;
 import org.apache.commons.rng.LongJumpableUniformRandomProvider;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
+import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.core.RandomAssert;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -138,4 +141,29 @@ class L128X128MixTest extends AbstractLXMTest {
         final L128X128Mix rng2 = new L128X128Mix(seed[0], seed[1], seed[2], 
seed[3], seed[4], seed[5]);
         RandomAssert.assertNextLongEquals(seed.length * 2, rng1, rng2);
     }
+
+    /**
+     * Test split with zero bits from the source. This should be robust to 
escape the state
+     * of all zero bits that will create an invalid state for the xor-based 
generator (XBG).
+     */
+    @Test
+    void testSplitWithZeroBits() {
+        final UniformRandomProvider zeroSource = () -> 0;
+        final long[] seed = new long[Factory.INSTANCE.seedSize()];
+        // Here we copy the split which sets the LCG increment to odd
+        seed[(Factory.INSTANCE.lcgSeedSize() / 2) - 1] = 1;
+        final SplittableUniformRandomProvider rng1 = new L128X128Mix(seed);
+        final SplittableUniformRandomProvider rng2 = rng1.split(zeroSource);
+        RandomAssert.assertNextLongNotEquals(seed.length * 2, rng1, rng2);
+
+        // Since we know how the zero seed is amended
+        long z = 0;
+        for (int i = Factory.INSTANCE.lcgSeedSize(); i < seed.length; i++) {
+            seed[i] = LXMSupport.lea64(z);
+            z += LXMSupport.GOLDEN_RATIO_64;
+        }
+        final SplittableUniformRandomProvider rng3 = new L128X128Mix(seed);
+        final SplittableUniformRandomProvider rng4 = rng1.split(zeroSource);
+        RandomAssert.assertNextLongEquals(seed.length * 2, rng3, rng4);
+    }
 }
diff --git 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L128X256MixTest.java
 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L128X256MixTest.java
index cfdf95d3..9317874a 100644
--- 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L128X256MixTest.java
+++ 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L128X256MixTest.java
@@ -18,7 +18,10 @@ package org.apache.commons.rng.core.source64;
 
 import java.util.stream.Stream;
 import org.apache.commons.rng.LongJumpableUniformRandomProvider;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
+import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.core.RandomAssert;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -138,4 +141,29 @@ class L128X256MixTest extends AbstractLXMTest {
                                                  seed[4], seed[5], seed[6], 
seed[7]);
         RandomAssert.assertNextLongEquals(seed.length * 2, rng1, rng2);
     }
+
+    /**
+     * Test split with zero bits from the source. This should be robust to 
escape the state
+     * of all zero bits that will create an invalid state for the xor-based 
generator (XBG).
+     */
+    @Test
+    void testSplitWithZeroBits() {
+        final UniformRandomProvider zeroSource = () -> 0;
+        final long[] seed = new long[Factory.INSTANCE.seedSize()];
+        // Here we copy the split which sets the LCG increment to odd
+        seed[(Factory.INSTANCE.lcgSeedSize() / 2) - 1] = 1;
+        final SplittableUniformRandomProvider rng1 = new L128X256Mix(seed);
+        final SplittableUniformRandomProvider rng2 = rng1.split(zeroSource);
+        RandomAssert.assertNextLongNotEquals(seed.length * 2, rng1, rng2);
+
+        // Since we know how the zero seed is amended
+        long z = 0;
+        for (int i = Factory.INSTANCE.lcgSeedSize(); i < seed.length; i++) {
+            seed[i] = LXMSupport.lea64(z);
+            z += LXMSupport.GOLDEN_RATIO_64;
+        }
+        final SplittableUniformRandomProvider rng3 = new L128X256Mix(seed);
+        final SplittableUniformRandomProvider rng4 = rng1.split(zeroSource);
+        RandomAssert.assertNextLongEquals(seed.length * 2, rng3, rng4);
+    }
 }
diff --git 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L64X1024MixTest.java
 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L64X1024MixTest.java
index 871ce7aa..cc53a161 100644
--- 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L64X1024MixTest.java
+++ 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L64X1024MixTest.java
@@ -19,6 +19,10 @@ package org.apache.commons.rng.core.source64;
 import java.util.Arrays;
 import java.util.stream.Stream;
 import org.apache.commons.rng.LongJumpableUniformRandomProvider;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.core.RandomAssert;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.provider.Arguments;
 
 /**
@@ -137,4 +141,29 @@ class L64X1024MixTest extends AbstractLXMTest {
                     0xaa84a5cf5b0668caL, 0xecb643d0e758e7edL, 
0xe6eba4065ff373abL, 0xb80a1412a869cef7L,
                 }));
     }
+
+    /**
+     * Test split with zero bits from the source. This should be robust to 
escape the state
+     * of all zero bits that will create an invalid state for the xor-based 
generator (XBG).
+     */
+    @Test
+    void testSplitWithZeroBits() {
+        final UniformRandomProvider zeroSource = () -> 0;
+        final long[] seed = new long[Factory.INSTANCE.seedSize()];
+        // Here we copy the split which sets the LCG increment to odd
+        seed[(Factory.INSTANCE.lcgSeedSize() / 2) - 1] = 1;
+        final SplittableUniformRandomProvider rng1 = new L64X1024Mix(seed);
+        final SplittableUniformRandomProvider rng2 = rng1.split(zeroSource);
+        RandomAssert.assertNextLongNotEquals(seed.length * 2, rng1, rng2);
+
+        // Since we know how the zero seed is amended
+        long z = 0;
+        for (int i = Factory.INSTANCE.lcgSeedSize(); i < seed.length; i++) {
+            seed[i] = LXMSupport.lea64(z);
+            z += LXMSupport.GOLDEN_RATIO_64;
+        }
+        final SplittableUniformRandomProvider rng3 = new L64X1024Mix(seed);
+        final SplittableUniformRandomProvider rng4 = rng1.split(zeroSource);
+        RandomAssert.assertNextLongEquals(seed.length * 2, rng3, rng4);
+    }
 }
diff --git 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L64X128MixTest.java
 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L64X128MixTest.java
index cafc9539..289ec973 100644
--- 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L64X128MixTest.java
+++ 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L64X128MixTest.java
@@ -18,7 +18,10 @@ package org.apache.commons.rng.core.source64;
 
 import java.util.stream.Stream;
 import org.apache.commons.rng.LongJumpableUniformRandomProvider;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
+import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.core.RandomAssert;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -135,4 +138,29 @@ class L64X128MixTest extends AbstractLXMTest {
         final L64X128Mix rng2 = new L64X128Mix(seed[0], seed[1], seed[2], 
seed[3]);
         RandomAssert.assertNextLongEquals(seed.length * 2, rng1, rng2);
     }
+
+    /**
+     * Test split with zero bits from the source. This should be robust to 
escape the state
+     * of all zero bits that will create an invalid state for the xor-based 
generator (XBG).
+     */
+    @Test
+    void testSplitWithZeroBits() {
+        final UniformRandomProvider zeroSource = () -> 0;
+        final long[] seed = new long[Factory.INSTANCE.seedSize()];
+        // Here we copy the split which sets the LCG increment to odd
+        seed[(Factory.INSTANCE.lcgSeedSize() / 2) - 1] = 1;
+        final SplittableUniformRandomProvider rng1 = new L64X128Mix(seed);
+        final SplittableUniformRandomProvider rng2 = rng1.split(zeroSource);
+        RandomAssert.assertNextLongNotEquals(seed.length * 2, rng1, rng2);
+
+        // Since we know how the zero seed is amended
+        long z = 0;
+        for (int i = Factory.INSTANCE.lcgSeedSize(); i < seed.length; i++) {
+            seed[i] = LXMSupport.lea64(z);
+            z += LXMSupport.GOLDEN_RATIO_64;
+        }
+        final SplittableUniformRandomProvider rng3 = new L64X128Mix(seed);
+        final SplittableUniformRandomProvider rng4 = rng1.split(zeroSource);
+        RandomAssert.assertNextLongEquals(seed.length * 2, rng3, rng4);
+    }
 }
diff --git 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L64X128StarStarTest.java
 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L64X128StarStarTest.java
index ed8275e5..aad16ff0 100644
--- 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L64X128StarStarTest.java
+++ 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L64X128StarStarTest.java
@@ -18,7 +18,10 @@ package org.apache.commons.rng.core.source64;
 
 import java.util.stream.Stream;
 import org.apache.commons.rng.LongJumpableUniformRandomProvider;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
+import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.core.RandomAssert;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -141,4 +144,29 @@ class L64X128StarStarTest extends AbstractLXMTest {
         final L64X128StarStar rng2 = new L64X128StarStar(seed[0], seed[1], 
seed[2], seed[3]);
         RandomAssert.assertNextLongEquals(seed.length * 2, rng1, rng2);
     }
+
+    /**
+     * Test split with zero bits from the source. This should be robust to 
escape the state
+     * of all zero bits that will create an invalid state for the xor-based 
generator (XBG).
+     */
+    @Test
+    void testSplitWithZeroBits() {
+        final UniformRandomProvider zeroSource = () -> 0;
+        final long[] seed = new long[Factory.INSTANCE.seedSize()];
+        // Here we copy the split which sets the LCG increment to odd
+        seed[(Factory.INSTANCE.lcgSeedSize() / 2) - 1] = 1;
+        final SplittableUniformRandomProvider rng1 = new L64X128StarStar(seed);
+        final SplittableUniformRandomProvider rng2 = rng1.split(zeroSource);
+        RandomAssert.assertNextLongNotEquals(seed.length * 2, rng1, rng2);
+
+        // Since we know how the zero seed is amended
+        long z = 0;
+        for (int i = Factory.INSTANCE.lcgSeedSize(); i < seed.length; i++) {
+            seed[i] = LXMSupport.lea64(z);
+            z += LXMSupport.GOLDEN_RATIO_64;
+        }
+        final SplittableUniformRandomProvider rng3 = new L64X128StarStar(seed);
+        final SplittableUniformRandomProvider rng4 = rng1.split(zeroSource);
+        RandomAssert.assertNextLongEquals(seed.length * 2, rng3, rng4);
+    }
 }
diff --git 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L64X256MixTest.java
 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L64X256MixTest.java
index a0c46591..1792a10d 100644
--- 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L64X256MixTest.java
+++ 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/source64/L64X256MixTest.java
@@ -18,7 +18,10 @@ package org.apache.commons.rng.core.source64;
 
 import java.util.stream.Stream;
 import org.apache.commons.rng.LongJumpableUniformRandomProvider;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
+import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.core.RandomAssert;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -138,4 +141,29 @@ class L64X256MixTest extends AbstractLXMTest {
         final L64X256Mix rng2 = new L64X256Mix(seed[0], seed[1], seed[2], 
seed[3], seed[4], seed[5]);
         RandomAssert.assertNextLongEquals(seed.length * 2, rng1, rng2);
     }
+
+    /**
+     * Test split with zero bits from the source. This should be robust to 
escape the state
+     * of all zero bits that will create an invalid state for the xor-based 
generator (XBG).
+     */
+    @Test
+    void testSplitWithZeroBits() {
+        final UniformRandomProvider zeroSource = () -> 0;
+        final long[] seed = new long[Factory.INSTANCE.seedSize()];
+        // Here we copy the split which sets the LCG increment to odd
+        seed[(Factory.INSTANCE.lcgSeedSize() / 2) - 1] = 1;
+        final SplittableUniformRandomProvider rng1 = new L64X256Mix(seed);
+        final SplittableUniformRandomProvider rng2 = rng1.split(zeroSource);
+        RandomAssert.assertNextLongNotEquals(seed.length * 2, rng1, rng2);
+
+        // Since we know how the zero seed is amended
+        long z = 0;
+        for (int i = Factory.INSTANCE.lcgSeedSize(); i < seed.length; i++) {
+            seed[i] = LXMSupport.lea64(z);
+            z += LXMSupport.GOLDEN_RATIO_64;
+        }
+        final SplittableUniformRandomProvider rng3 = new L64X256Mix(seed);
+        final SplittableUniformRandomProvider rng4 = rng1.split(zeroSource);
+        RandomAssert.assertNextLongEquals(seed.length * 2, rng3, rng4);
+    }
 }
diff --git 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/util/RandomStreamsTest.java
 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/util/RandomStreamsTest.java
new file mode 100644
index 00000000..87b0fbca
--- /dev/null
+++ 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/util/RandomStreamsTest.java
@@ -0,0 +1,448 @@
+/*
+ * 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
+ *
+ *      http://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.rng.core.util;
+
+import java.util.Arrays;
+import java.util.Spliterator;
+import java.util.SplittableRandom;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+import java.util.function.IntConsumer;
+import java.util.function.LongConsumer;
+import java.util.function.Supplier;
+import java.util.stream.LongStream;
+import org.apache.commons.math3.stat.inference.ChiSquareTest;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.core.util.RandomStreams.ObjectFactory;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Tests for {@link RandomStreams}.
+ */
+class RandomStreamsTest {
+    /** The size in bits of the seed characters. */
+    private static final int CHAR_BITS = 4;
+
+    /**
+     * Class for outputting a unique sequence from the nextLong() method even 
under
+     * recursive splitting. Splitting creates a new instance.
+     */
+    private static class SequenceGenerator implements 
SplittableUniformRandomProvider {
+        /** The value for nextLong. */
+        private final AtomicLong value;
+
+        /**
+         * @param seed Sequence seed value.
+         */
+        SequenceGenerator(long seed) {
+            value = new AtomicLong(seed);
+        }
+
+        /**
+         * @param value The value for nextLong.
+         */
+        SequenceGenerator(AtomicLong value) {
+            this.value = value;
+        }
+
+        @Override
+        public long nextLong() {
+            return value.getAndIncrement();
+        }
+
+        @Override
+        public SplittableUniformRandomProvider split(UniformRandomProvider 
source) {
+            // Ignore the source (use of the source is optional)
+            return new SequenceGenerator(value);
+        }
+    }
+
+    /**
+     * Class for decoding the combined seed ((seed << shift) | position).
+     * Requires the unshifted seed. The shift is assumed to be a multiple of 4.
+     * The first call to the consumer will extract the current position.
+     * Further calls will compare the value with the predicted value using
+     * the last known position.
+     */
+    private static class SeedDecoder implements Consumer<Long>, LongConsumer {
+        /** The initial (unshifted) seed. */
+        private final long initial;
+        /** The current shifted seed. */
+        private long seed;
+        /** The last known position. */
+        private long position = -1;
+
+        /**
+         * @param initial Unshifted seed value.
+         */
+        SeedDecoder(long initial) {
+            this.initial = initial;
+        }
+
+        @Override
+        public void accept(long value) {
+            if (position < 0) {
+                // Search for the initial seed value
+                seed = initial;
+                long mask = -1;
+                while (seed != 0 && (value & mask) != seed) {
+                    seed <<= CHAR_BITS;
+                    mask <<= CHAR_BITS;
+                }
+                if (seed == 0) {
+                    Assertions.fail(() -> String.format("Failed to decode 
position from %s using seed %s",
+                        Long.toBinaryString(value), 
Long.toBinaryString(initial)));
+                }
+                // Remove the seed contribution leaving the position
+                position = value & ~seed;
+            } else {
+                // Predict
+                final long expected = position + 1;
+                //seed = initial;
+                while (seed != 0 && 
Long.compareUnsigned(Long.lowestOneBit(seed), expected) <= 0) {
+                    seed <<= CHAR_BITS;
+                }
+                Assertions.assertEquals(expected | seed, value);
+                position = expected;
+            }
+        }
+
+        @Override
+        public void accept(Long t) {
+            accept(t.longValue());
+        }
+
+        /**
+         * Reset the decoder.
+         */
+        void reset() {
+            position = -1;
+        }
+    }
+
+    /**
+     * Test the seed has the required properties:
+     * <ul>
+     * <li>Test the seed has an odd character in the least significant position
+     * <li>Test the remaining characters in the seed do not match this 
character
+     * <li>Test the distribution of characters is uniform
+     * <ul>
+     *
+     * <p>The test assumes the character size is 4-bits.
+     *
+     * @param seed the seed
+     */
+    @ParameterizedTest
+    @ValueSource(longs = {1628346812812L})
+    void testCreateSeed(long seed) {
+        final UniformRandomProvider rng = new SplittableRandom(seed)::nextLong;
+
+        // Histogram the distribution for each unique 4-bit character
+        final int m = (1 << CHAR_BITS) - 1;
+        // Number of remaining characters
+        final int n = (int) Math.ceil((Long.SIZE - CHAR_BITS) / CHAR_BITS);
+        final int[][] h = new int[m + 1][m + 1];
+        final int samples = 1 << 16;
+        for (int i = 0; i < samples; i++) {
+            long s = RandomStreams.createSeed(rng);
+            final int unique = (int) (s & m);
+            for (int j = 0; j < n; j++) {
+                s >>>= CHAR_BITS;
+                h[unique][(int) (s & m)]++;
+            }
+        }
+
+        // Test unique characters are always odd.
+        final int[] empty = new int[m + 1];
+        for (int i = 0; i <= m; i += 2) {
+            Assertions.assertArrayEquals(empty, h[i], "Even histograms should 
be empty");
+        }
+
+        // Test unique characters are not repeated
+        for (int i = 1; i <= m; i += 2) {
+            Assertions.assertEquals(0, h[i][i]);
+        }
+
+        // Chi-square test the distribution of unique characters
+        final long[] sum = new long[(m + 1) / 2];
+        for (int i = 1; i <= m; i += 2) {
+            final long total = Arrays.stream(h[i]).sum();
+            Assertions.assertEquals(0, total % n, "Samples should be a 
multiple of the number of characters");
+            sum[i / 2] = total / n;
+        }
+
+        assertChiSquare(sum, () -> "Unique character distribution");
+
+        // Chi-square test the distribution for each unique character.
+        // Note: This will fail if the characters do not evenly divide into 64.
+        // In that case the expected values are not uniform as the final
+        // character will be truncated and skew the expected values to lower 
characters.
+        // For simplicity this has not been accounted for as 4-bits evenly 
divides 64.
+        Assertions.assertEquals(0, Long.SIZE % CHAR_BITS, "Character 
distribution cannot be tested as uniform");
+        for (int i = 1; i <= m; i += 2) {
+            final long[] obs = Arrays.stream(h[i]).filter(c -> c != 
0).asLongStream().toArray();
+            final int c = i;
+            assertChiSquare(obs, () -> "Other character distribution for 
unique character " + c);
+        }
+    }
+
+    /**
+     * Assert the observations are uniform using a chi-square test.
+     *
+     * @param obs Observations.
+     * @param msg Failure message prefix.
+     */
+    private static void assertChiSquare(long[] obs, Supplier<String> msg) {
+        final ChiSquareTest t = new ChiSquareTest();
+        final double alpha = 0.001;
+        final double[] expected = new double[obs.length];
+        Arrays.fill(expected, 1.0 / obs.length);
+        final double p = t.chiSquareTest(expected, obs);
+        Assertions.assertFalse(p < alpha, () -> String.format("%s: chi2 
p-value: %s < %s", msg.get(), p, alpha));
+    }
+
+    @ParameterizedTest
+    @ValueSource(longs = {-1, -2, Long.MIN_VALUE})
+    void testGenerateWithSeedInvalidStreamSizeThrows(long size) {
+        final SplittableUniformRandomProvider source = new 
SequenceGenerator(0);
+        final ObjectFactory<Long> factory = (s, r) -> Long.valueOf(s);
+        final IllegalArgumentException ex1 = 
Assertions.assertThrows(IllegalArgumentException.class,
+            () -> RandomStreams.generateWithSeed(size, source, factory));
+        // Check the exception method is consistent with UniformRandomProvider 
stream methods
+        final IllegalArgumentException ex2 = 
Assertions.assertThrows(IllegalArgumentException.class,
+            () -> source.ints(size));
+        Assertions.assertEquals(ex2.getMessage(), ex1.getMessage(), 
"Inconsistent exception message");
+    }
+
+    @Test
+    void testGenerateWithSeedNullArgumentThrows() {
+        final long size = 10;
+        final SplittableUniformRandomProvider source = new 
SequenceGenerator(0);
+        final ObjectFactory<Long> factory = (s, r) -> Long.valueOf(s);
+        Assertions.assertThrows(NullPointerException.class,
+            () -> RandomStreams.generateWithSeed(size, null, factory));
+        Assertions.assertThrows(NullPointerException.class,
+            () -> RandomStreams.generateWithSeed(size, source, null));
+    }
+
+    /**
+     * Test that the seed passed to the factory is ((seed << shift) | 
position).
+     * This is done by creating an initial seed value of 1. When removed the
+     * remaining values should be a sequence.
+     *
+     * @param threads Number of threads.
+     * @param streamSize Stream size.
+     */
+    @ParameterizedTest
+    @CsvSource({
+        "1, 23",
+        "4, 31",
+        "4, 3",
+        "8, 127",
+    })
+    void testGenerateWithSeed(int threads, long streamSize) throws 
InterruptedException, ExecutionException {
+        // Provide a generator that results in the seed being set as 1.
+        final SplittableUniformRandomProvider rng = new 
SplittableUniformRandomProvider() {
+            @Override
+            public long nextLong() {
+                return 1;
+            }
+
+            @Override
+            public SplittableUniformRandomProvider split(UniformRandomProvider 
source) {
+                return this;
+            }
+        };
+        Assertions.assertEquals(1, RandomStreams.createSeed(rng), "Unexpected 
seed value");
+
+        // Create a factory that will return the seed passed to the factory
+        final ObjectFactory<Long> factory = (s, r) -> {
+            Assertions.assertSame(rng, r, "The source RNG is not used");
+            return Long.valueOf(s);
+        };
+
+        // Stream in a custom pool
+        final ForkJoinPool threadPool = new ForkJoinPool(threads);
+        Long[] values;
+        try {
+            values = threadPool.submit(() ->
+                RandomStreams.generateWithSeed(streamSize, rng, 
factory).parallel().toArray(Long[]::new)).get();
+        } finally {
+            threadPool.shutdown();
+        }
+
+        // Remove the highest 1 bit from each long. The rest should be a 
sequence.
+        final long[] actual = Arrays.stream(values).mapToLong(Long::longValue)
+                .map(l -> l - Long.highestOneBit(l)).sorted().toArray();
+        final long[] expected = LongStream.range(0, streamSize).toArray();
+        Assertions.assertArrayEquals(expected, actual);
+    }
+
+    @Test
+    void testGenerateWithSeedSpliteratorThrows() {
+        final long size = 10;
+        final SplittableUniformRandomProvider source = new 
SequenceGenerator(0);
+        final ObjectFactory<Long> factory = (s, r) -> Long.valueOf(s);
+        final Spliterator<Long> s = RandomStreams.generateWithSeed(size, 
source, factory).spliterator();
+        final Consumer<Long> badAction = null;
+        final NullPointerException ex1 = 
Assertions.assertThrows(NullPointerException.class, () -> 
s.tryAdvance(badAction), "tryAdvance");
+        final NullPointerException ex2 = 
Assertions.assertThrows(NullPointerException.class, () -> 
s.forEachRemaining(badAction), "forEachRemaining");
+        // Check the exception method is consistent with UniformRandomProvider 
stream methods
+        final NullPointerException ex3 = 
Assertions.assertThrows(NullPointerException.class, () -> 
source.ints().spliterator().tryAdvance((IntConsumer) null), "tryAdvance");
+        Assertions.assertEquals(ex3.getMessage(), ex1.getMessage(), 
"Inconsistent tryAdvance exception message");
+        Assertions.assertEquals(ex3.getMessage(), ex2.getMessage(), 
"Inconsistent forEachRemaining exception message");
+    }
+
+    @Test
+    void testGenerateWithSeedSpliterator() {
+        // Create an initial seed value. This should not be modified by the 
algorithm
+        // when generating a 'new' seed from the RNG.
+        final long initial = RandomStreams.createSeed(new 
SplittableRandom()::nextLong);
+        final SplittableUniformRandomProvider rng = new 
SplittableUniformRandomProvider() {
+            @Override
+            public long nextLong() {
+                return initial;
+            }
+
+            @Override
+            public SplittableUniformRandomProvider split(UniformRandomProvider 
source) {
+                return this;
+            }
+        };
+        Assertions.assertEquals(initial, RandomStreams.createSeed(rng), 
"Unexpected seed value");
+
+        // Create a factory that will return the seed passed to the factory
+        final ObjectFactory<Long> factory = (s, r) -> {
+            Assertions.assertSame(rng, r, "The source RNG is not used");
+            return Long.valueOf(s);
+        };
+
+        // Split a large spliterator into four smaller ones;
+        // each is used to test different functionality
+        final long size = 41;
+        Spliterator<Long> s1 = RandomStreams.generateWithSeed(size, rng, 
factory).spliterator();
+        Assertions.assertEquals(size, s1.estimateSize());
+        Assertions.assertTrue(s1.hasCharacteristics(Spliterator.SIZED | 
Spliterator.SUBSIZED | Spliterator.IMMUTABLE),
+            "Invalid characteristics");
+        final Spliterator<Long> s2 = s1.trySplit();
+        final Spliterator<Long> s3 = s1.trySplit();
+        final Spliterator<Long> s4 = s2.trySplit();
+        Assertions.assertEquals(size, s1.estimateSize() + s2.estimateSize() + 
s3.estimateSize() + s4.estimateSize());
+
+        // s1. Test cannot split indefinitely
+        while (s1.estimateSize() > 1) {
+            final long currentSize = s1.estimateSize();
+            final Spliterator<Long> other = s1.trySplit();
+            Assertions.assertEquals(currentSize, s1.estimateSize() + 
other.estimateSize());
+            s1 = other;
+        }
+        Assertions.assertNull(s1.trySplit(), "Cannot split when size <= 1");
+
+        // Create an action that will decode the shift and position using the
+        // known initial seed. This can be used to predict and assert the next 
value.
+        final SeedDecoder action = new SeedDecoder(initial);
+
+        // s2. Test advance
+        for (long newSize = s2.estimateSize(); newSize-- > 0;) {
+            Assertions.assertTrue(s2.tryAdvance(action));
+            Assertions.assertEquals(newSize, s2.estimateSize(), "s2 size 
estimate");
+        }
+        final Consumer<Long> throwIfCalled = r -> Assertions.fail("spliterator 
should be empty");
+        Assertions.assertFalse(s2.tryAdvance(throwIfCalled));
+        s2.forEachRemaining(throwIfCalled);
+
+        // s3. Test forEachRemaining
+        action.reset();
+        s3.forEachRemaining(action);
+        Assertions.assertEquals(0, s3.estimateSize());
+        s3.forEachRemaining(throwIfCalled);
+
+        // s4. Test tryAdvance and forEachRemaining when the action throws an 
exception
+        final IllegalStateException ex = new IllegalStateException();
+        final Consumer<Long> badAction = r -> {
+            throw ex;
+        };
+        final long currentSize = s4.estimateSize();
+        Assertions.assertTrue(currentSize > 1, "Spliterator requires more 
elements to test advance");
+        Assertions.assertSame(ex, 
Assertions.assertThrows(IllegalStateException.class, () -> 
s4.tryAdvance(badAction)));
+        Assertions.assertEquals(currentSize - 1, s4.estimateSize(), 
"Spliterator should be advanced even when action throws");
+
+        Assertions.assertSame(ex, 
Assertions.assertThrows(IllegalStateException.class, () -> 
s4.forEachRemaining(badAction)));
+        Assertions.assertEquals(0, s4.estimateSize(), "Spliterator should be 
finished even when action throws");
+        s4.forEachRemaining(throwIfCalled);
+    }
+
+    /**
+     * Test a very large stream size above 2<sup>60</sup>.
+     * In this case it is not possible to prepend a 4-bit character
+     * to the stream position. The seed passed to the factory will be the 
stream position.
+     */
+    @Test
+    void testLargeStreamSize() {
+        // Create an initial seed value. This should not be modified by the 
algorithm
+        // when generating a 'new' seed from the RNG.
+        final long initial = RandomStreams.createSeed(new 
SplittableRandom()::nextLong);
+        final SplittableUniformRandomProvider rng = new 
SplittableUniformRandomProvider() {
+            @Override
+            public long nextLong() {
+                return initial;
+            }
+
+            @Override
+            public SplittableUniformRandomProvider split(UniformRandomProvider 
source) {
+                return this;
+            }
+        };
+        Assertions.assertEquals(initial, RandomStreams.createSeed(rng), 
"Unexpected seed value");
+
+        // Create a factory that will return the seed passed to the factory
+        final ObjectFactory<Long> factory = (s, r) -> {
+            Assertions.assertSame(rng, r, "The source RNG is not used");
+            return Long.valueOf(s);
+        };
+
+        final Spliterator<Long> s = RandomStreams.generateWithSeed(1L << 62, 
rng, factory).spliterator();
+
+        // Split uses a divide-by-two approach. The child uses the smaller 
half.
+        final Spliterator<Long> s1 = s.trySplit();
+
+        // Lower half. The next position can be predicted using the decoder.
+        final SeedDecoder action = new SeedDecoder(initial);
+        long size = s1.estimateSize();
+        for (int i = 1; i <= 5; i++) {
+            Assertions.assertTrue(s1.tryAdvance(action));
+            Assertions.assertEquals(size - i, s1.estimateSize(), "s1 size 
estimate");
+        }
+
+        // Upper half. This should be just the stream position which we can
+        // collect with a call to advance.
+        final long[] expected = {0};
+        s.tryAdvance(seed -> expected[0] = seed);
+        size = s.estimateSize();
+        for (int i = 1; i <= 5; i++) {
+            Assertions.assertTrue(s.tryAdvance(seed -> 
Assertions.assertEquals(++expected[0], seed)));
+            Assertions.assertEquals(size - i, s.estimateSize(), "s size 
estimate");
+        }
+    }
+}
diff --git 
a/commons-rng-core/src/test/java/org/apache/commons/rng/core/util/RandomStreamsTestHelper.java
 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/util/RandomStreamsTestHelper.java
new file mode 100644
index 00000000..d1028e7b
--- /dev/null
+++ 
b/commons-rng-core/src/test/java/org/apache/commons/rng/core/util/RandomStreamsTestHelper.java
@@ -0,0 +1,39 @@
+/*
+ * 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
+ *
+ *      http://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.rng.core.util;
+
+import org.apache.commons.rng.UniformRandomProvider;
+
+/**
+ * Test helper class to expose package-private functionality for tests in 
other packages.
+ */
+public final class RandomStreamsTestHelper {
+
+    /** No instances. */
+    private RandomStreamsTestHelper() {}
+
+    /**
+     * Creates a seed to prepend to a counter. This method makes public the 
package-private
+     * seed generation method used in {@link RandomStreams} for test classes 
in other packages.
+     *
+     * @param rng Source of randomness.
+     * @return the seed
+     */
+    public static long createSeed(UniformRandomProvider rng) {
+        return RandomStreams.createSeed(rng);
+    }
+}
diff --git 
a/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/RandomSource.java
 
b/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/RandomSource.java
index 2432631f..0c834499 100644
--- 
a/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/RandomSource.java
+++ 
b/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/RandomSource.java
@@ -783,6 +783,29 @@ public enum RandomSource {
         return 
isAssignableTo(org.apache.commons.rng.LongJumpableUniformRandomProvider.class);
     }
 
+    /**
+     * Checks whether the implementing class represented by this random source
+     * supports the {@link 
org.apache.commons.rng.SplittableUniformRandomProvider
+     * SplittableUniformRandomProvider} interface. If {@code true} the 
instance returned
+     * by {@link #create(RandomSource)} may be cast to the interface; 
otherwise a class
+     * cast exception will occur.
+     *
+     * <p>Usage example:</p>
+     * <pre><code>
+     *  RandomSource source = ...;
+     *  if (source.isSplittable()) {
+     *      SplittableUniformRandomProvider rng =
+     *          (SplittableUniformRandomProvider) source.create();
+     *  }
+     * </code></pre>
+     *
+     * @return {@code true} if splittable
+     * @since 1.5
+     */
+    public boolean isSplittable() {
+        return 
isAssignableTo(org.apache.commons.rng.SplittableUniformRandomProvider.class);
+    }
+
     /**
      * Determines if the implementing class represented by this random source 
is either the same
      * as, or is a subclass or subinterface of, the class or interface 
represented
diff --git 
a/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/ProvidersCommonParametricTest.java
 
b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/ProvidersCommonParametricTest.java
index da062de4..d32de81f 100644
--- 
a/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/ProvidersCommonParametricTest.java
+++ 
b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/ProvidersCommonParametricTest.java
@@ -37,6 +37,7 @@ import org.apache.commons.rng.JumpableUniformRandomProvider;
 import org.apache.commons.rng.LongJumpableUniformRandomProvider;
 import org.apache.commons.rng.RandomProviderState;
 import org.apache.commons.rng.RestorableUniformRandomProvider;
+import org.apache.commons.rng.SplittableUniformRandomProvider;
 import org.apache.commons.rng.core.RandomProviderDefaultState;
 import org.apache.commons.rng.core.source64.LongProvider;
 import org.apache.commons.rng.core.source64.SplitMix64;
@@ -360,6 +361,9 @@ class ProvidersCommonParametricTest {
         Assertions.assertEquals(rng instanceof 
LongJumpableUniformRandomProvider,
                                 originalSource.isLongJumpable(),
                                 "isLongJumpable");
+        Assertions.assertEquals(rng instanceof SplittableUniformRandomProvider,
+                                originalSource.isSplittable(),
+                                "isSplittable");
     }
 
     ///// Support methods below.
diff --git 
a/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/RandomSourceTest.java
 
b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/RandomSourceTest.java
index 49bd9635..1fa09f59 100644
--- 
a/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/RandomSourceTest.java
+++ 
b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/RandomSourceTest.java
@@ -92,6 +92,13 @@ class RandomSourceTest {
         Assertions.assertTrue(RandomSource.XO_SHI_RO_256_SS.isLongJumpable(), 
"XO_SHI_RO_256_SS is LongJumpable");
     }
 
+    @Test
+    void testIsSplittable() {
+        Assertions.assertFalse(RandomSource.JDK.isSplittable(), "JDK is not 
Splittable");
+        Assertions.assertTrue(RandomSource.L32_X64_MIX.isSplittable(), 
"L32_X64_MIX is Splittable");
+        Assertions.assertTrue(RandomSource.L64_X128_MIX.isSplittable(), 
"L64_X128_MIX is Splittable");
+    }
+
     /**
      * MSWS should not infinite loop if the input RNG fails to provide 
randomness to create a seed.
      * See RNG-175.
diff --git a/src/main/resources/pmd/pmd-ruleset.xml 
b/src/main/resources/pmd/pmd-ruleset.xml
index 9c4e4daa..96e00ff1 100644
--- a/src/main/resources/pmd/pmd-ruleset.xml
+++ b/src/main/resources/pmd/pmd-ruleset.xml
@@ -121,7 +121,7 @@
           or @SimpleName='ThreadLocalRandomSource' or @SimpleName='SeedFactory'
           or @SimpleName='Coordinates' or @SimpleName='Hex' or 
@SimpleName='SpecialMath'
           or @SimpleName='Conversions' or @SimpleName='MixFunctions' or 
@SimpleName='LXMSupport'
-          or @SimpleName='UniformRandomProviderSupport']"/>
+          or @SimpleName='UniformRandomProviderSupport' or 
@SimpleName='RandomStreams']"/>
       <!-- Allow samplers to have only factory constructors -->
       <property name="utilityClassPattern" 
value="[A-Z][a-zA-Z0-9]+(Utils?|Helper|Sampler)" />
     </properties>
@@ -279,4 +279,12 @@
     </properties>
   </rule>
 
+  <rule ref="category/java/performance.xml/AvoidArrayLoops">
+    <properties>
+      <!-- False positive. The array loop is generating, not copying, values. 
-->
+      <property name="violationSuppressXPath"
+        
value="./ancestor-or-self::ClassOrInterfaceDeclaration[matches(@SimpleName, 
'^.*L.*X1024Mix$')]"/>
+    </properties>
+  </rule>
+
 </ruleset>
diff --git a/src/main/resources/revapi/api-changes.json 
b/src/main/resources/revapi/api-changes.json
index f979bc87..608e1140 100644
--- a/src/main/resources/revapi/api-changes.json
+++ b/src/main/resources/revapi/api-changes.json
@@ -9,6 +9,12 @@
             "code": "java.method.abstractMethodAdded",
             "new": "method java.lang.Object 
org.apache.commons.rng.simple.internal.NativeSeedType::createSeed(int, int, 
int)",
             "justification": "Abstract method added to enum; all 
implementations are within this class. This is an internal package with no 
compatibility enforcement."
+          },
+          {
+            "ignore": true,
+            "code": "java.class.externalClassExposedInAPI",
+            "new": "interface 
org.apache.commons.rng.SplittableUniformRandomProvider",
+            "justification": "Split support was added to the client API and 
can be used by other modules."
           }
         ]
       }

Reply via email to