前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >给开源项目提了个PR

给开源项目提了个PR

作者头像
玛卡bug卡
发布2022-12-18 12:06:08
2300
发布2022-12-18 12:06:08
举报
文章被收录于专栏:Java后端修炼Java后端修炼

拖了很久的文章终于动笔了,两个月前提的PR现在才开始写总结文章,lazydog一只....

1.背景

首先介绍下 Sa-Token ,这是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。

优化源自其中的一个随机数获取逻辑实现,具体 PR 如图所示,该PR已被合并到主线代码中,在10月28日发布的 v1.32.0 中已经完成替换。

主要工作就是使用 ThreadLocalRadom 获取随机数去替换原来的 Random 获取,PR中也有给出具体的测试样例和数据来佐证更换之后的性能提升,但今天这篇文章还是想更全面地讲一下几种随机数获取逻辑的使用和性能差异。

2.随机数获取方式

1)多例Random

在 Java 中我们获取随机数自然而然会想到使用 Random 对象的 nextXX 方法去获取,这也是最简单的一种,例如可以这样:

代码语言:javascript
复制
public class SaUtils1 {
    public static String getRandomString(int length) {
        String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < length; i++) {
            int number = random.nextInt(62);
            sb.append(str.charAt(number));
        }
        return sb.toString();
    }
}

这种方式获取确实很简单,但还是存在一个可优化的点,我们可以发现这是一个静态方法获取随机数,每次进入到这个方法都会创建一个新的 Random 对象。虽然说方法结束后内存会被回收,但在访问量大的情况下,反复创建对象的开销也不能忽视,因此有了第二种方式获取随机数。

2)单例Random

单例获取实际上就是将 Random 对象升级为类成员,这样可以避免重复创建对象,例如:

代码语言:javascript
复制
public class SaUtils2 {
    private static final Random random = new Random();
    public static String getRandomString(int length) {
        String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < length; i++) {
            int number = random.nextInt(62);
            sb.append(str.charAt(number));
        }
        return sb.toString();
    }
}

显而易见地,在升级为类成员之后 Random 对象只需创建一次,在创建开销上明显是占优的,但具体的性能分析还要等待真正的测试。

3)单例ThreadLocalRandom

ThreadLocalRandom 是 JDK7 提供并发产生随机数的工具类,能够解决多线程下的争用问题,在使用上和Random略有不同:

代码语言:javascript
复制
public class SaUtils3 {
    private static final ThreadLocalRandom random = ThreadLocalRandom.current();
    public static String getRandomString(int length) {
        String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < length; i++) {
            int number = random.nextInt(62);
            sb.append(str.charAt(number));
        }
        return sb.toString();
    }
}

可以看到这里首先是用 ThreadLocalRandom 的静态方法 current 来获取到一个实例,接着再去调用 nextXX 方法实现随机数获取。

3.线程争用分析

1)Random 的线程争用问题

实际上我们使用单例 Random 对象在单线程下是没问题的,但在多线程下虽不会有线程安全问题但会发生线程争用,进而影响生成效率,那么线程争用主要体现在哪呢?

以 nextInt 为例,贴代码分析下:

可以看到里面调用了 next 方法,进而在 next 方法中是取旧的 seed 去做位运算,进而得到新的 seed 并计算出随机数,这里我们只需要知道这个过程就行了,位运算的具体操作不是本文重点。

这里我们也可以发现这个 seed 是 AtomicLong 类型的,意味着其可以保证原子操作,也就避免了线程安全问题的出现。同时,意味着在高并发场景下会有较多的 CAS 失败操作,不断自旋重试,进而造成 CPU 处理效率降低且吞吐量下降。

2)ThreadLocalRandom 的解决方案

接下来我们看看 ThreadLocalRandom 在源码层面有什么区别。

首先进入 current 方法,可以发现这里是调用了 UNSAFE 来保证单例,如果还没有创建过就进入到初始化方法,我们跟进去看看。

这里需要说明的是,PROBE 实际上是一个状态变量,如果为0说明还没初始化,如果为1说明已经初始化完成,而 SEED 很明显就是种子变量了,这两个变量都是通过UNSAFE中的方法给设置到线程中去,那么我们去 Thread 中找一下。

可以发现这两个变量在 Thread 类中是有体现的,也就是说实际上种子是存储在 Thread 中的,与线程强相关的。

从这里我们可以发现在单例下种子实际上是通过存储在 Thread 中来实现线程隔离的。

接下来看看 nextInt 方法,可以发现这里除了生成随机数,还对 seed 进行更新,同样是使用 UNSAFE 去更新种子。

其他更细节的操作就不分析了,到这里我们可以得出结论:ThreadLocalRandom 减少线程争用的操作就是将 seed 与线程绑定起来,每个线程有自己的 seed 这样即使在多线程环境下,seed 的更新也不会造成频繁 CAS 导致吞吐量降低。

4.性能分析

在分析了源码层面的区别之后,还应该从实际的使用出发,测试下性能的表现。

1)单线程测试

代码语言:javascript
复制
public class RandomTest {
    static int times = 100000;
    public static void main(String[] args) throws InterruptedException {
        singleThread();
    }
    public static void singleThread() {
        long start = System.currentTimeMillis();
        for (int i = 0; i < times; i++) {
            SaUtils.getRandomString(64);
        }
        long end = System.currentTimeMillis();
        System.out.println("single thread-cost time : " + (end - start) + "ms");

        long start1 = System.currentTimeMillis();
        for (int i = 0; i < times; i++) {
            SaUtils2.getRandomString(64);
        }
        long end1 = System.currentTimeMillis();
        System.out.println("single thread-cost time : " + (end1 - start1) + "ms");

        long start2 = System.currentTimeMillis();
        for (int i = 0; i < times; i++) {
            SaUtils3.getRandomString(64);
        }
        long end2 = System.currentTimeMillis();
        System.out.println("single thread-cost time : " + (end2 - start2) + "ms");

    }
}

这里简单地做一个单线程的测试,其中涉及到的生成随机数的类都在上面第二点中有列出,这里不再重复。

这里做了10w次的获取操作,打印出耗时具体如下,可能稍有误差,不过大致可以体现出差异。

可以发现多例 Random 确实有一些性能损耗,而单例 ThreadLocalRandom 的耗时是最短的。

2)多线程测试

代码语言:javascript
复制
public class RandomTest {
    static int times = 100000;
    static int threadNum = 10;
    public static void main(String[] args) throws InterruptedException {
        poolThread();
    }
    public static void poolThread() throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(threadNum);
        CountDownLatch cdl = new CountDownLatch(times);
        long start1 = System.currentTimeMillis();
        for (int i = 0; i < times; i++) {
            pool.execute(() -> {
                SaUtils.getRandomString(64);
                cdl.countDown();
            });
        }
        cdl.await();
        long end1 = System.currentTimeMillis();
        System.out.println("cost time : " + (end1 - start1) + "ms");
        pool.shutdown();

        pool = Executors.newFixedThreadPool(threadNum);
        CountDownLatch cdl2 = new CountDownLatch(times);
        long start2 = System.currentTimeMillis();
        for (int i = 0; i < times; i++) {
            pool.execute(() -> {
                SaUtils2.getRandomString(64);
                cdl2.countDown();
            });
        }
        cdl2.await();
        long end2 = System.currentTimeMillis();
        System.out.println("cost time : " + (end2 - start2) + "ms");
        pool.shutdown();

        pool = Executors.newFixedThreadPool(threadNum);
        CountDownLatch cdl3 = new CountDownLatch(times);
        long start3 = System.currentTimeMillis();
        for (int i = 0; i < times; i++) {
            pool.execute(() -> {
                SaUtils3.getRandomString(64);
                cdl3.countDown();
            });
        }
        cdl3.await();
        long end3 = System.currentTimeMillis();
        System.out.println("cost time : " + (end3 - start3) + "ms");
        pool.shutdown();

    }
}

这里测试了10个线程同时获取的情况,同样是获取10w次,具体的数据如下:

从结果可以看出,在多线程环境下,由于存在线程争用,单例的 Random 表现最不理想,耗时几乎是多例 Random 的四倍,这也印证了我们前面的分析。

相比之下,单例 ThreadLocalRandom 表现依旧是最佳的,耗时上大概是多例 Random 的 40%,由于机器差异可能数据有所不同,不过大致上是这个量级。

5.总结

基于上面的理论分析和性能分析,我们可以发现不管是单线程还是多线程环境下,ThreadLocalRandom 的表现都是比较占优的,特别是在高并发的情况下,使用这种随机数获取方式可以带来一定的性能提升。

回到PR本身,Sa-Token 是一个认证鉴权框架,其 token 的生成默认基于随机数生成,同时 token 的获取会贯穿整个认证鉴权过程,那么随机数的获取效率在这里就显得至关重要了,也是基于框架的特点,才提出了这个修改建议,同时也很高兴这个修改建议在经过官方团队审核之后得以采纳。

其实回看整个分析过程,并没有涉及到很高深的技术点,这也启发自己积极去探索更多的应用场景,找到更合适的解决方案,为开源社区贡献自己微不足道的力量。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-11-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java后端修炼 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档