在本文中,探讨将Java随机数算法优化为高吞吐量和低延迟的各种技巧。技巧包括更有效的对象分配,更有效的内存访问,消除不必要的间接访问以及机械同情。(对于分布式环境的抢拍很重要)

Java 7引入了,ThreadLocalRandom以在竞争激烈的环境中提高随机数生成的吞吐量。

背后的原理ThreadLocalRandom很简单:Random每个线程都维护自己的版本,而不是共享一个全局实例Random。反过来,这减少了争用,从而提高了吞吐量。

由于这是一个简单的想法,因此我们应该能够袖手旁观,并ThreadLocalRandom以类似的性能实现类似的功能,对吗?

让我们来看看。

第一次尝试

在我们的第一次尝试中,我们将使用简单的方法ThreadLocal:

// A few annotations

public class RandomBenchmark {

private final Random random = new Random();

private final ThreadLocal simpleThreadLocal = ThreadLocal.withInitial(Random::new);

@Benchmark

@BenchmarkMode(Throughput)

public int regularRandom() {

return random.nextInt();

}

@Benchmark

@BenchmarkMode(Throughput)

public int simpleThreadLocal() {

return simpleThreadLocal.get().nextInt();

}

@Benchmark

@BenchmarkMode(Throughput)

public int builtinThreadLocal() {

return ThreadLocalRandom.current().nextInt();

}

// omitted

}

在此基准测试中,我们正在比较Random,我们自己简单的ThreadLocal和内置的ThreadLocalRandom:

Benchmark                             Mode  Cnt           Score          Error  Units

RandomBenchmark.builtinThreadLocal   thrpt   40  1023676193.004 ± 26617584.814  ops/s

RandomBenchmark.regularRandom        thrpt   40     7487301.035 ±   244268.309  ops/s

RandomBenchmark.simpleThreadLocal    thrpt   40   382674281.696 ± 13197821.344  ops/s

ThreadLocalRandom生成每秒约1十亿随机数。

线性同余法

迄今为止,当今使用最广泛的随机数生成器是DH Lehmer在1949年推出的线性同余伪随机数生成器。

(具体算法见原文),Java实现:

protected int next(int bits) {

long oldseed, nextseed;

AtomicLong seed = this.seed;

do {

oldseed = seed.get();

nextseed = (oldseed * multiplier + addend) & mask;

} while (!seed.compareAndSet(oldseed, nextseed));

return (int)(nextseed >>> (48 - bits));

}

由于多个线程可以潜在地同时更新值seed,因此我们需要某种同步来协调并发访问。在这里,Java 在原子的帮助下使用了无锁方法。

基本上,每个线程都会尝试通过原子地将种子值更改为一个新值compareAndSet。如果线程无法执行此操作,它将重试相同的过程,直到可以成功提交更新。

当争用较高时,CAS失败的次数会增加。这是Random并发环境中性能低下的主要原因。

没有更多的CAS

在基于ThreadLocal的实现中,seed值限于每个线程。因此,由于没有共享的可变状态,因此我们不需要原子或任何其他形式的同步:

public class MyThreadLocalRandom extends Random {

// omitted

private static final ThreadLocal threadLocal =

ThreadLocal.withInitial(MyThreadLocalRandom::new);

private MyThreadLocalRandom() {}

public static MyThreadLocalRandom current() {

return threadLocal.get();

}

@Override

protected int next(int bits) {

seed = (seed * multiplier + addend) & mask;

return (int) (seed >>> (48 - bits));

}

}

如果我们再次运行相同的基准测试:

Benchmark Mode Cnt Score Error Units RandomBenchmark.builtinThreadLocal thrpt 40 1023676193.004 ± 26617584.814 ops/s RandomBenchmark.lockFreeThreadLocal thrpt 40 695217843.076 ± 17455041.160 ops/s RandomBenchmark.regularRandom thrpt 40 7487301.035 ± 244268.309 ops/s RandomBenchmark.simpleThreadLocal thrpt 40 382674281.696 ± 13197821.344 ops/s

MyThreadLocalRandom的吞吐量几乎是简单ThreadLocal的两倍。

在compareAndSet提供了原子和内存排序保证,我们只是在一个线程上下文限制也不需要。由于这些保证是昂贵且不必要的,因此删除保证会大大提高吞吐量。

但是,我们仍然落后于内置功能ThreadLocalRandom!

删除间接

事实证明,每个线程都不需要自己的单独且完整的副本Random。它只需要最新seed值即可。

从Java 8开始,这些值已添加到Thread类本身:

/** The current seed for a ThreadLocalRandom */@jdk.internal.vm.annotation.Contended("tlr")

long threadLocalRandomSeed;/** Probe hash value; nonzero if threadLocalRandomSeed initialized */@jdk.internal.vm.annotation.Contended("tlr")

int threadLocalRandomProbe;/** Secondary seed isolated from public ThreadLocalRandom sequence */@jdk.internal.vm.annotation.Contended("tlr")

int threadLocalRandomSecondarySeed;

MyThreadLocalRandom每个线程实例都在threadLocalRandomSeed变量中维护其当前值seed。结果,ThreadLocalRandom类成为单例:

/** The common ThreadLocalRandom */static final ThreadLocalRandom instance = new ThreadLocalRandom();

每次我们调用ThreadLocalRandom.current()的时候,它懒初始化threadLocalRandomSeed,然后返回singelton:

public static ThreadLocalRandom current() {

if (U.getInt(Thread.currentThread(), PROBE) == 0)

localInit();

return instance;

}

使用MyThreadLocalRandom,每次对current()factory方法的调用都会转换为ThreadLocal实例的哈希值计算和在基础哈希表中的查找。

相反,使用这种新的Java 8+方法,我们要做的就是直接读取threadLocalRandomSeed值,然后再对其进行更新。

高效的内存访问

为了更新种子值,java.util.concurrent.ThreadLocalRandom需要更改类中的threadLocalRandomSeed状态java.lang.Thread。如果我们设置为state public,那么每个人都可能更新threadLocalRandomSeed,这不是很好。

我们可以使用反射来更新非公开状态,但是仅仅因为我们可以,并不意味着我们应该!

事实证明,ThreadLocalRandom可以使用本机方法Unsafe.putLong有效地更新threadLocalRandomSeed状态:

/**

* The seed increment.

*/private static final long GAMMA = 0x9e3779b97f4a7c15L;

private static final Unsafe U = Unsafe.getUnsafe();

private static final long SEED = U.objectFieldOffset(Thread.class,"threadLocalRandomSeed");

final long nextSeed() {

Thread t;

long r;// read and update per-thread seedU.putLong(t = Thread.currentThread(), SEED, r = U.getLong(t, SEED) + GAMMA);

return r;

}

putLong方法将r值写入相对于当前线程的某个内存地址。内存偏移量已经通过调用另一个本机方法计算得出Unsafe.objectFieldOffset。

与反射相反,所有这些方法都具有本机实现,并且非常有效。

虚假共享False Sharing

CPU高速缓存根据高速缓存行进行工作。即,高速缓存行是CPU高速缓存和主存储器之间的传输单位。

基本上,处理器倾向于将一些其他值与请求的值一起缓存。这种空间局部性优化通常可以提高吞吐量和内存访问延迟。

但是,当两个或多个线程竞争同一条缓存行时,多线程可能会产生适得其反的效果。

为了更好地理解这一点,让我们假设以下变量位于同一缓存行中:

public class Thread implements Runnable {

private final long tid;

long threadLocalRandomSeed;

int threadLocalRandomProbe;

int threadLocalRandomSecondarySeed;

// omitted}

一些线程tid出于某些未知目的而使用or线程ID。现在,如果我们更新threadLocalRandomSeed线程中的值以生成随机数,那么应该不会发生什么不好的事情,对吗?听起来好像没什么大不了的,因为有些线程正在读取tid,而另一个线程则将整个线程写入另一个内存位置。

尽管我们可能会想,但由于所有这些值都在同一缓存行中,因此读取线程将遇到缓存未命中。编写器需要刷新其存储缓冲区。这种现象称为错误共享False Sharing,会给我们的多线程应用程序带来性能下降。

为了避免错误的共享问题,我们可以在有争议的值周围添加一些填充。这样,每个竞争激烈的值将驻留在自己的缓存行中:

public class Thread implements Runnable {

private final long tid;

private long p11, p12, p13, p14, p15, p16, p17 = 0; // one 64 bit long + 7 more => 64 Byteslong threadLocalRandomSeed;

private long p21, p22, p23, p24, p25, p26, p27 = 0;

int threadLocalRandomProbe;

private long p31, p32, p33, p34, p35, p36, p37 = 0;

int threadLocalRandomSecondarySeed;

private long p41, p42, p43, p44, p45, p46, p47 = 0;// omitted}

在大多数现代处理器中,缓存行大小通常为64或128字节。在我的机器上,它是64个字节,因此long在tid声明之后,我又添加了7个哑数值。

通常,这些threadLocal*变量将在同一线程中更新。因此,最好隔离一下tid:

public class Thread implements Runnable {

private final long tid;

private long p11, p12, p13, p14, p15, p16, p17 = 0;

long threadLocalRandomSeed;

int threadLocalRandomProbe;

int threadLocalRandomSecondarySeed;

// omitted}

读取器线程不会遇到高速缓存未命中的情况,而写入器则不需要立即清除其存储缓冲区,因为这些局部变量不是volatile。

竞争注释

jdk.internal.vm.annotation.Contended注解(如果你是在Java8则是sun.misc.Contended)是JVM隔离注释字段,以避免错误共享的提示。因此,以下内容应该更有意义:

/** The current seed for a ThreadLocalRandom */@jdk.internal.vm.annotation.Contended("tlr")

long threadLocalRandomSeed;/** Probe hash value; nonzero if threadLocalRandomSeed initialized */@jdk.internal.vm.annotation.Contended("tlr")

int threadLocalRandomProbe;/** Secondary seed isolated from public ThreadLocalRandom sequence */@jdk.internal.vm.annotation.Contended("tlr")

int threadLocalRandomSecondarySeed;

借助ContendedPaddingWidth调整标记,我们可以控制填充宽度。

threadLocalRandomSecondarySeed是ForkJoinPool或ConcurrentSkipListMap的内部使用的seed。另外,threadLocalRandomProbe表示当前线程是否已初始化其seed。

在本文中,我们探讨了将RNG优化为高吞吐量和低延迟的各种技巧。技巧包括更有效的对象分配,更有效的内存访问,消除不必要的间接访问以及机械同情。

java 提高随机数效率_抢拍神器的关键:优化提升Java线程局部随机数ThreadLocalRandom高并发技巧 - alidg...相关推荐

  1. java提高代码效率_提高java代码运行效率

    1.尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面 第一,控制资源的使用,通过线程同步来控制资 ...

  2. 推荐8个可以显著提高工作效率的办公神器

    分享8个可以显著提高工作效率的办公神器. 1.多译 多译是一款非常好用且高效的桌面端翻译工具,目前支持MacOS与Windows系统.使用多译,从文段翻译.截图翻译到字典查词,从自动识别.合并换行到自 ...

  3. 办公求生指南:分享10个可以提高办公效率的优质神器

    推荐10个小众好用的软件,可以解决大家很多需求. 1.火绒安全软件 火绒安全软件是一款轻巧高效免费的电脑防御及杀毒类安全软件,它可以显著增强电脑系统应对安全问题时的防御能力. 火绒安全软件能全面拦截查 ...

  4. 如何提高工作效率?这些神器可以实现

    干货预警!作为一个软件爱好者,小智给大家推荐7款windows端超赞的软件,可以解决大家很多问题,提升工作效率.收藏的同时记得点赞哦~ 1.火柴(原火萤酱) 火柴是一款PC端效率神器,可以快速打开文件 ...

  5. java byte 判断相等_转发收藏 | 史上最全Java面试题+面试网站推荐!(含答案)

    今天要谈的主题是关于求职,求职是在每个技术人员的生涯中都要经历多次.对于我们大部分人而言,在进入自己心仪的公司之前少不了准备工作,有一份全面细致面试题将帮助我们减少许多麻烦. 相关概念 面向对象的三个 ...

  6. 用java编写验证码程序_编写,验证和分析实时Java应用程序

    本文是" 用实时Java开发"系列的第三篇也是最后一部分,展示了如何设计,编写,验证和分析基本的实时应用程序. 我们将说明: 应用程序的时间和性能要求. 为什么传统的非实时Java ...

  7. 没有基础的人学java开发难吗_零基础的人怎么学习Java

    编程语言Java,已经21岁了.从1995年诞生以来,就一直活跃于企业中,名企应用天猫,百度,知乎......都是Java语言编写,就连现在使用广泛的XMind也是Java编写的.Java应用的广泛已 ...

  8. java的jmm模型_【深入理解JVM】:Java内存模型JMM

    多任务和高并发的内存交互 多任务和高并发是衡量一台计算机处理器的能力重要指标之一.一般衡量一个服务器性能的高低好坏,使用每秒事务处理数(Transactions Per Second,TPS)这个指标 ...

  9. java执行db2命令_送你一份P6级Java面试题

    导读: 作者:瞿云康,英文名jacksonKang,是一名努力成长中的Java爱好者.本文出处:http://mayiyk.cn/article/6本文为昨天Java面试题整理的第二篇,这个系列的文章 ...

  10. java职业发展路线图_从程序员到CTO的Java技术路线图 JAVA职业规划 JAVA职业发展路线图 系统后台框架图、前端工程师技能图 B2C电子商务基础系统架构解析...

    http://zz563143188.iteye.com/blog/1877266在技术方面无论我们怎么学习,总感觉需要提升自已不知道自己处于什么水平了.但如果有清晰的指示图供参考还是非常不错的,这样 ...

最新文章

  1. mysql 安全删除_mysql的binlog安全删除的一种方法
  2. [论文阅读][经典ICP] A Method For Registration Of 3D Shapes
  3. Centos7安装mongodb
  4. gson 不忽略空_仅在不为null或不为空的情况下,Gson序列化字段
  5. CVPR 2022NTIRE 2022|首个用于高光谱图像重建的 Transformer
  6. h264编解码器知识点
  7. 扇贝有道180924每日一句
  8. vue模块单独封装html,在vue中怎么定义自定义组件?
  9. ubuntu+vulkan
  10. android 自定义太阳,第一个AOSP安卓10自定义ROM已经可用,并且非常稳定
  11. [转] 全套汽车标志,好不容易找到的哦!
  12. 【语言模型系列】实践篇:ALBERT在房产领域的实践
  13. 我猜这将是程序员副业接单赚外快的最好的平台!
  14. POI DataValidation 删除数据有效性验证
  15. CTF学习-web解题思路
  16. CRM客户关系管理:赢得和留住客户的指南
  17. 生成yolo.h5的方法
  18. MySQL 设置 创建时间 和 更新时间
  19. java中一个有意思的字符串intern问题
  20. 互斥量(mutex)与事件(event)的使用

热门文章

  1. gae mysql_国内几大云服务引擎 BAE、SAE 与 GAE 优劣对比
  2. ifconfig eth0网卡配置
  3. 预测评价指标RMSE、MSE、MAE、MAPE、SMAPE
  4. 参考文献tool-mendeley_拔剑-浆糊的传说_新浪博客
  5. Android-弹窗AlterDialog对话框使用全解析
  6. ORACLE SQL 优化的若干方法(详细)
  7. 解决Spring文件下载时文件损毁问题
  8. windows之IP地址(一)
  9. 电脑怎么设置计算机系统,电脑定时开关机如何设置?
  10. 大数据和人工智能属于什么专业 - 学大数据和人工智能出来做什么