惊呆了,高并发下System.currentTimeMillis()竟然有这么大的问题?!!!

前言

最近在做滑动窗口的优化实现中,了解到在并发情况下System.currentTimeMillis()竟然有严重的性能问题,所以自己做测试测试下。在印象中我们感觉这是基于底层的api应该不会有什么大问题,也确实,在不是高并发下也不会出现什么问题。

 /**
     * 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方法,doc中解释了获取ms完全取决于操作系统的实现,有的操作系统甚至是以10ms为计量,所以这也导致了获取时间会有精度问题

我们获取到的ms数值是什么

接下来做这么一个测试。

public class Main {
    public static void main(String[] args) {
        long time = new Date(0).getTime();
        long now = new Date().getTime();
        System.out.println(time);
        System.out.println(now);
        System.out.println(now - time);
        System.out.println(System.currentTimeMillis());
    }
}

输出结果

0
1600499192698
1600499192698
1600499192698

所以我们可以知道new Date().getTime()获取到的数据是格林尼治标准时间1970年1月1日 0时0分0秒到现在的毫秒数。当然文档中有写,自己只是验证下。

接下来我们测试下性能情况

我们先在单线程下测试。

public class Main {

    public static void main(String[] args) {
      //执行一百次循环
        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);
        }
    }
}

结果是大多数集中在260左右但是会有一小部分数据在310左右(这个结果仅限于作者本机操作数据情况)。

278,266,310,271,292,261,252,253,253,251,251,252,252,250,251,253,251,251,252,252,252,251,252,250,252,250,251,251,251,250,254,252,250,251,250,250,250,250,254,256,252,252,251,252,253,251,252,267,274,282,273,261,277,290,310,265,278,272,265,258,255,252,251,261,264,257,254,252,251,255,266,262,253,259,257,252,250,252,253,323,251,251,255,259,257,255,262,277,288,268,273,267,271,261,257,263,253,257,263,261

我们知道cpu运算是非常快的,出现了50ms的误差这个误差还是非常大的。

然后再看多线程下的对比。

public class Main {

    public static void main(String[] args) throws InterruptedException {
        // 测试执行1次
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        for (int i = 0; i < 1; i++) {
            System.currentTimeMillis();
        }
        stopWatch.stop();
        long totalTimeNanos = stopWatch.getLastTaskTimeNanos();
        System.out.println(totalTimeNanos);
        System.out.println("=====================");
        System.out.println("=====================");
        //100个线程各执行一次
        CountDownLatch wait = new CountDownLatch(1);
        CountDownLatch threadLatch = new CountDownLatch(100);
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                try {
                    StopWatch watch = new StopWatch();
                    //先阻塞住所有线程
                    wait.await();
                    watch.start();
                    System.currentTimeMillis();
                    watch.stop();
                    System.out.println(watch.getTotalTimeNanos());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    threadLatch.countDown();
                }
            }).start();
        }
        //暂停1s保证线程创建完成
        Thread.sleep(1000);
        wait.countDown();
        threadLatch.await();
    }
}

果然最后的测试结果 没有让我们失望。

5313
=====================
=====================
1407,724,565,848,719,507,535,520,740,504,537,432,540,399,458,467,428,371,572,417,680,434,437,480,584,486,583,466,367,752,744,623,490,570,449,640,694,543,528,564,441,442,705,512,611,518,363,566,495,693,605,693,586,737,474,578,593,540,666,462,648,729,735,714,645,628,501,474,678,636,577,487,378,745,570,1010,479,535,421,433,1964,415,608,471,451,558,345,632,727,431,1499,681,487,2171,527,323,457,575,715,386

数据基本上都集中在500ns左右,但是其中个别数据到了1000ns甚至2000ns以上。

所以我们不管是单线程还是多线程下,高频的调用System.currentTimeMillis()都会产生延迟。

为什么呢

单线程下产生延迟说明在系统底层上该线程和其他进程或线程产生了竞争,探究下hotspot中的实现:

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计时器下性能尤其差;
  • 系统只有一个全局时钟源,高并发或频繁访问会造成严重的争用。

如何对此做优化呢

既然频繁的从用户态切换到内核态,那我们能不能减少切换的次数呢,我们上面的测试可以得知1ms我们可以调用多次System.currentTimeMillis(),但是如果我们精度是1ms,是不是我们只要让他保证1ms获取一次就可以了,这样还大大的减少了和其他线程抢夺资源的概率,同时也节约了资源。我们把每1ms获取到的数据保存到内存中,这样我们所有的线程都从内存中取数据,岂不快哉。

import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 时钟工具
 **/
class SystemClock {

    /**
     * 设置周期
     */
    private final long period;

    /**
     * 用来作为我们时间戳存储的容器
     */
    private final AtomicLong now;

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

    /**
     * 初始化单例
     * @return
     */
    private static SystemClock instance() {
        return InstanceHolder.INSTANCE;
    }

    /**
     * 获取毫秒时间戳 替换System.currentTimeMillis()
     * @return
     */
    public static long currentTimeMillis() {
        return instance().now();
    }

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

    /**
     * 初始化定时器
     */
    private void scheduleClockUpdating() {
        ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1, r -> {
            Thread thread = new Thread(r, "System Clock");
            //设置为守护线程
            thread.setDaemon(true);
            return thread;
        });
        scheduler.scheduleAtFixedRate(() -> now.set(System.currentTimeMillis()), period, period, TimeUnit.MILLISECONDS);
    }

    private static class InstanceHolder {
        //设置1ms更新一次时间
        static final SystemClock INSTANCE = new SystemClock(1);
    }
}

我们在测试下单线程的情况

public static void main(String[] args) throws InterruptedException {

        //执行一百次循环
        for (int t = 0; t < 100; t++) {
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();
            //获取一千万次时间
            for (int i = 0; i < 10000000; i++) {
                //替换成我们自己的实现类
                SystemClock.currentTimeMillis();
            }
            stopWatch.stop();
            long totalTimeMillis = stopWatch.getTotalTimeMillis();
            System.out.println(totalTimeMillis);
        }

    }
58,14,9,8,9,10,11,11,11,9,10,11,10,9,10,9,9,9,9,9,9,9,9,9,9,8,9,9,10,9,9,9,9,9,9,9,9,9,9,9,8,9,9,8,13,14,10,9,9,9,9,9,8,8,8,8,8,10,10,9,8,8,9,11,13,15,16,15,12,10,11,11,10,10,10,9,9,9,10,13,12,11,11,10,10,10,10,10,11,10,10,10,9,9,9,9,8,9,9,9

得到的结果对比来看还是非常明显的,几十倍的差距。

😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉
👉感觉不错的小伙伴,点个关注,加个收藏!!! 👈
👉感觉不错的小伙伴,点个关注,加个收藏!!! 👈
👉感觉不错的小伙伴,点个关注,加个收藏!!! 👈
😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉😉

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值