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