时间戳锁
一直以来,多线程代码是服务器开发人员的毒药(问问 Oracle 的 Java 语言架构师和并行开发大师 Brian Goetz)。Java 的核心库不断加入各种复杂的用法来减少访问共享资源时的线程等待时间。其中之一就是经典的读写锁(ReadWriteLock),它让你把代码分成两部分:需要互斥的写操作和不需要互斥的读操作。
表面上看起来很不错。问题是读写锁有可能是极慢的(最多 10 倍),这已经和它的初衷相悖了。Java 8 引入了一种新的读写锁——叫做时间戳锁。好消息是这个家伙真的非常快。坏消息是它使用起来更复杂,有更多的状态需要处理。并且它是不可重入的,这意味着一个线程有可能跟自己死锁。
时间戳锁有一种“乐观”模式,在这种模式下每次加锁操作都会返回一个时间戳作为某种权限凭证;每次解锁操作都需要提供它对应的时间戳。如果一个线程在请求一个写操作锁的时候,这个锁碰巧已经被一个读操作持有,那么这个读操作的解锁将会失效(因为时间戳已经失效)。这个时候应用程序需要从头再来,也许要使用悲观模式的锁(时间戳锁也有实现)。你需要自己搞定这一切,并且一个时间戳只能解锁它对应的锁——这一点必须非常小心。
下面我们来看一下这种锁的实例——
long stamp = lock.tryOptimisticRead(); // 非阻塞路径——超级快 work(); // 我们希望不要有写操作在这时发生 if (lock.validate(stamp)){ // 成功!没有写操作干扰 } else { // 肯定同时有另外一个线程获得了写操作锁,改变了时间戳 // 懒汉说——我们切换到开销更大的锁吧 stamp = lock.readLock(); // 这是传统的读操作锁,会阻塞 try { // 现在不可能有写操作发生了 work(); } finally { lock.unlock(stamp); // 使用对应的时间戳解锁 } }
并发加法器
Java 8 另一个出色的功能是并发“加法器”,它对大规模运行的代码尤其有意义。一种最基本的并发模式就是对一个计数器的读写。就其本身而言,现今处理这个问题有很多方法,但是没有一种能比 Java 8 提供的方法高效或优雅。
到目前为止,这个问题是用原子类(Atomics)来解决的,它直接利用了 CPU 的“比较并交换”指令(CAS)来测试并设置计数器的值。问题在于当一条 CAS 指令因为竞争而失败的时候,AtomicInteger 类会死等,在无限循环中不断尝试 CAS 指令,直到成功为止。在发生竞争概率很高的环境中,这种实现被证明是非常慢的。
来看 Java 8 的 LongAdder。这一系列类为大量并行读写数值的代码提供了方便的解决办法。使用超级简单。只要初始化一个 LongAdder 对象并使用它的 add() 和 intValue() 方法来累加和采样计数器。
这和旧的 Atomic 类的区别在于,当 CAS 指令因为竞争而失败时,Adder 不会一直占着 CPU,而是为当前线程分配一个内部 cell 对象来存储计数器的增量。然后这个值和其他待处理的 cell 对象一起被加到 intValue() 的结果上。这减少了反复使用 CAS 指令或阻塞其他线程的可能性。
如果你问你自己,什么时候应该用并发加法器而不是原子类来管理计数器?简单的答案就是——一直这么做。
并行排序
正像并发加法器能加速计数一样,Java 8 还实现了一种简洁的方法来加速排序。这个秘诀很简单。你不再这么做:
Array.sort(myArray);
而是这么做:
Arrays.parallelSort(myArray);
这会自动把目标数组分割成几个部分,这些部分会被放到独立的 CPU 核上去运行,再把结果合并起来。这里唯一需要注意的是,在一个大量使用多线程的环境中,比如一个繁忙的 Web 容器,这种方法的好处就会减弱(降低 90% 以上),因为越来越多的 CPU 上下文切换增加了开销。
切换到新的日期接口
Java 8 引入了全新的 date-time 接口。当前接口的大多数方法都已被标记为 deprecated,你就知道是时候推出新接口了。新的日期接口为 Java 核心库带来了易用性和准确性,而以前只能用 Joda time 才能达到这样的效果(译者注:Joda time 是一个第三方的日期库,比 Java 自带的库更友好更易于管理)。
跟任何新接口一样,好消息是接口变得更优雅更强大。但不幸的是还有大量的代码在使用旧接口,这个短时间内不会有改变。
为了衔接新旧接口,历史悠久的 Date 类新增了 toInstant() 方法,用于把 Date 转换成新的表示形式。当你既要享受新接口带来的好处,又要兼顾那些只接受旧的日期表示形式的接口时,这个方法会显得尤其高效。
控制操作系统进程
想在你的代码里启动一个操作系统进程,通过 JNI 调用就能完成——但这个东西总令人一知半解,你很有可能得到一个意想不到的结果,并且一路伴随着一些很糟糕的异常。
即便如此,这是无法避免的事情。但进程还有一个讨厌的特性就是——它们搞不好就会变成僵尸进程。目前从 Java 中运行进程带来的问题是,进程一旦启动就很难去控制它。
为了帮我们解决这个问题,Java 8 在 Process 类中引入了三个新的方法
- destroyForcibly——结束一个进程,成功率比以前高很多。
- isAlive——查询你启动的进程是否还活着。
- 重载了 waitFor(),你现在可以指定等待进程结束的时间了。进程成功退出后这个接口会返回,超时的话也会返回,因为你有可能要手动终止它。
这里有两个关于如何使用这些新方法的好例子——
- 如果进程没有在规定时间内退出,终止它并继续往前走。
if (process.wait(MY_TIMEOUT, TimeUnit.MILLISECONDS)){ // 成功 } else { process.destroyForcibly();<br></br>}
- 在你的代码结束前,确保所有的进程都已退出。僵尸进程会逐渐耗尽系统资源。
for (Process p : processes) { if (p.isAlive()) { p.destroyForcibly(); } }
精确的数字运算
数字溢出会导致一些讨厌的 bug,因为它本质上不会出错。在一些系统中,整型值不停地增长(比如计数器),溢出的问题就尤为严重。在这些案例里面,产品在演进阶段运行得很好,甚至商用后的很长时间内也没问题,但最终会出奇怪的故障,因为运算开始溢出,产生了完全无法预料的值。
为了解决这个问题,Java 8 为 Math 类添加了几个新的“精确型”方法,以便保护重要的代码不受溢出的影响,它的做法是当运算超过它的精度范围的时候,抛出一个未检查的 ArithmeticException 异常。
int safeC = Math.multiplyExact(bigA, bigB); // 如果结果超出 +-2^31,就会抛出 ArithmeticException 异常
唯一不好的地方就是你必须自己找出可能产生溢出的代码。无论如何,没有什么自动的解决方案。但我觉得有这些接口总比没有好。
安全的随机数发生器
在过去几年中 Java 一直因为安全漏洞而饱受诟病。无论是否合理,Java 已经做了大量工作来加强虚拟机和框架层,使之免受攻击。如果随机数来源于随机性不高的种子,那么那些用随机数来产生密钥或者散列敏感信息的系统就更易受攻击。
到目前为止,随机数发生算法由开发人员来决定。但问题是,如果你想要的算法依赖于特定的硬件、操作系统、虚拟机,那你就不一定能实现它。这种情况下,应用程序倾向于使用更弱的默认发生器,这就使他们暴露在更大的风险下了。
Java 8 添加了一个新的方法叫 SecureRandom.getInstanceStrong(),它的目标是让虚拟机为你选择一个安全的随机数发生器。如果你的代码无法完全掌控操作系统、硬件、虚拟机(如果你的程序部署到云或者 PaaS 上,这是很常见的),我建议你认真考虑一下使用这个接口。
可选引用
空指针就像“踢到脚趾”一样——从你学会走路开始就伴随着你,无论现在你有多聪明——你还是会犯这个错。为了帮助解决这个老问题,Java 8 引入了一个新模板叫 Optional
这个模板是从 Scala 和 Hashkell 借鉴来的,用于明确声明传给函数或函数返回的引用有可能是空的。有了它,过度依赖旧文档或者看过的代码经常变动的人,就不需要去猜测某个引用是否可能为空。
Optional<User> tryFindUser(int userID) {
或
void processUser(User user, Optional<Cart> shoppingCart) {
Optional 模板有一套函数,使得采样它更方便,比如 isPresent() 用来检查这个值是不是非空,或者 ifPresent() 你可以传递一个 Lambda 函数过去,如果 isPresent() 返回 true,这个 Lambda 函数就会被执行。不好的地方就跟 Java 8 的新日期接口一样,等这种模式逐渐流行,渗透到我们使用的库中和日常设计中,需要时间和工作量。
用新的 Lambda 语法打印 Optional 值:
value.ifPresent(System.out::print);
关于作者
Tal Weiss 是 Takipi 的 CEO。过去十五年中,Tal 一直在设计大规模的实时的 Java 和 C++ 应用。可是他仍然陶醉于分析有挑战性的 bug,以及评估 Java 代码的性能。业余时间他喜欢爵士鼓。
查看英文原文: 8 Great Java 8 Features No One’s Talking about
感谢崔康对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论