神奇好望角 The Magical Cape of Good Hope

庸人不必自扰,智者何需千虑?
posts - 26, comments - 50, trackbacks - 0, articles - 11
  BlogJava :: 首页 ::  :: 联系 :: 聚合  :: 管理

2012年2月13日

21 世纪初,Spring 框架的诞生和崛起让沉重而腐朽的 J2EE 遭到了当头棒喝,随后大批开发人员转投 Spring 阵营,呼吸间就让 J2EE 阵营大伤元气。然而这种命悬一线的危机并没有造成毁灭性的打击,尤其是对于 Java 这种提倡开放的平台而言,取长补短,互相促进才是正道。于是,JCP 委员会痛定思痛,在 2006 年推出 Java EE 5 规范,主要是对 EJB 的开发进行了极大幅度的简化。2008 年发布的 Java EE 6 引入了 CDI、BV、JAX-RS 等一系列新功能,并且以配置文件(profile)的方式让 Java EE 向轻量级迈进了一步。特别有趣的是,Spring 框架也开始提供对某些 Java EE 注解的支持,是否标志着两大阵营开始合流?Java EE 7 预定于今年下半年发布,目标是支持云计算。最近几年来,云计算一直被炒作,却从来没有一个准确的定义和规范,希望 Java EE 7 能够在 Java 界扭转这种尴尬的局面。

下面开始详细列举 Java EE 7 的新功能前瞻,数据来源于《Java Magazine 2012-01/02》中的《Cloud/Java EE: Looking Ahead to Java EE 7》一文。Java EE 7 是以“日期驱动”的方式开发的,也就是说,在计划日期到达前没有完成的功能都将被推迟到 Java EE 8。

Java EE 7(JSR-342)

  • 主题:让应用程序能够在私有或公共云上容易地运行。
  • 该平台将定义一个应用程序元数据描述符,以描述 PaaS 执行环境(例如多租户、资源共享、服务质量,以及应用程序间的依赖)。
  • 支持 HTML5、WebSocket、JSON 等新标准,并为它们一一提供 API。
  • 消除受管 Bean、EJB、Servlet、JSF、CDI 和 JAX-RS 之间不一致的地方。
  • 可能在 Web 配置文件中包含 JAX-RS 2.0 和 JMS 2.0 API 修订版。
  • 更新一些现有的技术,可能引入用于 Java EE 的并发工具(JSR-236)和 JCache(JSR-107)。

Java Persistence 2.1(JSR-338)

  • 支持多租户。
  • 支持存储过程和厂商函数。
  • 用规则(Criteria)进行更新和删除。
  • 支持数据库大纲(Scheme)的生成。
  • 持久化上下文的同步。
  • 侦听器中的 CDI 注入。

JAX-RS 2.0: The Java API for RESTful Web Services(JSR-339)

  • 客户端 API——底层使用构建者模式,可能提供上层封装。
  • 超媒体——轻松创建和处理关联了资源的链接。
  • 使用 Bean 验证框架来验证表单或查询参数。
  • @Inject 更紧密集成。
  • 服务端的异步请求处理。
  • 使用“qs”进行服务端的内容协商。

Java Servlet 3.1(JSR-340)

  • 为 Web 应用程序优化 PaaS 模型。
  • 用于安全、会话和资源的多租户。
  • 基于 NIO2 的异步 I/O。
  • 简化的异步 Servlet。
  • 利用 Java EE 并发工具。
  • 支持 WebSocket。

Expression Language 3.0(JSR-341)

  • ELContext 分离为解析和求值上下文。
  • 可定制的 EL 强迫规则。
  • 在 EL 表达式中直接引用静态方法和成员。
  • 添加运算符,例如等于、字符串连接和取大小。
  • 与 CDI 集成,例如在表达式求值前/中/后生成事件。

Java Message Service 2.0(JSR-343)

  • 简化开发——改变 JMS 编程模型,让应用程序开发变得更加简单容易。
  • 清除/澄清现有规范中的模糊之处。
  • 与 CDI 集成。
  • 澄清 JMS 和其他 Java EE 规范之间的关系。
  • 新的强制性 API允许任何 JMS 提供者能与任何 Java EE 容器集成。
  • 来自平台的多租户和其他云相关的功能。

JavaServer Faces 2.2(JSR-344)

  • 简化开发——使配置选项动态化,使复合组件中的 cc:interface 可选,Facelet 标记库的速记 URL,与 CDI 集成,JSF 组件的 OSGi 支持。
  • 支持 Portlet 2.0 桥(JSR-329)的实现。
  • 支持 HTML5 的功能,例如 HTML5 表单、元数据、头部和区段内容模型。
  • 流管理,页面导航事件的侦听器,以及 fileUploadBackButton 等新组件。

Enterprise JavaBeans 3.2(JSR-345)

  • 增强 EJB 架构以支持 PaaS,例如多租户。
  • 对在 EJB 外使用容器管理的事务进行工厂化。
  • 更进一步使用注解。
  • 与平台中的其他规范对齐和集成。

Contexts and Dependency Injection 1.1(JSR-346)

  • 拦截器的全局排序和管理内建上下文的装饰器 API。
  • 可在 Java EE 容器外启动的嵌入式模式。
  • 声明式地控制归档中的哪些包和 Bean 将被扫描。
  • 注入日志之类的静态成员。
  • 将 Servlet 事件作为 CDI 事件发送。

Bean Validation 1.1(JSR-349)

  • 与其他 Java EE 规范集成。
  • JAX-RS:在 HTTP 调用中验证参数和返回值。
  • JAXB:将约束条件转换到 XML 模式描述符中。
  • 方法级别的验证。
  • 在组集合上应用约束条件。
  • 扩展模型以支持“与”和“或”风格的组合。

JCache: Java Temporary Caching API(JSR-107)

  • 在内存中暂存 Java 对象的 API 和语义,包括对象的创建、共享访问、缓存池、失效,以及跨 JVM 的一致性。

Java State Management(JSR-350)

  • 应用程序和 Java EE 容器可使用该 API 将状态管理的任务交给具有不同 QoS 特征的第三方提供者。
  • 基于 Java SE 的调用者可通过查询状态提供者来访问状态数据。
  • 可添加具有不同 QoS 的提供者,API 调用者能够按自己的规则进行查询。

Batch Applications for the Java Platform(JSR-352)

  • 用于批处理应用程序的编程模型,以及用于调度和执行工作的运行时。
  • 为标准编程模型定义批处理工作、批处理工作步骤、批处理应用程序、批处理执行器和批处理工作管理器。

Concurrency Utilities for Java EE(JSR-236)

  • 提供一个整洁、简单且独立的 API,使其能用于任何 Java EE 容器中。

Java API for JSON Processing(JSR-353)

  • 处理 JSON 的 Java API。

posted @ 2012-02-13 22:23 蜀山兆孨龘 阅读(5545) | 评论 (0)编辑 收藏

2012年2月9日

ForkJoinPool 是 Java SE 7 新功能“分叉/结合框架”的核心类,现在可能乏人问津,但我觉得它迟早会成为主流。分叉/结合框架是一个比较特殊的线程池框架,专用于需要将一个任务不断分解成子任务(分叉),再不断进行汇总得到最终结果(结合)的计算过程。比起传统的线程池类 ThreadPoolExecutorForkJoinPool 实现了工作窃取算法,使得空闲线程能够主动分担从别的线程分解出来的子任务,从而让所有的线程都尽可能处于饱满的工作状态,提高执行效率。

ForkJoinPool 提供了三类方法来调度子任务:

execute 系列
异步执行指定的任务。
invokeinvokeAll
执行指定的任务,等待完成,返回结果。
submit 系列
异步执行指定的任务并立即返回一个 Future 对象。

子任务由 ForkJoinTask 的实例来代表。它是一个抽象类,JDK 为我们提供了两个实现:RecursiveTaskRecursiveAction,分别用于需要和不需要返回计算结果的子任务。ForkJoinTask 提供了三个静态的 invokeAll 方法来调度子任务,注意只能在 ForkJoinPool 执行计算的过程中调用它们。

ForkJoinPoolForkJoinTask 还提供了很多让人眼花缭乱的公共方法,其实它们大多数都是其内部实现去调用的,对于应用开发人员来说意义不大。

下面以统计 D 盘文件个数为例。这实际上是对一个文件树的遍历,我们需要递归地统计每个目录下的文件数量,最后汇总,非常适合用分叉/结合框架来处理:

// 处理单个目录的任务
public class CountingTask extends RecursiveTask<Integer> {
    private Path dir;

    public CountingTask(Path dir) {
        this.dir = dir;
    }

    @Override
    protected Integer compute() {
        int count = 0;
        List<CountingTask> subTasks = new ArrayList<>();

        // 读取目录 dir 的子路径。
        try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) {
            for (Path subPath : ds) {
                if (Files.isDirectory(subPath, LinkOption.NOFOLLOW_LINKS)) {
                    // 对每个子目录都新建一个子任务。
                    subTasks.add(new CountingTask(subPath));
                } else {
                    // 遇到文件,则计数器增加 1。
                    count++;
                }
            }

            if (!subTasks.isEmpty()) {
                // 在当前的 ForkJoinPool 上调度所有的子任务。
                for (CountingTask subTask : invokeAll(subTasks)) {
                    count += subTask.join();
                }
            }
        } catch (IOException ex) {
            return 0;
        }
        return count;
    }
}

// 用一个 ForkJoinPool 实例调度“总任务”,然后敬请期待结果……
Integer count = new ForkJoinPool().invoke(new CountingTask(Paths.get("D:/")));
    

在我的笔记本上,经多次运行这段代码,耗费的时间稳定在 600 豪秒左右。普通线程池(Executors.newCachedThreadPool())耗时 1100 毫秒左右,足见工作窃取的优势。

结束本文前,我们来围观一个最神奇的结果:单线程算法(使用 Files.walkFileTree(...))比这两个都快,平均耗时 550 毫秒!这警告我们并非引入多线程就能优化性能,并须要先经过多次测试才能下结论。

posted @ 2012-02-09 10:40 蜀山兆孨龘 阅读(2423) | 评论 (2)编辑 收藏

2012年1月19日

前面已经看到,Socket 类的 getInputStream()getOutStream() 方法分别获取套接字的输入流和输出流。输入流用来读取远端发送过来的数据,输出流则用来向远端发送数据。

输入流

使用套接字的输入流读取数据时,当前线程会进入阻塞状态,直到套接字收到一些数据为止(亦即套接字的接收缓冲区有可用数据)。该输入流的 available() 方法只是返回接收缓冲区的可用字节数量,不可能知道远端还要发送多少字节。使用输入流的时候,最好先将它包装为一个 BufferedInputStream,因为读取接收缓冲区将导致 JVM 和底层系统之间的切换,应当尽量减少切换次数以提高性能。BufferedInputStream 的缓冲区大小最好设为套接字接收缓冲区的大小。

如果直接调用输入流的 close() 方法来关闭它,则将导致套接字被关闭。对此,Socket 类提供了一个 shutdownInput() 方法来禁用输入流。调用该方法后,每次读操作都将返回 EOF,无法再读取远端发送的数据。对这个 EOF 的检测,不同的输入流包装体现出不同的结果,可能读到 -1 个字节,可能读到的字符串为 null,还可能收到一个 EOFException 等等。禁用输入流后,远端输出流的行为是平台相关的:

  • 在 BSD 平台上,远端的发送的数据能正常接收,然后直接丢弃。远端无法知道本端的输入流已禁用。这和 JDK 文档描述的行为一致。
  • 在 WINSOCK 平台上,远端发送数据将会导致“连接被重置”的错误。
  • 在 Linux 平台上,远端发送的数据能继续接收,直到套接字输入缓冲区填满,之后远端再也无法发送数据(若使用阻塞模式则进入死锁)。

禁用输入流这种技术并不常用。

输出流

套接字的输出操作实际上仅仅将数据写到发送缓冲区内,当发送缓冲区填满且上次的发送成功后,由底层系统负责发送。如果发送缓冲区的剩余空间不够,当前线程就会阻塞。和输入流类似,最好将输出流包装为 BufferedOutputStream

如果套接字的双发都使用 ObjectInputStreamObjectOutputStream 来读写 Java 对象,则必须先创建 ObjectOutputStream,因为 ObjectInputStream 在构造的时候会试图读取对象头部,如果双发都先创建 ObjectInputStream,则会互相等待对方的输出,造成死锁:

// 创建的顺序不能颠倒!
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
    

类似于输入流,关闭输出流也导致关闭套接字,所以 Socket 类同样提供了一个 shutdownOutput() 来禁用输出流。禁用输出流后,已写入发送缓冲区的数据会正常发送,之后的任何写操作都会导致 IOException,且远端的输入流始终会读到 EOF。禁用输出流非常有用,例如套接字的双发都在发送完毕数据后禁用输入流,然后双方都会收到 EOF,从而知道数据已经全部交换完毕,可以安全关闭套接字。直接关闭套接字会同时关闭输入流和输出流,且断开连接,达不到这种效果。

使用流的阻塞套接字的优缺点

如果要使用流进行输入和输出,就只能用阻塞模式的套接字。这里总结一下阻塞套接字的优缺点。先看看优点:

  1. 编程模型简单,非常适合初学者上手。
  2. 以装饰器模式设计的 Java I/O 使得开发人员可以轻松地从 I/O 流读写任何类型的数据。

但在性能方面有致命的缺点:

  1. 由于服务器套接字接受连接,以及套接字的读写都会阻塞,性能低下。
  2. 如果不对 I/O 流手动进行缓冲,则可能造成一次只处理一个字节,性能低下。
  3. 服务器套接字每次只能接受一个连接,导致 JVM 和底层系统之间频繁的调用切换,性能低下。

下一篇文章开始探讨使用基于 NIO 的套接字通道和缓冲区实现伸缩性更强的 TCP 套接字。

posted @ 2012-01-19 14:37 蜀山兆孨龘 阅读(2094) | 评论 (1)编辑 收藏

2012年1月16日

ServerSocket 类和 Socket 类都提供了多个公共构造方法。不同的构造方法不仅带的参数不同,所具有的意义也不一样。下面分别解析这两个类的实例初始化过程。

ServerSocket 实例的初始化

ServerSocket 类提供了四个构造器:

public ServerSocket(int port) throws IOException
public ServerSocket(int port, int backlog) throws IOException
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
public ServerSocket() throws IOException

带参构造器用来创建已绑定的服务器套接字,也就是说构造成功后它就已经开始侦听指定的端口,且能够调用 accept() 方法来接受客户端连接。默认构造器则会创建未绑定的服务器套接字,构造成功后必须手动将其绑定到一个本地地址才能用,在绑定之前可以进行一些选项配置。

带参构造器

总的来说,带参构造器提供了三个参数:

port
指定该服务器套接字所要侦听的本地端口。如果为 0,则由系统自动分配一个端口号,这必须以另外的方式让客户端获取端口号。
backlog
这个名词目前还没有合适的译名。底层系统的 TCP 实现会维护一个连接队列,该队列缓存了已被 TCP 处理完毕,但还没有被服务器套接字接受的客户端连接。一旦某个连接被接受(通过调用 accept() 方法),它就会被从队列中移除。backlog 参数就用于指定队列的最大长度,默认值为 50,但这个值只是一个建议,底层系统可能根据需要自动调整。如果队列满了,则其行为是平台相关的:微软的 WINSOCK 会拒绝新的连接,其他实现则什么都不做。严格地说,微软没有遵守规范,破坏了游戏规则……
bindAddr
一台机器可能会有多个本地 IP 地址,例如同时使用多块网卡。使用其他两个带参构造器时,该参数为 null,服务器套接字会在所有的本地 IP 地址(0.0.0.0::0)上侦听。如果希望只侦听一个地址,则可使用该参数。

默认构造器

如果使用默认构造器,在绑定地址前,还可以做些配置。绑定操作由两个 bind 方法定义,参数类似于带参构造器。配置项包括以下方面(都必须在绑定前配置):

设置是否重用本地地址
该选项由 setReuseAddress(boolean on) 方法配置,对应底层系统的 SO_REUSEADDR 套接字选项。JDK 没有定义该选项的默认值。如果该选项为 false,则在关闭 TCP 连接时,为了保证可靠性,该连接可能在关闭后的一段时间(大约两分钟)内保持超时状态(通常称为 TIME_WAIT 状态或 2MSL 等待状态),这段时间里无法将新建的服务器套接字绑定到同一个地址。在开发阶段,服务器可能不断重启,打开改选项会非常有用。
设置接收缓冲区大小
该选项由 setReceiveBufferSize(int size) 方法配置,对应底层系统的 SO_RCVBUF 套接字选项,单位是字节。《RFC 1323 - TCP Extensions for High Performance》将缓冲区大小定义为 64KB。该选项只是一个建议值,底层系统可能根据需要自行调整。
设置超时值
该选项由 setSoTimeout(int timeout) 方法配置,对应底层系统的 SO_TIMEOUT 套接字选项,单位是毫秒。默认值为 0。该选项影响 accept 方法的阻塞时间长度,如果超时将引发 SocketTimeoutException。如果设为 0,则表示永不超时。
设置性能首选项
性能首选项包括连接时间、延迟和带宽三个选项,由 setPerformancePreferences(int connectionTime, int latency, int bandwidth) 方法配置。这三个数值分别表示短连接时间、低延迟和高带宽的相对重要性,数值越大则越重要;其各自的绝对值没有意义。该方法的初衷是为了让 Java 能在用非 TCP/IP 实现的套接字环境下工作得更好,某些需要对网络进行调优的程序也可以将这三个首选项作为配置参数提供给用户。

Socket 实例的初始化

Socket 类提供了六个公共构造器(已过时的除外):

public Socket(String host, int port) throws UnknownHostException, IOException
public Socket(InetAddress address, int port) throws IOException
public Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException
public Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException
public Socket()
public Socket(Proxy proxy)

前四个构造器创建已连接的客户端套接字,也就是说构造的时候就会去连接服务器。前两个构造器需要提供服务器的地址和端口作为参数,本地地址和端口由系统自动分配;后两个允许手动指定本地地址和端口,但极少使用。后两个构造器创建未连接的套接字,创建后需要调用 connect 方法手动连接,连接之前可以做一些配置。最后一个构造器接受一个代表代理服务其的 Proxy 对象,JDK 支持 HTTP 和 SOCKS(V4 或 V5)两种代理类型。

连接前的配置

在连接前,客户端套接字不仅像服务器套接字那样可以设置是否重用本地地址、缓冲区大小、超时值和性能首选项,还能够配置以下各项(都必须在连接前配置):

设置是否保持活跃
该选项由 setKeepAlive(boolean on) 方法配置,对应底层系统的 SO_KEEPALIVE 套接字选项。默认值为 false。如果打开该选项,则套接字会定期自动发送保持活跃的探测性消息,类似于心跳检测。根据《RFC 1122 - Requirements for Internet Hosts》的规定,保持活跃机制只是 TCP 的一个可选功能,如果支持的话,默认必须为 false,而且这种机制默认在成功建立连接后,且连续两小时没有数据传输的情况下才会被激活。从另一方面来看,通过套接字的 I/O 操作完全可以知道连接是否还有效,所以该选项的实用价值不大。
设置是否收发带外数据
该选项由 setOOBInline(boolean on) 方法配置,对应底层系统的 SO_OOBINLINE 套接字选项。默认值为 off。带外数据(Out-of-band Data)也叫做紧急数据,表示数据很重要,需要使用不同于发送普通数据的一个专用通道来发送。打开该选项后,就可以调用 sendUrgentData(int data) 方法发送一个字节的紧急数据。JDK 对带外数据只提供了有限支持,紧急数据将会和普通数据一起被收到,并且无法自动区分。该选项对应用开发人员意义不大。
设置是否从容关闭连接
该选项由 setSoLinger(boolean on, int linger) 方法配置,对应底层系统的 SO_LINGER 套接字选项。默认为 false。该选项只会影响套接字的关闭,其中的 linger 参数表示超时时间,单位为秒。如果打开改选项:如果将 linger 设为 0,则关闭套接字的时候,未发送的数据会被丢弃,且另一端会出现连接被同位体重置的异常;如果 linger 非 0,则关闭套接字的线程将被阻塞,直到数据全部发送或超时,超时后的行为与底层系统相关,JDK 无法控制。如果关闭该选项,则套接字正常关闭,数据也会全部发送。由于底层实现的差异性,不提倡应用开发人员打开该选项。
设置是否延迟发送数据
该选项由 setTcpNoDelay(boolean on) 方法配置,对应底层系统的 TCP_NODELAY TCP 选项。默认值为 off。打开该选项将禁用 Nagle 算法,TCP 包会立即发送;关闭该选项则会启用 Nagle 算法,多个较小的 TCP 包会被组合成一个大包一起发送,虽然发送延迟了,但有利于避免网络拥塞。默认为 false。该选项对实时性很强的程序可能有用,但一般的程序不需要关心。
设置流量类别
该选项由 setTrafficClass(int tc) 方法配置,对应底层系统的“流量类别”套接字属性。该选项用于向网络(例如路由器)提示从该套接字发送的包需要获取哪些服务类型,对本地 TCP 协议栈没有影响。IPv4 和 IPv6 分别定义了多个不同的值,例如 IPv4 将 0x08 定义为最大吞吐量,0x10 定义为最小延迟,等等。可以用或运算将多个值合并为一个选项。该选项用来调整性能,需要根据实际情况设置。由于只是建议值,可能被网络忽略。

posted @ 2012-01-16 10:31 蜀山兆孨龘 阅读(2107) | 评论 (2)编辑 收藏

2012年1月10日

网上很多关于单例模式写法的文章,不外乎饿汉和懒汉两种形式的讨论。很多人喜欢用懒汉式,因为觉得它实现了延迟加载,可以让系统的性能更好。但事实果真如此吗?我对此存疑。

首先我们检查一下饿汉和懒汉单例模式最简单的写法(这里不讨论哪种懒汉写法更好):

// 饿汉
public final class HungrySingleton {
    private static final HungrySingleton INSTANCE = new HungrySingleton();

    private HungrySingleton() {
        System.out.println("Initializing...");
    }

    public static HungrySingleton getInstance() {
        return INSTANCE;
    }
}

// 懒汉
public final class LazySingleton {
    private static LazySingleton INSTANCE;

    private LazySingleton() {
        System.out.println("Initializing...");
    }

    public static synchronized LazySingleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new LazySingleton();
        }
        return INSTANCE;
    }
}
    

从理论上来说,HungrySingleton 的单例在该类第一次使用的时候创建,而 LazySingleton 的单例则在其 getInstance() 方法被调用的时候创建。至于网上有人声称“饿汉式不管用不用都会初始化”,纯属走路的时候步子迈得太大。谁的加载更迟?如果你只是调用它们的 getInstance() 方法来得到单例对象,则它们都是延迟加载,这样懒汉式没有任何意义,而且由于 LazySingleton 采取了同步措施,性能更低(可以说任何懒汉式的性能都低于饿汉式)。当你使用一个单例类的时候,难道第一步不是调用 getInstance() 么?所以在自己的代码里,我更喜欢用饿汉式。

下面用一个例子来测试加载顺序:

// 饿汉
System.out.println("Before");
HungrySingleton.getInstance();
System.out.println("After");

// 懒汉
System.out.println("Before");
LazySingleton.getInstance();
System.out.println("After");
    

输出结果都是:

Before
Initializing...
After

那么,懒汉模式还有什么存在意义?如果系统使用了某些需要在启动时对类进行扫描的框架,使用饿汉式的话,启动时间比懒汉式更长,如果使用了大量单例类,不利于开发阶段。在系统的正式运行阶段,所有的单例类迟早都要加载的,总的说来两者性能持平,但是懒汉式每次都至少多一个判断,所以越到后期越体现饿汉的优越性。

最后,推荐下《Effective Java》第二版指出的用枚举类型实现的饿汉单例模式:

// 饿汉
public enum HungrySingleton {
    INSTANCE;

    private HungrySingleton() {
    }
}

这种写法不但最简洁,还能轻易扩展为实例数量固定的“多例模式”。

posted @ 2012-01-10 17:39 蜀山兆孨龘 阅读(1723) | 评论 (6)编辑 收藏