Java 序列化简介
Java 对象序列化是 JDK 1.1 中引入的一组开创性特性之一,用于作为一种将 Java 对象的状态转换为字节数组,以便存储或传输的机制,以后,仍可以将字节数组转换回 Java 对象原有的状态。
实际上,序列化的思想是 “冻结” 对象状态,传输对象状态(写到磁盘、通过网络传输等等),然后 “解冻” 状态,重新获得可用的 Java 对象。所有这些事情的发生有点像是魔术,这要归功于 ObjectInputStream/ObjectOutputStream 类、完全保真的元数据以及程序员愿意用 Serializable 标识接口标记他们的类,从而 “参与” 这个过程。
1. 序列化允许重构
序列化允许一定数量的类变种,甚至重构之后也是如此,ObjectInputStream 仍可以很好地将其读出来。
Java Object Serialization 规范可以自动管理的关键任务是:
将新字段添加到类中
将字段从 static 改为非 static
将字段从 transient 改为非 transient
取决于所需的向后兼容程度,转换字段形式(从非 static 转换为 static 或从非 transient 转换为 transient)或者删除字段需要额外的消息传递。
2. 序列化并不安全
让 Java 开发人员诧异并感到不快的是,序列化二进制格式完全编写在文档中,并且完全可逆。实际上,只需将二进制序列化流的内容转储到控制台,就足以看清类是什么样子,以及它包含什么内容。
这对于安全性有着不良影响。例如,当通过 RMI 进行远程方法调用时,通过连接发送的对象中的任何 private 字段几乎都是以明文的方式出现在套接字流中,这显然容易招致哪怕最简单的安全问题。
幸运的是,序列化允许 “hook” 序列化过程,并在序列化之前和反序列化之后保护(或模糊化)字段数据。可以通过在 Serializable对象上提供一个 writeObject 方法来做到这一点。
3. 序列化的数据可以被签名和密封
上一个技巧假设您想模糊化序列化数据,而不是对其加密或者确保它不被修改。当然,通过使用 writeObject 和 readObject 可以实现密码加密和签名管理,但其实还有更好的方式。
如果需要对整个对象进行加密和签名,最简单的是将它放在一个 javax.crypto.SealedObject 和/或 java.security.SignedObject 包装器中。两者都是可序列化的,所以将对象包装在 SealedObject 中可以围绕原对象创建一种 “包装盒”。必须有对称密钥才能解密,而且密钥必须单独管理。同样,也可以将 SignedObject 用于数据验证,并且对称密钥也必须单独管理。
结合使用这两种对象,便可以轻松地对序列化数据进行密封和签名,而不必强调关于数字签名验证或加密的细节。很简洁,是吧?
4. 序列化允许将代理放在流中
很多情况下,类中包含一个核心数据元素,通过它可以派生或找到类中的其他字段。在此情况下,没有必要序列化整个对象。可以将字段标记为 transient,但是每当有方法访问一个字段时,类仍然必须显式地产生代码来检查它是否被初始化。
如果首要问题是序列化,那么最好指定一个 flyweight 或代理放在流中。为原始 Person 提供一个 writeReplace 方法,可以序列化不同类型的对象来代替它。类似地,如果反序列化期间发现一个 readResolve 方法,那么将调用该方法,将替代对象提供给调用者。
打包和解包代理
writeReplace 和 readResolve 方法使 Person 类可以将它的所有数据(或其中的核心数据)打包到一个 PersonProxy 中,将它放入到一个流中,然后在反序列化时再进行解包。
5. 信任,但要验证
认为序列化流中的数据总是与最初写到流中的数据一致,这没有问题。但是,正如一位美国前总统所说的,“信任,但要验证”。
对于序列化的对象,这意味着验证字段,以确保在反序列化之后它们仍具有正确的值,“以防万一”。为此,可以实现ObjectInputValidation 接口,并覆盖 validateObject() 方法。如果调用该方法时发现某处有错误,则抛出一个InvalidObjectException。
结束语
Java 对象序列化比大多数 Java 开发人员想象的更灵活,这使我们有更多的机会解决棘手的情况。
幸运的是,像这样的编程妙招在 JVM 中随处可见。关键是要知道它们,在遇到难题的时候能用上它们。
关于 Java Collections API 的 5 件事
Collections 比数组好
刚接触 Java 技术的开发人员可能不知道,Java 语言最初包括数组,是为了应对上世纪 90 年代初期 C++ 开发人员对于性能方面的批评。从那时到现在,我们已经走过一段很长的路,如今,与 Java Collections 库相比,数组不再有性能优势。
例如,若要将数组的内容转储到一个字符串,需要迭代整个数组,然后将内容连接成一个 String;而 Collections 的实现都有一个可用的 toString() 实现。
除少数情况外,好的做法是尽快将遇到的任何数组转换成集合。
2. 迭代的效率较低
将一个集合(特别是由数组转化而成的集合)的内容转移到另一个集合,或者从一个较大对象集合中移除一个较小对象集合,这些事情并不鲜见。
您也许很想对集合进行迭代,然后添加元素或移除找到的元素,但是不要这样做。
在此情况下,迭代有很大的缺点:
每次添加或移除元素后重新调整集合将非常低效。
每次在获取锁、执行操作和释放锁的过程中,都存在潜在的并发困境。
当添加或移除元素时,存取集合的其他线程会引起竞争条件。
可以通过使用 addAll 或 removeAll,传入包含要对其添加或移除元素的集合作为参数,来避免所有这些问题。
3. 用 for 循环遍历任何 Iterable
Java 5 中加入 Java 语言的最大的便利功能之一,增强的 for 循环,消除了使用 Java 集合的最后一道障碍。
以前,开发人员必须手动获得一个 Iterator,使用 next() 获得 Iterator 指向的对象,并通过 hasNext() 检查是否还有更多可用对象。从 Java 5 开始,我们可以随意使用 for 循环的变种,它可以在幕后处理上述所有工作。
实际上,这个增强适用于实现 Iterable 接口的任何对象,而不仅仅是 Collections。
4. 经典算法和定制算法
您是否曾想过以倒序遍历一个 Collection?对于这种情况,使用经典的 Java Collections 算法非常方便。
在上面的 清单 2 中,Person 的孩子是按照传入的顺序排列的;但是,现在要以相反的顺序列出他们。虽然可以编写另一个 for 循环,按相反顺序将每个对象插入到一个新的 ArrayList 中,但是 3、4 次重复这样做之后,就会觉得很麻烦。
Collections 类有很多这样的 “算法”,它们被实现为静态方法,以 Collections 作为参数,提供独立于实现的针对整个集合的行为。 而且,由于很棒的 API 设计,我们不必完全受限于 Collections 类中提供的算法 — 例如,我喜欢不直接修改(传入的 Collection 的)内容的方法。所以,可以编写定制算法是一件很棒的事情.
5. 扩展 Collections API
以上定制算法阐释了关于 Java Collections API 的一个最终观点:它总是适合加以扩展和修改,以满足开发人员的特定目的。
例如,假设您需要 Person 类中的孩子总是按年龄排序。虽然可以编写代码一遍又一遍地对孩子排序(也许是使用 Collections.sort方法),但是通过一个 Collection 类来自动排序要好得多。
实际上,您甚至可能不关心是否每次按固定的顺序将对象插入到 Collection 中(这正是 List 的基本原理)。您可能只是想让它们按一定的顺序排列。
java.util 中没有 Collection 类能满足这些需求,但是编写一个这样的类很简单。只需创建一个接口,用它描述 Collection 应该提供的抽象行为。对于 SortedCollection,它的作用完全是行为方面的。
关于 Java Collections API 的 5 件事,第 2 部分
1. List 不同于数组
Java 开发人员常常错误地认为 ArrayList 就是 Java 数组的替代品。Collections 由数组支持,在集合内随机查找内容时性能较好。与数组一样,集合使用整序数获取特定项。但集合不是数组的简单替代。
要明白数组与集合的区别需要弄清楚顺序 和位置 的不同。当第三个元素从上面的 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 中盲目地处理.
4. 注意可变的 hashCode()
Map 是很好的集合,为我们带来了在其他语言(比如 Perl)中经常可见的好用的键/值对集合。JDK 以 HashMap 的形式为我们提供了方便的 Map 实现,它在内部使用哈希表实现了对键的对应值的快速查找。但是这里也有一个小问题:支持哈希码的键依赖于可变字段的内容,这样容易产生 bug,即使最耐心的 Java 开发人员也会被这些 bug 逼疯。
5. equals() 与 Comparable
在浏览 Javadoc 时,Java 开发人员常常会遇到 SortedSet 类型(它在 JDK 中唯一的实现是 TreeSet)。因为 SortedSet 是java.util 包中唯一提供某种排序行为的 Collection,所以开发人员通常直接使用它而不会仔细地研究它。
使用上述代码一段时间后,可能会发现这个 Set 的核心特性之一:它不允许重复。该特性在 Set Javadoc 中进行了介绍。Set 是不包含重复元素的集合。更准确地说,set 不包含成对的 e1 和 e2 元素,因此如果 e1.equals(e2),那么最多包含一个 null 元素。 与 set 的有状态本质相反,TreeSet 要求对象直接实现 Comparable 或者在构造时传入 Comparator,它不使用 equals() 比较对象;它使用 Comparator/Comparable 的 compare 或 compareTo 方法。
因此存储在 Set 中的对象有两种方式确定相等性:大家常用的 equals() 方法和 Comparable/Comparator 方法,采用哪种方法取决于上下文。
更糟的是,简单的声明两者相等还不够,因为以排序为目的的比较不同于以相等性为目的的比较:可以想象一下按姓排序时两个Person 相等,但是其内容却并不相同。
一定要明白 equals() 和 Comparable.compareTo() 两者之间的不同 — 实现 Set 时会返回 0。甚至在文档中也要明确两者的区别。
关于 java.util.concurrent 您不知道的 5 件事,第 1 部分
事实上,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 教程中的代码有效,但是它耗时久,混乱,而且也并非完全直观。退回到 Java 平台较早的时候,没错,Java 开发人员不得不纠缠于这种代码;但现在是 2010 年 — 情况难道没有改善?
ArrayBlockingQueue 还体现了“公平” — 意思是它为读取器和编写器提供线程先入先出访问。这种替代方法是一个更有效,但又冒穷尽部分线程风险的政策。(即,允许一些读取器在其他读取器锁定时运行效率更高,但是您可能会有读取器线程的流持续不断的风险,导致编写器无法进行工作。)
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 使用的阻塞语义。
关于 java.util.concurrent 您不知道的 5 件事,第 2 部分
1. Semaphore
在一些企业系统中,开发人员经常需要限制未处理的特定资源请求(线程/操作)数量,事实上,限制有时候能够提高系统的吞吐量,因为它们减少了对特定资源的争用。尽管完全可以手动编写限制代码,但使用 Semaphore 类可以更轻松地完成此任务,它将帮您执行限制.
2. CountDownLatch
如果 Semaphore 是允许一次进入一个(这可能会勾起一些流行夜总会的保安的记忆)线程的并发性类,那么 CountDownLatch 就像是赛马场的起跑门栅。此类持有所有空闲线程,直到满足特定条件,这时它将会一次释放所有这些线程。
Executor
在一些 JVM 中,创建 Thread 是一项重量型的操作,重用现有 Thread 比创建新线程要容易得多。而在另一些 JVM 中,情况正好相反:Thread 是轻量型的,可以在需要时很容易地新建一个线程。当然,如果 Murphy 拥有自己的解决办法(他通常都会拥有),那么您无论使用哪种方法对于您最终将部署的平台都是不对的。
使用 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 可以轻松实现.
这项功能怎么样?不用过于担心线程,不用过于担心用户希望取消心跳时会发生什么,也不用明确地将线程标记为前台或后台;只需将所有的计划细节留给 ScheduledExecutorService。
顺便说一下,如果用户希望取消心跳,scheduleAtFixedRate 调用将返回一个 ScheduledFuture 实例,它不仅封装了结果(如果有),还拥有一个 cancel 方法来关闭计划的操作。
这项功能怎么样?不用过于担心线程,不用过于担心用户希望取消心跳时会发生什么,也不用明确地将线程标记为前台或后台;只需将所有的计划细节留给 ScheduledExecutorService。
顺便说一下,如果用户希望取消心跳,scheduleAtFixedRate 调用将返回一个 ScheduledFuture 实例,它不仅封装了结果(如果有),还拥有一个 cancel 方法来关闭计划的操作。
5. Timeout 方法
为阻塞操作设置一个具体的超时值(以避免死锁)的能力是 java.util.concurrent 库相比起早期并发特性的一大进步,比如监控锁定。
这些方法几乎总是包含一个 int/TimeUnit 对,指示这些方法应该等待多长时间才释放控制权并将其返回给程序。它需要开发人员执行更多工作 — 如果没有获取锁,您将如何重新获取? — 但结果几乎总是正确的:更少的死锁和更加适合生产的代码。
结束语
java.util.concurrent 包还包含了其他许多好用的实用程序,它们很好地扩展到了 Collections 之外,尤其是在 .locks 和 .atomic包中。深入研究,您还将发现一些有用的控制结构,比如 CyclicBarrier 等。
关于 Java 性能监控您不知道的 5 件事,第 1 部分
1. JDK 附带分析器
许多开发人员没有意识到从 Java 5 开始 JDK 中包含了一个分析器。JConsole(或者 Java 平台最新版本,VisualVM)是一个内置分析器,它同 Java 编译器一样容易启动。如果是从命令行启动,使 JDK 在 PATH 上,运行 jconsole 即可。如果从 GUI shell 启动,找到 JDK 安装路径,打开 bin 文件夹,双击 jconsole。
当分析工具弹出时(取决于正在运行的 Java 版本以及正在运行的 Java 程序数量),可能会出现一个对话框,要求输入一个进程的 URL 来连接,也可能列出许多不同的本地 Java 进程(有时包含 JConsole 进程本身)来连接。
使用 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。
跟踪统计
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 分析快照。
5. JConsole 并不是高深莫测的
作为一个分析器实用工具,JConsole 是极好的,但是还有更好的工具。一些分析插件附带分析器或者灵巧的用户界面,默认情况下比 JConsole 跟踪更多的数据。
JConsole 真正吸引人的是整个程序是用 “普通旧式 Java ” 编写的,这意味着任何 Java 开发人员都可以编写这样一个实用工具。事实上,JDK 其中甚至包括如何通过创建一个插件来定制 JConsole 的示例(参见 参考资料)。建立在 NetBeans 顶部的 VisualVM 进一步延伸了插件概念。
如果 JConsole(或者 VisualVM,或者其他任何工具)不符合您的需求,或者不能跟踪您想要跟踪的,或者不能按照您的方式跟踪,您可以编写属于自己的工具。如果您觉得 Java 代码很麻烦,Groovy 或 JRuby 或很多其他 JVM 语言都可以帮助您更快完成。
您真正需要的是一个快速而粗糙(quick-and-dirty)的由 JVM 连接的命令行工具,可以以您想要的方式确切地跟踪您感兴趣的数据。
关于 Java 性能监控您不知道的 5 件事,第 2 部分
1. jps (sun.tools.jps)
很多命令行工具都要求您识别您希望监控的 Java 进程。这与监控本地操作系统进程、同样需要一个程序识别器的同类工具没有太大区别。
“VMID” 识别器与本地操作系统进程识别器(“pid”)并不总是相同的,这就是我们需要 JDK jps 实用程序的原因。
jps — 名称反映了在大多数 UNIX 系统上发现的 ps 实用程序 — 告诉我们运行 Java 应用程序的 JVMID。顾名思义,jps 返回指定机器上运行的所有已发现的 Java 进程的 VMID。如果 jps 没有发现进程,并不意味着无法附加或研究 Java 进程,而只是意味着它并未宣传自己的可用性。
如果发现 Java 进程,jps 将列出启用它的命令行。这种区分 Java 进程的方法非常重要,因为只要涉及操作系统,所有的 Java 进程都被统称为 “java”。在大多数情况下,VMID 是值得注意的重要数字。
2. jstat (sun.tools.jstat)
jstat 实用程序可以用于收集各种各样不同的统计数据。jstat 统计数据被分类到 “选项” 中,这些选项在命令行中被指定作为第一参数。对于 JDK 1.6 来说,您可以通过采用命令 -options 运行 jstat 查看可用的选项清单。
默认情况下,jstat 在您核对信息时显示信息。如果您希望每隔一定时间拍摄快照,请在 -options 指令后以毫秒为单位指定间隔时间。jstat 将持续显示监控进程信息的快照。如果您希望 jstat 在终止前进行特定数量的快照,在间隔时间/时间值后指定该数字。
3. jstack (sun.tools.jstack)
了解 Java 进程及其对应的执行线程内部发生的情况是一种常见的诊断挑战。例如,当一个应用程序突然停止进程时,很明显出现了资源耗尽,但是仅通过查看代码无法明确知道何处出现资源耗尽,且为什么会发生。
jstack 是一个可以返回在应用程序上运行的各种各样线程的一个完整转储的实用程序,您可以使用它查明问题。
采用期望进程的 VMID 运行 jstack 会产生一个堆转储。就这一点而言,jstack 与在控制台窗口内按 Ctrl-Break 键起同样的作用,在控制台窗口中,Java 进程正在运行或调用 VM 内每个 Thread 对象上的 Thread.getAllStackTraces() 或Thread.dumpStack()。jstack 调用也转储关于在 VM 内运行的非 Java 线程的信息,这些线程作为 Thread 对象并不总是可用的。
jstack 的 -l 参数提供了一个较长的转储,包括关于每个 Java 线程持有锁的更多详细信息,因此发现(和 squash)死锁或可伸缩性 bug 是极其重要的。
4. jmap (sun.tools.jmap)
有时,您正在处理的问题是一个对象泄露,如一个 ArrayList (可能持有成千上万个对象)该释放时没有释放。另一个更普遍的问题是,看似从不会压缩的扩展堆,却有活跃的垃圾收集。
当您努力寻找一个对象泄露时,在指定时刻对堆及时进行拍照,然后审查其中内容非常有用。jmap 通过对堆拍摄快照来提供该功能的第一部分。然后您可以采用下一部分中描述的 jhat 实用程序分析堆数据。
与这里描述的其他所有实用程序一样,使用 jmap 非常简单。将 jmap 指向您希望拍快照的 Java 进程的 VMID,然后给予它部分参数,用来描述产生的结果文件。您要传递给 jmap 的选项包括转储文件的名称以及是否使用一个文本文件或二进制文件。二进制文件是最有用的选项,但是只有当与某一种索引工具 结合使用时 — 通过十六进制值的文本手动操作数百兆字节不是最好的方法。
随意看一下 Java 堆的更多信息,jmap 同样支持 -histo 选项。-histo 产生一个对象文本柱状图,现在在堆中大量引用,由特定类型消耗的字节总数分类。它同样给出了特定类型的总示例数量,支持部分原始计算,并猜测每个实例的相对成本。
不幸的是,jmap 没有像 jstat 一样的 period-and-max-count 选项,但是将 jmap(或 jmap.main())调用放入 shell 脚本或其他类的循环,周期性地拍摄快照相对简单。(事实上,这是加入 jmap 的一个好的扩展,不管是作为 OpenJDK 本身的源补丁,还是作为其他实用程序的扩展。)
5. jhat (com.sun.tools.hat.Main)
将堆转储至一个二进制文件后,您就可以使用 jhat 分析二进制堆转储文件。jhat 创建一个 HTTP/HTML 服务器,该服务器可以在浏览器中被浏览,提供一个关于堆的 object-by-object 视图,及时冻结。根据对象引用草率处理堆可能会非常可笑,您可以通过对总体混乱进行某种自动分析而获得更好的服务。幸运的是,jhat 支持 OQL 语法进行这样的分析。
结束语
当您需要近距离观察 Java 进程内发生的事情时,JDK 的分析扩展会非常有用。本文中介绍的所有工具都可以从命令行中由其自己使用。它们还可以与 JConsole 或 VisualVM 有力地结合使用。JConsole 和 VisualVM 提供 Java 虚拟机的总体视图,jstat 和 jmap 等有针对性的工具支持您对研究进行微调。
关于 Java Scripting API
Java Scripting API 是从 Java 6 开始引入的,它填补了便捷的小脚本语言和健壮的 Java 生态系统之间的鸿沟。通过使用 Java Scripting API,您就可以在您的 Java 代码中快速整合几乎所有的脚本语言,这使您能够在解决一些很小的问题时有更多可选择的方法。
1. 使用 jrunscript 执行 JavaScript
每一个新的 Java 平台发布都会带来新的命令行工具集,它们位于 JDK 的 bin 目录。Java 6 也一样,其中 jrunscript 便是 Java 平台工具集中的一个不小的补充。
2. 从脚本访问 Java 对象
能够编写 JavaScript/ECMAScript 代码是非常好的,但是我们不希望被迫重新编译我们在 Java 语言中使用的所有代码 — 这是违背我们初衷的。幸好,所有使用 Java Scripting API 引擎的代码都完全能够访问整个 Java 生态系统,因为本质上一切代码都还是 Java 字节码。所以,回到我们之前的问题,我们可以在 Java 平台上使用传统的 Runtime.exec() 调用来启动进程
3. 从 Java 代码调用脚本
从脚本调用 Java 对象仅仅完成了一半的工作:Java 脚本环境也提供了从 Java 代码调用脚本的功能。这只需要实例化一个ScriptEngine 对象,然后加载和评估脚本
4. 将 Java 对象绑定到脚本空间
仅仅调用一个脚本还不够:脚本通常会与 Java 环境中创建的对象进行交互。这时,Java 主机环境必须创建一些对象并将它们绑定,这样脚本就可以很容易找到和使用这些对象。这个过程是 ScriptContext 对象的任务
5. 编译频繁使用的脚本
脚本语言的缺点一直存在于性能方面。其中的原因是,大多数情况下脚本语言是 “即时” 解译的,因而它在执行时会损失一些解析和验证文本的时间和 CPU 周期。运行在 JVM 的许多脚本语言最终会将接收的代码转换为 Java 字节码,至少在脚本被第一次解析和验证时进行转换;在 Java 程序关闭时,这些即时编译的代码会消失。将频繁使用的脚本保持为字节码形式可以帮助提升可观的性能。
我们可以以一种很自然和有意义的方法使用 Java Scripting API。如果返回的 ScriptEngine 实现了 Compilable 接口,那么这个接口所编译的方法可用于将脚本(以一个 String 或一个 Reader 传递过来的)编译为一个 CompiledScript 实例,然后它可用于在 eval() 方法中使用不同的绑定重复地处理编译后的代码
在大多数情况中,CompiledScript 实例需要存储在一个长时间存储中(例如,servlet-context),这样才能避免一次次地重复编译相同的脚本。然而,如果脚本发生变化,您就需要创建一个新的 CompiledScript 来反映这个变化;一旦编译完成,CompiledScript就不再执行原始的脚本文件内容。
Java 对象序列化是 JDK 1.1 中引入的一组开创性特性之一,用于作为一种将 Java 对象的状态转换为字节数组,以便存储或传输的机制,以后,仍可以将字节数组转换回 Java 对象原有的状态。
实际上,序列化的思想是 “冻结” 对象状态,传输对象状态(写到磁盘、通过网络传输等等),然后 “解冻” 状态,重新获得可用的 Java 对象。所有这些事情的发生有点像是魔术,这要归功于 ObjectInputStream/ObjectOutputStream 类、完全保真的元数据以及程序员愿意用 Serializable 标识接口标记他们的类,从而 “参与” 这个过程。
1. 序列化允许重构
序列化允许一定数量的类变种,甚至重构之后也是如此,ObjectInputStream 仍可以很好地将其读出来。
Java Object Serialization 规范可以自动管理的关键任务是:
将新字段添加到类中
将字段从 static 改为非 static
将字段从 transient 改为非 transient
取决于所需的向后兼容程度,转换字段形式(从非 static 转换为 static 或从非 transient 转换为 transient)或者删除字段需要额外的消息传递。
2. 序列化并不安全
让 Java 开发人员诧异并感到不快的是,序列化二进制格式完全编写在文档中,并且完全可逆。实际上,只需将二进制序列化流的内容转储到控制台,就足以看清类是什么样子,以及它包含什么内容。
这对于安全性有着不良影响。例如,当通过 RMI 进行远程方法调用时,通过连接发送的对象中的任何 private 字段几乎都是以明文的方式出现在套接字流中,这显然容易招致哪怕最简单的安全问题。
幸运的是,序列化允许 “hook” 序列化过程,并在序列化之前和反序列化之后保护(或模糊化)字段数据。可以通过在 Serializable对象上提供一个 writeObject 方法来做到这一点。
3. 序列化的数据可以被签名和密封
上一个技巧假设您想模糊化序列化数据,而不是对其加密或者确保它不被修改。当然,通过使用 writeObject 和 readObject 可以实现密码加密和签名管理,但其实还有更好的方式。
如果需要对整个对象进行加密和签名,最简单的是将它放在一个 javax.crypto.SealedObject 和/或 java.security.SignedObject 包装器中。两者都是可序列化的,所以将对象包装在 SealedObject 中可以围绕原对象创建一种 “包装盒”。必须有对称密钥才能解密,而且密钥必须单独管理。同样,也可以将 SignedObject 用于数据验证,并且对称密钥也必须单独管理。
结合使用这两种对象,便可以轻松地对序列化数据进行密封和签名,而不必强调关于数字签名验证或加密的细节。很简洁,是吧?
4. 序列化允许将代理放在流中
很多情况下,类中包含一个核心数据元素,通过它可以派生或找到类中的其他字段。在此情况下,没有必要序列化整个对象。可以将字段标记为 transient,但是每当有方法访问一个字段时,类仍然必须显式地产生代码来检查它是否被初始化。
如果首要问题是序列化,那么最好指定一个 flyweight 或代理放在流中。为原始 Person 提供一个 writeReplace 方法,可以序列化不同类型的对象来代替它。类似地,如果反序列化期间发现一个 readResolve 方法,那么将调用该方法,将替代对象提供给调用者。
打包和解包代理
writeReplace 和 readResolve 方法使 Person 类可以将它的所有数据(或其中的核心数据)打包到一个 PersonProxy 中,将它放入到一个流中,然后在反序列化时再进行解包。
5. 信任,但要验证
认为序列化流中的数据总是与最初写到流中的数据一致,这没有问题。但是,正如一位美国前总统所说的,“信任,但要验证”。
对于序列化的对象,这意味着验证字段,以确保在反序列化之后它们仍具有正确的值,“以防万一”。为此,可以实现ObjectInputValidation 接口,并覆盖 validateObject() 方法。如果调用该方法时发现某处有错误,则抛出一个InvalidObjectException。
结束语
Java 对象序列化比大多数 Java 开发人员想象的更灵活,这使我们有更多的机会解决棘手的情况。
幸运的是,像这样的编程妙招在 JVM 中随处可见。关键是要知道它们,在遇到难题的时候能用上它们。
关于 Java Collections API 的 5 件事
Collections 比数组好
刚接触 Java 技术的开发人员可能不知道,Java 语言最初包括数组,是为了应对上世纪 90 年代初期 C++ 开发人员对于性能方面的批评。从那时到现在,我们已经走过一段很长的路,如今,与 Java Collections 库相比,数组不再有性能优势。
例如,若要将数组的内容转储到一个字符串,需要迭代整个数组,然后将内容连接成一个 String;而 Collections 的实现都有一个可用的 toString() 实现。
除少数情况外,好的做法是尽快将遇到的任何数组转换成集合。
2. 迭代的效率较低
将一个集合(特别是由数组转化而成的集合)的内容转移到另一个集合,或者从一个较大对象集合中移除一个较小对象集合,这些事情并不鲜见。
您也许很想对集合进行迭代,然后添加元素或移除找到的元素,但是不要这样做。
在此情况下,迭代有很大的缺点:
每次添加或移除元素后重新调整集合将非常低效。
每次在获取锁、执行操作和释放锁的过程中,都存在潜在的并发困境。
当添加或移除元素时,存取集合的其他线程会引起竞争条件。
可以通过使用 addAll 或 removeAll,传入包含要对其添加或移除元素的集合作为参数,来避免所有这些问题。
3. 用 for 循环遍历任何 Iterable
Java 5 中加入 Java 语言的最大的便利功能之一,增强的 for 循环,消除了使用 Java 集合的最后一道障碍。
以前,开发人员必须手动获得一个 Iterator,使用 next() 获得 Iterator 指向的对象,并通过 hasNext() 检查是否还有更多可用对象。从 Java 5 开始,我们可以随意使用 for 循环的变种,它可以在幕后处理上述所有工作。
实际上,这个增强适用于实现 Iterable 接口的任何对象,而不仅仅是 Collections。
4. 经典算法和定制算法
您是否曾想过以倒序遍历一个 Collection?对于这种情况,使用经典的 Java Collections 算法非常方便。
在上面的 清单 2 中,Person 的孩子是按照传入的顺序排列的;但是,现在要以相反的顺序列出他们。虽然可以编写另一个 for 循环,按相反顺序将每个对象插入到一个新的 ArrayList 中,但是 3、4 次重复这样做之后,就会觉得很麻烦。
Collections 类有很多这样的 “算法”,它们被实现为静态方法,以 Collections 作为参数,提供独立于实现的针对整个集合的行为。 而且,由于很棒的 API 设计,我们不必完全受限于 Collections 类中提供的算法 — 例如,我喜欢不直接修改(传入的 Collection 的)内容的方法。所以,可以编写定制算法是一件很棒的事情.
5. 扩展 Collections API
以上定制算法阐释了关于 Java Collections API 的一个最终观点:它总是适合加以扩展和修改,以满足开发人员的特定目的。
例如,假设您需要 Person 类中的孩子总是按年龄排序。虽然可以编写代码一遍又一遍地对孩子排序(也许是使用 Collections.sort方法),但是通过一个 Collection 类来自动排序要好得多。
实际上,您甚至可能不关心是否每次按固定的顺序将对象插入到 Collection 中(这正是 List 的基本原理)。您可能只是想让它们按一定的顺序排列。
java.util 中没有 Collection 类能满足这些需求,但是编写一个这样的类很简单。只需创建一个接口,用它描述 Collection 应该提供的抽象行为。对于 SortedCollection,它的作用完全是行为方面的。
关于 Java Collections API 的 5 件事,第 2 部分
1. List 不同于数组
Java 开发人员常常错误地认为 ArrayList 就是 Java 数组的替代品。Collections 由数组支持,在集合内随机查找内容时性能较好。与数组一样,集合使用整序数获取特定项。但集合不是数组的简单替代。
要明白数组与集合的区别需要弄清楚顺序 和位置 的不同。当第三个元素从上面的 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 中盲目地处理.
4. 注意可变的 hashCode()
Map 是很好的集合,为我们带来了在其他语言(比如 Perl)中经常可见的好用的键/值对集合。JDK 以 HashMap 的形式为我们提供了方便的 Map 实现,它在内部使用哈希表实现了对键的对应值的快速查找。但是这里也有一个小问题:支持哈希码的键依赖于可变字段的内容,这样容易产生 bug,即使最耐心的 Java 开发人员也会被这些 bug 逼疯。
5. equals() 与 Comparable
在浏览 Javadoc 时,Java 开发人员常常会遇到 SortedSet 类型(它在 JDK 中唯一的实现是 TreeSet)。因为 SortedSet 是java.util 包中唯一提供某种排序行为的 Collection,所以开发人员通常直接使用它而不会仔细地研究它。
使用上述代码一段时间后,可能会发现这个 Set 的核心特性之一:它不允许重复。该特性在 Set Javadoc 中进行了介绍。Set 是不包含重复元素的集合。更准确地说,set 不包含成对的 e1 和 e2 元素,因此如果 e1.equals(e2),那么最多包含一个 null 元素。 与 set 的有状态本质相反,TreeSet 要求对象直接实现 Comparable 或者在构造时传入 Comparator,它不使用 equals() 比较对象;它使用 Comparator/Comparable 的 compare 或 compareTo 方法。
因此存储在 Set 中的对象有两种方式确定相等性:大家常用的 equals() 方法和 Comparable/Comparator 方法,采用哪种方法取决于上下文。
更糟的是,简单的声明两者相等还不够,因为以排序为目的的比较不同于以相等性为目的的比较:可以想象一下按姓排序时两个Person 相等,但是其内容却并不相同。
一定要明白 equals() 和 Comparable.compareTo() 两者之间的不同 — 实现 Set 时会返回 0。甚至在文档中也要明确两者的区别。
关于 java.util.concurrent 您不知道的 5 件事,第 1 部分
事实上,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 教程中的代码有效,但是它耗时久,混乱,而且也并非完全直观。退回到 Java 平台较早的时候,没错,Java 开发人员不得不纠缠于这种代码;但现在是 2010 年 — 情况难道没有改善?
ArrayBlockingQueue 还体现了“公平” — 意思是它为读取器和编写器提供线程先入先出访问。这种替代方法是一个更有效,但又冒穷尽部分线程风险的政策。(即,允许一些读取器在其他读取器锁定时运行效率更高,但是您可能会有读取器线程的流持续不断的风险,导致编写器无法进行工作。)
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 使用的阻塞语义。
关于 java.util.concurrent 您不知道的 5 件事,第 2 部分
1. Semaphore
在一些企业系统中,开发人员经常需要限制未处理的特定资源请求(线程/操作)数量,事实上,限制有时候能够提高系统的吞吐量,因为它们减少了对特定资源的争用。尽管完全可以手动编写限制代码,但使用 Semaphore 类可以更轻松地完成此任务,它将帮您执行限制.
2. CountDownLatch
如果 Semaphore 是允许一次进入一个(这可能会勾起一些流行夜总会的保安的记忆)线程的并发性类,那么 CountDownLatch 就像是赛马场的起跑门栅。此类持有所有空闲线程,直到满足特定条件,这时它将会一次释放所有这些线程。
Executor
在一些 JVM 中,创建 Thread 是一项重量型的操作,重用现有 Thread 比创建新线程要容易得多。而在另一些 JVM 中,情况正好相反:Thread 是轻量型的,可以在需要时很容易地新建一个线程。当然,如果 Murphy 拥有自己的解决办法(他通常都会拥有),那么您无论使用哪种方法对于您最终将部署的平台都是不对的。
使用 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 可以轻松实现.
这项功能怎么样?不用过于担心线程,不用过于担心用户希望取消心跳时会发生什么,也不用明确地将线程标记为前台或后台;只需将所有的计划细节留给 ScheduledExecutorService。
顺便说一下,如果用户希望取消心跳,scheduleAtFixedRate 调用将返回一个 ScheduledFuture 实例,它不仅封装了结果(如果有),还拥有一个 cancel 方法来关闭计划的操作。
这项功能怎么样?不用过于担心线程,不用过于担心用户希望取消心跳时会发生什么,也不用明确地将线程标记为前台或后台;只需将所有的计划细节留给 ScheduledExecutorService。
顺便说一下,如果用户希望取消心跳,scheduleAtFixedRate 调用将返回一个 ScheduledFuture 实例,它不仅封装了结果(如果有),还拥有一个 cancel 方法来关闭计划的操作。
5. Timeout 方法
为阻塞操作设置一个具体的超时值(以避免死锁)的能力是 java.util.concurrent 库相比起早期并发特性的一大进步,比如监控锁定。
这些方法几乎总是包含一个 int/TimeUnit 对,指示这些方法应该等待多长时间才释放控制权并将其返回给程序。它需要开发人员执行更多工作 — 如果没有获取锁,您将如何重新获取? — 但结果几乎总是正确的:更少的死锁和更加适合生产的代码。
结束语
java.util.concurrent 包还包含了其他许多好用的实用程序,它们很好地扩展到了 Collections 之外,尤其是在 .locks 和 .atomic包中。深入研究,您还将发现一些有用的控制结构,比如 CyclicBarrier 等。
关于 Java 性能监控您不知道的 5 件事,第 1 部分
1. JDK 附带分析器
许多开发人员没有意识到从 Java 5 开始 JDK 中包含了一个分析器。JConsole(或者 Java 平台最新版本,VisualVM)是一个内置分析器,它同 Java 编译器一样容易启动。如果是从命令行启动,使 JDK 在 PATH 上,运行 jconsole 即可。如果从 GUI shell 启动,找到 JDK 安装路径,打开 bin 文件夹,双击 jconsole。
当分析工具弹出时(取决于正在运行的 Java 版本以及正在运行的 Java 程序数量),可能会出现一个对话框,要求输入一个进程的 URL 来连接,也可能列出许多不同的本地 Java 进程(有时包含 JConsole 进程本身)来连接。
使用 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。
跟踪统计
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 分析快照。
5. JConsole 并不是高深莫测的
作为一个分析器实用工具,JConsole 是极好的,但是还有更好的工具。一些分析插件附带分析器或者灵巧的用户界面,默认情况下比 JConsole 跟踪更多的数据。
JConsole 真正吸引人的是整个程序是用 “普通旧式 Java ” 编写的,这意味着任何 Java 开发人员都可以编写这样一个实用工具。事实上,JDK 其中甚至包括如何通过创建一个插件来定制 JConsole 的示例(参见 参考资料)。建立在 NetBeans 顶部的 VisualVM 进一步延伸了插件概念。
如果 JConsole(或者 VisualVM,或者其他任何工具)不符合您的需求,或者不能跟踪您想要跟踪的,或者不能按照您的方式跟踪,您可以编写属于自己的工具。如果您觉得 Java 代码很麻烦,Groovy 或 JRuby 或很多其他 JVM 语言都可以帮助您更快完成。
您真正需要的是一个快速而粗糙(quick-and-dirty)的由 JVM 连接的命令行工具,可以以您想要的方式确切地跟踪您感兴趣的数据。
关于 Java 性能监控您不知道的 5 件事,第 2 部分
1. jps (sun.tools.jps)
很多命令行工具都要求您识别您希望监控的 Java 进程。这与监控本地操作系统进程、同样需要一个程序识别器的同类工具没有太大区别。
“VMID” 识别器与本地操作系统进程识别器(“pid”)并不总是相同的,这就是我们需要 JDK jps 实用程序的原因。
jps — 名称反映了在大多数 UNIX 系统上发现的 ps 实用程序 — 告诉我们运行 Java 应用程序的 JVMID。顾名思义,jps 返回指定机器上运行的所有已发现的 Java 进程的 VMID。如果 jps 没有发现进程,并不意味着无法附加或研究 Java 进程,而只是意味着它并未宣传自己的可用性。
如果发现 Java 进程,jps 将列出启用它的命令行。这种区分 Java 进程的方法非常重要,因为只要涉及操作系统,所有的 Java 进程都被统称为 “java”。在大多数情况下,VMID 是值得注意的重要数字。
2. jstat (sun.tools.jstat)
jstat 实用程序可以用于收集各种各样不同的统计数据。jstat 统计数据被分类到 “选项” 中,这些选项在命令行中被指定作为第一参数。对于 JDK 1.6 来说,您可以通过采用命令 -options 运行 jstat 查看可用的选项清单。
默认情况下,jstat 在您核对信息时显示信息。如果您希望每隔一定时间拍摄快照,请在 -options 指令后以毫秒为单位指定间隔时间。jstat 将持续显示监控进程信息的快照。如果您希望 jstat 在终止前进行特定数量的快照,在间隔时间/时间值后指定该数字。
3. jstack (sun.tools.jstack)
了解 Java 进程及其对应的执行线程内部发生的情况是一种常见的诊断挑战。例如,当一个应用程序突然停止进程时,很明显出现了资源耗尽,但是仅通过查看代码无法明确知道何处出现资源耗尽,且为什么会发生。
jstack 是一个可以返回在应用程序上运行的各种各样线程的一个完整转储的实用程序,您可以使用它查明问题。
采用期望进程的 VMID 运行 jstack 会产生一个堆转储。就这一点而言,jstack 与在控制台窗口内按 Ctrl-Break 键起同样的作用,在控制台窗口中,Java 进程正在运行或调用 VM 内每个 Thread 对象上的 Thread.getAllStackTraces() 或Thread.dumpStack()。jstack 调用也转储关于在 VM 内运行的非 Java 线程的信息,这些线程作为 Thread 对象并不总是可用的。
jstack 的 -l 参数提供了一个较长的转储,包括关于每个 Java 线程持有锁的更多详细信息,因此发现(和 squash)死锁或可伸缩性 bug 是极其重要的。
4. jmap (sun.tools.jmap)
有时,您正在处理的问题是一个对象泄露,如一个 ArrayList (可能持有成千上万个对象)该释放时没有释放。另一个更普遍的问题是,看似从不会压缩的扩展堆,却有活跃的垃圾收集。
当您努力寻找一个对象泄露时,在指定时刻对堆及时进行拍照,然后审查其中内容非常有用。jmap 通过对堆拍摄快照来提供该功能的第一部分。然后您可以采用下一部分中描述的 jhat 实用程序分析堆数据。
与这里描述的其他所有实用程序一样,使用 jmap 非常简单。将 jmap 指向您希望拍快照的 Java 进程的 VMID,然后给予它部分参数,用来描述产生的结果文件。您要传递给 jmap 的选项包括转储文件的名称以及是否使用一个文本文件或二进制文件。二进制文件是最有用的选项,但是只有当与某一种索引工具 结合使用时 — 通过十六进制值的文本手动操作数百兆字节不是最好的方法。
随意看一下 Java 堆的更多信息,jmap 同样支持 -histo 选项。-histo 产生一个对象文本柱状图,现在在堆中大量引用,由特定类型消耗的字节总数分类。它同样给出了特定类型的总示例数量,支持部分原始计算,并猜测每个实例的相对成本。
不幸的是,jmap 没有像 jstat 一样的 period-and-max-count 选项,但是将 jmap(或 jmap.main())调用放入 shell 脚本或其他类的循环,周期性地拍摄快照相对简单。(事实上,这是加入 jmap 的一个好的扩展,不管是作为 OpenJDK 本身的源补丁,还是作为其他实用程序的扩展。)
5. jhat (com.sun.tools.hat.Main)
将堆转储至一个二进制文件后,您就可以使用 jhat 分析二进制堆转储文件。jhat 创建一个 HTTP/HTML 服务器,该服务器可以在浏览器中被浏览,提供一个关于堆的 object-by-object 视图,及时冻结。根据对象引用草率处理堆可能会非常可笑,您可以通过对总体混乱进行某种自动分析而获得更好的服务。幸运的是,jhat 支持 OQL 语法进行这样的分析。
结束语
当您需要近距离观察 Java 进程内发生的事情时,JDK 的分析扩展会非常有用。本文中介绍的所有工具都可以从命令行中由其自己使用。它们还可以与 JConsole 或 VisualVM 有力地结合使用。JConsole 和 VisualVM 提供 Java 虚拟机的总体视图,jstat 和 jmap 等有针对性的工具支持您对研究进行微调。
关于 Java Scripting API
Java Scripting API 是从 Java 6 开始引入的,它填补了便捷的小脚本语言和健壮的 Java 生态系统之间的鸿沟。通过使用 Java Scripting API,您就可以在您的 Java 代码中快速整合几乎所有的脚本语言,这使您能够在解决一些很小的问题时有更多可选择的方法。
1. 使用 jrunscript 执行 JavaScript
每一个新的 Java 平台发布都会带来新的命令行工具集,它们位于 JDK 的 bin 目录。Java 6 也一样,其中 jrunscript 便是 Java 平台工具集中的一个不小的补充。
2. 从脚本访问 Java 对象
能够编写 JavaScript/ECMAScript 代码是非常好的,但是我们不希望被迫重新编译我们在 Java 语言中使用的所有代码 — 这是违背我们初衷的。幸好,所有使用 Java Scripting API 引擎的代码都完全能够访问整个 Java 生态系统,因为本质上一切代码都还是 Java 字节码。所以,回到我们之前的问题,我们可以在 Java 平台上使用传统的 Runtime.exec() 调用来启动进程
3. 从 Java 代码调用脚本
从脚本调用 Java 对象仅仅完成了一半的工作:Java 脚本环境也提供了从 Java 代码调用脚本的功能。这只需要实例化一个ScriptEngine 对象,然后加载和评估脚本
4. 将 Java 对象绑定到脚本空间
仅仅调用一个脚本还不够:脚本通常会与 Java 环境中创建的对象进行交互。这时,Java 主机环境必须创建一些对象并将它们绑定,这样脚本就可以很容易找到和使用这些对象。这个过程是 ScriptContext 对象的任务
5. 编译频繁使用的脚本
脚本语言的缺点一直存在于性能方面。其中的原因是,大多数情况下脚本语言是 “即时” 解译的,因而它在执行时会损失一些解析和验证文本的时间和 CPU 周期。运行在 JVM 的许多脚本语言最终会将接收的代码转换为 Java 字节码,至少在脚本被第一次解析和验证时进行转换;在 Java 程序关闭时,这些即时编译的代码会消失。将频繁使用的脚本保持为字节码形式可以帮助提升可观的性能。
我们可以以一种很自然和有意义的方法使用 Java Scripting API。如果返回的 ScriptEngine 实现了 Compilable 接口,那么这个接口所编译的方法可用于将脚本(以一个 String 或一个 Reader 传递过来的)编译为一个 CompiledScript 实例,然后它可用于在 eval() 方法中使用不同的绑定重复地处理编译后的代码
在大多数情况中,CompiledScript 实例需要存储在一个长时间存储中(例如,servlet-context),这样才能避免一次次地重复编译相同的脚本。然而,如果脚本发生变化,您就需要创建一个新的 CompiledScript 来反映这个变化;一旦编译完成,CompiledScript就不再执行原始的脚本文件内容。