本系列的文章转载自infoq专栏合集《Java深度历险》,10篇文章分别由浅到深描述了Java的多个细节,具体包括
1. Java 字节代码的操纵 2. Java类的加载、链接和初始化 3. Java线程:基本概念、可见性与同步
4. Java垃圾回收机制与引用类型 5. Java泛型 6. Java注解 7. Java反射与动态代理
8. Java I/O 9. Java安全 10. Java对象序列化与RMI,
相信通过十个主题的学习,读者能对Java的这几个方面有较深入的理解。enjoy it!
Java反射与动态代理
以前文章中介绍Java注解的时候,多次提到了Java的反射API。与javax.lang.model不同的是,通过反射API可以获取程序在运行时刻的内部结构。反射API中提供的动态代理也是非常强大的功能,可以原生实现AOP中 的方法拦截功能。正如英文单词reflection的含义一样,使用反射API的时候就好像在看一个Java类在水中的倒影一样。知道了Java类的内部 结构之后,就可以与它进行交互,包括创建新的对象和调用对象中的方法等。这种交互方式与直接在源代码中使用的效果是相同的,但是又额外提供了运行时刻的灵活性。使用反射的一个最大的弊端是性能比较差。相同的操作,用反射API所需的时间大概比直接的使用要慢一两个数量级。不过现在的JVM实现中,反射操作的性能已经有了很大的提升。在灵活性与性能之间,总是需要进行权衡的。应用可以在适当的时机来使用反射API。
基本用法
Java 反射API的第一个主要作用是获取程序在运行时刻的内部结构。这对于程序的检查工具和调试器来说,是非常实用的功能。只需要短短的十几行代码,就可以遍历出来一个Java类的内部结构,包括其中的构造方法、声明的域和定义的方法等。这不得不说是一个很强大的能力。只要有了java.lang.Class类 的对象,就可以通过其中的方法来获取到该类中的构造方法、域和方法。对应的方法分别是getConstructor、getField和getMethod。这三个方法还有相应的getDeclaredXXX版本,区别在于getDeclaredXXX版本的方法只会获取该类自身所声明的元素,而不会考虑继承下来的。Constructor、Field和Method这三个类分别表示类中的构造方法、域和方法。这些类中的方法可以获取到所对应结构的元数据。
反射API的另外一个作用是在运行时刻对一个Java对象进行操作。 这些操作包括动态创建一个Java类的对象,获取某个域的值以及调用某个方法。在Java源代码中编写的对类和对象的操作,都可以在运行时刻通过反射API来实现。考虑下面一个简单的Java类。
1 2 3 4 5 6 7 8 9 | class MyClass { public int count; public MyClass(int start) { count = start; } public void increase(int step) { count = count + step; } } |
class MyClass { public int count; public MyClass(int start) { count = start; } public void increase(int step) { count = count + step; } }
使用一般做法和反射API都非常简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 | MyClass myClass = new MyClass(0); //一般做法 myClass.increase(2); System.out.println("Normal -> " + myClass.count); try { Constructor constructor = MyClass.class.getConstructor(int.class); //获取构造方法 MyClass myClassReflect = constructor.newInstance(10); //创建对象 Method method = MyClass.class.getMethod("increase", int.class); //获取方法 method.invoke(myClassReflect, 5); //调用方法 Field field = MyClass.class.getField("count"); //获取域 System.out.println("Reflect -> " + field.getInt(myClassReflect)); //获取域的值 } catch (Exception e) { e.printStackTrace(); } |
MyClass myClass = new MyClass(0); //一般做法 myClass.increase(2); System.out.println("Normal -> " + myClass.count); try { Constructor constructor = MyClass.class.getConstructor(int.class); //获取构造方法 MyClass myClassReflect = constructor.newInstance(10); //创建对象 Method method = MyClass.class.getMethod("increase", int.class); //获取方法 method.invoke(myClassReflect, 5); //调用方法 Field field = MyClass.class.getField("count"); //获取域 System.out.println("Reflect -> " + field.getInt(myClassReflect)); //获取域的值 } catch (Exception e) { e.printStackTrace(); }
由于数组的特殊性,Array类提供了一系列的静态方法用来创建数组和对数组中的元素进行访问和操作。
1 2 3 4 | Object array = Array.newInstance(String.class, 10); //等价于 new String[10] Array.set(array, 0, "Hello"); //等价于array[0] = "Hello" Array.set(array, 1, "World"); //等价于array[1] = "World" System.out.println(Array.get(array, 0)); //等价于array[0] |
Object array = Array.newInstance(String.class, 10); //等价于 new String[10] Array.set(array, 0, "Hello"); //等价于array[0] = "Hello" Array.set(array, 1, "World"); //等价于array[1] = "World" System.out.println(Array.get(array, 0)); //等价于array[0]
使用Java反射API的时候可以绕过Java默认的访问控制检查,比如可以直接获取到对象的私有域的值或是调用私有方法。只需要在获取到Constructor、Field和Method类的对象之后,调用setAccessible方法并设为true即可。有了这种机制,就可以很方便的在运行时刻获取到程序的内部状态。
处理泛型
Java 5中引入了泛型的概念之后,Java反射API也做了相应的修改,以提供对泛型的支持。由于类型擦除机制的存在,泛型类中的类型参数等信息,在运行时刻是不存在的。JVM看到的都是原始类型。对此,Java 5对Java类文件的格式做了修订,添加了Signature属性,用来包含不在JVM类型系统中的类型信息。比如以java.util.List接口为例,在其类文件中的Signature属性的声明是<E:Ljava/lang/Object;>Ljava/lang/Object;Ljava/util/Collection<TE;>;; ,这就说明List接口有一个类型参数E。在运行时刻,JVM会读取Signature属性的内容并提供给反射API来使用。
比如在代码中声明了一个域是List<String>类型的,虽然在运行时刻其类型会变成原始类型List,但是仍然可以通过反射来获取到所用的实际的类型参数。
1 2 3 4 5 6 7 8 9 10 11 12 | Field field = Pair.class.getDeclaredField("myList"); //myList的类型是List Type type = field.getGenericType(); if (type instanceof ParameterizedType) { ParameterizedType paramType = (ParameterizedType) type; Type[] actualTypes = paramType.getActualTypeArguments(); for (Type aType : actualTypes) { if (aType instanceof Class) { Class clz = (Class) aType; System.out.println(clz.getName()); //输出java.lang.String } } } |
Field field = Pair.class.getDeclaredField("myList"); //myList的类型是List Type type = field.getGenericType(); if (type instanceof ParameterizedType) { ParameterizedType paramType = (ParameterizedType) type; Type[] actualTypes = paramType.getActualTypeArguments(); for (Type aType : actualTypes) { if (aType instanceof Class) { Class clz = (Class) aType; System.out.println(clz.getName()); //输出java.lang.String } } }
动态代理
熟悉设计模式的人对于代理模式可 能都不陌生。 代理对象和被代理对象一般实现相同的接口,调用者与代理对象进行交互。代理的存在对于调用者来说是透明的,调用者看到的只是接口。代理对象则可以封装一些内部的处理逻辑,如访问控制、远程通信、日志、缓存等。比如一个对象访问代理就可以在普通的访问机制之上添加缓存的支持。这种模式在RMI和EJB中都得到了广泛的使用。传统的代理模式的实现,需要在源代码中添加一些附加的类。这些类一般是手写或是通过工具来自动生成。JDK 5引入的动态代理机制,允许开发人员在运行时刻动态的创建出代理类及其对象。在运行时刻,可以动态创建出一个实现了多个接口的代理类。每个代理类的对象都会关联一个表示内部处理逻辑的InvocationHandler接 口的实现。当使用者调用了代理对象所代理的接口中的方法的时候,这个调用的信息会被传递给InvocationHandler的invoke方法。在 invoke方法的参数中可以获取到代理对象、方法对应的Method对象和调用的实际参数。invoke方法的返回值被返回给使用者。这种做法实际上相 当于对方法调用进行了拦截。熟悉AOP的人对这种使用模式应该不陌生。但是这种方式不需要依赖AspectJ等AOP框架。
下面的代码用来代理一个实现了List接口的对象。所实现的功能也非常简单,那就是禁止使用List接口中的add方法。如果在getList中传入一个实现List接口的对象,那么返回的实际就是一个代理对象,尝试在该对象上调用add方法就会抛出来异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 | public List getList(final List list) { return (List) Proxy.newProxyInstance(DummyProxy.class.getClassLoader(), new Class[] { List.class }, new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("add".equals(method.getName())) { throw new UnsupportedOperationException(); } else { return method.invoke(list, args); } } }); } |
public List getList(final List list) { return (List) Proxy.newProxyInstance(DummyProxy.class.getClassLoader(), new Class[] { List.class }, new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("add".equals(method.getName())) { throw new UnsupportedOperationException(); } else { return method.invoke(list, args); } } }); }
这里的实际流程是,当代理对象的add方法被调用的时候,InvocationHandler中的invoke方法会被调用。参数method就包含了调用的基本信息。因为方法名称是add,所以会抛出相关的异常。如果调用的是其它方法的话,则执行原来的逻辑。
使用案例
Java 反射API的存在,为Java语言添加了一定程度上的动态性,可以实现某些动态语言中的功能。比如在JavaScript的代码中,可以通过 obj["set" + propName]()来根据变量propName的值找到对应的方法进行调用。虽然在Java源代码中不能这么写,但是通过反射API同样可以实现类似 的功能。这对于处理某些遗留代码来说是有帮助的。比如所需要使用的类有多个版本,每个版本所提供的方法名称和参数不尽相同。而调用代码又必须与这些不同的版本都能协同工作,就可以通过反射API来依次检查实际的类中是否包含某个方法来选择性的调用。
Java 反射API实际上定义了一种相对于编译时刻而言更加松散的契约。如果被调用的Java对象中并不包含某个方法,而在调用者代码中进行引用的话,在编译时刻就会出现错误。而反射API则可以把这样的检查推迟到运行时刻来完成。通过把Java中的字节代码增强、类加载器和反射API结合起来,可以处理一些对灵 活性要求很高的场景。
在 有些情况下,可能会需要从远端加载一个Java类来执行。比如一个客户端Java程序可以通过网络从服务器端下载Java类来执行,从而可以实现自动更新 的机制。当代码逻辑需要更新的时候,只需要部署一个新的Java类到服务器端即可。一般的做法是通过自定义类加载器下载了类字节代码之后,定义出 Class类的对象,再通过newInstance方法就可以创建出实例了。不过这种做法要求客户端和服务器端都具有某个接口的定义,从服务器端下载的是 这个接口的实现。这样的话才能在客户端进行所需的类型转换,并通过接口来使用这个对象实例。如果希望客户端和服务器端采用更加松散的契约的话,使用反射API就可以了。两者之间的契约只需要在方法的名称和参数这个级别就足够了。服务器端Java类并不需要实现特定的接口,可以是一般的Java类。
动态代理的使用场景就更加广泛了。需要使用AOP中的方法拦截功能的地方都可以用到动态代理。Spring框架的AOP实现默认也使用动态代理。不过JDK中的动态代理只支持对接口的代理,不能对一个普通的Java类提供代理。不过这种实现在大部分的时候已经够用了。
参考资料
- Classworking toolkit: Reflecting generics
- D?ecorating with dynamic proxies
Java I/O
在应用程序中,通常会涉及到两种类型的计算:CPU计算和I/O计算。对于大多数应用来说,花费在等待I/O上的时间是占较大比重的。通常需要等待速度较慢的磁盘或是网络连接完成I/O请求,才能继续后面的CPU计算任务。因此提高I/O操作的效率对应用的性能有较大的帮助。本文将介绍Java语言中与I/O操作相关的内容,包括基本的Java I/O和Java NIO,着重于基本概念和最佳实践。
流
Java语言提供了多个层次不同的概念来对I/O操作进行抽象。Java I/O中最早的概念是流,包括输入流和输出流,早在JDK 1.0中就存在了。简单的来说,流是一个连续的字节的序列。输入流是用来读取这个序列,而输出流则构建这个序列。InputStream和OutputStream所操纵的基本单元就是字节。每次读取和写入单个字节或是字节数组。如果从字节的层次来处理数据类型的话,操作会非常繁琐。可以用更易使用的流实现来包装基本的字节流。如果想读取或输出Java的基本数据类型,可以使用DataInputStream和DataOutputStream。它们所提供的类似readFloat和writeDouble这样的方法,会让处理基本数据类型变得很简单。如果希望读取或写入的是Java中的对象的话,可以使用ObjectInputStream和ObjectOutputStream。它们与对象的序列化机制一起,可以实现Java对象状态的持久化和数据传递。基本流所提供的对于输入和输出的控制比较弱。InputStream只提供了顺序读取、跳过部分字节和标记/重置的支持,而OutputStream则只能顺序输出。
流的使用
由于I/O操作所对应的实体在系统中都是有限的资源,需要妥善的进行管理。每个打开的流都需要被正确的关闭以释放资源。所遵循的原则是谁打开谁释放。如果一个流只在某个方法体内使用,则通过finally语句或是JDK 7中的try-with-resources语句来确保在方法返回之前,流被正确的关闭。如果一个方法只是作为流的使用者,就不需要考虑流的关闭问题。典型的情况是在servlet实现中并不需要关闭HttpServletResponse中的输出流。如果你的代码需要负责打开一个流,并且需要在不同的对象之间进行传递的话,可以考虑使用Execute Around Method模式。如下面的代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | public void use(StreamUser user) { InputStream input = null; try { input = open(); user.use(input); } catch(IOException e) { user.onError(e); } finally { if (input != null) { try { input.close(); } catch (IOException e) { user.onError(e); } } } } |
public void use(StreamUser user) { InputStream input = null; try { input = open(); user.use(input); } catch(IOException e) { user.onError(e); } finally { if (input != null) { try { input.close(); } catch (IOException e) { user.onError(e); } } } }
如上述代码中所看到的一样,由专门的类负责流的打开和关闭。流的使用者StreamUser并不需要关心资源释放的细节,只需要对流进行操作即可。
在使用输入流的过程中,经常会遇到需要复用一个输入流的情况,即多次读取一个输入流中的内容。比如通过URL.openConnection方法打开了一个远端站点连接的输入流,希望对其中的内容进行多次处理。这就需要把一个InputStream对象在多个对象中传递。为了保证每个使用流的对象都能获取到正确的内容,需要对流进行一定的处理。通常有两种解决的办法,一种是利用InputStream的标记支持。如果一个流支持标记的话(通过markSupported方法判断),就可以在流开始的地方通过mark方法添加一个标记,当完成一次对流的使用之后,通过reset方法就可以把流的读取位置重置到上次标记的位置,即流开始的地方。如此反复,就可以复用这个输入流。大部分输入流的实现是不支持标记的。可以通过BufferedInputStream进行包装来支持标记。
1 2 3 4 5 6 7 8 9 | private InputStream prepareStream(InputStream ins) { BufferedInputStream buffered = new BufferedInputStream(ins); buffered.mark(Integer.MAX_VALUE); return buffered; } private void resetStream(InputStream ins) throws IOException { ins.reset(); ins.mark(Integer.MAX_VALUE); } |
private InputStream prepareStream(InputStream ins) { BufferedInputStream buffered = new BufferedInputStream(ins); buffered.mark(Integer.MAX_VALUE); return buffered; } private void resetStream(InputStream ins) throws IOException { ins.reset(); ins.mark(Integer.MAX_VALUE); }
如上面的代码所示,通过prepareStream方法可以用一个BufferedInputStream来包装基本的InputStream。通过 mark方法在流开始的时候添加一个标记,允许读入Integer.MAX_VALUE个字节。每次流使用完成之后,通过resetStream方法重置即可。
另外一种做法是把输入流的内容转换成字节数组,进而转换成输入流的另外一个实现ByteArrayInputStream。这样做的好处是使用字节数组作为参数传递的格式要比输入流简单很多,可以不需要考虑资源相关的问题。另外也可以尽早的关闭原始的输入流,而无需等待所有使用流的操作完成。这两种做法的思路其实是相似的。BufferedInputStream在内部也创建了一个字节数组来保存从原始输入流中读入的内容。
1 2 3 4 5 6 7 8 9 10 11 12 | private byte[] saveStream(InputStream input) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(1024); ReadableByteChannel readChannel = Channels.newChannel(input); ByteArrayOutputStream output = new ByteArrayOutputStream(32 * 1024); WritableByteChannel writeChannel = Channels.newChannel(output); while ((readChannel.read(buffer)) > 0 || buffer.position() != 0) { buffer.flip(); writeChannel.write(buffer); buffer.compact(); } return output.toByteArray(); } |
private byte[] saveStream(InputStream input) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(1024); ReadableByteChannel readChannel = Channels.newChannel(input); ByteArrayOutputStream output = new ByteArrayOutputStream(32 * 1024); WritableByteChannel writeChannel = Channels.newChannel(output); while ((readChannel.read(buffer)) > 0 || buffer.position() != 0) { buffer.flip(); writeChannel.write(buffer); buffer.compact(); } return output.toByteArray(); }
上面的代码中saveStream方法把一个InputStream保存为字节数组。
缓冲区
由于流背后的数据有可能比较大,在实际的操作中,通常会使用缓冲区来提高性能。传统的缓冲区的实现是使用数组来完成。比如经典的从InputStream到OutputStream的复制的实现,就是使用一个字节数组作为中间的缓冲区。NIO中引入的Buffer类及其子类,可以很方便的用来创建各种基本数据类型的缓冲区。相对于数组而言,Buffer类及其子类提供了更加丰富的方法来对其中的数据进行操作。后面会提到的通道也使用Buffer类进行数据传递。
在Buffer上进行的元素添加和删除操作,都围绕3个属性position、limit和capacity展开,分别表示Buffer当前的读写位置、可用的读写范围和容量限制。容量限制是在创建的时候指定的。Buffer提供的get/put方法都有相对和绝对两种形式。相对读写时的位置是相对于position的值,而绝对读写则需要指定起始的序号。在使用Buffer的常见错误就是在读写操作时没有考虑到这3个元素的值,因为大多数时候都是使用的是相对读写操作,而position的值可能早就发生了变化。一些应该注意的地方包括:将数据读入缓冲区之前,需要调用clear方法;将缓冲区中的数据输出之前,需要调用flip方法。
1 2 3 4 | ByteBuffer buffer = ByteBuffer.allocate(32); CharBuffer charBuffer = buffer.asCharBuffer(); String content = charBuffer.put("Hello ").put("World").flip().toString(); System.out.println(content); |
ByteBuffer buffer = ByteBuffer.allocate(32); CharBuffer charBuffer = buffer.asCharBuffer(); String content = charBuffer.put("Hello ").put("World").flip().toString(); System.out.println(content);
上面的代码展示了Buffer子类的使用。首先可以在已有的ByteBuffer上面创建出其它数据类型的缓冲区视图,其次Buffer子类的很多方法是可以级联的,最后是要注意flip方法的使用。
字符与编码
在程序中,总是免不了与字符打交道,毕竟字符是用户直接可见的信息。而与字符处理直接相关的就是编码。相信不少人都曾经为了程序中的乱码问题而困扰。要弄清楚这个问题,就需要理解字符集和编码的概念。字符集,顾名思义,就是字符的集合。一个字符集中所包含的字符通常与地区和语言有关。字符集中的每个字符通常会有一个整数编码与其对应。常见的字符集有ASCII、ISO-8859-1和Unicode等。对于字符集中的每个字符,为了在计算机中表示,都需要转换某种字节的序列,即该字符的编码。同一个字符集可以有不同的编码方式。如果某种编码格式产生的字节序列,用另外一种编码格式来解码的话,就可能会得到错误的字符,从而产生乱码的情况。所以将一个字节序列转换成字符串的时候,需要知道正确的编码格式。
NIO中的java.nio.charset包提供了与字符集相关的类,可以用来进行编码和解码。其中的CharsetEncoder和CharsetDecoder允许对编码和解码过程进行精细的控制,如处理非法的输入以及字符集中无法识别的字符等。通过这两个类可以实现字符内容的过滤。比如应用程序在设计的时候就只支持某种字符集,如果用户输入了其它字符集中的内容,在界面显示的时候就是乱码。对于这种情况,可以在解码的时候忽略掉无法识别的内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | String input = "你123好"; Charset charset = Charset.forName("ISO-8859-1"); CharsetEncoder encoder = charset.newEncoder(); encoder.onUnmappableCharacter(CodingErrorAction.IGNORE); CharsetDecoder decoder = charset.newDecoder(); CharBuffer buffer = CharBuffer.allocate(32); buffer.put(input); buffer.flip(); try { ByteBuffer byteBuffer = encoder.encode(buffer); CharBuffer cbuf = decoder.decode(byteBuffer); System.out.println(cbuf); //输出123 } catch (CharacterCodingException e) { e.printStackTrace(); } |
String input = "你123好"; Charset charset = Charset.forName("ISO-8859-1"); CharsetEncoder encoder = charset.newEncoder(); encoder.onUnmappableCharacter(CodingErrorAction.IGNORE); CharsetDecoder decoder = charset.newDecoder(); CharBuffer buffer = CharBuffer.allocate(32); buffer.put(input); buffer.flip(); try { ByteBuffer byteBuffer = encoder.encode(buffer); CharBuffer cbuf = decoder.decode(byteBuffer); System.out.println(cbuf); //输出123 } catch (CharacterCodingException e) { e.printStackTrace(); }
上面的代码中,通过使用ISO-8859-1字符集的编码和解码器,就可以过滤掉字符串中不在此字符集中的字符。
Java I/O在处理字节流字之外,还提供了处理字符流的类,即Reader/Writer类及其子类,它们所操纵的基本单位是char类型。在字节和字符之间的桥梁就是编码格式。通过编码器来完成这两者之间的转换。在创建Reader/Writer子类实例的时候,总是应该使用两个参数的构造方法,即显式指定使用的字符集或编码解码器。如果不显式指定,使用的是JVM的默认字符集,有可能在其它平台上产生错误。
通道
通道作为NIO中的核心概念,在设计上比之前的流要好不少。通道相关的很多实现都是接口而不是抽象类。通道本身的抽象层次也更加合理。通道表示的是对支持I/O操作的实体的一个连接。一旦通道被打开之后,就可以执行读取和写入操作,而不需要像流那样由输入流或输出流来分别进行处理。与流相比,通道的操作使用的是Buffer而不是数组,使用更加方便灵活。通道的引入提升了I/O操作的灵活性和性能,主要体现在文件操作和网络操作上。
文件通道
对文件操作方面,文件通道FileChannel提供了与其它通道之间高效传输数据的能力,比传统的基于流和字节数组作为缓冲区的做法,要来得简单和快速。比如下面的把一个网页的内容保存到本地文件的实现。
1 2 3 4 5 6 | FileOutputStream output = new FileOutputStream("baidu.txt"); FileChannel channel = output.getChannel(); URL url = new URL("http://www.baidu.com"); InputStream input = url.openStream(); ReadableByteChannel readChannel = Channels.newChannel(input); channel.transferFrom(readChannel, 0, Integer.MAX_VALUE); |
FileOutputStream output = new FileOutputStream("baidu.txt"); FileChannel channel = output.getChannel(); URL url = new URL("http://www.baidu.com"); InputStream input = url.openStream(); ReadableByteChannel readChannel = Channels.newChannel(input); channel.transferFrom(readChannel, 0, Integer.MAX_VALUE);
文件通道的另外一个功能是对文件的部分片段进行加锁。当在一个文件上的某个片段加上了排它锁之后,其它进程必须等待这个锁释放之后,才能访问该文件的这个片段。文件通道上的锁是由JVM所持有的,因此适合于与其它应用程序协同时使用。比如当多个应用程序共享某个配置文件的时候,如果Java程序需要更新此文件,则可以首先获取该文件上的一个排它锁,接着进行更新操作,再释放锁即可。这样可以保证文件更新过程中不会受到其它程序的影响。
另外一个在性能方面有很大提升的功能是内存映射文件的支持。通过FileChannel的map方法可以创建出一个MappedByteBuffer对象,对这个缓冲区的操作都会直接反映到文件内容上。这点尤其适合对大文件进行读写操作。
套接字通道
在套接字通道方面的改进是提供了对非阻塞I/O和多路复用I/O的支持。传统的流的I/O操作是阻塞式的。在进行I/O操作的时候,线程会处于阻塞状态等待操作完成。NIO中引入了非阻塞I/O的支持,不过只限于套接字I/O操作。所有继承自SelectableChannel的通道类都可以通过configureBlocking方法来设置是否采用非阻塞模式。在非阻塞模式下,程序可以在适当的时候查询是否有数据可供读取。一般是通过定期的轮询来实现的。
多路复用I/O是一种新的I/O编程模型。传统的套接字服务器的处理方式是对于每一个客户端套接字连接,都新创建一个线程来进行处理。创建线程是很耗时的操作,而有的实现会采用线程池。不过一个请求一个线程的处理模型并不是很理想。原因在于耗费时间创建的线程,在大部分时间可能处于等待的状态。而多路复用I/O的基本做法是由一个线程来管理多个套接字连接。该线程会负责根据连接的状态,来进行相应的处理。多路复用I/O依靠操作系统提供的select或相似系统调用的支持,选择那些已经就绪的套接字连接来处理。可以把多个非阻塞I/O通道注册在某个Selector上,并声明所感兴趣的操作类型。每次调用Selector的select方法,就可以选择到某些感兴趣的操作已经就绪的通道的集合,从而可以进行相应的处理。如果要执行的处理比较复杂,可以把处理转发给其它的线程来执行。
下面是一个简单的使用多路复用I/O的服务器实现。当有客户端连接上的时候,服务器会返回一个Hello World作为响应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | private static class IOWorker implements Runnable { public void run() { try { Selector selector = Selector.open(); ServerSocketChannel channel = ServerSocketChannel.open(); channel.configureBlocking(false); ServerSocket socket = channel.socket(); socket.bind(new InetSocketAddress("localhost", 10800)); channel.register(selector, channel.validOps()); while (true) { selector.select(); Iterator iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); if (!key.isValid()) { continue; } if (key.isAcceptable()) { ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); SocketChannel sc = ssc.accept(); sc.configureBlocking(false); sc.register(selector, sc.validOps()); } if (key.isWritable()) { SocketChannel client = (SocketChannel) key.channel(); Charset charset = Charset.forName("UTF-8"); CharsetEncoder encoder = charset.newEncoder(); CharBuffer charBuffer = CharBuffer.allocate(32); charBuffer.put("Hello World"); charBuffer.flip(); ByteBuffer content = encoder.encode(charBuffer); client.write(content); key.cancel(); } } } } catch (IOException e) { e.printStackTrace(); } } } |
private static class IOWorker implements Runnable { public void run() { try { Selector selector = Selector.open(); ServerSocketChannel channel = ServerSocketChannel.open(); channel.configureBlocking(false); ServerSocket socket = channel.socket(); socket.bind(new InetSocketAddress("localhost", 10800)); channel.register(selector, channel.validOps()); while (true) { selector.select(); Iterator iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); if (!key.isValid()) { continue; } if (key.isAcceptable()) { ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); SocketChannel sc = ssc.accept(); sc.configureBlocking(false); sc.register(selector, sc.validOps()); } if (key.isWritable()) { SocketChannel client = (SocketChannel) key.channel(); Charset charset = Charset.forName("UTF-8"); CharsetEncoder encoder = charset.newEncoder(); CharBuffer charBuffer = CharBuffer.allocate(32); charBuffer.put("Hello World"); charBuffer.flip(); ByteBuffer content = encoder.encode(charBuffer); client.write(content); key.cancel(); } } } } catch (IOException e) { e.printStackTrace(); } } }
上面的代码给出的只是非常简单的示例程序,只是展示了多路复用I/O的基本使用方式。在开发复杂网络应用程序的时候,使用一些Java NIO网络应用框架会让你事半功倍。目前来说最流行的两个框架是Apache MINA和Netty。在使用了Netty之后,Twitter的搜索功能速度提升达到了3倍之多。网络应用开发人员都可以使用这两个开源的优秀框架。
参考资料
- Java 6 I/O-related APIs & Developer Guides
- Top Ten New Things You Can Do with NIO
- Building Highly Scalable Servers with Java NIO