高并发场景下System.currentTimeMillis()的性能问题的优化

2022-11-21 20:31:11 浏览数 (1)

本篇文章,我们一起来看下System.currentTimeMillis()的性能问题。

一、发现问题

从一个示例看System.currentTimeMillis()的问题:

代码语言:javascript复制
import org.springframework.util.StopWatch;

public class Main {

  public static void main(String[] args) {

    // 执行20次循环
    for (int t = 0; t < 100; t  ) {
      StopWatch stopWatch = new StopWatch();
      stopWatch.start();
      // 获取一千万次时间
      for (int i = 0; i < 10000000; i  ) {
        System.currentTimeMillis();
      }
      stopWatch.stop();
      long totalTimeMillis = stopWatch.getTotalTimeMillis();
      System.out.println(totalTimeMillis);
    }
  }
}

一次执行结果:

代码语言:javascript复制
364
363
380
350
355
354
371
350
350
352
351
442
379
367
... ... 
349
385
381
348
... ...

353
362
352
379
... ...
446
404
371
357
... ...
384
378
583
468
518
444
440
393
466
454
461
396
388
538
419

我们可以从上面的某一次运行结果看出,大部分的时间集中在350ms左右,但是也有400ms,甚至500ms以上的,这中间将近有150ms的偏差。

二、分析问题

我们先来看下方法的内容:

代码语言:javascript复制
    /**
     * Returns the current time in milliseconds.  Note that
     * while the unit of time of the return value is a millisecond,
     * the granularity of the value depends on the underlying
     * operating system and may be larger.  For example, many
     * operating systems measure time in units of tens of
     * milliseconds.
     *
     * <p> See the description of the class <code>Date</code> for
     * a discussion of slight discrepancies that may arise between
     * "computer time" and coordinated universal time (UTC).
     *
     * @return  the difference, measured in milliseconds, between
     *          the current time and midnight, January 1, 1970 UTC.
     * @see     java.util.Date
     */
    public static native long currentTimeMillis();

我们可以看出,这是一个native的方法,其取值与操作系统的底层实现有关。

产生问题的主要原因是:在执行该 native 方法时会涉及从 用户态到内核态的切换,去调用 Linux 内核的时钟源,时钟源只有一个,因此在大量并发时会造成严重的资源竞争,从而导致出现性能问题

借助https://blog.csdn.net/qq_30062181/article/details/108681101中的描述,单线程下产生延迟说明在系统底层上该线程和其他进程或线程产生了竞争,探究下hotspot中的实现:

代码语言:javascript复制
jlong os::javaTimeMillis() {
  timeval time;
  int status = gettimeofday(&time, NULL);
  assert(status != -1, "linux error");
  return jlong(time.tv_sec) * 1000     jlong(time.tv_usec / 1000);
}

以下是查询得知,涉及到汇编层面了。

  • 调用gettimeofday()需要从用户态切换到内核态;
  • gettimeofday()的表现受系统的计时器(时钟源)影响,在HPET计时器下性能尤其差;
  • 系统只有一个全局时钟源,高并发或频繁访问会造成严重的争用。

三、解决问题

我们从#1和#2可以看出,1ms内可以有多次调用System.currentTimeMillis(),造成频繁的从用户态切换到内核态,从而影响性能。那么,在高并发情况下,可以考虑通过后台线程定时去调用 System.currentTimeMillis() 方法获取时间,然后保存在内存变量中,1ms精度下内存变量的值可以在线程复用,这样的话就能减少竞争以及用户态和内核态的切换。

思考:

多线程中使用,我们可以使用单例的方式来写。比如:

内部类的实现方式

代码语言:javascript复制
import java.sql.Timestamp;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public class SystemClock {

    private final long period;
    private final AtomicLong now;

    private SystemClock(long period) {
        this.period = period;
        this.now = new AtomicLong(System.currentTimeMillis());
        scheduleClockUpdating();
    }

    private static SystemClock instance() {
        return InstanceHolder.INSTANCE;
    }

    public static long now() {
        return instance().currentTimeMillis();
    }

    public static String nowDate() {
        return new Timestamp(instance().currentTimeMillis()).toString();
    }

    private void scheduleClockUpdating() {
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
            Thread thread = new Thread(runnable, "System Clock");
            thread.setDaemon(true);
            return thread;
        });
        scheduler.scheduleAtFixedRate(() -> now.set(System.currentTimeMillis()), period, period, TimeUnit.MILLISECONDS);
    }

    private long currentTimeMillis() {
        return now.get();
    }

    private static class InstanceHolder {
        public static final SystemClock INSTANCE = new SystemClock(1);
    }
}

枚举单例的实现

代码语言:javascript复制
import java.sql.Timestamp;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public enum SystemClock {

    INSTANCE(1);

    private final long period;
    private final AtomicLong nowTime;
    private boolean started = false;
    private ScheduledExecutorService executorService;

    SystemClock(long period) {
        this.period = period;
        this.nowTime = new AtomicLong(System.currentTimeMillis());
    }

    /**
     * The initialize scheduled executor service
     */
    public void initialize() {
        if (started) {
            return;
        }

        this.executorService = new ScheduledThreadPoolExecutor(1, r -> {
            Thread thread = new Thread(r, "system-clock");
            thread.setDaemon(true);
            return thread;
        });
        executorService.scheduleAtFixedRate(() -> nowTime.set(System.currentTimeMillis()),
                this.period, this.period, TimeUnit.MILLISECONDS);
        Runtime.getRuntime().addShutdownHook(new Thread(this::destroy));
        started = true;
    }

    /**
     * The get current time milliseconds
     *
     * @return long time
     */
    public long currentTimeMillis() {
        return started ? nowTime.get() : System.currentTimeMillis();
    }

    /**
     * The get string current time
     *
     * @return string time
     */
    public String currentTime() {
        return new Timestamp(currentTimeMillis()).toString();
    }

    /**
     * The destroy of executor service
     */
    public void destroy() {
        if (executorService != null) {
            executorService.shutdown();
        }
    }

}

当然,也可以使用其它单例的写法来处理。

四、比对一下

使用SystemClock.now();替换System.currentTimeMillis();,再执行一下:

代码语言:javascript复制
import org.springframework.util.StopWatch;

public class Main {

  public static void main(String[] args) {

    // 执行100次循环
    for (int t = 0; t < 100; t  ) {
      StopWatch stopWatch = new StopWatch();
      stopWatch.start();
      // 获取一千万次时间
      for (int i = 0; i < 10000000; i  ) {
        //System.currentTimeMillis();
        SystemClock.now();
      }
      stopWatch.stop();
      long totalTimeMillis = stopWatch.getTotalTimeMillis();
      System.out.println(totalTimeMillis);
    }
  }
}

某次输出结果:

代码语言:javascript复制
97
10
2
2
2
3
2
... ...
3
5
5
5
4
6
2
4
2
3
3
2
2
3
2
2
3
2
2
... ...
3
2
3
2
2
6
2
4
4
4
2
... ...
7
9

3
2
2
2
3
2
2

我们可以看出,优化后的执行的值相对较为集中且偏差相对较小。至此,我们利用ScheduledExecutorService实现高并发场景下System.curentTimeMillis()的性能问题的优化的示例就完成了。

当然,本文主要以单线程的角度分析了问题。在多线程场景中,高频使用System.curentTimeMillis()的话,同样存在延迟和偏差的问题。有兴趣的读者可以借助CountDownLatch完成多线程并发的模拟试验一下,本文就不再展开。

代码语言:javascript复制
CountDownLatch wait = new CountDownLatch(1);
CountDownLatch threadLatch = new CountDownLatch(300);

0 人点赞