Добро пожаловать на перепечатку, перепечатка с указанием автора оригинала и источника
Сомневаетесь, у 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 раз выше, чем у одного потока!
Но я присмотрелся и нашел эти виды дырявыми:
-
System.currentTimeMillis делаетДоступ к системным часам, если быть точным, заключается в чтении времени стены (xtime), xtime используется системой Linux для получения текущего времени для пользовательского пространства, а само ядро в основном его не использует, а только поддерживает и обновляет.Более того, xtime чтения и записи использует последовательную блокировку в ядре Linux, а не блокировку взаимного исключения, и потоки чтения не влияют друг на друга.
Вы можете думать о последовательных блокировках как о блокировках CompareAndSwap, которые решают «проблему ABA». Для ресурса критической секции (в данном случае xtime) существует порядковый номер операции, и операция записи увеличит порядковый номер на 1, а операция чтения — нет.
Операция записи: CAS делает серийный номер +1
Операция чтения: сначала получите серийный номер, прочитайте данные, а затем снова получите серийный номер.Если серийные номера, полученные до и после, совпадают, это доказывает, что во время операции записи нет помех от операции записи, затем это чтение действительно, и данные возвращаются, в противном случае это означает, что данные могли быть изменены при чтении, на этот раз чтение недействительно, и операция чтения выполняется снова.
У вас может возникнуть вопрос: могут ли данные измениться при чтении xtime? Разве операции жесткого чтения не атомарны? Это связано с тем, что xtime является 64-разрядным, для 32-разрядных машин его необходимо прочитать дважды, а 64-разрядные машины не вызовут этой проблемы параллелизма.
-
Взаимодействовал с системой, действительно, пользовательские процессы должны войти в режим ядра для доступа к системным ресурсам, однакоНовый объект, выделение памяти также является системным вызовом, и для работы с системой также необходимо войти в режим ядра., просто прочитать время стены системы в 100 раз больше времени, чем переместить указатель памяти и инициализировать память? невероятный
-
Что касается так называемой тестовой записи, позвольте мне показать вам его тестовый код:
Проблема с этим тестовым кодом заключается в том, чтоВремя, потраченное на блокировку 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;
}
}
}