同步与锁定


182

java.util.concurrentAPI提供了一个名为的类Lock,该类将基本上对控件进行序列化以访问关键资源。它给出了诸如park()和的方法unpark()

如果我们可以使用synchronized关键字以及using wait()notify() notifyAll()方法,我们可以做类似的事情。

我想知道其中哪一个在实践中更好,为什么?


Answers:


178

如果您只是锁定一个对象,我更喜欢使用 synchronized

例:

Lock.acquire();
doSomethingNifty(); // Throws a NPE!
Lock.release(); // Oh noes, we never release the lock!

您必须在try{} finally{}任何地方都明确地做。

而使用同步,则非常清晰,不可能出错:

synchronized(myObject) {
    doSomethingNifty();
}

也就是说,Lock对于无法以这种干净的方式获取和释放的更复杂的事物,s可能会更有用。老实说,我宁愿避免首先使用Locks,而可以使用更复杂的并发控制(例如a CyclicBarrier或a LinkedBlockingQueue)来满足您的需求。

我从未有过使用的理由,wait()或者notify()可能有一些不错的理由。


1
LockSupport的等待/通知与停放/取消停放之间有什么区别?docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/...
Pacerier

6
最初,该示例使用锁是有意义的,但是后来我意识到,如果您使用try finally块,则可以避免不释放锁而避免该问题
William Reed

啊...欣赏C ++ RAII模型的那一刻。std::lock_guard
WhiZTiM '17

67

我想知道其中哪一个在实践中更好,为什么?

我发现LockCondition(以及其他新concurrent类)只是该工具箱的更多工具。我可以用旧的羊角锤(synchronized关键字)来完成大部分所需的工作,但是在某些情况下使用起来很尴尬。一旦我在工具箱中添加了更多工具,其中一些尴尬的情况就变得更加简单:橡胶槌,圆头锤,撬棒和一些打孔器。但是,我的旧羊角锤仍然可以使用。

我不认为一个人真的比另一个人“更好”,但是每个人都更适合不同的问题。简而言之,的简单模型和面向范围的性质synchronized有助于保护我免受代码中的错误的侵害,但是在更复杂的场景中,这些相同的优点有时会成为障碍。创建并发包以帮助解决这些更复杂的情况。但是,使用这种更高级别的结构需要对代码进行更明确,更仔细的管理。

===

我认为JavaDoc中确实描述之间的区别的一个好工作Locksynchronized(重点是我的):

与使用同步方法和语句相比,锁实现提供了更广泛的锁操作。它们允许更灵活的结构,可以具有完全不同的属性,并且可以支持多个关联的Condition对象

...

使用同步方法或语句可访问与每个对象关联的隐式监视器锁,但会强制所有锁的获取和释放以块结构的方式发生:当获取多个锁时,它们必须以相反的顺序释放,并且所有锁必须在获得它们的相同词汇范围内释放

尽管用于同步方法和语句的作用域机制使使用监视器锁编程变得更加容易,并且有助于避免许多常见的涉及锁的编程错误,但在某些情况下,您需要以更灵活的方式使用锁。例如,用于遍历并发访问的数据结构的某些算法需要使用“交接”或“链锁”:您先获得节点A的锁,然后获得节点B的锁,然后释放A并获得C,然后释放B并获得D,依此类推。所述的实施方式中锁定接口通过使得能够使用这样的技术允许获得并在不同的范围释放锁,并允许以任意顺序获取和释放多个锁

有了这个增加的灵活性带来了更多的责任。在不存在块结构锁定的去除锁的自动释放,与同步方法和语句发生。在大多数情况下,应使用以下惯用法:

...

锁定和解锁发生在不同的范围内时,必须小心以确保通过try-finally或try-catch保护持有锁定时执行的所有代码,以确保在必要时释放锁定

锁实现通过使用非阻塞尝试获取锁(tryLock()),尝试获取可以被中断的锁(lockInterruptible()以及尝试获取),提供了比同步方法和语句更多的功能可能超时的锁(tryLock(long,TimeUnit))。

...


23

您可以实现一切在公用事业java.util.concurrent中 做的低级原像synchronizedvolatile等待 / 通知

但是,并发是棘手的,大多数人至少会误认为其中的某些部分,使他们的代码不正确或效率低下(或两者兼而有之)。

并发API提供了更高级别的方法,该方法更易于使用(因此更加安全)。简而言之,您不再需要synchronized, volatile, wait, notify直接使用。

类本身就是对这个工具箱的下级侧,你甚至可能不会需要使用直接或者(你可以用Queues信号量和材料等,大部分的时间)。


2
是否将普通的旧的wait / notify视为比java.util.concurrent.locks.LockSupport的park / unpark更低级别的原语,还是相反?
Pacerier 2012年

@Pacerier:我认为两者都是低级的(即应用程序程序员想要避免直接使用的东西),但是肯定java.util.concurrency的低级部分(例如locks包)建立在顶部本地JVM原语的等待/通知(甚至更低级别)。
Thilo 2012年

2
不,我的意思不是3:原始的Thread.sleep / interrupt,Object.wait / notify,LockSupport.park / unpark ?
Pacerier,2012年

2
@Thilo我不确定您如何支持java.util.concurrent比一般语言功能(synchronized,等等)更容易的语句。当您使用时,java.util.concurrent您必须养成lock.lock(); try { ... } finally { lock.unlock() }在编写代码之前先完成操作的习惯,而使用a synchronized则从一开始就基本没问题。仅凭此基础,我会说synchronized(假设您想要它的行为)比容易java.util.concurrent.locks.Lock标准杆4杆
伊万·奥坎普

1
不要以为仅使用并发原语就可以完全复制AtomicXXX类的行为,因为它们依赖于java.util.concurrent之前不可用的本机CAS调用。
Duncan Armstrong

15

您要使用synchronized或的原因有4个主要因素java.util.concurrent.Lock

注意:当我说内在锁定时,我指的是同步锁定。

  1. 当Java 5推出ReentrantLocks时,事实证明它们与固有锁定相比在吞吐量上有显着差异。如果您正在寻找更快的锁定机制并正在运行1.5,请考虑使用jucReentrantLock。Java 6的固有锁定现在是可比较的。

  2. jucLock具有不同的锁定机制。锁定可中断-尝试锁定直到锁定线程被中断;定时锁定-尝试锁定一定的时间,如果失败则放弃;tryLock-尝试锁定,如果其他线程持有该锁定则放弃。除了简单的锁以外,所有这些都包括在内。本质锁定仅提供简单的锁定

  3. 样式。如果1和2都不属于大多数人(包括我自己)所关注的范畴,则将发现内在锁定语义学比jucLock锁定更易于阅读且不那么冗长。
  4. 多个条件。您锁定的对象只能被通知并等待一种情况。Lock的newCondition方法允许单个Lock具有多个原因等待或发出信号。实际上,我还没有实际需要此功能,但是对于需要它的人来说,这是一个不错的功能。

我喜欢您评论中的详细信息。我还要再加上一个要点-如果要处理多个线程,则只有ReadWriteLock提供有用的行为,其中只有一些线程需要写入对象。多个线程可以同时读取该对象,并且只有在另一个线程已经向其写入时才被阻塞。
山姆·戈德堡

5

我想在Bert F答案的基础上添加更多内容 。

Locks支持各种用于更细粒度的锁控制的方法,这些方法比隐式监视器(synchronized锁)更具表现力

锁提供对共享资源的独占访问:一次只能有一个线程可以获取该锁,对共享资源的所有访问都需要首先获取该锁。但是,某些锁可能允许并发访问共享资源,例如ReadWriteLock的读锁。

文档页面上的“同步锁定”的优点

  1. 使用同步方法或语句可访问与每个对象关联的隐式监视器锁,但强制所有锁的获取和释放以块结构方式进行

  2. 锁实现通过使用非阻塞尝试获取a lock (tryLock()),尝试获取可以被中断的锁(lockInterruptibly()以及尝试获取可以锁)来提供与同步方法和语句相比更多的功能timeout (tryLock(long, TimeUnit))

  3. Lock类还可以提供与隐式监视器锁完全不同的行为和语义,例如保证顺序,不可重用或死锁检测

ReentrantLock:根据我的理解,简单而言,它ReentrantLock允许对象从一个关键部分重新输入到另一关键部分。由于您已经具有进入一个关键部分的锁定,因此可以使用当前锁定在同一对象上的其他关键部分。

ReentrantLock主要特点为每本文章

  1. 能够中断锁定。
  2. 能够在等待锁定时超时。
  3. 建立公平锁定的权力。
  4. 用于获取锁等待线程列表的A​​PI。
  5. 尝试锁定的灵活性而不会阻塞。

您可以 ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock用来进一步获取对读写操作的粒度锁定的控制。

除了这三个ReentrantLocks,Java 8还提供了一个Lock

盖章锁:

Java 8附带了一种称为StampedLock的新型锁,它也支持读写锁,就像上面的示例一样。与ReadWriteLock相比,StampedLock的锁定方法返回以长值表示的图章。

您可以使用这些标记来释放锁或检查锁是否仍然有效。另外,加盖式锁支持另一种称为乐观锁的锁模式。

看一下这篇关于不同类型的ReentrantLockStampedLock锁用法的文章


4

主要区别在于公平性,换句话说,请求是按FIFO处理还是可以进行插入?方法级别同步可确保公平分配或FIFO分配锁。使用

synchronized(foo) {
}

要么

lock.acquire(); .....lock.release();

不保证公平。

如果您对锁有很多争执,那么您很容易在新请求获得锁而旧请求陷入阻塞的过程中遇到插入问题。我见过这样的情况,其中200个线程在短时间内到达一个锁,而第二个线程最后被处理。对于某些应用程序这是可以的,但对于其他应用程序则是致命的。

有关此主题的完整讨论,请参见Brian Goetz的“ Java Concurrency in Practice”(第13.3节)。


5
“方法级同步确保公平或先进先出的锁分配。” =>真的吗?您是说同步方法的行为公平性与将方法内容包装到Synchronized {}块中不同吗?我不这么认为,还是我听错了这句话...?
weiresr

是的,尽管令人惊讶并且与直觉相反是正确的。格茨的书是最好的解释。
Brian Tarbox 2012年

如果查看@BrianTarbox提供的代码,则同步块正在使用“ this”以外的其他对象进行锁定。从理论上讲,只要同步块将“ this”用作锁定,同步方法和将所述方法的整个主体放入同步块之间就没有区别。
xburgos 2015年

答案应进行编辑以包括引号,并在此处明确表示“保证”是“统计保证”,而不是确定性的。
内森·休斯

抱歉,我刚刚发现我几天前错误地否决了这个答案(笨拙的点击)。不幸的是,SO目前不允许我还原它。希望以后能解决。
MikkoÖstlund'18

3

Brian Goetz的“ Java Concurrency In Practice”一书,第13.3节:“ ...像默认的ReentrantLock一样,内在锁不提供确定性的公平保证,但是大多数锁实现的统计公平保证足以满足几乎所有情况……”


1

锁使程序员的生活更轻松。通过锁定可以轻松实现以下几种情况。

  1. 锁定一种方法,然后释放另一种方法的锁定。
  2. 您有两个线程在两个不同的代码段上工作,但是第一个线程依赖于第二个线程来完成某些代码,然后再继续执行(而其他一些线程也同时工作)。共享锁可以很容易地解决此问题。
  3. 实施监视器。例如,一个简单的队列,其中put和get方法是从许多不同的线程执行的。但是,您是否不想让相同的方法相互重叠,或者put和get方法都不能重叠。在这种情况下,私人锁使生活变得非常轻松。

While,锁和条件建立在同步的基础上。因此,您当然可以达到相同的目标。但是,这可能会使您的生活变得困难,并使您无法解决实际的问题。


1

锁定和同步之间的主要区别:

  • 使用锁,您可以按任何顺序释放和获取锁。
  • 同步时,您只能按获取顺序释放锁。

0

锁定和同步块的作用相同,但取决于用法。考虑以下部分

void randomFunction(){
.
.
.
synchronize(this){
//do some functionality
}

.
.
.
synchronize(this)
{
// do some functionality
}


} // end of randomFunction

在上述情况下,如果线程进入同步块,则另一个块也被锁定。如果同一对象上有多个这样的同步块,则所有块都将被锁定。在这种情况下,可以使用java.util.concurrent.Lock防止不必要的块锁定

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.