庄周梦蝶

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


    今年在阅读某个项目源码的时候看到DelayQueue的使用,xmemcached 1.2.6.1的重连任务也是采用DelayQueue管理,ReconnectRequest实现Delayed接口,我突然想起去review下xmc的源码,发现一个严重的BUG,原始代码如下:
public final class ReconnectRequest implements Delayed {
      
public long getDelay(TimeUnit unit) {
        
return  nextReconnectTimestamp - System.currentTimeMillis();
    }
}

    getDelay返回该任务还剩下多少时间可以被执行,将下次执行的时间戳减去当前时间即可,问题在于这里返回的是毫秒,而没有调用getDelay传入的TimeUnit做转换,在DelayQueue内部其实是用纳秒做单位交给Condition对象去等待
  for (;;) {
                E first 
= q.peek();
                
if (first == null) {
                    available.await();
                } 
else {
                    
long delay =  first.getDelay(TimeUnit.NANOSECONDS);
                    
if (delay > 0) {
                        
long tl = available.awaitNanos(delay);
                    } 
else {
                        E x 
= q.poll();
                        
assert x != null;
                        
if (q.size() != 0)
                            available.signalAll(); 
// wake up other takers
                        return x;

                    }
                }
            }
  
     最终导致的问题是,awaitNanos很快返回(awaitNanos接受的是纳秒,这里却传入毫秒),循环执行发现重新计算的delay仍然大于0,循环等到getDelay返回的越来越小直到0才执行相应的Task,,造成的现象是在重连的时候cpu占用率很高。

     单元测试的时候没有发现这个问题,主要是因为功能正常,没有关注资源消耗情况,因此惭愧地忽略了。

      解决的办法很简单,修改getDelay方法即可:
    public long getDelay(TimeUnit unit) {
        
return unit.convert(
                nextReconnectTimestamp 
- System.currentTimeMillis(),
                TimeUnit.MILLISECONDS);
    }

      这个BUG比较严重,已经升级1.2.6.1的朋友建议马上升级到1.2.6.2,使用maven的朋友只要修改版本即可,没有使用maven的请到这里下载。

posted @ 2010-10-22 15:52 dennis 阅读(4684) | 评论 (4)编辑 收藏


    NIO中的Selector封装了底层的系统调用,其中wakeup用于唤醒阻塞在select方法上的线程,它的实现很简单,在linux上就是创建一个管道并加入poll的fd集合,wakeup就是往管道里写一个字节,那么阻塞的poll方法有数据可读就立即返回。证明这一点很简单,strace即可知道:
public class SelectorTest {
    
public static void main(String[] args) throws Exception {
        Selector selector 
= Selector.open();
        selector.wakeup();
    }
}

     使用strace调用,只关心write的系统调用
sudo strace --e write java SelectorTest

     输出:
Process 29181 attached
Process 
29182 attached
Process 
29183 attached
Process 
29184 attached
Process 
29185 attached
Process 
29186 attached
Process 
29187 attached
Process 
29188 attached
Process 
29189 attached
Process 
29190 attached
Process 
29191 attached
[pid 
29181] write(36"\1"1)          = 1
Process 
29191 detached
Process 
29184 detached
Process 
29181 detached

    有的同学说了,怎么证明这个write是wakeup方法调用的,而不是其他方法呢,这个很好证明,我们多调用几次:
public class SelectorTest {
    
public static void main(String[] args) throws Exception {
        Selector selector 
= Selector.open();
        selector.wakeup();
        selector.selectNow();
        selector.wakeup();
        selector.selectNow();
        selector.wakeup();
    }
}

    修改程序调用三次wakeup,心细的朋友肯定注意到我们还调用了两次selectNow,这是因为在两次成功的select方法之间调用wakeup多次都只算做一次,为了显示3次write,这里就每次调用前select一下将前一次写入的字节读到,同样执行上面的strace调用,输出:

Process 29303 attached
Process 
29304 attached
Process 
29305 attached
Process 
29306 attached
Process 
29307 attached
Process 
29308 attached
Process 
29309 attached
Process 
29310 attached
Process 
29311 attached
Process 
29312 attached
Process 
29313 attached
[pid 
29303] write(36"\1"1)          = 1
[pid 
29303] write(36"\1"1)          = 1
[pid 
29303] write(36"\1"1)          = 1
Process 
29313 detached
Process 
29309 detached
Process 
29306 detached
Process 
29303 detached

     果然是3次write的系统调用,都是写入一个字节,如果我们去掉selectNow,那么三次wakeup还是等于一次:
public class SelectorTest {
    
public static void main(String[] args) throws Exception {
        Selector selector 
= Selector.open();
        selector.wakeup();
        selector.wakeup();
        selector.wakeup();
    }
}
 
   输出:
Process 29331 attached
Process 
29332 attached
Process 
29333 attached
Process 
29334 attached
Process 
29335 attached
Process 
29336 attached
Process 
29337 attached
Process 
29338 attached
Process 
29339 attached
Process 
29340 attached
Process 
29341 attached
[pid 
29331] write(36"\1"1)          = 1
Process 
29341 detached
Process 
29337 detached
Process 
29334 detached
Process 
29331 detached

      wakeup方法的API说明没有欺骗我们。wakeup方法的API还告诉我们,如果当前Selector没有阻塞在select方法上,那么本次wakeup调用会在下一次select阻塞的时候生效,这个道理很简单,wakeup方法写入一个字节,下次poll等待的时候立即发现可读并返回,因此不会阻塞。

     具体到源码级别,在linux平台上的wakeup方法其实调用了pipe创建了管道,wakeup调用了EPollArrayWrapperinterrupt方法:
public  void interrupt() 

{
        interrupt(outgoingInterruptFD);
}

    实际调用的是interrupt(fd)的native方法,查看EPollArrayWrapper.c可见清晰的write系统调用:

JNIEXPORT 
void JNICALL
Java_sun_nio_ch_EPollArrayWrapper_interrupt(JNIEnv 
*env, jobject this, jint fd)
{
    
int fakebuf[1];
    fakebuf[
0= 1;
    
if (write(fd, fakebuf, 1< 0) {
        JNU_ThrowIOExceptionWithLastError(env,
"write to interrupt fd failed");
    }
}
    写入一个字节的fakebuf。有朋友问起这个问题,写个注记在此。strace充分利用对了解这些细节很有帮助。
 

posted @ 2010-10-22 10:35 dennis 阅读(6203) | 评论 (3)编辑 收藏


     Xmemcached 1.2.6.1正式发布,这个版本的主要是做bug fix以及一些细节改进,主要变动如下:

1、修复BUG,包括:
Issue 85: 当存在多个MemcachedClient的时候,JMX的统计只显示其中一个
Issue 87: 当使用一致性哈希的时候,连接池不起作用
Issue 90: 用户线程中断引起的连接关闭
Issue 94:BinaryMemcachedClientUnitTest测试失败
Issue 95: JMX addServer,removeServer存在缺陷
Issue 96: OOM Error while decompressing 60 KB of actuall data
Issue 97: 使得关闭连接更友好

2、改进重连机制,重连不再是以固定间隔(默认2秒)做重试连接,而是以一个等差数列递增间隔时间,第一次2秒,第二次4秒,第三次6秒……直到最大间隔时间1分钟做重连尝试。

3、改善关闭机制,关闭连接前发送quit命令,尽量做到友好关闭,等待服务器主动断开连接。

4、添加新的方法用于设置XmemcachedClient实例名称,用于区分不同的缓存集群,方便统计显示:
public interface MemcachedClient{
       
/**
     * Return the cache instance name
     * 
     * 
@return
     
*/
    
public String getName();

    
/**
     * Set cache instance name
     * 
     * 
@param name
     
*/
    
public void setName(String name);
}

名称也可通过spring配置。

5、提供英文的用户指南,非常感谢cnscud的帮助,这份文档是他一人搞定的。

6、更新了Java Memcached Client Benchmark,跟最新版本的spymemcached,Java-Memcached-Client做对比测试,提供给需要的朋友参考。


项目首页  http://code.google.com/p/xmemcached/
下载地址  http://code.google.com/p/xmemcached/downloads/list
wiki地址   http://code.google.com/p/xmemcached/w/list




posted @ 2010-10-17 14:06 dennis 阅读(2436) | 评论 (0)编辑 收藏


     Xmemcached 1.2.6.1 released,所以更新了一下Java Memcached Client Benchmark。对比下Xmemached,SpymemcachedJava-Memcached-Client这三个开源客户端的性能,具体的测试信息可以看这个链接

    测试源码
svn co http://xmemcached.googlecode.com/svn/trunk/benchmark/
    测试结果:
svn co http://xmemcached.googlecode.com/svn/trunk/benchmark/result

    总结下测试结果,为还在选择和考察java memcached client的朋友提供参考:

1、Java-Memcached-Client 2.5.1这个版本果然有很大改进,性能上有非常大的提升,从测试结果来看在小于100并发下有非常明显的优势,同时耗费资源也相对较多。但是在300并发访问下,Java-Memcached-Client会不断地报错:
com.schooner.MemCached.SchoonerSockIOPool Sun Oct 17 11:09:05 GMT+08:00 2010 - ++++ failed to get SockIO obj for10.232.36.82:12000
com.schooner.MemCached.SchoonerSockIOPool Sun Oct 17 11:09:05 GMT+08:00 2010 - ++++ failed to get SockIO obj for: 10.232.36.82:12000
com.schooner.MemCached.SchoonerSockIOPool Sun Oct 17 11:09:05 GMT+08:00 2010 - ++++ failed to get SockIO obj for: 10.232.36.90:12000
com.schooner.MemCached.SchoonerSockIOPool Sun Oct 17 11:09:05 GMT+08:00 2010 - ++++ failed to get SockIO obj for: 10.232.36.82:12000
com.schooner.MemCached.SchoonerSockIOPool Sun Oct 17 11:09:05 GMT+08:00 2010 - ++++ failed to get SockIO obj for: 10.232.36.90:12000
com.schooner.MemCached.SchoonerSockIOPool Sun Oct 17 11:09:05 GMT+08:00 2010 - ++++ failed to get SockIO obj for: 10.232.36.82:12000


 并且无法正常地存取数据, 而xmc和spy却可以正常应对这一场景。因此可以看到在300并发下,Java-Memcached-Client测试的结果直接为0,因为测试无法完成。尽管我尝试将最大连接数调到2000,仍然是无法完成测试。

2、Xmemcached无论在低并发还是高并发访问的情况下,都可以保持一个比较优秀的性能表现,从xmc和spy的对比来看,xmc的优势相当大。

3、从用户选择角度来说,如果你的应用对memached的访问负载并不高,Java-Memcached-Client是一个不错的选择,但是在高峰访问的时候可能命中率会有个急剧的波动;如果你的应用访问memached的负载较高,此时我推荐你选择xmemcached;如果你需要异步的批量处理(future模式),可以选择spymemcached;如果你不知道你的应用是什么状况,我推荐你用xmemcached,可以在任何情况下获得一个比较好的性能表现


posted @ 2010-10-17 13:44 dennis 阅读(4181) | 评论 (0)编辑 收藏


    Kilim是一个Java的actor框架,让你可以在JVM里使用基于协程的actor模型,bluedavy曾经介绍过,这里不再赘言。这篇blog的目的在于分析下kilim实现的基本原理,看看怎么在JVM上实现协程。

    在一些语言层面上支持协程的语言,如lua、ruby,都是直接在VM级别支持协程,VM帮你做context的保存和恢复。JVM没有提供这样的指令来保存和恢复方法栈的状态,因此kilim的实现还是需要在bytecode级别做文章。首先,试想下,如果是你来实现协程,你会怎么做?协程的两个基本原语resume和yield,resume运行协程,yield让出执行权,下次resume的时候会从yield的地方重新执行,并且context保持不变。可见,你需要做这么几个事情:
1、在yield的时候保存当前context。
2、在resume的时候恢复context,并根据pc计数来决定从哪里恢复执行。
3、半协程的实现来说,还需要一个调度器来调度所有协程。
4、为了做到用户代码透明,可能需要某种手段去修改用户代码,自动帮你做上面三个事情。

    kilim的实现就是干了这么几个事情:
1、利用字节码增强,将普通的java代码转换为支持协程的代码。
2、在调用pausable方法的时候,如果pause了就保存当前方法栈的State,停止执行当前协程,将控制权交给调度器
3、调度器负责调度就绪的协程
4、协程resume的时候,自动恢复State,根据协程的pc计数跳转到上次执行的位置,继续执行。


    下面是来自kilim文档的一个例子,同一段代码,在字节码增前前后的变化:

   
左边是原始代码,右边是通过字节码增强后的代码。其中h方法是pausable的,也就是说可能被暂停阻塞的,g方法因为调用了h这个方法也变成了pausable。

首先看,原始的h方法是没有传入任何参数的,增强后的代码,多了个参数Fiber,指向当前的协程,同样,g方法本来只有一个参数n,现在在后面也多了个Fiber类型的参数,同样是指向当前执行的协程。

其次,在原始的g方法里,一旦调用,马上进入一个for循环。但是在增强后的代码,多了个switch派发的过程,这就是前面提到的,根据当前的Fiber的pc计数,跳转到上一次执行的地方执行。如果是第一次resume,也就是启动协程,那么就将初始循环的i设置为0,进入原始代码的循环部分。Fiber有一个pc计数,称为程序计数器,用于指向恢复context的时候需要跳转到位置。

第三,在g方法里调用h这个可被暂停阻塞的方法的时候,在h方法前后多了一些调用:
f.down();
h(f);
f.up();

kilim的Fiber将每个pauseable方法的调用组织成一个栈,每个pauseable方法都有一个activation frame,翻译过来可以称为活动栈帧,这个栈帧记录了当前的栈的State,注意这个栈跟java本身的方法调用栈区分开来,一个是VM层面的,一个是kilim框架层面的。这里的down方法就是将栈向下延伸,表示将调用一个pauseable方法,并且设置当前State和pc计数。
调用了down之后,才是调用实际的h方法,最后还要调用一次up,顾名思义,就是说一次pauseable方法调用完成,fiber的活动栈要递增一层,回到上一层。但是h方法调用可能出现四种情况:
1、正常的顺利返回,没有状态需要恢复,所谓NOT_PAUSING__NO_STATE
2、也是正常返回,有状态需要恢复,也就是NOT_PAUSING__HAS_STATE
3、h方法暂停阻塞,当前没有保存状态,需要保存状态,这是第一次暂停的时候,称为PAUSING__NO_STATE
4、h方法暂停阻塞,当前已经有状态,不需要保存状态,这是第一次暂停之后的resume再次暂停,称为PAUSING__HAS_STATE,通常不需要处理什么。

第四,可以看到,在up之后,就要根据up返回的上述4种状态执行不同的逻辑:
if (f.isPausing){
    
//第一次暂停,没有状态
   if (!f.hasState){
     
//new一个State_I2,并保存i和n
     f.state = new State_I2(i,n);
    
//记录pc,还记的前面的switch吗?
     f.pc = H1;
   }
   
return;
else if (f.hasState)
   
//正常返回,有状态需要恢复,恢复i和n
   State_I2 st = (State_I2) f.state;
   i 
= st.i1; n = st.i2;
}


这里没有处理NOT_PAUSING__NO_STATE和PAUSING__HAS_STATE,因为这两种情况在这里不需要处理。


    通过上面的分析,我想大家对kilim的实现应该已经有一个很基本的认识。下一步,我们分析一个实际的代码例子,查看整个运作流程。
 

posted @ 2010-09-17 12:05 dennis 阅读(10495) | 评论 (5)编辑 收藏


    java.util.LinkedList是双向链表,这个大家都知道,比如Java的基础面试题喜欢问ArrayList和LinkedList的区别,在什么场景下用。大家都会说LinkedList随机增删多的场景比较合适,而ArrayList的随机访问多的场景比较合适。更进一步,我有时候会问,LinkedList.remove(Object)方法的时间复杂度是什么?有的人回答对了,有的人回答错了。回答错的应该是没有读过源码。

    理论上说,双向链表的删除的时间复杂度是O(1),你只需要将要删除的节点的前节点和后节点相连,然后将要删除的节点的前节点和后节点置为null即可,
//伪代码
node.prev.next=node.next;
node.next.prev
=node.prev;
node.prev
=node.next=null;

这个操作的时间复杂度可以认为是O(1)级别的。但是LinkedList的实现是一个通用的数据结构,因此没有暴露内部的节点Entry对象,remove(Object)传入的Object其实是节点存储的value,这里还需要一个查找过程:
  public boolean remove(Object o) {
        
if (o==null) {
            
for (Entry<E> e = header.next; e != header; e = e.next) {
                
if (e.element==null) {
                    remove(e);
                    
return true;
                }
            }
        } 
else {
            
//查找节点Entry
            for (Entry<E> e = header.next; e != header; e = e.next) {
                
if (o.equals(e.element)) {
                    
//删除节点
                    remove(e);
                    
return true;
                }
            }
        }
        
return false;
    }


    删除节点的操作就是刚才伪代码描述的:
   private E remove(Entry<E> e) {
        E result 
= e.element;
    e.previous.next 
= e.next;
    e.next.previous 
= e.previous;
        e.next 
= e.previous = null;
        e.element 
= null;
    size
--;
    modCount
++;
        
return result;
    }

    因此,显然,LinkedList.remove(Object)方法的时间复杂度是O(n)+O(1),结果仍然是O(n)的时间复杂度,而非推测的O(1)复杂度。最坏情况下要删除的元素是最后一个,你都要比较N-1次才能找到要删除的元素。

    既然如此,说LinkedList适合随机删减有个前提,链表的大小不能太大,如果链表元素非常多,调用remove(Object)去删除一个元素的效率肯定有影响,一个简单测试,插入100万数据,随机删除1000个元素:
        final List<Integer> list = new LinkedList<Integer>();
        
final int count = 1000000;
        
for (int i = 0; i < count; i++) {
            list.add(i);
        }
        
final Random rand=new Random();
        
long start=System.nanoTime();
        
for(int i=0;i<1000;i++){
            
//这里要强制转型为Integer,否则调用的是remove(int)
            list.remove((Integer)rand.nextInt(count));
        }
        System.out.println((System.nanoTime()
-start)/Math.pow(109));
   
    在我的机器上耗时近9.5秒,删除1000个元素耗时9.5秒,是不是很恐怖?注意到上面的注释,产生的随机数强制转为Integer对象,否则调用的是remove(int)方法,而非remove(Object)。如果我们调用remove(int)根据索引来删除:
        for(int i=0;i<1000;i++){
            list.remove(rand.nextInt(list.size()
-1));
        }
    随机数范围要递减,防止数组越界,换成remove(int)效率提高不少,但是仍然需要2.2秒左右(包括了随机数产生开销)。这是因为remove(int)的实现很有技巧,它首先判断索引位置在链表的前半部分还是后半部分,如果是前半部分则从head往前查找,如果在后半部分,则从head往后查找(LinkedList的实现是一个环):
        Entry<E> e = header;
        
if (index < (size >> 1)) {
            
//前一半,往前找
            for (int i = 0; i <= index; i++)
                e 
= e.next;
        } 
else {
            
//后一半,往后找
            for (int i = size; i > index; i--)
                e 
= e.previous;
        }

     最坏情况下要删除的节点在中点左右,查找的次数仍然达到n/2次,但是注意到这里没有比较的开销,并且比remove(Object)最坏情况下n次查找还是好很多。

    总结下,LinkedList的两个remove方法,remove(Object)和remove(int)的时间复杂度都是O(n),在链表元素很多并且没有索引可用的情况下,LinkedList也并不适合做随机增删元素。在对性能特别敏感的场景下,还是需要自己实现专用的双向链表结构,真正实现O(1)级别的随机增删。更进一步,jdk5引入的ConcurrentLinkedQueue是一个非阻塞的线程安全的双向队列实现,同样有本文提到的问题,有兴趣可以测试一下在大量元素情况下的并发随机增删,效率跟自己实现的特定类型的线程安全的链表差距是惊人的。

    题外,ArrayList比LinkedList更不适合随机增删的原因是多了一个数组移动的动作,假设你删除的元素在m,那么除了要查找m次之外,还需要往前移动n-m-1个元素。


   

posted @ 2010-09-16 13:51 dennis 阅读(7737) | 评论 (9)编辑 收藏

     摘要:     update:更正选择中数的描述,在7到39之间的数组大小选择median-of-three来选择pivot,大小等于7的数组则直接使用中数作为pivot。     quicksort可以说是应用最广泛的排序算法之一,它的基本思想是分治法,选择一个pivot(中轴点),将小于pivot放在左边,将大于pivot放在右边,针对...  阅读全文

posted @ 2010-09-08 19:10 dennis 阅读(19839) | 评论 (9)编辑 收藏


    Aviator是一个表达式执行引擎,最近由于工作上的原因,又将这个东西扩充了一下,加入了静态的编译优化和seq库。

    对于类似"1+2"这样由常量组成的表达式,会在编译的时候直接计算出结果而非生成字节码运行时计算。非常量组成的表达式如"3.14*R*R+4/2"也会在编译的时候优化成"3.14*R*R+2",也就是说能在编译的时候计算的都计算出来,不能在编译的时候确定的就生成字节码,运行时动态计算。默认不启用编译优化,除非设置:
AviatorEvaluator.setOptimize(AviatorEvaluator.EVAL);

    另外,加入了seq库用于操作集合和数组,在aviator中,你可以用[ ]操作符直接访问数组和java.util.List,除此之外seq库添加了一些对数组和集合的常用操作,示例如下:
map(seq,println)            //打印集合
map(seq,-)                  //取集合中元素的相反数组成的集合
include(seq,element)       //判断element是否在集合中
sort(seq)                  //排序,返回新的集合
reduce(seq,+,0)            //求和
reduce(seq,-,1)           //求积
filter(seq,seq.gt(3)      //大于3的元素组成的新集合
filter(seq,seq.exists())  //不为nil元素组成的新集合
count(seq)            //集合大小

可以看到seq库的风格偏向FP,但是能做的事情其实有限,毕竟aviator不是一门语言,seq库只提供了最常见的一些函数,其他的只有用户自己扩展了。

    Aviator的一个介绍PPT


    Aviator 1.0.1也已经放到maven的中心仓库,你可以直接引用:
        <dependency>
            
<groupId>com.googlecode.aviator</groupId>
            
<artifactId>aviator</artifactId>
           
<version>1.0.1</version>
        
</dependency>
  
    

posted @ 2010-09-07 14:22 dennis 阅读(4761) | 评论 (1)编辑 收藏

    详解clojure递归(上)
    详解clojure递归(下)
   
    这篇blog拖到现在才写,如果再不写,估计就只有上篇没有下篇了,趁周末写一下。

    上篇提到了Clojure仅支持有限的TCO,不支持间接的TCO,但是有一类特殊的尾递归clojure是支持,这就是相互递归。且看一个例子,定义两个函数用于判断奇数偶数:
(declare my-odd? my-even?)
(defn my
-odd? [n]
      (
if (= n 0)
          false
         (my
-even? (dec n))))
(defn my
-even? [n]
      (
if (= n 0)
          true
         (my
-odd? (dec n))))

    这里使用declare做前向声明,不然在定义my-odd?的时候my-even?还没有定义,导致出错。可以看到,my-odd?调用了my-even?,而my-even?调用了my-odd?,这是一个相互递归的定义:如果n为0,那肯定不是奇数,否则就判断n-1是不是偶数,反之亦然。

    这个递归定义看起来巧妙,但是刚才已经提到clojure通过recur支持直接的TCO优化,这个相互递归在大数计算上会导致堆栈溢出:
user=> (my-odd? 10000)
java.lang.StackOverflowError (NO_SOURCE_FILE:
0)

user=> (my-even? 10000)
java.lang.StackOverflowError (NO_SOURCE_FILE:0)


    怎么解决呢?一个办法是转化成一个直接递归调用,定义一个parity函数,当偶数的时候返回0,当奇数的时候返回1:
(defn parity [n]
      (loop [n n par 
0]
            (
if (= n 0)
                par
                (recur (dec n) (
- 1 par)))))
user=> (parity 3)
1
user=> (parity 100)
0
user=> (parity 10000)
0
user=> (parity 10001)
1
   

    parity是直接的尾递归,并且使用了recur优化,重新定义my-odd和my-even就很简单了:
(defn my-even? [n] (= 0 (parity n)))
(defn my
-odd?  [n] (= 1 (parity n)))
   
    但是这样的方式终究不是特别优雅,相互递归的定义简洁优雅,有没有什么办法保持这个优雅的定义并且避免堆栈溢出呢?答案是trampoline,翻译过来是弹簧床,查看trampoline函数文档:
user=> (doc trampoline)
-------------------------
clojure.core
/trampoline
([f] [f 
& args])
  trampoline can be used to convert algorithms requiring mutual
  recursion without stack consumption. Calls f with supplied args, 
if
  any. If f returns a fn, calls that fn with no arguments, and
  continues to repeat, until the 
return value is not a fn, then
  returns that non
-fn value. Note that if you want to return a fn as a
  
final value, you must wrap it in some data structure and unpack it
  after trampoline returns.
   简单来说trampoline接收一个函数做参数并调用,如果结果为函数,那么就递归调用函数,如果不是,则返回结果,主要就是为了解决相互递归调用堆栈溢出的问题,查看trampoline的定义:
(defn trampoline
  ([f]
     (let [ret (f)]
       (
if (fn? ret)
          (recur ret)
          ret)))

   看到trampoline利用recur做了尾递归优化,原有的my-odd?和my-even?需要做一个小改动,就可以利用trampoline来做递归优化了:
(declare my-odd? my-even?)
(defn my
-odd? [n]
      (
if (= n 0)
          
false
         #(my
-even? (dec n))))
(defn my
-even? [n]
      (
if (= n 0)
          
true
         #(my
-odd? (dec n))))
 
   唯一的改动就是函数的末尾行前面加了个#,将递归调用修改成返回一个匿名函数了,使用trampoline调用my-even?和my-odd?不会再堆栈溢出了:
user=> (trampoline my-odd? 10000000)
false
user
=> (trampoline my-even? 10000)  
true
user
=> (trampoline my-even? (* 1000 1000))
true
  
   弹簧床这个名称很形象,一次调用就是落到弹簧床上,如果是函数,反弹回去再来一次,如果不是,本次表演结束。
   
 

posted @ 2010-08-22 13:52 dennis 阅读(3388) | 评论 (0)编辑 收藏

    下班后记一些有趣的东西。

    这个话题是阿宝同学问我为什么clojure的PersistentVector的节点Node为什么要有个原子引用指向一个线程:
static class Node implements Serializable {
    
//记录的线程
    transient final AtomicReference<Thread> edit;
    
final Object[] array;

    Node(AtomicReference
<Thread> edit, Object[] array){
        
this.edit = edit;
        
this.array = array;
    }

    Node(AtomicReference
<Thread> edit){
        
this.edit = edit;
        
this.array = new Object[32];
    }
}

    我还真不懂,没有细看过这部分代码,早上花点时间学习了下。
    PersistentVector的实现是另一个话题,这里不提。我们都知道clojure的数据结构是immutable的,修改任意一个数据结构都将生成一个新的数据结构,原来的不变。为了减少复制的开销,clojure的数据结构同时是persistent,所谓持久数据结构是将数据组织为树形的层次结构,修改的时候只是root改变,指向不同的节点,原有的节点仍然复用,从而避免了大量的数据复制,具体可以搜索下ideal hash trees这篇paper, paper难懂,可以看看这篇blog

    但是在创建PersistentVector的时候,从一堆现有的元素或者集合创建一个PersistentVector,如果每次都重新生成一个PersistentVector,未免太浪费,创建过程的性能会受到影响。我们完全可以假设创建PersistentVector这个过程肯定是线程安全的,没有必要每添加一个元素就重新生成一个PersistentVector,完全可以在同一个PersistentVector上修改。这就是TransientVector的意义所在。
    TransientVector就是一个可修改的Vector,调用它添加一个元素,删除一个元素,都是在同一个对象上进行,而不是生成新的对象。查看PersistentVector的创建:

static public PersistentVector create(ISeq items){
    TransientVector ret 
= EMPTY.asTransient();
    
for(; items != null; items = items.next())
        ret 
= ret.conj(items.first());
    
return ret.persistent();
}

static public PersistentVector create(List items){
    TransientVector ret 
= EMPTY.asTransient();
    
for(Object item : items)
        ret 
= ret.conj(item);
    
return ret.persistent();
}

static public PersistentVector create(Object items){
    TransientVector ret 
= EMPTY.asTransient();
    
for(Object item : items)
        ret 
= ret.conj(item);
    
return ret.persistent();
}


    看到三个方法的第一步都是将EMPTY集合transient化,生成一个可修改的TransientVector:
TransientVector(PersistentVector v){
        
this(v.cnt, v.shift, editableRoot(v.root), editableTail(v.tail));
    }

static Node editableRoot(Node node){
        
return new Node(new AtomicReference<Thread>(Thread.currentThread()), node.array.clone());
    }

    生成的时候记录了当前的线程在root节点。然后添加元素的时候直接调用TransientVector的conj方法,查看conj可以看到每次返回的都是this:
public TransientVector conj(Object val){
                
//确保修改过程合法
        ensureEditable();
         
//忽略逻辑
        return this;
    }

    查看ensureEditable方法:
void ensureEditable(){
        Thread owner 
= root.edit.get();
        
if(owner == Thread.currentThread())
            
return;
        
if(owner != null)
            
throw new IllegalAccessError("Transient used by non-owner thread");
        
throw new IllegalAccessError("Transient used after persistent! call");
    }


    终于看到Node中的edit引用的线程被使用了,判断当前修改的线程是否是使得集合transient化的线程,如果不是则抛出异常,这是为了保证对TransientVector的编辑是在同一个线程里,防止因为意外发布TransientVector引用引起的线程安全隐患。

    知道了transient集合的用途,我们能在clojure中使用吗?完全没问题,clojure.core有个transient方法,可以将一个集合transient化:
(defn transient 
  [
^clojure.lang.IEditableCollection coll] 
  (.asTransient coll))
  
    前提是这个集合是可编辑的,clojure的map、vector和set都是可编辑的。让我们确认下transient修改后的集合还是不是自身:
user=> (def v1 [1 2 3])
#
'user/v1
user=> (def v2 (transient v1))
#
'user/v2
user=> v2
#
<TransientVector clojure.lang.PersistentVector$TransientVector@7eb366>

    定义了集合v1,v2是调用了transient之后的集合,查看v2,果然是一个TransientVector。查看v2的元素个数是不是3个:
user=> (.count v2)
3

    没问题,注意,我们不能直接调用count函数,因为v2是个普通的java对象,我们必须使用dot操作符来调用java对象的方法。添加一个元素看看:
user=> (def v3 (.conj v2 4))
#
'user/v3
user=> v3
#
<TransientVector clojure.lang.PersistentVector$TransientVector@7eb366>

    添加一个元素后形成集合v3,查看v3,跟v2是同一个对象#<TransientVector clojure.lang.PersistentVector$TransientVector@7eb366>
。证明了transient集合修改的是自身,而不是生成一个新集合。确认下4有加入v2和v3:
user=> (.nth v3 3)
4
user
=> (.count v2)
4
user
=> (.count v3)
4
user
=> (.nth v2 3)
4

    果然没有问题。transient集合的使用应当慎重,除非能确认没有其他线程会去修改集合,并且对线程的可见性要求不高的时候,也许可以尝试下这个技巧。
  

posted @ 2010-08-18 18:46 dennis 阅读(2272) | 评论 (0)编辑 收藏

仅列出标题
共56页: First 上一页 5 6 7 8 9 10 11 12 13 下一页 Last