关于Java您不知道的5件事

更新时间:2024-06-18 11:04:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

关于 Java 对象序列化您不知道的 5 件事

序列化的数据是安全的?不见得吧。

简介: Java 对象序列化(Java Object Serialization)在 Java 编程中是如此基本,以致于很容易让人想当然。但是,和 Java 平台的很多方面一样,只要肯深入挖掘,序列化总能给予回报。在这个新系列的第一篇文章中,Ted Neward 给出 5 个需重新审视 Java 对象序列化的理由,并提供重构、加密和验证序列化数据的技巧(和代码)。

发布日期: 2010 年 5 月 04 日 级别: 初级 其他语言版本: 英文

数年前,当和一个软件团队一起用 Java 语言编写一个应用程序时,我体会到比一般程序员多知道一点关于 Java 对象序列化的知识所带来的好处。

关于本系列

您觉得自己懂 Java 编程?事实上,大多数程序员对于 Java 平台都是浅尝则止,只学习了足以完成手头上任务的知识而已。在本 系列 中,Ted Neward 深入挖掘 Java 平台的核心功能,揭示一些鲜为人知的事实,帮助您解决最棘手的编程挑战。

大约一年前,一个负责管理应用程序所有用户设置的开发人员,决定将用户设置存储在一个 Hashtable 中,然后将这个 Hashtable 序列化到磁盘,以便持久化。当用户更改设置时,便重新将 Hashtable 写到磁盘。

这是一个优雅的、开放式的设置系统,但是,当团队决定从 Hashtable 迁移到 Java Collections 库中的 HashMap 时,这个系统便面临崩溃。

Hashtable 和 HashMap 在磁盘上的格式是不相同、不兼容的。除非对每个持久化的用户设置运行某种类型的数据转换实用程序(极其庞大的任务),否则以后似乎只能一直用 Hashtable 作为应用程序的存储格式。

团队感到陷入僵局,但这只是因为他们不知道关于 Java 序列化的一个重要事实:Java 序列化允许随着时间的推移而改变类型。当我向他们展示如何自动进行序列化替换后,他们终于按计划完成了向 HashMap 的转变。

本文是本系列的第一篇文章,这个系列专门揭示关于 Java 平台的一些有用的小知识 — 这些小知识不易理解,但对于解决 Java 编程挑战迟早有用。

将 Java 对象序列化 API 作为开端是一个不错的选择,因为它从一开始就存在于 JDK 1.1 中。本文介绍的关于序列化的 5 件事情将说服您重新审视那些标准 Java API。

Java 序列化简介

Java 对象序列化是 JDK 1.1 中引入的一组开创性特性之一,用于作为一种将 Java 对象的状态转换为字节数组,以便存储或传输的机制,以后,仍可以将字节数组转换回 Java 对象原有的状态。

实际上,序列化的思想是 “冻结” 对象状态,传输对象状态(写到磁盘、通过网络传输等等),然后 “解冻” 状态,重新获得可用的 Java 对象。所有这些事情的发生有点像是魔

术,这要归功于 ObjectInputStream/ObjectOutputStream 类、完全保真的元数据以及程序员愿意用 Serializable 标识接口标记他们的类,从而 “参与” 这个过程。

清单 1 显示一个实现 Serializable 的 Person 类。

清单 1. Serializable Person package com.tedneward;

public class Person implements java.io.Serializable{ public Person(String fn, String ln, int a) { this.firstName = fn;

this.lastName = ln; this.age = a;

}

public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; }

public Person getSpouse() { return spouse; }

public void setFirstName(String value) { firstName = value; } public void setLastName(String value) { lastName = value; } public void setAge(int value) { age = value; }

public void setSpouse(Person value) { spouse = value; }

public String toString(){

return \ \ \

\ \ }

private String firstName; private String lastName; private int age; private Person spouse; }

将 Person 序列化后,很容易将对象状态写到磁盘,然后重新读出它,下面的 JUnit 4 单元测试对此做了演示。

清单 2. 对 Person 进行反序列化 public class SerTest{ @Test

public void serializeToDisk(){

try{

com.tedneward.Person ted = new com.tedneward.Person(\ com.tedneward.Person charl = new com.tedneward.Person(\

ted.setSpouse(charl);

charl.setSpouse(ted);

FileOutputStream fos = new FileOutputStream(\ ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(ted); oos.close(); } catch (Exception ex) {

fail(\ }

try{

FileInputStream fis = new FileInputStream(\ ObjectInputStream ois = new ObjectInputStream(fis);

com.tedneward.Person ted = (com.tedneward.Person) ois.readObject(); ois.close();

assertEquals(ted.getFirstName(),\

assertEquals(ted.getSpouse().getFirstName(),\

// Clean up the file

new File(\ }catch (Exception ex) {

fail(\ } } }

到现在为止,还没有看到什么新鲜的或令人兴奋的事情,但是这是一个很好的出发点。我们将使用 Person 来发现您可能不 知道的关于 Java 对象序列化 的 5 件事。

1. 序列化允许重构

序列化允许一定数量的类变种,甚至重构之后也是如此,ObjectInputStream 仍可以很好地将其读出来。

Java Object Serialization 规范可以自动管理的关键任务是:

? 将新字段添加到类中

? 将字段从 static 改为非 static

? 将字段从 transient 改为非 transient 取决于所需的向后兼容程度,转换字段形式(从非 static 转换为 static 或从非 transient

转换为 transient)或者删除字段需要额外的消息传递。

重构序列化类

既然已经知道序列化允许重构,我们来看看当把新字段添加到 Person 类中时,会发生什么事情。

如清单 3 所示,PersonV2 在原先 Person 类的基础上引入一个表示性别的新字段。

清单 3. 将新字段添加到序列化的 Person 中 enum Gender{ MALE, FEMALE }

public class Person implements java.io.Serializable{ public Person(String fn, String ln, int a, Gender g) { this.firstName = fn;

this.lastName = ln; this.age = a; this.gender = g;

}

public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public Gender getGender() { return gender; } public int getAge() { return age; }

public Person getSpouse() { return spouse; }

public void setFirstName(String value) { firstName = value; } public void setLastName(String value) { lastName = value; } public void setGender(Gender value) { gender = value; } public void setAge(int value) { age = value; }

public void setSpouse(Person value) { spouse = value; }

public String toString(){

return \ \ \ \

\ \ }

private String firstName; private String lastName; private int age; private Person spouse; private Gender gender; }

序列化使用一个 hash,该 hash 是根据给定源文件中几乎所有东西 — 方法名称、字段名称、字段类型、访问修改方法等 — 计算出来的,序列化将该 hash 值与序列化流中的 hash 值相比较。

为了使 Java 运行时相信两种类型实际上是一样的,第二版和随后版本的 Person 必须与第一版有相同的序列化版本 hash(存储为 private static final serialVersionUID 字段)。因此,我们需要 serialVersionUID 字段,它是通过对原始(或 V1)版本的 Person 类运行 JDK serialver 命令计算出的。

一旦有了 Person 的 serialVersionUID,不仅可以从原始对象 Person 的序列化数据创建 PersonV2 对象(当出现新字段时,新字段被设为缺省值,最常见的是“null”),还可以反过来做:即从 PersonV2 的数据通过反序列化得到 Person,这毫不奇怪。

2. 序列化并不安全

让 Java 开发人员诧异并感到不快的是,序列化二进制格式完全编写在文档中,并且完全可逆。实际上,只需将二进制序列化流的内容转储到控制台,就足以看清类是什么样子,以及它包含什么内容。

这对于安全性有着不良影响。例如,当通过 RMI 进行远程方法调用时,通过连接发送的对象中的任何 private 字段几乎都是以明文的方式出现在套接字流中,这显然容易招致哪怕最简单的安全问题。

幸运的是,序列化允许 “hook” 序列化过程,并在序列化之前和反序列化之后保护(或模糊化)字段数据。可以通过在 Serializable 对象上提供一个 writeObject 方法来做到这一点。

模糊化序列化数据

假设 Person 类中的敏感数据是 age 字段。毕竟,女士忌谈年龄。我们可以在序列化之前模糊化该数据,将数位循环左移一位,然后在反序列化之后复位。(您可以开发更安全的算法,当前这个算法只是作为一个例子。)

为了 “hook” 序列化过程,我们将在 Person 上实现一个 writeObject 方法;为了 “hook” 反序列化过程,我们将在同一个类上实现一个 readObject 方法。重要的是这两个方法的细节要正确 — 如果访问修改方法、参数或名称不同于清单 4 中的内容,那么代码将不被察觉地失败,Person 的 age 将暴露。

清单 4. 模糊化序列化数据

public class Person implements java.io.Serializable{ public Person(String fn, String ln, int a) { this.firstName = fn;

this.lastName = ln; this.age = a;

}

public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; }

public Person getSpouse() { return spouse; }

public void setFirstName(String value) { firstName = value; }

public void setLastName(String value) { lastName = value; } public void setAge(int value) { age = value; }

public void setSpouse(Person value) { spouse = value; }

private void writeObject(java.io.ObjectOutputStream stream)

throws java.io.IOException {

// \ age = age << 2;

stream.defaultWriteObject(); }

private void readObject(java.io.ObjectInputStream stream) throws java.io.IOException, ClassNotFoundException { stream.defaultReadObject();

// \ age = age << 2; }

public String toString() {

return \ \ \

\ \ }

private String firstName; private String lastName; private int age; private Person spouse; }

如果需要查看被模糊化的数据,总是可以查看序列化数据流/文件。而且,由于该格式被完全文档化,即使不能访问类本身,也仍可以读取序列化流中的内容。

3. 序列化的数据可以被签名和密封

上一个技巧假设您想模糊化序列化数据,而不是对其加密或者确保它不被修改。当然,通过使用 writeObject 和 readObject 可以实现密码加密和签名管理,但其实还有更好的方式。

如果需要对整个对象进行加密和签名,最简单的是将它放在一个 javax.crypto.SealedObject 和/或 java.security.SignedObject 包装器中。两者都是可序列化的,所以将对象包装在 SealedObject 中可以围绕原对象创建一种 “包装盒”。必须有对称密钥才

能解密,而且密钥必须单独管理。同样,也可以将 SignedObject 用于数据验证,并且对称密钥也必须单独管理。

结合使用这两种对象,便可以轻松地对序列化数据进行密封和签名,而不必强调关于数字签名验证或加密的细节。很简洁,是吧?

4. 序列化允许将代理放在流中

很多情况下,类中包含一个核心数据元素,通过它可以派生或找到类中的其他字段。在此情况下,没有必要序列化整个对象。可以将字段标记为 transient,但是每当有方法访问一个字段时,类仍然必须显式地产生代码来检查它是否被初始化。

如果首要问题是序列化,那么最好指定一个 flyweight 或代理放在流中。为原始 Person 提供一个 writeReplace 方法,可以序列化不同类型的对象来代替它。类似地,如果反序列化期间发现一个 readResolve 方法,那么将调用该方法,将替代对象提供给调用者。

打包和解包代理

writeReplace 和 readResolve 方法使 Person 类可以将它的所有数据(或其中的核心数据)打包到一个 PersonProxy 中,将它放入到一个流中,然后在反序列化时再进行解包。

清单 5. 你完整了我,我代替了你

class PersonProxy implements java.io.Serializable{ public PersonProxy(Person orig){

data = orig.getFirstName() + \ if (orig.getSpouse() != null){

Person spouse = orig.getSpouse();

data = data + \ } }

public String data;

private Object readResolve()

throws java.io.ObjectStreamException { String[] pieces = data.split(\

Person result = new Person(pieces[0], pieces[1], Integer.parseInt(pieces[2])); if (pieces.length > 3) {

result.setSpouse(new Person(pieces[3], pieces[4], Integer.parseInt (pieces[5]))); result.getSpouse().setSpouse(result); }

return result; } }

public class Person implements java.io.Serializable{ public Person(String fn, String ln, int a){ this.firstName = fn;

this.lastName = ln; this.age = a;

}

public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public int getAge() { return age; }

public Person getSpouse() { return spouse; }

private Object writeReplace()

throws java.io.ObjectStreamException { return new PersonProxy(this); }

public void setFirstName(String value) { firstName = value; } public void setLastName(String value) { lastName = value; } public void setAge(int value) { age = value; }

public void setSpouse(Person value) { spouse = value; }

public String toString(){

return \ \ \

\ \ }

private String firstName; private String lastName; private int age; private Person spouse; }

注意,PersonProxy 必须跟踪 Person 的所有数据。这通常意味着代理需要是 Person 的一个内部类,以便能访问 private 字段。有时候,代理还需要追踪其他对象引用并手动序列化它们,例如 Person 的 spouse。

这种技巧是少数几种不需要读/写平衡的技巧之一。例如,一个类被重构成另一种类型后的版本可以提供一个 readResolve 方法,以便静默地将被序列化的对象转换成新类型。类似地,它可以采用 writeReplace 方法将旧类序列化成新版本。

5. 信任,但要验证

认为序列化流中的数据总是与最初写到流中的数据一致,这没有问题。但是,正如一位美国前总统所说的,“信任,但要验证”。

对于序列化的对象,这意味着验证字段,以确保在反序列化之后它们仍具有正确的值,

“以防万一”。为此,可以实现 ObjectInputValidation 接口,并覆盖 validateObject() 方法。如果调用该方法时发现某处有错误,则抛出一个 InvalidObjectException。

结束语

Java 对象序列化比大多数 Java 开发人员想象的更灵活,这使我们有更多的机会解决棘手的情况。

幸运的是,像这样的编程妙招在 JVM 中随处可见。关键是要知道它们,在遇到难题的时候能用上它们。

参考资料

学习 ? ? ? ? ? 讨论 ?

加入 My developerWorks 社区。

“测试对象串行化”(Elliotte Rusty Harold,IBM developerWorks,2006 年 6 月):了解测试对象“Discover the secrets of the Java Serialization API”(Todd M. Greanier,JavaWorld,2000 年 7 月):“The Java Serialization algorithm revealed”(Sathiskumar Palaniappan,JavaWorld,2009 年 5 月):Java Object Serialization: 下载 PDF 格式的 Java Serialization 规范。

developerWorks Java 技术专区: 这里有数百篇关于 Java 编程每个方面的文章。 的序列化格式为什么重要,然后试着用不同的方法测试对象的序列化。

这篇文章对 Java Serialization API 作了概述,并提供 3 种序列化 Java 对象的方法。 更仔细地研究 Java 序列化算法的细节。

关于 Java Collections API 您不知道的 5 件事1

定制和扩展 Java Collections

简介: Java? Collections API 远不止是数组的替代品,虽然一开始这样用也不错。Ted Neward 提供了关于用 Collections 做更多事情的 5 个技巧,包括关于定制和扩展 Java Collections API 的基础。

发布日期: 2010 年 5 月 24 日 级别: 初级 其他语言版本: 英文

对于很多 Java 开发人员来说,Java Collections API 是标准 Java 数组及其所有缺点的一个非常需要的替代品。将 Collections 主要与 ArrayList 联系到一起本身没有错,但是对于那些有探索精神的人来说,这只是 Collections 的冰山一角。

关于本系列

您觉得自己懂 Java 编程?事实上,大多数程序员对于 Java 平台都是浅尝则止,只学

习了足以完成手头上任务的知识而已。在本 系列 中,Ted Neward 深入挖掘 Java 平台的核心功能,揭示一些鲜为人知的事实,帮助您解决最棘手的编程挑战。

虽然 Map(以及它的常用实现 HashMap)非常适合名-值对或键-值对,但是没有理由让自己局限于这些熟悉的工具。可以使用适当的 API,甚至适当的 Collection 来修正很多易错的代码。

本文是 5 件事 系列 中的第二篇文章,也是专门讨论 Collections 的 7 篇文章中的第一篇文章,之所以花这么大的篇幅讨论 Collections,是因为这些集合在 Java 编程中是如此重要。首先我将讨论做每件事的最快(但也许不是最常见)的方式,例如将 Array 中的内容转移到 List。然后我们深入探讨一些较少人知道的东西,例如编写定制的 Collections 类和扩展 Java Collections API。

1. Collections 比数组好

刚接触 Java 技术的开发人员可能不知道,Java 语言最初包括数组,是为了应对上世纪 90 年代初期 C++ 开发人员对于性能方面的批评。从那时到现在,我们已经走过一段很长的路,如今,与 Java Collections 库相比,数组不再有性能优势。

例如,若要将数组的内容转储到一个字符串,需要迭代整个数组,然后将内容连接成一个 String;而 Collections 的实现都有一个可用的 toString() 实现。

除少数情况外,好的做法是尽快将遇到的任何数组转换成集合。于是问题来了,完成这种转换的最容易的方式是什么?事实证明,Java Collections API 使这种转换变得容易,如清单 1 所示:

清单 1. ArrayToList import java.util.*;

public class ArrayToList{

public static void main(String[] args) { // This gives us nothing good System.out.println(args);

// Convert args to a List of String List argList = Arrays.asList(args);

// Print them out

System.out.println(argList); } }

注意,返回的 List 是不可修改的,所以如果尝试向其中添加新元素将抛出一个 UnsupportedOperationException。

而且,由于 Arrays.asList() 使用 varargs 参数表示添加到 List 的元素,所以还可以使用它轻松地用以 new 新建的对象创建 List。

2. 迭代的效率较低

将一个集合(特别是由数组转化而成的集合)的内容转移到另一个集合,或者从一个较大对象集合中移除一个较小对象集合,这些事情并不鲜见。

您也许很想对集合进行迭代,然后添加元素或移除找到的元素,但是不要这样做。 在此情况下,迭代有很大的缺点:

? 每次添加或移除元素后重新调整集合将非常低效。

? 每次在获取锁、执行操作和释放锁的过程中,都存在潜在的并发困境。 ? 当添加或移除元素时,存取集合的其他线程会引起竞争条件。 可以通过使用 addAll 或 removeAll,传入包含要对其添加或移除元素的集合作为参数,来避免所有这些问题。

3. 用 for 循环遍历任何 Iterable

Java 5 中加入 Java 语言的最大的便利功能之一,增强的 for 循环,消除了使用 Java 集合的最后一道障碍。

以前,开发人员必须手动获得一个 Iterator,使用 next() 获得 Iterator 指向的对象,并通过 hasNext() 检查是否还有更多可用对象。从 Java 5 开始,我们可以随意使用 for 循环的变种,它可以在幕后处理上述所有工作。

实际上,这个增强适用于实现 Iterable 接口的任何对象,而不仅仅是 Collections。 清单 2 显示通过 Iterator 提供 Person 对象的孩子列表的一种方法。 这里不是提供内部 List 的一个引用 (这使 Person 外的调用者可以为家庭增加孩子 — 而大多数父母并不希望如此),Person 类型实现 Iterable。这种方法还使得 for 循环可以遍历所有孩子。

清单 2. 增强的 for 循环:显示孩子 // Person.java import java.util.*;

public class Person implements Iterable{

public Person(String fn, String ln, int a, Person... kids){ this.firstName = fn;

this.lastName = ln; this.age = a;

for (Person child : kids) children.add(child); }

public String getFirstName() { return this.firstName; } public String getLastName() { return this.lastName; } public int getAge() { return this.age; }

public Iterator iterator() { return children.iterator(); }

public void setFirstName(String value) { this.firstName = value; }

public void setLastName(String value) { this.lastName = value; } public void setAge(int value) { this.age = value; }

public String toString() { return \

\ \ \ }

private String firstName; private String lastName; private int age;

private List children = new ArrayList(); }

// App.java public class App{

public static void main(String[] args) {

Person ted = new Person(\ new Person(\), new Person(\

// Iterate over the kids for (Person kid : ted) {

System.out.println(kid.getFirstName()); } } }

在域建模的时候,使用 Iterable 有一些明显的缺陷,因为通过 iterator() 方法只能那么 “隐晦” 地支持一个那样的对象集合。但是,如果孩子集合比较明显,Iterable 可以使针对域类型的编程更容易,更直观。

4. 经典算法和定制算法

您是否曾想过以倒序遍历一个 Collection?对于这种情况,使用经典的 Java Collections 算法非常方便。

在上面的 清单 2 中,Person 的孩子是按照传入的顺序排列的;但是,现在要以相反的顺序列出他们。虽然可以编写另一个 for 循环,按相反顺序将每个对象插入到一个新的 ArrayList 中,但是 3、4 次重复这样做之后,就会觉得很麻烦。

在此情况下,清单 3 中的算法就有了用武之地:

清单 3. ReverseIterator public class ReverseIterator{

public static void main(String[] args) { Person ted = new Person(\ new Person(\), new Person(\

// Make a copy of the List

List kids = new ArrayList(ted.getChildren()); // Reverse it

Collections.reverse(kids); // Display it

System.out.println(kids); } }

Collections 类有很多这样的 “算法”,它们被实现为静态方法,以 Collections 作为参数,提供独立于实现的针对整个集合的行为。

而且,由于很棒的 API 设计,我们不必完全受限于 Collections 类中提供的算法 — 例如,我喜欢不直接修改(传入的 Collection 的)内容的方法。所以,可以编写定制算法是一件很棒的事情,例如清单 4 就是一个这样的例子:

清单 4. ReverseIterator 使事情更简单 class MyCollections{

public static List reverse(List src) { List results = new ArrayList(src); Collections.reverse(results); return results; } }

5. 扩展 Collections API

以上定制算法阐释了关于 Java Collections API 的一个最终观点:它总是适合加以扩展和修改,以满足开发人员的特定目的。

例如,假设您需要 Person 类中的孩子总是按年龄排序。虽然可以编写代码一遍又一遍地对孩子排序(也许是使用 Collections.sort 方法),但是通过一个 Collection 类来自动排序要好得多。

实际上,您甚至可能不关心是否每次按固定的顺序将对象插入到 Collection 中(这正是 List 的基本原理)。您可能只是想让它们按一定的顺序排列。

java.util 中没有 Collection 类能满足这些需求,但是编写一个这样的类很简单。只需创建一个接口,用它描述 Collection 应该提供的抽象行为。对于 SortedCollection,它的作用完全是行为方面的。

清单 5. SortedCollection

public interface SortedCollection extends Collection{ public Comparator getComparator();

public void setComparator(Comparator comp);

}

编写这个新接口的实现简直不值一提:

清单 6. ArraySortedCollection import java.util.*;

public class ArraySortedCollection implements SortedCollection, Iterable{ private Comparator comparator; private ArrayList list;

public ArraySortedCollection(Comparator c) { this.list = new ArrayList(); this.comparator = c; }

public ArraySortedCollection(Collection src, Comparator c) { this.list = new ArrayList(src); this.comparator = c; sortThis(); }

public Comparator getComparator() { return comparator; }

public void setComparator(Comparator cmp) { comparator = cmp; sortThis(); }

public boolean add(E e) { boolean r = list.add(e); sortThis(); return r; }

public boolean addAll(Collection ec) { boolean r = list.addAll(ec); sortThis(); return r; } public boolean remove(Object o) { boolean r = list.remove(o); sortThis(); return r; }

public boolean removeAll(Collection c) { boolean r = list.removeAll(c); sortThis(); return r; } public boolean retainAll(Collection ec) { boolean r = list.retainAll(ec); sortThis(); return r; }

public void clear() { list.clear(); }

public boolean contains(Object o) { return list.contains(o); }

public boolean containsAll(Collection c) { return list.containsAll(c); } public boolean isEmpty() { return list.isEmpty(); } public Iterator iterator() { return list.iterator(); } public int size() { return list.size(); }

public Object[] toArray() { return list.toArray(); } public T[] toArray(T[] a) { return list.toArray(a); }

public boolean equals(Object o) { if (o == this)

return true;

if (o instanceof ArraySortedCollection) {

ArraySortedCollection rhs = (ArraySortedCollection)o; return this.list.equals(rhs.list);

} return false; }

public int hashCode() { return list.hashCode(); } public String toString() { return list.toString(); }

private void sortThis() { Collections.sort(list, comparator); } }

这个实现非常简陋,编写时并没有考虑优化,显然还需要进行重构。但关键是 Java Collections API 从来无意将与集合相关的任何东西定死。它总是需要扩展,同时也鼓励扩展。

当然,有些扩展比较复杂,例如 java.util.concurrent 中引入的扩展。但是另一些则非常简单,只需编写一个定制算法,或者已有 Collection 类的简单的扩展。

扩展 Java Collections API 看上去很难,但是一旦开始着手,您会发现远不如想象的那样难。

结束语

和 Java Serialization 一样,Java Collections API 还有很多角落等待有人去探索 —正因为如此,我们还不准备结束这个话题。在 5 件事 系列 的下一篇文章中,将可以看到用 Java Collections API 做更多事情的 5 种新的方式。

参考资料

学习 ? 绍。 ? ? ? 档。 ? 讨论 ?

加入 My developerWorks 社区。

developerWorks Java 技术专区: 这里有数百篇关于 Java 编程每个方面的文章。

“Java 理论与实践: 并发集合类”(Brian Goetz,developerWorks,2003 年 7 月):学习 Doug Lea “使用泛型和并发改善集合”(John Zukowski,developerWorks,2008 年 4 月):介绍 Java 6 中 Java Collections Framework:阅读 Sun Microsystems 的 Java Collections Framework 和 API 文的 util.concurrent 包如何为标准集合类型 List 和 Map 注入新的活力。 Java Collections Framework 的变化。

“Introduction to the Collections Framework”(MageLang Institute, Sun Developer Network, 1999):这篇教程是很早以前的,但是很棒,它对并发集合之前的 Java Collections Framework 做了完整的介

关于 Java Collections API 您不知道的 5 件事2

注意可变对象

简介: 您可以在任何地方使用 Java? 集合,但是一定要小心。集合有很多秘密,如果不正确处理可能会带来麻烦。Ted 探索了 Java Collections API 复杂、多变的一面并为您提供了一些技巧,帮您充分利用 Iterable、HashMap 和 SortedSet,又不会带来 bug。

发布日期: 2010 年 6 月 21 日 级别: 初级 其他语言版本: 英文

关于本系列

您觉得自己懂 Java 编程?事实上,大多数程序员对于 Java 平台都是浅尝辄止,只学习了足以完成手头上任务的知识而已。在本 系列 中,Ted Neward 深入挖掘 Java 平台的核心功能,揭示一些鲜为人知的事实,帮助您解决最棘手的编程困难。

java.util 中的 Collections 类旨在通过取代数组提高 Java 性能。如您在 第 1 部分 中了解到的,它们也是多变的,能够以各种方式定制和扩展,帮助实现优质、简洁的代码。

Collections 非常强大,但是很多变:使用它们要小心,滥用它们会带来风险。

1. List 不同于数组

Java 开发人员常常错误地认为 ArrayList 就是 Java 数组的替代品。Collections 由数组支持,在集合内随机查找内容时性能较好。与数组一样,集合使用整序数获取特定项。但集合不是数组的简单替代。

要明白数组与集合的区别需要弄清楚顺序 和位置 的不同。例如,List 是一个接口,它保存各个项被放入集合中的顺序,如清单 1 所示:

清单 1. 可变键值 import java.util.*;

public class OrderAndPosition{

public static void dumpArray(T[] array) { System.out.println(\ for (int i=0; i

System.out.println(\ }

public static void dumpList(List list) { System.out.println(\ for (int i=0; i

System.out.println(\ }

public static void main(String[] args) {

List argList = new ArrayList(Arrays.asList(args)); dumpArray(args); args[1] = null; dumpArray(args);

dumpList(argList); argList.remove(1); dumpList(argList); } }

当第三个元素从上面的 List 中被移除时,其 “后面” 的各项会上升填补空位。很显然,此集合行为与数组的行为不同(事实上,从数组中移除项与从 List 中移除它也不完全是一回事儿 — 从数组中 “移除” 项意味着要用新引用或 null 覆盖其索引槽)。

2. 令人惊讶的 Iterator!

无疑 Java 开发人员很喜爱 Java 集合 Iterator,但是您最后一次使用 Iterator 接口是什么时候的事情了?可以这么说,大部分时间我们只是将 Iterator 随意放到 for() 循环或加强 for() 循环中,然后就继续其他操作了。

但是进行深入研究后,您会发现 Iterator 实际上有两个十分有用的功能。

第一,Iterator 支持从源集合中安全地删除对象,只需在 Iterator 上调用 remove() 即可。这样做的好处是可以避免 ConcurrentModifiedException,这个异常顾名思意:当打开 Iterator 迭代集合时,同时又在对集合进行修改。有些集合不允许在迭代时删除或添加元素,但是调用 Iterator 的 remove() 方法是个安全的做法。

第二,Iterator 支持派生的(并且可能是更强大的)兄弟成员。ListIterator,只存在于 List 中,支持在迭代期间向 List 中添加或删除元素,并且可以在 List 中双向滚动。

双向滚动特别有用,尤其是在无处不在的 “滑动结果集” 操作中,因为结果集中只能显示从数据库或其他集合中获取的众多结果中的 10 个。它还可以用于 “反向遍历” 集合或列表,而无需每次都从前向后遍历。插入 ListIterator 比使用向下计数整数参数 List.get() “反向” 遍历 List 容易得多。

3. 并非所有 Iterable 都来自集合

Ruby 和 Groovy 开发人员喜欢炫耀他们如何能迭代整个文本文件并通过一行代码将其内容输出到控制台。通常,他们会说在 Java 编程中完成同样的操作需要很多行代码:打开 FileReader,然后打开 BufferedReader,接着创建 while() 循环来调用 getLine(),直到它返回 null。当然,在 try/catch/finally 块中必须要完成这些操作,它要处理异常并在结束时关闭文件句柄。

这看起来像是一个没有意义的学术上的争论,但是它也有其自身的价值。 他们(包括相当一部分 Java 开发人员)不知道并不是所有 Iterable 都来自集合。Iterable 可以创建 Iterator,该迭代器知道如何凭空制造下一个元素,而不是从预先存在的 Collection 中盲目地处理:

清单 2. 迭代文件 // FileUtils.java import java.io.*; import java.util.*;

public class FileUtils {

public static Iterable readlines(String filename) throws IOException {

final FileReader fr = new FileReader(filename); final BufferedReader br = new BufferedReader(fr);

return new Iterable() { }; } }

//DumpApp.java import java.util.*;

public Iterator iterator() { }

return new Iterator() { };

public boolean hasNext() { }

public String next() { }

public void remove() { }

String getLine() { }

String line = getLine();

String line = null; try { } return line;

line = br.readLine(); line = null;

}catch (IOException ioEx) {

throw new UnsupportedOperationException(); String retval = line; line = getLine(); return retval; return line != null;

public class DumpApp{

public static void main(String[] args) throws Exception {

for (String line : FileUtils.readlines(args[0])) System.out.println(line); } }

此方法的优势是不会在内存中保留整个内容,但是有一个警告就是,它不能 close() 底层文件句柄(每当 readLine() 返回 null 时就关闭文件句柄,可以修正这一问题,但是在 Iterator 没有结束时不能解决这个问题)。

4. 注意可变的 hashCode()

Map 是很好的集合,为我们带来了在其他语言(比如 Perl)中经常可见的好用的键/值对集合。JDK 以 HashMap 的形式为我们提供了方便的 Map 实现,它在内部使用哈希表实现了对键的对应值的快速查找。但是这里也有一个小问题:支持哈希码的键依赖于可变字段的内容,这样容易产生 bug,即使最耐心的 Java 开发人员也会被这些 bug 逼疯。

假设清单 3 中的 Person 对象有一个常见的 hashCode() (它使用 firstName、lastName 和 age 字段 — 所有字段都不是 final 字段 — 计算 hashCode()),对 Map 的 get() 调用会失败并返回 null:

清单 3. 可变 hashCode() 容易出现 bug // Person.java import java.util.*;

public class Person implements Iterable{ public Person(String fn, String ln, int a, Person... kids) { this.firstName = fn; this.lastName = ln; this.age = a; for (Person kid : kids) children.add(kid); } // ...

public void setFirstName(String value) { this.firstName = value; } public void setLastName(String value) { this.lastName = value; } public void setAge(int value) { this.age = value; }

public int hashCode() {

return firstName.hashCode() & lastName.hashCode() & age; } // ...

private String firstName; private String lastName; private int age;

private List children = new ArrayList(); }

// MissingHash.java import java.util.*;

public class MissingHash{

public static void main(String[] args) {

Person p1 = new Person(\ Person p2 = new Person(\ System.out.println(p1.hashCode());

Map map = new HashMap(); map.put(p1, p2);

p1.setLastName(\ System.out.println(p1.hashCode());

System.out.println(map.get(p1)); } }

很显然,这种方法很糟糕,但是解决方法也很简单:永远不要将可变对象类型用作 HashMap 中的键。

5. equals() 与 Comparable

在浏览 Javadoc 时,Java 开发人员常常会遇到 SortedSet 类型(它在 JDK 中唯一的实现是 TreeSet)。因为 SortedSet 是 java.util 包中唯一提供某种排序行为的 Collection,所以开发人员通常直接使用它而不会仔细地研究它。清单 4 展示了:

清单 4. SortedSet,我很高兴找到了它! import java.util.*;

public class UsingSortedSet{

public static void main(String[] args) { List persons = Arrays.asList( new Person(\ new Person(\ new Person(\ new Person(\ );

SortedSet ss = new TreeSet(new Comparator() { public int compare(Person lhs, Person rhs) {

return lhs.getLastName().compareTo(rhs.getLastName()); } });

ss.addAll(perons); System.out.println(ss); } }

使用上述代码一段时间后,可能会发现这个 Set 的核心特性之一:它不允许重复。该特性在 Set Javadoc 中进行了介绍。Set 是不包含重复元素的集合。更准确地说,set 不包含成对的 e1 和 e2 元素,因此如果 e1.equals(e2),那么最多包含一个 null 元素。

但实际上似乎并非如此 — 尽管 清单 4 中没有相等的 Person 对象(根据 Person 的 equals() 实现),但在输出时只有三个对象出现在 TreeSet 中。

与 set 的有状态本质相反,TreeSet 要求对象直接实现 Comparable 或者在构造时传入 Comparator,它不使用 equals() 比较对象;它使用 Comparator/Comparable 的 compare 或 compareTo 方法。

因此存储在 Set 中的对象有两种方式确定相等性:大家常用的 equals() 方法和 Comparable/Comparator 方法,采用哪种方法取决于上下文。

更糟的是,简单的声明两者相等还不够,因为以排序为目的的比较不同于以相等性为目的的比较:可以想象一下按姓排序时两个 Person 相等,但是其内容却并不相同。

一定要明白 equals() 和 Comparable.compareTo() 两者之间的不同 — 实现 Set 时会返回 0。甚至在文档中也要明确两者的区别。

结束语

Java Collections 库中有很多有用之物,如果您能加以利用,它们可以让您的工作更轻松、更高效。但是发掘这些有用之物可能有点复杂,比如只要您不将可变对象类型作为键,您就可以用自己的方式使用 HashMap。

至此我们挖掘了 Collections 的一些有用特性,但我们还没有挖到金矿:Concurrent Collections,它在 Java 5 中引入。本 系列 的后 5 个窍门将关注 java.util.concurrent。

参考资料

学习 ? ? 绍。 ?

“Java 理论与实践:并发集合类”(Brian Goetz,developerWorks,2003 年 7 月):学习 Doug Lea 的 util.concurrent 包如何为标准集合类型 List 和 Map 注入新的活力。

“关于 Java Collections API 您不知道的 5 件事,第 1 部分”(Ted Neward,developerWorks,2010 “Introduction to the Collections Framework”(MageLang Institute,Sun Developer Network,1999):年 4 月):用 Java Collections 做更多事情,包括关于定制和扩展 Java Collections API。

这篇教程是很早以前的,但是很棒,它对并发集合之前的 Java Collections Framework 做了完整的介

? ? 档。 ? 讨论 ?

“使用泛型和并发改善集合”(John Zukowski,developerWorks,2008 年 4 月):介绍 Java 6 中 Java Collections Framework:阅读 Sun Microsystems 的 Java Collections Framework 和 API 文developerWorks Java 技术专区:这里有数百篇关于 Java 编程每个方面的文章。 加入 My developerWorks 社区。

Java Collections Framework 的变化。

关于 java.util.concurrent 您不知道的 5 件事1

通过并发 Collections 进行多线程编程

简介: 编写能够良好执行,防止应用程序受损的多线程代码是很艰巨的任务 — 这也是为什么我们需要 java.util.concurrent 的原因。Ted Neward 会向您说明并发 Collections 类,比如 CopyOnWriteArrayList,BlockingQueue,还有 ConcurrentMap,如何针对您的并发编程需求改进标准 Collections 类。

发布日期: 2010 年 7 月 07 日 (最初发布 2010 年 7 月 01 日) 级别: 初级 其他语言版本: 英文

关于本系列

您觉得自己懂 Java 编程?事实是,大多数开发人员都只领会到了 Java 平台的皮毛,所学也只够应付工作。在本 系列 中,Ted Neward 深度挖掘 Java 平台的核心功能,揭示一些鲜为人知的事实,帮助您解决最棘手的编程困难。

Concurrent Collections 是 Java? 5 的巨大附加产品,但是在关于注释和泛型的争执中很多 Java 开发人员忽视了它们。此外(或者更老实地说),许多开发人员避免使用这个数据包,因为他们认为它一定很复杂,就像它所要解决的问题一样。

事实上,java.util.concurrent 包含许多类,能够有效解决普通的并发问题,无需复杂工序。阅读本文,了解 java.util.concurrent 类,比如 CopyOnWriteArrayList 和 BlockingQueue 如何帮助您解决多线程编程的棘手问题。

1. TimeUnit

尽管本质上 不是 Collections 类,但 java.util.concurrent.TimeUnit 枚举让代码更易读懂。使用 TimeUnit 将使用您的方法或 API 的开发人员从毫秒的 “暴政” 中解放出来。

TimeUnit 包括所有时间单位,从 MILLISECONDS 和 MICROSECONDS 到 DAYS 和 HOURS,这就意味着它能够处理一个开发人员所需的几乎所有的时间范围类型。同时,因为在列举上声明了转换方法,在时间加快时,将 HOURS 转换回 MILLISECONDS 甚至变得更容易。

2. CopyOnWriteArrayList

创建数组的全新副本是过于昂贵的操作,无论是从时间上,还是从内存开销上,因此在通常使用中很少考虑;开发人员往往求助于使用同步的 ArrayList。然而,这也是一个成本较高的选择,因为每当您跨集合内容进行迭代时,您就不得不同步所有操作,包括读和写,以此保证一致性。

这又让成本结构回到这样一个场景:需多读者都在读取 ArrayList,但是几乎没人会去修改它。

CopyOnWriteArrayList 是个巧妙的小宝贝,能解决这一问题。它的 Javadoc 将 CopyOnWriteArrayList 定义为一个 “ArrayList 的线程安全变体,在这个变体中所有易变操作(添加,设置等)可以通过复制全新的数组来实现”。

集合从内部将它的内容复制到一个没有修改的新数组,这样读者访问数组内容时就不会产生同步成本(因为他们从来不是在易变数据上操作)。

本质上讲,CopyOnWriteArrayList 很适合处理 ArrayList 经常让我们失败的这种场景:读取频繁,但很少有写操作的集合,例如 JavaBean 事件的 Listeners。

3. BlockingQueue

BlockingQueue 接口表示它是一个 Queue,意思是它的项以先入先出(FIFO)顺序存储。在特定顺序插入的项以相同的顺序检索 — 但是需要附加保证,从空队列检索一个项的任何尝试都会阻塞调用线程,直到这个项准备好被检索。同理,想要将一个项插入到满队列的尝试也会导致阻塞调用线程,直到队列的存储空间可用。

BlockingQueue 干净利落地解决了如何将一个线程收集的项“传递”给另一线程用于处理的问题,无需考虑同步问题。Java Tutorial 的 Guarded Blocks 试用版就是一个很好的例子。它构建一个单插槽绑定的缓存,当新的项可用,而且插槽也准备好接受新的项时,使用手动同步和 wait()/notifyAll() 在线程之间发信。(详见 Guarded Blocks 实现。)

尽管 Guarded Blocks 教程中的代码有效,但是它耗时久,混乱,而且也并非完全直观。退回到 Java 平台较早的时候,没错,Java 开发人员不得不纠缠于这种代码;但现在是 2010 年 — 情况难道没有改善?

清单 1 显示了 Guarded Blocks 代码的重写版,其中我使用了一个 ArrayBlockingQueue,而不是手写的 Drop。

清单 1. BlockingQueue import java.util.*;

import java.util.concurrent.*;

class Producer implements Runnable{ private BlockingQueue drop; List messages = Arrays.asList( \ \ \ \

public Producer(BlockingQueue d) { this.drop = d; }

public void run() { try{

for (String s : messages) drop.put(s); drop.put(\

}catch (InterruptedException intEx) { System.out.println(\ \ } } }

class Consumer implements Runnable{ private BlockingQueue drop;

public Consumer(BlockingQueue d) { this.drop = d; }

public void run(){ try{

String msg = null;

while (!((msg = drop.take()).equals(\ System.out.println(msg); }catch (InterruptedException intEx) {

System.out.println(\ \ } } }

public class ABQApp{

public static void main(String[] args) {

BlockingQueue drop = new ArrayBlockingQueue(1, true); (new Thread(new Producer(drop))).start(); (new Thread(new Consumer(drop))).start(); } }

ArrayBlockingQueue 还体现了“公平” — 意思是它为读取器和编写器提供线程先入先出访问。这种替代方法是一个更有效,但又冒穷尽部分线程风险的政策。(即,允许一些读取器在其他读取器锁定时运行效率更高,但是您可能会有读取器线程的流持续不断的风险,导致编写器无法进行工作。)

注意 Bug!

顺便说一句,如果您注意到 Guarded Blocks 包含一个重大 bug,那么您是对的 — 如果开发人员在 main() 中的 Drop 实例上同步,会出现什么情况呢?

BlockingQueue 还支持接收时间参数的方法,时间参数表明线程在返回信号故障以插入或者检索有关项之前需要阻塞的时间。这么做会避免非绑定的等待,这对一个生产系统是致命的,因为一个非绑定的等待会很容易导致需要重启的系统挂起。

4. ConcurrentMap

Map 有一个微妙的并发 bug,这个 bug 将许多不知情的 Java 开发人员引入歧途。ConcurrentMap 是最容易的解决方案。

当一个 Map 被从多个线程访问时,通常使用 containsKey() 或者 get() 来查看给定键是否在存储键/值对之前出现。但是即使有一个同步的 Map,线程还是可以在这个过程中潜入,然后夺取对 Map 的控制权。问题是,在对 put() 的调用中,锁在 get() 开始时获取,然后在可以再次获取锁之前释放。它的结果是个竞争条件:这是两个线程之间的竞争,结果也会因谁先运行而不同。

如果两个线程几乎同时调用一个方法,两者都会进行测试,调用 put,在处理中丢失第一线程的值。幸运的是,ConcurrentMap 接口支持许多附加方法,它们设计用于在一个锁下进行两个任务:putIfAbsent(),例如,首先进行测试,然后仅当键没有存储在 Map 中时进行 put。

5. SynchronousQueues

根据 Javadoc,SynchronousQueue 是个有趣的东西:

这是一个阻塞队列,其中,每个插入操作必须等待另一个线程的对应移除操作,反之亦然。一个同步队列不具有任何内部容量,甚至不具有 1 的容量。

本质上讲,SynchronousQueue 是之前提过的 BlockingQueue 的又一实现。它给我们提供了在线程之间交换单一元素的极轻量级方法,使用 ArrayBlockingQueue 使用的阻塞语义。在清单 2 中,我重写了 清单 1 的代码,使用 SynchronousQueue 替代 ArrayBlockingQueue:

清单 2. SynchronousQueue import java.util.*;

import java.util.concurrent.*;

class Producer implements Runnable{ private BlockingQueue drop; List messages = Arrays.asList( \ \ \ \

public Producer(BlockingQueue d) { this.drop = d; }

public void run() { try{

for (String s : messages) drop.put(s); drop.put(\

}catch (InterruptedException intEx) {

System.out.println(\ \ } } }

class Consumer implements Runnable{ private BlockingQueue drop;

public Consumer(BlockingQueue d) { this.drop = d; }

public void run(){ try{

String msg = null;

while (!((msg = drop.take()).equals(\ System.out.println(msg); }catch (InterruptedException intEx) { System.out.println(\ \ } } }

public class SynQApp{

public static void main(String[] args) {

BlockingQueue drop = new SynchronousQueue(); (new Thread(new Producer(drop))).start(); (new Thread(new Consumer(drop))).start(); } }

实现代码看起来几乎相同,但是应用程序有额外获益:SynchronousQueue 允许在队列进行一个插入,只要有一个线程等着使用它。

在实践中,SynchronousQueue 类似于 Ada 和 CSP 等语言中可用的 “会合通道”。这些通道有时在其他环境中也称为 “连接”,这样的环境包括 .NET (见 参考资料)。

结束语

当 Java 运行时知识库提供便利、预置的并发性时,为什么还要苦苦挣扎,试图将并发性导入到您的 Collections 类?本系列的下一篇文章将会进一步探讨 java.util.concurrent 名称空间的内容。

参考资料

学习 ? ? ? ? ?

“Java 理论与实践: 并发集合类”(Brian Goetz,developerWorks,2003 年 7 月):学习 Doug Lea “使用泛型和并发改善集合”(John Zukowski,developerWorks,2008 年 4 月):介绍 Java 6 中 Package java.util.concurrent, Java platform SE 6:进一步学习本文讨论的工具类。 Guarded Blocks:协调线程最常用的惯用语。

“Introduction to the Collections Framework”(MageLang Institute,Sun Developer Network,1999 的 util.concurrent 包如何为标准集合类型 List 和 Map 注入新的活力。 Java Collections Framework 的变化。

年):这篇教程是很早以前的,但是很棒,它对并发集合之前的 Java Collections Framework 做了完整介绍。 ? ? ? 讨论 ?

加入 My developerWorks 社区。

“The Collections Framework”:阅读 Sun Microsystems 的 Java Collections Framework 和 API 文档。 The Joins Concurrency Library: Microsoft? Research 发布这个库,将连接 实现为一个同步机制;developerWorks Java 技术专区: 关于 Java 编程各个方面的数百篇文章。

相关的研究报告(PDF 格式)是学习连接背后理论的良好资源。

关于 java.util.concurrent 您不知道的 5 件事2

并发编程意味着更智慧地工作,而不是更困难地工作

简介: 除了具有很好的并发性的 Collections,java.util.concurrent 还引入了其他一些预先构建的组件,它们可帮助您调整和执行多线程应用程序中的线程。Ted Neward 介绍在 Java? 编程过程中使用 java.util.concurrent 包要注意的 5 点。

发布日期: 2010 年 7 月 09 日 级别: 初级 其他语言版本: 英文

并发 Collections 提供了线程安全、经过良好调优的数据结构,简化了并发编程。然而,在一些情形下,开发人员需要更进一步,思考如何调节和/或限制线程执行。由于 java.util.concurrent 的总体目标是简化多线程编程,您可能希望该包包含同步实用程序,而它确实包含。

本文是 第 1 部分 的延续,将介绍几个比核心语言原语(监视器)更高级的同步结构,但它们还未包含在 Collection 类中。一旦您了解了这些锁和门的用途,使用它们将非常直观。

关于本系列

您觉得自己懂 Java 编程?事实是,大多数开发人员都只领会到了 Java 平台的皮毛,所学也只够应付工作。在本 系列 中,Ted Neward 深度挖掘 Java 平台的核心功能,揭示一些鲜为人知的事实,帮助您解决最棘手的编程困难。

1. Semaphore

在一些企业系统中,开发人员经常需要限制未处理的特定资源请求(线程/操作)数量,事实上,限制有时候能够提高系统的吞吐量,因为它们减少了对特定资源的争用。尽管完全可以手动编写限制代码,但使用 Semaphore 类可以更轻松地完成此任务,它将帮您执行限制,如清单 1 所示:

清单 1. 使用 Semaphore 执行限制 import java.util.*;import java.util.concurrent.*;

public class SemApp{

public static void main(String[] args) { Runnable limitedCall = new Runnable() { final Random rand = new Random();

final Semaphore available = new Semaphore(3); int count = 0; public void run() {

int time = rand.nextInt(15); int num = count++; try{

available.acquire();

System.out.println(\ Thread.sleep(time * 1000);

System.out.println(\ available.release();

} catch (InterruptedException intEx) { intEx.printStackTrace(); } } };

for (int i=0; i<10; i++)

new Thread(limitedCall).start(); } }

即使本例中的 10 个线程都在运行(您可以对运行 SemApp 的 Java 进程执行 jstack 来验证),但只有 3 个线程是活跃的。在一个信号计数器释放之前,其他 7 个线程都处于空闲状态。(实际上,Semaphore 类支持一次获取和释放多个 permit,但这不适用于本场景。)

2. CountDownLatch

如果 Semaphore 是允许一次进入一个(这可能会勾起一些流行夜总会的保安的记忆)

线程的并发性类,那么 CountDownLatch 就像是赛马场的起跑门栅。此类持有所有空闲线程,直到满足特定条件,这时它将会一次释放所有这些线程。

清单 2. CountDownLatch:让我们去赛马吧! import java.util.*;

import java.util.concurrent.*;

class Race{

private Random rand = new Random();

private int distance = rand.nextInt(250); private CountDownLatch start; private CountDownLatch finish;

private List horses = new ArrayList();

public Race(String... names) {

this.horses.addAll(Arrays.asList(names)); }

public void run() throws InterruptedException{

System.out.println(\ final CountDownLatch start = new CountDownLatch(1);

final CountDownLatch finish = new CountDownLatch(horses.size());

final List places = Collections.synchronizedList(new ArrayList());

for (final String h : horses) { new Thread(new Runnable() { public void run() { try{

System.out.println(h + \ start.await();

int traveled = 0;

while (traveled < distance) {

// In a 0-2 second period of time.... Thread.sleep(rand.nextInt(3) * 1000);

// ... a horse travels 0-14 lengths traveled += rand.nextInt(15);

System.out.println(h + \ }

finish.countDown();

System.out.println(h + \ places.add(h);

}catch (InterruptedException intEx) {

System.out.println(\ intEx.printStackTrace(); } } }).start(); }

System.out.println(\ start.countDown();

finish.await();

System.out.println(\ System.out.println(places.get(0) + \ System.out.println(places.get(1) + \

System.out.println(\ } }

public class CDLApp{

public static void main(String[] args) throws InterruptedException, java.io.IOException{ System.out.println(\

Race r = new Race( \ \ \ \ \

\ \ );

System.out.println(\ System.out.println(\ System.in.read(); r.run(); } }

注意,在 清单 2 中,CountDownLatch 有两个用途:首先,它同时释放所有线程,模拟马赛的起点,但随后会设置一个门闩模拟马赛的终点。这样,“主” 线程就可以输出结果。 为了让马赛有更多的输出注释,可以在赛场的 “转弯处” 和 “半程” 点,比如赛马跨过跑道的四分之一、二分之一和四分之三线时,添加 CountDownLatch。

3. Executor

清单 1 和 清单 2 中的示例都存在一个重要的缺陷,它们要求您直接创建 Thread 对象。这可以解决一些问题,因为在一些 JVM 中,创建 Thread 是一项重量型的操作,重用现有 Thread 比创建新线程要容易得多。而在另一些 JVM 中,情况正好相反:Thread 是轻量型的,可以在需要时很容易地新建一个线程。当然,如果 Murphy 拥有自己的解决办法(他通常都会拥有),那么您无论使用哪种方法对于您最终将部署的平台都是不对的。

JSR-166 专家组(参见 参考资料)在一定程度上预测到了这一情形。Java 开发人员无需直接创建 Thread,他们引入了 Executor 接口,这是对创建新线程的一种抽象。如清单 3 所示,Executor 使您不必亲自对 Thread 对象执行 new 就能够创建新线程:

清单 3. Executor

Executor exec = getAnExecutorFromSomeplace(); exec.execute(new Runnable() { ... });

使用 Executor 的主要缺陷与我们在所有工厂中遇到的一样:工厂必须来自某个位置。不幸的是,与 CLR 不同,JVM 没有附带一个标准的 VM 级线程池。

Executor 类实际上 充当着一个提供 Executor 实现实例的共同位置,但它只有 new 方法(例如用于创建新线程池);它没有预先创建实例。所以您可以自行决定是否希望在代码中创建和使用 Executor 实例。(或者在某些情况下,您将能够使用所选的容器/平台提供的实例。)

ExecutorService 随时可以使用

尽管不必担心 Thread 来自何处,但 Executor 接口缺乏 Java 开发人员可能期望的某种功能,比如结束一个用于生成结果的线程并以非阻塞方式等待结果可用。(这是桌面应用程序的一个常见需求,用户将执行需要访问数据库的 UI 操作,然后如果该操作花费了很长时间,可能希望在它完成之前取消它。)

对于此问题,JSR-166 专家创建了一个更加有用的抽象(ExecutorService 接口),它将线程启动工厂建模为一个可集中控制的服务。例如,无需每执行一项任务就调用一次 execute(),ExecutorService 可以接受一组任务并返回一个表示每项任务的未来结果的未来列表。

4. ScheduledExecutorServices

尽管 ExecutorService 接口非常有用,但某些任务仍需要以计划方式执行,比如以确定的时间间隔或在特定时间执行给定的任务。这就是 ScheduledExecutorService 的应用范围,它扩展了 ExecutorService。

如果您的目标是创建一个每隔 5 秒跳一次的 “心跳” 命令,使用 ScheduledExecutorService 可以轻松实现,如清单 4 所示:

清单 4. ScheduledExecutorService 模拟心跳 import java.util.concurrent.*;

public class Ping{

public static void main(String[] args) {

ScheduledExecutorService ses = Executors.newScheduledThreadPool(1);

Runnable pinger = new Runnable() { public void run() {

System.out.println(\ } };

ses.scheduleAtFixedRate(pinger, 5, 5, TimeUnit.SECONDS); } }

这项功能怎么样?不用过于担心线程,不用过于担心用户希望取消心跳时会发生什么,也不用明确地将线程标记为前台或后台;只需将所有的计划细节留给 ScheduledExecutorService。

顺便说一下,如果用户希望取消心跳,scheduleAtFixedRate 调用将返回一个 ScheduledFuture 实例,它不仅封装了结果(如果有),还拥有一个 cancel 方法来关闭计划的操作。

5. Timeout 方法

为阻塞操作设置一个具体的超时值(以避免死锁)的能力是 java.util.concurrent 库相比起早期并发特性的一大进步,比如监控锁定。

这些方法几乎总是包含一个 int/TimeUnit 对,指示这些方法应该等待多长时间才释放控制权并将其返回给程序。它需要开发人员执行更多工作 — 如果没有获取锁,您将如何重新获取? — 但结果几乎总是正确的:更少的死锁和更加适合生产的代码。(关于编写生产就绪代码的更多信息,请参见 参考资料 中 Michael Nygard 编写的 Release It!。)

结束语

java.util.concurrent 包还包含了其他许多好用的实用程序,它们很好地扩展到了 Collections 之外,尤其是在 .locks 和 .atomic 包中。深入研究,您还将发现一些有用的控制结构,比如 CyclicBarrier 等。

与 Java 平台的许多其他方面一样,您无需费劲地查找可能非常有用的基础架构代码。在编写多线程代码时,请记住本文讨论的实用程序和 上一篇文章 中讨论的实用程序。

参考资料

学习 ? ? ? ?

“关于 java.util.concurrent 您不知道的 5 件事,第 1 部分”(Ted Neward,developerworks,2010 “Release it!”(Michael Nygard,2007 年 3 月,Pragmatic Programmers):设计和搭建您的应用程JSR 166:了解关于 Executor 接口和通过此 JSR 引入的其他更改的更多信息。

“Java theory and practice: Concurrent collections classes”(Brian Goetz,developerWorks,2003 年 7 年 5 月):了解 5 个并发性类,它们为 List 和 Map 等传统集合注入了新的活力。 序,实现最长的运行时间、最高的性能和投资回报。

月):学习 Doug Lea 的 util.concurrent 包如何为标准集合类型 List 和 Map 注入新的活力。 ? ? ? 讨论 ?

加入 My developerWorks 中文社区。

“使用泛型和并发改善集合”(John Zukowski,developerWorks,2008 年 4 月):介绍 Java 6 中 Package java.util.concurrent, Java platform SE 6:进一步学习本文讨论的工具类。 developerWorks Java 技术专区:关于 Java 编程各个方面的数百篇文章。 Java Collections Framework 的变化。

关于 JAR 您不知道的 5 件事

Java Archive 不仅仅是一堆类

简介: 除了一些基础的 JAR 之外,许多 Java? 开发人员绝没有想到 — 仅使用它们就可以绑定类。但 JAR 不仅仅是一个重命名的 ZIP 文件。在本文中,您将学习如何最大限度地使用 Java Archive 文件,包括 jarring Spring 依赖项和配置文件的一些技巧。

发布日期: 2010 年 7 月 22 日 级别: 初级 其他语言版本: 英文

对于大多数 Java 开发人员来说,JAR 文件及其 “近亲” WAR 和 EAR 都只不过是漫长的 Ant 或 Maven 流程的最终结果。标准步骤是将一个 JAR 复制到服务器(或者,少数情况下是用户机)中的合适位置,然后忘记它。

事实上,JAR 能做的不止是存储源代码,您应该了解 JAR 还能做什么,以及如何进行。在这一期的 5 件事 系列中,将向您展示如何最大限度地利用 Java Archive 文件(有时候也可是 WAR 和 EAR),特别是在部署时。

由于有很多 Java 开发人员使用 Spring(因为 Spring 框架给传统的 JAR 使用带来一些特有的挑战),这里有几个具体技巧用于在 Spring 应用程序中处理 JAR 。

关于本系列

您觉得自己懂 Java 编程?事实是,大多数开发人员都只领会到了 Java 平台的皮毛,所学也只够应付工作。在本 系列 中,Ted Neward 深度挖掘 Java 平台的核心功能,揭示一些鲜为人知的事实,帮助您解决最棘手的编程困难。

我将以一个标准 Java Archive 文件产生过程的简单示例开始,这将作为以下技巧的基础。

把它放在 JAR 中

通常,在源代码被编译之后,您需要构建一个 JAR 文件,使用 jar 命令行实用工具,或者,更常用的是 Ant jar 任务将 Java 代码(已经被包分离)收集到一个单独的集合中,过程简洁易懂,我不想在这做过多的说明,稍后将继续说明如何构建 JAR。现在,我只需要存档 Hello,这是一个独立控制台实用工具,对于执行打印消息到控制台这个任务十分有用。如清单 1 所示:

清单 1. 存档控制台实用工具 package com.tedneward.jars;

public class Hello{

public static void main(String[] args) { System.out.println(\ } }

Hello 实用工具内容并不多,但是对于研究 JAR 文件却是一个很有用的 “脚手架”,我们先从执行此代码开始。

1. JAR 是可执行的

.NET 和 C++ 这类语言一直是 OS 友好的,只需要在命令行(helloWorld.exe)引用其名称,或在 GUI shell 中双击它的图标就可以启动应用程序。然而在 Java 编程中,启动器程序 —java— 将 JVM 引导入进程中,我们需要传递一个命令行参数(com.tedneward.Hello)指定想要启动的 main() 方法的类。

这些附加步骤使使用 Java 创建界面友好的应用程序更加困难。不仅终端用户需要在命令行输入所有参数(终端用户宁愿避开),而且极有可能使他或她操作失误以及返回一个难以理解的错误。

这个解决方案使 JAR 文件 “可执行” ,以致 Java 启动程序在执行 JAR 文件时,自动识别哪个类将要启动。我们所要做的是,将一个入口引入 JAR 文件清单文件(MANIFEST.MF 在 JAR 的 META-INF 子目录下),像这样:

清单 2. 展示入口点!

Main-Class: com.tedneward.jars.Hello

这个清单文件只是一个名值对。因为有时候清单文件很难处理回车和空格,然而在构建 JAR 时,使用 Ant 来生成清单文件是很容易的。在清单 3 中,使用 Ant jar 任务的 manifest 元素来指定清单文件:

清单 3. 构建我的入口点!

现在用户在执行 JAR 文件时需要做的就是通过 java -jar outapp.jar 在命令行上指定其文件名。就 GUI shell 来说,双击 JAR 文件即可。

2. JAR 可以包括依赖关系信息

似乎 Hello 实用工具已经展开,改变实现的需求已经出现。Spring 或 Guice 这类依赖项注入(DI)容器可以为我们处理许多细节,但是仍然有点小问题:修改代码使其含有 DI

容器的用法可能导致清单 4 所示的结果,如:

清单 4. Hello、Spring world! package com.tedneward.jars;

import org.springframework.context.*; import org.springframework.context.support.*;

public class Hello{

public static void main(String[] args) {

ApplicationContext appContext = new FileSystemXmlApplicationContext(\ ISpeak speaker = (ISpeak) appContext.getBean(\ System.out.println(speaker.sayHello()); } }

关于 Spring 的更多信息

这个技巧将帮助您熟悉依赖项注入和 Spring 框架。如果您需要温习其他主题,见 参考资料。

由于启动程序的 -jar 选项将覆盖 -classpath 命令行选项中的所有内容,因此运行这些代码时,Spring 必须是在 CLASSPATH和 环境变量中。幸运的是,JAR 允许在清单文件中出现其他的 JAR 依赖项声明,这使得无需声明就可以隐式创建 CLASSPATH,如清单 5 所示:

清单 5. Hello、Spring CLASSPATH!

value=\ ./lib/org.springframework.core-3.0.1.RELEASE-A.jar ./lib/org.springframework.asm-3.0.1.RELEASE-A.jar ./lib/org.springframework.beans-3.0.1.RELEASE-A.jar ./lib/org.springframework.expression-3.0.1.RELEASE-A.jar ./lib/commons-logging-1.0.4.jar\

注意 Class-Path 属性包含一个与应用程序所依赖的 JAR 文件相关的引用。您可以将它写成一个绝对引用或者完全没有前缀。这种情况下,我们假设 JAR 文件同应用程序 JAR 在同一个目录下。

不幸的是,value 属性和 Ant Class-Path 属性必须出现在同一行,因为 JAR 清单文件不能处理多个 Class-Path 属性。因此,所有这些依赖项在清单文件中必须出现在一行。当

然,这很难看,但为了使 java -jar outapp.jar 可用,还是值得的!

3. JAR 可以被隐式引用

如果有几个不同的命令行实用工具(或其他的应用程序)在使用 Spring 框架,可能更容易将 Spring JAR 文件放在公共位置,使所有实用工具能够引用。这样就避免了文件系统中到处都有 JAR 副本。Java 运行时 JAR 的公共位置,众所周知是 “扩展目录” ,默认位于 lib/ext 子目录,在 JRE 的安装位置之下。

JRE 是一个可定制的位置,但是在一个给定的 Java 环境中很少定制,以至于可以完全假设 lib/ext 是存储 JAR 的一个安全地方,以及它们将隐式地用于 Java 环境的 CLASSPATH 上。

4. Java 6 允许类路径通配符

为了避免庞大的 CLASSPATH 环境变量(Java 开发人员几年前就应该抛弃的)和/或命令行 -classpath 参数,Java 6 引入了类路径通配符 的概念。与其不得不启动参数中明确列出的每个 JAR 文件,还不如自己指定 lib/*,让所有 JAR 文件列在该目录下(不递归),在类路径中。

不幸的是,类路径通配符不适用于之前提到的 Class-Path 属性清单入口。但是这使得它更容易启动 Java 应用程序(包括服务器)开发人员任务,例如 code-gen 工具或分析工具。

5. JAR 有的不只是代码

Spring,就像许多 Java 生态系统一样,依赖于一个描述构建环境的配置文件,前面提到过,Spring 依赖于一个 app.xml 文件,此文件同 JAR 文件位于同一目录 — 但是开发人员在复制 JAR 文件的同时忘记复制配置文件,这太常见了!

一些配置文件可用 sysadmin 进行编辑,但是其中很大一部分(例如 Hibernate 映射)都位于 sysadmin 域之外,这将导致部署漏洞。一个合理的解决方案是将配置文件和代码封装在一起 — 这是可行的,因为 JAR 从根本上来说就是一个 “乔装的” ZIP 文件。 当构建一个 JAR 时,只需要在 Ant 任务或 jar 命令行包括一个配置文件即可。

JAR 也可以包含其他类型的文件,不仅仅是配置文件。例如,如果我的 SpeakEnglish 部件要访问一个属性文件,我可以进行如下设置,如清单 6 所示:

清单 6. 随机响应 package com.tedneward.jars;

import java.util.*;

public class SpeakEnglish implements ISpeak{ Properties responses = new Properties(); Random random = new Random();

public String sayHello(){ // Pick a response at random

int which = random.nextInt(5);

return responses.getProperty(\ } }

可以将 responses.properties 放入 JAR 文件,这意味着部署 JAR 文件时至少可以少考虑一个文件。这只需要在 JAR 步骤中包含 responses.properties 文件即可。

当您在 JAR 中存储属性之后,您可能想知道如何将它取回。如果所需要的数据与 JAR 文件在同一位置,正如前面的例子中提到的那样,不需要费心找出 JAR 文件的位置,使用 JarFile 对象就可将其打开。相反,可以使用类的 ClassLoader 找到它,像在 JAR 文件中寻找 “资源” 那样,使用 ClassLoader getResourceAsStream() 方法,如清单 7 所示:

清单 7. ClassLoader 定位资源 package com.tedneward.jars; import java.util.*;

public class SpeakEnglish implements ISpeak{ Properties responses = new Properties(); // ...

public SpeakEnglish() { try{

ClassLoader myCL = SpeakEnglish.class.getClassLoader();

responses.load( myCL.getResourceAsStream( \ }catch (Exception x) { x.printStackTrace(); } } // ... }

您可以按照以上步骤寻找任何类型的资源:配置文件、审计文件、图形文件,等等。几乎任何文件类型都能被捆绑进 JAR 中,作为一个 InputStream 获取(通过 ClassLoader),并通过您喜欢的方式使用。

结束语

本文涵盖了关于 JAR 大多数开发人员所不知道的 5 件最重要的事 — 至少基于历史,有据可查。注意,所有的 JAR 相关技巧对于 WAR 同样可用,一些技巧(特别是 Class-Path 和 Main-Class 属性)对于 WAR 来说不是那么出色,因为 servlet 环境需要全部目录,并且要有一个预先确定的入口点,但是,总体上来看这些技巧可以使我们摆脱 “好的,开始在该目录下复制......” 的模式,这也使得他们部署 Java 应用程序更为简单。

本系列的下一个主题是:关于 Java 应用程序性能监视您不知道的 5 件事。

参考资料

学习 ? ? ? ? ? ? ? ?

关于 ... 您不知道的 5 件事(Ted Neward,developerworks):本系列将 Java 琐事变成编程宝典。 “JAR 文件揭密”(Pagadala Suresh 和 Palaniyappan Thiagarajan,developerWorks,2003 年 10 将程序打包进 JAR 文件(Java Tutorials 系列):JAR 基础。

Spring:关于这个健壮的、灵活的、流行的框架的信息,直接从源中获取。

“通过 Guice 进行依赖项注入”(Nicholas Lesiecki,developerworks,2008 年 10 月):DI 提高“在 java 类路径中设置多个 jar” (StackOverflow Q&A,2010 年 5 月最后一次更新):关于类路“查看 JAR 运行”(Shawn Silverman,JavaWorld.com,2002 年 5 月):一个关于创建可执行 JAR “编程语言系列的高级话题:JSR 277: Java Module System”(Google Tech Talks,2007 年 5 月):

月):介绍了 Java Archive 模式的特性和优势,包括打包、可执行 JAR 文件、安全性和索引。

了可维护性、可测试性和灵活性,Guice 使 DI 变得很简单。 径通配符的更多信息。 文件的教程。

JAR 也有不足之处 — 令人讨厌的类路径、JAR 文件和扩展,略举一二。这篇 Google Tech Talk 文章解释 JSR 277 是如何对它们进行更正的。 ? 讨论 ?

加入 My developerWorks 中文社区。

developerWorks Java 技术专区:这里有数百篇关于 Java 编程各个方面的文章。

关于 Java 性能监控您不知道的 5 件事1

使用 JConsole 和 VisualVM 进行 Java 性能分析

简介: 责怪糟糕的代码(或不良代码对象)并不能帮助您发现瓶颈,提高 Java? 应用程序速度,猜测也不能帮您解决。Ted Neward 引导您关注 Java 性能监控工具,从 5 个技巧开始,使用 Java 5 的内置分析器 JConsole 收集和分析性能数据。

本文的标签: j2se_(java_2_standard_edition), java, java性能, performance, 应用开发, 性能

发布日期: 2010 年 8 月 20 日 级别: 初级 其他语言版本: 英文

关于本系列

您觉得自己懂 Java 编程?事实是,大多数开发人员都只领会到了 Java 平台的皮毛,所学也只够应付工作。在本 系列 中,Ted Neward 深度挖掘 Java 平台的核心功能,揭示一些鲜为人知的事实,帮助您解决最棘手的编程困难。

当应用程序性能受到损害时,大多数开发人员都惊慌失措,这在情理之中。跟踪 Java 应用程序瓶颈来源一直以来都是很麻烦的,因为 Java 虚拟机有黑盒效应,而且 Java 平台分析工具一贯就有缺陷。

然而,随着 Java 5 中 JConsole 的引入,一切都发生了改变。JConsole 是一个内置 Java 性能分析器,可以从命令行或在 GUI shell 中运行。它不是完美的,但是当尖头老板来问你关于性能的问题时,用它来应对还是绰绰有余的 — 这比查询 Papa Google 要好得多。

在本期 5 件事 系列中,我将向您展示 5 个方法,使您可以轻松地使用 JConsole(或者,它更高端的 “近亲” VisualVM )来监控 Java 应用程序性能和跟踪 Java 中的代码。

1. JDK 附带分析器

许多开发人员没有意识到从 Java 5 开始 JDK 中包含了一个分析器。JConsole(或者 Java 平台最新版本,VisualVM)是一个内置分析器,它同 Java 编译器一样容易启动。如果是从命令行启动,使 JDK 在 PATH 上,运行 jconsole 即可。如果从 GUI shell 启动,找到 JDK 安装路径,打开 bin 文件夹,双击 jconsole。

当分析工具弹出时(取决于正在运行的 Java 版本以及正在运行的 Java 程序数量),可能会出现一个对话框,要求输入一个进程的 URL 来连接,也可能列出许多不同的本地 Java 进程(有时包含 JConsole 进程本身)来连接。

JConsole 或 VisualVM?

JConsole 从 Java 5 开始就随着 Java 平台版本一起发布,而 VisualVM 是在 NetBeans 基础上升级的一个分析器,在 Java 6 的更新版 12 中第一次发布。多数商店还没有更新到 Java 6 ,因此这篇文章主要介绍 JConsole 。然而,多数技巧和这两个分析器都有关。(注意:除了包含在 Java 6 中之外,VisualVM 还有一个独立版下载。下载 VisualVM,参见 参考资料。)

使用 JConsole 进行工作

在 Java 5 中,Java 进程并不是被设置为默认分析的,而是通过一个命令行参数 — -Dcom.sun.management.jmxremote — 在启动时告诉 Java 5 VM 打开连接,以便分析器可以找到它们;当进程被 JConsole 捡起时,您只能双击它开始分析。

分析器有自己的开销,因此最好的办法就是花点时间来弄清是什么开销。发现 JConsole 开销最简单的办法是,首先独自运行一个应用程序,然后在分析器下运行,并测量差异。(应用程序不能太大或者太小;我最喜欢使用 JDK 附带的 SwingSet2 样本。)因此,我使用 -verbose:gc 尝试运行 SwingSet2 来查看垃圾收集清理,然后运行同一个应用程序并将 JConsole 分析器连接到它。当 JConsole 连接好了之后,一个稳定的 GC 清理流出现,否则不会出现。这就是分析器的性能开销。

2. 远程连接进程

因为 Web 应用程序分析工具假设通过一个套接字进行连通性分析,您只需要进行少许配置来设置 JConsole(或者是基于 JVMTI 的分析器,就这点而言),监控/分析远程运行的应用程序。

如果 Tomcat 运行在一个名为 “webserve” 的机器上,且 JVM 已经启动了 JMX 并监听端口 9004,从 JConsole(或者任何 JMX 客户端)连接它需要一个 JMX URL “service:jmx:rmi:///jndi/rmi://webserver:9004/jmxrmi”。

基本上,要分析一个运行在远程数据中心的应用程序服务器,您所需要的仅仅是一个 JMX URL。更多关于使用 JMX 和 JConsole 远程监控和管理的信息,参见 参考资料。)

3. 跟踪统计

不要成为典型

发现应用程序代码中性能问题的常用响应多种多样,但也是可预测的。早期的 Java 编程人员对旧的 IDE 可能十分生气,并开始进行代码库中主要部分的代码复查,在源代码中寻找熟悉的 “红色标志”,像异步块、对象配额等等。随着编程经验的增加,开发人员可能会仔细研究 JVM 支持的 -X 标志,寻找优化垃圾收集器的方法。当然,对于新手,直接去 Google 查询,希望有其他人发现了 JVM 的神奇的 “make it go fast” 转换,避免重写代码。

从本质上来说,这些方法没什么错,但都是有风险的。对于一个性能问题最有效的响应就是使用一个分析器 — 现在它们内置在 Java 平台 ,我们确实没有理由不这样做!

JConsole 有许多对收集统计数据有用的选项卡,包括:

? Memory:在 JVM 垃圾收集器中针对各个堆跟踪活动。 ? Threads:在目标 JVM 中检查当前线程活动。 ? Classes:观察 VM 已加载类的总数。

这些选项卡(和相关的图表)都是由每个 Java 5 及更高版本 VM 在 JMX 服务器上注册的 JMX 对象提供的,是内置到 JVM 的。一个给定 JVM 中可用 bean 的完整清单在 MBeans 选项卡上列出,包括一些元数据和一个有限的用户界面来查看数据或执行操作。(然而,注册通知是在 JConsole 用户界面之外。)

使用统计数据

假设一个 Tomcat 进程死于 OutOfMemoryError。如果您想要弄清楚发生了什么,打开 JConsole,单击 Classes 选项卡,过一段时间查看一次类计数。如果数量稳定上升,您可以假设应用程序服务器或者您的代码某个地方有一个 ClassLoader 漏洞,不久之后将耗尽 PermGen 空间。如果需要更进一步的确认问题,请看 Memory 选项卡。

4. 为离线分析创建一个堆转储

生产环境中一切都在快速地进行着,您可能没有时间花费在您的应用程序分析器上,相反地,您可以为 Java 环境中的每个事件照一个快照保存下来过后再看。在 JConsole 中您也可以这样做,在 VisualVM 中甚至会做得更好。

先找到 MBeans 选项卡,在其中打开 com.sun.management 节点,接着是 HotSpotDiagnostic 节点。现在,选择 Operations,注意右边面板中的 “dumpHeap” 按钮。如果您在第一个(“字符串”)输入框中向 dumpHeap 传递一个文件名来转储,它将为整个 JVM 堆照一个快照,并将其转储到那个文件。

稍后,您可以使用各种不同的商业分析器来分析文件,或者使用 VisualVM 分析快照。(记住,VisualVM 是在 Java 6 中可用的,且是单独下载的。)

5. JConsole 并不是高深莫测的

作为一个分析器实用工具,JConsole 是极好的,但是还有更好的工具。一些分析插件附带分析器或者灵巧的用户界面,默认情况下比 JConsole 跟踪更多的数据。

JConsole 真正吸引人的是整个程序是用 “普通旧式 Java ” 编写的,这意味着任何 Java

本文来源:https://www.bwwdw.com/article/6dl3.html

Top