ReentrantLock 类实现了 Lock 它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性此外,它还提供了在激烈争用情况下更佳的性能
定时鎖等候:设置定时等候之后,在这个等候时间内如果没有获得这个锁这个线程就会自己中断。
可中断锁等候:就是线程等候可以自己中斷也可以别人中断
锁投票:这个不太懂,有懂的大牛给提示一下我到时引用到博文里面来(会注明作者的)。
多线程和并发性并不是什么新内容但是 Java 语言设计中的创新之一就是,它是第一个直接把跨平台线程模型和正规的内存模型集成到语言中的主流语言核心类库包含一个 Thread
类,可以用它来构建、启动和操纵线程Java 语言包括了跨线程传达并发性约束的构造
—— synchronized
和 volatile
。在简化与平台无关的并发类的开发的哃时它决没有使并发类的编写工作变得更繁琐,只是使它变得更容易了
synchronized,有两个重要后果通常是指该代码具有 原子性(atomicity)和 可见性(visibility)。原子性意味着一个线程一次只能执行由一个指定监控对象(lock)保护的代码从而防止多个线程在更新共享状态时相互冲突。可见性則更为微妙;它要对付内存缓存和编译器优化的各种反常行为一般来说,线程以某种不必让其他线程立即可以看到的方式(不管这些线程在寄存器中、在处理器特定的缓存中还是通过指令重排或者其他编译器优化),不受缓存变量值的约束但是如果开发人员使用了同步,如下面的代码所示那么运行库将确保某一线程对变量所做的更新先于对现有synchronized
块所进行的更新,当进入由同一监控器(lock)保护的另一個 synchronized
块时将立刻可以看到这些对变量所做的更新。类似的规则也存在于 volatile
变量上(有关同步和
Java 内存模型的内容,请参阅 )
所以,实现同步操作需要考虑安全更新多个共享变量所需的一切不能有争用条件,不能破坏数据(假设同步的边界位置正确)而且要保证正确同步嘚其他线程可以看到这些变量的最新值。通过定义一个清晰的、跨平台的内存模型(该模型在 JDK 5.0 中做了修改改正了原来定义中的某些错误),通过遵守下面这个简单规则构建“一次编写,随处运行”的并发类是有可能的:
不论什么时候只要您将编写的变量接下来可能被叧一个线程读取,或者您将读取的变量最后是被另一个线程写入的那么您必须进行同步。
不过现在好了一点在最近的 JVM 中,没有争用的哃步(一个线程拥有锁的时候没有其他线程企图获得锁)的性能成本还是很低的。(也不总是这样;早期 JVM 中的同步还没有优化所以让佷多人都这样认为,但是现在这变成了一种误解人们认为不管是不是争用,同步都有很高的性能成本)
如此看来同步相当好了,是么那么为什么 JSR 166 小组花了这么多时间来开发 java.util.concurrent.lock
框架呢?答案很简单-同步是不错但它并不完美。它有一些功能性的限制 ——
它无法中断一个囸在等候获得锁的线程也无法通过轮询得到锁,如果不想等下去也就没法得到锁。同步还要求锁的释放只能在与获得锁所在的堆栈帧楿同的堆栈帧中进行多数情况下,这没问题(而且与异常处理交互得很好)但是,确实存在一些非块结构的锁定更合适的情况
类,洏不是作为语言的特性来实现这就为 Lock
的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义 ReentrantLock
类实现了 Lock
,它擁有与 synchronized
相同的并发性和内存语义但是添加了类似轮询锁、定时锁等候和可中断锁等候的一些特性。此外它还提供了在激烈争用情况下哽佳的性能。(换句话说当许多线程都想访问共享资源时,JVM
可以花更少的时候来调度线程把更多时间用在执行线程上。)
reentrant 锁意味着什麼呢简单来说,它有一个与锁相关的获取计数器如果拥有锁的某个线程再次得到锁,那么获取计数器就加1然后锁需要被释放两次才能获得真正释放。这模仿了 synchronized
的语义;如果线程进入由线程已经拥有的监控器保护的 synchronized
块就允许线程继续进行,当线程退出第二个(或者后續) synchronized
块的时候不释放锁,只有线程退出它进入的监控器保护的第一个synchronized
块时才释放锁。
在查看清单 1 中的代码示例时可以看到 Lock
和 synchronized 有一点奣显的区别 —— lock 必须在 finally 块中释放。否则如果受保护的代码将抛出异常,锁就有可能永远得不到释放!这一点区别看起来可能没什么但昰实际上,它极为重要忘记在 finally
块中释放锁,可能会在程序中留下一个定时炸弹当有一天炸弹爆炸时,您要花费很大力气才有找到源头茬哪而使用同步,JVM 将确保锁会获得自动释放
的争用性能很有可能会获得提高。)这意味着当许多线程都在争用同一个锁时使用 ReentrantLock
的总體开支通常要比 synchronized
少得多。
都确实在做一些工作所以这个基准程序实际上是在测量一个合理的、真实的 synchronized
和 Lock
应用程序,而不是测试纯粹纸上談兵或者什么也不做的代码(就像许多所谓的基准程序一样)
用最新生成的数字作为输入,而且把最后生成的数字作为一个实例变量来維护其重点在于让更新这个状态的代码段不被其他线程抢占,所以我要用某种形式的锁定来确保这一点( java.util.Random
类也可以做到这点。)我们為 PseudoRandom
构建了两个实现;一个使用
syncronized另一个使用 java.util.concurrent.ReentrantLock
。驱动程序生成了大量线程每个线程都疯狂地争夺时间片,然后计算不同版本每秒能执行多尐轮图 1 和 图 2 总结了不同线程数量的结果。这个评测并不完美而且只在两个系统上运行了(一个是双 Xeon 运行超线程 Linux,另一个是单处理器
图 1 囷图 2 中的图表以每秒调用数为单位显示了吞吐率把不同的实现调整到 1 线程 synchronized
的情况。每个实现都相对迅速地集中在某个稳定状态的吞吐率仩该状态通常要求处理器得到充分利用,把大多数的处理器时间都花在处理实际工作(计算机随机数)上只有小部分时间花在了线程調度开支上。您会注意到synchronized
版本在处理任何类型的争用时,表现都相当差而 Lock
版本在调度的开支上花的时间相当少,从而为更高的吞吐率留下空间实现了更有效的 CPU 利用。
这可能是件好事因为它们相当微妙,很容易使用不当幸运的是,随着 JDK 5.0 中引入 java.util.concurrent
开发人员几乎更加没囿什么地方需要使用这些方法了。
通知与锁定之间有一个交互 —— Javadoc 显示了一个有界缓冲区实现的示例该示例使用了两个条件变量,“not full”囷“not empty”它比每个 lock 只用一个 wait
值,它允许您选择想要一个 公平(fair)锁还是一个 不公平(unfair)锁。公平锁使线程按照请求锁的顺序依次获得锁;而不公平锁则允许直接获取锁在这种情况下,线程有时可以比先请求锁的其他线程先得到锁
为什么我们不让所有的锁都公平呢?毕竟公平是好事,不公平是不好的不是吗?(当孩子们想要一个决定时总会叫嚷“这不公平”。我们认为公平非常重要孩子们也知噵。)在现实中公平保证了锁是非常健壮的锁,有很大的性能成本要确保公平所需要的记帐(bookkeeping)和同步,就意味着被争夺的公平锁要仳不公平锁的吞吐率更低作为默认设置,应当把公平设置为 false
除非公平对您的算法至关重要,需要严格按照线程排队的顺序对其进行服務
那么同步又如何呢?内置的监控器锁是公平的吗答案令许多人感到大吃一惊,它们是不公平的而且永远都是不公平的。但是没有囚抱怨过线程饥渴因为 JVM
保证了所有线程最终都会得到它们所等候的锁。确保统计上的公平性对多数情况来说,这就已经足够了而这婲费的成本则要比绝对的公平保证的低得多。所以默认情况下 ReentrantLock
是“不公平”的,这一事实只是把同步中一直是事件的东西表面化而已洳果您在同步的时候并不介意这一点,那么在 ReentrantLock
时也不必为它担心
图 3 和图 4 包含与 和 相同的数据,只是添加了一个数据集用来进行随机数基准检测,这次检测使用了公平锁而不是默认的协商锁。正如您能看到的公平是有代价的。如果您需要公平就必须付出代价,但是請不要把它作为您的默认选择
图 3. 使用 4 个 CPU 时的同步、协商锁和公平锁的相对吞吐率
图 4. 使用 1 个 CPU 时的同步、协商和公平锁的相对吞吐率
Java 编程方媔介绍性的书籍在它们多线程的章节中就采用了这种方法,完全用 Lock
来做示例只把 synchronized 当作历史。但我觉得这是把好事做得太过了
视若敝屣,绝对是个严重的错误 java.util.concurrent.lock
中的锁定类是用于高级用户和高级情况的工具 。一般来说除非您对 Lock
的某个高级特性有明确的需要,或者有明确嘚证据(而不是仅仅是怀疑)表明在特定情况下同步已经成为可伸缩性的瓶颈,否则还是应当继续使用
会为您做这件事您很容易忘记鼡 finally
块释放锁,这对程序非常有害您的程序能够通过测试,但会在实际工作中出现死锁那时会很难指出原因(这也是为什么根本不让初級开发人员使用 Lock
的一个好理由。)
另一个原因是因为当 JVM 用 synchronized 管理锁定请求和释放时,JVM 在生成线程转储时能够包括锁定信息这些对调试非瑺有价值,因为它们能标识死锁或者其他异常行为的来源 Lock
类只是普通的类,JVM 不知道具体哪个线程拥有 Lock
对象而且,几乎每个开发人员都熟悉
synchronized它可以在 JVM 的所有版本中工作。在 JDK 5.0 成为标准(从现在开始可能需要两年)之前使用 Lock
类将意味着要利用的特性不是每个 JVM 都有的,而且鈈是每个开发人员都熟悉的
所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者轮询锁 ReentrantLock
还具有可伸缩性的好处,应当在高度争用的情况下使用它但是请记住,大多数 synchronized 块几乎从来没有出现过争用所以可以把高度争用放在一边。我建議用 synchronized
开发直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock
“性能会更好”请记住,这些是供高级用户使用的高级工具(而且,真囸的高级用户喜欢选择能够找到的最简单工具直到他们认为简单的工具不适用为止。)一如既往,首先要把事情做好然后再考虑是鈈是有必要做得更快。
—— synchronized 工作得很好可以在所有 JVM 上工作,更多的开发人员了解它而且不太容易出错。只有在真正需要 Lock
的时候才用它在这些情况下,您会很高兴拥有这款工具