ReentrantLock 与Synchronized 的区别

主要相同点:Lock能完成synchronized所实现的全部功能
主要不一样点:Lock有比synchronized更精确的线程语义和更好的性能。synchronized会自动释放锁,而Lock必定
要求程序员手工释放,而且必须在finally从句中释放。
java

 

详细内容以下:程序员

JDK 5.0为开发人员开发高性能的并发应用程序提供了一些颇有效的新选择。例如,java.util.concurrent.lock 中的类 ReentrantLock 被做为Java 语言中synchronized 功能的替代,它具备相同的内存语义、相同的锁定,但在争用条件下却有更好的性能,此外,它还有synchronized 没有提供的其余特性。这是否意味着咱们应当忘记synchronized ,转而只用 ReentrantLock 呢?并发性专家 Brian Goetz 刚从他的夏季休假中返回,他将为咱们提供答案。
多线程和并发性并非什么新内容,可是Java 语言设计中的创新之一就是,它是第一个直接把跨平台线程模型和正规的内存模型集成到语言中的主流语言。核心类库包含一个Thread 类,能够用它来构建、启动和操纵线程,Java 语言包括了跨线程传达并发性约束的构造 —— synchronized 和 volatile 。在简化与平台无关的并发类的开发的同时,它决没有使并发类的编写工做变得更繁琐,只是使它变得更容易了。
synchronized 快速回顾
把代码块声明为 synchronized,有两个重要后果,一般是指该代码具备 原子性(atomicity)和可见性(visibility)。原子性意味着一个线程一次只能执行由一个指定监控对象(lock)保护的代码,从而防止多个线程在更新共享状态时相互冲突。可见性则更为微妙;它要对付内存缓存和编译器优化的各类反常行为。通常来讲,线程以某种没必要让其余线程当即能够看到的方式(无论这些线程在寄存器中、在处理器特定的缓存中,仍是经过指令重排或者其余编译器优化),不受缓存变量值的约束,可是若是开发人员使用了同步,以下面的代码所示,那么运行库将确保某一线程对变量所作的更新先于对现有 synchronized 块所进行的更新,当进入由同一监控器(lock)保护的另外一个 synchronized 块时,将马上能够看到这些对变量所作的更新。相似的规则也存在于 volatile 变量上。
synchronized (lockObject) {
// update object state
}
因此,实现同步操做须要考虑安全更新多个共享变量所需的一切,不能有争用条件,不能破坏数据(假设同步的边界位置正确),并且要保证正确同步的其余线程能够看到这些变量的最新值。经过定义一个清晰的、跨平台的内存模型(该模型在 JDK 5.0 中作了修改,改正了原来定义中的某些错误),经过遵照下面这个简单规则,构建“一次编写,随处运行”的并发类是有可能的:
不论何时,只要您将编写的变量接下来可能被另外一个线程读取,或者您将读取的变量最后是被另外一个线程写入的,那么您必须进行同步。
不过如今好了一点,在最近的 JVM 中,没有争用的同步(一个线程拥有锁的时候,没有其余线程企图得到锁)的性能成本仍是很低的。(也不老是这样;早期 JVM 中的同步尚未优化,因此让不少人都这样认为,可是如今这变成了一种误解,人们认为无论是否是争用,同步都有很高的性能成本。)
对synchronized 的改进
如此看来同步至关好了,是么?那么为何 JSR 166 小组花了这么多时间来开发 java.util.concurrent.lock 框架呢?答案很简单-同步是不错,但它并不完美。它有一些功能性的限制 —— 它没法中断一个正在等候得到锁的线程,也没法经过投票获得锁,若是不想等下去,也就无法获得锁。同步还要求锁的释放只能在与得到锁所在的堆栈帧相同的堆栈帧中进行,多数状况下,这没问题(并且与异常处理交互得很好),可是,确实存在一些非块结构的锁定更合适的状况。
ReentrantLock 类
java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它容许把锁定的实现做为 Java 类,而不是做为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各类实现可能有不一样的调度算法、性能特性或者锁定语义。 ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,可是添加了相似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用状况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 能够花更少的时候来调度线程,把更多时间用在执行线程上。)
reentrant 锁意味着什么呢?简单来讲,它有一个与锁相关的获取计数器,若是拥有锁的某个线程再次获得锁,那么获取计数器就加1,而后锁须要被释放两次才能得到真正释放。这模仿了 synchronized 的语义;若是线程进入由线程已经拥有的监控器保护的 synchronized 块,就容许线程继续进行,当线程退出第二个(或者后续) synchronized 块的时候,不释放锁,只有线程退出它进入的监控器保护的第一个 synchronized 块时,才释放锁。
在查看清单 1 中的代码示例时,能够看到 Lock 和 synchronized 有一点明显的区别 —— lock 必须在 finally 块中释放。不然,若是受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么,可是实际上,它极为重要。忘记在 finally 块中释放锁,可能会在程序中留下一个定时炸弹,当有一天炸弹爆炸时,您要花费很大力气才有找到源头在哪。而使用同步,JVM 将确保锁会得到自动释放。
清单1:用 ReentrantLock 保护代码块
Lock lock = new ReentrantLock();
lock.lock();
try {
// update object state
}
finally {
lock.unlock();
}
除此以外,与目前的 synchronized 实现相比,争用下的 ReentrantLock 实现更具可伸缩性。(在将来的 JVM 版本中,synchronized 的争用性能颇有可能会得到提升。)这意味着当许多线程都在争用同一个锁时,使用 ReentrantLock 的整体开支一般要比 synchronized 少得多。
比较ReentrantLock和synchronized的可伸缩性
Tim Peierls 用一个简单的线性全等伪随机数生成器(PRNG)构建了一个简单的评测,用它来测量 synchronized 和 Lock 之间相对的可伸缩性。这个示例很好,由于每次调用 nextRandom() 时,PRNG 都确实在作一些工做,因此这个基准程序其实是在测量一个合理的、真实的 synchronized 和 Lock 应用程序,而不是测试纯粹纸上谈兵或者什么也不作的代码(就像许多所谓的基准程序同样。)
在这个基准程序中,有一个 PseudoRandom 的接口,它只有一个方法 nextRandom(int bound) 。该接口与 java.util.Random 类的功能很是相似。由于在生成下一个随机数时,PRNG 用最新生成的数字做为输入,并且把最后生成的数字做为一个实例变量来维护,其重点在于让更新这个状态的代码段不被其余线程抢占,因此我要用某种形式的锁定来确保这一点。( java.util.Random 类也能够作到这点。)咱们为 PseudoRandom 构建了两个实现;一个使用 syncronized,另外一个使用 java.util.concurrent.ReentrantLock 。驱动程序生成了大量线程,每一个线程都疯狂地争夺时间片,而后计算不一样版本每秒能执行多少轮。这个评测并不完美,并且只在两个系统上运行了(一个是双 Xeon 运行超线程 Linux,另外一个是单处理器 Windows 系统),可是,应当足以表现 synchronized 与 ReentrantLock 相比所具备的伸缩性优点了。
图表以每秒调用数为单位显示了吞吐率,把不一样的实现调整到 1 线程 synchronized 的状况。每一个实现都相对迅速地集中在某个稳定状态的吞吐率上,该状态一般要求处理器获得充分利用,把大多数的处理器时间都花在处理实际工做(计算机随机数)上,只有小部分时间花在了线程调度开支上。您会注意到,synchronized 版本在处理任何类型的争用时,表现都至关差,而 Lock 版本在调度的开支上花的时间至关少,从而为更高的吞吐率留下空间,实现了更有效的 CPU 利用。
条件变量
根类 Object 包含某些特殊的方法,用来在线程的 wait() 、 notify() 和 notifyAll() 之间进行通讯。这些是高级的并发性特性,许多开发人员历来没有用过它们 —— 这多是件好事,由于它们至关微妙,很容易使用不当。幸运的是,随着 JDK 5.0 中引入 java.util.concurrent ,开发人员几乎更加没有什么地方须要使用这些方法了。
通知与锁定之间有一个交互 —— 为了在对象上 wait 或 notify ,您必须持有该对象的锁。就像 Lock 是同步的归纳同样, Lock 框架包含了对 wait 和 notify 的归纳,这个归纳叫做 条件(Condition) 。 Lock 对象则充当绑定到这个锁的条件变量的工厂对象,与标准的 wait 和 notify 方法不一样,对于指定的 Lock ,能够有不止一个条件变量与它关联。这样就简化了许多并发算法的开发。例如, 条件(Condition) 的 Javadoc 显示了一个有界缓冲区实现的示例,该示例使用了两个条件变量,“not full”和“not empty”,它比每一个 lock 只用一个 wait 设置的实现方式可读性要好一些(并且更有效)。 Condition 的方法与 wait 、 notify 和 notifyAll 方法相似,分别命名为 await 、 signal 和 signalAll ,由于它们不能覆盖 Object 上的对应方法。
这不公平
若是查看 Javadoc,您会看到,ReentrantLock 构造器的一个参数是 boolean值,它容许您选择想要一个公平(fair)锁,仍是一个不公平(unfair)锁。公平锁使线程按照请求锁的顺序依次得到锁;而不公平锁则容许讨价还价,在这种状况下,线程有时能够比先请求锁的其余线程先获得锁。
为何咱们不让全部的锁都公平呢?毕竟,公平是好事,不公平是很差的,不是吗?(当孩子们想要一个决定时,总会叫嚷“这不公平”。咱们认为公平很是重要,孩子们也知道。)在现实中,公平保证了锁是很是健壮的锁,有很大的性能成本。要确保公平所须要的记账(bookkeeping)和同步,就意味着被争夺的公平锁要比不公平锁的吞吐率更低。做为默认设置,应当把公平设置为 false ,除非公平对您的算法相当重要,须要严格按照线程排队的顺序对其进行服务。
那么同步又如何呢?内置的监控器锁是公平的吗?答案令许多人感到大吃一惊,它们是不公平的,并且永远都是不公平的。可是没有人抱怨过线程饥渴,由于 JVM 保证了全部线程最终都会获得它们所等候的锁。确保统计上的公平性,对多数状况来讲,这就已经足够了,而这花费的成本则要比绝对的公平保证的低得多。因此,默认状况下 ReentrantLock 是“不公平”的,这一事实只是把同步中一直是事件的东西表面化而已。若是您在同步的时候并不介意这一点,那么在 ReentrantLock 时也没必要为它担忧。
只是添加了一个数据集,用来进行随机数基准检测,此次检测使用了公平锁,而不是默认的协商锁。正如您能看到的,公平是有代价的。若是您须要公平,就必须付出代价,可是请不要把它做为您的默认选择。
到处都好?
看起来 ReentrantLock 不管在哪方面都比 synchronized 好 —— 全部 synchronized 能作的,它都能作,它拥有与 synchronized 相同的内存和并发性语义,还拥有 synchronized 所没有的特性,在负荷下还拥有更好的性能。那么,咱们是否是应当忘记 synchronized ,再也不把它看成已经已经获得优化的好主意呢?或者甚至用 ReentrantLock 重写咱们现有的 synchronized 代码?实际上,几本 Java 编程方面介绍性的书籍在它们多线程的章节中就采用了这种方法,彻底用 Lock 来作示例,只把 synchronized 看成历史。但我以为这是把好事作得太过了。
还不要抛弃synchronized
虽然 ReentrantLock 是个很是动人的实现,相对 synchronized 来讲,它有一些重要的优点,可是我认为急于把 synchronized 视若敝屣,绝对是个严重的错误。 java.util.concurrent.lock 中的锁定类是用于高级用户和高级状况的工具 。通常来讲,除非您对 Lock 的某个高级特性有明确的须要,或者有明确的证据(而不是仅仅是怀疑)代表在特定状况下,同步已经成为可伸缩性的瓶颈,不然仍是应当继续使用 synchronized。
为何我在一个显然“更好的”实现的使用上主张保守呢?由于对于 java.util.concurrent.lock 中的锁定类来讲,synchronized 仍然有一些优点。好比,在使用 synchronized 的时候,不能忘记释放锁;在退出 synchronized 块时,JVM 会为您作这件事。您很容易忘记用 finally 块释放锁,这对程序很是有害。您的程序可以经过测试,但会在实际工做中出现死锁,那时会很难指出缘由(这也是为何根本不让初级开发人员使用 Lock 的一个好理由。)
另外一个缘由是由于,当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时可以包括锁定信息。这些对调试很是有价值,由于它们能标识死锁或者其余异常行为的来源。 Lock 类只是普通的类,JVM 不知道具体哪一个线程拥有 Lock 对象。并且,几乎每一个开发人员都熟悉 synchronized,它能够在 JVM 的全部版本中工做。在 JDK 5.0 成为标准(从如今开始可能须要两年)以前,使用 Lock 类将意味着要利用的特性不是每一个 JVM 都有的,并且不是每一个开发人员都熟悉的。
何时选择用ReentrantLock 代替synchronized
既然如此,咱们何时才应该使用 ReentrantLock 呢?答案很是简单 —— 在确实须要一些 synchronized 所没有的特性的时候,好比时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。 ReentrantLock 还具备可伸缩性的好处,应当在高度争用的状况下使用它,可是请记住,大多数 synchronized 块几乎历来没有出现过争用,因此能够把高度争用放在一边。我建议用 synchronized 开发,直到确实证实 synchronized 不合适,而不要仅仅是假设若是使用 ReentrantLock “性能会更好”。请记住,这些是供高级用户使用的高级工具。(并且,真正的高级用户喜欢选择可以找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情作好,而后再考虑是否是有必要作得更快。
结束语
Lock 框架是同步的兼容替代品,它提供了 synchronized 没有提供的许多特性,它的实如今争用下提供了更好的性能。可是,这些明显存在的好处,还不足以成为用 ReentrantLock 代替 synchronized 的理由。相反,应当根据您是否 须要 ReentrantLock 的能力来做出选择。大多数状况下,您不该当选择它 —— synchronized 工做得很好,能够在全部 JVM 上工做,更多的开发人员了解它,并且不太容易出错。只有在真正须要 Lock 的时候才用它。在这些状况下,您会很高兴拥有这款工具。 算法

相关文章
相关标签/搜索