You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by ah...@apache.org on 2022/09/05 12:23:26 UTC

[commons-rng] branch master updated: RNG-180: Add a SplittableUniformRandomProvider interface

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


The following commit(s) were added to refs/heads/master by this push:
     new d8cef06a RNG-180: Add a SplittableUniformRandomProvider interface
d8cef06a is described below

commit d8cef06abf90e2effbb967899101d2b4fe090290
Author: Alex Herbert <ah...@apache.org>
AuthorDate: Mon Sep 5 13:23:21 2022 +0100

    RNG-180: Add a SplittableUniformRandomProvider interface
    
    Provide default implementations for spliterators to support parallel
    streams for the interface stream methods. This includes the parent
    UniformRandomProvider interface methods for primitive streams.
    
    Refactor stream tests for UniformRandomProvider to allow the same tests
    to be used on SplittableUniformRandomProvider.
---
 .../rng/SplittableUniformRandomProvider.java       | 207 ++++++
 .../commons/rng/UniformRandomProviderSupport.java  | 307 +++++++++
 .../commons/rng/BaseRandomProviderStreamTest.java  | 370 ++++++++++
 .../SplittableUniformRandomProviderStreamTest.java | 128 ++++
 .../rng/SplittableUniformRandomProviderTest.java   | 748 +++++++++++++++++++++
 .../rng/UniformRandomProviderStreamTest.java       | 122 ++++
 .../commons/rng/UniformRandomProviderTest.java     | 294 +-------
 src/changes/changes.xml                            |   6 +
 .../checkstyle/checkstyle-suppressions.xml         |   3 +
 src/main/resources/pmd/pmd-ruleset.xml             |  10 +
 10 files changed, 1904 insertions(+), 291 deletions(-)

diff --git a/commons-rng-client-api/src/main/java/org/apache/commons/rng/SplittableUniformRandomProvider.java b/commons-rng-client-api/src/main/java/org/apache/commons/rng/SplittableUniformRandomProvider.java
new file mode 100644
index 00000000..8ce6e7be
--- /dev/null
+++ b/commons-rng-client-api/src/main/java/org/apache/commons/rng/SplittableUniformRandomProvider.java
@@ -0,0 +1,207 @@
+/*
+ * 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;
+
+import java.util.Objects;
+import java.util.stream.DoubleStream;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * Applies to generators that can be split into two objects (the original and a new instance)
+ * each of which implements the same interface (and can be recursively split indefinitely).
+ * It is assumed that the two generators resulting from a split can be used concurrently on
+ * different threads.
+ *
+ * <p>Ideally all generators produced by recursive splitting from the original object are
+ * statistically independent and individually uniform. In this case it would be expected that
+ * the set of values collectively generated from a group of split generators would have the
+ * same statistical properties as the same number of values produced from a single generator
+ * object.
+ *
+ * @since 1.5
+ */
+public interface SplittableUniformRandomProvider extends UniformRandomProvider {
+    /**
+     * Creates a new random generator, split off from this one, that implements
+     * the {@link SplittableUniformRandomProvider} interface.
+     *
+     * <p>The current generator may be used a source of randomness to initialise the new instance.
+     * In this case repeat invocations of this method will return objects with a different
+     * initial state that are expected to be statistically independent.
+     *
+     * @return A new instance.
+     */
+    default SplittableUniformRandomProvider split() {
+        return split(this);
+    }
+
+    /**
+     * Creates a new random generator, split off from this one, that implements
+     * the {@link SplittableUniformRandomProvider} interface.
+     *
+     * @param source A source of randomness used to initialise the new instance.
+     * @return A new instance.
+     * @throws NullPointerException if {@code source} is null
+     */
+    SplittableUniformRandomProvider split(UniformRandomProvider source);
+
+    /**
+     * Returns an effectively unlimited stream of new random generators, each of which
+     * implements the {@link SplittableUniformRandomProvider} interface.
+     *
+     * <p>The current generator may be used a source of randomness to initialise the new instances.
+     *
+     * @return a stream of random generators.
+     */
+    default Stream<SplittableUniformRandomProvider> splits() {
+        return splits(Long.MAX_VALUE, this);
+    }
+
+    /**
+     * Returns an effectively unlimited stream of new random generators, each of which
+     * implements the {@link SplittableUniformRandomProvider} interface.
+     *
+     * @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.
+     * @return a stream of random generators.
+     * @throws NullPointerException if {@code source} is null
+     */
+    default Stream<SplittableUniformRandomProvider> splits(SplittableUniformRandomProvider source) {
+        return this.splits(Long.MAX_VALUE, source);
+    }
+
+    /**
+     * Returns a stream producing the given {@code streamSize} number of new random
+     * generators, each of which implements the {@link SplittableUniformRandomProvider}
+     * interface.
+     *
+     * <p>The current generator may be used a source of randomness to initialise the new instances.
+     *
+     * @param streamSize Number of objects to generate.
+     * @return a stream of random generators; the stream is limited to the given
+     * {@code streamSize}.
+     * @throws IllegalArgumentException if {@code streamSize} is negative.
+     */
+    default Stream<SplittableUniformRandomProvider> splits(long streamSize) {
+        return splits(streamSize, this);
+    }
+
+    /**
+     * Returns a stream producing the given {@code streamSize} number of new random
+     * generators, each of which implements the {@link SplittableUniformRandomProvider}
+     * interface.
+     *
+     * @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.
+     * @return a stream of random generators; the stream is limited to the given
+     * {@code streamSize}.
+     * @throws IllegalArgumentException if {@code streamSize} is negative.
+     * @throws NullPointerException if {@code source} is null
+     */
+    default Stream<SplittableUniformRandomProvider> splits(long streamSize,
+                                                           SplittableUniformRandomProvider source) {
+        UniformRandomProviderSupport.validateStreamSize(streamSize);
+        Objects.requireNonNull(source, "source");
+        return StreamSupport.stream(
+            new UniformRandomProviderSupport.ProviderSplitsSpliterator(0, streamSize, source, this), false);
+    }
+
+    @Override
+    default IntStream ints() {
+        return ints(Long.MAX_VALUE);
+    }
+
+    @Override
+    default IntStream ints(int origin, int bound) {
+        return ints(Long.MAX_VALUE, origin, bound);
+    }
+
+    @Override
+    default IntStream ints(long streamSize) {
+        UniformRandomProviderSupport.validateStreamSize(streamSize);
+        return StreamSupport.intStream(
+            new UniformRandomProviderSupport.ProviderIntsSpliterator(
+                0, streamSize, this, UniformRandomProvider::nextInt), false);
+    }
+
+    @Override
+    default IntStream ints(long streamSize, int origin, int bound) {
+        UniformRandomProviderSupport.validateStreamSize(streamSize);
+        UniformRandomProviderSupport.validateRange(origin, bound);
+        return StreamSupport.intStream(
+            new UniformRandomProviderSupport.ProviderIntsSpliterator(
+                0, streamSize, this, rng -> rng.nextInt(origin, bound)), false);
+    }
+
+    @Override
+    default LongStream longs() {
+        return longs(Long.MAX_VALUE);
+    }
+
+    @Override
+    default LongStream longs(long origin, long bound) {
+        return longs(Long.MAX_VALUE, origin, bound);
+    }
+
+    @Override
+    default LongStream longs(long streamSize) {
+        UniformRandomProviderSupport.validateStreamSize(streamSize);
+        return StreamSupport.longStream(
+            new UniformRandomProviderSupport.ProviderLongsSpliterator(
+                0, streamSize, this, UniformRandomProvider::nextLong), false);
+    }
+
+    @Override
+    default LongStream longs(long streamSize, long origin, long bound) {
+        UniformRandomProviderSupport.validateStreamSize(streamSize);
+        UniformRandomProviderSupport.validateRange(origin, bound);
+        return StreamSupport.longStream(
+            new UniformRandomProviderSupport.ProviderLongsSpliterator(
+                0, streamSize, this, rng -> rng.nextLong(origin, bound)), false);
+    }
+
+    @Override
+    default DoubleStream doubles() {
+        return doubles(Long.MAX_VALUE);
+    }
+
+    @Override
+    default DoubleStream doubles(double origin, double bound) {
+        return doubles(Long.MAX_VALUE, origin, bound);
+    }
+
+    @Override
+    default DoubleStream doubles(long streamSize) {
+        UniformRandomProviderSupport.validateStreamSize(streamSize);
+        return StreamSupport.doubleStream(
+            new UniformRandomProviderSupport.ProviderDoublesSpliterator(
+                0, streamSize, this, UniformRandomProvider::nextDouble), false);
+    }
+
+    @Override
+    default DoubleStream doubles(long streamSize, double origin, double bound) {
+        UniformRandomProviderSupport.validateStreamSize(streamSize);
+        UniformRandomProviderSupport.validateRange(origin, bound);
+        return StreamSupport.doubleStream(
+            new UniformRandomProviderSupport.ProviderDoublesSpliterator(
+                0, streamSize, this, rng -> rng.nextDouble(origin, bound)), false);
+    }
+}
diff --git a/commons-rng-client-api/src/main/java/org/apache/commons/rng/UniformRandomProviderSupport.java b/commons-rng-client-api/src/main/java/org/apache/commons/rng/UniformRandomProviderSupport.java
index b7981030..e4039c0c 100644
--- a/commons-rng-client-api/src/main/java/org/apache/commons/rng/UniformRandomProviderSupport.java
+++ b/commons-rng-client-api/src/main/java/org/apache/commons/rng/UniformRandomProviderSupport.java
@@ -16,6 +16,16 @@
  */
 package org.apache.commons.rng;
 
+import java.util.Objects;
+import java.util.Spliterator;
+import java.util.function.Consumer;
+import java.util.function.DoubleConsumer;
+import java.util.function.IntConsumer;
+import java.util.function.LongConsumer;
+import java.util.function.ToDoubleFunction;
+import java.util.function.ToIntFunction;
+import java.util.function.ToLongFunction;
+
 /**
  * Support for {@link UniformRandomProvider} default methods.
  *
@@ -30,6 +40,8 @@ final class UniformRandomProviderSupport {
     private static final String INVALID_RANGE = "Invalid range: [%s, %s)";
     /** 2^32. */
     private static final long POW_32 = 1L << 32;
+    /** Message when the consumer action is null. */
+    private static final String NULL_ACTION = "action must not be null";
 
     /** No instances. */
     private UniformRandomProviderSupport() {}
@@ -420,4 +432,299 @@ final class UniformRandomProviderSupport {
         }
         return v;
     }
+
+    // Spliterator support
+
+    /**
+     * Base class for spliterators for streams of values. Contains the range current position and
+     * end position. Splitting is expected to divide the range in half and create instances
+     * that span the two ranges.
+     */
+    private static class ProviderSpliterator {
+        /** The current position in the range. */
+        protected long position;
+        /** The upper limit of the range. */
+        protected final long end;
+
+        /**
+         * @param start Start position of the stream (inclusive).
+         * @param end Upper limit of the stream (exclusive).
+         */
+        ProviderSpliterator(long start, long end) {
+            position = start;
+            this.end = end;
+        }
+
+        // Methods required by all Spliterators
+
+        // See Spliterator.estimateSize()
+        public long estimateSize() {
+            return end - position;
+        }
+
+        // See Spliterator.characteristics()
+        public int characteristics() {
+            return Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.NONNULL | Spliterator.IMMUTABLE;
+        }
+    }
+
+    /**
+     * Spliterator for streams of SplittableUniformRandomProvider.
+     */
+    static class ProviderSplitsSpliterator extends ProviderSpliterator
+            implements Spliterator<SplittableUniformRandomProvider> {
+        /** Source of randomness used to initialise the new instances. */
+        private final SplittableUniformRandomProvider source;
+        /** Generator to split to create new instances. */
+        private final SplittableUniformRandomProvider rng;
+
+        /**
+         * @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 rng Generator to split to create new instances.
+         */
+        ProviderSplitsSpliterator(long start, long end,
+                                  SplittableUniformRandomProvider source,
+                                  SplittableUniformRandomProvider rng) {
+            super(start, end);
+            this.source = source;
+            this.rng = rng;
+        }
+
+        @Override
+        public Spliterator<SplittableUniformRandomProvider> trySplit() {
+            final long start = position;
+            final long middle = (start + end) >>> 1;
+            if (middle <= start) {
+                return null;
+            }
+            position = middle;
+            return new ProviderSplitsSpliterator(start, middle, source.split(), rng);
+        }
+
+        @Override
+        public boolean tryAdvance(Consumer<? super SplittableUniformRandomProvider> 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(rng.split(source));
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void forEachRemaining(Consumer<? super SplittableUniformRandomProvider> 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 SplittableUniformRandomProvider r = rng;
+                do {
+                    action.accept(r.split(s));
+                } while (++pos < last);
+            }
+        }
+    }
+
+    /**
+     * Spliterator for streams of int values that may be recursively split.
+     */
+    static class ProviderIntsSpliterator extends ProviderSpliterator
+            implements Spliterator.OfInt {
+        /** Source of randomness. */
+        private final SplittableUniformRandomProvider source;
+        /** Value generator function. */
+        private final ToIntFunction<SplittableUniformRandomProvider> gen;
+
+        /**
+         * @param start Start position of the stream (inclusive).
+         * @param end Upper limit of the stream (exclusive).
+         * @param source Source of randomness.
+         * @param gen Value generator function.
+         */
+        ProviderIntsSpliterator(long start, long end,
+                                SplittableUniformRandomProvider source,
+                                ToIntFunction<SplittableUniformRandomProvider> gen) {
+            super(start, end);
+            this.source = source;
+            this.gen = gen;
+        }
+
+        @Override
+        public Spliterator.OfInt trySplit() {
+            final long start = position;
+            final long middle = (start + end) >>> 1;
+            if (middle <= start) {
+                return null;
+            }
+            position = middle;
+            return new ProviderIntsSpliterator(start, middle, source.split(), gen);
+        }
+
+        @Override
+        public boolean tryAdvance(IntConsumer 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(gen.applyAsInt(source));
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void forEachRemaining(IntConsumer 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 ToIntFunction<SplittableUniformRandomProvider> g = gen;
+                do {
+                    action.accept(g.applyAsInt(s));
+                } while (++pos < last);
+            }
+        }
+    }
+
+    /**
+     * Spliterator for streams of long values that may be recursively split.
+     */
+    static class ProviderLongsSpliterator extends ProviderSpliterator
+            implements Spliterator.OfLong {
+        /** Source of randomness. */
+        private final SplittableUniformRandomProvider source;
+        /** Value generator function. */
+        private final ToLongFunction<SplittableUniformRandomProvider> gen;
+
+        /**
+         * @param start Start position of the stream (inclusive).
+         * @param end Upper limit of the stream (exclusive).
+         * @param source Source of randomness.
+         * @param gen Value generator function.
+         */
+        ProviderLongsSpliterator(long start, long end,
+                                SplittableUniformRandomProvider source,
+                                ToLongFunction<SplittableUniformRandomProvider> gen) {
+            super(start, end);
+            this.source = source;
+            this.gen = gen;
+        }
+
+        @Override
+        public Spliterator.OfLong trySplit() {
+            final long start = position;
+            final long middle = (start + end) >>> 1;
+            if (middle <= start) {
+                return null;
+            }
+            position = middle;
+            return new ProviderLongsSpliterator(start, middle, source.split(), gen);
+        }
+
+        @Override
+        public boolean tryAdvance(LongConsumer 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(gen.applyAsLong(source));
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void forEachRemaining(LongConsumer 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 ToLongFunction<SplittableUniformRandomProvider> g = gen;
+                do {
+                    action.accept(g.applyAsLong(s));
+                } while (++pos < last);
+            }
+        }
+    }
+
+    /**
+     * Spliterator for streams of double values that may be recursively split.
+     */
+    static class ProviderDoublesSpliterator extends ProviderSpliterator
+            implements Spliterator.OfDouble {
+        /** Source of randomness. */
+        private final SplittableUniformRandomProvider source;
+        /** Value generator function. */
+        private final ToDoubleFunction<SplittableUniformRandomProvider> gen;
+
+        /**
+         * @param start Start position of the stream (inclusive).
+         * @param end Upper limit of the stream (exclusive).
+         * @param source Source of randomness.
+         * @param gen Value generator function.
+         */
+        ProviderDoublesSpliterator(long start, long end,
+                                SplittableUniformRandomProvider source,
+                                ToDoubleFunction<SplittableUniformRandomProvider> gen) {
+            super(start, end);
+            this.source = source;
+            this.gen = gen;
+        }
+
+        @Override
+        public Spliterator.OfDouble trySplit() {
+            final long start = position;
+            final long middle = (start + end) >>> 1;
+            if (middle <= start) {
+                return null;
+            }
+            position = middle;
+            return new ProviderDoublesSpliterator(start, middle, source.split(), gen);
+        }
+
+        @Override
+        public boolean tryAdvance(DoubleConsumer 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(gen.applyAsDouble(source));
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public void forEachRemaining(DoubleConsumer 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 ToDoubleFunction<SplittableUniformRandomProvider> g = gen;
+                do {
+                    action.accept(g.applyAsDouble(s));
+                } while (++pos < last);
+            }
+        }
+    }
 }
diff --git a/commons-rng-client-api/src/test/java/org/apache/commons/rng/BaseRandomProviderStreamTest.java b/commons-rng-client-api/src/test/java/org/apache/commons/rng/BaseRandomProviderStreamTest.java
new file mode 100644
index 00000000..f243476b
--- /dev/null
+++ b/commons-rng-client-api/src/test/java/org/apache/commons/rng/BaseRandomProviderStreamTest.java
@@ -0,0 +1,370 @@
+/*
+ * 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;
+
+import java.util.Spliterator;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.stream.DoubleStream;
+import java.util.stream.IntStream;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Assertions;
+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;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Tests for default stream method implementations in {@link UniformRandomProvider} and derived
+ * interfaces.
+ *
+ * <p>This class exists to test that {@link UniformRandomProvider} and any derived interface that
+ * overloads the base implementation function identically for the stream based methods. Stream
+ * methods are asserted to call the corresponding single value generation method in the interface.
+ */
+abstract class BaseRandomProviderStreamTest {
+    private static final long STREAM_SIZE_ONE = 1;
+
+    static Stream<Arguments> invalidNextIntOriginBound() {
+        return UniformRandomProviderTest.invalidNextIntOriginBound();
+    }
+
+    static Stream<Arguments> invalidNextLongOriginBound() {
+        return UniformRandomProviderTest.invalidNextLongOriginBound();
+    }
+
+    static Stream<Arguments> invalidNextDoubleOriginBound() {
+        return UniformRandomProviderTest.invalidNextDoubleOriginBound();
+    }
+
+    static long[] streamSizes() {
+        return new long[] {0, 1, 13};
+    }
+
+    /**
+     * Creates the provider used to test the stream methods.
+     * The instance will be used to verify the following conditions:
+     * <ul>
+     * <li>Invalid stream sizes
+     * <li>Unspecified stream size has an iterator that initially reports Long.MAX_VALUE
+     * <li>Invalid bounds for the bounded stream methods
+     * </ul>
+     *
+     * @return the uniform random provider
+     */
+    abstract UniformRandomProvider create();
+
+    /**
+     * Creates the provider using the specified {@code values} for the
+     * {@link UniformRandomProvider#nextInt()} method. All other primitive
+     * generation methods should raise an exception to ensure the
+     * {@link UniformRandomProvider#ints()} method calls the correct generation
+     * method.
+     *
+     * @param values Values to return from the generation method.
+     * @return the uniform random provider
+     */
+    abstract UniformRandomProvider createInts(int[] values);
+
+    /**
+     * Creates the provider using the specified {@code values} for the
+     * {@link UniformRandomProvider#nextInt(int, int)} method. All other primitive
+     * generation methods should raise an exception to ensure the
+     * {@link UniformRandomProvider#ints(int, int)} method calls the correct
+     * generation method.
+     *
+     * @param values Values to return from the generation method.
+     * @param origin Origin for the generation method. Can be asserted to match the argument passed to the method.
+     * @param bound Bound for the generation method. Can be asserted to match the argument passed to the method.
+     * @return the uniform random provider
+     */
+    abstract UniformRandomProvider createInts(int[] values, int origin, int bound);
+
+    /**
+     * Creates the provider using the specified {@code values} for the
+     * {@link UniformRandomProvider#nextLong()} method.
+     * All other primitive generation methods should raise an exception to
+     * ensure the {@link UniformRandomProvider#longs()} method calls the correct
+     * generation method.
+     *
+     * @param values Values to return from the generation method.
+     * @return the uniform random provider
+     */
+    abstract UniformRandomProvider createLongs(long[] values);
+
+    /**
+     * Creates the provider using the specified {@code values} for the
+     * {@link UniformRandomProvider#nextLong(long, long)} method.
+     * All other primitive generation methods should raise an exception to
+     * ensure the {@link UniformRandomProvider#longs(long, long)} method calls the correct
+     * generation method.
+     *
+     * @param values Values to return from the generation method.
+     * @param origin Origin for the generation method. Can be asserted to match the argument passed to the method.
+     * @param bound Bound for the generation method. Can be asserted to match the argument passed to the method.
+     * @return the uniform random provider
+     */
+    abstract UniformRandomProvider createLongs(long[] values, long origin, long bound);
+
+    /**
+     * Creates the provider using the specified {@code values} for the
+     * {@link UniformRandomProvider#nextDouble()} method.
+     * All other primitive generation methods should raise an exception to
+     * ensure the {@link UniformRandomProvider#doubles()} method calls the correct
+     * generation method.
+     *
+     * @param values Values to return from the generation method.
+     * @return the uniform random provider
+     */
+    abstract UniformRandomProvider createDoubles(double[] values);
+
+    /**
+     * Creates the provider using the specified {@code values} for the
+     * {@link UniformRandomProvider#nextDouble(double, double)} method.
+     * All other primitive generation methods should raise an exception to
+     * ensure the {@link UniformRandomProvider#doubles(double, double)} method calls the correct
+     * generation method.
+     *
+     * @param values Values to return from the generation method.
+     * @param origin Origin for the generation method. Can be asserted to match the argument passed to the method.
+     * @param bound Bound for the generation method. Can be asserted to match the argument passed to the method.
+     * @return the uniform random provider
+     */
+    abstract UniformRandomProvider createDoubles(double[] values, double origin, double bound);
+
+    /**
+     * Gets the expected stream characteristics for the initial stream created with unlimited size.
+     *
+     * @return the characteristics
+     */
+    abstract int getCharacteristics();
+
+    @ParameterizedTest
+    @ValueSource(longs = {-1, -2, Long.MIN_VALUE})
+    void testInvalidStreamSizeThrows(long size) {
+        final UniformRandomProvider rng = create();
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.ints(size), "ints()");
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.ints(size, 1, 42), "ints(lower, upper)");
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.longs(size), "longs()");
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.longs(size, 3L, 33L), "longs(lower, upper)");
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.doubles(size), "doubles()");
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.doubles(size, 1.5, 2.75), "doubles(lower, upper)");
+    }
+
+    @Test
+    void testUnlimitedStreamSize() {
+        final UniformRandomProvider rng = create();
+        assertUnlimitedSpliterator(rng.ints().spliterator(), "ints()");
+        assertUnlimitedSpliterator(rng.ints(1, 42).spliterator(), "ints(lower, upper)");
+        assertUnlimitedSpliterator(rng.longs().spliterator(), "longs()");
+        assertUnlimitedSpliterator(rng.longs(1627384682623L, 32676823622343L).spliterator(), "longs(lower, upper)");
+        assertUnlimitedSpliterator(rng.doubles().spliterator(), "doubles()");
+        assertUnlimitedSpliterator(rng.doubles(1.5, 2.75).spliterator(), "doubles(lower, upper)");
+    }
+
+    /**
+     * Assert the spliterator has an unlimited expected size and the characteristics specified
+     * by {@link #getCharacteristics()}.
+     *
+     * @param spliterator Spliterator.
+     * @param msg Error message.
+     */
+    private void assertUnlimitedSpliterator(Spliterator<?> spliterator, String msg) {
+        assertSpliterator(spliterator, Long.MAX_VALUE, getCharacteristics(), msg);
+    }
+
+    /**
+     * Assert the spliterator has the expected size and characteristics.
+     *
+     * @param spliterator Spliterator.
+     * @param expectedSize Expected size.
+     * @param characteristics Expected characteristics.
+     * @param msg Error message.
+     * @see Spliterator#hasCharacteristics(int)
+     */
+    static void assertSpliterator(Spliterator<?> spliterator, long expectedSize, int characteristics, String msg) {
+        Assertions.assertEquals(expectedSize, spliterator.estimateSize(), msg);
+        Assertions.assertTrue(spliterator.hasCharacteristics(characteristics),
+            () -> String.format("%s: characteristics = %s, expected %s", msg,
+                Integer.toBinaryString(spliterator.characteristics()),
+                Integer.toBinaryString(characteristics)
+            ));
+    }
+
+    // Test stream methods throw immediately for invalid range arguments.
+
+    @ParameterizedTest
+    @MethodSource(value = {"invalidNextIntOriginBound"})
+    void testIntsOriginBoundThrows(int origin, int bound) {
+        final UniformRandomProvider rng = create();
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.ints(origin, bound));
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.ints(STREAM_SIZE_ONE, origin, bound));
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"invalidNextLongOriginBound"})
+    void testLongsOriginBoundThrows(long origin, long bound) {
+        final UniformRandomProvider rng = create();
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.longs(origin, bound));
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.longs(STREAM_SIZE_ONE, origin, bound));
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"invalidNextDoubleOriginBound"})
+    void testDoublesOriginBoundThrows(double origin, double bound) {
+        final UniformRandomProvider rng = create();
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.doubles(origin, bound));
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.doubles(STREAM_SIZE_ONE, origin, bound));
+    }
+
+    // Test stream methods call the correct generation method in the UniformRandomProvider.
+    // If range arguments are supplied they are asserted to be passed through.
+    // Streams are asserted to be sequential.
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testInts(long streamSize) {
+        final int[] values = ThreadLocalRandom.current().ints(streamSize).toArray();
+        final UniformRandomProvider rng = createInts(values);
+        final IntStream stream = rng.ints();
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testIntsOriginBound(long streamSize) {
+        final int origin = 13;
+        final int bound = 42;
+        final int[] values = ThreadLocalRandom.current().ints(streamSize, origin, bound).toArray();
+        final UniformRandomProvider rng = createInts(values, origin, bound);
+        final IntStream stream = rng.ints(origin, bound);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testIntsWithSize(long streamSize) {
+        final int[] values = ThreadLocalRandom.current().ints(streamSize).toArray();
+        final UniformRandomProvider rng = createInts(values);
+        final IntStream stream = rng.ints(streamSize);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testIntsOriginBoundWithSize(long streamSize) {
+        final int origin = 13;
+        final int bound = 42;
+        final int[] values = ThreadLocalRandom.current().ints(streamSize, origin, bound).toArray();
+        final UniformRandomProvider rng = createInts(values, origin, bound);
+        final IntStream stream = rng.ints(streamSize, origin, bound);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testLongs(long streamSize) {
+        final long[] values = ThreadLocalRandom.current().longs(streamSize).toArray();
+        final UniformRandomProvider rng = createLongs(values);
+        final LongStream stream = rng.longs();
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testLongsOriginBound(long streamSize) {
+        final long origin = 26278368423L;
+        final long bound = 422637723236L;
+        final long[] values = ThreadLocalRandom.current().longs(streamSize, origin, bound).toArray();
+        final UniformRandomProvider rng = createLongs(values, origin, bound);
+        final LongStream stream = rng.longs(origin, bound);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testLongsWithSize(long streamSize) {
+        final long[] values = ThreadLocalRandom.current().longs(streamSize).toArray();
+        final UniformRandomProvider rng = createLongs(values);
+        final LongStream stream = rng.longs(streamSize);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testLongsOriginBoundWithSize(long streamSize) {
+        final long origin = 26278368423L;
+        final long bound = 422637723236L;
+        final long[] values = ThreadLocalRandom.current().longs(streamSize, origin, bound).toArray();
+        final UniformRandomProvider rng = createLongs(values, origin, bound);
+        final LongStream stream = rng.longs(streamSize, origin, bound);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testDoubles(long streamSize) {
+        final double[] values = ThreadLocalRandom.current().doubles(streamSize).toArray();
+        final UniformRandomProvider rng = createDoubles(values);
+        final DoubleStream stream = rng.doubles();
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testDoublesOriginBound(long streamSize) {
+        final double origin = 1.23;
+        final double bound = 4.56;
+        final double[] values = ThreadLocalRandom.current().doubles(streamSize, origin, bound).toArray();
+        final UniformRandomProvider rng = createDoubles(values, origin, bound);
+        final DoubleStream stream = rng.doubles(origin, bound);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testDoublesWithSize(long streamSize) {
+        final double[] values = ThreadLocalRandom.current().doubles(streamSize).toArray();
+        final UniformRandomProvider rng = createDoubles(values);
+        final DoubleStream stream = rng.doubles(streamSize);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.toArray());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"streamSizes"})
+    void testDoublesOriginBoundWithSize(long streamSize) {
+        final double origin = 1.23;
+        final double bound = 4.56;
+        final double[] values = ThreadLocalRandom.current().doubles(streamSize, origin, bound).toArray();
+        final UniformRandomProvider rng = createDoubles(values, origin, bound);
+        final DoubleStream stream = rng.doubles(streamSize, origin, bound);
+        Assertions.assertFalse(stream.isParallel());
+        Assertions.assertArrayEquals(values, stream.toArray());
+    }
+}
diff --git a/commons-rng-client-api/src/test/java/org/apache/commons/rng/SplittableUniformRandomProviderStreamTest.java b/commons-rng-client-api/src/test/java/org/apache/commons/rng/SplittableUniformRandomProviderStreamTest.java
new file mode 100644
index 00000000..91c3c74f
--- /dev/null
+++ b/commons-rng-client-api/src/test/java/org/apache/commons/rng/SplittableUniformRandomProviderStreamTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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;
+
+import java.util.Spliterator;
+import org.junit.jupiter.api.Assertions;
+
+/**
+ * Tests for default stream method implementations in {@link UniformRandomProvider}.
+ */
+class SplittableUniformRandomProviderStreamTest extends BaseRandomProviderStreamTest {
+
+    /**
+     * Dummy class for checking the behavior of the SplittableUniformRandomProvider.
+     */
+    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 method should not be invoked");
+        }
+    }
+
+    @Override
+    UniformRandomProvider create() {
+        return DummyGenerator.INSTANCE;
+    }
+
+    @Override
+    UniformRandomProvider createInts(int[] values) {
+        return new DummyGenerator() {
+            private int i;
+            @Override
+            public int nextInt() {
+                return values[i++];
+            }
+        };
+    }
+
+    @Override
+    UniformRandomProvider createInts(int[] values, int origin, int bound) {
+        return new DummyGenerator() {
+            private int i;
+            @Override
+            public int nextInt(int o, int b) {
+                Assertions.assertEquals(origin, o, "origin");
+                Assertions.assertEquals(bound, b, "bound");
+                return values[i++];
+            }
+        };
+    }
+
+    @Override
+    UniformRandomProvider createLongs(long[] values) {
+        return new DummyGenerator() {
+            private int i;
+            @Override
+            public long nextLong() {
+                return values[i++];
+            }
+        };
+    }
+
+    @Override
+    UniformRandomProvider createLongs(long[] values, long origin, long bound) {
+        return new DummyGenerator() {
+            private int i;
+            @Override
+            public long nextLong(long o, long b) {
+                Assertions.assertEquals(origin, o, "origin");
+                Assertions.assertEquals(bound, b, "bound");
+                return values[i++];
+            }
+        };
+    }
+
+    @Override
+    UniformRandomProvider createDoubles(double[] values) {
+        return new DummyGenerator() {
+            private int i;
+            @Override
+            public double nextDouble() {
+                return values[i++];
+            }
+        };
+    }
+
+    @Override
+    UniformRandomProvider createDoubles(double[] values, double origin, double bound) {
+        return new DummyGenerator() {
+            private int i;
+            @Override
+            public double nextDouble(double o, double b) {
+                Assertions.assertEquals(origin, o, "origin");
+                Assertions.assertEquals(bound, b, "bound");
+                return values[i++];
+            }
+        };
+    }
+
+    @Override
+    int getCharacteristics() {
+        // Since this is splittable it supports sized and sub-sized.
+        // Add non-null although this may not be relevant for primitive streams.
+        return Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.NONNULL | Spliterator.IMMUTABLE;
+    }
+}
diff --git a/commons-rng-client-api/src/test/java/org/apache/commons/rng/SplittableUniformRandomProviderTest.java b/commons-rng-client-api/src/test/java/org/apache/commons/rng/SplittableUniformRandomProviderTest.java
new file mode 100644
index 00000000..de556849
--- /dev/null
+++ b/commons-rng-client-api/src/test/java/org/apache/commons/rng/SplittableUniformRandomProviderTest.java
@@ -0,0 +1,748 @@
+/*
+ * 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;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Spliterator;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+import java.util.function.DoubleConsumer;
+import java.util.function.IntConsumer;
+import java.util.function.LongConsumer;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Assertions;
+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;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Tests for split method implementations in
+ * {@link SplittableUniformRandomProvider}.
+ *
+ * <p>This class verifies all exception conditions for the split methods and the
+ * arguments to the methods to stream RNGs. Exception conditions and sequential
+ * (default) output from the primitive stream methods are tested in
+ * {@link SplittableUniformRandomProviderStreamTest}.
+ *
+ * <p>Parallel streams (RNGs and primitives) are tested using a splittable
+ * generator that outputs a unique sequence using an atomic counter that is
+ * thread-safe.
+ */
+class SplittableUniformRandomProviderTest {
+    private static final long STREAM_SIZE_ONE = 1;
+    /** The expected characteristics for the spliterator from the splittable stream. */
+    private static final int SPLITERATOR_CHARACTERISTICS =
+        Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.NONNULL | 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");
+        }
+    }
+
+    /**
+     * 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 outputting a fixed value from the nextLong() method even under
+     * recursive splitting. Splitting creates a new instance seeded with the nextLong value
+     * from the source of randomness. This can be used to distinguish self-seeding from
+     * seeding with an alternative source.
+     */
+    private class FixedGenerator implements SplittableUniformRandomProvider {
+        /** The value for nextLong. */
+        private final long value;
+
+        /**
+         * @param value Fixed value.
+         */
+        FixedGenerator(long value) {
+            this.value = value;
+        }
+
+        @Override
+        public long nextLong() {
+            return value;
+        }
+
+        @Override
+        public SplittableUniformRandomProvider split(UniformRandomProvider source) {
+            return new FixedGenerator(source.nextLong());
+        }
+    }
+
+    /**
+     * Class to track recursive splitting and iterating over a fixed set of values.
+     * Splitting without a source of randomness returns the same instance; with a
+     * source of randomness will throw an exception. All generation methods throw an
+     * exception.
+     *
+     * <p>An atomic counter is maintained to allow concurrent return of unique
+     * values from a fixed array. The values are expected to be maintained in child
+     * classes. Any generation methods that are overridden for tests should
+     * be thread-safe, e.g. returning {@code values[count.getAndIncrement()]}.
+     *
+     * <p>A count of the number of splits is maintained. This is not used for assertions
+     * to avoid test failures that may occur when streams are split differently, or not
+     * at all, by the current JVM. The count can be used to debug splitting behavior
+     * on JVM implementations.
+     */
+    private static class CountingGenerator extends DummyGenerator {
+        /** The split count. Incrementded when the generator is split. */
+        protected final AtomicInteger splitCount = new AtomicInteger();
+        /** The count of returned values. */
+        protected final AtomicInteger count = new AtomicInteger();
+
+        @Override
+        public SplittableUniformRandomProvider split() {
+            splitCount.getAndIncrement();
+            return this;
+        }
+    }
+
+    /**
+     * Class to return the same instance when splitting without a source of randomness;
+     * with a source of randomness will throw an exception. All generation methods
+     * throw an exception. Any generation methods that are overridden for tests should
+     * be thread-safe.
+     */
+    private abstract static class SingleInstanceGenerator extends DummyGenerator {
+        @Override
+        public SplittableUniformRandomProvider split() {
+            return this;
+        }
+    }
+
+    /**
+     * Thread and stream sizes used to test parallel streams.
+     *
+     * @return the arguments
+     */
+    static Stream<Arguments> threadAndStreamSizes() {
+        return Stream.of(
+            Arguments.of(1, 16),
+            Arguments.of(2, 16),
+            Arguments.of(4, 16),
+            Arguments.of(8, 16),
+            Arguments.of(4, 2),
+            Arguments.of(8, 4)
+        );
+    }
+
+    /**
+     * Execute the task in a ForkJoinPool with the specified level of parallelism. Any
+     * parallel stream executing in the task should be limited to the specified level of
+     * parallelism.
+     *
+     * <p><b>Note</b>
+     *
+     * <p>This is a JDK undocumented feature of streams to use the enclosing ForkJoinPool
+     * in-place of {@link ForkJoinPool#commonPool()}; this behaviour may be subject to
+     * change.
+     *
+     * <p>Here the intention is to force the parallel stream to execute with a varying
+     * number of threads. Note that debugging using the {@link CountingGenerator}
+     * indicates that the number of splits is not influenced by the enclosing pool
+     * parallelism but rather the number of stream elements and possibly the
+     * <em>standard</em> number of available processors. Further testing on Linux using
+     * {@code numactl -C 1} to limit the number of processors returns 1 for
+     * {@link ForkJoinPool#getCommonPoolParallelism()} and
+     * {@link Runtime#availableProcessors()} with no change in the number of splits
+     * performed by parallel streams. This indicates the splitting of parallel streams may
+     * not respect the limits imposed on the executing JVM. However this does mean that
+     * tests using this method do test the splitting of the stream, irrespective of
+     * configured parallelism when executed on a machine that has multiple CPU cores, i.e.
+     * the <em>potential</em> for parallelism.
+     *
+     * <p>It is unknown if the parallel streams will split when executed on a true single-core
+     * JVM such as that provided by a continuous integration build environment running for
+     * example in a virtual machine.
+     *
+     * @param <T> Return type of the task.
+     * @param parallelism Level of parallelism.
+     * @param task Task.
+     * @return the task result
+     * @throws InterruptedException the interrupted exception
+     * @throws ExecutionException the execution exception
+     */
+    private static <T> T execute(int parallelism, Callable<T> task) throws InterruptedException, ExecutionException {
+        final ForkJoinPool threadPool = new ForkJoinPool(parallelism);
+        try {
+            return threadPool.submit(task).get();
+        } finally {
+            threadPool.shutdown();
+        }
+    }
+
+    /**
+     * 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");
+    }
+
+    @Test
+    void testDefaultSplit() {
+        // Create the split result so we can check the return value
+        final SplittableUniformRandomProvider expected = new DummyGenerator();
+        // Implement split(UniformRandomProvider)
+        final SplittableUniformRandomProvider rng = new DummyGenerator() {
+            @Override
+            public SplittableUniformRandomProvider split(UniformRandomProvider source) {
+                Assertions.assertSame(this, source, "default split should use itself as the source");
+                return expected;
+            }
+        };
+        // Test the default split()
+        Assertions.assertSame(expected, rng.split());
+    }
+
+    // Tests for splitting the stream of splittable RNGs
+
+    @ParameterizedTest
+    @ValueSource(longs = {-1, -2, Long.MIN_VALUE})
+    void testSplitsInvalidStreamSizeThrows(long size) {
+        final SplittableUniformRandomProvider rng = DummyGenerator.INSTANCE;
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.splits(size), "splits(size)");
+        final SplittableUniformRandomProvider source = new SequenceGenerator(42);
+        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.splits(size, source), "splits(size, source)");
+    }
+
+    @Test
+    void testSplitsUnlimitedStreamSize() {
+        final SplittableUniformRandomProvider rng = DummyGenerator.INSTANCE;
+        assertUnlimitedSpliterator(rng.splits().spliterator(), "splits()");
+        final SplittableUniformRandomProvider source = new SequenceGenerator(42);
+        assertUnlimitedSpliterator(rng.splits(source).spliterator(), "splits(source)");
+    }
+
+    /**
+     * Assert the spliterator has an unlimited expected size and the characteristics for a sized
+     * non-null immutable stream.
+     *
+     * @param spliterator Spliterator.
+     * @param msg Error message.
+     */
+    private static void assertUnlimitedSpliterator(Spliterator<?> spliterator, String msg) {
+        BaseRandomProviderStreamTest.assertSpliterator(spliterator, Long.MAX_VALUE, SPLITERATOR_CHARACTERISTICS, msg);
+    }
+
+    @Test
+    void testSplitsNullSourceThrows() {
+        final SplittableUniformRandomProvider rng = DummyGenerator.INSTANCE;
+        final SplittableUniformRandomProvider source = null;
+        Assertions.assertThrows(NullPointerException.class, () -> rng.splits(source));
+        Assertions.assertThrows(NullPointerException.class, () -> rng.splits(STREAM_SIZE_ONE, source));
+    }
+
+    /**
+     * Test the splits method. The test asserts that a parallel stream of RNGs output a
+     * sequence using a specialised sequence generator that maintains the sequence output
+     * under recursive splitting.
+     */
+    @ParameterizedTest
+    @MethodSource(value = {"threadAndStreamSizes"})
+    void testSplitsParallel(int threads, long streamSize) throws InterruptedException, ExecutionException {
+        final long start = Integer.toUnsignedLong(ThreadLocalRandom.current().nextInt());
+        final long[] actual = execute(threads, (Callable<long[]>) () -> {
+            // The splits method will use itself as the source and the output should be the sequence
+            final SplittableUniformRandomProvider rng = new SequenceGenerator(start);
+            final SplittableUniformRandomProvider[] rngs =
+                    rng.splits(streamSize).parallel().toArray(SplittableUniformRandomProvider[]::new);
+            // 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);
+            Arrays.stream(rngs).forEach(r -> {
+                Assertions.assertTrue(observed.add(r), "Instance should be unique");
+                Assertions.assertEquals(SequenceGenerator.class, r.getClass());
+            });
+            // Get output from the unique RNGs: these return from the same atomic sequence
+            return Arrays.stream(rngs).mapToLong(UniformRandomProvider::nextLong).toArray();
+        });
+        // Required to reorder the sequence to ascending
+        Arrays.sort(actual);
+        final long[] expected = LongStream.range(start, start + streamSize).toArray();
+        Assertions.assertArrayEquals(expected, actual);
+    }
+
+    /**
+     * Test the splits method. The test asserts that a parallel stream of RNGs output a
+     * sequence using a specialised sequence generator that maintains the sequence output
+     * under recursive splitting. The sequence is used to seed a fixed generator. The stream
+     * instances are verified to be the correct class type.
+     */
+    @ParameterizedTest
+    @MethodSource(value = {"threadAndStreamSizes"})
+    void testSplitsParallelWithSource(int threads, long streamSize) throws InterruptedException, ExecutionException {
+        final long start = Integer.toUnsignedLong(ThreadLocalRandom.current().nextInt());
+        final long[] actual = execute(threads, (Callable<long[]>) () -> {
+            // This generator defines the instances created.
+            // It should not be split without a source.
+            // Seed with something not the start value.
+            final SplittableUniformRandomProvider rng = new FixedGenerator(~start) {
+                @Override
+                public SplittableUniformRandomProvider split() {
+                    throw new UnsupportedOperationException("The split method should not be invoked");
+                }
+            };
+            // The splits method will use this to seed each instance.
+            // This generator is split within the spliterator.
+            final SplittableUniformRandomProvider source = new SequenceGenerator(start);
+            final SplittableUniformRandomProvider[] rngs =
+                rng.splits(streamSize, source).parallel().toArray(SplittableUniformRandomProvider[]::new);
+            // 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);
+            Arrays.stream(rngs).forEach(r -> {
+                Assertions.assertTrue(observed.add(r), "Instance should be unique");
+                Assertions.assertEquals(FixedGenerator.class, r.getClass());
+            });
+            // Get output from the unique RNGs: these return from the same atomic sequence
+            return Arrays.stream(rngs).mapToLong(UniformRandomProvider::nextLong).toArray();
+        });
+        // Required to reorder the sequence to ascending
+        Arrays.sort(actual);
+        final long[] expected = LongStream.range(start, start + streamSize).toArray();
+        Assertions.assertArrayEquals(expected, actual);
+    }
+
+    @Test
+    void testSplitsSpliterator() {
+        final int start = 42;
+        final SplittableUniformRandomProvider rng = new SequenceGenerator(start);
+
+        // 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");
+
+        // The expected value is incremented for each generation call
+        final long[] expected = {start};
+
+        // s2. Test advance
+        for (long newSize = s2.estimateSize(); newSize-- > 0;) {
+            Assertions.assertTrue(s2.tryAdvance(r -> Assertions.assertEquals(expected[0]++, r.nextLong())));
+            Assertions.assertEquals(newSize, s2.estimateSize(), "s2 size estimate");
+        }
+        Assertions.assertFalse(s2.tryAdvance(r -> failSpliteratorShouldBeEmpty()));
+        s2.forEachRemaining(r -> failSpliteratorShouldBeEmpty());
+
+        // s3. Test forEachRemaining
+        s3.forEachRemaining(r -> Assertions.assertEquals(expected[0]++, r.nextLong()));
+        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());
+    }
+
+    // Tests for splitting the primitive streams to test support for parallel execution
+
+    @ParameterizedTest
+    @MethodSource(value = {"threadAndStreamSizes"})
+    void testIntsParallelWithSize(int threads, long streamSize) throws InterruptedException, ExecutionException {
+        final int[] values = ThreadLocalRandom.current().ints(streamSize).toArray();
+        final CountingGenerator rng = new CountingGenerator() {
+            @Override
+            public int nextInt() {
+                return values[count.getAndIncrement()];
+            }
+        };
+        final int[] actual = execute(threads, (Callable<int[]>) () ->
+            rng.ints(streamSize).parallel().toArray()
+        );
+        Arrays.sort(values);
+        Arrays.sort(actual);
+        Assertions.assertArrayEquals(values, actual);
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"threadAndStreamSizes"})
+    void testIntsParallelOriginBoundWithSize(int threads, long streamSize) throws InterruptedException, ExecutionException {
+        final int origin = 13;
+        final int bound = 42;
+        final int[] values = ThreadLocalRandom.current().ints(streamSize, origin, bound).toArray();
+        final CountingGenerator rng = new CountingGenerator() {
+            @Override
+            public int nextInt(int o, int b) {
+                Assertions.assertEquals(origin, o, "origin");
+                Assertions.assertEquals(bound, b, "bound");
+                return values[count.getAndIncrement()];
+            }
+        };
+        final int[] actual = execute(threads, (Callable<int[]>) () ->
+            rng.ints(streamSize, origin, bound).parallel().toArray()
+        );
+        Arrays.sort(values);
+        Arrays.sort(actual);
+        Assertions.assertArrayEquals(values, actual);
+    }
+
+    @Test
+    void testIntsSpliterator() {
+        final int start = 42;
+        final SplittableUniformRandomProvider rng = new SingleInstanceGenerator() {
+            private final AtomicInteger value = new AtomicInteger(start);
+
+            @Override
+            public int nextInt() {
+                return value.getAndIncrement();
+            }
+        };
+
+        // Split a large spliterator into four smaller ones;
+        // each is used to test different functionality
+        final long size = 41;
+        Spliterator.OfInt s1 = rng.ints(size).spliterator();
+        Assertions.assertEquals(size, s1.estimateSize());
+        final Spliterator.OfInt s2 = s1.trySplit();
+        final Spliterator.OfInt s3 = s1.trySplit();
+        final Spliterator.OfInt 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.OfInt other = s1.trySplit();
+            Assertions.assertEquals(currentSize, s1.estimateSize() + other.estimateSize());
+            s1 = other;
+        }
+        Assertions.assertNull(s1.trySplit(), "Cannot split when size <= 1");
+
+        // The expected value is incremented for each generation call
+        final int[] expected = {start};
+
+        // s2. Test advance
+        for (long newSize = s2.estimateSize(); newSize-- > 0;) {
+            Assertions.assertTrue(s2.tryAdvance((IntConsumer) i -> Assertions.assertEquals(expected[0]++, i)));
+            Assertions.assertEquals(newSize, s2.estimateSize(), "s2 size estimate");
+        }
+        Assertions.assertFalse(s2.tryAdvance((IntConsumer) i -> failSpliteratorShouldBeEmpty()));
+        s2.forEachRemaining((IntConsumer) i -> failSpliteratorShouldBeEmpty());
+
+        // s3. Test forEachRemaining
+        s3.forEachRemaining((IntConsumer) i -> Assertions.assertEquals(expected[0]++, i));
+        Assertions.assertEquals(0, s3.estimateSize());
+        s3.forEachRemaining((IntConsumer) i -> failSpliteratorShouldBeEmpty());
+
+        // s4. Test tryAdvance and forEachRemaining when the action throws an exception
+        final IllegalStateException ex = new IllegalStateException();
+        final IntConsumer badAction = i -> {
+            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((IntConsumer) i -> failSpliteratorShouldBeEmpty());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"threadAndStreamSizes"})
+    void testLongsParallelWithSize(int threads, long streamSize) throws InterruptedException, ExecutionException {
+        final long[] values = ThreadLocalRandom.current().longs(streamSize).toArray();
+        final CountingGenerator rng = new CountingGenerator() {
+            @Override
+            public long nextLong() {
+                return values[count.getAndIncrement()];
+            }
+        };
+        final long[] actual = execute(threads, (Callable<long[]>) () ->
+            rng.longs(streamSize).parallel().toArray()
+        );
+        Arrays.sort(values);
+        Arrays.sort(actual);
+        Assertions.assertArrayEquals(values, actual);
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"threadAndStreamSizes"})
+    void testLongsParallelOriginBoundWithSize(int threads, long streamSize) throws InterruptedException, ExecutionException {
+        final long origin = 195267376168313L;
+        final long bound = 421268681268318L;
+        final long[] values = ThreadLocalRandom.current().longs(streamSize, origin, bound).toArray();
+        final CountingGenerator rng = new CountingGenerator() {
+            @Override
+            public long nextLong(long o, long b) {
+                Assertions.assertEquals(origin, o, "origin");
+                Assertions.assertEquals(bound, b, "bound");
+                return values[count.getAndIncrement()];
+            }
+        };
+        final long[] actual = execute(threads, (Callable<long[]>) () ->
+            rng.longs(streamSize, origin, bound).parallel().toArray()
+        );
+        Arrays.sort(values);
+        Arrays.sort(actual);
+        Assertions.assertArrayEquals(values, actual);
+    }
+
+    @Test
+    void testLongsSpliterator() {
+        final long start = 42;
+        final SplittableUniformRandomProvider rng = new SingleInstanceGenerator() {
+            private final AtomicLong value = new AtomicLong(start);
+
+            @Override
+            public long nextLong() {
+                return value.getAndIncrement();
+            }
+        };
+
+        // Split a large spliterator into four smaller ones;
+        // each is used to test different functionality
+        final long size = 41;
+        Spliterator.OfLong s1 = rng.longs(size).spliterator();
+        Assertions.assertEquals(size, s1.estimateSize());
+        final Spliterator.OfLong s2 = s1.trySplit();
+        final Spliterator.OfLong s3 = s1.trySplit();
+        final Spliterator.OfLong 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.OfLong other = s1.trySplit();
+            Assertions.assertEquals(currentSize, s1.estimateSize() + other.estimateSize());
+            s1 = other;
+        }
+        Assertions.assertNull(s1.trySplit(), "Cannot split when size <= 1");
+
+        // The expected value is incremented for each generation call
+        final long[] expected = {start};
+
+        // s2. Test advance
+        for (long newSize = s2.estimateSize(); newSize-- > 0;) {
+            Assertions.assertTrue(s2.tryAdvance((LongConsumer) i -> Assertions.assertEquals(expected[0]++, i)));
+            Assertions.assertEquals(newSize, s2.estimateSize(), "s2 size estimate");
+        }
+        Assertions.assertFalse(s2.tryAdvance((LongConsumer) i -> failSpliteratorShouldBeEmpty()));
+        s2.forEachRemaining((LongConsumer) i -> failSpliteratorShouldBeEmpty());
+
+        // s3. Test forEachRemaining
+        s3.forEachRemaining((LongConsumer) i -> Assertions.assertEquals(expected[0]++, i));
+        Assertions.assertEquals(0, s3.estimateSize());
+        s3.forEachRemaining((LongConsumer) i -> failSpliteratorShouldBeEmpty());
+
+        // s4. Test tryAdvance and forEachRemaining when the action throws an exception
+        final IllegalStateException ex = new IllegalStateException();
+        final LongConsumer badAction = i -> {
+            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((LongConsumer) i -> failSpliteratorShouldBeEmpty());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"threadAndStreamSizes"})
+    void testDoublesParallelWithSize(int threads, long streamSize) throws InterruptedException, ExecutionException {
+        final double[] values = ThreadLocalRandom.current().doubles(streamSize).toArray();
+        final CountingGenerator rng = new CountingGenerator() {
+            @Override
+            public double nextDouble() {
+                return values[count.getAndIncrement()];
+            }
+        };
+        final double[] actual = execute(threads, (Callable<double[]>) () ->
+            rng.doubles(streamSize).parallel().toArray()
+        );
+        Arrays.sort(values);
+        Arrays.sort(actual);
+        Assertions.assertArrayEquals(values, actual);
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"threadAndStreamSizes"})
+    void testDoublesParallelOriginBoundWithSize(int threads, long streamSize) throws InterruptedException, ExecutionException {
+        final double origin = 0.123;
+        final double bound = 0.789;
+        final double[] values = ThreadLocalRandom.current().doubles(streamSize, origin, bound).toArray();
+        final CountingGenerator rng = new CountingGenerator() {
+            @Override
+            public double nextDouble(double o, double b) {
+                Assertions.assertEquals(origin, o, "origin");
+                Assertions.assertEquals(bound, b, "bound");
+                return values[count.getAndIncrement()];
+            }
+        };
+        final double[] actual = execute(threads, (Callable<double[]>) () ->
+            rng.doubles(streamSize, origin, bound).parallel().toArray()
+        );
+        Arrays.sort(values);
+        Arrays.sort(actual);
+        Assertions.assertArrayEquals(values, actual);
+    }
+
+    @Test
+    void testDoublesSpliterator() {
+        // Due to lack of an AtomicDouble this uses an AtomicInteger. Any int value can be
+        // represented as a double and the increment operator functions without loss of
+        // precision (the same is not true if using an AtomicLong with >53 bits of precision).
+        final int start = 42;
+        final SplittableUniformRandomProvider rng = new SingleInstanceGenerator() {
+            private final AtomicInteger value = new AtomicInteger(start);
+
+            @Override
+            public double nextDouble() {
+                return value.getAndIncrement();
+            }
+        };
+
+        // Split a large spliterator into four smaller ones;
+        // each is used to test different functionality
+        final long size = 41;
+        Spliterator.OfDouble s1 = rng.doubles(size).spliterator();
+        Assertions.assertEquals(size, s1.estimateSize());
+        final Spliterator.OfDouble s2 = s1.trySplit();
+        final Spliterator.OfDouble s3 = s1.trySplit();
+        final Spliterator.OfDouble s4 = s2.trySplit();
+        Assertions.assertEquals(size, s1.estimateSize() + s2.estimateSize() + s3.estimateSize() + s4.estimateSize());
+
+        // s1. Test cannot split indefinitely
+        while (s1.estimateSize() > 1) {
+            final double currentSize = s1.estimateSize();
+            final Spliterator.OfDouble other = s1.trySplit();
+            Assertions.assertEquals(currentSize, s1.estimateSize() + other.estimateSize());
+            s1 = other;
+        }
+        Assertions.assertNull(s1.trySplit(), "Cannot split when size <= 1");
+
+        // The expected value is incremented for each generation call
+        final double[] expected = {start};
+
+        // s2. Test advance
+        for (double newSize = s2.estimateSize(); newSize-- > 0;) {
+            Assertions.assertTrue(s2.tryAdvance((DoubleConsumer) i -> Assertions.assertEquals(expected[0]++, i)));
+            Assertions.assertEquals(newSize, s2.estimateSize(), "s2 size estimate");
+        }
+        Assertions.assertFalse(s2.tryAdvance((DoubleConsumer) i -> failSpliteratorShouldBeEmpty()));
+        s2.forEachRemaining((DoubleConsumer) i -> failSpliteratorShouldBeEmpty());
+
+        // s3. Test forEachRemaining
+        s3.forEachRemaining((DoubleConsumer) i -> Assertions.assertEquals(expected[0]++, i));
+        Assertions.assertEquals(0, s3.estimateSize());
+        s3.forEachRemaining((DoubleConsumer) i -> failSpliteratorShouldBeEmpty());
+
+        // s4. Test tryAdvance and forEachRemaining when the action throws an exception
+        final IllegalStateException ex = new IllegalStateException();
+        final DoubleConsumer badAction = i -> {
+            throw ex;
+        };
+        final double 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((DoubleConsumer) i -> failSpliteratorShouldBeEmpty());
+    }
+}
diff --git a/commons-rng-client-api/src/test/java/org/apache/commons/rng/UniformRandomProviderStreamTest.java b/commons-rng-client-api/src/test/java/org/apache/commons/rng/UniformRandomProviderStreamTest.java
new file mode 100644
index 00000000..01601e31
--- /dev/null
+++ b/commons-rng-client-api/src/test/java/org/apache/commons/rng/UniformRandomProviderStreamTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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;
+
+import java.util.Spliterator;
+import org.junit.jupiter.api.Assertions;
+
+/**
+ * Tests for default stream method implementations in {@link UniformRandomProvider}.
+ */
+class UniformRandomProviderStreamTest extends BaseRandomProviderStreamTest {
+
+    /**
+     * Dummy class for checking the behavior of the UniformRandomProvider.
+     */
+    private static class DummyGenerator implements UniformRandomProvider {
+        /** An instance. */
+        static final DummyGenerator INSTANCE = new DummyGenerator();
+
+        @Override
+        public long nextLong() {
+            throw new UnsupportedOperationException("The nextLong method should not be invoked");
+        }
+    }
+
+    @Override
+    UniformRandomProvider create() {
+        return DummyGenerator.INSTANCE;
+    }
+
+    @Override
+    UniformRandomProvider createInts(int[] values) {
+        return new DummyGenerator() {
+            private int i;
+            @Override
+            public int nextInt() {
+                return values[i++];
+            }
+        };
+    }
+
+    @Override
+    UniformRandomProvider createInts(int[] values, int origin, int bound) {
+        return new DummyGenerator() {
+            private int i;
+            @Override
+            public int nextInt(int o, int b) {
+                Assertions.assertEquals(origin, o, "origin");
+                Assertions.assertEquals(bound, b, "bound");
+                return values[i++];
+            }
+        };
+    }
+
+    @Override
+    UniformRandomProvider createLongs(long[] values) {
+        return new DummyGenerator() {
+            private int i;
+            @Override
+            public long nextLong() {
+                return values[i++];
+            }
+        };
+    }
+
+    @Override
+    UniformRandomProvider createLongs(long[] values, long origin, long bound) {
+        return new DummyGenerator() {
+            private int i;
+            @Override
+            public long nextLong(long o, long b) {
+                Assertions.assertEquals(origin, o, "origin");
+                Assertions.assertEquals(bound, b, "bound");
+                return values[i++];
+            }
+        };
+    }
+
+    @Override
+    UniformRandomProvider createDoubles(double[] values) {
+        return new DummyGenerator() {
+            private int i;
+            @Override
+            public double nextDouble() {
+                return values[i++];
+            }
+        };
+    }
+
+    @Override
+    UniformRandomProvider createDoubles(double[] values, double origin, double bound) {
+        return new DummyGenerator() {
+            private int i;
+            @Override
+            public double nextDouble(double o, double b) {
+                Assertions.assertEquals(origin, o, "origin");
+                Assertions.assertEquals(bound, b, "bound");
+                return values[i++];
+            }
+        };
+    }
+
+    @Override
+    int getCharacteristics() {
+        // The current stream produced by the generate method only returns immutable
+        return Spliterator.IMMUTABLE;
+    }
+}
diff --git a/commons-rng-client-api/src/test/java/org/apache/commons/rng/UniformRandomProviderTest.java b/commons-rng-client-api/src/test/java/org/apache/commons/rng/UniformRandomProviderTest.java
index 0cd70765..9462d046 100644
--- a/commons-rng-client-api/src/test/java/org/apache/commons/rng/UniformRandomProviderTest.java
+++ b/commons-rng-client-api/src/test/java/org/apache/commons/rng/UniformRandomProviderTest.java
@@ -25,9 +25,6 @@ import java.util.SplittableRandom;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.function.LongSupplier;
 import java.util.stream.Collectors;
-import java.util.stream.DoubleStream;
-import java.util.stream.IntStream;
-import java.util.stream.LongStream;
 import java.util.stream.Stream;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -40,14 +37,13 @@ import org.junit.jupiter.params.provider.ValueSource;
 /**
  * Tests for default method implementations in {@link UniformRandomProvider}.
  *
- * <p>This class verifies all exception conditions for the range methods and
- * the range arguments to the stream methods. Streams methods are asserted
- * to call the corresponding single value generation method in the interface.
+ * <p>This class verifies all exception conditions for the range methods.
  * Single value generation methods are asserted using a test of uniformity
  * from multiple samples.
+ *
+ * <p>Stream methods are tested {@link UniformRandomProviderStreamTest}.
  */
 class UniformRandomProviderTest {
-    private static final long STREAM_SIZE_ONE = 1;
     /** Sample size for statistical uniformity tests. */
     private static final int SAMPLE_SIZE = 1000;
     /** Sample size for statistical uniformity tests as a BigDecimal. */
@@ -128,10 +124,6 @@ class UniformRandomProviderTest {
         );
     }
 
-    static long[] streamSizes() {
-        return new long[] {0, 1, 13};
-    }
-
     /**
      * Creates a functional random generator by implementing the
      * {@link UniformRandomProvider#nextLong} method with a high quality source of randomness.
@@ -385,286 +377,6 @@ class UniformRandomProviderTest {
         }
     }
 
-    @ParameterizedTest
-    @ValueSource(longs = {-1, -2, Long.MIN_VALUE})
-    void testInvalidStreamSizeThrows(long size) {
-        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
-        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.ints(size), "ints");
-        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.ints(size, 1, 42), "ints(lower, upper)");
-        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.longs(size), "longs");
-        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.longs(size, 3L, 33L), "longs(lower, upper)");
-        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.doubles(size), "doubles");
-        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.doubles(size, 1.5, 2.75), "doubles(lower, upper)");
-    }
-
-    @Test
-    void testUnlimitedStreamSize() {
-        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
-        Assertions.assertEquals(Long.MAX_VALUE, rng.ints().spliterator().estimateSize(), "ints");
-        Assertions.assertEquals(Long.MAX_VALUE, rng.ints(1, 42).spliterator().estimateSize(), "ints(lower, upper)");
-        Assertions.assertEquals(Long.MAX_VALUE, rng.longs().spliterator().estimateSize(), "longs");
-        Assertions.assertEquals(Long.MAX_VALUE, rng.longs(3L, 33L).spliterator().estimateSize(), "longs(lower, upper)");
-        Assertions.assertEquals(Long.MAX_VALUE, rng.doubles().spliterator().estimateSize(), "doubles");
-        Assertions.assertEquals(Long.MAX_VALUE, rng.doubles(1.5, 2.75).spliterator().estimateSize(), "doubles(lower, upper)");
-    }
-
-    // Test stream methods throw immediately for invalid range arguments.
-
-    @ParameterizedTest
-    @MethodSource(value = {"invalidNextIntOriginBound"})
-    void testIntsOriginBoundThrows(int origin, int bound) {
-        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
-        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.ints(origin, bound));
-        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.ints(STREAM_SIZE_ONE, origin, bound));
-    }
-
-    @ParameterizedTest
-    @MethodSource(value = {"invalidNextLongOriginBound"})
-    void testLongsOriginBoundThrows(long origin, long bound) {
-        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
-        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.longs(origin, bound));
-        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.longs(STREAM_SIZE_ONE, origin, bound));
-    }
-
-    @ParameterizedTest
-    @MethodSource(value = {"invalidNextDoubleOriginBound"})
-    void testDoublesOriginBoundThrows(double origin, double bound) {
-        final UniformRandomProvider rng = DummyGenerator.INSTANCE;
-        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.doubles(origin, bound));
-        Assertions.assertThrows(IllegalArgumentException.class, () -> rng.doubles(STREAM_SIZE_ONE, origin, bound));
-    }
-
-    // Test stream methods call the correct generation method in the UniformRandomProvider.
-    // If range arguments are supplied they are asserted to be passed through.
-
-    @ParameterizedTest
-    @MethodSource(value = {"streamSizes"})
-    void testInts(long streamSize) {
-        final int[] values = ThreadLocalRandom.current().ints(streamSize).toArray();
-        final UniformRandomProvider rng = new DummyGenerator() {
-            private int i;
-            @Override
-            public int nextInt() {
-                return values[i++];
-            }
-        };
-
-        final IntStream stream = rng.ints();
-        Assertions.assertFalse(stream.isParallel());
-        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
-    }
-
-    @ParameterizedTest
-    @MethodSource(value = {"streamSizes"})
-    void testIntsOriginBound(long streamSize) {
-        final int origin = 13;
-        final int bound = 42;
-        final int[] values = ThreadLocalRandom.current().ints(streamSize, origin, bound).toArray();
-        final UniformRandomProvider rng = new DummyGenerator() {
-            private int i;
-            @Override
-            public int nextInt(int o, int b) {
-                Assertions.assertEquals(origin, o, "origin");
-                Assertions.assertEquals(bound, b, "bound");
-                return values[i++];
-            }
-        };
-
-        final IntStream stream = rng.ints(origin, bound);
-        Assertions.assertFalse(stream.isParallel());
-        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
-    }
-
-    @ParameterizedTest
-    @MethodSource(value = {"streamSizes"})
-    void testIntsWithSize(long streamSize) {
-        final int[] values = ThreadLocalRandom.current().ints(streamSize).toArray();
-        final UniformRandomProvider rng = new DummyGenerator() {
-            private int i;
-            @Override
-            public int nextInt() {
-                return values[i++];
-            }
-        };
-
-        final IntStream stream = rng.ints(streamSize);
-        Assertions.assertFalse(stream.isParallel());
-        Assertions.assertArrayEquals(values, stream.toArray());
-    }
-
-    @ParameterizedTest
-    @MethodSource(value = {"streamSizes"})
-    void testIntsOriginBoundWithSize(long streamSize) {
-        final int origin = 13;
-        final int bound = 42;
-        final int[] values = ThreadLocalRandom.current().ints(streamSize, origin, bound).toArray();
-        final UniformRandomProvider rng = new DummyGenerator() {
-            private int i;
-            @Override
-            public int nextInt(int o, int b) {
-                Assertions.assertEquals(origin, o, "origin");
-                Assertions.assertEquals(bound, b, "bound");
-                return values[i++];
-            }
-        };
-
-        final IntStream stream = rng.ints(streamSize, origin, bound);
-        Assertions.assertFalse(stream.isParallel());
-        Assertions.assertArrayEquals(values, stream.toArray());
-    }
-
-    @ParameterizedTest
-    @MethodSource(value = {"streamSizes"})
-    void testLongs(long streamSize) {
-        final long[] values = ThreadLocalRandom.current().longs(streamSize).toArray();
-        final UniformRandomProvider rng = new DummyGenerator() {
-            private int i;
-            @Override
-            public long nextLong() {
-                return values[i++];
-            }
-        };
-
-        final LongStream stream = rng.longs();
-        Assertions.assertFalse(stream.isParallel());
-        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
-    }
-
-    @ParameterizedTest
-    @MethodSource(value = {"streamSizes"})
-    void testLongsOriginBound(long streamSize) {
-        final long origin = 13;
-        final long bound = 42;
-        final long[] values = ThreadLocalRandom.current().longs(streamSize, origin, bound).toArray();
-        final UniformRandomProvider rng = new DummyGenerator() {
-            private int i;
-            @Override
-            public long nextLong(long o, long b) {
-                Assertions.assertEquals(origin, o, "origin");
-                Assertions.assertEquals(bound, b, "bound");
-                return values[i++];
-            }
-        };
-
-        final LongStream stream = rng.longs(origin, bound);
-        Assertions.assertFalse(stream.isParallel());
-        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
-    }
-
-    @ParameterizedTest
-    @MethodSource(value = {"streamSizes"})
-    void testLongsWithSize(long streamSize) {
-        final long[] values = ThreadLocalRandom.current().longs(streamSize).toArray();
-        final UniformRandomProvider rng = new DummyGenerator() {
-            private int i;
-            @Override
-            public long nextLong() {
-                return values[i++];
-            }
-        };
-
-        final LongStream stream = rng.longs(streamSize);
-        Assertions.assertFalse(stream.isParallel());
-        Assertions.assertArrayEquals(values, stream.toArray());
-    }
-
-    @ParameterizedTest
-    @MethodSource(value = {"streamSizes"})
-    void testLongsOriginBoundWithSize(long streamSize) {
-        final long origin = 13;
-        final long bound = 42;
-        final long[] values = ThreadLocalRandom.current().longs(streamSize, origin, bound).toArray();
-        final UniformRandomProvider rng = new DummyGenerator() {
-            private int i;
-            @Override
-            public long nextLong(long o, long b) {
-                Assertions.assertEquals(origin, o, "origin");
-                Assertions.assertEquals(bound, b, "bound");
-                return values[i++];
-            }
-        };
-
-        final LongStream stream = rng.longs(streamSize, origin, bound);
-        Assertions.assertFalse(stream.isParallel());
-        Assertions.assertArrayEquals(values, stream.toArray());
-    }
-
-    @ParameterizedTest
-    @MethodSource(value = {"streamSizes"})
-    void testDoubles(long streamSize) {
-        final double[] values = ThreadLocalRandom.current().doubles(streamSize).toArray();
-        final UniformRandomProvider rng = new DummyGenerator() {
-            private int i;
-            @Override
-            public double nextDouble() {
-                return values[i++];
-            }
-        };
-
-        final DoubleStream stream = rng.doubles();
-        Assertions.assertFalse(stream.isParallel());
-        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
-    }
-
-    @ParameterizedTest
-    @MethodSource(value = {"streamSizes"})
-    void testDoublesOriginBound(long streamSize) {
-        final double origin = 13;
-        final double bound = 42;
-        final double[] values = ThreadLocalRandom.current().doubles(streamSize, origin, bound).toArray();
-        final UniformRandomProvider rng = new DummyGenerator() {
-            private int i;
-            @Override
-            public double nextDouble(double o, double b) {
-                Assertions.assertEquals(origin, o, "origin");
-                Assertions.assertEquals(bound, b, "bound");
-                return values[i++];
-            }
-        };
-
-        final DoubleStream stream = rng.doubles(origin, bound);
-        Assertions.assertFalse(stream.isParallel());
-        Assertions.assertArrayEquals(values, stream.limit(streamSize).toArray());
-    }
-
-    @ParameterizedTest
-    @MethodSource(value = {"streamSizes"})
-    void testDoublesWithSize(long streamSize) {
-        final double[] values = ThreadLocalRandom.current().doubles(streamSize).toArray();
-        final UniformRandomProvider rng = new DummyGenerator() {
-            private int i;
-            @Override
-            public double nextDouble() {
-                return values[i++];
-            }
-        };
-
-        final DoubleStream stream = rng.doubles(streamSize);
-        Assertions.assertFalse(stream.isParallel());
-        Assertions.assertArrayEquals(values, stream.toArray());
-    }
-
-    @ParameterizedTest
-    @MethodSource(value = {"streamSizes"})
-    void testDoublesOriginBoundWithSize(long streamSize) {
-        final double origin = 13;
-        final double bound = 42;
-        final double[] values = ThreadLocalRandom.current().doubles(streamSize, origin, bound).toArray();
-        final UniformRandomProvider rng = new DummyGenerator() {
-            private int i;
-            @Override
-            public double nextDouble(double o, double b) {
-                Assertions.assertEquals(origin, o, "origin");
-                Assertions.assertEquals(bound, b, "bound");
-                return values[i++];
-            }
-        };
-
-        final DoubleStream stream = rng.doubles(streamSize, origin, bound);
-        Assertions.assertFalse(stream.isParallel());
-        Assertions.assertArrayEquals(values, stream.toArray());
-    }
-
     // Statistical tests for uniform distribution
 
     // Monobit tests
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 040888e4..f94e787a 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -86,6 +86,12 @@ behavioural compatibility between releases; derived types
 may break behavioural compatibility. Any functional changes
 will be recorded in the release notes.
 ">
+      <action dev="aherbert" type="add" issue="180">
+        New "SplittableUniformRandomProvider" interface to allow splitting a RNG into two
+        objects, each of which implements the same interface (and can be recursively split
+        indefinitely). Add default methods to support parallel stream implementations
+        of the UniformRandomProvider stream methods.
+      </action>
       <action dev="aherbert" type="add" issue="179">
         "FastLoadedDiceRollerDiscreteSampler": Distribution sampler that uses the
         Fast Loaded Dice Roller (FLDR) algorithm for exact sampling from a discrete
diff --git a/src/main/resources/checkstyle/checkstyle-suppressions.xml b/src/main/resources/checkstyle/checkstyle-suppressions.xml
index 7317f671..b3c71e1a 100644
--- a/src/main/resources/checkstyle/checkstyle-suppressions.xml
+++ b/src/main/resources/checkstyle/checkstyle-suppressions.xml
@@ -26,6 +26,9 @@
   <suppress checks="UnnecessaryParentheses" files=".*stress[/\\]StressTestCommand\.java$" lines="672" />
   <!-- Special to allow withUniformRandomProvider to act as a constructor. -->
   <suppress checks="HiddenField" files=".*Sampler\.java$" message="'rng' hides a field." />
+  <!-- Methods have the names from the Spliterator interface that is implemented by child classes.
+       Classes are package-private and should not require documentation. -->
+  <suppress checks="MissingJavadocMethod" files="[\\/]UniformRandomProviderSupport\.java$" lines="461-466"/>
   <!-- Be more lenient on tests. -->
   <suppress checks="Javadoc" files=".*[/\\]test[/\\].*" />
   <suppress checks="MultipleStringLiterals" files=".*[/\\]test[/\\].*" />
diff --git a/src/main/resources/pmd/pmd-ruleset.xml b/src/main/resources/pmd/pmd-ruleset.xml
index 1d4f0ecf..9c4e4daa 100644
--- a/src/main/resources/pmd/pmd-ruleset.xml
+++ b/src/main/resources/pmd/pmd-ruleset.xml
@@ -244,6 +244,16 @@
     </properties>
   </rule>
 
+  <rule ref="category/java/documentation.xml/CommentRequired">
+    <properties>
+      <!-- Public methods have the names from the Spliterator interface that is implemented by
+           child classes. These cannot inherit javadoc as the Spliterator interface must be
+           generic-typed by the child class and the parent does not implement Spliterator. -->
+      <property name="violationSuppressXPath"
+        value="./ancestor-or-self::ClassOrInterfaceDeclaration[@SimpleName='ProviderSpliterator']"/>
+    </properties>
+  </rule>
+
   <rule ref="category/java/errorprone.xml/AvoidLiteralsInIfCondition">
     <properties>
       <property name="ignoreMagicNumbers" value="-1,0,1" />