【问题标题】:Synchronization vs Lock同步与锁定
【发布时间】:2011-05-11 05:52:24
【问题描述】:

java.util.concurrent API 提供了一个名为Lock 的类,它基本上会序列化控件以访问关键资源。它给出了park()unpark()等方法。

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

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

【问题讨论】:

标签: java multithreading concurrency synchronization java.util.concurrent


【解决方案1】:

Lock 让程序员的生活更轻松。这里有几种情况可以用lock轻松实现。

  1. 在一种方法中锁定,在另一种方法中释放锁定。
  2. 如果您有两个线程处理两段不同的代码,但是,在第一个线程中,第二个线程中的某段代码具有先决条件(而其他一些线程也在处理同一段代码同时在第二个线程中)。共享锁可以很容易地解决这个问题。
  3. 实施监视器。例如,一个简单的队列,其中 put 和 get 方法从许多其他线程执行。但是,您不希望同时运行多个 put(或 get)方法,也不希望 put 和 get 方法同时运行。私人锁使您的生活更容易实现这一目标。

而锁和条件建立在同步机制之上。因此,当然可以实现与使用锁可以实现的功能相同的功能。但是,使用同步解决复杂场景可能会让您的生活变得困难,并且可能会偏离解决实际问题。

【讨论】:

    【解决方案2】:

    锁和同步的主要区别:

    • 使用锁,您可以按任意顺序释放和获取锁。
    • 通过同步,您只能按照获取锁的顺序释放锁。

    【讨论】:

      【解决方案3】:

      锁定和同步块都具有相同的目的,但取决于使用情况。考虑以下部分

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

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

      【讨论】:

        【解决方案4】:

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

        synchronized(foo) {
        }
        

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

        不保证公平。

        如果您对锁有很多争用,您很容易遇到新请求获得锁而旧请求卡住的闯入。我见过这样的情况:200 个线程在短时间内到达一个锁,而第二个到达的线程最后得到处理。这对于某些应用程序来说是可以的,但对于其他应用程序来说却是致命的。

        有关此主题的完整讨论,请参阅 Brian Goetz 的“Java Concurrency In Practice”一书第 13.3 节。

        【讨论】:

        • "方法级同步确保锁的公平或先进先出分配。" => 真的吗?您是说同步方法的行为与 w.r.t 不同吗?比将方法内容包装到同步{}块中更公平吗?我不这么认为,还是我把那句话理解错了……?
        • 是的,虽然令人惊讶且违反直觉,但这是正确的。 Goetz 的书是最好的解释。
        • 如果您查看@BrianTarbox 提供的代码,同步块正在使用“this”以外的其他对象进行锁定。理论上,同步方法与将所述方法的整个主体放在同步块中没有区别,只要该块使用“this”作为锁。
        • 应编辑答案以包含报价,并明确“保证”这里是“统计保证”,而不是确定性。
        【解决方案5】:

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

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

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

        锁定同步的优点来自文档page

        1. 同步方法或语句的使用提供了对与每个对象关联的隐式监控锁的访问,但强制所有锁的获取和释放以块结构的方式发生

        2. 锁实现通过提供获取lock (tryLock()) 的非阻塞尝试、获取可中断锁的尝试(lockInterruptibly() 和尝试获取可以timeout (tryLock(long, TimeUnit))的锁。

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

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

        ReentrantLockarticle 的主要功能

        1. 能够中断锁定。
        2. 能够在等待锁定时超时。
        3. 创建公平锁的权力。
        4. API 获取等待锁的线程列表。
        5. 灵活地尝试锁定而不阻塞。

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

        除了这三个 ReentrantLocks,java 8 还提供了一个锁

        StampedLock:

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

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

        看看这个article 了解不同类型的ReentrantLockStampedLock 锁的用法。

        【讨论】:

          【解决方案6】:

          您可以使用 synchronizedvolatilewait / notify 等低级原语实现 java.util.concurrent 中的实用程序所做的所有事情

          但是,并发性很棘手,而且大多数人至少在其中的某些部分会出错,从而导致他们的代码不正确或效率低下(或两者兼而有之)。

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

          Lock 类本身位于此工具箱的较低级别,您甚至可能不需要直接使用它(您可以使用 QueuesSemaphore 等等,大多数时候)。

          【讨论】:

          • 普通的等待/通知是否被认为是比 java.util.concurrent.locks.LockSupport 的 park/unpark 更低级别的原语,还是相反?
          • @Pacerier:我认为两者都是低级的(即应用程序程序员希望避免直接使用的东西),但肯定是 java.util.concurrency 的低级部分(例如locks 包)建立在原生 JVM 原语等待/通知(甚至更低级别)之上。
          • 不,我的意思是在 3 个中:Thread.sleep/interrupt、Object.wait/notify、LockSupport.park/unpark,哪个是原语?
          • @Thilo 我不确定你如何支持你的说法,即java.util.concurrent [通常] 比语言功能(synchronized 等)更容易。当您使用java.util.concurrent 时,您必须养成在编写代码之前完成lock.lock(); try { ... } finally { lock.unlock() } 的习惯,而使用synchronized 您基本上从一开始就很好。仅在此基础上,我会说synchronizedjava.util.concurrent.locks.Lock 更容易(假设你想要它的行为)。 par 4
          • 不要认为仅使用并发原语就可以完全复制 AtomicXXX 类的行为,因为它们依赖于 java.util.concurrent 之前不可用的本机 CAS 调用。
          【解决方案7】:

          您想要使用synchronizedjava.util.concurrent.Lock 的原因有4 个主要因素。

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

          1. 当 Java 5 与 ReentrantLocks,他们证明有 相当明显的吞吐量 差异然后是内在锁定。 如果您正在寻找更快的锁定 机制并正在运行 1.5 考虑 j.u.c.ReentrantLock。爪哇 6 的内在锁定现在是 可比。

          2. j.u.c.Lock 具有不同的机制 用于锁定。锁定可中断 - 尝试锁定直到锁定 线程被中断;定时锁—— 尝试锁定一定数量 时间,如果你不放弃 成功; tryLock - 尝试锁定, 如果其他线程持有 锁放弃。这一切都包括在内 除了简单的锁。 内在锁定只提供简单的 锁定

          3. 风格。如果 1 和 2 都没有下降 归入你的类别 与大多数人有关, 包括我自己,会发现 内在锁定语义学更容易 阅读并且不那么冗长 j.u.c.Lock 锁定。
          4. 多个条件。你的一个对象 lock on 只能被通知和 等待一个案例。锁的 newCondition 方法允许一个 单锁有多重原因 等待或发出信号。我还没有 实际上需要这个功能 练习,但对于 需要的人。

          【讨论】:

          • 我喜欢您评论中的详细信息。我还要添加一个要点——如果您正在处理多个线程,ReadWriteLock 提供了有用的行为,其中只有一些线程需要写入对象。多个线程可以同时读取对象,并且只有在另一个线程已经在写入时才会被阻塞。
          • 补充第四点 - 在 j.u.c.ArrayBlockingQueue 中,锁有两个等待原因:队列非空和队列非满。出于这个原因,j.u.c.ArrayBlockingQueue 使用了显式锁和 lock.newCondition()。
          【解决方案8】:

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

          我发现LockCondition(以及其他新的concurrent 类)只是工具箱的更多工具。我可以用我的旧羊角锤(synchronized 关键字)做大部分我需要的事情,但在某些情况下使用起来很尴尬。一旦我在我的工具箱中添加了更多工具:橡胶槌、圆头锤、撬杆和一些钉子拳,其中一些尴尬的情况变得更加简单。 然而,我的旧羊角锤仍然有它的用处。

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

          ===

          我认为JavaDoc 很好地描述了Locksynchronized 之间的区别(重点是我的):

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

          ...

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

          虽然同步方法和语句的作用域机制使得使用监视器锁进行编程变得更加容易,并且有助于避免许多涉及锁的常见编程错误,在某些情况下您需要以更灵活的方式使用锁。例如,**一些算法*用于遍历并发访问的数据结构需要使用“hand-over-hand”或“chain locks” strong>:获取节点A的锁,然后获取节点B,然后释放A获取C,释放B获取D,以此类推。 Lock 接口 的实现通过允许在不同范围内获取和释放锁 来实现此类技术的使用>,以及允许以任意顺序获取和释放多个锁

          增加的灵活性带来了额外的责任没有块结构的锁定消除了同步方法和语句中发生的锁的自动释放。在大多数情况下,应使用以下成语:

          ...

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

          锁实现通过提供非阻塞获取尝试 一个锁 (tryLock()),尝试获取可中断的锁 (lockInterruptibly(),并尝试获取可以超时的锁 (tryLock(long, TimeUnit))。

          ...

          【讨论】:

            【解决方案9】:

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

            【讨论】:

              【解决方案10】:

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

              例子:

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

              你必须在任何地方明确地做try{} finally{}

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

              synchronized(myObject) {
                  doSomethingNifty();
              }
              

              也就是说,Locks 对于无法以如此干净的方式获取和释放的更复杂的事情可能更有用。老实说,我宁愿首先避免使用裸 Locks,而只使用更复杂的并发控制,例如 CyclicBarrierLinkedBlockingQueue,如果它们满足您的需求。

              我从来没有理由使用wait()notify(),但可能有一些好的。

              【讨论】:

              • LockSupport 的 wait/notify 与 park/unpark 有什么区别? docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/…
              • 起初这个例子对锁来说是有意义的,但后来我意识到如果你使用 try finally 块,这个问题可以避免,因为锁没有被释放
              • 啊...欣赏 C++ 中的 RAII 模型的时刻之一。 std::lock_guard
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多