原文:http://www.ibm.com/developerworks/cn/java/j-jtp10264/index.html
多线程和并发性并不是什么新内容,但是Java语言设计中的创新之一就是,它是第一个直接把跨平台线程模型和正规的内存模型集成到语言中的主流语言。核心类库包含一个Thread类,可以用它来构建、启动和操纵线程,Java语言包括了跨线程传达并发性约束的构造——synchronized和volatile。在简化与平台无关的并发类的开发的同时,它决没有使并发类的编写工作变得更繁琐,只是使它变得更容易了。
synchronized快速回顾
把代码块声明为synchronized,有两个重要后果,通常是指该代码具有原子性(atomicity)和可见性(visibility)。原子性意味着一个线程一次只能执行由一个指定监控对象(lock)保护的代码,从而防止多个线程在更新共享状态时相互冲突。可见性则更为微妙;它要对付内存缓存和编译器优化的各种反常行为。一般来说,线程以某种不必让其他线程立即可以看到的方式(不管这些线程在寄存器中、在处理器特定的缓存中,还是通过指令重排或者其他编译器优化),不受缓存变量值的约束,但是如果开发人员使用了同步,如下面的代码所示,那么运行库将确保某一线程对变量所做的更新先于对现有synchronized块所进行的更新,当进入由同一监控器(lock)保护的另一个synchronized块时,将立刻可以看到这些对变量所做的更新。类似的规则也存在于volatile变量上。
[java]viewplaincopy
1.synchronized(lockObject){
2.//updateobjectstate
3.}
所以,实现同步操作需要考虑安全更新多个共享变量所需的一切,不能有争用条件,不能破坏数据(假设同步的边界位置正确),而且要保证正确同步的其他线程可以看到这些变量的最新值。通过定义一个清晰的、跨平台的内存模型(该模型在JDK5.0中做了修改,改正了原来定义中的某些错误),通过遵守下面这个简单规则,构建“一次编写,随处运行”的并发类是有可能的:
不论什么时候,只要您将编写的变量接下来可能被另一个线程读取,或者您将读取的变量最后是被另一个线程写入的,那么您必须进行同步。
不过现在好了一点,在最近的JVM中,没有争用的同步(一个线程拥有锁的时候,没有其他线程企图获得锁)的性能成本还是很低的。(也不总是这样;早期JVM中的同步还没有优化,所以让很多人都这样认为,但是现在这变成了一种误解,人们认为不管是不是争用,同步都有很高的性能成本。)
对synchronized的改进
如此看来同步相当好了,是么?那么为什么JSR166小组花了这么多时间来开发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保护代码块。
[java]viewplaincopy
1.Locklock=newReentrantLock();
2.lock.lock();
3.try{
4.//updateobjectstate
5.}
6.finally{
7.lock.unlock();
8.}
除此之外,与目前的synchronized实现相比,争用下的ReentrantLock实现更具可伸缩性。(在未来的JVM版本中,synchronized的争用性能很有可能会获得提高。)这意味着当许多线程都在争用同一个锁时,使用ReentrantLock的总体开支通常要比synchronized少得多。
比较ReentrantLock和synchronized的可伸缩性
TimPeierls用一个简单的线性全等伪随机数生成器(PRNG)构建了一个简单的评测,用它来测量synchronized和Lock之间相对的可伸缩性。这个示例很好,因为每次调用nextRandom()时,PRNG都确实在做一些工作,所以这个基准程序实际上是在测量一个合理的、真实的synchronized和Lock应用程序,而不是测试纯粹纸上谈兵或者什么也不做的代码(就像许多所谓的基准程序一样。)
在这个基准程序中,有一个PseudoRandom的接口,它只有一个方法nextRandom(intbound)。该接口与java.util.Random类的功能非常类似。因为在生成下一个随机数时,PRNG用最新生成的数字作为输入,而且把最后生成的数字作为一个实例变量来维护,其重点在于让更新这个状态的代码段不被其他线程抢占,所以我要用某种形式的锁定来确保这一点。(java.util.Random类也可以做到这点。)我们为PseudoRandom构建了两个实现;一个使用syncronized,另一个使用java.util.concurrent.ReentrantLock。驱动程序生成了大量线程,每个线程都疯狂地争夺时间片,然后计算不同版本每秒能执行多少轮。图1和图2总结了不同线程数量的结果。这个评测并不完美,而且只在两个系统上运行了(一个是双Xeon运行超线程Linux,另一个是单处理器Windows系统),但是,应当足以表现synchronized与ReentrantLock相比所具有的伸缩性优势了。
图1和图2中的图表以每秒调用数为单位显示了吞吐率,把不同的实现调整到1线程synchronized的情况。每个实现都相对迅速地集中在某个稳定状态的吞吐率上,该状态通常要求处理器得到充分利用,把大多数的处理器时间都花在处理实际工作(计算机随机数)上,只有小部分时间花在了线程调度开支上。您会注意到,synchronized版本在处理任何类型的争用时,表现都相当差,而Lock版本在调度的开支上花的时间相当少,从而为更高的吞吐率留下空间,实现了更有效的CPU利用。
条件变量
根类Object包含某些特殊的方法,用来在线程的wait()、notify()和notifyAll()之间进行通信。这些是高级的并发性特性,许多开发人员从来没有用过它们——这可能是件好事,因为它们相当微妙,很容易使用不当。幸运的是,随着JDK5.0中引入java.util.concurrent,开发人员几乎更加没有什么地方需要使用这些方法了。
通知与锁定之间有一个交互——为了在对象上wait或notify,您必须持有该对象的锁。就像Lock是同步的概括一样,Lock框架包含了对wait和notify的概括,这个概括叫作条件(Condition)。Lock对象则充当绑定到这个锁的条件变量的工厂对象,与标准的wait和notify方法不同,对于指定的Lock,可以有不止一个条件变量与它关联。这样就简化了许多并发算法的开发。例如,条件(Condition)的Javadoc显示了一个有界缓冲区实现的示例,该示例使用了两个条件变量,“notfull”和“notempty”,它比每个lock只用一个wait设置的实现方式可读性要好一些(而且更有效)。Condition的方法与wait、notify和notifyAll方法类似,分别命名为await、signal和signalAll,因为它们不能覆盖Object上的对应方法。
这不公平
如果查看Javadoc,您会看到,ReentrantLock构造器的一个参数是boolean值,它允许您选择想要一个公平(fair)锁,还是一个不公平(unfair)锁。公平锁使线程按照请求锁的顺序依次获得锁;而不公平锁则允许讨价还价,在这种情况下,线程有时可以比先请求锁的其他线程先得到锁。
为什么我们不让所有的锁都公平呢?毕竟,公平是好事,不公平是不好的,不是吗?(当孩子们想要一个决定时,总会叫嚷“这不公平”。我们认为公平非常重要,孩子们也知道。)在现实中,公平保证了锁是非常健壮的锁,有很大的性能成本。要确保公平所需要的记帐(bookkeeping)和同步,就意味着被争夺的公平锁要比不公平锁的吞吐率更低。作为默认设置,应当把公平设置为false,除非公平对您的算法至关重要,需要严格按照线程排队的顺序对其进行服务。
那么同步又如何呢?内置的监控器锁是公平的吗?答案令许多人感到大吃一惊,它们是不公平的,而且永远都是不公平的。但是没有人抱怨过线程饥渴,因为JVM保证了所有线程最终都会得到它们所等候的锁。确保统计上的公平性,对多数情况来说,这就已经足够了,而这花费的成本则要比绝对的公平保证的低得多。所以,默认情况下ReentrantLock是“不公平”的,这一事实只是把同步中一直是事件的东西表面化而已。如果您在同步的时候并不介意这一点,那么在ReentrantLock时也不必为它担心。
图3和图4包含与图1和图2相同的数据,只是添加了一个数据集,用来进行随机数基准检测,这次检测使用了公平锁,而不是默认的协商锁。正如您能看到的,公平是有代价的。如果您需要公平,就必须付出代价,但是请不要把它作为您的默认选择。
处处都好?
看起来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的所有版本中工作。在JDK5.0成为标准(从现在开始可能需要两年)之前,使用Lock类将意味着要利用的特性不是每个JVM都有的,而且不是每个开发人员都熟悉的。
什么时候选择用ReentrantLock代替synchronized
既然如此,我们什么时候才应该使用ReentrantLock呢?答案非常简单——在确实需要一些synchronized所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者锁投票。ReentrantLock还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数synchronized块几乎从来没有出现过争用,所以可以把高度争用放在一边。我建议用synchronized开发,直到确实证明synchronized不合适,而不要仅仅是假设如果使用ReentrantLock“性能会更好”。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。
Lock框架是同步的兼容替代品,它提供了synchronized没有提供的许多特性,它的实现在争用下提供了更好的性能。但是,这些明显存在的好处,还不足以成为用ReentrantLock代替synchronized的理由。相反,应当根据您是否需要ReentrantLock的能力来作出选择。大多数情况下,您不应当选择它——synchronized工作得很好,可以在所有JVM上工作,更多的开发人员了解它,而且不太容易出错。只有在真正需要Lock的时候才用它。在这些情况下,您会很高兴拥有这款工具。
分享到:
相关推荐
内容概要:本文深入探讨了Java中的并发控制机制,重点讲解了ReentrantLock和synchronized的特点及其背后的实现原理。通过对两者的特性进行对比,详细解析了ReentrantLock在灵活性、公平性和中断响应等方面的优点。并...
在Java多线程编程中,`ReentrantLock`和`synchronized`都是用于实现线程同步的重要工具,确保在并发环境中数据的一致性和正确性。两者虽然都能实现互斥访问,但在功能、性能以及使用场景上有所不同。下面我们将深入...
其中,`synchronized`关键字与`ReentrantLock`类是两种常用且重要的同步机制。本文旨在探讨这两种同步机制的特点、使用场景及其差异。 #### 二、synchronized 关键字 `synchronized`关键字是Java提供的内置同步机制...
在Java并发编程中,理解和熟练...总的来说,Java并发编程中的锁机制是确保线程安全的关键,理解并熟练使用`synchronized`和`ReentrantLock`,以及相应的等待/通知机制,能够帮助开发者设计出高效且可靠的多线程程序。
总结来说,Synchronized和ReentrantLock是Java多线程编程中的两种重要锁机制。Synchronized以其简洁性和自动释放的特性赢得了广泛的应用,而ReentrantLock则以其灵活性和控制性在复杂场景中表现出色。理解和熟练掌握...
本文将深入探讨四种关键的并发控制机制:synchronized关键字、ReentrantLock(可重入锁)、volatile关键字以及Atomic类的原理与应用。 ### 1. synchronized关键字 `synchronized`关键字是Java提供的内置锁,用于...
Java 锁可以分为两大类:synchronized 锁和 ReentrantLock 锁。 一、Synchronized 锁 1. 锁的原理:synchronized 锁是基于对象头的 Mark Word 来实现的。Mark Word 中有一个锁标志位,用于标识对象的锁状态。 2. ...
本文将深入探讨synchronized锁的实现原理,特别是从Java对象头的角度来分析其工作机制。 首先,我们需要理解synchronized锁住的是对象,而不是代码块。例如,当我们使用`synchronized (o)`对一个对象o进行加锁时,...
首先,synchronized是Java内置的同步机制,它提供了互斥和可见性保证。在Java 5之前,它是唯一的同步手段。synchronized可以通过修饰方法或包裹代码块来实现同步。当一个线程获得对象的监视器锁后,其他尝试获取该锁...
下面对这三种机制进行详细的分析和比较。 一、Synchronized Synchronized 是 Java 中最基本的同步机制,它可以用来同步方法或代码块。Synchronized 的实现是基于锁机制的,它会锁定一个对象的监视器,以便防止其他...
在 Java 中,我们可以使用 synchronized、ReentrantLock 等锁机制来实现线程之间的同步。锁机制可以分为悲观锁和乐观锁两种,悲观锁会引起其他线程阻塞,而乐观锁则基于冲突检测。 并发编程的挑战 并发编程中最大...
在Java中,有两种主要的锁机制:内置的`synchronized`关键字和显式的`ReentrantLock`类。这两者各有优劣,适用于不同的场景。下面我们将详细讨论它们的区别、性能、特性以及使用上的差异。 1. **功能对比**: - `...
与传统的synchronized关键字相比,ReentrantLock提供了更多控制手段,比如可以指定是否公平锁、支持中断等特性。 #### 二、ReentrantLock的主要功能 ReentrantLock提供了多种锁获取方式,包括但不限于: - `lock()`...
在Java中,主要的锁机制包括`synchronized`关键字和`Lock`接口(如`ReentrantLock`)。下面将详细讲解这两种锁机制及其应用。 1. `synchronized`关键字 `synchronized`用于标记方法或代码块,确保同一时间只有一个...
ReentrantLock是一个可重入的锁,这意味着一个线程可以多次获取同一锁,这与synchronized内置锁的行为相同。然而,与内置锁不同的是,使用ReentrantLock需要显式调用`lock()`和`unlock()`方法来获取和释放锁,这增加...
Java锁机制的发展历经了多个版本的改进,尤其是Java 5.0引入的显示锁(Explicit Locks),提供了一系列比内置锁(synchronized)更灵活的同步手段。 在Java中,内置锁是通过synchronized关键字实现的,而显示锁则是...
Java中有两种锁机制:内置锁(synchronized)和显式锁(ReentrantLock)。内置锁是Java语言提供的一种同步机制,使用synchronized关键字声明的方法或代码块可以被线程安全地执行。显式锁则是JDK 1.5引入的,提供了...
除了以上所述的锁类型外,Java还提供了一些与锁相关的高级特性,例如锁粗化和锁消除。锁粗化是指将多个细粒度的锁操作合并为一个粗粒度的锁操作,以减少线程在获取和释放锁时产生的开销。锁消除是指在运行时,编译器...
Java多线程ReentrantLock互斥锁详解 ...ReentrantLock是Java多线程编程中的一种锁机制,提供了加锁和解锁的方法,可以实现线程之间的同步访问资源。ReentrantLock的使用可以避免死锁的出现,提高程序的可靠性和稳定性。