- 浏览: 393669 次
- 性别:
- 来自: 深圳
-
文章分类
最新评论
-
Nabulio:
写的详细,特殊语法学习到了
jdk1.5-1.9新特性 -
wooddawn:
您好,最近在做个足球数据库系统,用到了betbrain的数据表 ...
javascript深入理解js闭包 -
lwpan:
很受启发 update也可以
mysql 的delete from 子查询限制 -
wuliaolll:
不错,总算找到原因了
mysql 的delete from 子查询限制
这个故事源自一个很简单的想法:创建一个对开发人员友好的、简单轻量的线程间通讯框架,完全不用锁、同步器、信号量、等待和通知,在Java里开发一个轻量、无锁的线程内通讯框架;并且也没有队列、消息、事件或任何其他并发专用的术语或工具。
只用普通的老式Java接口实现POJO的通讯。
它可能跟Akka的类型化actor类似,但作为一个必须超级轻量,并且要针对单台多核计算机进行优化的新框架,那个可能有点过了。
相关厂商内容
高德地图街景API发布,开放全部街景数据及功能(FREE)
《婚恋交友中的推荐系统应用》——世纪佳缘研发中心总监吴金龙确认QCon分享
《你应该更新的Java知识》——JavaOne Duke大奖得主郑晔北京QCon分享话题确认
2013~2014年度InfoQ读者深度调查火热进行中
《PM2.5的大数据分析》—— 英特尔中国研究院首席架构师姜小凡确认QCon分享
当actor跨越不同JVM实例(在同一台机器上,或分布在网络上的不同机器上)的进程边界时,Akka框架很善于处理进程间的通讯。
但对于那种只需要线程间通讯的小型项目而言,用Akka类型化actor可能有点儿像用牛刀杀鸡,不过类型化actor仍然是一种理想的实现方式。
我花了几天时间,用动态代理,阻塞队列和缓存线程池创建了一个解决方案。
图一是这个框架的高层次架构:
图一: 框架的高层次架构
SPSC队列是指单一生产者/单一消费者队列。MPSC队列是指多生产者/单一消费者队列。
派发线程负责接收Actor线程发送的消息,并把它们派发到对应的SPSC队列中去。
接收到消息的Actor线程用其中的数据调用相应的actor实例中的方法。借助其他actor的代理,actor实例可以将消息发送到MPSC队列中,然后消息会被发送给目标actor线程。
我创建了一个简单的例子来测试,就是下面这个打乒乓球的程序:
public interface PlayerA (
void pong(long ball); //发完就忘的方法调用
}
public interface PlayerB {
void ping(PlayerA playerA, long ball); //发完就忘的方法调用
}
public class PlayerAImpl implements PlayerA {
@Override
public void pong(long ball) {
}
}
public class PlayerBImpl implements PlayerB {
@Override
public void ping(PlayerA playerA, long ball) {
playerA.pong(ball);
}
}
public class PingPongExample {
public void testPingPong() {
// 管理器隐藏了线程间通讯的复杂性
// 控制actor代理,actor实现和线程
ActorManager manager = new ActorManager();
// 在管理器内注册actor实现
manager.registerImpl(PlayerAImpl.class);
manager.registerImpl(PlayerBImpl.class);
//创建actor代理。代理会将方法调用转换成内部消息。
//会在线程间发给特定的actor实例。
PlayerA playerA = manager.createActor(PlayerA.class);
PlayerB playerB = manager.createActor(PlayerB.class);
for(int i = 0; i < 1000000; i++) {
playerB.ping(playerA, i);
}
}
经过测试,速度大约在每秒500,000 次乒/乓左右;还不错吧。然而跟单线程的运行速度比起来,我突然就感觉没那么好了。在 单线程 中运行的代码每秒速度能达到20亿 (2,681,850,373)!
居然差了5,000 多倍。太让我失望了。在大多数情况下,单线程代码的效果都比多线程代码更高效。
我开始找原因,想看看我的乒乓球运动员们为什么这么慢。经过一番调研和测试,我发现是阻塞队列的问题,我用来在actor间传递消息的队列影响了性能。
图 2: 只有一个生产者和一个消费者的SPSC队列
所以我发起了一场竞赛,要将它换成Java里最快的队列。我发现了Nitsan Wakart的 博客 。他发了几篇文章介绍单一生产者/单一消费者(SPSC)无锁队列的实现。这些文章受到了Martin Thompson的演讲 终极性能的无锁算法的启发。
跟基于私有锁的队列相比,无锁队列的性能更优。在基于锁的队列中,当一个线程得到锁时,其它线程就要等着锁被释放。而在无锁的算法中,某个生产者线程生产消息时不会阻塞其它生产者线程,消费者也不会被其它读取队列的消费者阻塞。
在Martin Thompson的演讲以及在Nitsan的博客中介绍的SPSC队列的性能简直令人难以置信—— 超过了100M ops/sec。比JDK的并发队列实现还要快10倍 (在4核的 Intel Core i7 上的性能大约在 8M ops/sec 左右)。
我怀着极大的期望,将所有actor上连接的链式阻塞队列都换成了无锁的SPSC队列。可惜,在吞吐量上的性能测试并没有像我预期的那样出现大幅提升。不过很快我就意识到,瓶颈并不在SPSC队列上,而是在多个生产者/单一消费者(MPSC)那里。
用SPSC队列做MPSC队列的任务并不那么简单;在做put操作时,多个生产者可能会覆盖掉彼此的值。SPSC 队列就没有控制多个生产者put操作的代码。所以即便换成最快的SPSC队列,也解决不了我的问题。
为了处理多个生产者/单一消费者的情况,我决定启用LMAX Disruptor ——一个基于环形缓冲区的高性能进程间消息库。
图3: 单一生产者和单一消费者的LMAX Disruptor
借助Disruptor,很容易实现低延迟、高吞吐量的线程间消息通讯。它还为生产者和消费者的不同组合提供了不同的用例。几个线程可以互不阻塞地读取环形缓冲中的消息:
图 4: 单一生产者和两个消费者的LMAX Disruptor
下面是有多个生产者写入环形缓冲区,多个消费者从中读取消息的场景。
图 5: 两个生产者和两个消费者的LMAX Disruptor
经过对性能测试的快速搜索,我找到了 三个发布者和一个消费者的吞吐量测试。 这个真是正合我意,它给出了下面这个结果:
LinkedBlockingQueue
Disruptor
Run 0
4,550,625 ops/sec
11,487,650 ops/sec
Run 1
4,651,162 ops/sec
11,049,723 ops/sec
Run 2
4,404,316 ops/sec
11,142,061 ops/sec
在3 个生产者/1个 消费者场景下, Disruptor要比LinkedBlockingQueue快两倍多。然而这跟我所期望的性能上提升10倍仍有很大差距。
这让我觉得很沮丧,并且我的大脑一直在搜寻解决方案。就像命中注定一样,我最近不在跟人拼车上下班,而是改乘地铁了。突然灵光一闪,我的大脑开始将车站跟生产者消费者对应起来。在一个车站里,既有生产者(车和下车的人),也有消费者(同一辆车和上车的人)。
我创建了 Railway类,并用AtomicLong追踪从一站到下一站的列车。我先从简单的场景开始,只有一辆车的铁轨。
public class RailWay {
private final Train train = new Train();
// stationNo追踪列车并定义哪个车站接收到了列车
private final AtomicInteger stationIndex = new AtomicInteger();
// 会有多个线程访问这个方法,并等待特定车站上的列车
public Train waitTrainOnStation(final int stationNo) {
while (stationIndex.get() % stationCount != stationNo) {
Thread.yield(); // 为保证高吞吐量的消息传递,这个是必须的。
//但在等待列车时它会消耗CPU周期
}
// 只有站号等于stationIndex.get() % stationCount时,这个忙循环才会返回
return train;
}
// 这个方法通过增加列车的站点索引将这辆列车移到下一站
public void sendTrain() {
stationIndex.getAndIncrement();
}
}
为了测试,我用的条件跟在Disruptor性能测试中用的一样,并且也是测的SPSC队列——测试在线程间传递long值。我创建了下面这个Train类,其中包含了一个long数组:
public class Train {
//
public static int CAPACITY = 2*1024;
private final long[] goodsArray; // 传输运输货物的数组
private int index;
public Train() {
goodsArray = new long[CAPACITY];
}
public int goodsCount() { //返回货物数量
return index;
}
public void addGoods(long i) { // 向列车中添加条目
goodsArray[index++] = i;
}
public long getGoods(int i) { //从列车中移走条目
index--;
return goodsArray[i];
}
}
然后我写了一个简单的测试 :两个线程通过列车互相传递long值。
图 6: 使用单辆列车的单一生产者和单一消费者Railway
public void testRailWay() {
final Railway railway = new Railway();
final long n = 20000000000l;
//启动一个消费者进程
new Thread() {
long lastValue = 0;
@Override
public void run() {
while (lastValue < n) {
Train train = railway.waitTrainOnStation(1); //在#1站等列车
int count = train.goodsCount();
for (int i = 0; i < count; i++) {
lastValue = train.getGoods(i); // 卸货
}
railway.sendTrain(); //将当前列车送到第一站
}
}
}.start();
final long start = System.nanoTime();
long i = 0;
while (i < n) {
Train train = railway.waitTrainOnStation(0); // 在#0站等列车
int capacity = train.getCapacity();
for (int j = 0; j < capacity; j++) {
train.addGoods((int)i++); // 将货物装到列车上
}
railway.sendTrain();
if (i % 100000000 == 0) { //每隔100M个条目测量一次性能
final long duration = System.nanoTime() - start;
final long ops = (i * 1000L * 1000L * 1000L) / duration;
System.out.format("ops/sec = %,d\n", ops);
System.out.format("trains/sec = %,d\n", ops / Train.CAPACITY);
System.out.format("latency nanos = %.3f%n\n",
duration / (float)(i) * (float)Train.CAPACITY);
}
}
}
在不同的列车容量下运行这个测试,结果惊着我了:
容量
吞吐量: ops/sec
延迟: ns
1
5,190,883
192.6
2
10,282,820
194.5
32
104,878,614
305.1
256
344,614,640
742. 9
2048
608,112,493
3,367.8
32768
767,028,751
42,720.7
在列车容量达到32,768时,两个线程传送消息的吞吐量达到了767,028,751 ops/sec。比Nitsan博客中的SPSC队列快了几倍。
继续按铁路列车这个思路思考,我想知道如果有两辆列车会怎么样?我觉得应该能提高吞吐量,同时还能降低延迟。每个车站都会有它自己的列车。当一辆列车在第一个车站装货时,第二辆列车会在第二个车站卸货,反之亦然。
图 7: 使用两辆列车的单一生产者和单一消费者Railway
下面是吞吐量的结果:
容量
吞吐量: ops/sec
延时: ns
1
7,492,684
133.5
2
14,754,786
135.5
32
174,227,656
183.7
256
613,555,475
417.2
2048
940,144,900
2,178.4
32768
797,806,764
41,072.6
结果是惊人的;比单辆列车的结果快了1.4倍多。列车容量为一时,延迟从192.6纳秒降低到133.5纳秒;这显然是一个令人鼓舞的迹象。
因此我的实验还没结束。列车容量为2048的两个线程传递消息的延迟为2,178.4 纳秒,这太高了。我在想如何降低它,创建一个有很多辆列车 的例子:
图 8: 使用多辆列车的单一生产者和单一消费者Railway
我还把列车容量降到了1个long值,开始玩起了列车数量。下面是测试结果:
列车数量
吞吐量: ops/sec
延迟: ns
2
10,917,951
91.6
32
31,233,310
32.0
256
42,791,962
23.4
1024
53,220,057
18.8
32768
71,812,166
13.9
用32,768 列车在线程间发送一个long值的延迟降低到了13.9 纳秒。通过调整列车数量和列车容量,当延时不那么高,吞吐量不那么低时,吞吐量和延时就达到了最佳平衡。
对于单一生产者和单一消费者(SPSC)而言,这些数值很棒;但我们怎么让它在有多个生产者和消费者时也能生效呢?答案很简单,添加更多的车站!
图 9:一个生产者和两个消费者的Railway
每个线程都等着下一趟列车,装货/卸货,然后把列车送到下一站。在生产者往列车上装货时,消费者在从列车上卸货。列车周而复始地从一个车站转到另一个车站。
为了测试单一生产者/多消费者(SPMC) 的情况,我创建了一个有8个车站的Railway测试。 一个车站属于一个生产者,而另外7个车站属于消费者。结果是:
列车数量 = 256 ,列车容量 = 32:
ops/sec = 116,604,397 延迟(纳秒) = 274.4
列车数量= 32,列车容量= 256:
ops/sec = 432,055,469 延迟(纳秒) = 592.5
如你所见,即便有8个工作线程,测试给出的结果也相当好-- 32辆容量为256个long的列车吞吐量为432,055,469 ops/sec。在测试期间,所有CPU内核的负载都是100%。
图 10:在测试有8个车站的Railway 期间的CPU 使用情况
在玩这个Railway算法时,我几乎忘了我最初的目标:提升多生产者/单消费者情况下的性能。
图 11:三个生产者和一个消费者的 Railway
我创建了3个生产者和1个消费者的新测试。每辆列车一站一站地转圈,而每个生产者只给每辆车装1/3容量的货。消费者取出每辆车上三个生产者给出的全部三项货物。性能测试给出的平均结果如下所示:
ops/sec = 162,597,109 列车/秒 = 54,199,036 延迟(纳秒) = 18.5
结果相当棒。生产者和消费者工作的速度超过了160M ops/sec。
为了填补差异,下面给出相同情况下的Disruptor结果- 3个生产者和1个消费者:
Run 0, Disruptor=11,467,889 ops/sec
Run 1, Disruptor=11,280,315 ops/sec
Run 2, Disruptor=11,286,681 ops/sec
Run 3, Disruptor=11,254,924 ops/sec
下面是另一个批量消息的Disruptor 3P:1C 测试 (10 条消息每批):
Run 0, Disruptor=116,009,280 ops/sec
Run 1, Disruptor=128,205,128 ops/sec
Run 2, Disruptor=101,317,122 ops/sec
Run 3, Disruptor=98,716,683 ops/sec;
最后是用带LinkedBlockingQueue 实现的Disruptor 在3P:1C场景下的测试结果:
Run 0, BlockingQueue=4,546,281 ops/sec
Run 1, BlockingQueue=4,508,769 ops/sec
Run 2, BlockingQueue=4,101,386 ops/sec
Run 3, BlockingQueue=4,124,561 ops/sec
如你所见,Railway方式的平均吞吐量是162,597,109 ops/sec,而Disruptor在同样的情况下的最好结果只有128,205,128 ops/sec。至于 LinkedBlockingQueue,最好的结果只有4,546,281 ops/sec。
Railway算法为事件批处理提供了一种可以显著增加吞吐量的简易办法。通过调整列车容量或列车数量,很容易达成想要的吞吐量/延迟。
另外, 当同一个线程可以用来消费消息,处理它们并向环中返回结果时,通过混合生产者和消费者,Railway也能用来处理复杂的情况:
图 12: 混合生产者和消费者的Railway
最后,我会提供一个经过优化的超高吞吐量 单生产者/单消费者测试:
图 13:单个生产者和单个消费者的Railway
它的平均结果为:吞吐量超过每秒15亿 (1,569,884,271)次操作,延迟为1.3 微秒。如你所见,本文开头描述的那个规模相同的单线程测试的结果是每秒2,681,850,373。
你自己想想结论是什么吧。
我希望将来再写一篇文章,阐明如何用Queue和 BlockingQueue接口支持Railway算法,用来处理不同的生产者和消费者组合。敬请关注。
关于作者
Aliaksei Papou 是Specific集团的首席软件工程师和架构师,那是一家位于奥地利维也纳的软件开发公司。Aliaksei有超过10年的小型和大型企业应用软件开发经验。他有一个坚定的信念:编写并发代码不应该这么难。
原文英文链接:Inter-thread communications in Java at the speed of light
感谢侯伯薇对本文的审校。
给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。
只用普通的老式Java接口实现POJO的通讯。
它可能跟Akka的类型化actor类似,但作为一个必须超级轻量,并且要针对单台多核计算机进行优化的新框架,那个可能有点过了。
相关厂商内容
高德地图街景API发布,开放全部街景数据及功能(FREE)
《婚恋交友中的推荐系统应用》——世纪佳缘研发中心总监吴金龙确认QCon分享
《你应该更新的Java知识》——JavaOne Duke大奖得主郑晔北京QCon分享话题确认
2013~2014年度InfoQ读者深度调查火热进行中
《PM2.5的大数据分析》—— 英特尔中国研究院首席架构师姜小凡确认QCon分享
当actor跨越不同JVM实例(在同一台机器上,或分布在网络上的不同机器上)的进程边界时,Akka框架很善于处理进程间的通讯。
但对于那种只需要线程间通讯的小型项目而言,用Akka类型化actor可能有点儿像用牛刀杀鸡,不过类型化actor仍然是一种理想的实现方式。
我花了几天时间,用动态代理,阻塞队列和缓存线程池创建了一个解决方案。
图一是这个框架的高层次架构:
图一: 框架的高层次架构
SPSC队列是指单一生产者/单一消费者队列。MPSC队列是指多生产者/单一消费者队列。
派发线程负责接收Actor线程发送的消息,并把它们派发到对应的SPSC队列中去。
接收到消息的Actor线程用其中的数据调用相应的actor实例中的方法。借助其他actor的代理,actor实例可以将消息发送到MPSC队列中,然后消息会被发送给目标actor线程。
我创建了一个简单的例子来测试,就是下面这个打乒乓球的程序:
public interface PlayerA (
void pong(long ball); //发完就忘的方法调用
}
public interface PlayerB {
void ping(PlayerA playerA, long ball); //发完就忘的方法调用
}
public class PlayerAImpl implements PlayerA {
@Override
public void pong(long ball) {
}
}
public class PlayerBImpl implements PlayerB {
@Override
public void ping(PlayerA playerA, long ball) {
playerA.pong(ball);
}
}
public class PingPongExample {
public void testPingPong() {
// 管理器隐藏了线程间通讯的复杂性
// 控制actor代理,actor实现和线程
ActorManager manager = new ActorManager();
// 在管理器内注册actor实现
manager.registerImpl(PlayerAImpl.class);
manager.registerImpl(PlayerBImpl.class);
//创建actor代理。代理会将方法调用转换成内部消息。
//会在线程间发给特定的actor实例。
PlayerA playerA = manager.createActor(PlayerA.class);
PlayerB playerB = manager.createActor(PlayerB.class);
for(int i = 0; i < 1000000; i++) {
playerB.ping(playerA, i);
}
}
经过测试,速度大约在每秒500,000 次乒/乓左右;还不错吧。然而跟单线程的运行速度比起来,我突然就感觉没那么好了。在 单线程 中运行的代码每秒速度能达到20亿 (2,681,850,373)!
居然差了5,000 多倍。太让我失望了。在大多数情况下,单线程代码的效果都比多线程代码更高效。
我开始找原因,想看看我的乒乓球运动员们为什么这么慢。经过一番调研和测试,我发现是阻塞队列的问题,我用来在actor间传递消息的队列影响了性能。
图 2: 只有一个生产者和一个消费者的SPSC队列
所以我发起了一场竞赛,要将它换成Java里最快的队列。我发现了Nitsan Wakart的 博客 。他发了几篇文章介绍单一生产者/单一消费者(SPSC)无锁队列的实现。这些文章受到了Martin Thompson的演讲 终极性能的无锁算法的启发。
跟基于私有锁的队列相比,无锁队列的性能更优。在基于锁的队列中,当一个线程得到锁时,其它线程就要等着锁被释放。而在无锁的算法中,某个生产者线程生产消息时不会阻塞其它生产者线程,消费者也不会被其它读取队列的消费者阻塞。
在Martin Thompson的演讲以及在Nitsan的博客中介绍的SPSC队列的性能简直令人难以置信—— 超过了100M ops/sec。比JDK的并发队列实现还要快10倍 (在4核的 Intel Core i7 上的性能大约在 8M ops/sec 左右)。
我怀着极大的期望,将所有actor上连接的链式阻塞队列都换成了无锁的SPSC队列。可惜,在吞吐量上的性能测试并没有像我预期的那样出现大幅提升。不过很快我就意识到,瓶颈并不在SPSC队列上,而是在多个生产者/单一消费者(MPSC)那里。
用SPSC队列做MPSC队列的任务并不那么简单;在做put操作时,多个生产者可能会覆盖掉彼此的值。SPSC 队列就没有控制多个生产者put操作的代码。所以即便换成最快的SPSC队列,也解决不了我的问题。
为了处理多个生产者/单一消费者的情况,我决定启用LMAX Disruptor ——一个基于环形缓冲区的高性能进程间消息库。
图3: 单一生产者和单一消费者的LMAX Disruptor
借助Disruptor,很容易实现低延迟、高吞吐量的线程间消息通讯。它还为生产者和消费者的不同组合提供了不同的用例。几个线程可以互不阻塞地读取环形缓冲中的消息:
图 4: 单一生产者和两个消费者的LMAX Disruptor
下面是有多个生产者写入环形缓冲区,多个消费者从中读取消息的场景。
图 5: 两个生产者和两个消费者的LMAX Disruptor
经过对性能测试的快速搜索,我找到了 三个发布者和一个消费者的吞吐量测试。 这个真是正合我意,它给出了下面这个结果:
LinkedBlockingQueue
Disruptor
Run 0
4,550,625 ops/sec
11,487,650 ops/sec
Run 1
4,651,162 ops/sec
11,049,723 ops/sec
Run 2
4,404,316 ops/sec
11,142,061 ops/sec
在3 个生产者/1个 消费者场景下, Disruptor要比LinkedBlockingQueue快两倍多。然而这跟我所期望的性能上提升10倍仍有很大差距。
这让我觉得很沮丧,并且我的大脑一直在搜寻解决方案。就像命中注定一样,我最近不在跟人拼车上下班,而是改乘地铁了。突然灵光一闪,我的大脑开始将车站跟生产者消费者对应起来。在一个车站里,既有生产者(车和下车的人),也有消费者(同一辆车和上车的人)。
我创建了 Railway类,并用AtomicLong追踪从一站到下一站的列车。我先从简单的场景开始,只有一辆车的铁轨。
public class RailWay {
private final Train train = new Train();
// stationNo追踪列车并定义哪个车站接收到了列车
private final AtomicInteger stationIndex = new AtomicInteger();
// 会有多个线程访问这个方法,并等待特定车站上的列车
public Train waitTrainOnStation(final int stationNo) {
while (stationIndex.get() % stationCount != stationNo) {
Thread.yield(); // 为保证高吞吐量的消息传递,这个是必须的。
//但在等待列车时它会消耗CPU周期
}
// 只有站号等于stationIndex.get() % stationCount时,这个忙循环才会返回
return train;
}
// 这个方法通过增加列车的站点索引将这辆列车移到下一站
public void sendTrain() {
stationIndex.getAndIncrement();
}
}
为了测试,我用的条件跟在Disruptor性能测试中用的一样,并且也是测的SPSC队列——测试在线程间传递long值。我创建了下面这个Train类,其中包含了一个long数组:
public class Train {
//
public static int CAPACITY = 2*1024;
private final long[] goodsArray; // 传输运输货物的数组
private int index;
public Train() {
goodsArray = new long[CAPACITY];
}
public int goodsCount() { //返回货物数量
return index;
}
public void addGoods(long i) { // 向列车中添加条目
goodsArray[index++] = i;
}
public long getGoods(int i) { //从列车中移走条目
index--;
return goodsArray[i];
}
}
然后我写了一个简单的测试 :两个线程通过列车互相传递long值。
图 6: 使用单辆列车的单一生产者和单一消费者Railway
public void testRailWay() {
final Railway railway = new Railway();
final long n = 20000000000l;
//启动一个消费者进程
new Thread() {
long lastValue = 0;
@Override
public void run() {
while (lastValue < n) {
Train train = railway.waitTrainOnStation(1); //在#1站等列车
int count = train.goodsCount();
for (int i = 0; i < count; i++) {
lastValue = train.getGoods(i); // 卸货
}
railway.sendTrain(); //将当前列车送到第一站
}
}
}.start();
final long start = System.nanoTime();
long i = 0;
while (i < n) {
Train train = railway.waitTrainOnStation(0); // 在#0站等列车
int capacity = train.getCapacity();
for (int j = 0; j < capacity; j++) {
train.addGoods((int)i++); // 将货物装到列车上
}
railway.sendTrain();
if (i % 100000000 == 0) { //每隔100M个条目测量一次性能
final long duration = System.nanoTime() - start;
final long ops = (i * 1000L * 1000L * 1000L) / duration;
System.out.format("ops/sec = %,d\n", ops);
System.out.format("trains/sec = %,d\n", ops / Train.CAPACITY);
System.out.format("latency nanos = %.3f%n\n",
duration / (float)(i) * (float)Train.CAPACITY);
}
}
}
在不同的列车容量下运行这个测试,结果惊着我了:
容量
吞吐量: ops/sec
延迟: ns
1
5,190,883
192.6
2
10,282,820
194.5
32
104,878,614
305.1
256
344,614,640
742. 9
2048
608,112,493
3,367.8
32768
767,028,751
42,720.7
在列车容量达到32,768时,两个线程传送消息的吞吐量达到了767,028,751 ops/sec。比Nitsan博客中的SPSC队列快了几倍。
继续按铁路列车这个思路思考,我想知道如果有两辆列车会怎么样?我觉得应该能提高吞吐量,同时还能降低延迟。每个车站都会有它自己的列车。当一辆列车在第一个车站装货时,第二辆列车会在第二个车站卸货,反之亦然。
图 7: 使用两辆列车的单一生产者和单一消费者Railway
下面是吞吐量的结果:
容量
吞吐量: ops/sec
延时: ns
1
7,492,684
133.5
2
14,754,786
135.5
32
174,227,656
183.7
256
613,555,475
417.2
2048
940,144,900
2,178.4
32768
797,806,764
41,072.6
结果是惊人的;比单辆列车的结果快了1.4倍多。列车容量为一时,延迟从192.6纳秒降低到133.5纳秒;这显然是一个令人鼓舞的迹象。
因此我的实验还没结束。列车容量为2048的两个线程传递消息的延迟为2,178.4 纳秒,这太高了。我在想如何降低它,创建一个有很多辆列车 的例子:
图 8: 使用多辆列车的单一生产者和单一消费者Railway
我还把列车容量降到了1个long值,开始玩起了列车数量。下面是测试结果:
列车数量
吞吐量: ops/sec
延迟: ns
2
10,917,951
91.6
32
31,233,310
32.0
256
42,791,962
23.4
1024
53,220,057
18.8
32768
71,812,166
13.9
用32,768 列车在线程间发送一个long值的延迟降低到了13.9 纳秒。通过调整列车数量和列车容量,当延时不那么高,吞吐量不那么低时,吞吐量和延时就达到了最佳平衡。
对于单一生产者和单一消费者(SPSC)而言,这些数值很棒;但我们怎么让它在有多个生产者和消费者时也能生效呢?答案很简单,添加更多的车站!
图 9:一个生产者和两个消费者的Railway
每个线程都等着下一趟列车,装货/卸货,然后把列车送到下一站。在生产者往列车上装货时,消费者在从列车上卸货。列车周而复始地从一个车站转到另一个车站。
为了测试单一生产者/多消费者(SPMC) 的情况,我创建了一个有8个车站的Railway测试。 一个车站属于一个生产者,而另外7个车站属于消费者。结果是:
列车数量 = 256 ,列车容量 = 32:
ops/sec = 116,604,397 延迟(纳秒) = 274.4
列车数量= 32,列车容量= 256:
ops/sec = 432,055,469 延迟(纳秒) = 592.5
如你所见,即便有8个工作线程,测试给出的结果也相当好-- 32辆容量为256个long的列车吞吐量为432,055,469 ops/sec。在测试期间,所有CPU内核的负载都是100%。
图 10:在测试有8个车站的Railway 期间的CPU 使用情况
在玩这个Railway算法时,我几乎忘了我最初的目标:提升多生产者/单消费者情况下的性能。
图 11:三个生产者和一个消费者的 Railway
我创建了3个生产者和1个消费者的新测试。每辆列车一站一站地转圈,而每个生产者只给每辆车装1/3容量的货。消费者取出每辆车上三个生产者给出的全部三项货物。性能测试给出的平均结果如下所示:
ops/sec = 162,597,109 列车/秒 = 54,199,036 延迟(纳秒) = 18.5
结果相当棒。生产者和消费者工作的速度超过了160M ops/sec。
为了填补差异,下面给出相同情况下的Disruptor结果- 3个生产者和1个消费者:
Run 0, Disruptor=11,467,889 ops/sec
Run 1, Disruptor=11,280,315 ops/sec
Run 2, Disruptor=11,286,681 ops/sec
Run 3, Disruptor=11,254,924 ops/sec
下面是另一个批量消息的Disruptor 3P:1C 测试 (10 条消息每批):
Run 0, Disruptor=116,009,280 ops/sec
Run 1, Disruptor=128,205,128 ops/sec
Run 2, Disruptor=101,317,122 ops/sec
Run 3, Disruptor=98,716,683 ops/sec;
最后是用带LinkedBlockingQueue 实现的Disruptor 在3P:1C场景下的测试结果:
Run 0, BlockingQueue=4,546,281 ops/sec
Run 1, BlockingQueue=4,508,769 ops/sec
Run 2, BlockingQueue=4,101,386 ops/sec
Run 3, BlockingQueue=4,124,561 ops/sec
如你所见,Railway方式的平均吞吐量是162,597,109 ops/sec,而Disruptor在同样的情况下的最好结果只有128,205,128 ops/sec。至于 LinkedBlockingQueue,最好的结果只有4,546,281 ops/sec。
Railway算法为事件批处理提供了一种可以显著增加吞吐量的简易办法。通过调整列车容量或列车数量,很容易达成想要的吞吐量/延迟。
另外, 当同一个线程可以用来消费消息,处理它们并向环中返回结果时,通过混合生产者和消费者,Railway也能用来处理复杂的情况:
图 12: 混合生产者和消费者的Railway
最后,我会提供一个经过优化的超高吞吐量 单生产者/单消费者测试:
图 13:单个生产者和单个消费者的Railway
它的平均结果为:吞吐量超过每秒15亿 (1,569,884,271)次操作,延迟为1.3 微秒。如你所见,本文开头描述的那个规模相同的单线程测试的结果是每秒2,681,850,373。
你自己想想结论是什么吧。
我希望将来再写一篇文章,阐明如何用Queue和 BlockingQueue接口支持Railway算法,用来处理不同的生产者和消费者组合。敬请关注。
关于作者
Aliaksei Papou 是Specific集团的首席软件工程师和架构师,那是一家位于奥地利维也纳的软件开发公司。Aliaksei有超过10年的小型和大型企业应用软件开发经验。他有一个坚定的信念:编写并发代码不应该这么难。
原文英文链接:Inter-thread communications in Java at the speed of light
感谢侯伯薇对本文的审校。
给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。
发表评论
-
将json格式的字符数组转为List对象
2015-08-10 15:18 915使用的是json-lib.jar包 将json格式的字符数组 ... -
用httpPost对JSON发送和接收的例子
2015-08-10 11:16 1108HTTPPost发送JSON: private static ... -
zookeeper适用场景:zookeeper解决了哪些问题
2015-07-31 18:01 772问题导读: 1.master挂机 ... -
java泛型
2015-07-29 10:48 778什么是泛型? 泛型(Ge ... -
Java线程Dump分析工具--jstack
2015-06-23 11:09 738jstack用于打印出给定的java进程ID或core fil ... -
什么是spark?
2015-04-10 09:37 506关于Spark: Spark是UC Berkeley AM ... -
dubbo 教程
2015-04-09 19:21 791先给出阿里巴巴dubbo的 ... -
jre/bin目录下面工具说明
2015-03-20 16:45 644jre/bin目录下面工具说明 ... -
JVM系列三:JVM参数设置、分析
2015-01-30 11:18 709不管是YGC还 ... -
jstat使用
2015-01-27 11:11 685jstat 1. jstat -gc pid ... -
查看java堆栈情况(cpu占用过高)
2015-01-27 11:10 7541. 确定占用cpu高的线程id: 方法一: 直接使用 ps ... -
慎用ArrayList的contains方法,使用HashSet的contains方法代替
2015-01-20 14:14 1155在启动一个应用的时候,发现其中有一处数据加载要数分钟,刚开始 ... -
Java虚拟机工作原理详解
2015-01-16 10:00 729一、类加载器首先来 ... -
jdk1.5-1.9新特性
2014-11-11 10:22 84351.51.自动装箱与拆箱:2.枚举(常用来设计单例模式 ... -
java动态代理(JDK和cglib)
2014-09-24 15:51 483JAVA的动态代理 代理模式 代理模式是常用的java设计 ... -
Java动态代理机制详解(JDK 和CGLIB,Javassist,ASM)
2014-09-24 15:45 708class文件简介及加载 Java编译器编译 ... -
怎么用github下载资源
2014-09-24 11:18 4661、下载github:到http://windows. ... -
maven项目时jar包没有到lib目录下
2014-09-01 20:05 2624在建项目时路径都设置好了,为什么在eclipse中运行mav ... -
使用并行计算大幅提升递归算法效率
2014-08-27 15:04 614前言: 无论什么样的 ... -
JAVA 实现FTP
2014-08-22 14:41 714一个JAVA 实现FTP功能的代码,包括了服务器的设置模块, ...
相关推荐
第一个文件“Java里快如闪电的线程间通讯.docx”可能详细讨论了Java平台中高效实现线程间通信的方法。线程间通信是并发编程中的关键概念,Java提供了多种机制,如wait()、notify()、synchronized关键字、管程、信号...
一、IIS的添加 请进入“控制面板”,依次选“添加/删除程序→添加/删除Windows组件”,将“Internet信息服务(IIS)”前的小钩去掉(如有),重新勾选中后按提示操作即可完成IIS组件的添加。用这种方法添加的IIS...
包括:源程序工程文件、Proteus仿真工程文件、配套技术手册等 1、采用51/52单片机作为主控芯片; 2、采用1602液晶显示; 3、采用5*8矩阵键盘输入; 4、功能键包括:复位键(RST),回删键(DEL),确定键(OK),第二功能切换(2U),背光灯键(LED); 5、运算均为单精度浮点数,包括: 加(+),减(-),乘(x),除(÷), e底指数(e^n),N次方(x^n),开N次方(sqrt), 正弦(sin),余弦(cos),正切(tan), 对数(log), 阶乘(n!)(n<35), 排列(Arn), 累加(∑), *开启第二功能(2U)后可用: 反正弦(asin),反余弦(acos),反正切(atan), 组合(Crn)
内容概要:本文详细介绍了如何利用三菱FX2N系列PLC构建机械手控制系统。主要内容涵盖电路图设计、IO表配置、源程序编写以及单机组态。文中提供了具体的梯形图编程实例,展示了如何通过PLC精确控制机械手的各种动作,如抓取、移动和放置。此外,还分享了许多实用的调试技巧和注意事项,强调了传感器状态交叉验证和关键动作的时间守护机制。通过这些内容,读者可以全面了解PLC在机械手控制中的应用。 适合人群:从事工业自动化领域的工程师和技术人员,尤其是对PLC编程和机械手控制感兴趣的初学者和有一定经验的研发人员。 使用场景及目标:适用于需要设计和实施机械手控制系统的工业场合,帮助工程师掌握PLC编程技巧,提高机械手控制系统的稳定性和可靠性。 其他说明:文章不仅提供理论指导,还包括大量实战代码和调试经验,有助于读者快速上手并在实践中不断优化系统性能。
内容概要:本文档提供了用于生成具有时尚性感元素的美女跳舞图像的提示词指南。文档内容包括角色设定为擅长描绘时尚与超现实主义图片的创作者,背景设定强调女性形象,偏好展现性感漂亮女孩的镜头表达。目标在于根据用户指令创作三幅统一风格的图像,注重色彩搭配和高清效果,同时确保每张图片都具备半身像、真实感和电影效果的特点。文档还给出了具体的输出示例,详细描述了人物形象、服装搭配以及场景布置等要素,旨在为用户提供满意的图像生成服务。; 适合人群:对图像生成感兴趣,尤其是喜欢带有时尚性感元素的美女图像的用户。; 使用场景及目标:①根据用户提供的简单场景信息(如户外或室内)生成三幅不同场景但风格统一的赛博朋克风格美女跳舞图像;②确保生成的图像符合特定的要求,如半身像、真实感、电影效果、性感服装、特定灯光效果等;③通过询问用户对生成图像的满意度来保证服务质量。; 其他说明:文档明确了图像生成的工作流程,从接收用户指令到根据反馈调整生成内容,确保整个过程高效且满足用户需求。同时,文档还限制了生成图像的具体条件,如场景必须为赛博朋克风格、不能出现鞋子和其他人等,以保证图像的独特性和一致性。
题目描述 1.问题描述 一个正整数如果任何一个数位不大于右边相邻的数位,则称为一个数位递增的 数,例如1135是一个数位递增的数,而1024不是一个数位递增的数。 给定正整数n,请问在整数1至n中有多少个数位递增的数? 输入格式 输入的第一行包含一个整数n。 输出格式 输出一行包含一个整数,表示答案。 样例输入 30 样例输出
内容概要:本文详细介绍了基于非对称纳什谈判的多微网电能共享运行优化策略及其MATLAB代码实现。首先阐述了纳什谈判和合作博弈的基本理论,然后将多微网电能共享合作运行模型分解为微网联盟效益最大化和合作收益分配两个子问题。文中展示了如何通过交替方向乘子法(ADMM)进行分布式求解,确保各微网隐私安全。此外,还探讨了电转气(P2G)和碳捕集(CCS)设备的应用,以实现低碳调度。最后,通过具体代码示例解释了模型的构建、求解及优化过程。 适合人群:对电力系统优化、博弈论、MATLAB编程感兴趣的科研人员和技术开发者。 使用场景及目标:适用于希望深入了解多微网电能共享优化策略的研究者,旨在提高微网联盟的整体效益并实现公平合理的收益分配。同时,该策略有助于降低碳排放,提升系统的环境友好性和经济性。 其他说明:文章提供了详细的代码注释和调试技巧,帮助读者更好地理解和实现这一复杂的优化策略。
内容概要:本文详细介绍了如何利用MATLAB进行六轴机械臂的视觉控制系统仿真。首先,通过MATLAB的图像处理工具箱捕捉并处理实时视频流,使用HSV颜色空间进行颜色阈值处理,从而定位红色小球的位置。然后,借助Robotics Toolbox中的逆运动学模块,将摄像头获取的目标位置转换为机械臂的关节角度,确保机械臂能够精准地追踪目标。此外,还讨论了路径规划的方法,如使用五次多项式插值和平滑滤波器,使机械臂的动作更加流畅。文中强调了实际应用中可能遇到的问题及其解决方法,如奇异点处理、坐标系转换和机械臂的速度限制等。 适合人群:具有一定编程基础和技术背景的研究人员、工程师以及对机器人视觉控制感兴趣的开发者。 使用场景及目标:适用于希望在MATLAB环境中快速搭建和测试机械臂视觉控制系统的科研人员和工程师。主要目标是掌握从图像处理到机械臂控制的完整流程,理解各模块的工作原理,并能够在实际项目中应用。 其他说明:本文不仅提供了详细的代码示例,还分享了许多实用的经验和技巧,帮助读者更好地理解和优化仿真系统。同时提醒读者注意仿真与现实之间的差异,如摄像头延迟、机械臂传动误差等问题。
KUKA机器人相关文档
KUKA机器人相关文档
内容概要:本文详细介绍了三相变流器的模型预测控制(MPC)在Matlab/Simulink环境下的实现过程。首先,初始化程序设置了关键参数,如直流母线电压、开关频率和控制周期等,确保系统的稳定性和效率。接着,通过MPC_sfun.c实现了核心控制算法,采用状态空间模型进行滚动预测,提高了系统的动态响应能力。最后,利用out.m生成高质量的仿真结果图,展示了负载突变时的快速恢复特性,并提供了优化建议,如调整代价函数权重和引入软约束等。 适合人群:电力电子工程师、控制系统研究人员以及对MPC感兴趣的科研工作者。 使用场景及目标:适用于需要精确控制电压电流的场合,如电动汽车充电站、风力发电系统等。主要目标是提高系统的动态响应速度、降低总谐波失真(THD),并在性能和计算负担之间取得平衡。 其他说明:文中提到了一些实用技巧,如控制周期的选择、预测步长的优化、图形绘制的最佳实践等,有助于读者更好地理解和应用MPC控制策略。同时,强调了在实际应用中需要注意的问题,如避免过高开关频率导致器件损坏等。
网络炒作策划要点解析.ppt
内容概要:本文详细介绍了三菱Q03UDE PLC使用SFC(顺序功能图)编程方法在16轴伺服控制系统中的应用。文章首先概述了硬件配置,包括500个IO点、16轴伺服控制以及触摸屏的画面编程。接着深入探讨了SFC编程的具体实现方式,如将复杂的轴控制分解为独立的流程块,利用并行结构解决多轴同步问题,通过触摸屏实时监控和反馈SFC步状态,以及如何高效管理和复用输出点。此外,文章还讨论了SFC在状态管理和报警处理方面的优势,并提供了具体的代码示例来展示其实现细节。最后,作者分享了一些实用技巧和注意事项,强调了SFC编程相比传统梯形图的优势。 适合人群:从事工业自动化控制系统的工程师和技术人员,尤其是对三菱PLC和SFC编程感兴趣的读者。 使用场景及目标:适用于需要进行复杂多轴伺服控制项目的工程师,旨在提高调试效率、减少信号冲突、缩短新人培养周期,并提供一种更加直观和高效的编程方法。 其他说明:文中提到的实际项目经验有助于读者更好地理解和应用SFC编程技术,同时也提醒了一些常见的错误和陷阱,帮助读者避免不必要的麻烦。
内容概要:本文详细介绍了如何使用LabVIEW实现与三菱FX3U PLC的串口通讯,采用Modbus无协议通讯方式进行简单读写操作。主要内容包括PLC通讯参数配置、LabVIEW工程结构搭建、Modbus报文构造方法以及具体的读写数据模块实现。文中提供了详细的代码示例和注意事项,帮助读者快速理解和实践这一通讯过程。 适合人群:对工业自动化有一定兴趣的技术人员,尤其是熟悉LabVIEW和三菱PLC的工程师。 使用场景及目标:适用于需要将LabVIEW作为上位机与三菱FX3U PLC进行串口通讯的应用场合,如工业控制系统、实验教学等。主要目标是掌握Modbus协议的基础知识及其在LabVIEW中的具体实现。 其他说明:文章还提供了一些常见的错误排查方法和实用技巧,如CRC校验的处理、地址偏移量的注意事项等。此外,附带了完整的源码供读者下载和参考。
图像检索_基于零样本开集的草图图像检索系统实现_附项目源码+流程教程_优质项目实战
基于C语言写的电话簿程序
包括:源程序工程文件、Proteus仿真工程文件、配套技术手册等 1、采用51单片机作为主控芯片; 2、采用1602液晶显示检测电压值,范围0~20V; 3、采用ADC0808进行模数转换;
内容概要:本文介绍了一个专业的剧本杀创作作家AI。它能根据客户需求创作各种风格和难度的剧本杀剧本,并提供创作建议和修改意见。其目标是创造引人入胜、逻辑严密的剧本体验。它的工作流程包括接收理解剧本要求、创作剧本框架情节、设计角色背景线索任务剧情走向、提供修改完善建议、确保剧本可玩性和故事连贯性。它需保证剧本原创、符合道德法律标准并在规定时间内完成创作。它具备剧本创作技巧、角色构建理解、线索悬念编织、文学知识和创意思维、不同文化背景下剧本风格掌握以及剧本杀游戏机制和玩家心理熟悉等技能。; 适合人群:有剧本杀创作需求的人群,如剧本杀爱好者、创作者等。; 使用场景及目标:①为用户提供符合要求的剧本杀剧本创作服务;②帮助用户完善剧本杀剧本,提高剧本质量。; 阅读建议:此资源详细介绍了剧本杀创作作家AI的功能和服务流程,用户可以依据自身需求与该AI合作,明确表达自己的创作需求并配合其工作流程。
内容概要:本文详细介绍了如何利用Matlab进行静态图片的美颜和特效处理。首先通过Viola-Jones算法进行人脸定位,然后采用双边滤波对皮肤进行磨皮处理,在HSV色彩空间中调整亮度以达到美白效果,最后运用小波变换将星空图等特效融合到图片中。整个过程中涉及多个图像处理技术和算法,如Haar特征、双边滤波、HSV转换、小波变换等。 适合人群:对图像处理感兴趣的初学者以及有一定Matlab基础的研发人员。 使用场景及目标:适用于希望深入了解图像处理原理并掌握具体实现方法的学习者;目标是能够独立完成简单的图像美化任务,如人像磨皮、美白、特效添加等。 其他说明:文中提供了完整的代码示例,帮助读者更好地理解和实践相关技术。同时强调了参数选择的重要性,并给出了合理的建议范围。
KUKA机器人相关文档