2010年2月26日星期五

[转][精品]Java的类装载器(Class Loader)和命名空间(NameSpace)

转自:http://blog.csdn.net/lovingprince/archive/2009/06/03/4239491.aspx

摘要

Java的类装载器是Java动态性的核心,本文将向大家简要介绍Java的类装载器,及相关的parent delegation模型,命名空间,运行时包等概念,同时讨论一些在学习中容易混淆的问题。

类装载器的功能及分类

顾名思义,类装载器是用来把类(class)装载进JVM的。JVM规范定义了两种类型的类装载器:启动内装载器(bootstrap)和用户自定义装载器(user-defined class loader)。

bootstrap是JVM自带的类装载器,用来装载核心类库,如java.lang.*等。由例1可以看出,java.lang.Object是由bootstrap装载的。

Java 提供了抽象类ClassLoader,所有用户自定义类装载器都实例化自ClassLoader的子类。 System Class Loader是一个特殊的用户自定义类装载器,由JVM的实现者提供,在编程者不特别指定装载器的情况下默认装载用户类。系统类装载器可以通过 ClassLoader.getSystemClassLoader() 方法得到。

例1,测试你所使用的JVM的ClassLoader

/*LoaderSample1.java*/
public class LoaderSample1 {
    public static void main(String[] args) {
        Class c;
        ClassLoader cl;
        cl = ClassLoader.getSystemClassLoader();
        System.out.println(cl);
        while (cl != null) {
            cl = cl.getParent();
            System.out.println(cl);
        }
        try {
            c = Class.forName("java.lang.Object");
            cl = c.getClassLoader();
            System.out.println("java.lang.Object's loader is " + cl);
            c = Class.forName("LoaderSample1");
            cl = c.getClassLoader();
            System.out.println("LoaderSample1's loader is " + cl);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在我的机器上(Sun Java 1.4.2)的运行结果

 sun.misc.Launcher$AppClassLoader@1a0c10f
sun.misc.Launcher$ExtClassLoader@e2eec8
null
java.lang.Object's loader is null
LoaderSample1's loader is sun.misc.Launcher$AppClassLoader@1a0c10f

第一行表示,系统类装载器实例化自类sun.misc.Launcher$AppClassLoader

第二行表示,系统类装载器的parent实例化自类sun.misc.Launcher$ExtClassLoader

第三行表示,系统类装载器parent的parent为bootstrap

第四行表示,核心类java.lang.Object是由bootstrap装载的

第五行表示,用户类LoaderSample1是由系统类装载器装载的

parent delegation模型

从 1.2版本开始,Java引入了双亲委托模型,从而更好的保证Java平台的安全。在此模型下,当一个装载器被请求装载某个类时,它首先委托自己的 parent去装载,若parent能装载,则返回这个类所对应的Class对象,若parent不能装载,则由parent的请求者去装载。

如 图1所示,loader2的parent为loader1,loader1的parent为system class loader。假设loader2被要求装载类MyClass,在parent delegation模型下,loader2首先请求loader1代为装载,loader1再请求系统类装载器去装载MyClass。若系统装载器能成 功装载,则将MyClass所对应的Class对象的reference返回给loader1,loader1再将reference返回给 loader2,从而成功将类MyClass装载进虚拟机。若系统类装载器不能装载MyClass,loader1会尝试装载MyClass,若 loader1也不能成功装载,loader2会尝试装载。若所有的parent及loader2本身都不能装载,则装载失败。

若 有一个能成功装载,实际装载的类装载器被称为定义类装载器,所有能成功返回Class对象的装载器(包括定义类装载器)被称为初始类装载器。如图1所示, 假设loader1实际装载了MyClass,则loader1为MyClass的定义类装载器,loader2和loader1为MyClass的初始 类装载器。

图 1 parent delegation模型

需 要指出的是,Class Loader是对象,它的父子关系和类的父子关系没有任何关系。一对父子loader可能实例化自同一个Class,也可能不是,甚至父loader实例 化自子类,子loader实例化自父类。假设MyClassLoader继承自ParentClassLoader,我们可以有如下父子loader:

ClassLoader loader1 = new MyClassLoader();
//参数 loader1 为 parent
ClassLoader loader2 = new ParentClassLoader(loader1);

那么parent delegation模型为什么更安全了?因为在此模型下用户自定义的类装载器不可能装载应该由父亲装载器装载的可靠类,从而防止不可靠甚至恶意的代码代 替由父亲装载器装载的可靠代码。实际上,类装载器的编写者可以自由选择不用把请求委托给parent,但正如上所说,会带来安全的问题。

命名空间及其作用

每个类装载器有自己的命名空间,命名空间由所有以此装载器为创始类装载器的类组成。不同命名空间的两个类是不可见的,但只要得到类所对应的Class对象的reference,还是可以访问另一命名空间的类。

例 2演示了一个命名空间的类如何使用另一命名空间的类。在例子中,LoaderSample2由系统类装载器装载,LoaderSample3由自定义的装 载器loader负责装载,两个类不在同一命名空间,但LoaderSample2得到了LoaderSample3所对应的Class对象的 reference,所以它可以访问LoaderSampl3中公共的成员(如age)。

例2不同命名空间的类的访问

/*LoaderSample2.java*/
import java.net.*;
import java.lang.reflect.*;
public class LoaderSample2 {
    public static void main(String[] args) {
        try {
            String path = System.getProperty("user.dir");
            URL[] us = {new URL("file://" + path + "/sub/")};
            ClassLoader loader = new URLClassLoader(us);
            Class c = loader.loadClass("LoaderSample3");
            Object o = c.newInstance();
            Field f = c.getField("age");
            int age = f.getInt(o);
            System.out.println("age is " + age);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
} 
/*sub/Loadersample3.java*/
public class LoaderSample3 {
    static {
        System.out.println("LoaderSample3 loaded");
    }
    public int age = 30;
}

编译:javac LoaderSample2.java; javac sub/LoaderSample3.java

运行:java LoaderSample2

LoaderSample3 loaded
age is 30

从运行结果中可以看出,在类LoaderSample2中可以创建处于另一命名空间的类LoaderSample3中的对象并可以访问其公共成员age。

运行时包(runtime package)

由 同一类装载器定义装载的属于相同包的类组成了运行时包,决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看的定义类装载器是否相 同。只有属于同一运行时包的类才能互相访问包可见的类和成员。这样的限制避免了用户自己的代码冒充核心类库的类访问核心类库包可见成员的情况。假设用户自 己定义了一个类java.lang.Yes,并用用户自定义的类装载器装载,由于java.lang.Yes和核心类库java.lang.*由不同的装 载器装载,它们属于不同的运行时包,所以java.lang.Yes不能访问核心类库java.lang中类的包可见的成员。

总结

在简单讨论了类装载器,parent delegation模型,命名空间,运行时包后,相信大家已经对它们的作用有了一定的了解。命名空间并没有完全禁止属于不同空间的类的互相访问,双亲委托模型加强了Java的安全,运行时包增加了对包可见成员的保护。

2010年2月24日星期三

[note][精品]Javascript编程中的this

this代表当前作用域的对象。看代码1:

name = "Global";
o = {
   name:"Object",
   show:function() {
      alert(name);
      alert(this.name);
   }
}
o.show();

结果为:Global
             Object

看代码2:
function a() {
    alert(this);
}
a();
结果为:[object Window]

看代码3:
function a() {
alert(this);
}
new a();
结果为:[object Object]

new操作符生成了a对象的this指针。只有对象有指针,function没有,代码1是直接赋值了一个对象,所以也有this指针。但是函数的属性name不想Java那样被看成this.name,而是window.name

2010年2月19日星期五

[转][精品]Java memory model

转自:http://jiangyongyuan.javaeye.com/blog/356579


内存模型 (memory model)
内存模型描述的是程序中各变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存取出变量这样的低层细节.

不同平台间的处理器架构将直接影响内存模型的结构.

在C或C++中, 可以利用不同操作平台下的内存模型来编写并发程序. 但是, 这带给开发人员的是, 更高的学习成本.
相比之下, java利用了自身虚拟机的优势, 使内存模型不束缚于具体的处理器架构, 真正实现了跨平台.
(针对hotspot jvm, jrockit等不同的jvm, 内存模型也会不相同)

内存模型的特征:
a, Visibility 可视性 (多核,多线程间数据的共享)
b, Ordering 有序性 (对内存进行的操作应该是有序的)



java 内存模型 (java memory model)
根据Java Language Specification中的说明, jvm系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。

每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。



其中, 工作内存里的变量, 在多核处理器下, 将大部分储存于处理器高速缓存中, 高速缓存在不经过内存时, 也是不可见的.

jmm怎么体现 可视性(Visibility) ?
在jmm中, 通过并发线程修改变量值, 必须将线程变量同步回主存后, 其他线程才能访问到.

jmm怎么体现 有序性(Ordering) ?
 
通过java提供的同步机制或volatile关键字, 来保证内存的访问顺序. 



缓存一致性(cache coherency)

什么是缓存一致性?
它是一种管理多处理器系统的高速缓存区结构,其可以保证数据在高速缓存区到内存的传输中不会丢失或重复。(来自wikipedia)

举例理解:
假如有一个处理器有一个更新了的变量值位于其缓存中,但还没有被写入主内存,这样别的处理器就可能会看不到这个更新的值.

解决缓存一致性的方法?
a, 顺序一致性模型:
要求某处理器对所改变的变量值立即进行传播, 并确保该值被所有处理器接受后, 才能继续执行其他指令.


b, 释放一致性模型: (类似jmm cache coherency)
允许处理器将改变的变量值延迟到释放锁时才进行传播.



jmm缓存一致性模型 - "happens-before ordering(先行发生排序)"

一般情况下的示例程序:

x = 0;
y = 0;
i = 0;
j = 0;

// thread A
y = 1;
x = 1;

// thread B
i = x;
j = y;

在如上程序中, 如果线程A,B在无保障情况下运行, 那么i,j各会是什么值呢?

答案是, 不确定. (00,01,10,11都有可能出现)
这里没有使用java同步机制, 所以 jmm 有序性和可视性 都无法得到保障.

happens-before ordering(先行发生排序)如何避免这种情况?

排序原则已经做到:
a, 在程序顺序中, 线程中的每一个操作, 发生在当前操作后面将要出现的每一个操作之前.
b, 对象监视器的解锁发生在等待获取对象锁的线程之前.
c, 对volitile关键字修饰的变量写入操作, 发生在对该变量的读取之前.
d, 
对一个线程的 Thread.start() 调用 发生在启动的线程中的所有操作之前.
e, 线程中的所有操作 发生在从这个线程的 Thread.join()成功返回的所有其他线程之前.

为了实现
happends-before ordering原则, java及jdk提供的工具:
a, synchronized关键字
b, volatile关键字
c, final变量
d, java.util.concurrent.locks包(since jdk 1.5)
e, java.util.concurrent.atmoic包(since jdk 1.5)
...

使用了happens-before ordering的例子:



 

(1) 获取对象监视器的锁(lock)

(2) 清空工作内存数据, 从主存复制变量到当前工作内存, 即同步数据 (read and load)

(3) 执行代码,改变共享变量值 (use and assign)

(4) 将工作内存数据刷回主存 (store and write)

(5) 释放对象监视器的锁 (unlock)

注意: 其中4,5两步是同时进行的.

这边最核心的就是第二步, 他同步了主内存,即前一个线程对变量改动的结果,可以被当前线程获知!(利用了happens-before ordering原则)

对比之前的例子
如果多个线程同时执行一段未经锁保护的代码段,很有可能某条线程已经改动了变量的值,但是其他线程却无法看到这个改动,依然在旧的变量值上进行运算,最终导致不可预料的运算结果。

 


 


经典j2ee设计模式Double-Checked Locking失效问题
双重检查锁定失效问题,一直是JMM无法避免的缺陷之一.了解DCL失效问题, 可以帮助我们深入JMM运行原理.

要展示DCL失效问题, 首先要理解一个重要概念- 延迟加载(lazy loading).

非单例的单线程延迟加载示例:
class Foo 
{
private Resource res = null;
public Resource getResource() 
{
    // 普通的延迟加载
if (res == null) 
        res = new Resource(); 
return res;
}
}

非单例的
多线程延迟加载示例:
Class Foo 
{
Private Resource res = null;
Public synchronized Resource getResource()
{
      // 获取实例操作使用同步方式, 性能不高
If (res == null) res = new Resource();
return res;
}
}

非单例的
DCL多线程延迟加载示例:
Class Foo 
{
Private Resource res = null;
Public Resource getResource() 
{
If (res == null)
{
       //只有在第一次初始化时,才使用同步方式.
synchronized(this)
{
if(res == null)
{
res = new Resource();
}
}
}
return res;
}
}

Double-Checked Locking看起来是非常完美的。但是很遗憾,根据Java的语言规范,上面的代码是不可靠的。

出现上述问题, 最重要的2个原因如下:
1, 编译器优化了程序指令, 以加快cpu处理速度.
2, 多核cpu动态调整指令顺序, 以加快并行运算能力.

问题出现的顺序:
1, 线程A, 发现对象未实例化, 准备开始实例化
2, 由于编译器优化了程序指令, 允许对象在构造函数未调用完前, 将
共享变量的引用指向
部分构造的对象, 虽然对象未完全实例化, 但已经不为null了.
3, 线程B, 发现部分构造的对象已不是null, 则直接返回了该对象.

不过, 一些著名的开源框架, 包括jive,lenya等也都在使用DCL模式, 且未见一些极端异常.
说明, DCL失效问题的出现率还是比较低的.
接下来就是性能与稳定之间的选择了?

DCL的替代
Initialize-On-Demand
:

public class Foo {
    // 似有静态内部类, 只有当有引用时, 该类才会被装载
    private static class LazyFoo {
       public static Foo foo = new Foo();
    }

    public static Foo getInstance() {
       return LazyFoo.foo;
    }
}


维基百科的DCL解释:
http://en.wikipedia.org/wiki/Double-checked_locking

DCL的完美解决方案:
http://www.theserverside.com/patterns/thread.tss?thread_id=39606

[转]lockInterruptibly 和lock的区别


参考 
http://gceclub.sun.com.cn/Java_Docs/html/zh_CN/api/java/util/concurrent/locks/ReentrantLock.html#lockInterruptibly(); 
http://tomboxfan.javaeye.com/blog/368270; 

lockInterruptibly 与 lock比较区别在于 
lockInterruptibly 优先考虑响应中断,而不是响应锁定的普通获取或重入获取 
Java代码 
  1. package test;  
  2.   
  3. import java.util.concurrent.TimeUnit;  
  4. import java.util.concurrent.locks.Lock;  
  5. import java.util.concurrent.locks.ReentrantLock;  
  6.   
  7. public class TTTT {  
  8.     public static void main(String[] args){  
  9.   
  10.         Thread i1 = new Thread(new RunIt3());  
  11.         Thread i2 = new Thread(new RunIt3());  
  12.         i1.start();  
  13.         i2.start();  
  14.         i2.interrupt();  
  15.     }  
  16.   
  17. }  
  18.   
  19. class RunIt3 implements Runnable{  
  20.   
  21.     private static Lock lock = new ReentrantLock();  
  22.     public void run(){  
  23.         try{  
  24.             //---------------------------------a  
  25.             lock.lock();  
  26.             //lock.lockInterruptibly();   
  27.               
  28.               
  29.             System.out.println(Thread.currentThread().getName() + " running");  
  30.             TimeUnit.SECONDS.sleep(20);  
  31.             lock.unlock();  
  32.             System.out.println(Thread.currentThread().getName() + " finished");  
  33.         }  
  34.         catch (InterruptedException e){  
  35.             System.out.println(Thread.currentThread().getName() + " interrupted");  
  36.   
  37.         }  
  38.   
  39.     }  
  40. }  


如果a处 是lock.lock(); 
输出 
Thread-0 running 
(这里休眠了20s) 
Thread-0 finished 
Thread-1 running 
Thread-1 interrupted 


============================ 

如果a处是lock.lockInterruptibly() 
Thread-0 running 
Thread-1 interrupted 
(这里休眠了20s) 
Thread-0 finished 
========================= 

如果a处是lock.tryLock(); 


Thread-1 running 
Thread-0 running 
(这里休眠了20s) 
Thread-1 finished 
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException 
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(Unknown Source) 
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(Unknown Source) 
at java.util.concurrent.locks.ReentrantLock.unlock(Unknown Source) 
at test.RunIt3.run(TTTT.java:32) 
at java.lang.Thread.run(Unknown Source) 


========================= 

如果a处是lock.tryLock(10,TimeUnit.SECONDS); 

Thread-0 running 
(这里休眠了10s) 
Thread-1 running 

(这里休眠了10s) 
Thread-0 finished 
(这里休眠了10s) 
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException 
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(Unknown Source) 
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(Unknown Source) 
at java.util.concurrent.locks.ReentrantLock.unlock(Unknown Source) 
at test.RunIt3.run(TTTT.java:32) 
at java.lang.Thread.run(Unknown Source) 

2010年2月15日星期一

[转][精品]在Java中使用协程

转自:http://www.blogjava.net/BlueDavy/archive/2010/01/28/311148.html


在Java中使用协程(Coroutine)

本篇blog将讲述coroutine的一些背景知识,以及在Java中如何使用Coroutine,包括一个简单的benchmark对比,希望能借助这篇blog让大家了解到更多在java中使用coroutine的方法,本篇blog的PDF版本可从此下载:http://www.bluedavy.com/open/UseCoroutineInJava.pdf
在讲到具体内容之前,不能不先讲下
Coroutine的一些背景知识,来先具体了解下什么是Coroutine

1.       背景知识

         现在的操作系统都是支持多任务的,多任务可通过多进程或多线程的方式去实现,进程和线程的对比就不在这里说了,在多任务的调度上操作系统采取抢占式和协作式两种方式,抢占式是指操作系统给每个任务一定的执行时间片,在到达这个时间片后如任务仍然未释放对CPU的占用,那么操作系统将强制释放,这是目前多数操作系统采取的方式;协作式是指操作系统按照任务的顺序来分配CPU,每个任务执行过程中除非其主动释放,否则将一直占据CPU,这种方式非常值得注意的是一旦有任务占据CPU不放,会导致其他任务饿死的现象,因此操作系统确实不太适合采用这种方式。

         说完操作系统多任务的调度方式后,来看看通常程序是如何实现支持高并发的,一种就是典型的基于操作系统提供的多进程或多线程机制,每个任务占据一个进程或一个线程,当任务中有IO等待等动作时,则将进程或线程放入待调度队列中,这种方式是目前大多数程序采取的方式,这种方式的坏处在于如想支持高的并发量,就不得不创建很多的进程或线程,而进程和线程都是要消耗不少系统资源的,另外一方面,进程或线程创建太多后,操作系统需要花费很多的时间在进程或线程的切换上,切换动作需要做状态保持和恢复,这也会消耗掉很多的系统资源;另外一种方式则是每个任务不完全占据一个进程或线程,当任务执行过程中需要进行IO等待等动作时,任务则将其所占据的进程或线程释放,以便其他任务使用这个进程或线程,这种方式的好处在于可以减少所需要的原生的进程或线程数,并且由于操作系统不需要做进程或线程的切换,而是自行来实现任务的切换,其成本会较操作系统切换低,这种方式也就是本文的重点,Coroutine方式,又称协程方式,这种方式在目前的大多数语言中都有支持。

         各种语言在实现Coroutine方式的支持时,多数都采用了Actor Model来实现,Actor Model简单来说就是每个任务就是一个ActorActor之间通过消息传递的方式来进行交互,而不采用共享的方式,Actor可以看做是一个轻量级的进程或线程,通常在一台4G内存的机器上,创建几十万个Actor是毫无问题的,Actor支持Continuations,即对于如下代码:

         Actor

                   act方法

                            进行一些处理

创建并执行另外一个Actor

                            通过消息box阻塞获取另一个Actor执行的结果

                            继续基于这个结果进行一些处理

         在支持Continuations的情况下,可以做到消息box阻塞时并不是进程或线程级的阻塞,而只是Actor本身的阻塞,并且在阻塞时可将所占据的进程或线程释放给其他Actor使用,Actor Model实现最典型的就是erLang了。

         对于Java应用而言,传统方式下为了支持高并发,由于一个线程只能用于处理一个请求,即使是线程中其实有很多IO中断、锁等待也同样如此,因此通常的做法是通过启动很多的线程来支撑高并发,但当线程过多时,就造成了CPU需要消耗不少的时间在线程的切换上,从而出现瓶颈,按照上面对Coroutine的描述,Coroutine的方式理论上而言能够大幅度的提升Java应用所能支撑的并发量。

2.       Java中使用Coroutine

         Java尚不能从语言层次上支持Coroutine,也许Java 7能够支持,目前已经有了一个测试性质的版本[1],在Sun JDK 7尚未正式发布的情况下如希望在Java中使用CoroutineScalaKilim是可以做的选择,来分别看下。

         Scala是现在很火的语言之一,Twitter消息中间件基于Scala编写更是让Scala名声鹊起,除了在语法方面所做出的改进外,其中一个最突出的特色就是Scala ActorScala ActorScala用于实现Coroutine的方式,先来具体看看ScalaCoroutine支持实现的关键概念。

l  Actor

Scala Actor可以看做是一个轻量级的Java Thread,其使用方式和Java Thread基本也一致,继承Actor,实现act方法,启动时也是调用start方法,但和Java Thread不同的是,Scala Actor可等待外部发送过来的消息,并进行相应的处理。

l  Actor的消息发送机制

发送消息到Actor的方式有异步、Future两种方式,异步即指发送后立即返回,继续后续流程,使用异步发送的方法为:actor ! MessageObject,其中消息对象可以为任何类型,并且Scala还支持一种称为case Object的对象,便于在收到消息时做pattern matching

Future方式是指阻塞线程等待消息处理的结果,使用Future方式发送的方法为:actor !! MessageObject,在等待结果方面,Scala支持不限时等待,限时等待以及等待多个Future或个别Future完成,使用方法如下:

val ft=actor !! MessageObject // Future方式发送消息

val result=ft() // 不限时等待

val results=awaitAll(500,ft1,ft2,ft3)  // 限时等待多个Future返回值

val results=awaitEither(ft1,ft2) // 等待个别future完成

接收消息方通过reply方法返回Future方式所等待的结果。

l  Actor的消息接收机制

当代码处于Actoract方法或Actor环境(例如为Actoract方法调用过来的代码)中时,可通过以下两种方式来接收外部发送给Actor的消息:一为receive方式,二为react方式,代码例子如下:

receive{

         case MessageObject(args) => doHandle(args)

}

react{

         case MessageObject(args) => doHandle(args)

}

receivereact的差别在于receive需要阻塞当前Java线程,react则仅为阻塞当前Actor,但并不会阻塞Java线程,因此react模式更适合于充分发挥coroutine带来的原生线程数减少的好处,但react模式有个缺点是react不支持返回。

receivereact都有限时接收的方式,方法为:receiveWithin(timeout)reactWithin(timeout),超时的消息通过case TIMEOUT的方式来接收。

下面来看基于Scala Actor实现并发处理请求的一个简单例子。

         class Processor extends Actor{

                   def act(){

                            loop{

                                     react{

                                               case command:String => doHandle(command)

}

}

                   }

 

                   def doHandle(command:String){

                            // 业务逻辑处理

}

}

当需要并发执行此Processor时,在处理时需要的仅为调用以下代码:

val processor=new Processor()

processor.start

processor ! “Hello”

         从以上说明来看,要在旧的应用中使用Scala还是会有一些成本,部署运行则非常简单,在Scala IDE Plugin编写了上面的scala代码后,即生成了java class文件,可直接在jvm中运行。

Kilim是由剑桥的两位博士开发的一个用于在Java中使用Coroutine的框架,Kilim基于Java语法,先来看看Kilim中的关键概念。

l  Task

可以认为Task就是Actor,使用方式和Java Thread基本相同,只是继承的为Task,覆盖的为execute方法,启动也是调用taskstart方法。

l  Task的消息发送机制

Kilim中通过Mailbox对象来发送消息,Mailbox的基本原则为可以有多个消息发送者,但只能有一个消息接收者,发送的方式有同步发送、异步发送和阻塞线程方式的同步发送三种,同步发送是指保证一定能将消息放入发送队列中,如当前发送队列已满,则等待到可用为止,阻塞的为当前Task;异步发送则是尝试将消息放入发送队列一次,如失败,则返回false,成功则返回true,不会阻塞Task;阻塞线程方式的同步发送是指阻塞当前线程,并保证将消息发送给接收者,三种方式的使用方法如下:

mailbox.put(messageObject); // 同步发送

mailbox.putnb(messageObject); // 异步发送

mailbox.putb(messageObject); // 阻塞线程方式发送

l  Task的消息接收机制

Kilim中通过Mailbox来接收消息,接收消息的方式有同步接收、异步接收以及阻塞线程方式的同步接收三种,同步接收是指阻塞当前Task,直到接收到消息才返回;异步接收是指立刻返回Mailbox中的消息,有就返回,没有则返回null;阻塞线程方式的同步接收是指阻塞当前线程,直到接收到消息才返回,使用方法如下:

mailbox.get(); // 同步接收,传入long参数表示等待的超时时间,单位为毫秒

mailbox.getnb(); // 异步接收,立刻返回

mailbox.getb(); // 阻塞线程方式接收

下面来看基于Kilim实现并发处理请求的一个简单例子。

         public class Processor extends Task{

                   private String command;

                   public Processor(String command){

                            this.command=command;

}

public void execute() throws Pausable,Exception{

         // 业务逻辑处理

}

}

在处理时,仅需调用以下代码:

Task processor=new Processor(command);

processor.start();

从以上代码来看,Kilim对于Java人员而言学习门槛更低,但对于需要采用coroutine方式执行的代码在编译完毕后,还需要采用Kilimkilim.tools.Weaver类来对这些已编译出来的class文件做织入,运行时需要用织入后生成的class文件才行,织入的方法为:java kilim.tools.Weaver –d [织入后生成的class文件存放的目录] [需要织入的类文件所在的目录],目前尚没有Kilim IDE Plugin可用,因此weaver这个过程还是比较的麻烦。

上面对ScalaKilim做了一个简单的介绍,在实际Java应用中使用Coroutine时,通常会出现以下几种典型的更复杂的使用场景,由于Actor模式本身就是异步的,因此其天然对异步场景支持的就非常好,更多的问题会出现在以下几个同步场景上,分别来看看基于ScalaKilim如何来实现。

l  Actor同步调用

Actor同步调用是经常会出现的使用场景,主要为Actor发送消息给其他的Actor处理,并等待结果才能继续。

n  Scala

对于这种情况,在Scala 2.7.7中,目前可采取的为以下两种方法:

一种为通过Future方式发送消息来实现:

class Processor(command:String) extends Actor{

         def act(){

                   val actor=new NetSenderActor()

                   val ft=actor !! command

                   println(ft())

}

}

class NetSenderActor extends Actor{

         def act(){

                   case command:String => {

                            reply(“received command:”+command)

}

}

}

第二种为通过receive的方式来实现:

class Processor(command:String) extends Actor{

         def act(){

                   val actor=new NetSenderActor()

                   actor ! command

                   var senderResult=””

receive{

                            case result:String => {

senderResult=result

}

}

println(senderResult)

}

}

class NetSenderActor extends Actor{

         def act(){

                   case command:String => {

                            sender ! (“received command:”+command)

}

}

}

但这两种方式其实都不好,因为这两种方式都会造成当前Actor的线程阻塞,这也是因为目前Scala版本对continuations尚不支持的原因,Scala 2.8版本将提供continuations的支持,希望到时能有不需要阻塞Actor线程实现上述需求的方法。

还有一种常见的场景是Actor调一段普通的Scala类,然后那个类中进行了一些处理,并调用了其他Actor,此时在该类中如需要等待Actor的返回结果,也可使用上面两种方法。

n  Kilim

Kilim中要实现Task之间的同步调用非常简单,代码如下:

public class TaskA extends Task{

         public void execute() throws Pausable,Exception{

                   Mailbox<Object> result=new Mailbox<Object>();

Task task=new TaskB(result);

                   task.start();

                   Object resultObject=result.get();

                   System.out.println(resultObject);

}

}

public class TaskB extends Task{

         private Mailbox<Object> result;

public TaskB(Mailbox<Object> result){

                   this.result=result;

}

public void execute() throws Pausable,Exception{

         result.put(“result from TaskB”);

}

}

KilimMailbox.get并不会阻塞线程,因此这种方式是完全满足需求的。

l  普通Java代码同步调用Actor

由于已有的应用是普通的Java代码,经常会出现这样的场景,就是希望实现在这些Java代码中同步的调用Actor,并等待Actor的返回结果,但由于ScalaKilim都强调首先必须是在ActorTask的环境下才行,因此此场景更佳的方式应为Scala Actor(Kilim Task) à Java Code à Scala Actor(Kilim Task),这种场景在对已有的应用中会是最常出现的,来看看在ScalaKilim中如何应对这样的需求。

n  Scala

目前Scala中如希望在Java Code中调用Scala Actor,并等待其返回结果,暂时还没办法,做法只能改为从Java Code中去调一个ScalaObject,然后在这个Object中调用Actor,并借助上面提到的receivefuture的方法来获取返回值,最后将这个返回值返回Java Code

n  Kilim

目前Kilim中如希望实现上面的需求,其实非常简单,只需要在Java Code的方法上加上Throw Pausable,然后通过mailbox.get来等待Kilim Task返回的结果即可,在Kilim中只要调用栈上的每个方法都有Throw Pausable,就可在这些方法上做等待返回这类的同步操作。

从上面这两个最常见的需求来看,无疑Kilim更符合需求,但要注意的是对于Kilim而言,如果出现Task à nonpausable method à pausable method这样的状况时,pausable method中如果想执行阻塞当前Task的操作,是无法做到的,只能改造成Task (mailbox上做等待,并传递mailbox给后续步骤à nonpausable method (传递mailbox) àpausable method (将逻辑转为放入一个Task中,并将返回值放入传递过来的mailbox),这种状况在面对spring aop、反射调用等现象时就会出现了,目前kilim 0.6的版本尚未提供更透明的使用方法,不过据kilim作者提供的一个试用版本,其中已经有了对于反射调用的透明化的支持,暂时在目前只能采用上述方法,迁移成本相对较大,也许以后的kilim版本会考虑这样的场景,提供相应的方法来降低迁移的成本。

3.       性能、所能支撑的并发量对比

在对ScalaKilim有了这些了解后,来具体看看采用ScalaKilim后与传统Java方式在性能、所能支撑的并发量上的对比。

l  测试模型

采用一个比较简单的模型进行测试,具体为有4个线程,这4个线程分别接收到了一定数量的请求,每个请求需要交给另外一个线程去执行,这个线程所做的动作为循环10次获取另外一个线程的执行结果,此执行线程所做的动作为循环1000次拼接一个字符串,然后返回。

l  实现代码

由于目前Scala版本对Continuation支持不够好,但上面的场景中又有此类需求,所以导致Scala版本的代码写的比较麻烦一些。

实现代码以及可运行的环境请从此处下载:

http://www.bluedavy.com/open/benchmark.zip

l  结果对比

测试机器为一台4核的linux机器。

TPS的对比结果如下:


    Load
的对比结果如下:
    

从上面的测试结果来看,在这个benchmark的场景中,基于KilimScala实现的Coroutine版本在随着请求数增长的情况下load的增长幅度都比纯粹的Java版本低很多,Kilim版本表现尤其突出,在TPS方面,由于目前Scala版本对Continuation支持的不好,因此在这个测试场景中有点吃亏,表现反而最差,经过上面的测试可以看到,基于Coroutine版本可以以同样的load或更低的load来支撑更高的TPS

到此为止,基本上对Java中使用Coroutine的相关知识做了一个介绍,总结而言,采用Coroutine方式可以很好的绕开需要启动太多线程来支撑高并发出现的瓶颈,提高Java应用所能支撑的并发量,但在开发模式上也会带来变化,并且需要特别注意不能造成线程被阻塞的现象,从开发易用和透明迁移现有Java应用两个角度而言目前Coroutine方式还有很多不足,但相信随着越来越多的人在Java中使用Coroutine,其易用性必然是能够得到提升的。

4.       参考资料

1.         http://en.wikipedia.org/wiki/Computer_multitasking

2.         http://en.wikipedia.org/wiki/Coroutine

3.         http://en.wikipedia.org/wiki/Actor_model

4.         http://en.wikipedia.org/wiki/Continuation

5.         http://lamp.epfl.ch/~phaller/doc/haller07coord.pdf

6.         http://www.scala-lang.org/sites/default/files/odersky/jmlc06.pdf

7.         http://www.malhar.net/sriram/kilim/kilim_ecoop08.pdf

8.         http://lamp.epfl.ch/~phaller/doc/ScalaActors.pdf