diff --git a/core/core.services.yml b/core/core.services.yml
index bcf528b..6f5d77b 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -898,6 +898,9 @@ services:
   entity.query.keyvalue:
     class: Drupal\Core\Entity\KeyValueStore\Query\QueryFactory
     arguments: ['@keyvalue']
+  entity.common_queries:
+    class: Drupal\Core\Entity\Query\Queries
+    arguments: ['@entity_type.manager']
   router.dumper:
     class: Drupal\Core\Routing\MatcherDumper
     arguments: ['@database', '@state']
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
index 2c62589..0a558fc 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -713,6 +713,15 @@ public function onChange($name) {
         elseif (isset($this->translatableEntityKeys[$key][$this->activeLangcode])) {
           unset($this->translatableEntityKeys[$key][$this->activeLangcode]);
         }
+        // If the revision identifier field is being populated with an actual
+        // value, we need to make sure the "new revision" static cache is reset
+        // accordingly.
+        if ($key == 'revision') {
+          $revision_id = $this->getRevisionId();
+          if (isset($revision_id)) {
+            $this->newRevision = FALSE;
+          }
+        }
       }
     }
 
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityForm.php b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
index 92bbbae..30791f6 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityForm.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
@@ -127,6 +127,17 @@ public function form(array $form, FormStateInterface $form_state) {
       $this->addRevisionableFormFields($form);
     }
 
+    // Store the revision id at the time the form is built, so it can be used
+    // later to detect concurrent edits.
+    $entity_type = $this->entity->getEntityType();
+    if ($entity_type->isRevisionable()) {
+      $revision_key = $entity_type->getKey('revision');
+      $form[$revision_key] = [
+        '#type' => 'hidden',
+        '#default_value' => $this->entity->getRevisionId(),
+      ];
+    }
+
     $form['footer'] = [
       '#type' => 'container',
       '#weight' => 99,
@@ -155,6 +166,14 @@ public function buildEntity(array $form, FormStateInterface $form_state) {
     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
     $entity = parent::buildEntity($form, $form_state);
 
+    // We did not save the entity yet, so we expect the loaded revision id to
+    // match the revision id. However this may not be the case if the entity was
+    // saved in another form. Thus we need to make sure the loaded revision id
+    // actually matches the revision id.
+    if ($this->entity->getEntityType()->isRevisionable()) {
+      $entity->updateLoadedRevisionId();
+    }
+
     // Mark the entity as requiring validation.
     $entity->setValidationRequired(!$form_state->getTemporaryValue('entity_validated'));
 
diff --git a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
index 874a341..fb76fb6 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
@@ -356,7 +356,12 @@ protected function doPostSave(EntityInterface $entity, $update) {
     // The revision is stored, it should no longer be marked as new now.
     if ($this->entityType->isRevisionable()) {
       $entity->updateLoadedRevisionId();
-      $entity->setNewRevision(FALSE);
+      if ($entity->isNewRevision()) {
+        $entity->setNewRevision(FALSE);
+        // Finally notify the entity that the revision id has changed, so that
+        // any pending clean-up can be performed.
+        $entity->onChange($this->entityType->getKey('revision'));
+      }
     }
   }
 
diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
index b1fee2a..034eab6 100644
--- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
+++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
@@ -2,6 +2,9 @@
 
 namespace Drupal\Core\Entity\Plugin\Validation\Constraint;
 
+use Drupal\Core\Entity\EntityChangedInterface;
+use Drupal\Core\Entity\Query\Queries;
+use Drupal\Core\Entity\RevisionableInterface;
 use Symfony\Component\Validator\Constraint;
 use Symfony\Component\Validator\ConstraintValidator;
 
@@ -14,15 +17,29 @@ class EntityChangedConstraintValidator extends ConstraintValidator {
    * {@inheritdoc}
    */
   public function validate($entity, Constraint $constraint) {
-    if (isset($entity)) {
-      /** @var \Drupal\Core\Entity\EntityInterface $entity */
-      if (!$entity->isNew()) {
-        $saved_entity = \Drupal::entityManager()->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id());
+    if (isset($entity) && !$entity->isNew()) {
+      $valid = TRUE;
+
+      if ($entity instanceof RevisionableInterface) {
+        // If the entity is revisionable, we need to make sure that no new
+        // revision was created while the entity was being changed.
+        $latest_revision_id = Queries::get()->getLatestRevisionId($entity);
+        $loaded_revision_id = $entity->getLoadedRevisionId();
+        $valid = $loaded_revision_id == $latest_revision_id;
+      }
+
+      if ($valid && $entity instanceof EntityChangedInterface) {
+        /** @var \Drupal\Core\Entity\EntityChangedInterface|\Drupal\Core\Entity\RevisionableInterface $stored_entity */
+        $stored_entity = \Drupal::entityTypeManager()
+          ->getStorage($entity->getEntityTypeId())
+          ->loadUnchanged($entity->id());
         // A change to any other translation must add a violation to the current
         // translation because there might be untranslatable shared fields.
-        if ($saved_entity && $saved_entity->getChangedTimeAcrossTranslations() > $entity->getChangedTimeAcrossTranslations()) {
-          $this->context->addViolation($constraint->message);
-        }
+        $valid = $stored_entity->getChangedTimeAcrossTranslations() <= $entity->getChangedTimeAcrossTranslations();
+      }
+
+      if (!$valid) {
+        $this->context->addViolation($constraint->message);
       }
     }
   }
diff --git a/core/lib/Drupal/Core/Entity/Query/Queries.php b/core/lib/Drupal/Core/Entity/Query/Queries.php
new file mode 100644
index 0000000..b11228a
--- /dev/null
+++ b/core/lib/Drupal/Core/Entity/Query/Queries.php
@@ -0,0 +1,75 @@
+<?php
+
+namespace Drupal\Core\Entity\Query;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\RevisionableInterface;
+
+/**
+ * Implements a collection of common entity queries.
+ *
+ * @internal It is not clear yet how this will be instantiated and the final
+ *   shape it will assume, so avoid using this until it is published as an
+ *   official API.
+ */
+class Queries {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Returns an instance of the Queries class.
+   *
+   * @return static
+   */
+  public static function get() {
+    return \Drupal::service('entity.common_queries');
+  }
+
+  /**
+   * Queries constructor.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * Returns the latest revision identifier for the specified entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   An entity object.
+   *
+   * @return int|null
+   *   The latest revision identifier or NULL if no revision could be found.
+   */
+  public function getLatestRevisionId(EntityInterface $entity) {
+    $entity_type = $entity->getEntityType();
+    if (!$entity instanceof RevisionableInterface || $entity->isNew() || !$entity_type->isRevisionable()) {
+      return NULL;
+    }
+
+    $id_key = $entity_type->getKey('id');
+    $revision_key = $entity_type->getKey('revision');
+
+    $result = $this->entityTypeManager
+      ->getStorage($entity->getEntityTypeId())
+      ->getQuery()
+      ->allRevisions()
+      ->condition($id_key, $entity->id())
+      ->sort($revision_key, 'DESC')
+      ->range(0, 1)
+      ->execute();
+
+    return key($result);
+  }
+
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
index 22238d4..6dcd457 100644
--- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
+++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
@@ -851,7 +851,7 @@ protected function doSaveFieldItems(ContentEntityInterface $entity, array $names
         }
         if ($this->revisionTable) {
           if ($full_save) {
-            $entity->{$this->revisionKey} = $this->saveRevision($entity);
+            $this->saveRevision($entity);
           }
           else {
             $record = $this->mapToStorageRecord($entity->getUntranslated(), $this->revisionTable);
@@ -884,7 +884,7 @@ protected function doSaveFieldItems(ContentEntityInterface $entity, array $names
         }
         $entity->{$this->idKey} = (string) $record->{$this->idKey};
         if ($this->revisionTable) {
-          $record->{$this->revisionKey} = $this->saveRevision($entity);
+          $this->saveRevision($entity);
         }
         if ($this->dataTable) {
           $this->saveToSharedTables($entity);
@@ -1095,8 +1095,13 @@ protected function saveRevision(ContentEntityInterface $entity) {
         ->execute();
     }
 
-    // Make sure to update the new revision key for the entity.
-    $entity->{$this->revisionKey}->value = $record->{$this->revisionKey};
+    // Make sure to update the new revision key for the entity. Since the save
+    // process still needs to know that we are saving a "new revision", we avoid
+    // notifying the parent entity of the revision id change, so that the "new
+    // revision" flag will be kept until the save process is over.
+    $entity
+      ->get($this->revisionKey)
+      ->setValue($record->{$this->revisionKey}, FALSE);
 
     return $record->{$this->revisionKey};
   }
diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
index 2fefc01..f21ce04 100644
--- a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
+++ b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
@@ -173,6 +173,7 @@ public function testInvalidStateMultilingual() {
     $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
     $workflow->save();
 
+    /** @var \Drupal\node\NodeInterface $node */
     $node = Node::create([
       'type' => 'example',
       'title' => 'English Published Node',
@@ -181,6 +182,7 @@ public function testInvalidStateMultilingual() {
     ]);
     $node->save();
 
+    /** @var \Drupal\node\NodeInterface $node_fr */
     $node_fr = $node->addTranslation('fr');
     $node_fr->setTitle('French Published Node');
     $node_fr->save();
@@ -199,14 +201,16 @@ public function testInvalidStateMultilingual() {
     $this->assertCount(1, $violations);
     $this->assertEquals('Invalid state transition from <em class="placeholder">Draft</em> to <em class="placeholder">Archived</em>', $violations->get(0)->getMessage());
 
-    // From the default french published revision, there should be none.
-    $node_fr = Node::load($node->id())->getTranslation('fr');
-    $this->assertEquals('published', $node_fr->moderation_state->value);
-    $node_fr->moderation_state = 'archived';
-    $violations = $node_fr->validate();
-    $this->assertCount(0, $violations);
+    // @todo Creating a new revision from a past revision is currently not
+    //   supported. Evaluate this again in https://www.drupal.org/node/2860097.
+    // // From the default french published revision, there should be none.
+    // $node_fr = Node::load($node->id())->getTranslation('fr');
+    // $this->assertEquals('published', $node_fr->moderation_state->value);
+    // $node_fr->moderation_state = 'archived';
+    // $violations = $node_fr->validate();
+    // $this->assertCount(0, $violations);
 
-    // From the latest french revision, there should also be no violation.
+    // From the latest french revision, there should be no violation.
     $node_fr = $node->getTranslation('fr');
     $this->assertEquals('published', $node_fr->moderation_state->value);
     $node_fr->moderation_state = 'archived';
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityConcurrentRevisionSaveTest.php b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityConcurrentRevisionSaveTest.php
new file mode 100644
index 0000000..96d4561
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/ContentEntityConcurrentRevisionSaveTest.php
@@ -0,0 +1,122 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\entity_test\Entity\EntityTestMulRevChanged;
+
+/**
+ * Tests concurrent entity saves and revisions.
+ *
+ * @group Entity
+ *
+ * @coversDefaultClass \Drupal\Core\Entity\Plugin\Validation\Constraint\EntityChangedConstraintValidator
+ */
+class ContentEntityConcurrentRevisionSaveTest extends EntityKernelTestBase {
+
+  const SAVE_NO_REVISION = 0x0;
+  const SAVE_NEW_REVISION = 0x1;
+  const SAVE_PENDING_REVISION = 0x3;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['user', 'system', 'field', 'text', 'filter', 'entity_test'];
+
+  /**
+   * The EntityTestMulRevChanged entity type storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $storage;
+
+  /**
+   * Counter used to generate meaningful entity labels.
+   *
+   * @var int
+   */
+  protected $saveCounter;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installEntitySchema('entity_test_mulrev_changed');
+    $this->storage = $this->entityManager->getStorage('entity_test_mulrev_changed');
+    $this->saveCounter = 1;
+  }
+
+  /**
+   * Data provider for testConcurrentSave().
+   */
+  public function dataTestConcurrentSave() {
+    return [
+      'pending revision - pending revision' => [[static::SAVE_PENDING_REVISION, static::SAVE_PENDING_REVISION]],
+      'no revision - pending revision' => [[static::SAVE_NO_REVISION, static::SAVE_PENDING_REVISION]],
+      'new revision - pending revision' => [[static::SAVE_NEW_REVISION, static::SAVE_PENDING_REVISION]],
+      'pending revision - no revision' => [[static::SAVE_PENDING_REVISION, static::SAVE_NO_REVISION]],
+      'no revision - no revision' => [[static::SAVE_NO_REVISION, static::SAVE_NO_REVISION]],
+      'new revision - no revision' => [[static::SAVE_NEW_REVISION, static::SAVE_NO_REVISION]],
+      'pending revision - new revision' => [[static::SAVE_PENDING_REVISION, static::SAVE_NEW_REVISION]],
+      'no revision - new revision' => [[static::SAVE_NO_REVISION, static::SAVE_NEW_REVISION]],
+      'new revision - new revision' => [[static::SAVE_NEW_REVISION, static::SAVE_NEW_REVISION]],
+    ];
+  }
+
+  /**
+   * Tests concurrent entity saves with various save types.
+   *
+   * @param int[] $type
+   *   An array of save types as defined by the related ::SAVE_* constants. One
+   *   for each of the two concurrent saves.
+   *
+   * @covers ::validate
+   *
+   * @dataProvider dataTestConcurrentSave
+   */
+  public function testConcurrentSave(array $type) {
+    $user = $this->createUser();
+
+    // Create a test entity.
+    $entity = EntityTestMulRevChanged::create([
+      'not_translatable' => $this->randomString(),
+      'user_id' => $user->id(),
+      'language' => 'en',
+    ]);
+    $this->doEntitySave($entity, static::SAVE_NO_REVISION, FALSE);
+
+    // Perform two "concurrent" saves and verify the second one triggers a
+    // validation error.
+    $entity_other_instance = clone $entity;
+    $this->doEntitySave($entity, $type[0], FALSE);
+    $this->doEntitySave($entity_other_instance, $type[1], TRUE);
+  }
+
+  /**
+   * Performs an entity save of the specified "type".
+   *
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   The entity to be saved.
+   * @param int $type
+   *   The save type, as defined by the ::SAVE_* constants.
+   * @param bool $error_expected
+   *   Flag indicating whether a validation error is expected.
+   */
+  protected function doEntitySave(ContentEntityInterface $entity, $type, $error_expected) {
+    // Set a meaningful entity label, useful for debugging.
+    $entity->set('name', 'Title ' . ($this->saveCounter++));
+    if ($type & static::SAVE_NEW_REVISION) {
+      $entity->setNewRevision();
+    }
+    if ($type == static::SAVE_PENDING_REVISION) {
+      $entity->isDefaultRevision(FALSE);
+    }
+    $violations = $entity->validate();
+    $this->assertEquals($error_expected, (bool) $violations->count());
+    $entity->save();
+  }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php
index a3b99fe..5659280 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityLoadedRevisionTest.php
@@ -167,4 +167,35 @@ public function testSaveInHookEntityInsert() {
 
   }
 
+  /**
+   * Tests manual revert of the revision id value.
+   *
+   * @covers \Drupal\Core\Entity\ContentEntityBase::getRevisionId
+   * @covers \Drupal\Core\Entity\ContentEntityBase::getLoadedRevisionId
+   * @covers \Drupal\Core\Entity\ContentEntityBase::setNewRevision
+   */
+  public function testNewRevisionRevert() {
+    $entity = EntityTestMulRev::create(['name' => 'EntityLoadedRevisionTest']);
+    $entity->save();
+
+    // Check that revision id field is reset while the loaded revision id is
+    // preserved when flagging a new revision.
+    $revision_id = $entity->getRevisionId();
+    $entity->setNewRevision();
+    $this->assertNull($entity->getRevisionId());
+    $this->assertEquals($revision_id, $entity->getLoadedRevisionId());
+
+    // Check that manually reverting the new revision state by assigning a value
+    // to the revision id field works as expected.
+    $key = $entity->getEntityType()->getKey('revision');
+    $entity->set($key, $revision_id);
+    $this->assertEquals($revision_id, $entity->getRevisionId());
+    $this->assertEquals($revision_id, $entity->getLoadedRevisionId());
+
+    // Check that flagging a new revision again works correctly.
+    $entity->setNewRevision();
+    $this->assertNull($entity->getRevisionId());
+    $this->assertEquals($revision_id, $entity->getLoadedRevisionId());
+  }
+
 }
