Java 8:StampedLock、ReadWriteLock以及synchronized的比较

原文链接     作者:Tal Weiss   译者:iDestiny    校对:郭蕾

同步区有点像拜访你的公公婆婆。你当然是希望待的时间越短越好。说到锁的话情况也是一样的,你希望获取锁以及进入临界区域的时间越短越好,这样才不会造成瓶颈。

对于方法和代码块,语言层面的加锁机制是synchronized关键字,该关键字是由HotSpot虚拟机内置的。我们在代码中分配的每一个对象,如String、Array或者一个完整的JSON文档,在本地垃圾回收级别都具有内置的加锁能力。JIT编译器也是类似的,它在进行字节码的编译和反编译的时候,都取决于特定的某个锁的具体的状态和竞争级别。

同步块的关键是:进入临界区域内的线程不能超过一个 。这一点对于生产者消费者场景中来说非常糟糕,当一些线程独占地修改某些数据时,而另外一些线程只是希望读取数据,这个是可以和别的线程同时进行的。

读写锁(ReadWriteLock)是这种情况最好的解决方案。你指定哪些线程可以阻塞其他线程(写线程),哪些线程可以与其他线程共享数据(读线程)。这是一个完美的解决方案?恐怕不是。

读写锁不像同步块,它不是JVM内建的,它只不过是段普通的代码。为了实现加锁的语义,它得命令CPU原子地或者以特定的顺序执行操作,以避免竞态条件。这通常都是通过JVM预留的一个后门来实现的——Unsafe类。读写锁使用比较并交换(CAS)操作直接将值设置到内存中去,这是它们线程排队算法中的一部分。

即便如此,读写锁还是不够快,并且有时候慢得要死,慢到你觉得就不应该使用它。然而JDK的伙计们并没有放弃读写锁,现在他们带来了一个全新的StampedLock。StampedLock使用了一组新的算法以及Java 8 JDK中引入的内存屏障的特性,这使得这个锁更高效也更健壮。

它兑现了自己的诺言了吗?让我们拭目以待。

使用锁。从表面上看StampedLock使用起来更复杂。它们使用了一个票据(stamp)的概念,这是一个long值,在加锁和解锁操作时,它被用作一张门票。这意味着要解锁一个操作你需要传递相应的的门票。如果传递错误的门票,那么可能会抛出一个异常,或者其他意想不到的错误。

另外一个值得关注的重要问题是,不像ReadWriteLock,StampedLocks是不可重入的。因此尽管StampedLocks可能更快,但可能产生死锁。在实践中,这意味着你应该始终确保锁以及对应的门票不要逃逸出所在的代码块。

1 long stamp = lock.writeLock(); //blocking lock, returns a stamp
2 try {
3     write(stamp); // this is a bad move, you’re letting the stamp escape
4 } finally {
5     lock.unlock(stamp);// release the lock in the same block - way better
6 }

这个设计还有个让人无法忍受的地方就是这个long类型的票据对你而言没有任何意义。我希望锁操作返回的是一个描述票据的对象——包括它的类型(读/写)、加锁时间、所有者线程等等。这样处理的话更容易调试和跟踪日志。不过这么做很有可能是故意的,以便阻止开发人员不要将这个戳在代码里传来传去,同时也减少了分配对象的开销。

乐观锁。StampedLocks最重要的一个新功能就是它的乐观锁模式。研究和实践经验表明,读操作是在大多数情况下不会与写操作竞争。因此,获取全占的读锁可能就如杀鸡用牛刀了。一个更好的方法可能是继续执行读,并且结束后同时判断该值是否被修改,如果被修改,你再进行重试,或者升级成一个更重的锁。

01 long stamp = lock.tryOptimisticRead(); // non blocking
02 read();
03 if(!lock.validate(stamp)){ // if a write occurred, try again with a read lock
04     long stamp = lock.readLock();
05     try {
06        read();
07     } finally {
08        lock.unlock(stamp);
09     }
10 }

选择一个锁,最大的难点之一是其在生产环境中的表现会因应用状态的不同而有所差异。也就是说你不能凭空选择使用何种锁,而是得将代码执行的具体环境也考虑进来 。

并发读写线程的数量将决定你应该使用哪一种锁——同步块或者读写锁。如果这些线程数在JVM的执行生命周期内发生改变的话,这个问题就更棘手了,这取决于应用的状态以及线程的竞争级别。

为了解释,我对四种模式下的锁分别进行了压力测试——在不同竞争级别和读写线程组合下的synchronized、读写锁、StampedLock的读写锁以及读写乐观锁。读线程将读取一个计数器的值,写线程会将它从0增加到1M。

5个读线程和5个写线程:5个读写线程分别在并发地执行,我们发现StampedLock表现得最好,比synchronized性能高3倍多。读写锁也表现得不错。这里奇怪的是乐观锁,表面上看它该是最快的,实际却是最慢的。

10个读线程和10个写线程:接下来,我增加竞争级别提高到10个读线程和10个写线程。现在情况开始发生了变化。在同级别执行下,读写锁现在要比StampedLock和synchronized慢一个数量级。请注意,乐观锁令人惊讶的仍然比StampedLock的读写锁慢。

16个读线程和4个写线程:接下来,我保持同样的竞争级别,不过将读写线程的比重调整了下:16个读线程和4个写线程。读写锁再说次说明了为什么它要被替换掉了——它慢了百倍以上。Stamped以及乐观锁都表现得不错,synchronized也紧随其后。

19个读线程和1个写线程:最后,我看看19个读线程和1个写线程会怎样。注意到结果慢得多了,因为单线程需要更长的时间来完成计数增加操作。在这里我们得到了一些非常有趣的结果。读写锁需要太多时间来完成。尽管Stamped锁没有表现得很好…乐观锁明显是这里的赢家,打败了读写锁100倍。即使如此,记住这种锁定模式可能会失败,因为这段时间内可能会出现一个写线程。Synchronized, 我们的老朋友,继续表现出可靠的结果。

 

完整的结果可以在这里找到。硬件:MBP, Core i7。

基础测试代码可以在这里下载。

总结

总体看来, 整体性能表现最好的仍然是内置的同步锁。但是,这里并不是说内置的同步锁会在所有的情况下都执行得最好。这里主要想表达的是在你将你的代码投入生产之前,应该基于预期的竞争级别和读写线程之间的分配进行测试,再选择适当一个适当的锁。否则你会面临线上故障的风险。

其他的关于StampedLocks的资料请点击这里

时间: 2024-11-05 18:47:32

Java 8:StampedLock、ReadWriteLock以及synchronized的比较的相关文章

java同步关键词解释、synchronized、线程锁(Lock)

1.java同步关键词解释 21.1 synchronized synchronized是用来实现线程同步的!!!                      加同步格式:                    synchronized( 需要一个任意的对象(锁) ){                             代码块中放操作共享数据的代码.                    }          见代码MySynchronized package thread1;   publ

Java中的ReentrantLock和synchronized两种锁机制的对比

原文:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html 多线程和并发性并不是什么新内容,但是 Java 语言设计中的创新之一就是,它是第一个直接把跨平台线程模型和正规的内存模型集成到语言中的主流语言.核心类库包含一个 Thread 类,可以用它来构建.启动和操纵线程,Java 语言包括了跨线程传达并发性约束的构造 -- synchronized 和 volatile.在简化与平台无关的并发类的开发的同时,它决没有使并发

Java同步机制浅谈――synchronized对代码作何影响?

Java对多线程的支持与同步机制深受大家的喜爱,似乎看起来使用了synchronized关键字就可以轻松地解决多线程共享数据同步问题.到底如何?――还得对synchronized关键字的作用进行深入了解才可定论.总的说来,synchronized关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块.如果再细的分类,synchronized可作用于instance变量.object reference(对象引用).static函数和class literals(类名

聊聊并发(二)Java SE1.6中的Synchronized

本文属作者原创,原文发表于InfoQ:http://www.infoq.com/cn/articles/java-se-16-synchronized 1 引言 在多线程并发编程中Synchronized一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6对Synchronized进行了各种优化之后,有些情况下它并不那么重了,本文详细介绍了Java SE1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程. 2 术语定义 术语 英

Java多线程的例子及synchronized关键字锁定对象的用法

该例子所应用场景:一个线程负责生产,多个线程(该例为3个)负责消费:生产者不断的往堆栈中加入数据,消费者不断的从堆栈中取数据. 代码如下: package com.xs.mail.thread; import java.util.ArrayList; import java.util.List; class Widget { } class WidgetMaker extends Thread { List<Widget> finishedWidgets = new ArrayList<

java多线程编程之使用Synchronized关键字同步类方法_java

复制代码 代码如下: public synchronized void run(){     } 从上面的代码可以看出,只要在void和public之间加上synchronized关键字,就可以使run方法同步,也就是说,对于同一个Java类的对象实例,run方法同时只能被一个线程调用,并当前的run执行完后,才能被其他的线程调用.即使当前线程执行到了run方法中的yield方法,也只是暂停了一下.由于其他线程无法执行run方法,因此,最终还是会由当前的线程来继续执行.先看看下面的代码:sych

java多线程编程之使用Synchronized块同步变量_java

下面的代码演示了如何同步特定的类方法: 复制代码 代码如下: package mythread; public class SyncThread extends Thread{ private static String sync = ""; private String methodType = "";  private static void method(String s) {  synchronized (sync)  {sync = s;System.ou

java多线程之:Java中的ReentrantLock和synchronized两种锁定机制的对比 (转载)

原文:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html 多线程和并发性并不是什么新内容,但是 Java 语言设计中的创新之一就是,它是第一个直接把跨平台线程模型和正规的内存模型集成到语言中的主流语言.核心类库包含一个 Thread 类,可以用它来构建.启动和操纵线程,Java 语言包括了跨线程传达并发性约束的构造 -- synchronized 和 volatile .在简化与平台无关的并发类的开发的同时,它决没有使并

java多线程编程之使用Synchronized块同步方法_java

synchronized关键字有两种用法.第一种就是在<使用Synchronized关键字同步类方法>一文中所介绍的直接用在方法的定义中.另外一种就是synchronized块.我们不仅可以通过synchronized块来同步一个对象变量.也可以使用synchronized块来同步类中的静态方法和非静态方法.synchronized块的语法如下: 复制代码 代码如下: public void method(){    - -    synchronized(表达式)    {        -