diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 292e918e33b0bb974c28f64ec7d66cbc99c489cb..63df563ec2d5a97decf6d62210b0ea771017f5ec 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -11,6 +11,7 @@
 use Drupal\Core\Logger\RfcLogLevel;
 use Drupal\Core\Render\Markup;
 use Drupal\Component\Render\MarkupInterface;
+use Drupal\Core\Test\TestDatabase;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Site\Settings;
 use Drupal\Core\Utility\Error;
@@ -622,7 +623,7 @@ function drupal_valid_test_ua($new_prefix = NULL) {
   // string.
   $http_user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : NULL;
   $user_agent = isset($_COOKIE['SIMPLETEST_USER_AGENT']) ? $_COOKIE['SIMPLETEST_USER_AGENT'] : $http_user_agent;
-  if (isset($user_agent) && preg_match("/^(simpletest\d+):(.+):(.+):(.+)$/", $user_agent, $matches)) {
+  if (isset($user_agent) && preg_match("/^simple(\w+\d+):(.+):(.+):(.+)$/", $user_agent, $matches)) {
     list(, $prefix, $time, $salt, $hmac) = $matches;
     $check_string = $prefix . ':' . $time . ':' . $salt;
     // Read the hash salt prepared by drupal_generate_test_ua().
@@ -630,7 +631,8 @@ function drupal_valid_test_ua($new_prefix = NULL) {
     // handlers are set up. While Drupal's error handling may be properly
     // configured on production sites, the server's PHP error_reporting may not.
     // Ensure that no information leaks on production sites.
-    $key_file = DRUPAL_ROOT . '/sites/simpletest/' . substr($prefix, 10) . '/.htkey';
+    $test_db = new TestDatabase($prefix);
+    $key_file = DRUPAL_ROOT . '/' . $test_db->getTestSitePath() . '/.htkey';
     if (!is_readable($key_file)) {
       header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
       exit;
@@ -661,7 +663,8 @@ function drupal_generate_test_ua($prefix) {
 
   if (!isset($key) || $last_prefix != $prefix) {
     $last_prefix = $prefix;
-    $key_file = DRUPAL_ROOT . '/sites/simpletest/' . substr($prefix, 10) . '/.htkey';
+    $test_db = new TestDatabase($prefix);
+    $key_file = DRUPAL_ROOT . '/' . $test_db->getTestSitePath() . '/.htkey';
     // When issuing an outbound HTTP client request from within an inbound test
     // request, then the outbound request has to use the same User-Agent header
     // as the inbound request. A newly generated private key for the same test
@@ -686,7 +689,7 @@ function drupal_generate_test_ua($prefix) {
   // Generate a moderately secure HMAC based on the database credentials.
   $salt = uniqid('', TRUE);
   $check_string = $prefix . ':' . time() . ':' . $salt;
-  return $check_string . ':' . Crypt::hmacBase64($check_string, $key);
+  return 'simple' . $check_string . ':' . Crypt::hmacBase64($check_string, $key);
 }
 
 /**
diff --git a/core/lib/Drupal/Core/Command/DbDumpCommand.php b/core/lib/Drupal/Core/Command/DbDumpCommand.php
index 274f8f658e0875df952f13c97df193770345b51b..264bd3f2a1e37566c8262cac78fecf5a05a19bb6 100644
--- a/core/lib/Drupal/Core/Command/DbDumpCommand.php
+++ b/core/lib/Drupal/Core/Command/DbDumpCommand.php
@@ -33,7 +33,7 @@ class DbDumpCommand extends DbCommandBase {
    *
    * @var array
    */
-  protected $excludeTables = ['simpletest.+'];
+  protected $excludeTables = ['test[0-9]+'];
 
   /**
    * {@inheritdoc}
diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php
index 3ccb2d74df4ed3cc9f9dc184da32f70df24b2b26..a132ff1714e75f03ee91b7cf79b7481d927d945b 100644
--- a/core/lib/Drupal/Core/DrupalKernel.php
+++ b/core/lib/Drupal/Core/DrupalKernel.php
@@ -18,6 +18,7 @@
 use Drupal\Core\Http\TrustedHostsRequestFactory;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Site\Settings;
+use Drupal\Core\Test\TestDatabase;
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\ClassLoader\ApcClassLoader;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -365,7 +366,8 @@ public static function findSitePath(Request $request, $require_settings = TRUE,
 
     // Check for a simpletest override.
     if ($test_prefix = drupal_valid_test_ua()) {
-      return 'sites/simpletest/' . substr($test_prefix, 10);
+      $test_db = new TestDatabase($test_prefix);
+      return $test_db->getTestSitePath();
     }
 
     // Determine whether multi-site functionality is enabled.
@@ -944,6 +946,7 @@ public static function bootEnvironment($app_root = NULL) {
     // Indicate that code is operating in a test child site.
     if (!defined('DRUPAL_TEST_IN_CHILD_SITE')) {
       if ($test_prefix = drupal_valid_test_ua()) {
+        $test_db = new TestDatabase($test_prefix);
         // Only code that interfaces directly with tests should rely on this
         // constant; e.g., the error/exception handler conditionally adds further
         // error information into HTTP response headers that are consumed by
@@ -958,7 +961,7 @@ public static function bootEnvironment($app_root = NULL) {
 
         // Log fatal errors to the test site directory.
         ini_set('log_errors', 1);
-        ini_set('error_log', $app_root . '/sites/simpletest/' . substr($test_prefix, 10) . '/error.log');
+        ini_set('error_log', $app_root . '/' . $test_db->getTestSitePath() . '/error.log');
 
         // Ensure that a rewritten settings.php is used if opcache is on.
         ini_set('opcache.validate_timestamps', 'on');
diff --git a/core/lib/Drupal/Core/Test/TestDatabase.php b/core/lib/Drupal/Core/Test/TestDatabase.php
index a0121dc22603114328a198d0cb91e14c081038e9..4ee1bc013087a84309b773dc3bcaa014f0e4372c 100644
--- a/core/lib/Drupal/Core/Test/TestDatabase.php
+++ b/core/lib/Drupal/Core/Test/TestDatabase.php
@@ -5,11 +5,31 @@
 use Drupal\Core\Database\ConnectionNotDefinedException;
 use Drupal\Core\Database\Database;
 
+// This class has a dependency on file_directory_os_temp().
+// @todo https://www.drupal.org/node/2794249 Remove once function moved to a
+//   class.
+require_once __DIR__ . '/../../../../includes/file.inc';
+
 /**
  * Provides helper methods for interacting with the Simpletest database.
  */
 class TestDatabase {
 
+  /**
+   * A random number used to ensure that test fixtures are unique to each test
+   * method.
+   *
+   * @var int
+   */
+  protected $lockId;
+
+  /**
+   * The test database prefix.
+   *
+   * @var string
+   */
+  protected $databasePrefix;
+
   /**
    * Returns the database connection to the site running Simpletest.
    *
@@ -41,4 +61,115 @@ public static function getConnection() {
     return $connection;
   }
 
+  /**
+   * TestDatabase constructor.
+   *
+   * @param string|null $db_prefix
+   *   If not provided a new test lock is generated.
+   *
+   * @throws \InvalidArgumentException
+   *   Thrown when $db_prefix does not match the regular expression.
+   */
+  public function __construct($db_prefix = NULL) {
+    if ($db_prefix === NULL) {
+      $this->lockId = $this->getTestLock();
+      $this->databasePrefix = 'test' . $this->lockId;
+    }
+    else {
+      $this->databasePrefix = $db_prefix;
+      // It is possible that we're running a test inside a test. In which case
+      // $db_prefix will be something like test12345678test90123456 and the
+      // generated lock ID for the running test method would be 90123456.
+      preg_match('/test(\d+)$/', $db_prefix, $matches);
+      if (!isset($matches[1])) {
+        throw new \InvalidArgumentException("Invalid database prefix: $db_prefix");
+      }
+      $this->lockId = $matches[1];
+    }
+  }
+
+  /**
+   * Gets the relative path to the test site directory.
+   *
+   * @return string
+   *   The relative path to the test site directory.
+   */
+  public function getTestSitePath() {
+    return 'sites/simpletest/' . $this->lockId;
+  }
+
+  /**
+   * Gets the test database prefix.
+   *
+   * @return string
+   *   The test database prefix.
+   */
+  public function getDatabasePrefix() {
+    return $this->databasePrefix;
+  }
+
+  /**
+   * Generates a unique lock ID for the test method.
+   *
+   * @return int
+   *   The unique lock ID for the test method.
+   */
+  protected function getTestLock() {
+    // Ensure that the generated lock ID is not in use, which may happen when
+    // tests are run concurrently.
+    do {
+      $lock_id = mt_rand(10000000, 99999999);
+      if (@symlink(__FILE__, $this->getLockFile($lock_id)) === FALSE) {
+        $lock_id = NULL;
+      }
+    } while ($lock_id === NULL);
+    return $lock_id;
+  }
+
+  /**
+   * Releases a test lock.
+   *
+   * This should only be called once the related test fixtures have been cleaned
+   * up.
+   */
+  public function releaseTestLock() {
+    $concurrency = getenv('RUN_TESTS_CONCURRENCY');
+    // If we're doing concurrent testing then ensure no dupes in the whole run.
+    if (!$concurrency || $concurrency == 1) {
+      unlink($this->getLockFile($this->lockId));
+    }
+  }
+
+  /**
+   * Releases all test locks.
+   *
+   * This should only be called once all the test fixtures have been cleaned up.
+   */
+  public static function releaseAllTestLocks() {
+    $tmp = file_directory_os_temp();
+    $dir = dir($tmp);
+    while (($entry = $dir->read()) !== FALSE) {
+      if ($entry === '.' || $entry === '..') {
+        continue;
+      }
+      $entry_path = $tmp . '/' . $entry;
+      if (preg_match('/^test_\d+/', $entry) && is_link($entry_path)) {
+        unlink($entry_path);
+      }
+    }
+  }
+
+  /**
+   * Gets the lock file path.
+   *
+   * @param int $lock_id
+   *   The test method lock ID.
+   *
+   * @return string
+   *   A file path to the symbolic link that prevents the lock ID being re-used.
+   */
+  protected function getLockFile($lock_id) {
+    return file_directory_os_temp() . '/test_' . $lock_id;
+  }
+
 }
diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module
index dbdb7a8fad8198c6fe5cd93e79ff0b545efa287c..77128aaad98352d75d4cdbf83542fcc2c39f383b 100644
--- a/core/modules/simpletest/simpletest.module
+++ b/core/modules/simpletest/simpletest.module
@@ -535,7 +535,8 @@ function simpletest_last_test_get($test_id) {
  *   Whether any fatal errors were found.
  */
 function simpletest_log_read($test_id, $database_prefix, $test_class) {
-  $log = DRUPAL_ROOT . '/sites/simpletest/' . substr($database_prefix, 10) . '/error.log';
+  $test_db = new TestDatabase($database_prefix);
+  $log = DRUPAL_ROOT . '/' . $test_db->getTestSitePath() . '/error.log';
   $found = FALSE;
   if (file_exists($log)) {
     foreach (file($log) as $line) {
diff --git a/core/modules/simpletest/src/TestBase.php b/core/modules/simpletest/src/TestBase.php
index 944810a4aa8e09fc62968aa3fa157c92b89508b0..5fb36a44906a7a4b85845784f8cab5864757ae8d 100644
--- a/core/modules/simpletest/src/TestBase.php
+++ b/core/modules/simpletest/src/TestBase.php
@@ -1108,14 +1108,9 @@ public function run(array $methods = array()) {
    * @see drupal_valid_test_ua()
    */
   private function prepareDatabasePrefix() {
-    // Ensure that the generated test site directory does not exist already,
-    // which may happen with a large amount of concurrent threads and
-    // long-running tests.
-    do {
-      $suffix = mt_rand(100000, 999999);
-      $this->siteDirectory = 'sites/simpletest/' . $suffix;
-      $this->databasePrefix = 'simpletest' . $suffix;
-    } while (is_dir(DRUPAL_ROOT . '/' . $this->siteDirectory));
+    $test_db = new TestDatabase();
+    $this->siteDirectory = $test_db->getTestSitePath();
+    $this->databasePrefix = $test_db->getDatabasePrefix();
 
     // As soon as the database prefix is set, the test might start to execute.
     // All assertions as well as the SimpleTest batch operations are associated
@@ -1371,6 +1366,10 @@ private function restoreEnvironment() {
     // Delete test site directory.
     file_unmanaged_delete_recursive($this->siteDirectory, array($this, 'filePreDeleteCallback'));
 
+    // Release the test lock.
+    $test_db = new TestDatabase($test_prefix);
+    $test_db->releaseTestLock();
+
     // Restore original database connection.
     Database::removeConnection('default');
     Database::renameConnection('simpletest_original_default', 'default');
diff --git a/core/modules/simpletest/src/Tests/SimpleTestBrowserTest.php b/core/modules/simpletest/src/Tests/SimpleTestBrowserTest.php
index 91e37d33a69653d18b09b76c2b39c23b6e842803..c4528a53adfd9e6c9bdff4de921f060041630156 100644
--- a/core/modules/simpletest/src/Tests/SimpleTestBrowserTest.php
+++ b/core/modules/simpletest/src/Tests/SimpleTestBrowserTest.php
@@ -89,7 +89,7 @@ public function testUserAgentValidation() {
     $HTTP_path = $system_path . '/tests/http.php/user/login';
     $https_path = $system_path . '/tests/https.php/user/login';
     // Generate a valid simpletest User-Agent to pass validation.
-    $this->assertTrue(preg_match('/simpletest\d+/', $this->databasePrefix, $matches), 'Database prefix contains simpletest prefix.');
+    $this->assertTrue(preg_match('/test\d+/', $this->databasePrefix, $matches), 'Database prefix contains test prefix.');
     $test_ua = drupal_generate_test_ua($matches[0]);
     $this->additionalCurlOptions = array(CURLOPT_USERAGENT => $test_ua);
 
diff --git a/core/modules/simpletest/src/Tests/SimpleTestTest.php b/core/modules/simpletest/src/Tests/SimpleTestTest.php
index 19ab17f586a4a762f8d252f8bd32ab38680184bc..ff20f5e9680f611265581d29848902cb957be2c6 100644
--- a/core/modules/simpletest/src/Tests/SimpleTestTest.php
+++ b/core/modules/simpletest/src/Tests/SimpleTestTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\simpletest\Tests;
 
 use Drupal\Component\Utility\Crypt;
+use Drupal\Core\Test\TestDatabase;
 use Drupal\simpletest\WebTestBase;
 
 /**
@@ -154,7 +155,8 @@ function stubTest() {
     // test in a test since the prefix has changed.
     // @see \Drupal\Core\Test\HttpClientMiddleware\TestHttpClientMiddleware::onBeforeSendRequest()
     // @see drupal_generate_test_ua();
-    $key_file = DRUPAL_ROOT . '/sites/simpletest/' . substr($this->databasePrefix, 10) . '/.htkey';
+    $test_db = new TestDatabase($this->databasePrefix);
+    $key_file = DRUPAL_ROOT . '/' . $test_db->getTestSitePath() . '/.htkey';
     $private_key = Crypt::randomBytesBase64(55);
     $site_path = $this->container->get('site.path');
     file_put_contents($key_file, $private_key);
diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php
index b0f90ed3715303c563635dc5dc63592a53ae9dc4..2e03af972738fda92053fdbba6cd0963a9fec8f2 100644
--- a/core/modules/simpletest/src/WebTestBase.php
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -1162,9 +1162,7 @@ protected function curlInitialize() {
     }
     // We set the user agent header on each request so as to use the current
     // time and a new uniqid.
-    if (preg_match('/simpletest\d+/', $this->databasePrefix, $matches)) {
-      curl_setopt($this->curlHandle, CURLOPT_USERAGENT, drupal_generate_test_ua($matches[0]));
-    }
+    curl_setopt($this->curlHandle, CURLOPT_USERAGENT, drupal_generate_test_ua($this->databasePrefix));
   }
 
   /**
diff --git a/core/scripts/run-tests.sh b/core/scripts/run-tests.sh
index cd00d7431e7c668a3e9412ec14f3a408dc40e6d8..d064331e825c603f4b8d1a8e7f2cf1acce9bc46f 100755
--- a/core/scripts/run-tests.sh
+++ b/core/scripts/run-tests.sh
@@ -11,6 +11,7 @@
 use Drupal\Core\Database\Database;
 use Drupal\Core\Site\Settings;
 use Drupal\Core\StreamWrapper\PublicStream;
+use Drupal\Core\Test\TestDatabase;
 use Drupal\Core\Test\TestRunnerKernel;
 use Drupal\simpletest\Form\SimpletestResultsForm;
 use Drupal\simpletest\TestBase;
@@ -124,6 +125,12 @@
 // Stop the timer.
 simpletest_script_reporter_timer_stop();
 
+// Ensure all test locks are released once finished. If tests are run with a
+// concurrency of 1 the each test will clean up it's own lock. Test locks are
+// not released if using a higher concurrency to ensure each test method has
+// unique fixtures.
+TestDatabase::releaseAllTestLocks();
+
 // Display results before database is cleared.
 if ($args['browser']) {
   simpletest_script_open_browser();
@@ -447,6 +454,10 @@ function simpletest_script_init() {
   $_SERVER['PHP_SELF'] = $path . '/index.php';
   $_SERVER['HTTP_USER_AGENT'] = 'Drupal command line';
 
+  if ($args['concurrency'] > 1) {
+    putenv('RUN_TESTS_CONCURRENCY=' . $args['concurrency']);
+  }
+
   if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
     // Ensure that any and all environment variables are changed to https://.
     foreach ($_SERVER as $key => $value) {
@@ -665,7 +676,8 @@ function simpletest_script_execute_batch($test_classes) {
           );
           if ($args['die-on-fail']) {
             list($db_prefix) = simpletest_last_test_get($child['test_id']);
-            $test_directory = 'sites/simpletest/' . substr($db_prefix, 10);
+            $test_db = new TestDatabase($db_prefix);
+            $test_directory = $test_db->getTestSitePath();
             echo 'Simpletest database and files kept and test exited immediately on fail so should be reproducible if you change settings.php to use the database prefix ' . $db_prefix . ' and config directories in ' . $test_directory . "\n";
             $args['keep-results'] = TRUE;
             // Exit repeat loop immediately.
@@ -841,7 +853,8 @@ function simpletest_script_cleanup($test_id, $test_class, $exitcode) {
 
   // Check whether a test site directory was setup already.
   // @see \Drupal\simpletest\TestBase::prepareEnvironment()
-  $test_directory = DRUPAL_ROOT . '/sites/simpletest/' . substr($db_prefix, 10);
+  $test_db = new TestDatabase($db_prefix);
+  $test_directory = DRUPAL_ROOT . '/' . $test_db->getTestSitePath();
   if (is_dir($test_directory)) {
     // Output the error_log.
     if (is_file($test_directory . '/error.log')) {
diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php
index 0146607aa6fb3a9a8939edb9766e0368f29751c9..c34e45ff804241d355432486bd823917977a7135 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBase.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBase.php
@@ -15,6 +15,7 @@
 use Drupal\Core\Extension\ExtensionDiscovery;
 use Drupal\Core\Language\Language;
 use Drupal\Core\Site\Settings;
+use Drupal\Core\Test\TestDatabase;
 use Drupal\simpletest\AssertContentTrait;
 use Drupal\simpletest\AssertHelperTrait;
 use Drupal\Tests\ConfigTestTrait;
@@ -251,19 +252,14 @@ protected function bootEnvironment() {
     require_once $this->root . '/core/includes/bootstrap.inc';
 
     // Set up virtual filesystem.
-    // Ensure that the generated test site directory does not exist already,
-    // which may happen with a large amount of concurrent threads and
-    // long-running tests.
-    do {
-      $suffix = mt_rand(100000, 999999);
-      $this->siteDirectory = 'sites/simpletest/' . $suffix;
-      $this->databasePrefix = 'simpletest' . $suffix;
-    } while (is_dir($this->root . '/' . $this->siteDirectory));
+    Database::addConnectionInfo('default', 'test-runner', $this->getDatabaseConnectionInfo()['default']);
+    $test_db = new TestDatabase();
+    $this->siteDirectory = $test_db->getTestSitePath();
 
     // Ensure that all code that relies on drupal_valid_test_ua() can still be
     // safely executed. This primarily affects the (test) site directory
     // resolution (used by e.g. LocalStream and PhpStorage).
-    $this->databasePrefix = 'simpletest' . $suffix;
+    $this->databasePrefix = $test_db->getDatabasePrefix();
     drupal_valid_test_ua($this->databasePrefix);
 
     $settings = array(
@@ -287,15 +283,12 @@ protected function bootEnvironment() {
    * Sets up the filesystem, so things like the file directory.
    */
   protected function setUpFilesystem() {
-    $suffix = str_replace('simpletest', '', $this->databasePrefix);
-    $this->vfsRoot = vfsStream::setup('root', NULL, array(
-      'sites' => array(
-        'simpletest' => array(
-          $suffix => array(),
-        ),
-      ),
-    ));
-    $this->siteDirectory = vfsStream::url('root/sites/simpletest/' . $suffix);
+    $test_db = new TestDatabase($this->databasePrefix);
+    $test_site_path = $test_db->getTestSitePath();
+
+    $this->vfsRoot = vfsStream::setup('root');
+    $this->vfsRoot->addChild(vfsStream::newDirectory($test_site_path));
+    $this->siteDirectory = vfsStream::url('root/' . $test_site_path);
 
     mkdir($this->siteDirectory . '/files', 0775);
     mkdir($this->siteDirectory . '/files/config/' . CONFIG_SYNC_DIRECTORY, 0775, TRUE);
@@ -708,6 +701,10 @@ protected function tearDown() {
     $this->vfsRoot = NULL;
     $this->configImporter = NULL;
 
+    // Release the prefix.
+    $test_db = new TestDatabase($test_prefix);
+    $test_db->releaseTestLock();
+
     // Free up memory: Custom test class properties.
     // Note: Private properties cannot be cleaned up.
     $rc = new \ReflectionClass(__CLASS__);
diff --git a/core/tests/Drupal/KernelTests/KernelTestBaseTest.php b/core/tests/Drupal/KernelTests/KernelTestBaseTest.php
index 1605ad69ad7bd256574729ad6ed7b404054bebd5..469debed27defd99699c31dec314e33f02f5d898 100644
--- a/core/tests/Drupal/KernelTests/KernelTestBaseTest.php
+++ b/core/tests/Drupal/KernelTests/KernelTestBaseTest.php
@@ -25,13 +25,13 @@ public function testSetUpBeforeClass() {
    * @covers ::bootEnvironment
    */
   public function testBootEnvironment() {
-    $this->assertRegExp('/^simpletest\d{6}$/', $this->databasePrefix);
+    $this->assertRegExp('/^test\d{8}$/', $this->databasePrefix);
     $this->assertStringStartsWith('vfs://root/sites/simpletest/', $this->siteDirectory);
     $this->assertEquals(array(
       'root' => array(
         'sites' => array(
           'simpletest' => array(
-            substr($this->databasePrefix, 10) => array(
+            substr($this->databasePrefix, 4) => array(
               'files' => array(
                 'config' => array(
                   'sync' => array(),
diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php
index b4a685eafe55c6c003dacd2676f12bc07433cd5e..bfc71fc67a035615b060f360c7be09a3b11fdcae 100644
--- a/core/tests/Drupal/Tests/BrowserTestBase.php
+++ b/core/tests/Drupal/Tests/BrowserTestBase.php
@@ -541,6 +541,10 @@ protected function cleanupEnvironment() {
 
     // Delete test site directory.
     file_unmanaged_delete_recursive($this->siteDirectory, array($this, 'filePreDeleteCallback'));
+
+    // Release the prefix.
+    $test_db = new TestDatabase($test_prefix);
+    $test_db->releaseTestLock();
   }
 
   /**
@@ -1201,14 +1205,9 @@ protected function installParameters() {
    * @see BrowserTestBase::prepareEnvironment()
    */
   private function prepareDatabasePrefix() {
-    // Ensure that the generated test site directory does not exist already,
-    // which may happen with a large amount of concurrent threads and
-    // long-running tests.
-    do {
-      $suffix = mt_rand(100000, 999999);
-      $this->siteDirectory = 'sites/simpletest/' . $suffix;
-      $this->databasePrefix = 'simpletest' . $suffix;
-    } while (is_dir(DRUPAL_ROOT . '/' . $this->siteDirectory));
+    $test_db = new TestDatabase();
+    $this->siteDirectory = $test_db->getTestSitePath();
+    $this->databasePrefix = $test_db->getDatabasePrefix();
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Test/TestDatabaseTest.php b/core/tests/Drupal/Tests/Core/Test/TestDatabaseTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..7084278b804bf86d26425de1cb8f3a618c39d30d
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Test/TestDatabaseTest.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\Tests\Core\Test;
+
+use Drupal\Core\Test\TestDatabase;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Test\TestDatabase
+ * @group Template
+ */
+class TestDatabaseTest extends UnitTestCase {
+
+  /**
+   * @covers ::__construct
+   */
+  public function testConstructorException() {
+    $this->setExpectedException(\InvalidArgumentException::class, "Invalid database prefix: blah1253");
+    new TestDatabase('blah1253');
+  }
+
+  /**
+   * @covers ::__construct
+   * @covers ::getDatabasePrefix
+   * @covers ::getTestSitePath
+   *
+   * @dataProvider providerTestConstructor
+   */
+  public function testConstructor($db_prefix, $expected_db_prefix, $expected_site_path) {
+    $test_db = new TestDatabase($db_prefix);
+    $this->assertEquals($expected_db_prefix, $test_db->getDatabasePrefix());
+    $this->assertEquals($expected_site_path, $test_db->getTestSitePath());
+  }
+
+  /**
+   * Data provider for self::testConstructor()
+   */
+  public function providerTestConstructor() {
+    return [
+      ['test1234', 'test1234', 'sites/simpletest/1234'],
+      ['test123456test234567', 'test123456test234567', 'sites/simpletest/234567']
+    ];
+  }
+
+}