上述代码中,第8~11行,创建了10个CounterActor对象。第12~16行,使用Inbox与CounterActor进行通信。第14行的消息将触发CounterActor进行累加操作。第20~30行系统将等待所有10个CounterActor运行结束。执行完成后,我们便已经收集了所有的Future。在第32行,将所有的Future进行串行组合(使用sequence()方法),构造了一个整体的Future,并为它创建onComplete()回调函数。在所有的Agent操作执行完成后,onComplete()方法就会被调用(第35行)。在这个例子中,我们简单地输出最终的counterAgent值(第36行),并关闭系统(第37行)。
执行上述程序,我们将看到:
counterAgent=100000
7.12 像数据库一样操作内存数据:软件事务内存
在一些函数式编程语言中,支持一种叫做软件事务内存(STM)的技术。什么是软件事务内存呢?这里的事务和数据库中所说的事务非常类似,具有隔离性、原子性和一致性。与数据库事务不同的是,内存事务不具备持久性(很显然内存数据不会保存下来)。
在很多场合,某一项工作可能要由多个Actor协作完成。在这种协作事务中,如果一个Actor处理失败,那么根据事务的原子性,其他Actor所进行的操作必须要回滚。下面,就让我们来看一个简单的案例。
假设有一个公司要给他的员工发放福利,公司账户里有100元。每次,公司账户会给员工账户转一笔钱,假设转账10元,那么公司账户中应该减去10元,同时,员工账户中应该增加10元。这两个操作必须同时完成,或者同时不完成。
首先,让我们看一下主函数中是如何启动一个内存事务的:
01 public class STMDemo { 02 public static ActorRef company=null; 03 public static ActorRef employee=null; 04 05 public static void main(String[] args) throws Exception { 06 final ActorSystem system = ActorSystem.create("transactionDemo", ConfigFactory.load ("samplehello.conf")); 07 company=system.actorOf(Props.create(CompanyActor.class), "company"); 08 employee=system.actorOf(Props.create(EmployeeActor.class), "employee"); 09 10 Timeout timeout = new Timeout(1, TimeUnit.SECONDS); 11 12 for(int i=1;i<20;i++){ 13 company.tell(new Coordinated(i, timeout), ActorRef.noSender()); 14 Thread.sleep(200); 15 Integer companyCount = (Integer) Await.result( 16 ask(company, "GetCount", timeout), timeout.duration()); 17 Integer employeeCount = (Integer) Await.result( 18 ask(employee, "GetCount", timeout), timeout.duration()); 19 20 System.out.println("company count="+companyCount); 21 System.out.println("employee count="+employeeCount); 22 System.out.println("================="); 23 } 24 } 25 }
上述代码中CompanyActor和EmployeeActor分别用于管理公司账户和雇员账户。在第12~23行中,我们尝试进行19次汇款,第一次汇款额度为1元,第二次为2元,依此类推,最后一笔汇款为19元。
在第13行,新建一个Coordinated协调者,并且将这个协调者当做消息发送给company。当company收到这个协调者消息后,自动成为这个事务的第一个成员。
第15~18行询问公司账户和雇员账户的当前余额,并在第20~21行进行输出。
下面是代表公司账户的Actor:
01 public class CompanyActor extends UntypedActor { 02 private Ref.View<Integer> count = STM.newRef(100); 03 04 @Override 05 public void onReceive(Object msg) { 06 if (msg instanceof Coordinated) { 07 final Coordinated c=(Coordinated)msg; 08 final int downCount=(Integer)c.getMessage(); 09 STMDemo.employee.tell(c.coordinate(downCount), getSelf()); 10 try{ 11 c.atomic(new Runnable() { 12 @Override 13 public void run() { 14 if(count.get()<downCount){ 15 throw new RuntimeException("less than "+downCount); 16 } 17 STM.increment(count, -downCount); 18 } 19 }); 20 }catch(Exception e){ 21 e.printStackTrace(); 22 } 23 24 }else if ("GetCount".equals(msg)) { 25 getSender().tell(count.get(), getSelf()); 26 }else{ 27 unhandled(msg); 28 } 29 } 30 }
在CompanyActor中,首先判断接收的msg是否是Coordinated。如果是Coordinated,则表示这是一个新事务的开始。在第8行,获得事务的参数也就是需要转账的金额。接着在第9行,将调用Coordinated.coordinate()方法,将employee也加入到当前事务中,这样这个事务中就有两个参与者了。
第11行,调用了Coordinated.atomic()定义了原子执行块作为这个事务的一部分。在这个执行块中,对公司账户进行余额调整(第17行)。但是当汇款额度大于可用余额时,就会抛出异常,宣告失败。
第25行用于处理GetCount消息,返回当前账户余额。
作为转账接收方的雇员账户如下:
01 public class EmployeeActor extends UntypedActor { 02 private Ref.View<Integer> count = STM.newRef(50); 03 04 @Override 05 public void onReceive(Object msg) { 06 if (msg instanceof Coordinated) { 07 final Coordinated c = (Coordinated) msg; 08 final int downCount = (Integer) c.getMessage(); 09 try { 10 c.atomic(new Runnable() { 11 @Override 12 public void run() { 13 STM.increment(count, downCount); 14 } 15 }); 16 } catch (Exception e) { 17 } 18 } else if ("GetCount".equals(msg)) { 19 getSender().tell(count.get(), getSelf()); 20 } else { 21 unhandled(msg); 22 } 23 } 24 }
上述代码第2行,设置雇员账户初始金额是50元。第6行,判断消息是否为Coordinated,如果是Coordinated,则当前Actor会自动加入Coordinated指定的事务。第10行,定义原子操作,在这个操作中将修改雇员账户余额。在这里,我们并没有给出异常情况的判断,只要接收到转入金额,一律将其增加到雇员账户中。
大家可能就会产生疑问,如果在公司账户中由于余额不足而导致转账失败了,那在这个雇员账户中不还是正常增加了金额吗?那岂不是钱多出来了?
不过这个担心是完全多余的。因为在这里,两个Actor都已经加入到同一个协调事务Coordinated中了,因此当公司账户出现异常后,雇员账户的余额就会回滚。
执行上述程序,部分输出如下:
..... company count=85 employee count=65 ================= java.lang.RuntimeException: less than 14 company count=9 employee count=141 .... ================= java.lang.RuntimeException: less than 19 省略堆栈信息 实在太多了 at scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107) company count=9 employee count=141 =================
可以看到,无论转账操作是否成功,公司账户和雇员账户的金额总是一致的。当转账失败时,雇员账户的余额并不会增加。这就是软件事务内存的作用。
7.13 一个有趣的例子:并发粒子群的实现
粒子群算法(PSO)是一种进化算法。它与大名鼎鼎的遗传算法非常类似,可以用来解决一些优化问题。大家知道,一些优化问题(比如旅行商问题TSP)都属于NP问题。它们的时间复杂度可能会达到O(n!)或者O(2n),这种在多项式时间内不可解的问题总是会让人望而生畏。而以PSO算法为代表的进化计算,往往可以将这些NP问题,转变为一个多项式问题。但这种转变是有代价的,进化算法往往都不保证你可以从结果中得到最优解。我这么说,也许就有人会问了,这个算法都不能保证得到最优解,那有什么用呢?其实,在生活中的很多场景下,并不是特别需要最优解,我们更加希望得到的是一个满意解。比如说,去水果店买西瓜,店里可能放着一大堆西瓜,每个人都想挑一个最好的。但你想拿到最好的那个西瓜必须得挨个检查过去,并且还得认真做好记录才行。我相信,没有一个人会这么买西瓜,因为成本太高了。对于大部分人来说,更倾向于在表面上挑几个顺眼的看看,如果还过得去,也就下手了。这也就是说只要这个结果不要差得太离谱就行了。
既然最优的方案很难得到,那么我们就想办法以很低的成本获得一个还算过得去的方案,也不失为一计良策。在后面给出的小案例中大家也可以看到,在很多情况下,虽然进化算法无法让你获得最优解,也无法证明它得到的解与最优解到底有多少差距,但实际中,通过进化算法搜索到的满意解很可能与最优解已经非常接近了。
7.13.1 什么是粒子群算法
粒子群优化算法(PSO)是一种进化计算技术,最早由Kenny与Eberhart于1995年提出。它源于对鸟群捕食行为的研究,与遗传算法相似,是一种基于迭代的优化算法,广泛应用于函数优化和神经网络训练等方面。与遗传算法相比,PSO算法的实现简单得多,参数配置也相对较少,对使用人员的经验要求不高,因此更加易于实际工程应用。
从日常生活的观察中可以知道,鸟类的觅食往往会表现成群体特性。如果在地上有一小撮食物,那么鸟群很可能就会聚集在这一堆食物旁边。如果其中一只小鸟发现了另外一堆更丰盛的食物,那它可能会离群飞向更丰盛的食物,而这有可能带动整个鸟群一起飞向新的地点。当然了,在整个种群中,难免会出现几只特别有“个性”的小鸟,它们不喜欢太热闹的地方,当整个种群迁移时,它们不会跟着种群走,或者自己散步,或者自行游荡。
粒子群算法正是对上述过程的模拟。在程序中,我们可以模拟大量的小鸟,小鸟的觅食点正是要求解的问题的解。解越是优秀,意味着食物越是丰盛,因此,模拟的小鸟会从自己的位置出发以一定的速度向最优点的方向移动。在移动过程中,任何一只小鸟都有可能发现更好的解,这又会进一步影响群体的行为。就这样如此反复迭代,最终,将得到一个不错的答案。
7.13.2 粒子群算法的计算过程
粒子群算法的大体步骤如下:
初始化所有粒子,粒子的位置随机生成。计算每个粒子当前的适应度,并将此设为当前粒子的个体最优值(记为pBest)。
所有粒子将自己的个体最优值发送给管理者Master。Master获得所有粒子的信息后,筛选出全局最优的解(记为gBest)。
Master将gBest通知所有粒子,所有粒子便知道全局最优点的位置。
接着,所有粒子根据自己的pBest和全局gBest,更新自己的速度,在有了速度后,再更新自己的位置。
vk+1=c0×rand()×vk+c1×rand()×(pbestk−xk)+c2×rand()×(gbestk−xk)
xk+1=xk+vk+1
其中,rand()函数产生一个0,1之间的随即数。c0=1,c1=2,c2=2,k表示进化的代数。vk表示当前速度,pbestk和gbestk表示个体最优解和全局最优解。当然,对于每一个维度上的速度分量,我们可以为它限定一个最大值。确保“小鸟”不会飞得太快,错过了重要的信息。
如果粒子产生了新的个体最优点,则发送给Master,在此,转到步骤2。
整体过程的示意图如图7.2所示。
图7-2 PSO算法示意图
从这个计算步骤中可以看到,计算过程拥有一定的随机性。但由于我们可以启用大量的例子,因此其计算效果在统计学意义上是稳定的。在这个标准的粒子群算法中,由于所有粒子都会向全局最优靠拢,因此,其跳出局部最优的能力并不算太强。因此,我们也可以想办法对标准的粒子群算法进行一些合理的改进。比如,允许各个粒子随机移动,甚至逆向移动来试图突破局部最优。在这里为简单起见,我不打算做这些复杂的实现。
7.13.3 粒子群算法能做什么
粒子群算法能为我们做些什么呢?它应用最多的场景是进行最优化计算。实际上,以粒子群算法为代表的进化计算,可以说是最优化方法中的通用方法。几乎一切最优化问题都可以通过这种随机搜索的模式解决,其成本低、难度小、效果好,因此颇受欢迎。
下面,就让我们来探讨一个典型的优化问题:
假设现在有400万资金,要求4年内使用完。若在第1年使用x万元,则可以得到效益万元(效益不能再使用),当年不用的资金可存入银行,年利率为10%。尝试制订出资金的使用规划,使4年效益之和最大。
很明显,对于这类问题,不同的方案得到的结果可能会有很大的差异。比如,若第一年把400万元全部用完,则总效益为万元;若前3年均不用而存入银行,第4年把本金和利息全部用完,则总效益为万元,显然优于第一种方案。
如果我们将此问题转为一般化的优化问题,则可以得到以下方程组,如图7.3所示。
图7.3 一般化的约束问题
其中,x1、x2、x3、x4分别表示第1、2、3、4年使用的资金。使用拉格朗日乘子法对此方程组进行求解,可以得到第一年使用86.19万元、第2年使用104.29万元,第3年使用126.19万元,第4年使用152.69万元为这个问题的最优解,此时总效益达43.09万元。
由于求解过程过于复杂,使用拉格朗日乘子法时,需要对先后12个未知数和方程进行联立求解,比较难以实现。由于求解过程与我们讨论的主题无关,所以在这里不再给出。
对于类似的优化问题,正是粒子群算法的涉猎范围。当使用粒子群算法时,我们可以先随机给出若干个满足提交的资金规划方案。接着,根据粒子群的演化公式,不断调整各个粒子的位置(粒子的每一个位置代表一套方案),逐步探索更优的方案。
7.13.4 使用Akka实现粒子群
现在,我们已经知道粒子群的原理,并且有了一个较为复杂的优化问题等待我们求解。接下来,就需要开动脑筋,使用Akka来实现一个简单的粒子群,来解决这个优化问题了。
使用Actor的模式与粒子群算法之间有着天生切合度。粒子群算法由于涉及到多个甚至是极其大量的粒子参与运算,因此它隐含着并行计算的模式。其次,从直观上我们也可以知道,粒子群算法的求解精度或者说求解的质量,与参与运算的粒子有着直接的关系。很显然,参与运算的粒子数量越多,得到的解自然也就越精确。
如果我们使用传统的多线程方式实现粒子群,一个最大的问题就是线程的数量可能是非常有限的。在当前这种应用场景中,我们希望可以拥有数万,甚至数十万的粒子,以提高计算精度,但众所周知,在一台计算机上运行数万个线程基本是不可能的,就算可以,系统的性能也会大打折扣。因此,使用多线程的模型无法很好地和粒子群的实现相融合。
但Akka的Actor的模型则不同。由于多个Actor可以复用一个线程,而Actor本身作为轻量级的并发执行单元可以有极其大量的存在。因此,我们就可以使用Actor来模拟整个粒子群计算的场景。下面就让我们仔细看一下系统的实现。
首先,我们需要两个表示pBest和gBest的消息类型,用于在多个Actor之间传递个体最优和全局最优。
01 public final class GBestMsg { 02 final PsoValue value; 03 public GBestMsg(PsoValue v){ 04 value=v; 05 } 06 public PsoValue getValue() { 07 return value; 08 } 09 } 10 11 public final class PBestMsg { 12 final PsoValue value; 13 public PBestMsg(PsoValue v){ 14 value=v; 15 } 16 17 public PsoValue getValue() { 18 return value; 19 } 20 21 public String toString(){ 22 return value.toString(); 23 } 24 }
上述代码中,GBestMsg(代码第1行)表示携带全局最优解的消息。而PBestMsg(代码第11行)表示携带个体最优的消息。它们都使用PsoValue来表示一个可行的解。
在PsoValue中,主要包括两个信息,第一是表示投资规划的方案,即每一年分别需要投资多少钱;第二是这个投资方案的总收益:
01 public final class PsoValue { 02 final double value; 03 final List<Double> x; 04 public PsoValue(double v,List<Double> x){ 05 value=v; 06 List<Double> b=new ArrayList<Double>(5); 07 b.addAll(x); 08 this.x=Collections.unmodifiableList(b); 09 } 10 public double getValue(){ 11 return value; 12 } 13 public List<Double> getX(){ 14 return x; 15 } 16 17 public String toString(){ 18 StringBuffer sb=new StringBuffer(); 19 sb.append("value:").append(value).append("\n") 20 .append(x.toString()); 21 return sb.toString(); 22 } 23 }
上述代码中,数组x中,x[1]、x[2]、x[3]、x[4]分别表示第1年、第2年、第3年和第4年的投资额。这里为了方便起见,我忽略了x[0](它在我们的程序中是没有作用的)。成员变量value表示这组投资方案的收益值。
因此,根据需求x与value之间的关系如下代码所示:
1 public class Fitness { 2 public static double fitness(List<Double> x){ 3 double sum=0; 4 for(int i=1;i<x.size();i++){ 5 sum+=Math.sqrt(x.get(i)); 6 } 7 return sum; 8 } 9 }
上述代码定义的fitness()函数返回了给定投资方案的适应度。适应度也就是投资的收益,我们自然应该更倾向于选择适应度更高的投资方案。在这里适应度=。
有了这些基础工具,我们就可以来实现简单的粒子(这里我把它叫作Bird)了。
对于基本粒子,我们需要定义以下成员变量:
1 public class Bird extends UntypedActor { 2 private final LoggingAdapter log = Logging.getLogger(getContext().system(), this); 3 private PsoValue pBest=null; 4 private PsoValue gBest=null; 5 private List<Double> velocity =new ArrayList<Double>(5); 6 private List<Double> x =new ArrayList<Double>(5); 7 private Random r = new Random();
上述代码中,pBest和gBest分别表示个体最优和全局最优,velocity表示粒子在各个维度上的速度(在当前案例中,每一年的投资额就可以认为是一个维度,因此系统有4个维度)。x表示投资方案,即每一年的投资额。由于在粒子群算法中,需要使用随机数,因此,这里定义了r。
当一个粒子被创建时,我们需要初始化粒子的当前位置。粒子的每一个位置都代表一个投资方案,下面的代码展示了粒子的初始化逻辑:
01 @Override 02 public void preStart(){ 03 for(int i=0;i<5;i++){ 04 velocity.add(Double.NEGATIVE_INFINITY); 05 x.add(Double.NEGATIVE_INFINITY); 06 } 07 //x1<=400 08 x.set(1, (double)r.nextInt(401)); 09 10 //x2<=440-1.1*x1 11 double max=400-1.1*x.get(1); 12 if(max<0)max=0; 13 x.set(2, r.nextDouble()*max); 14 15 //x3<=484-1.21*x1-1.1*x2 16 max=484-1.21*x.get(1)-1.1*x.get(2); 17 if(max<=0)max=0; 18 x.set(3, r.nextDouble()*max); 19 20 //x4<= 532.4-1.331*x1-1.21*x2-1.1*x3 21 max=532.4-1.331*x.get(1)-1.21*x.get(2)-1.1*x.get(3); 22 if(max<=0)max=0; 23 x.set(4, r.nextDouble()*max); 24 25 double newFit=Fitness.fitness(x); 26 pBest=new PsoValue(newFit,x); 27 PBestMsg pBestMsg=new PBestMsg(pBest); 28 ActorSelection selection = getContext().actorSelection("/user/masterbird"); 29 selection.tell(pBestMsg, getSelf()); 30 }
由于在当前案例中,每一年的投资额度是有条件约束的,比如第一年的投资额不能超过400万(第7~8行),而第2年的投资上限是440万(假设第一年全部存银行,代码第10~13行),依此类推。粒子初始化时,随机生成一组满足基本约束条件的投资组合,并计算它的适应度(第25行)。初始的投资方案自然也就作为当前的个体最优,并发送给Master(第29行)。
当Master计算出当前全局最优后,会将全局最优发送给每一个粒子,粒子根据全局最优更新自己的运行速度,并更新自己的速度以及当前位置。
01 @Override 02 public void onReceive(Object msg) { 03 if (msg instanceof GBestMsg) { 04 gBest=((GBestMsg) msg).getValue(); 05 //更新速度 06 for(int i=1;i<velocity.size();i++){ 07 updateVelocity(i); 08 } 09 //更新位置 10 for(int i=1;i<x.size();i++){ 11 updateX(i); 12 } 13 validateX(); 14 double newFit=Fitness.fitness(x); 15 if(newFit>pBest.value){ 16 pBest=new PsoValue(newFit,x); 17 PBestMsg pBestMsg=new PBestMsg(pBest); 18 getSender().tell(pBestMsg, getSelf()); 19 } 20 } 21 else{ 22 unhandled(msg); 23 } 24 }
上述代码中,粒子接收到了全局最优(代码第4行),接着根据粒子群的标准公式更新自己的速度(第6~8行)。接着,根据速度,更新自己的位置(第10~12行)。由于当前问题是有约束的,也就是说解空间并不是随意的。粒子很可能在更新位置后,跑出了合理的范围之外,因此,还有必要进行有效性检查(第13行)。
在更新完成后,就可以计算新位置的适应度,如果产生了新的个体最优,就将其发送给Master(第15~19行)。
在当前案例中,速度和位置的更新是依据标准的粒子群实现,如下:
01 public double updateVelocity(int i){ 02 double v= Math.random()*velocity.get(i) 03 +2*Math.random()*(pBest.getX().get(i)-x.get(i)) 04 +2*Math.random()*(gBest.getX().get(i)-x.get(i)); 05 v=v>0? Math.min(v, 5): Math.max(v, -5); 06 velocity.set(i, v); 07 return v; 08 } 09 10 public double updateX(int i){ 11 double newX=x.get(i)+velocity.get(i); 12 x.set(i, newX); 13 return newX; 14 }
上述代码中updateVelocity()和updateX()分别更新了粒子的速度和位置。位置的更新依赖于当前的速度(第11行)。
由于每一年的投资都是有限额的,因此,要避免粒子跑到合理空间之外,下面的代码强制将粒子约束中合理的区间中。
01 public void validateX(){ 02 if(x.get(1)>400){ 03 x.set(1, (double)r.nextInt(401)); 04 } 05 06 //x2 07 double max=400-1.1*x.get(1); 08 if(x.get(2)>max || x.get(2)<0){ 09 x.set(2, r.nextDouble()*max); 10 } 11 //x3 12 max=484-1.21*x.get(1)-1.1*x.get(2); 13 if(x.get(3)>max || x.get(3)<0){ 14 x.set(3, r.nextDouble()*max); 15 } 16 //x4 17 max=532.4-1.331*x.get(1)-1.21*x.get(2)-1.1*x.get(3); 18 if(x.get(4)>max || x.get(4)<0){ 19 x.set(4, r.nextDouble()*max); 20 } 21 }
上述代码分别对x1、x2、x3、x4进行约束,一旦发现粒子跑出了定义范围就将它进行随机化。
此外,我们还需要一只MasterBird,用于管理和通知全局最优。
01 public class MasterBird extends UntypedActor { 02 private final LoggingAdapter log = Logging.getLogger(getContext().system(), this); 03 private PsoValue gBest=null; 04 05 @Override 06 public void onReceive(Object msg) { 07 if (msg instanceof PBestMsg) { 08 PsoValue pBest = ((PBestMsg) msg).getValue(); 09 if(gBest==null || gBest.value < pBest.value){ 10 //更新全局最优,通知所有粒子 11 System.out.println(msg+"\n"); 12 gBest=pBest; 13 ActorSelection selection = getContext().actorSelection("/user/bird_*"); 14 selection.tell(new GBestMsg(gBest), getSelf()); 15 } 16 } 17 else{ 18 unhandled(msg); 19 } 20 } 21 }
上述代码定义了MasterBird。当它收到一个个体最优的解时,会将其与全局最优进行比较,如果产生了新的全局最优,就更新这个全局最优并通知所有的粒子(第12~14行)。
好了,现在万事俱备只欠东风。下面就是主函数:
01 public class PSOMain { 02 public static final int BIRD_COUNT = 100000; 03 public static void main(String[] args) { 04 ActorSystem system = ActorSystem 05 .create("psoSystem", ConfigFactory.load("samplehello.conf")); 06 system.actorOf(Props.create(MasterBird.class), "masterbird"); 07 for (int i = 0; i < BIRD_COUNT; i++) { 08 system.actorOf(Props.create(Bird.class), "bird_" + i); 09 } 10 } 11 }
上述代码定义了粒子总数,这里是10万个粒子。接着创建一个MasterBird Actor(第6行),和10万个bird(第7~9行)。
执行上述代码,运行一小段时间,你就可以得到如下输出(截取部分):
value:36.15412875487459 [-Infinity, 168.0, 18.786423873345715, 102.1742923174793, 76.5657638235272] value:37.88452477135976 [-Infinity, 64.0, 87.66774733441137, 37.976681047619195, 206.17791816445362] .... value:42.240797528048176 [-Infinity, 113.0, 42.37168995110633, 141.70570102409184, 174.16812834843475] .... value:43.01934824083668 [-Infinity, 76.0, 112.89557345993592, 133.29270155682005, 147.16289926594942]
上述输出表示,当粒子群随机初始化时,最优解为36.15万元,但随着粒子的搜索,这个投资方案被逐步优化,由37.88万一直上升到43.02万元。根据我们前面的求解,我们知道这个投资方案的最优结果是43.09万元,可以看到,粒子群的搜索结果和全局最优已经非常接近了。
当然了,由于粒子群算法的随机性,每次执行结果可能并不一样,这意味着有时候,你可能会求得更好的解,或者得到一个稍差一些的解,但其偏差不会相差太远。
7.14 参考文献
Akka官方文档
http://doc.akka.io/docs/akka/2.3.7/java.html
有关最优化方法的介绍
《最优化方法》高等教育出版社施光燕著
Nobody Needs Reliable Messaging
http://www.infoq.com/articles/no-reliable-messaging