Действительно ли производительность System.currentTimeMillis настолько плоха?

Java
Действительно ли производительность System.currentTimeMillis настолько плоха?

Добро пожаловать на перепечатку, перепечатка с указанием автора оригинала и источника

Сомневаетесь, у System.currentTimeMillis действительно проблемы с производительностью?

Недавно, когда я исследовал исходный код промежуточного программного обеспечения, я обнаружил, что оно получает текущее время не через System.currentTimeMillis, а через собственный класс кэширования System.currentTimeMillis (см. ниже). невыносимо?? Должны ли они быть заменены пользовательскими кэшированными часами?

/**
 * 弱精度的计时器,考虑性能不使用同步策略。
 * 
 * @author mycat
 */
public class TimeUtil {
    //当前毫秒数的缓存
    private static volatile long CURRENT_TIME = System.currentTimeMillis();

    public static final long currentTimeMillis() { return CURRENT_TIME; }
    public static final long currentTimeNanos() { return System.nanoTime(); }
	//更新缓存
    public static final void update() { CURRENT_TIME = System.currentTimeMillis(); }

}
//使用定时任务调度线程池,定期(每1s)调用update方法更新缓存时钟
heartbeatScheduler.scheduleAtFixedRate(processorCheck(), 0L, 1000, TimeUnit.MILLISECONDS);

Чтобы не отставать от тенденции времени и не отставать от темпов оптимизации производительности «мастеров», я быстро поискал в Интернете «производительность currentTimeMillis», и 9 из 10 результатов поиска были о производительности system.currentTimeMillis. :

Нажмите на нее, здесь написано System.currentTimeMillisЭто занимает примерно в 100 раз больше времени, чем создание обычного объекта., который снова вынул тестовую запись и сказал System.currentTimeMillisОдновременное время в 250 раз выше, чем у однопоточных вызовов

Думая, в чем проблема производительности с System.currentTimeMillis

Видя это, я не могу дождаться, чтобы немедленно открыть IDEA и заменить все System.currentTimeMillis в коде, но как строгий программист, как я могу следовать тренду и следовать за толпой? Поэтому я внимательно прочитал эти статьи и резюмировал их пункты:

  • System.currentTimeMillisДоступ к системным часам, относящийся к ресурсу критической области, что неизбежно приведет к многопоточному конфликту в параллельных условиях.
  • System.currentTimeMillis() работает медленно, потому чтоВзаимодействовал с системой
  • яиметь протоколы испытаний, время параллелизма в 250 раз выше, чем у одного потока!

Но я присмотрелся и нашел эти виды дырявыми:

  1. System.currentTimeMillis делаетДоступ к системным часам, если быть точным, заключается в чтении времени стены (xtime), xtime используется системой Linux для получения текущего времени для пользовательского пространства, а само ядро ​​в основном его не использует, а только поддерживает и обновляет.Более того, xtime чтения и записи использует последовательную блокировку в ядре Linux, а не блокировку взаимного исключения, и потоки чтения не влияют друг на друга.

    Вы можете думать о последовательных блокировках как о блокировках CompareAndSwap, которые решают «проблему ABA». Для ресурса критической секции (в данном случае xtime) существует порядковый номер операции, и операция записи увеличит порядковый номер на 1, а операция чтения — нет.

    Операция записи: CAS делает серийный номер +1

    Операция чтения: сначала получите серийный номер, прочитайте данные, а затем снова получите серийный номер.Если серийные номера, полученные до и после, совпадают, это доказывает, что во время операции записи нет помех от операции записи, затем это чтение действительно, и данные возвращаются, в противном случае это означает, что данные могли быть изменены при чтении, на этот раз чтение недействительно, и операция чтения выполняется снова.

    У вас может возникнуть вопрос: могут ли данные измениться при чтении xtime? Разве операции жесткого чтения не атомарны? Это связано с тем, что xtime является 64-разрядным, для 32-разрядных машин его необходимо прочитать дважды, а 64-разрядные машины не вызовут этой проблемы параллелизма.

  2. Взаимодействовал с системой, действительно, пользовательские процессы должны войти в режим ядра для доступа к системным ресурсам, однакоНовый объект, выделение памяти также является системным вызовом, и для работы с системой также необходимо войти в режим ядра., просто прочитать время стены системы в 100 раз больше времени, чем переместить указатель памяти и инициализировать память? невероятный

  3. Что касается так называемой тестовой записи, позвольте мне показать вам его тестовый код:

    Проблема с этим тестовым кодом заключается в том, чтоВремя, потраченное на блокировку endLatch.countDown, также включается в общее время., блокировка реализована на основе CAS.В текущем сценарии с интенсивными вычислениями большое количество потоков перегружено, и почти все они будут приостановлены из-за сбоя CAS.Требуется много времени для приостановки, постановки в очередь и подавления большого количества потоков.Не маленькое число. ВторойИспользование этого метода (от начала выполнения до конца выполнения) для сравнения затрат времени на параллельные и однопоточные вызовы также проблематично., Как один поток соотносится с общим временем выполнения нескольких потоков? Сравнение должно представлять собой сумму затрат времени на каждый вызов (см. ниже).

    long begin = System.nanoTime();
    //单次调用System.currrentTimeMillis()
    long end = System.nanoTime();
    sum += end - begin;
    

    Запишите общее время, потраченное на каждый звонок, хотя этот метод также будет учитывать System.nanoTime() в общем времени, но поскольку System.nanoTime() будет записываться независимо от параллельного тестирования или однопоточного тестирования, это не приведет к недобросовестному тестированию

Данные говорят, с производительностью System.currentTimeMillis проблем нет

Улучшив тестовый код (см. в конце статьи тестовый код) и добавив для сравнения кэш-такты оптимизированных «мастеров», я получил следующие данные:

Время\Время ожидания\Сценарий Однопоточная система Однопоточные часы кэша 200 нитей системы Кэш-часы на 200 потоков
1w 3.682 ms 42.844 ms 0.583 ms 0.444 ms
10w 6.780 ms 35.837 ms 3.379 ms 3.066 ms
100w 30.764 ms 70.917 ms 36.416 ms 27.906 ms
1000w 263.287 ms 427.319 ms 355.452 ms 261.360 ms

Система означает System.currentTimeMillis

Кэшированные часы представляют класс часов, который использует статические переменные-члены в качестве кэша System.currentTimeMillis.

200 потоков — количество потоков по умолчанию для Tomcat.

Результаты тестирования с использованием JMH (Java Benchmarking Framework)

Количество тестов\среднее время\сценарий System кеш-часы
1w (Windows) 0,368 ± 0,667 мкс/время 0,578 ± 1,039 мкс/время
1w (Linux) 0,478 ± 0,393 мкс/время 6,083 ± 6,064 мкс/время

JMH использует в два раза больше потоков ЦП (8 потоков), чем рекомендуется, и подсчитывается среднее время.См. в конце статьи тестовый код. Кроме того, конфигурации Виндос и Линукс разные, и они несопоставимы друг с другом.

Анализ результатов испытаний

Вы можете увидеть System.currentTimeMillisПроизводительность параллелизма неплохая, и даже намного сильнее, чем один поток в случае меньшего количества раз (кратковременные одновременные вызовы)., пока вЭффективность однопоточных вызовов также примерно в два раза выше, чем тактовая частота кэша.. В реальной среде почти невозможно добиться многопоточных одновременных вызовов System.currentTimeMillis в течение длительного времени в приведенном выше тесте, поэтому я не думаю, что нужно делать так называемую «оптимизацию» System. текущийВремяМиллис

Здесь нет теста «новый объект», потому что JVM действительно даст вам новый объект в памяти кучи, если new Object() не написан в коде. Это оптимизация компиляции JVM -анализ побега: сначала проанализируйте область создаваемого объекта, если объект допустим только в одном методе (объект локальной переменной), он принадлежитНет способа убежать, вместо фактического создания объекта, какой бы метод объекта вы ни вызывали в методе, встраивайте блок кода этого метода. Действителен только в пределах потока принадлежитНет выхода из потока, который создает объект, но автоматически удаляет бесполезные меры синхронизации, которые мы сделали.

Наконец

На бумаге я чувствую себя поверхностным в конце, и я абсолютно точно знаю, что это дело должно быть сделано.

Если вы хотите изучить JMH, пожалуйста, следуйте официальной документации GitHub.Чужие блоги могут не работать и они будут перемещены вверх.Автор только что наступил на эту яму.

Наконец, вот мой тестовый код

Тестовый код:

public class CurrentTimeMillisTest {

    public static void main(String[] args) {
        int num = 10000000;
        System.out.print("单线程"+num+"次System.currentTimeMillis调用总耗时:    ");
        System.out.println(singleThreadTest(() -> {
            long l = System.currentTimeMillis();
        },num));
        System.out.print("单线程"+num+"次CacheClock.currentTimeMillis调用总耗时:");
        System.out.println(singleThreadTest(() -> {
            long l = CacheClock.currentTimeMillis();
        },num));
        System.out.print("并发"+num+"次System.currentTimeMillis调用总耗时:      ");
        System.out.println(concurrentTest(() -> {
            long l = System.currentTimeMillis();
        },num));
        System.out.print("并发"+num+"次CacheClock.currentTimeMillis调用总耗时:  ");
        System.out.println(concurrentTest(() -> {
            long l = CacheClock.currentTimeMillis();
        },num));
    }



    /**
     * 单线程测试
     * @return
     */
    private static long singleThreadTest(Runnable runnable,int num) {
        long sum = 0;
        for (int i = 0; i < num; i++) {
            long begin = System.nanoTime();
            runnable.run();
            long end = System.nanoTime();
            sum += end - begin;
        }
        return sum;
    }

    /**
     * 并发测试
     * @return
     */
    private static long concurrentTest(Runnable runnable,int num) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(200,200,60, TimeUnit.SECONDS,new LinkedBlockingQueue<>(num));
        long[] sum = new long[]{0};
        //闭锁基于CAS实现,并不适合当前的计算密集型场景,可能导致等待时间较长
        CountDownLatch countDownLatch = new CountDownLatch(num);
        for (int i = 0; i < num; i++) {
            threadPoolExecutor.submit(() -> {
                long begin = System.nanoTime();
                runnable.run();
                long end = System.nanoTime();
                //计算复杂型场景更适合使用悲观锁
                synchronized(CurrentTimeMillisTest.class) {
                    sum[0] += end - begin;
                }
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return sum[0];
    }


    /**
     * 缓存时钟,缓存System.currentTimeMillis()的值,每隔20ms更新一次
     */
    public static class CacheClock{
        //定时任务调度线程池
        private static ScheduledExecutorService timer = new ScheduledThreadPoolExecutor(1);
        //毫秒缓存
        private static volatile long timeMilis;      
        static {
            //每秒更新毫秒缓存
            timer.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    timeMilis = System.currentTimeMillis();
                }
            },0,1000,TimeUnit.MILLISECONDS);
        }
        
        public static long currentTimeMillis() {
            return timeMilis;
        }
    }
}

Тестовый код с использованием JMH:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
//120轮预热,充分利用JIT的编译优化技术
@Warmup(iterations = 120,time = 1,timeUnit = TimeUnit.MILLISECONDS)
@Measurement(time = 1,timeUnit = TimeUnit.MICROSECONDS)
//线程数:CPU*2(计算复杂型,也有CPU+1的说法)
@Threads(8)
@Fork(1)
@State(Scope.Benchmark)
public class JMHTest {

    public static void main(String[] args) throws RunnerException {
        testNTime(10000);
    }

    private static void testNTime(int num) throws RunnerException {
        Options options = new OptionsBuilder()
                .include(JMHTest.class.getSimpleName())
                .measurementIterations(num)
                .output("E://testRecord.log")
                .build();
        new Runner(options).run();
    }


    /**
     * System.currentMillisTime测试
     * @return 将结果返回是为了防止死码消除(编译器将 无引用的变量 当成无用代码优化掉)
     */
    @Benchmark
    public long testSystem() {
        return System.currentTimeMillis();
    }

    /**
     * 缓存时钟测试
     * @return
     */
    @Benchmark
    public long testCacheClock() {
        return JMHTest.CacheClock.currentTimeMillis();
    }

    /**
     * 缓存时钟,缓存System.currentTimeMillis()的值,每隔1s更新一次
     */
    public static class CacheClock{
        private static ScheduledExecutorService timer = new ScheduledThreadPoolExecutor(1);
        private static volatile long timeMilis;
        static {
            timer.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    timeMilis = System.currentTimeMillis();
                }
            },0,1000,TimeUnit.MILLISECONDS);
        }
        public static long currentTimeMillis() {
            return timeMilis;
        }
    }
}