那些年我们写过的Java代码

优化System.currentTimeMillis()高并发性能问题

高并发场景下,System.currentTimeMillis() 会有性能问题。System.currentTimeMillis() 的调用比 new 一个普通对象要耗时的多,System.currentTimeMillis() 之所以慢是因为去跟系统打了一次交道,后台定时更新时钟,JVM退出时,线程自动回收。

手动实现一个系统时钟,既能跟操作系统同步,又不影响调用速度(也就是在调用的时候减少跟操作系统交互),这种需求的实现方式只能设定定时器,让定时器每隔一定的时间同步OS系统时间,既然 System.currentTimeMillis 的单位是毫秒,所以设置为每毫秒同步一次即可。

ScheduledExecutorService 是 java 的定时器线程实现方式,该线程执行服务有两个方法:schedule 和 scheduleAtFixedRate,schedule 和scheduleAtFixedRate 的区别在于,如果指定开始执行的时间在当前系统运行时间之前,scheduleAtFixedRate会把已经过去的时间也作为周期执行,而schedule不会把过去的时间算上。

schedule 和 scheduleAtFixedRate 区别:

(1) 2个参数的schedule在制定任务计划时, 如果指定的计划执行时间scheduledExecutionTime<= systemCurrentTime,则task会被立即执行。scheduledExecutionTime 不会因为某一个task的过度执行而改变。

(2) 3个参数的schedule在制定反复执行一个task的计划时,每一次执行这个task的计划执行时间随着前一次的实际执行时间而变,也就是 scheduledExecutionTime(第n+1次)=realExecutionTime(第n次)+periodTime。也就是说如果第n 次执行task时,由于某种原因这次执行时间过长,执行完后的systemCurrentTime>= scheduledExecutionTime(第n+1次),则此时不做时隔等待,立即执行第n+1次task,而接下来的第n+2次task的 scheduledExecutionTime(第n+2次)就随着变成了realExecutionTime(第n+1次)+periodTime。说 白了,这个方法更注重保持间隔时间的稳定。

(3)3个参数的scheduleAtFixedRate在制定反复执行一个task的计划时,每一次 执行这个task的计划执行时间在最初就被定下来了,也就是scheduledExecutionTime(第n次)=firstExecuteTime +n*periodTime;如果第n次执行task时,由于某种原因这次执行时间过长,执行完后的systemCurrentTime>= scheduledExecutionTime(第n+1次),则此时不做period间隔等待,立即执行第n+1次task,而接下来的第n+2次的 task的scheduledExecutionTime(第n+2次)依然还是firstExecuteTime+(n+2)*periodTime这在第一次执行task就定下来了。说白了,这个方法更注重保持执行频率的稳定。

public class SystemClock {
    private final long period;
    private final AtomicLong now;
    ExecutorService executor = Executors.newSingleThreadExecutor();

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

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

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

    private void scheduleClockUpdating() {
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
            @Override
            public Thread newThread(Runnable runnable) {
                Thread thread = new Thread(runnable, "System Clock");
                thread.setDaemon(true);
                return thread;
            }
        });
        scheduler.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                now.set(System.currentTimeMillis());
            }
        }, period, period, TimeUnit.MILLISECONDS);
    }

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

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

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

在减少系统的开销以及获取系统时间的的稳定性方面,采取单例模式,让应用程序只能在一个入口获取系统时间。众所周知,单例模式有懒汉式和饿汉式,懒汉式是延迟加载的,直到第一次调用的时候才会创建,如果不加synchronized则其不是线程安全的,如果加了则会阻塞影响性能;饿汉式是在类创建的同时就已经创建好一个静态的对象供系统使用,以后不在改变,是线程安全的。因此,我们这里选取了饿汉式单例,让其随着系统启动就开始创建好,调用时直接在静态实例中获取系统对象即可。

使用推特的雪花算法Twitter_Snowflake生产唯一序列码

Note: 下面该算法的代码需要使用上面优化的 SystemClock 在高并发中获取系统时间。

SnowFlake的结构如下(每部分用-分开):

0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000`

  • 1 位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是 0,负数是 1,所以id一般是正数,最高位是 0

  • 41 位时间截(毫秒级),注意,41 位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截开始时间截得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用 69 年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69

  • 10 位的数据机器位,可以部署在 1024 个节点,包括 5 位 datacenterId 和 5 位 workerId

  • 12 位序列,毫秒内的计数,12 位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生 4096 个ID序号

  • 加起来刚好 64 位,为一个 Long 型。

  • SnowFlake 的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生 ID 碰撞(由数据中心 ID 和机器 ID 作区分),并且效率较高,经测试,SnowFlake 每秒能够产生 26 万 ID 左右。

public class SnowflakeIdWorker {
    /**
     * 开始时间截 (2015-01-01)
     */
    private final long twepoch = 1420041600000L;

    /**
     * 机器id所占的位数
     */
    private final long workerIdBits = 5L;

    /**
     * 数据标识id所占的位数
     */
    private final long datacenterIdBits = 5L;

    /**
     * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
     */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /**
     * 支持的最大数据标识id,结果是31
     */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    /**
     * 序列在id中占的位数
     */
    private final long sequenceBits = 12L;

    /**
     * 机器ID向左移12位
     */
    private final long workerIdShift = sequenceBits;

    /**
     * 数据标识id向左移17位(12+5)
     */
    private final long datacenterIdShift = sequenceBits + workerIdBits;

    /**
     * 时间截向左移22位(5+5+12)
     */
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    /**
     * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
     */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /**
     * 工作机器ID(0~31)
     */
    private long workerId;

    /**
     * 数据中心ID(0~31)
     */
    private long datacenterId;

    /**
     * 毫秒内序列(0~4095)
     */
    private long sequence = 0L;

    /**
     * 上次生成ID的时间截
     */
    private long lastTimestamp = -1L;

    /**
     * 构造函数
     *
     * @param workerId     工作ID (0~31)
     * @param datacenterId 数据中心ID (0~31)
     */
    public SnowflakeIdWorker(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    /**
     * 获得下一个ID (该方法是线程安全的)
     *
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = timeGen();

        //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        //如果是同一时间生成的,则进行毫秒内序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒内序列溢出
            if (sequence == 0) {
                //阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //时间戳改变,毫秒内序列重置
        else {
            sequence = 0L;
        }

        //上次生成ID的时间截
        lastTimestamp = timestamp;

        //移位并通过或运算拼到一起组成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) //
                | (datacenterId << datacenterIdShift) //
                | (workerId << workerIdShift) //
                | sequence;
    }

    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     *
     * @param lastTimestamp 上次生成ID的时间截
     * @return 当前时间戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒为单位的当前时间
     *
     * @return 当前时间(毫秒)
     */
    protected long timeGen() {
        //return System.currentTimeMillis();
        return SystemClock.now();
    }
}

下面写一个测试方法:

/**
* 测试
 */
public static void main(String[] args) {
    long start = System.currentTimeMillis();
    SnowflakeIdWorker idWorker0 = new SnowflakeIdWorker(0, 0);
    for (int i = 0; i < 10000000; i++) {
        long id = idWorker0.nextId();
        //System.out.println(id);
    }
    System.out.println("耗时:" + (System.currentTimeMillis() - start));
}

测试结果:

......
596687817631858718
596687817631858719
596687817631858720
596687817631858721
596687817631858722
596687817631858723
596687817631858724
596687817631858725
596687817631858726
596687817631858727
596687817631858728
596687817631858729
596687817631858730
596687817631858731
596687817631858732
596687817631858733
596687817631858734
596687817631858735
596687817631858736
596687817631858737
596687817631858738
耗时:87815

如何将数据库和中间件的密码加密,在使用时自动解密

通过ApplicationListener实现初始化完成后的事件操作

如何优雅停机

Redis加锁和解锁的正确姿势

多数据源的自动切换

记录dubbo调用日志

Dubbo接口的验证器实现及全局异常拦截

异常CODE的封装

核心数据的压缩和解压缩

点击量:44

发表评论