同步 vs 锁定

211

java.util.concurrent API提供了一个称为Lock的类,它可以序列化控制以便访问关键资源。 它提供了park()unpark()等方法。

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

在实践中,哪种方式更好呢?为什么?


1
这里有一篇有用的文章:http://javarevisited.blogspot.in/2013/03/reentrantlock-example-in-java-synchronized-difference-vs-lock.html - roottraveller
11个回答

208
如果你只是需要锁定一个对象,我更喜欢使用 synchronized
示例:
Lock.acquire();
doSomethingNifty(); // Throws a NPE!
Lock.release(); // Oh noes, we never release the lock!

您必须在每个地方明确使用 try{} finally{}

而对于 synchronized,它非常清晰,不可能出错:

synchronized(myObject) {
    doSomethingNifty();
}

话虽如此,Lock在更复杂的情况下可能更有用,这种情况下你不能像上面那样简单地获取和释放。如果符合您的需求,我会更倾向于避免首先使用裸的Lock,而是选择更复杂的并发控制,例如CyclicBarrierLinkedBlockingQueue

我从未有过使用wait()notify()的理由,但可能存在一些好的理由。


1
LockSupport的wait/notify和park/unpark有什么区别?http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/LockSupport.html - Pacerier
8
一开始使用锁的例子很有道理,但后来我意识到,如果你使用try finally块,就可以避免锁未被释放的问题。 - William Reed
啊...这是欣赏C++中RAII模型的时刻之一。std::lock_guard - WhiZTiM

81
我想知道在实践中哪一个更好,并且为什么?
我发现 LockCondition(以及其他新的 concurrent 类)只是工具箱中的更多工具。我可以用我的旧钳子(即 synchronized 关键字)做我需要做的大多数事情,但在某些情况下它很难使用。当我在工具箱中添加了更多工具时,其中一些棘手的情况就变得更加简单:橡皮锤、球锤、撬棒和一些钉击。然而,我的旧钳子仍然有它的用途。
我不认为哪一个真正比另一个“更好”,而是每个都更适合解决不同的问题。 简单模型和面向范围的本质使 synchronized 有助于保护代码中的错误,但这些同样的优点有时会在更复杂的情况下成为障碍。 这些更复杂的场景是并发包创建来帮助解决的。 但是,在代码中使用这些更高级别的结构需要更明确和仔细的管理。
我认为 JavaDoc 在描述 Locksynchronized 之间的区别时做得很好(强调是我的):
锁实现提供比使用同步方法和语句更广泛的锁定操作。 它们允许更灵活的结构,可能具有完全不同的属性,并且可以支持多个关联的条件对象。
...使用同步方法或语句可访问与每个对象关联的隐式监视器锁,但强制所有锁获取和释放按块结构方式发生:当多个锁被获取时,它们必须以相反的顺序释放,并且所有锁必须在获取它们的相同词法作用域中释放
尽管使用同步方法和语句的范围机制使得使用监视器锁更加容易,并且有助于避免涉及锁的许多常见编程错误,但有时您需要以更灵活的方式使用锁。例如,对于需要遍历并发访问的数据结构的某些算法,需要使用“递交式”或“链式锁定”:获取节点A的锁,然后是节点B的锁,然后释放A并获取C的锁,然后释放B并获取D的锁,依此类推。 Lock接口的实现通过允许在不同的作用域中获取和释放锁,以及允许以任意顺序获取和释放多个锁来使用这些技术。 增加的灵活性伴随着额外的责任。块结构锁定的缺失消除了同步方法和语句自动释放锁的行为。在大多数情况下,应使用以下习惯用法:

...

在不同范围内发生锁定和解锁时,必须注意采取措施以确保所有执行的代码在锁定被持有时都受到try-finally或try-catch的保护,以确保在必要时释放锁定。

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

...


30
您可以通过使用 java.util.concurrent 中的实用程序来实现所有内容,这些实用程序可以使用低级原语(如synchronizedvolatilewait/notify)来完成。
然而,并发编程是棘手的,大多数人至少会出错一些部分,使他们的代码不正确或效率低下(或两者兼有)。
并发API提供了一种更高级别的方法,更容易(因此更安全)使用。简而言之,您应该不需要直接再使用synchronized, volatile, wait, notifyLock 类本身在工具箱的较低级别上,您甚至可能不需要直接使用它(大多数时候,您可以使用队列Semaphore等东西)。

2
普通的wait/notify是否被认为是比java.util.concurrent.locks.LockSupport的park/unpark更低级的原语,还是反过来呢? - Pacerier
2
不,我的意思是在这3个选项中:Thread.sleep/interrupt、Object.wait/notify和LockSupport.park/unpark,哪一个是原始的? - Pacerier
@Pacerier:也许可以为此开一个新问题。它们中是否有任何一个是在另一个之上实现的,如果它们都共享相同的更低级别的JVM内部原语,或者它们彼此独立。 - Thilo
3
@Thilo,我不确定你如何支持你的说法,认为 java.util.concurrent 比语言特性(例如synchronized)更容易。当你使用 java.util.concurrent 时,你必须养成在编写代码之前完成 lock.lock(); try { ... } finally { lock.unlock() } 的习惯,而对于 synchronized ,从一开始就可以使用。仅基于这一点,我会说 synchronizedjava.util.concurrent.locks.Lock 更容易(如果你想要它的行为)。 [第4段] (http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/Lock.html) - Iwan Aucamp
1
不要以为只用并发原语就能完全复制AtomicXXX类的行为,因为它们依赖于本地CAS调用,而在java.util.concurrent之前是不可用的。 - Duncan Armstrong
显示剩余2条评论

19

为什么要使用synchronizedjava.util.concurrent.Lock,存在4个主要因素。

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

  1. 当Java 5发布ReentrantLocks时,它们证明比内在锁定有相当明显的吞吐量差异。如果您正在寻找更快的锁定机制并且正在运行1.5,请考虑使用j.u.c.ReentrantLock。 Java 6的内在锁定现在可比较。

  2. j.u.c.Lock具有不同的锁定机制。可中断锁定-尝试锁定,直到锁定线程被中断;定时锁定-尝试锁定一定时间并在失败时放弃;tryLock-尝试锁定,如果其他线程持有锁则放弃。这些都是除简单锁定之外的内容。内在锁定只提供简单锁定。

  3. 风格。如果1和2都不属于您最关心的类别,包括我自己在内的大多数人都会发现内在锁定语法更易于阅读,比j.u.c.Lock锁定更简洁。
  4. 多重条件。您锁定的对象只能通知并等待单个情况。 Lock的newCondition方法允许单个锁定具有多个等待或信号原因。在实践中,我尚未真正需要此功能,但对于需要该功能的人来说,这是一个不错的功能。

我喜欢你评论中的详细信息。我想再添加一个要点 - 如果你正在处理多个线程,而只有一些线程需要写入对象,则ReadWriteLock提供了有用的行为。多个线程可以并发地读取该对象,并且仅在另一个线程已经在写入它时才会被阻塞。 - Sam Goldberg
补充第四点 - 在j.u.c.ArrayBlockingQueue中,锁有两个等待的原因:队列非空和队列非满。因此,j.u.c.ArrayBlockingQueue使用显式锁和lock.newCondition()。 - yiksanchan

6

主要的区别在于公平性,也就是请求是否按照FIFO(先进先出)处理或者是否可以插队?方法级别的同步可以确保公平或FIFO方式分配锁。使用

synchronized(foo) {
}

或者

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

没有锁定并不保证公平。

如果有很多线程争用锁,你很容易遇到抢占问题,其中新请求获得锁定,而旧请求被卡住。我见过有200个线程短时间内请求锁,第二个到达的线程最后被处理。这对于某些应用程序来说没问题,但对于其他一些应用程序来说会有致命的影响。

有关此主题的全面讨论,请参阅Brian Goetz的《Java并发实战》书籍第13.3节。


6
“方法级别的同步确保锁的公平或FIFO分配。” => 真的吗?您是说同步方法在公平性方面的行为与将方法内容包装到synchronized {}块中有所不同吗?我不这么认为,或者我理解错了那句话...? - weiresr
是的,尽管令人惊讶和违反直觉,但这是正确的。Goetz的书是最好的解释。 - Brian Tarbox
如果您查看@BrianTarbox提供的代码,同步块使用的是除“this”之外的某个对象进行锁定。理论上,同步方法和将该方法的整个主体放入同步块中没有区别,只要该块使用“this”作为锁。 - xburgos
请编辑答案,明确“保证”在这里是“统计保证”而不是确定性。 - Nathan Hughes
锁可以是公平的。请参阅ReentrantLock的构造函数 - Robert

6
我想在Bert F的回答上再添加一些内容。
锁支持各种方法来进行更细粒度的锁控制,这比隐式监视器(同步锁)更具表现力。
锁提供对共享资源的独占访问:一次只能有一个线程获取锁,并且所有对共享资源的访问都需要先获取锁。但是,某些锁可能允许对共享资源进行并发访问,例如ReadWriteLock的读锁。
从文档page中了解锁相比同步锁的优势。
  1. 使用同步方法或语句提供对每个对象关联的隐式监视器锁的访问,但强制所有锁获取和释放以块结构方式发生。

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

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

ReentrantLock:简单来说,ReentrantLock 允许对象从一个临界区重新进入到另一个临界区。由于您已经拥有进入一个临界区所需的锁,因此可以使用当前锁在同一对象上进入其他临界区。

ReentrantLock 的主要特点如下 article

  1. 能够可中断地进行锁定。
  2. 能够在等待锁时设置超时时间。
  3. 具备创建公平锁的能力。
  4. 提供 API 来获取正在等待锁的线程列表。
  5. 可以灵活地尝试锁定而不会阻塞。

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

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

StampedLock:

Java 8 提供了一种新类型的锁,称为 StampedLock,它也支持读写锁,就像上面的示例一样。与 ReadWriteLock 不同,StampedLock 的锁定方法返回一个由长整型值表示的标记。

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

请看这篇关于不同类型的ReentrantLockStampedLock锁使用的文章


6

锁和synchronized的主要区别:

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

4

锁让程序员的工作更加轻松。下面是一些可以通过锁轻松实现的情况。

  1. 在一个方法中加锁,在另一个方法中释放锁。
  2. 如果你有两个线程分别处理不同的代码块,但第一个线程对于第二个线程的某个代码块有先决条件(同时其他线程也在同一个代码块上工作),共享锁可以很容易地解决这个问题。
  3. 实现监视器。例如,一个简单的队列,其中put和get方法被许多其他线程执行。然而,你不希望多个put(或get)方法同时运行,也不希望put和get方法同时运行。私有锁可以使你更轻松地实现这一点。

然而,锁和条件都建立在synchronized机制上。因此,使用锁肯定可以实现您可以使用锁实现的相同功能。但是,使用synchronized解决复杂场景可能会让你的工作变得困难,并且可能会偏离解决实际问题的目标。


3
Brian Goetz的《Java并发实战》第13.3节: “……与默认的ReentrantLock一样,内在锁定也不提供确定性公平保证,但大多数锁实现的统计公平保证对于几乎所有情况都足够好。”

1
这里是关于`synchronized`和`Lock`的全面比较。
`synchronized`的优势有:
  • 自动释放:无需使用try-finally,防止遗忘释放。

     synchronized {
         someCode();
     }
    

    对比

     lock.lock();
     try {
         someCode();
     } finally {
         lock.release();
     }
    
  • 更少的额外行数(如上所示的例子)

  • 能够标记整个方法为synchronized,避免额外的行数和额外的缩进

     public synchronized someMethod() {
         someCode();
     }
    

锁的优势:

  • 扩展的API: .lockInterruptibly(), .tryLock(), tryLock(timeout)
  • 能够创建与锁相关联的多个Condition。例如,ArrayBlockingQueue同时持有notEmptynotFull条件。
  • 在调试过程中能够获取持有锁的线程,通过sync.exclusiveOwnerThread。获取锁状态的API: .getHoldCount(), .hasQueuedThreads()等。
  • 可以作为对象传递。例如,保持一个Map<ResourceObject, Lock>
  • 能够以任意顺序获取和释放多个锁
  • 能够创建公平锁:new ReentrantLock(true)
  • 存在ReadWriteLock允许并行读取

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接