庄周梦蝶

生活、程序、未来
   :: 首页 ::  ::  :: 聚合  :: 管理

    今天同事遇到的问题,用JRuby调用一个java方法,该方法使用了jdk1.5的可变参数。我一开始以为只要简单地将可变参数表示为数组即可,例如下面的两个java类:
public class Echo{
    
public void echo(String name){
       System.out.println(name);
    }
}
public class Test{
    
public void hello(String name,Echoargs){
        System.out.println(
"hello,"+name);
        
for(Echo e:args){
            e.echo(name);
        }
    }
}
   我想在jruby中调用Test的hello方法,该方法有个可变参数args。所谓可变参数经过编译后其实也就是数组,这个可以通过观察字节码知道,那么如果用数组来调用可以不?
require 'java'
require 
'test.jar'
include_class 
'Test'
include_class 
'Echo'
t.hello(
"dennis")  #报错,参数不匹配
t.hello("dennis",[])  #报错,类型不匹配
   很遗憾,这样调用是错误的,原因如上面的注释。具体到类型不匹配,本质的原因是JRuby中的数组与java中对数组的字节码表示是不一致的,JRuby中的数组是用org.jruby.RubyArray类来表示,而hello方法需要的数组却是是[LEcho。解决的办法就是将JRuby的数组转成java需要的类型,通过to_java方法,因而下面的调用才是正确的,尽管显的麻烦:
require 'java'
require 
'test.jar'
include_class 
'Test'
include_class 
'Echo'
t
=Test.new
t.hello(
"dennis",[].to_java("Echo"))
e1
=Echo.new
t.hello(
"dennis",[e1].to_java("Echo"))
e2
=Echo.new
t.hello(
"dennis",[e1,e2].to_java("Echo"))


posted @ 2008-06-14 22:39 dennis 阅读(2175) | 评论 (1)编辑 收藏

     头发乃人之元,很多人看人,第一眼看的就是你的头发,当然看MM的部位可能不一样。我的头发从来都是像野草一样,从来没放心思在上面,都是理一次头发大半年不管他,长的跟杂草似的难受,然后回家找小区内的师傅理发——或者说剃头,而且发型从来都是板寸头,短短的,免的大半年不回家还得在外面理发。因而,我的理发时间跟我的回家时间一样。

小时候,那时候住在老家,没有专门的理发店,也可能是离我家的小村庄很远。有个走村串户的剃头匠,大概几个月能在村里见他一次,带着一身行头,谁家有人要剃头,就请他过去。记的那时是没用电的理发工具,一把类似夹子的东西,老师傅手工在各式各样的脑袋上推来推去,然后搞些肥皂水就着自家的井水冲洗。后来很久没再见到这位老师傅,而隔壁村开了家理发店,大人们就带着我们走路或者骑着自行车去隔壁村剃头了,记的那位师傅还是我们大队的干部,隔壁是猪肉店,至于剃头的细节却是记不清了。再后来隔壁村又开了一家理发店,是个小青年开的,印象特别深的一次是我奶奶带我过去理发,小青年说如果我帮他打一桶井水,钱可以减半。我帮他打了桶井水,然后真给我减半了,就着自己打的井水冲洗我的不规则的大脑袋。后来,跟父母搬到了县城,去外地读书,就再也没在老家理发过了,好几年之后路过小青年开的那家店,装修了,蓝色的玻璃后面,小青年变成了师傅,指导着自己的徒弟在理发,我想,大概他也记不得当年那个给他打水的小屁孩了。

进城了,我理发就固定在小区的周师傅这了。上大学,基本是五一、十一和春节回家的时候剃一次,哪怕再长,还是不愿意在福州理发,分不清是习惯还是莫名其妙的坚持了。其实挺不好意思的,每次都留那么长的头发,周师傅也还是固定收我5块钱,不过今年涨价了,7块。下午剃头的时候,周师傅的女儿找他要100来块钱,旁边的人问周师傅她拿去做啥呢?周师傅说去染发,语气有点黯然。父亲理的老式发型,女儿是不会喜欢的。我的头发就这样随着我离家的远近轮回着,现在在广州,回家还是容易的,以后去哪还是不知道的事情,希望还能半年回家一次理发。

posted @ 2008-06-13 21:54 dennis 阅读(577) | 评论 (4)编辑 收藏

Hadoop分布式文件系统:架构和设计要点
原文:http://hadoop.apache.org/core/docs/current/hdfs_design.html
一、前提和设计目标
1、硬件错误是常态,而非异常情况,HDFS可能是有成百上千的server组成,任何一个组件都有可能一直失效,因此错误检测和快速、自动的恢复是HDFS的核心架构目标。
2、跑在HDFS上的应用与一般的应用不同,它们主要是以流式读为主,做批量处理;比之关注数据访问的低延迟问题,更关键的在于数据访问的高吞吐量。
3HDFS以支持大数据集合为目标,一个存储在上面的典型文件大小一般都在千兆至T字节,一个单一HDFS实例应该能支撑数以千万计的文件。
4HDFS应用对文件要求的是write-one-read-many访问模型。一个文件经过创建、写,关闭之后就不需要改变。这一假设简化了数据一致性问 题,使高吞吐量的数据访问成为可能。典型的如MapReduce框架,或者一个web crawler应用都很适合这个模型。
5、移动计算的代价比之移动数据的代价低。一个应用请求的计算,离它操作的数据越近就越高效,这在数据达到海量级别的时候更是如此。将计算移动到数据附近,比之将数据移动到应用所在显然更好,HDFS提供给应用这样的接口。
6、在异构的软硬件平台间的可移植性。

二、NamenodeDatanode
    HDFS采用master/slave架构。一个HDFS集群是有一个Namenode和一定数目的Datanode组成。Namenode是一个中心服 务器,负责管理文件系统的namespace和客户端对文件的访问。Datanode在集群中一般是一个节点一个,负责管理节点上它们附带的存储。在内 部,一个文件其实分成一个或多个block,这些block存储在Datanode集合里。Namenode执行文件系统的namespace操作,例如 打开、关闭、重命名文件和目录,同时决定block到具体Datanode节点的映射。DatanodeNamenode的指挥下进行block的创 建、删除和复制。NamenodeDatanode都是设计成可以跑在普通的廉价的运行linux的机器上。HDFS采用java语言开发,因此可以部 署在很大范围的机器上。一个典型的部署场景是一台机器跑一个单独的Namenode节点,集群中的其他机器各跑一个Datanode实例。这个架构并不排 除一台机器上跑多个Datanode,不过这比较少见。

单一节点的Namenode大大简化了系统的架构。Namenode负责保管和管理所有的HDFS元数据,因而用户数据就不需要通过Namenode(也就是说文件数据的读写是直接在Datanode上)。

三、文件系统的namespace
   HDFS支持传统的层次型文件组织,与大多数其他文件系统类似,用户可以创建目录,并在其间创建、删除、移动和重命名文件。HDFS不支持user quotas和访问权限,也不支持链接(link),不过当前的架构并不排除实现这些特性。Namenode维护文件系统的namespace,任何对文 件系统namespace和文件属性的修改都将被Namenode记录下来。应用可以设置HDFS保存的文件的副本数目,文件副本的数目称为文件的 replication因子,这个信息也是由Namenode保存。

四、数据复制
    HDFS被设计成在一个大集群中可以跨机器地可靠地存储海量的文件。它将每个文件存储成block序列,除了最后一个block,所有的block都是同 样的大小。文件的所有block为了容错都会被复制。每个文件的block大小和replication因子都是可配置的。Replication因子可 以在文件创建的时候配置,以后也可以改变。HDFS中的文件是write-one,并且严格要求在任何时候只有一个writerNamenode全权管 理block的复制,它周期性地从集群中的每个Datanode接收心跳包和一个Blockreport。心跳包的接收表示该Datanode节点正常工 作,而Blockreport包括了该Datanode上所有的block组成的列表。


1、副本的存放,副本的存放是HDFS可靠性和性能的关键。HDFS采用一种称为rack-aware的策略来改进数据的可靠性、有效性和网络带宽的利 用。这个策略实现的短期目标是验证在生产环境下的表现,观察它的行为,构建测试和研究的基础,以便实现更先进的策略。庞大的HDFS实例一般运行在多个机 架的计算机形成的集群上,不同机架间的两台机器的通讯需要通过交换机,显然通常情况下,同一个机架内的两个节点间的带宽会比不同机架间的两台机器的带宽 大。
    通过一个称为Rack Awareness的过程,Namenode决定了每个Datanode所属的rack id。一个简单但没有优化的策略就是将副本存放在单独的机架上。这样可以防止整个机架(非副本存放)失效的情况,并且允许读数据的时候可以从多个机架读 取。这个简单策略设置可以将副本分布在集群中,有利于组件失败情况下的负载均衡。但是,这个简单策略加大了写的代价,因为一个写操作需要传输block到 多个机架。
    在大多数情况下,replication因子是3HDFS的存放策略是将一个副本存放在本地机架上的节点,一个副本放在同一机架上的另一个节点,最后一 个副本放在不同机架上的一个节点。机架的错误远远比节点的错误少,这个策略不会影响到数据的可靠性和有效性。三分之一的副本在一个节点上,三分之二在一个 机架上,其他保存在剩下的机架中,这一策略改进了写的性能。

2、副本的选择,为了降低整体的带宽消耗和读延时,HDFS会尽量让reader读最近的副本。如果在reader的同一个机架上有一个副本,那么就读该副本。如果一个HDFS集群跨越多个数据中心,那么reader也将首先尝试读本地数据中心的副本。

3SafeMode
    Namenode启动后会进入一个称为SafeMode的特殊状态,处在这个状态的Namenode是不会进行数据块的复制的。Namenode从所有的 Datanode接收心跳包和BlockreportBlockreport包括了某个Datanode所有的数据块列表。每个block都有指定的最 小数目的副本。当Namenode检测确认某个Datanode的数据块副本的最小数目,那么该Datanode就会被认为是安全的;如果一定百分比(这 个参数可配置)的数据块检测确认是安全的,那么Namenode将退出SafeMode状态,接下来它会确定还有哪些数据块的副本没有达到指定数目,并将 这些block复制到其他Datanode

五、文件系统元数据的持久化
    Namenode存储HDFS的元数据。对于任何对文件元数据产生修改的操作,Namenode都使用一个称为Editlog的事务日志记录下来。例如, 在HDFS中创建一个文件,Namenode就会在Editlog中插入一条记录来表示;同样,修改文件的replication因子也将往 Editlog插入一条记录。Namenode在本地OS的文件系统中存储这个Editlog。整个文件系统的namespace,包括block到文件 的映射、文件的属性,都存储在称为FsImage的文件中,这个文件也是放在Namenode所在系统的文件系统上。
    Namenode在内存中保存着整个文件系统namespace和文件Blockmap的映像。这个关键的元数据设计得很紧凑,因而一个带有4G内存的 Namenode足够支撑海量的文件和目录。当Namenode启动时,它从硬盘中读取EditlogFsImage,将所有Editlog中的事务作 用(apply)在内存中的FsImage ,并将这个新版本的FsImage从内存中flush到硬盘上,然后再truncate这个旧的Editlog,因为这个旧的Editlog的事务都已经 作用在FsImage上了。这个过程称为checkpoint。在当前实现中,checkpoint只发生在Namenode启动时,在不久的将来我们将 实现支持周期性的checkpoint
    Datanode并不知道关于文件的任何东西,除了将文件中的数据保存在本地的文件系统上。它把每个HDFS数据块存储在本地文件系统上隔离的文件中。 Datanode并不在同一个目录创建所有的文件,相反,它用启发式地方法来确定每个目录的最佳文件数目,并且在适当的时候创建子目录。在同一个目录创建 所有的文件不是最优的选择,因为本地文件系统可能无法高效地在单一目录中支持大量的文件。当一个Datanode启动时,它扫描本地文件系统,对这些本地 文件产生相应的一个所有HDFS数据块的列表,然后发送报告到Namenode,这个报告就是Blockreport

六、通讯协议
    所有的HDFS通讯协议都是构建在TCP/IP协议上。客户端通过一个可配置的端口连接到Namenode,通过ClientProtocolNamenode交互。而Datanode是使用DatanodeProtocolNamenode交互。从ClientProtocolDatanodeprotocol抽象出一个远程调用(RPC),在设计上,Namenode不会主动发起RPC,而是是响应来自客户端和 Datanode RPC请求。

七、健壮性
    HDFS的主要目标就是实现在失败情况下的数据存储可靠性。常见的三种失败:Namenode failures, Datanode failures和网络分割(network partitions)
1、硬盘数据错误、心跳检测和重新复制
    每个Datanode节点都向Namenode周期性地发送心跳包。网络切割可能导致一部分DatanodeNamenode失去联系。 Namenode通过心跳包的缺失检测到这一情况,并将这些Datanode标记为dead,不会将新的IO请求发给它们。寄存在dead Datanode上的任何数据将不再有效。Datanode的死亡可能引起一些block的副本数目低于指定值,Namenode不断地跟踪需要复制的 block,在任何需要的情况下启动复制。在下列情况可能需要重新复制:某个Datanode节点失效,某个副本遭到损坏,Datanode上的硬盘错 误,或者文件的replication因子增大。

2、集群均衡
   HDFS支持数据的均衡计划,如果某个Datanode节点上的空闲空间低于特定的临界点,那么就会启动一个计划自动地将数据从一个Datanode搬移 到空闲的Datanode。当对某个文件的请求突然增加,那么也可能启动一个计划创建该文件新的副本,并分布到集群中以满足应用的要求。这些均衡计划目前 还没有实现。

3、数据完整性
  从某个Datanode获取的数据块有可能是损坏的,这个损坏可能是由于Datanode的存储设备错误、网络错误或者软件bug造成的。HDFS客户端 软件实现了HDFS文件内容的校验和。当某个客户端创建一个新的HDFS文件,会计算这个文件每个block的校验和,并作为一个单独的隐藏文件保存这些 校验和在同一个HDFS namespace下。当客户端检索文件内容,它会确认从Datanode获取的数据跟相应的校验和文件中的校验和是否匹配,如果不匹配,客户端可以选择 从其他Datanode获取该block的副本。

4、元数据磁盘错误
    FsImageEditlogHDFS的核心数据结构。这些文件如果损坏了,整个HDFS实例都将失效。因而,Namenode可以配置成支持维护多 个FsImageEditlog的拷贝。任何对FsImage或者Editlog的修改,都将同步到它们的副本上。这个同步操作可能会降低 Namenode每秒能支持处理的namespace事务。这个代价是可以接受的,因为HDFS是数据密集的,而非元数据密集。当Namenode重启的 时候,它总是选取最近的一致的FsImageEditlog使用。
   NamenodeHDFS是单点存在,如果Namenode所在的机器错误,手工的干预是必须的。目前,在另一台机器上重启因故障而停止服务的Namenode这个功能还没实现。

5、快照
   快照支持某个时间的数据拷贝,当HDFS数据损坏的时候,可以恢复到过去一个已知正确的时间点。HDFS目前还不支持快照功能。

八、数据组织
1、数据块
    兼容HDFS的应用都是处理大数据集合的。这些应用都是写数据一次,读却是一次到多次,并且读的速度要满足流式读。HDFS支持文件的write- once-read-many语义。一个典型的block大小是64MB,因而,文件总是按照64M切分成chunk,每个chunk存储于不同的 Datanode
2、步骤
    某个客户端创建文件的请求其实并没有立即发给Namenode,事实上,HDFS客户端会将文件数据缓存到本地的一个临时文件。应用的写被透明地重定向到 这个临时文件。当这个临时文件累积的数据超过一个block的大小(默认64M),客户端才会联系NamenodeNamenode将文件名插入文件系 统的层次结构中,并且分配一个数据块给它,然后返回Datanode的标识符和目标数据块给客户端。客户端将本地临时文件flush到指定的 Datanode上。当文件关闭时,在临时文件中剩余的没有flush的数据也会传输到指定的Datanode,然后客户端告诉Namenode文件已经 关闭。此时Namenode才将文件创建操作提交到持久存储。如果Namenode在文件关闭前挂了,该文件将丢失。
   上述方法是对通过对HDFS上运行的目标应用认真考虑的结果。如果不采用客户端缓存,由于网络速度和网络堵塞会对吞估量造成比较大的影响。

3、流水线复制
    当某个客户端向HDFS文件写数据的时候,一开始是写入本地临时文件,假设该文件的replication因子设置为3,那么客户端会从Namenode 获取一张Datanode列表来存放副本。然后客户端开始向第一个Datanode传输数据,第一个Datanode一小部分一小部分(4kb)地接收数 据,将每个部分写入本地仓库,并且同时传输该部分到第二个Datanode节点。第二个Datanode也是这样,边收边传,一小部分一小部分地收,存储 在本地仓库,同时传给第三个Datanode,第三个Datanode就仅仅是接收并存储了。这就是流水线式的复制。

九、可访问性
    HDFS给应用提供了多种访问方式,可以通过DFSShell通过命令行与HDFS数据进行交互,可以通过java API调用,也可以通过C语言的封装API访问,并且提供了浏览器访问的方式。正在开发通过WebDav协议访问的方式。具体使用参考文档。
十、空间的回收
1、文件的删除和恢复
    用户或者应用删除某个文件,这个文件并没有立刻从HDFS中删除。相反,HDFS将这个文件重命名,并转移到/trash目录。当文件还在/trash目 录时,该文件可以被迅速地恢复。文件在/trash中保存的时间是可配置的,当超过这个时间,Namenode就会将该文件从namespace中删除。 文件的删除,也将释放关联该文件的数据块。注意到,在文件被用户删除和HDFS空闲空间的增加之间会有一个等待时间延迟。
    当被删除的文件还保留在/trash目录中的时候,如果用户想恢复这个文件,可以检索浏览/trash目录并检索该文件。/trash目录仅仅保存被删除 文件的最近一次拷贝。/trash目录与其他文件目录没有什么不同,除了一点:HDFS在该目录上应用了一个特殊的策略来自动删除文件,目前的默认策略是 删除保留超过6小时的文件,这个策略以后会定义成可配置的接口。

2Replication因子的减小
    当某个文件的replication因子减小,Namenode会选择要删除的过剩的副本。下次心跳检测就将该信息传递给DatanodeDatanode就会移除相应的block并释放空间,同样,在调用setReplication方法和集群中的空闲空间增加之间会有一个时间延迟。

参考资料:
HDFS Java API: http://hadoop.apache.org/core/docs/current/api/
HDFS source code: http://hadoop.apache.org/core/version_control.html
   







posted @ 2008-06-05 14:29 dennis 阅读(53354) | 评论 (25)编辑 收藏

    当将用scheme写的scheme求值器跑起来的时候,你不觉的兴奋是不可能的,真的太酷了,太magic了。
习题4.2,如果将application?判断放在define?判断之前,那么求值(define x 3)将把define当作一般的procedure应用于参数x和3,可是define是特殊的语法形式,而非一般过程,导致出错。
习题4.4,我的解答,eval增加两个判断:
 ((and? exp)
   (eval
-and (and-exps exp) env))
 ((or
? exp)
   (eval
-or (or-exps exp) env))
实现谓词和各自的过程:
(define (and? exp) 
  (tagged
-list? exp 'and))
(define (and-exps exp)
  (cdr exp))
(define (eval
-and exps env)
  (cond ((
null? exps)
          
'true)
        (else
          (let ((result (eval (car exps) env)))
            (
if (not result)
                result
                (eval
-and (cdr exps) env))))))
(define (or
? exp)
  (tagged
-list? exp 'or))
(define (or-exps exp) (cdr exp))
(define (eval
-or exps env)
  (cond ((
null? exps)
         
'false)
        (else
         (let ((result (eval (car exps) env)))
           (
if result
               result
               (eval
-or (cdr exps) env))))))

如果用宏实现就简单了:
(define-syntax and
  (syntax
-rules ()
      ((_) #t)
      ((_ e) e)
      ((_ e1 e2 e3 )
        (
if e1 (and e2 e3 ) #f))))
(define
-syntax or
   (syntax
-rules ()
             ((_) #f)
             ((_ e) e)
             ((_ e1 e2 e3 )
              (let ((t e1))
                  (
if t t (or e2 e3 ))))))

习题4.5,cond的扩展形式,也不难,在else子句之外增加个判断,是否带有=>符号,修改expand-clauses过程:
(define (cond-extended-clauses? clause)
  (and (> (length clause) 2) (eq? (cadr clause) '=>)))
(define (extended-cond-test clause)
  (car clause))
(define (extended-cond-recipient clause)
  (caddr clause)
(define (expand
-clauses clauses)
  (
if (null? clauses)
      
'false
      (let ((first (car clauses))
            (rest (cdr clauses)))
        (cond ((cond
-else-clauses? first)
                (
if (null? rest)
                    (sequence
->exp (cond-actions first))
                    (error 
"else clause is not LAST" clauses)))
              ((cond
-extended-clauses? first)  ;判断是否扩展形式
               (make
-if
                   (extended
-cond-test first)
                    (list
                      (extended
-cond-recipient first)
                      (extended
-cond-test first))
                      (expand
-clauses rest)))
              (
else
               (make
-if (cond-predicate first)
                        (sequence
->exp (cond-actions first))
                        (expand
-clauses rest)))))))

习题4.6,let如果用宏定义,类似这样:
(define-syntax let
  (syntax
-rules ()
    ((_ ((x v) ) e1 e2 )
     ((
lambda (x ) e1 e2 ) v ))))
求值器扩展,实现let->combination过程:
(define (let? exp)
  (tagged
-list? exp 'let))
(define (let->combination exp)
  (let ((vars (map car (cadr exp)))
        (vals (map cadr (cadr exp)))
        (body (caddr exp)))
  (cons (make
-lambda vars (list body)) vals)))
我们做的仅仅是syntax transform,将let转成对应的lambda形式。

习题4.7,这里的关键在于let*->netsted-lets要递归调用,从let*的宏定义可以看出:
(define-syntax let*
     (syntax
-rules()
       ((_ ((var1 val1)) e1 )
        (let ((var1 val1)) e1 ))
       ((_ ((var1 val1) (var2 val2) ) e1 )
        (let ((var1 val1))
          (let
* ((var2 val2) )
            e1 )))))
那么,let*->nested-lets可以定义为:
(define (let*? exp)
  (tagged
-list? exp 'let*))
(define (let*->nested-lets exp)
     (let ((pairs (cadr exp))
           (body (caddr exp)))
       (
if (null? (cdr pairs))
           (list 
'let pairs body)
           (list 'let (list (car pairs)) (let*->nested-lets (list 'let* (cdr pairs) body))))))
测试一下:
(let* ((x 1) (y (+ x 3))) (+ x y)) =》5

习题4.8,命名let,修改let->combination过程,判断cadr是pair还是symbol,如果是前者,那就是一般的let,如果是symbol就是命名let语句,那么需要定义一个名为(cadr exp)的过程放在body里,注意,我们是在做语法转换,因此,这个定义也应该描述成符号,定义一个make-define过程来生成define语句:
(define (make-define var parameters body)
  (list 
'define (cons var parameters) body))
然后修改let->combination过程,如上所述:
(define (let->combination exp)
  (
if (symbol? (cadr exp))
      (let ((var (cadr exp))
            (vars (map car (caddr exp)))
            (vals (map cadr (caddr exp)))
            (pairs (caddr exp))
            (body (cadddr exp)))
        (cons (make
-lambda vars (list (make-define var vars body) body)) vals))
      (let ((vars (map car (cadr exp)))
            (vals (map cadr (cadr exp)))
            (body (caddr exp)))
              (cons (make
-lambda vars (list body)) vals))))


习题4.1.4,原生的map过程接受的procedure,是以scheme内在数据结构表示的procedure,而在我们的求值器中,procedure的内在数据结构取决于我们,与原生的结构不同,导致原生的map无法接受自制求值器的procedure,如果map也以求值器中的结构定义,那么就没有这个问题。因此,本质上的困难就在于两个求值器对procedure的数据结构表示的不同。

习题4.1.5,著名的图灵停机问题,先是假设存在halts?过程可以正确地判断任何过程p和对象a是否p对a终止,定义了try过程:
(define (try p)
   (if (halts? p p)
       (run-forever)
        'halted))
当执行(try try),如果这个过程可终止,那么(halts? try try)应该返回false,也就是try过程对try不会终止,这与一开始的假设矛盾;如果这个过程将无穷运行下去,那么(halts? try try)应该返回true,也就是try对try终止,这也与(try try)将无穷运行的假设矛盾。因此,可以推论出,我们不可能写出一个过程halts?,使它能正确地判断任何过程p和对象a是否p对a终止。

posted @ 2008-06-01 15:51 dennis 阅读(1024) | 评论 (0)编辑 收藏

第一个程序:
import java.util.ArrayList;
import java.util.List;

public class TailRecursionTest {
    
public static void main(String[] args) {
        TailRecursionTest t 
= new TailRecursionTest();
        
for (int i = 0; i < 10000; i++)
            t.a(
0);
    }

    
public void a(int j) {
        j
++;
        List list 
= new ArrayList<Integer>(100000);
        
// 对list进行处理
    }
}
    没啥特殊的,仅仅是为了测试,我们将a方法调用10000次,a方法创建一个有100000个元素的list的局部变量。
第二个程序:
import java.util.ArrayList;
import java.util.List;

public class TailRecursionTest2 {
    
public static void main(String[] args) {
        TailRecursionTest2 t 
= new TailRecursionTest2();
        t.a(
0);
    }

    
public void a(int j) {
        System.out.println(j);
        j
++;
        
if (j == 10000)
            
return;
        List list 
= new ArrayList<Integer>(100000);
        
// 对list进行处理
        a(j);
    }
}

    也没啥特殊的,就是将循环换成了递归,a方法做的事情没变。两个都跑一下,程序1顺利结束,程序2出问题了,啥问题?如下:
161
162
163
164
165
Exception in thread 
"main" java.lang.OutOfMemoryError: Java heap space
    at java.util.ArrayList.
<init>(Unknown Source)
    at TailRecursionTest2.a(TailRecursionTest2.java:
17)
    at TailRecursionTest2.a(TailRecursionTest2.java:
20)
    at TailRecursionTest2.a(TailRecursionTest2.java:
20)
    at TailRecursionTest2.a(TailRecursionTest2.java:
20)
    at TailRecursionTest2.a(TailRecursionTest2.java:
20)
   我倒,才运行166次了,heap就满了。问题在哪呢?oh,yep,你肯定想到了,是不是重复创建list这个大集合引起的呢?它不是局部变量吗?怎么也会溢出?是的,list是局部变量,在a的方法栈里引用着,指向heap上的大对象,更关键的问题在于,java是没有尾递归优化的,递归方法是不会使用同一个栈帧,每一次递归调用,都将压入新的栈帧,并且这个栈帧上又new了一个list变量,引用着heap上新的一个大集合。随着栈深度的增加,jvm里维持着一条长长的方法调用轨迹以便你能回来,在方法没有返回之前,这些list变量一直被各自的栈帧引用着,不能被GC,你说,能不OOM吗?
    也许,你想到了个补救方法来挽救程序2,就是每次在处理完list后,我把它设置为null,不让栈帧继续引用着它,咱编写对gc友好的代码,这不就行了,试试:
import java.util.ArrayList;
import java.util.List;

public class TailRecursionTest2 {
    
public static void main(String[] args) {
        TailRecursionTest2 t 
= new TailRecursionTest2();
        t.a(
0);
    }

    
public void a(int j) {
        System.out.println(j);
        j
++;
        
if (j == 10000)
            
return;
        List list 
= new ArrayList<Integer>(100000);
        
// 对list进行处理
        list = null;  //gc友好
        a(j);
    }
}
    得意洋洋,我跑一下看看,这次跑到4000多次,但是:
......
4289

4290
4291
4292
java.lang.StackOverflowError
    at sun.nio.cs.ext.DoubleByteEncoder.encodeArrayLoop(Unknown Source)
    at sun.nio.cs.ext.DoubleByteEncoder.encodeLoop(Unknown Source)
    at java.nio.charset.CharsetEncoder.encode(Unknown Source)
    没办法啊,人家sun的jdk就是不支持尾递归优化,很不给你面子的栈溢出了。ibm的jdk据说支持尾递归优化,上面这个程序在ibm的jdk上可能可以正常结束,未经测试。

总结:在java里,递归最好咱还是别用,老老实实地while、for;就算递归了,最好递归方法不要new太大的对象,除非你能确定递归的深度不是那么大。

posted @ 2008-05-31 17:25 dennis 阅读(2136) | 评论 (4)编辑 收藏

     摘要: 6到11章  阅读全文

posted @ 2008-05-28 23:07 dennis 阅读(4079) | 评论 (0)编辑 收藏

     摘要: 《Logic Programming with Prolog》的读书笔记,书其实一般,内容都是基础性的语法和BIPs介绍,比较失望的是对Prolog的应用涉及很少。  阅读全文

posted @ 2008-05-28 23:05 dennis 阅读(4046) | 评论 (0)编辑 收藏

    去年申请的rubyeye.net,年初到期了,忘了去续费。今天又去申请了两个:rubyeye.net和zhuangxiaodan.cn。cn域名就是便宜,一块钱一个:)。rubyeye.net申请了两年,以免再次忘记续费,总计花费98块,还是挺划算的。两个都设置了url转发,现在可以通过 www.zhuangxiaodan.cn和www.rubyeye.net访问我的blog啦。貌似rubyeye.net的还没生效,明天应该可以了。前天同事推荐了个ror的虚拟主机,一年200块,300M空间,10G/月的流量,挺实惠,就想着搞个typo弄上去,搬家过去。可回想起前面几次搬家的经历,心里立马打退堂鼓,尽管可以去搞个脚本导入这些日志,也是够麻烦的,我还得去看下typo的数据表结构。看来还是老老实实呆在blogjava为妙。

posted @ 2008-05-27 16:43 dennis 阅读(656) | 评论 (3)编辑 收藏

    今天整理代码,发现一个去年写的简单的工作流引擎,基于petri网(参考这里的笔记),实现了顺序、并行、循环和选择四种路由,资源也实现了人工驱动和定时、延迟时间驱动;目前只实现了将工作流数据保存在内存的版本,然后就换工作,折腾着就忘了这个事儿,本来是计划加入数据库存储的。尽管只是个toy,可能对工作流感兴趣,或者想自己实现一个玩玩的朋友有参考价值,放到了google code上,svn地址:
 http://insectworkflow.googlecode.com/svn/trunk/

    源码中有在example包下给了个请假的例子,流程定义文件就是processes包下的leave.xml,实现大概是这么个流程:
填写假单-》提交假单-and-split节点-》项目经理审批-》and-join节点-》结束
                                                     -》部门经理审批-》

其中项目经理审批和部门经理审批是并行路由。xml配置大概这样:
<node type="and-split" name="and-split" id="2">
        
<inputs>
            
<place id="3" />
        
</inputs>
        
<outputs>
            
<place id="4" />
            
<place id="5" />
        
</outputs>
    
</node>
    
<node name="dept_manager_confirm" id="3">
        
<resource class="com.google.code.insect.workflow.impl.Group" id="2"
            name
="dept_manager">
        
</resource>
        
<conditions type="and">
            
<condition
                
class="com.google.code.insect.workflow.impl.NullHandler" value="false"
                variable-name
="LeaveInfo" />
        
</conditions>
        
<handler
            
class="com.google.code.insect.workflow.example.leave.SendRemindHandler" />
        
<inputs>
            
<place id="4" />
        
</inputs>
        
<outputs>
            
<place id="6" />
        
</outputs>
    
</node>
    
<node name="project_manager_confirm" id="4">
        
<resource class="com.google.code.insect.workflow.impl.Group" id="3"
            name
="project_manager">
        
</resource>
        
<conditions type="and">
            
<condition
                
class="com.google.code.insect.workflow.impl.NullHandler" value="false"
                variable-name
="LeaveInfo" />
        
</conditions>
        
<handler
            
class="com.google.code.insect.workflow.example.leave.SendRemindHandler" />
        
<inputs>
            
<place id="5" />
        
</inputs>
        
<outputs>
            
<place id="7" />
        
</outputs>
    
</node>
    
<node type="and-join" name="and-join" id="5">
        
<handler
            
class="com.google.code.insect.workflow.example.leave.ResultHandler" />
        
<inputs>
            
<place id="6" />
            
<place id="7"></place>
        
</inputs>
        
<outputs>
            
<place id="8" />
        
</outputs>
    
</node>

    其中的place就是各个Transition的输入或者输出库所,所谓node其实就是变迁(transition),每个变迁对应一个handler,执行具体的业务操作,比如这里的com.google.code.insect.workflow.example.leave.SendRemindHandler 用于发送提醒消息给经理们。

    具体调用和工作项的人工触发:

//初始化工作流管理器
WorkFlowManager wm = new BasicWorkflowManager();
wm.setConfiguration(
new DefaultConfiguration());

//启动一个案例
Token token = wm.startWorkFlow("leave");
token.setAttribute(
"LeaveInfo", leaveInfo);

//提交假单
wm.doAction(token.getId(), this.dennis, "给领导发送消息:"
                
+ leaveInfo.getStaff_name() + "申请请假,请批准!");
//将token的id传递给后续节点做处理。。token的id就是案例id
    processes包下面的流程定义文件和test包下的TestUnit,分别测试了四种路由和定时、延时触发,有兴趣的可以看一下。

posted @ 2008-05-21 15:09 dennis 阅读(2138) | 评论 (0)编辑 收藏

    转眼间,来广州快半年了,感觉还不错。广州如死鱼所说的那样,是个包容并且很有活力的城市,习惯了周末煲汤,去天河公园跑跑步,这生活还是挺舒适的,除了比较潮的天气。
    最近跟公司闹了点不愉快,在转正时间上,其实不是多大的事,只是心里不舒服罢了,干起活来也没什么激情了,呵呵。当然,手头的工作咱还是要高效率地完成,做完两个游戏后,现在转到棋牌类,棋牌类游戏核心就两个算法:随机发牌和出牌判断。随机发牌算法,学习了云风的blog上提到的方法,感觉还可以接受;出牌规则判断,倒是没想象中的复杂,建立牌型的OO模型,一切都很简单了。另外一个发现,用jdk6跑jruby1.1.1,比用jdk5跑效率(包括内存和CPU)好上很多,例如我们一个游戏进程,在使用jdk5时,CPU稳定在15%,内存85M,而改用jdk6后,cpu降到了%5以下,内存也缩减到70多M。
    搞完了之后,时间有点空闲,就想学点新东西,最后选择了Prolog。Yep,非常地有趣,真正的声明式编程语言。Prolog本质上就是两个东西:规则和事实,由事实和规则出发,Prolog的推理系统将回答你的查询(query)。有点类似现在流行中的规则引擎的概念,在对效率不是很考虑的场景中,嵌入一个Prolog引擎做规则引擎完全是可以的,java中有个tuProlog项目,可以关注一下。然后就是一直在读的sicp,延时求值模拟无穷级数实在是相当地cool,大开眼界。这两天一直在理解continuation这个概念,小有所得。一个表达式的求值可以分为两个阶段:“What to evaluate?”和“What to do with the value”,“What to do with the value”就是计算的Continuation。例如,scheme求值下列表达式:
(if (null? x) (quote ()) (cdr x))
先求值表达式(null? x),(null? x)就是“What to evaluate”,当(null? x)求值后,需要根据这个值来决定是执行(quote ())还是(cdr x),这个根据值来决定的过程就是Continuation。如果在每次函数调用时,同时传入当前的continuation,那么就完全可以不要堆栈。call/cc就提供了这样的一个语法糖,call/cc全称就是call-with-current-continuation,要求参数是一个过程,调用这个过程,并且向这个过程传入当前的continuation(一般称为k,kont,或者Ruby中一般是c,cont),这就是call/cc为我们做的。call/cc是实现Continuation的方式之一,coroutine/fiber/yield也是实现continuation的方式。《The Scheme Programming Language》给出的轻量级进程机制的例子比较有趣:
(define lwp-list '())
(define (lwp thunk)
  (set! lwp
-list (append lwp-list (list thunk))))
(define start
  (
lambda()
    (let ((p (car lwp
-list)))
      (set! lwp
-list (cdr lwp-list))
      (p))))
(define pause
  (
lambda()
    (callcc (
lambda(k) 
               (lwp (
lambda () (k #f)))
               (start)))))
(lwp (
lambda () (let f () (display "h") (pause) (f))))
(lwp (
lambda () (let f () (display "e") (pause) (f))))
(lwp (
lambda () (let f () (display "y") (pause) (f))))
(lwp (
lambda () (let f () (display "!") (pause) (f))))
(lwp (
lambda () (let f () (newline) (pause) (f))))
(start)
实现了代码级的进程调度。

posted @ 2008-05-21 11:45 dennis 阅读(1930) | 评论 (4)编辑 收藏

仅列出标题
共56页: First 上一页 25 26 27 28 29 30 31 32 33 下一页 Last