• 2009-12-11

    JDK 7 platform - [Java]

    Tag:Java

    JDK 7 Platform图:
    JDK 7 Platform
    其中,比较关注:
    1. VisualVM
    2. Networking(Classloader)
    3. NIO
    4. Collections
    5. Concurrency
    6. Performance
    7. Garbage-First Collector
    8. Modularization

    Resource:
    http://openjdk.java.net/projects/jdk7/features/
    http://java.sun.com/javase/7/docs/
    http://java.sun.com/javase/7/webnotes/index.html

     

  • JNI即是Java Native Interface,主要用来Java和其他语言之间的交互,多半是和平台依赖的调用、其他高级语言的库甚至是低级语言交互。相信大家平时在JDK中看到很多native的方法,但是自己写JNI应用的会确很少。这次因为在进行开发中,遇到需要Java调用一个现成的成熟的功能完善的C语言应用库,因此 JNI便成了Java和C库之间的桥的功能。既然是初探,那么下面我们就从HelloWorld开始吧:虽然Java是一个跨平台的语言,但是C语言是一个和平台密切相关的语言;所以针对Unix、Linux、Windows等不同的操作系统,C语言的预编译,动态库的创建等都有一些差别,也需要我们分别进行处理之。下面仅基于Sun的Unix平台Sun Solaris开发为例。
    1.首先声明第一个Java native程序:HelloWorld.java。
    public class HelloWorld {
        static {
            init();
        }

        private native int hello();

        public int sayHello() {
            return hello();
        }

        /**
         * @param args
         */
        public static void main(String[] args) {
            HelloWorld hw = new HelloWorld();
            System.out.println("Say in Java:" + hw.sayHello());
        }

        private static void init() {
            System.out.println("Starting to load HelloWorld lib.");
            try {
                System.loadLibrary("HelloWorld");
            } catch (Throwable t) {
                System.out.println("Load unsuccessfully.");
                t.printStackTrace();
                System.exit(-1);
            }
            System.out.println("Load successfully.");
        }
    }
    2.编译该Java文件。
    javac HelloWorld.java
    3.生成相应的C语言的HelloWorld.h头文件,javah HelloWorld,头文件内容如下:(其中,static Java方法和非static Java方法经过javah -jni生成的C头文件是有区别的,差异在于参数,static Java方法生成的相应的C方法的第二个参数是jclass类型,非static Java方法生成的相应的C方法的第二个参数是jobject类型。)
    more HelloWorld.h
    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class HelloWorld */

    #ifndef _Included_HelloWorld
    #define _Included_HelloWorld
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     HelloWorld
     * Method:    hello
     * Signature: ()I
     */
    JNIEXPORT jint JNICALL Java_HelloWorld_hello
      (JNIEnv *, jobject);

    #ifdef __cplusplus
    }
    #endif
    #endif
    4.开始编写第一个c语言的JNI的本地(native)方法:
    vi HelloWorld.c
    #include <jni.h>
    #include "HelloWorld.h"
    #include <stdio.h>

    JNIEXPORT jint JNICALL Java_HelloWorld_hello
    (JNIEnv* env, jobject target)
    {
      printf("Hello World in C program!!!\n");
      return 2;
    }
    5.生成Unix下的动态库.so文件:
    cc -G -I. -I$JAVA_HOME/include -I$JAVA_HOME/include/solaris HelloWorld.c -o libHelloWorld.so
    在当前目录下看到新生成的libHelloWorld.so文件。也许你会注意到我们在Java程序中System.loadLibrary()加载的是库HelloWorld,那么这其实是一个命名的规范而已。以lib开头的.so文件即是Unix下的动态库,而JNI加载器通过HelloWorld这个名字实际上希望加载的动态库是libHelloWorld.so。
    6.设置环境变量,查看LD_LIBRARY_PATH并设置:
    vi .cshrc
    setenv LD_LIBRARY_PATH '.:/usr/local/lib:$LD_LIBRARY_PATH'
    source .cshrc
    这个环境变量设置的是Java的动态库加载器需要加载的C动态库所在的路径。因此,我们生成的libHelloWorld.so一定要在这个环境变量中设置,在这个例子中我就把当前目录.加到环境变量LD_LIBRARY_PATH了:)
    7.执行HelloWorld文件,java HelloWorld,在标准输出上看到:
    Starting to load HelloWorld lib.
    Load successfully.
    Hello World in C program!!!
    Say in Java:2
    一切Okey,如此的easy,一阵狂喜!接下来就是将这个例子运用于JNI的开发之中了。

    Resource:
    Java Native Interface: Programmer's Guide and Specification
    http://java.sun.com/docs/books/jni/html/start.html#27008
    http://linuxmafia.com/faq/Admin/ld-lib-path.html
    https://www6.software.ibm.com/developerworks/cn/education/java/j-jni/tutorial/j-jni-2-15.html
  • 在Java1.4以前,Java的网络编程是只有阻塞方式的,在Java1.4以及之后,Java提供了非阻塞的网络编程API.从Java的发展来看,由于Java的快速发展,JVM性能的提升,涉足到服务端应用程序开发也越来越多,要求高性能的网络应用越来越多,这是Java推出非阻塞网络编程的最主要原因吧。
    对我而言,以前的大部分服务端应用主要是搭建在应用服务器之上,所以通讯这部分工作都是有应用服务器来实现和管理的。这次由于通讯和协议,我们必须自己实现一个能处理大量并发客户端的高性能并行处理的Java服务端程序。因此,选择非阻塞的处理方式也是必然的。我们首先来看看阻塞的处理方式:
    在阻塞的网络编程方式中,针对于每一个单独的网络连接,都必须有一个线程对应的绑定该网络连接,进行网络字节流的处理。下面是一段代码:
        public static void main(String[] args) {
            try {
                ServerSocket ssc = new ServerSocket(23456);
                while (true) {
                    System.out.println("Enter Accept:");
                    Socket s = ssc.accept();
                    try {
                        (new Thread(new Worker(s))).start();
                    } catch (Exception e) {
                        // TODO
                        e.printStackTrace();
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

        public static class Worker implements Runnable {
            private Socket s;

            private boolean running = true;;

            public Worker(Socket s) {
                this.s = s;
            }

            public void run() {
                try {
                    InputStream is = s.getInputStream();
                    OutputStream os = s.getOutputStream();
                    while (running) {
                        byte[] b = this.readByLength(is, 1024);
                        this.process(b);
                    }
                } catch (Throwable t) {
                    // TODO
                    t.printStackTrace();
                }
            }

            private byte[] readByLength(InputStream is, int contLen) throws IOException {
                byte[] b = new byte[contLen];
                int off = 0;
                int length = 0;
                while ((length = is.read(b, off, contLen - off)) >= 0) {
                    off = +length;
                    if (off >= contLen) {
                        break;
                    }
                }
                return b;
            }

            private void process(byte[] b) {

            }
        }
    在这段代码中,我们看到有两个阻塞的方法,是ServerSocket的accept()方法;和InputStream的read()方式。因此我们需要两类型的线程分别进行处理。而且每一个阻塞方法所绑定的线程的生命周期和网络连接的生命周期是一致的。基于以上的原因,NIO应运而生,一方面,为每一个网络连接建立一个线程对应,同时每一个线程有大量的线程处于读写以外的空闲状态,因此希望降低线程的数量,降低每个空闲状态,提高单个线程的运行执行效率,实际上是在更加充分运用CPU的计算、运行能力(因为,如果有大量的链路存在,就存在大量的线程,而大量的线程都阻塞在read()或者write()方法,同时CPU又需要来回频繁的在这些线程中间调度和切换,必然带来大量的系统调用和资源竞争.);另外一方面希望提高网络IO和硬盘IO操作的性能。在NIO主要出现了三个新特性:
    1.数据缓冲处理(ByteBuffer):由于操作系统和应用程序数据通信的原始类型是byte,也是IO数据操作的基本单元,在NIO中,每一个基本的原生类型(boolean除外)都有Buffer的实现:CharBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer、FloatBuffer和ByteBuffer,数据缓冲使得在IO操作中能够连续的处理数据流。当前有两种ByteBuffer,一种是Direct ByteBuffer,另外一种是NonDirect ByteBuffer;ByteBuffer是普通的Java对象,遵循Java堆中对象存在的规则;而Direct ByteBuffer是native代码,它内存的分配不在Java的堆栈中,不受Java内存回收的影响,每一个Direct ByteBuffer都是直接分配的一块连续的内存空间,也是NIO提高性能的重要办法之一。另外数据缓冲有一个很重要的特点是,基于一个数据缓冲可以建立一个或者多个逻辑的视图缓冲(View Buffer).比方说,通过View Buffer,可以将一个Byte类型的Buffer换作Int类型的缓冲;或者一个大的缓冲转作很多小的Buffer。之所以称为View Buffer是因为这个转换仅仅是逻辑上,在物理上并没有创建新的Buffer。这为我们操作Buffer带来诸多方便。
    2.异步通道(Channel):Channel是一个与操作系统紧密结合的本地代码较多的对象。通过Channel来实现网络编程的非阻塞操作,同时也是其与ByteBuffer、Socket有效结合充分利用非阻塞、ByteBuffer的特性的。在后面我们会看到具体的SocketChannel的用法。
    3.有条件的选择(Readiness Selection):大多数操作系统都有支持有条件选择准备就绪IO通道的API,即能够保证一个线程同时有效管理多个IO通道。在NIO中,由Selector(维护注册进来的Channel和这些Channel的状态)、SelectableChannel(能被Selector管理的Channel)和SelectionKey(SelectionKey标识Selector和SelectableChannel之间的映射关系,一旦一个Channel注册到Selector中,就会返回一个SelectionKey对象。SelectionKey保存了两类状态:对应的Channel注册了哪些操作;对应的Channel的那些操作已经准备好了,可以进行相应的数据操作了)结合来实现这个功能的。
    NIO非阻塞的典型编程模型如下:
        private Selector selector = null;

        private static final int BUF_LENGTH = 1024;

        public void start() throws IOException {
            if (selector != null) {
                selector = Selector.open();
            }
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ServerSocket serverSocket = ssc.socket();
            serverSocket.bind(new InetSocketAddress(80));
            ssc.configureBlocking(false);
            ssc.register(selector, SelectionKey.OP_ACCEPT);

            try {
                while (true) {
                    int nKeys = UnblockServer.this.selector.select();

                    if (nKeys > 0) {
                        Iterator it = selector.selectedKeys().iterator();
                        while (it.hasNext()) {
                            SelectionKey key = (SelectionKey) it.next();
                            if (key.isAcceptable()) {
                                ServerSocketChannel server = (ServerSocketChannel) key.channel();
                                SocketChannel channel = server.accept();
                                if (channel == null) {
                                    continue;
                                }
                                channel.configureBlocking(false);
                                channel.register(selector, SelectionKey.OP_READ);
                            }

                            if (key.isReadable()) {
                                readDataFromSocket(key);
                            }

                            it.remove();
                        }
                    }
                }
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }

        /**
         * @param key
         * @throws IOException
         */
        private void readDataFromSocket(SelectionKey key) throws IOException {
            ByteBuffer buf = ByteBuffer.allocate(BUF_LENGTH);
            SocketChannel sc = (SocketChannel) key.channel();

            int readBytes = 0;
            int ret;

            try {
                while ((ret = sc.read(buf.buf())) > 0) {
                    readBytes += ret;
                }
            } finally {
                buf.flip();
            }

            // process buffer
            // buf.clear();
        }

    从这段程序,我们基本可以了解到NIO网络编程的一些特点,创建一个SocketServer的方式已经发生了变化,需要指定非阻塞模式,需要创建一个Channel然后注册到Selector中去,同样,建立一个网络连接过程也是一样的模式,然后就是有条件的选择(Readiness Selection).这样,我们的每一个线程只需要处理一类型的网络选择。在代码上,我们发现处理的方式和阻塞完全不一样了,我们需要完全重新考虑如何编写网络通信的模块了:
    1.持久连接的超时问题(Timeout),因为API没有直接的支持timeout的参数设置功能,因此需要我们自己实现一个这样功能。
    2.如何使用Selector,由于每一个Selector的处理能力是有限的,因此在大量链接和消息处理过程中,需要考虑如何使用多个Selector.
    3.在非阻塞情况下,read和write都不在是阻塞的,因此需要考虑如何完整的读取到确定的消息;如何在确保在网络环境不是很好的情况下,一定将数据写进IO中。
    4.如何应用ByteBuffer,本身大量创建ByteBuffer就是很耗资源的;如何有效的使用ByteBuffer?同时ByteBuffer的操作需要仔细考虑,因为有position()、mark()、limit()、capacity等方法。
    5.由于每一个线程在处理网络连接的时候,面对的都是一系列的网络连接,需要考虑如何更好的使用、调度多线程。在对消息的处理上,也需要保证一定的顺序,比方说,登录消息最先到达,只有登录消息处理之后,才有可能去处理同一个链路上的其他类型的消息。
    6.在网络编程中可能出现的内存泄漏问题。
    在NIO的接入处理框架上,大约有两种并发线程:
    1.Selector线程,每一个Selector单独占用一个线程,由于每一个Selector的处理能力是有限的,因此需要多个Selector并行工作。
    2.对于每一条处于Ready状态的链路,需要线程对于相应的消息进行处理;对于这一类型的消息,需要并发线程共同工作进行处理。在这个过程中,不断可能需要消息的完整性;还要涉及到,每个链路上的消息可能有时序,因此在处理上,也可能要求相应的时序性。
    当前社区的开源NIO框架实现有MINA、Grizzly、NIO framework、QuickServer、xSocket等,其中MINA和Grizzly最为活跃,而且代码的质量也很高。他们俩在实现的方法上也完全大不一样。(大部分Java的开源服务器都已经用NIO重写了网络部分。 )
    不管是我们自己实现NIO的网络编程框架,还是基于MINA、Grizzly等这样的开源框架进行开发,都需要理解确定的了解NIO带来的益处和NIO编程需要解决的众多类型的问题。充足、有效的单元测试,是我们写好NIO代码的好助手:)

    Resource:
    http://www.cis.temple.edu/~ingargio/cis307/readings/unix4.html#states

    Java NIO
    GlassFish--开源的Java EE应用服务器

  • 2007-11-17

    Java线程编程 - [Java]

    Tag:Java Agile
    Java程序开发中有大量的可复用资源,可复用的公用类;有很多中间件已经帮助我们解决了多线程的问题,所以很多开发人员是不需要深入的涉及到这个话题的,而要自己去开发一个服务端的应用的时候,多线程的编程、调试、发布、问题跟踪就变得无可避免了。
    多线程编程需要涉及到两个方向,一个是使用Java语言进行程序设计,必须了解语言本身对于线程的支持和其编程模型;另外一个是运行时的运行状况,更多的是基于现有的硬件、先有的操作系统的配置问题。所有的应用都必须运行一定的硬件配置环境、一定的操作系统之上,所以线程的运行时状态无可避免的要在应用设计阶段进行考虑了。
    1.对于共享可变数据的互斥(什么时候使用关键字synchronized):Java虚拟机通过对象锁来实现互斥,达到多个线程在同一个共享数据上独立而互不干扰地工作。
    private int sequence = 0;

    public synchronized int getSequence(){
    return sequence++;
    }

    线程间通讯,保证共享对象对于多线程访问是从一种一致的状态跃迁到另外一种一致的状态。
    public class Worker implements Runnable{
    private boolean running = false;

    public void run(){
    while(isRunning()){
    //
    }
    }

    public synchronized boolean isRunning(){
    return this.running;
    }

    public synchronized void stop(){
    this.running = false;
    }

    public synchronized void startRequest(){
    this.running = true;
    }
    }
    2.协作(什么时候使用wait和notify):主要是用于多线程为了同一个目标而共同协作性的工作,应用需要设计多线程的协作工作机制。一般性地是,线程需要反复检查某一个数据结构,在等待某些条件的发生,又避免忙等(busy-wait)的时候。
    使用wait()是需要仔细阅读以下javadoc了,wait(0)的情况很特殊:线程一直处于等待,直到被唤醒(notify()或者notifyAll())。

    private Object monitor = new Object();

    public void run(){
    synchronized(monitor){
    while(){
    try{
    obj.wait()
    } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }
    }
    }
    }
    Java使用的同步机制是监视器,为了更好的使用同步机制,就需要虚拟机是怎么使用监视器的机制的?
    Java监视器主要分成了三个区域,入口区、监视区、等待区,所有进入到监视器开始处的线程首先都是进入到入口去,(可能阻塞)等待成为监视区的持有者;监视区域同时只能被一个线程持有并执行,只有当一个线程执行完了监视区域动作(该线程将释放监视区域并推出监视器)或者执行了等待命令(该线程将释放监视区域进入到等待区,直到执行了唤醒命令才能重新持有监视区域)才能释放监视区域;只有那些持有监视区域的线程执行了等待命令之后就会进入到等待区。所以基本可以了解了监视区域的各个功能、线程进出的时机动作。而且分析互斥和协作情况下线程和监视器的工作状态基本也就清晰了。
    1.互斥情况下,多线程将会在等待区阻塞的等待持有监视区域,因为同时只会有一个线程执行监视区域(代码即指令),监视区域主要是共享数据或者共享资源。(例外就是虚拟机实现不是基于时间片的,那么监视器就会用来协调多线程的执行策略,将不仅仅是共享数据或者共享资源了。)
    2.协作情况下,监视器主要协调多个线程之间共同工作,即是,一个已经持有监视区域的线程,通过执行等待命令,释放监视区域进入到等待区,那么该线程阻塞并一直持续暂停状态,只有监视区域持有线程执行了唤醒该线程命令并释放监视区域之后该线程才能重新持有监视区域,直到该线程再次释放监视区域。

    运行阶段,Java线程的线程模型又是什么样的呢,究竟需要应用设计开发人员需要注意什么呢?
    这主要涉及到Java的内存模型(Memory Model)、JVM实现等,其中我正在使用的虚拟机是Sun基于Solaris系统的,其线程模型参考《Threading》。Java的线程在Java2都是操作系统的线程,并没有了"green threads"的概念了,每一个线程的申请在向JVM申请资源的同时也是在向操作系统申请资源,对于各家JVM在不同的操作系统之上的thread stack size大小也有一些差别,其中Sun JVM可以通过-XX:ThreadStackSize(-Xss)参数进行设置(在默认情况下,"Thread Stack Size (in Kbytes). (0 means use default stack size) [Sparc: 512; Solaris x86: 320 (was 256 prior in 5.0 and earlier); Sparc 64 bit: 1024; Linux amd64: 1024 (was 0 in 5.0 and earlier); all others 0.]").
    在我们设计并发要求比较高的服务端应用的时候,线程将会变得相当宝贵了,而且就针对操作系统而言,一个进程所能申请的线程的数量也是限制的,所以如何使用和调度多线程是一个重要的环节。在系统运行阶段,进行调优的时候不可避免的要面对多线程的资源相关的几个关注点:thread stack size、thread local heap、garbage collection affects、intimate shared memory。

    进行多线程的设计编码,要极力追求简洁原则,尽力将多线程的调度和编码和业务相关的逻辑进行解耦,这样多线程处理模块被抽象出来,可复用度变高,对于分析、调试、问题跟踪多线程的设计编码就不再变得臃肿复杂;避免复杂、过多的条件判断等待、wait-notify,线程意外退出或者形成死锁。

    在今天的编程生活中,我们也要面对和解决多线程编程的调试、单元测试、跟踪等方面的问题。解决这些问题也是我们的乐趣之一:)

    参考:
    Effective Java 中文版
    深入Java虚拟机
    http://java.sun.com/docs/hotspot/threads/threads.html
    http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp
    http://www.theserverside.com/tt/knowledgecenter/knowledgecenter.tss?l=ConcurrencyTestingJavaApps
  • 2007-07-11

    温习Annotation - [Java]

    Tag:Java
    现在annotation正在改变着java developer的一些开发生活,各种框架都在充分应用annotation来简化开发,我们来看看annotation到底是怎么一回事?
    为什么引入annotation?
    由于在Java的一些实际开发中,已经有了一些annotation出现的雏形.比方说webservice使用一些tag自动生成一些接口和实现,EJB的deployment descriptor对象,transient修饰符代表不能被serialization,@deprecated等。那么他们的共用特性是什么呢?尽管这些数据不会直接对Java的语义产生影响,但是是通过工具、库或者运行时程序的语义来散发影响力的。以这种方式出现的新功能称之为annotation机制。所以Java是在增强Java语言支持易于开发的背景下提出annotation机制的,annotation对于已有的Java机制的影响力主要在于Java source files、Java class files和运行时的reflection;主要关联java classes,interfaces,methods和fields。(,或者能够能够被javac compiler或者其他的工具读取、或者作为配置项能够被存贮在class文件中、或者在运行时用Java reflection API检测的到。)
    虽然在第一天提出annotation这个特性,就一直在强调:annotion易于使用,但是应用开发者无须自己定义annotaion类型。但是不论是更好的使用现有的annotation类型,还是便于有朝一日自己确实需要扩展annotion类型,或者更好的理解开发模型,更好的理解程序的开发运行,都非常有必要了解一些annotation的。

    我们看一下Meta-annotations,看到底如何自己定义一个annnotation类型。Meta-annotations主要包括四个部分:
    Target
    Retention
    Documented
    Inherited
    Target里面的ElementType是用来指定Annotation类型可以用在哪一些元素上的.
     Target(ElementType.TYPE)-can be applied to any element of a class
     Target(ElementType.FIELD)-can be applied to a field or property
     Target(ElementType.METHOD)-can be applied to a method level annotation
     Target(ElementType.PARAMETER)-can be applied to the parameters of a method
     Target(ElementType.CONSTRUCTOR)-can be applied to constructors
     Target(ElementType.LOCAL_VARIABLE)-can be applied to local variables
     Target(ElementType.ANNOTATION_TYPE)-indicates that the declared type itself is an annotation typ
    Retention用来指定annotation类型保留在哪儿和多长时间。
     RetentionPolicy.SOURCE-Annotations with this type will be by retained only at the source level and will be ignored by the compiler
     RetentionPolicy.CLASS-Annotations with this type will be by retained by the compiler at compile time, but will be ignored by the VM
     RetentionPolicy.RUNTIME-Annotations with this type will be retained by the VM so they can be read only at run-time
    Documented用来指定这个annotation类型应当被javadoc工具文档化使用。
    Inherited用来指定使用这个类型的类是个继承来的。
    举一个简单的例子:
    测试用例:
    public class AnnotationTestableTeseCase {

        @Test
        public void go() throws SecurityException, ClassNotFoundException {
            int passed = 0, failed = 0;
            for (Method m : Class.forName(Foo.class.getName()).getMethods()) {
                if (m.isAnnotationPresent(Testable.class)) {
                    try {
                        m.invoke(null);
                        passed++;
                    } catch (Throwable ex) {

                        failed++;
                    }
                }
            }
            Assert.assertEquals(2, passed);
            Assert.assertEquals(2, failed);
        }
    }
    Annotation类型定义类:
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Testable {
    }
    使用Testable类型的客户端代码:
    public class Foo {
        @Testable
        public static void m1() {
        }

        public static void m2() {
        }

        @Testable
        public static void m3() {
            throw new RuntimeException("Boom");
        }

        public static void m4() {
        }

        @Testable
        public static void m5() {
        }

        public static void m6() {
        }

        @Testable
        public static void m7() {
            throw new RuntimeException("Crash");
        }

        public static void m8() {
        }
    }
    Okey!
    参考:
    http://java.sun.com/developer/technicalArticles/releases/j2se15/
    http://java.sun.com/j2se/1.5.0/docs/guide/language/annotations.html
    http://java.sun.com/j2se/1.5.0/docs/guide/apt/GettingStarted.html
    http://www.developer.com/java/other/article.php/10936_3556176_3