diff -Naur a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php
--- a/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php
+++ b/vendor/magento/module-cron/Observer/ProcessCronQueueObserver.php
@@ -9,9 +9,12 @@
  */
 namespace Magento\Cron\Observer;

+use Magento\Framework\App\State;
 use Magento\Framework\Console\Cli;
 use Magento\Framework\Event\ObserverInterface;
 use \Magento\Cron\Model\Schedule;
+use Magento\Framework\Profiler\Driver\Standard\Stat;
+use Magento\Framework\Profiler\Driver\Standard\StatFactory;

 /**
  * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -96,25 +99,54 @@ class ProcessCronQueueObserver implements ObserverInterface
     protected $_shell;

     /**
-     * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface
+     * @var \Magento\Framework\Stdlib\DateTime\DateTime
      */
-    protected $timezone;
+    protected $dateTime;

     /**
      * @var \Symfony\Component\Process\PhpExecutableFinder
      */
     protected $phpExecutableFinder;

+    /**
+     * @var \Psr\Log\LoggerInterface
+     */
+    private $logger;
+
+    /**
+     * @var \Magento\Framework\App\State
+     */
+    private $state;
+
+    /**
+     * @var array
+     */
+    private $invalid = [];
+
+    /**
+     * @var array
+     */
+    private $jobs;
+
+    /**
+     * @var Stat
+     */
+    private $statProfiler;
+
     /**
      * @param \Magento\Framework\ObjectManagerInterface $objectManager
-     * @param ScheduleFactory $scheduleFactory
+     * @param \Magento\Cron\Model\ScheduleFactory $scheduleFactory
      * @param \Magento\Framework\App\CacheInterface $cache
-     * @param ConfigInterface $config
+     * @param \Magento\Cron\Model\ConfigInterface $config
      * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
      * @param \Magento\Framework\App\Console\Request $request
      * @param \Magento\Framework\ShellInterface $shell
-     * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $timezone
+     * @param \Magento\Framework\Stdlib\DateTime\DateTime $dateTime
      * @param \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory
+     * @param \Psr\Log\LoggerInterface $logger
+     * @param \Magento\Framework\App\State $state
+     * @param StatFactory $statFactory
+     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
      */
     public function __construct(
         \Magento\Framework\ObjectManagerInterface $objectManager,
@@ -124,8 +156,11 @@ class ProcessCronQueueObserver implements ObserverInterface
         \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
         \Magento\Framework\App\Console\Request $request,
         \Magento\Framework\ShellInterface $shell,
-        \Magento\Framework\Stdlib\DateTime\TimezoneInterface $timezone,
-        \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory
+        \Magento\Framework\Stdlib\DateTime\DateTime $dateTime,
+        \Magento\Framework\Process\PhpExecutableFinderFactory $phpExecutableFinderFactory,
+        \Psr\Log\LoggerInterface $logger,
+        \Magento\Framework\App\State $state,
+        StatFactory $statFactory
     ) {
         $this->_objectManager = $objectManager;
         $this->_scheduleFactory = $scheduleFactory;
@@ -134,8 +169,11 @@ class ProcessCronQueueObserver implements ObserverInterface
         $this->_scopeConfig = $scopeConfig;
         $this->_request = $request;
         $this->_shell = $shell;
-        $this->timezone = $timezone;
+        $this->dateTime = $dateTime;
         $this->phpExecutableFinder = $phpExecutableFinderFactory->create();
+        $this->logger = $logger;
+        $this->state = $state;
+        $this->statProfiler = $statFactory->create();
     }

     /**
@@ -151,26 +189,29 @@ class ProcessCronQueueObserver implements ObserverInterface
      */
     public function execute(\Magento\Framework\Event\Observer $observer)
     {
-        $pendingJobs = $this->_getPendingSchedules();
-        $currentTime = $this->timezone->scopeTimeStamp();
+
+        $currentTime = $this->dateTime->gmtTimestamp();
         $jobGroupsRoot = $this->_config->getJobs();
+        // sort jobs groups to start from used in separated process
+        uksort(
+            $jobGroupsRoot,
+            function ($a, $b) {
+                return $this->getCronGroupConfigurationValue($b, 'use_separate_process')
+                    - $this->getCronGroupConfigurationValue($a, 'use_separate_process');
+            }
+        );

         $phpPath = $this->phpExecutableFinder->find() ?: 'php';

         foreach ($jobGroupsRoot as $groupId => $jobsRoot) {
-            if ($this->_request->getParam('group') !== null
-                && $this->_request->getParam('group') !== '\'' . ($groupId) . '\''
-                && $this->_request->getParam('group') !== $groupId) {
+            if (!$this->isGroupInFilter($groupId)) {
                 continue;
             }
-            if (($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1') && (
-                    $this->_scopeConfig->getValue(
-                        'system/cron/' . $groupId . '/use_separate_process',
-                        \Magento\Store\Model\ScopeInterface::SCOPE_STORE
-                    ) == 1
-                )) {
+            if ($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1'
+               && $this->getCronGroupConfigurationValue($groupId, 'use_separate_process') == 1
+            ) {
                 $this->_shell->execute(
                     $phpPath . ' %s cron:run --group=' . $groupId . ' --' . Cli::INPUT_KEY_BOOTSTRAP . '='
                     . self::STANDALONE_PROCESS_STARTED . '=1',
                     [
                         BP . '/bin/magento'
@@ -179,29 +220,9 @@ class ProcessCronQueueObserver implements ObserverInterface
                 continue;
             }

-            foreach ($pendingJobs as $schedule) {
-                $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null;
-                if (!$jobConfig) {
-                    continue;
-                }
-
-                $scheduledTime = strtotime($schedule->getScheduledAt());
-                if ($scheduledTime > $currentTime) {
-                    continue;
-                }
-
-                try {
-                    if ($schedule->tryLockJob()) {
-                        $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId);
-                    }
-                } catch (\Exception $e) {
-                    $schedule->setMessages($e->getMessage());
-                }
-                $schedule->save();
-            }
-
-            $this->_generate($groupId);
-            $this->_cleanup($groupId);
+            $this->cleanupJobs($groupId, $currentTime);
+            $this->generateSchedules($groupId);
+            $this->processPendingJobs($groupId, $jobsRoot, $currentTime);
         }
     }

@@ -218,58 +239,105 @@ class ProcessCronQueueObserver implements ObserverInterface
      */
     protected function _runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId)
     {
-        $scheduleLifetime = (int)$this->_scopeConfig->getValue(
-            'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME,
-            \Magento\Store\Model\ScopeInterface::SCOPE_STORE
-        );
+        $jobCode = $schedule->getJobCode();
+        $scheduleLifetime = $this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_LIFETIME);
         $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE;
         if ($scheduledTime < $currentTime - $scheduleLifetime) {
             $schedule->setStatus(Schedule::STATUS_MISSED);
+            $this->logger->info(sprintf('Cron Job %s is missed', $jobCode));
             throw new \Exception('Too late for the schedule');
         }

         if (!isset($jobConfig['instance'], $jobConfig['method'])) {
             $schedule->setStatus(Schedule::STATUS_ERROR);
+            $this->logger->error(sprintf('Cron Job %s has an error', $jobCode));
             throw new \Exception('No callbacks found');
         }
         $model = $this->_objectManager->create($jobConfig['instance']);
         $callback = [$model, $jobConfig['method']];
         if (!is_callable($callback)) {
             $schedule->setStatus(Schedule::STATUS_ERROR);
+            $this->logger->error(sprintf('Cron Job %s has an error', $jobCode));
             throw new \Exception(
                 sprintf('Invalid callback: %s::%s can\'t be called', $jobConfig['instance'], $jobConfig['method'])
             );
         }

-        $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->timezone->scopeTimeStamp()))->save();
+        $schedule->setExecutedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))->save();

+        $this->startProfiling();
         try {
+            $this->logger->info(sprintf('Cron Job %s is run', $jobCode));
             call_user_func_array($callback, [$schedule]);
         } catch (\Exception $e) {
             $schedule->setStatus(Schedule::STATUS_ERROR);
+            $this->logger->error(sprintf(
+                'Cron Job %s has an error. Statistics: %s %s',
+                $jobCode,
+                $this->getProfilingStat(), $e->getMessage()
+            ));
             throw $e;
+        } finally {
+            $this->stopProfiling();
         }

         $schedule->setStatus(Schedule::STATUS_SUCCESS)->setFinishedAt(strftime(
             '%Y-%m-%d %H:%M:%S',
-            $this->timezone->scopeTimeStamp()
+            $this->dateTime->gmtTimestamp()
+        ));
+
+        $this->logger->info(sprintf(
+            'Cron Job %s is successfully finished. Statistics: %s',
+            $jobCode,
+            $this->getProfilingStat()
         ));
     }

+    /**
+     * Starts profiling
+     *
+     * @return void
+     */
+    private function startProfiling()
+    {
+        $this->statProfiler->clear();
+        $this->statProfiler->start('job', microtime(true), memory_get_usage(true), memory_get_usage());
+    }
+
+    /**
+     * Stops profiling
+     *
+     * @return void
+     */
+    private function stopProfiling()
+    {
+        $this->statProfiler->stop('job', microtime(true), memory_get_usage(true), memory_get_usage());
+    }
+
+    /**
+     * Retrieves statistics in the JSON format
+     *
+     * @return string
+     */
+    private function getProfilingStat()
+    {
+        $stat = $this->statProfiler->get('job');
+        unset($stat[Stat::START]);
+        return json_encode($stat);
+    }
+
     /**
      * Return job collection from data base with status 'pending'
      *
      * @return \Magento\Cron\Model\ResourceModel\Schedule\Collection
      */
-    protected function _getPendingSchedules()
+    private function getPendingSchedules($groupId)
     {
-        if (!$this->_pendingSchedules) {
-            $this->_pendingSchedules = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter(
-                'status',
-                Schedule::STATUS_PENDING
-            )->load();
-        }
-        return $this->_pendingSchedules;
+        $jobs = $this->getJobs();
+        $pendingJobs = $this->_scheduleFactory->create()->getCollection();
+        $pendingJobs->addFieldToFilter('status', Schedule::STATUS_PENDING);
+        $pendingJobs->addFieldToFilter('job_code', ['in' => array_keys($jobs[$groupId])]);
+        return $pendingJobs;
     }

     /**
@@ -278,22 +346,32 @@ class ProcessCronQueueObserver implements ObserverInterface
      * @param string $groupId
      * @return $this
      */
-    protected function _generate($groupId)
+    private function generateSchedules($groupId)
     {
         /**
          * check if schedule generation is needed
          */
         $lastRun = (int)$this->_cache->load(self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId);
-        $rawSchedulePeriod = (int)$this->_scopeConfig->getValue(
-            'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_GENERATE_EVERY,
-            \Magento\Store\Model\ScopeInterface::SCOPE_STORE
+        $rawSchedulePeriod = (int)$this->getCronGroupConfigurationValue(
+            $groupId,
+            self::XML_PATH_SCHEDULE_GENERATE_EVERY
         );
         $schedulePeriod = $rawSchedulePeriod * self::SECONDS_IN_MINUTE;
-        if ($lastRun > $this->timezone->scopeTimeStamp() - $schedulePeriod) {
+        if ($lastRun > $this->dateTime->gmtTimestamp() - $schedulePeriod) {
             return $this;
         }

-        $schedules = $this->_getPendingSchedules();
+        /**
+         * save time schedules generation was ran with no expiration
+         */
+        $this->_cache->save(
+            $this->dateTime->gmtTimestamp(),
+            self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId,
+            ['crontab'],
+            null
+        );
+
+        $schedules = $this->getPendingSchedules($groupId);
         $exists = [];
         /** @var Schedule $schedule */
         foreach ($schedules as $schedule) {
@@ -303,18 +381,10 @@ class ProcessCronQueueObserver implements ObserverInterface
         /**
          * generate global crontab jobs
          */
-        $jobs = $this->_config->getJobs();
+        $jobs = $this->getJobs();
+        $this->invalid = [];
         $this->_generateJobs($jobs[$groupId], $exists, $groupId);
-
-        /**
-         * save time schedules generation was ran with no expiration
-         */
-        $this->_cache->save(
-            $this->timezone->scopeTimeStamp(),
-            self::CACHE_KEY_LAST_SCHEDULE_GENERATE_AT . $groupId,
-            ['crontab'],
-            null
-        );
+        $this->cleanupScheduleMismatches();

         return $this;
     }
@@ -325,22 +395,12 @@ class ProcessCronQueueObserver implements ObserverInterface
      * @param   array $jobs
      * @param   array $exists
      * @param   string $groupId
-     * @return  $this
+     * @return  void
      */
     protected function _generateJobs($jobs, $exists, $groupId)
     {
         foreach ($jobs as $jobCode => $jobConfig) {
-            $cronExpression = null;
-            if (isset($jobConfig['config_path'])) {
-                $cronExpression = $this->getConfigSchedule($jobConfig) ?: null;
-            }
-
-            if (!$cronExpression) {
-                if (isset($jobConfig['schedule'])) {
-                    $cronExpression = $jobConfig['schedule'];
-                }
-            }
-
+            $cronExpression = $this->getCronExpression($jobConfig);
             if (!$cronExpression) {
                 continue;
             }
@@ -348,75 +408,60 @@ class ProcessCronQueueObserver implements ObserverInterface
             $timeInterval = $this->getScheduleTimeInterval($groupId);
             $this->saveSchedule($jobCode, $cronExpression, $timeInterval, $exists);
         }
-        return $this;
     }

     /**
-     * Clean existed jobs
+     * Clean expired jobs
      *
-     * @param string $groupId
-     * @return $this
+     * @param $groupId
+     * @param $currentTime
+     * @return void
      */
-    protected function _cleanup($groupId)
+    private function cleanupJobs($groupId, $currentTime)
     {
         // check if history cleanup is needed
         $lastCleanup = (int)$this->_cache->load(self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId);
-        $historyCleanUp = (int)$this->_scopeConfig->getValue(
-            'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_CLEANUP_EVERY,
-            \Magento\Store\Model\ScopeInterface::SCOPE_STORE
-        );
-        if ($lastCleanup > $this->timezone->scopeTimeStamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) {
+        $historyCleanUp = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_CLEANUP_EVERY);
+        if ($lastCleanup > $this->dateTime->gmtTimestamp() - $historyCleanUp * self::SECONDS_IN_MINUTE) {
             return $this;
         }
-
-        // check how long the record should stay unprocessed before marked as MISSED
-        $scheduleLifetime = (int)$this->_scopeConfig->getValue(
-            'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_LIFETIME,
-            \Magento\Store\Model\ScopeInterface::SCOPE_STORE
+        // save time history cleanup was ran with no expiration
+        $this->_cache->save(
+            $this->dateTime->gmtTimestamp(),
+            self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId,
+            ['crontab'],
+            null
         );
-        $scheduleLifetime = $scheduleLifetime * self::SECONDS_IN_MINUTE;

-        /**
-         * @var \Magento\Cron\Model\ResourceModel\Schedule\Collection $history
-         */
-        $history = $this->_scheduleFactory->create()->getCollection()->addFieldToFilter(
-            'status',
-            ['in' => [Schedule::STATUS_SUCCESS, Schedule::STATUS_MISSED, Schedule::STATUS_ERROR]]
-        )->load();
+        $this->cleanupDisabledJobs($groupId);

-        $historySuccess = (int)$this->_scopeConfig->getValue(
-            'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_SUCCESS,
-            \Magento\Store\Model\ScopeInterface::SCOPE_STORE
-        );
-        $historyFailure = (int)$this->_scopeConfig->getValue(
-            'system/cron/' . $groupId . '/' . self::XML_PATH_HISTORY_FAILURE,
-            \Magento\Store\Model\ScopeInterface::SCOPE_STORE
-        );
+        $historySuccess = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_SUCCESS);
+        $historyFailure = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_HISTORY_FAILURE);
         $historyLifetimes = [
             Schedule::STATUS_SUCCESS => $historySuccess * self::SECONDS_IN_MINUTE,
             Schedule::STATUS_MISSED => $historyFailure * self::SECONDS_IN_MINUTE,
             Schedule::STATUS_ERROR => $historyFailure * self::SECONDS_IN_MINUTE,
+            Schedule::STATUS_PENDING => max($historyFailure, $historySuccess) * self::SECONDS_IN_MINUTE,
         ];

-        $now = $this->timezone->scopeTimeStamp();
-        /** @var Schedule $record */
-        foreach ($history as $record) {
-            $checkTime = $record->getExecutedAt() ? strtotime($record->getExecutedAt()) :
-                strtotime($record->getScheduledAt()) + $scheduleLifetime;
-            if ($checkTime < $now - $historyLifetimes[$record->getStatus()]) {
-                $record->delete();
-            }
+        $jobs = $this->getJobs()[$groupId];
+        $scheduleResource = $this->_scheduleFactory->create()->getResource();
+        $connection = $scheduleResource->getConnection();
+        $count = 0;
+        foreach ($historyLifetimes as $status => $time) {
+            $count += $connection->delete(
+                $scheduleResource->getMainTable(),
+                [
+                    'status = ?' => $status,
+                    'job_code in (?)' => array_keys($jobs),
+                    'created_at < ?' => $connection->formatDate($currentTime - $time)
+                ]
+            );
         }

-        // save time history cleanup was ran with no expiration
-        $this->_cache->save(
-            $this->timezone->scopeTimeStamp(),
-            self::CACHE_KEY_LAST_HISTORY_CLEANUP_AT . $groupId,
-            ['crontab'],
-            null
-        );
-
-        return $this;
+        if ($count) {
+            $this->logger->info(sprintf('%d cron jobs were cleaned', $count));
+        }
     }

     /**
@@ -442,16 +487,23 @@ class ProcessCronQueueObserver implements ObserverInterface
      */
     protected function saveSchedule($jobCode, $cronExpression, $timeInterval, $exists)
     {
-        $currentTime = $this->timezone->scopeTimeStamp();
+        $currentTime = $this->dateTime->gmtTimestamp();
         $timeAhead = $currentTime + $timeInterval;
         for ($time = $currentTime; $time < $timeAhead; $time += self::SECONDS_IN_MINUTE) {
-            $ts = strftime('%Y-%m-%d %H:%M:00', $time);
-            if (!empty($exists[$jobCode . '/' . $ts])) {
-                // already scheduled
+            $scheduledAt = strftime('%Y-%m-%d %H:%M:00', $time);
+            $alreadyScheduled = !empty($exists[$jobCode . '/' . $scheduledAt]);
+            $schedule = $this->createSchedule($jobCode, $cronExpression, $time);
+            $valid = $schedule->trySchedule();
+            if (!$valid) {
+                if ($alreadyScheduled) {
+                    if (!isset($this->invalid[$jobCode])) {
+                        $this->invalid[$jobCode] = [];
+                    }
+                    $this->invalid[$jobCode][] = $scheduledAt;
+                }
                 continue;
             }
-            $schedule = $this->generateSchedule($jobCode, $cronExpression, $time);
-            if ($schedule->trySchedule()) {
+            if (!$alreadyScheduled) {
                 // time matches cron expression
                 $schedule->save();
             }
@@ -464,13 +516,13 @@ class ProcessCronQueueObserver implements ObserverInterface
      * @param int $time
      * @return Schedule
      */
-    protected function generateSchedule($jobCode, $cronExpression, $time)
+    protected function createSchedule($jobCode, $cronExpression, $time)
     {
         $schedule = $this->_scheduleFactory->create()
             ->setCronExpr($cronExpression)
             ->setJobCode($jobCode)
             ->setStatus(Schedule::STATUS_PENDING)
-            ->setCreatedAt(strftime('%Y-%m-%d %H:%M:%S', $this->timezone->scopeTimeStamp()))
+            ->setCreatedAt(strftime('%Y-%m-%d %H:%M:%S', $this->dateTime->gmtTimestamp()))
             ->setScheduledAt(strftime('%Y-%m-%d %H:%M', $time));

         return $schedule;
@@ -482,12 +534,174 @@ class ProcessCronQueueObserver implements ObserverInterface
      */
     protected function getScheduleTimeInterval($groupId)
     {
-        $scheduleAheadFor = (int)$this->_scopeConfig->getValue(
-            'system/cron/' . $groupId . '/' . self::XML_PATH_SCHEDULE_AHEAD_FOR,
-            \Magento\Store\Model\ScopeInterface::SCOPE_STORE
-        );
+        $scheduleAheadFor = (int)$this->getCronGroupConfigurationValue($groupId, self::XML_PATH_SCHEDULE_AHEAD_FOR);
         $scheduleAheadFor = $scheduleAheadFor * self::SECONDS_IN_MINUTE;

         return $scheduleAheadFor;
     }
+
+    /**
+     * Clean up scheduled jobs that are disabled in the configuration
+     * This can happen when you turn off a cron job in the config and flush the cache
+     *
+     * @param string $groupId
+     * @return void
+     */
+    private function cleanupDisabledJobs($groupId)
+    {
+        $jobs = $this->getJobs();
+        $jobsToCleanup = [];
+        foreach ($jobs[$groupId] as $jobCode => $jobConfig) {
+            if (!$this->getCronExpression($jobConfig)) {
+                /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */
+                $jobsToCleanup[] = $jobCode;
+            }
+        }
+
+        if (count($jobsToCleanup) > 0) {
+            $scheduleResource = $this->_scheduleFactory->create()->getResource();
+            $count = $scheduleResource->getConnection()->delete(
+                $scheduleResource->getMainTable(),
+                [
+                    'status = ?' => Schedule::STATUS_PENDING,
+                    'job_code in (?)' => $jobsToCleanup,
+                ]
+            );
+
+            $this->logger->info(sprintf('%d cron jobs were cleaned', $count));
+        }
+    }
+
+    /**
+     * @param array $jobConfig
+     * @return null|string
+     */
+    private function getCronExpression($jobConfig)
+    {
+        $cronExpression = null;
+        if (isset($jobConfig['config_path'])) {
+            $cronExpression = $this->getConfigSchedule($jobConfig) ?: null;
+        }
+
+        if (!$cronExpression) {
+            if (isset($jobConfig['schedule'])) {
+                $cronExpression = $jobConfig['schedule'];
+            }
+        }
+        return $cronExpression;
+    }
+
+    /**
+     * Clean up scheduled jobs that do not match their cron expression anymore
+     * This can happen when you change the cron expression and flush the cache
+     *
+     * @return $this
+     */
+    private function cleanupScheduleMismatches()
+    {
+        /** @var \Magento\Cron\Model\ResourceModel\Schedule $scheduleResource */
+        $scheduleResource = $this->_scheduleFactory->create()->getResource();
+        foreach ($this->invalid as $jobCode => $scheduledAtList) {
+            $scheduleResource->getConnection()->delete($scheduleResource->getMainTable(), [
+                'status = ?' => Schedule::STATUS_PENDING,
+                'job_code = ?' => $jobCode,
+                'scheduled_at in (?)' => $scheduledAtList,
+            ]);
+        }
+        return $this;
+    }
+
+    /**
+     * @return array
+     */
+    private function getJobs()
+    {
+        if ($this->jobs === null) {
+            $this->jobs = $this->_config->getJobs();
+        }
+        return $this->jobs;
+    }
+
+    /**
+     * Get CronGroup Configuration Value
+     *
+     * @param $groupId
+     * @return int
+     */
+    private function getCronGroupConfigurationValue($groupId, $path)
+    {
+        return $this->_scopeConfig->getValue(
+            'system/cron/' . $groupId . '/' . $path,
+            \Magento\Store\Model\ScopeInterface::SCOPE_STORE
+        );
+        return $scheduleLifetime;
+    }
+
+    /**
+     * Is Group In Filter
+     *
+     * @param $groupId
+     * @return bool
+     */
+    private function isGroupInFilter($groupId): bool
+    {
+        return !($this->_request->getParam('group') !== null
+            && trim($this->_request->getParam('group'), "'") !== $groupId);
+    }
+
+    /**
+     * Process pending jobs
+     *
+     * @param $groupId
+     * @param $jobsRoot
+     * @param $currentTime
+     */
+    private function processPendingJobs($groupId, $jobsRoot, $currentTime)
+    {
+        $procesedJobs = [];
+        $pendingJobs = $this->getPendingSchedules($groupId);
+        /** @var \Magento\Cron\Model\Schedule $schedule */
+        foreach ($pendingJobs as $schedule) {
+            if (isset($procesedJobs[$schedule->getJobCode()])) {
+                // process only on job per run
+                continue;
+            }
+            $jobConfig = isset($jobsRoot[$schedule->getJobCode()]) ? $jobsRoot[$schedule->getJobCode()] : null;
+            if (!$jobConfig) {
+                continue;
+            }
+
+            $scheduledTime = strtotime($schedule->getScheduledAt());
+            if ($scheduledTime > $currentTime) {
+                continue;
+            }
+
+            try {
+                if ($schedule->tryLockJob()) {
+                    $this->_runJob($scheduledTime, $currentTime, $jobConfig, $schedule, $groupId);
+                }
+            } catch (\Exception $e) {
+                $schedule->setMessages($e->getMessage());
+                if ($schedule->getStatus() === Schedule::STATUS_ERROR) {
+                    $this->logger->critical($e);
+                }
+                if ($schedule->getStatus() === Schedule::STATUS_MISSED
+                    && $this->state->getMode() === State::MODE_DEVELOPER
+                ) {
+                    $this->logger->error(
+                        sprintf(
+                            "%s Schedule Id: %s Job Code: %s",
+                            $schedule->getMessages(),
+                            $schedule->getScheduleId(),
+                            $schedule->getJobCode()
+                        )
+                    );
+                }
+            }
+            if ($schedule->getStatus() === Schedule::STATUS_SUCCESS) {
+                $procesedJobs[$schedule->getJobCode()] = true;
+            }
+            $schedule->save();
+        }
+    }
 }

diff -Naur a/vendor/magento/module-cron/Model/Schedule.php b/vendor/magento/module-cron/Model/Schedule.php
--- a/vendor/magento/module-cron/Model/Schedule.php
+++ b/vendor/magento/module-cron/Model/Schedule.php
@@ -1,18 +1,18 @@
 <?php
 /**
  * Copyright © Magento, Inc. All rights reserved.
  * See COPYING.txt for license details.
  */

 namespace Magento\Cron\Model;

 use Magento\Framework\Exception\CronException;
+use Magento\Framework\App\ObjectManager;
+use Magento\Framework\Stdlib\DateTime\TimezoneInterface;

 /**
  * Crontab schedule model
  *
- * @method \Magento\Cron\Model\ResourceModel\Schedule _getResource()
- * @method \Magento\Cron\Model\ResourceModel\Schedule getResource()
  * @method string getJobCode()
  * @method \Magento\Cron\Model\Schedule setJobCode(string $value)
  * @method string getStatus()
@@ -30,7 +30,8 @@ use Magento\Framework\Exception\CronException;
  * @method array getCronExprArr()
  * @method \Magento\Cron\Model\Schedule setCronExprArr(array $value)
  *
- * @author      Magento Core Team <core@magentocommerce.com>
+ * @api
+ * @since 100.0.2
  */
 class Schedule extends \Magento\Framework\Model\AbstractModel
 {
@@ -45,20 +46,28 @@ class Schedule extends \Magento\Framework\Model\AbstractModel
     const STATUS_ERROR = 'error';

     /**
+     * @var TimezoneInterface
+     */
+    private $timezoneConverter;
+
+    /**
      * @param \Magento\Framework\Model\Context $context
      * @param \Magento\Framework\Registry $registry
      * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
      * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
      * @param array $data
+     * @param TimezoneInterface $timezoneConverter
      */
     public function __construct(
         \Magento\Framework\Model\Context $context,
         \Magento\Framework\Registry $registry,
         \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
         \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
-        array $data = []
+        array $data = [],
+        TimezoneInterface $timezoneConverter = null
     ) {
         parent::__construct($context, $registry, $resource, $resourceCollection, $data);
+        $this->timezoneConverter = $timezoneConverter ?: ObjectManager::getInstance()->get(TimezoneInterface::class);
     }

     /**
@@ -66,7 +75,7 @@ class Schedule extends \Magento\Framework\Model\AbstractModel
      */
     public function _construct()
     {
-        $this->_init('Magento\Cron\Model\ResourceModel\Schedule');
+        $this->_init(\Magento\Cron\Model\ResourceModel\Schedule::class);
     }

     /**
@@ -101,6 +110,9 @@ class Schedule extends \Magento\Framework\Model\AbstractModel
             return false;
         }
         if (!is_numeric($time)) {
+            //convert time from UTC to admin store timezone
+            //we assume that all schedules in configuration (crontab.xml and DB tables) are in admin store timezone
+            $time = $this->timezoneConverter->date($time)->format('Y-m-d H:i');
             $time = strtotime($time);
         }
         $match = $this->matchCronExpression($e[0], strftime('%M', $time))
@@ -221,16 +233,17 @@ class Schedule extends \Magento\Framework\Model\AbstractModel
     }

     /**
-     * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING.
-     * Returns true if status was changed and false otherwise.
+     * Lock the cron job so no other scheduled instances run simultaneously.
      *
-     * This is used to implement locking for cron jobs.
+     * Sets a job to STATUS_RUNNING only if it is currently in STATUS_PENDING
+     * and no other jobs of the same code are currently in STATUS_RUNNING.
+     * Returns true if status was changed and false otherwise.
      *
      * @return boolean
      */
     public function tryLockJob()
     {
-        if ($this->_getResource()->trySetJobStatusAtomic(
+        if ($this->_getResource()->trySetJobUniqueStatusAtomic(
             $this->getId(),
             self::STATUS_RUNNING,
             self::STATUS_PENDING

diff -Naur a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php
--- a/vendor/magento/module-cron/Model/ResourceModel/Schedule.php
+++ b/vendor/magento/module-cron/Model/ResourceModel/Schedule.php
@@ -8,7 +8,8 @@ namespace Magento\Cron\Model\ResourceModel;
 /**
  * Schedule resource
  *
- * @author      Magento Core Team <core@magentocommerce.com>
+ * @api
+ * @since 100.0.2
  */
 class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
 {
@@ -23,9 +24,10 @@ class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
     }

     /**
-     * If job is currently in $currentStatus, set it to $newStatus
-     * and return true. Otherwise, return false and do not change the job.
-     * This method is used to implement locking for cron jobs.
+     * Sets new schedule status only if it's in the expected current status.
+     *
+     * If schedule is currently in $currentStatus, set it to $newStatus and
+     * return true. Otherwise, return false.
      *
      * @param string $scheduleId
      * @param string $newStatus
@@ -45,4 +47,49 @@ class Schedule extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
         }
         return false;
     }
+
+    /**
+     * Sets schedule status only if no existing schedules with the same job code
+     * have that status.  This is used to implement locking for cron jobs.
+     *
+     * If the schedule is currently in $currentStatus and there are no existing
+     * schedules with the same job code and $newStatus, set the schedule to
+     * $newStatus and return true. Otherwise, return false.
+     *
+     * @param string $scheduleId
+     * @param string $newStatus
+     * @param string $currentStatus
+     * @return bool
+     * @since 100.2.0
+     */
+    public function trySetJobUniqueStatusAtomic($scheduleId, $newStatus, $currentStatus)
+    {
+        $connection = $this->getConnection();
+
+        // this condition added to avoid cron jobs locking after incorrect termination of running job
+        $match = $connection->quoteInto(
+            'existing.job_code = current.job_code ' .
+            'AND (existing.executed_at > UTC_TIMESTAMP() - INTERVAL 1 DAY OR existing.executed_at IS NULL) ' .
+            'AND existing.status = ?',
+            $newStatus
+        );
+
+        $selectIfUnlocked = $connection->select()
+            ->joinLeft(
+                ['existing' => $this->getTable('cron_schedule')],
+                $match,
+                ['status' => new \Zend_Db_Expr($connection->quote($newStatus))]
+            )
+            ->where('current.schedule_id = ?', $scheduleId)
+            ->where('current.status = ?', $currentStatus)
+            ->where('existing.schedule_id IS NULL');
+
+        $update = $connection->updateFromSelect($selectIfUnlocked, ['current' => $this->getTable('cron_schedule')]);
+        $result = $connection->query($update)->rowCount();
+
+        if ($result == 1) {
+            return true;
+        }
+        return false;
+    }
 }
