352

我试图了解是什么让并发锁如此重要,如果可以使用synchronized (this). 在下面的虚拟代码中,我可以执行以下任一操作:

  1. 同步整个方法或同步易受攻击的区域 ( synchronized(this){...})
  2. 或使用 ReentrantLock 锁定易受攻击的代码区域。

代码:

    private final ReentrantLock lock = new ReentrantLock(); 
    private static List<Integer> ints;

    public Integer getResult(String name) { 
        .
        .
        .
        lock.lock();
        try {
            if (ints.size()==3) {
                ints=null;
                return -9;
            }   

            for (int x=0; x<ints.size(); x++) {
                System.out.println("["+name+"] "+x+"/"+ints.size()+". values >>>>"+ints.get(x));
            }

        } finally {
            lock.unlock();
        } 
        return random;
}
4

8 回答 8

522

与构造不同,ReentrantLock非结构化synchronized——也就是说,您不需要使用块结构进行锁定,甚至可以跨方法持有锁定。一个例子:

private ReentrantLock lock;

public void foo() {
  ...
  lock.lock();
  ...
}

public void bar() {
  ...
  lock.unlock();
  ...
}

这种流不可能通过构造中的单个监视器来表示synchronized


除此之外,还ReentrantLock支持锁轮询支持超时的可中断锁等待ReentrantLock还支持可配置的公平策略,允许更灵活的线程调度。

此类的构造函数接受一个可选的公平参数。设置时true,在争用情况下,锁有利于授予对等待时间最长的线程的访问权限。否则,此锁不保证任何特定的访问顺序。使用由许多线程访问的公平锁的程序可能会显示出比使用默认设置的程序更低的整体吞吐量(即,更慢;通常要慢得多),但在获取锁和保证不会出现饥饿的情况下具有较小的时间差异。但是请注意,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的许多线程之一可能会连续多次获得它,而其他活动线程没有进展并且当前没有持有锁。另请注意,不定时tryLock方法不遵守公平设置。如果锁可用,即使其他线程正在等待,它也会成功。


ReentrantLock 可能更具可扩展性,在更高的争用下表现得更好。您可以在此处阅读有关此内容的更多信息。

然而,这一说法受到了质疑;请参阅以下评论:

在重入锁测试中,每次都会创建一个新锁,因此没有排他锁,结果数据无效。此外,IBM 链接没有提供底层基准测试的源代码,因此无法确定测试是否正确进行。


什么时候应该使用ReentrantLocks?根据那篇 developerWorks 文章...

答案很简单——当你真正需要它提供的东西时使用它synchronized,比如定时锁等待、可中断锁等待、非块结构锁、多个条件变量或锁轮询。ReentrantLock也有可扩展性的好处,如果你确实有一个表现出高争用的情况,你应该使用它,但请记住,绝大多数synchronized块几乎没有表现出任何争用,更不用说高争用了。我建议使用同步进行开发,直到同步被证明是不够的,而不是简单地假设“性能会更好”,如果你使用ReentrantLock. 请记住,这些是高级用户的高级工具。(而且真正高级的用户往往更喜欢他们能找到的最简单的工具,直到他们确信这些简单的工具是不够的。)和往常一样,先把它做好,然后再考虑是否必须让它更快。


在不久的将来会变得更加相关的最后一个方面与Java 15 和 Project Loom有关。ReentrantLock在虚拟线程的(新)世界中,底层调度程序将能够比它能够更好地工作synchronized,至少在最初的 Java 15 版本中是这样,但以后可能会进行优化。

在当前的 Loom 实现中,可以在两种情况下固定虚拟线程:当堆栈上有一个本地框架时——当 Java 代码调用本地代码 (JNI),然后再回调到 Java 时——以及在synchronized块或方法内部时. 在这些情况下,阻塞虚拟线程将阻塞承载它的物理线程。一旦本机调用完成或释放监视器(synchronized退出块/方法),线程将被取消固定。

如果您有一个由 a 保护的常见 I/O 操作synchronized,请将监视器替换为 aReentrantLock以让您的应用程序充分受益于 Loom 的可扩展性提升,甚至在我们修复监视器固定之前(或者,StampedLock如果可以的话,最好使用更高性能) .

于 2012-08-06T02:29:48.763 回答
14

ReentrantReadWriteLock是专用锁,而是synchronized(this)通用锁。它们相似但不完全相同。

你是对的,你可以使用synchronized(this)代替,ReentrantReadWriteLock但相反的情况并不总是正确的。

如果您想更好地了解什么是ReentrantReadWriteLock特殊的,请查看有关生产者-消费者线程同步的一些信息。

一般来说,您可以记住,全方法同步和通用同步(使用synchronized关键字)可以在大多数应用程序中使用,而无需过多考虑同步的语义,但是如果您需要从代码中挤出性能,则可能需要探索其他更细粒度或专用同步机制。

顺便说一句,使用synchronized(this)- 并且通常使用公共类实例锁定 - 可能会出现问题,因为它会使您的代码面临潜在的死锁,因为其他人可能会在不知情的情况下尝试锁定程序中其他地方的对象。

于 2012-08-06T02:10:55.810 回答
13

来自关于ReentrantLock的 oracle 文档页面:

一种可重入互斥锁,其基本行为和语义与使用同步方法和语句访问的隐式监视器锁相同,但具有扩展功能。

  1. ReentrantLock由上次成功锁定但尚未解锁的线程拥有。当锁不被另一个线程拥有时,调用锁的线程将返回,成功获取锁。如果当前线程已经拥有锁,该方法将立即返回。

  2. 此类的构造函数接受一个可选的公平参数。当设置为 true 时,在争用情况下, 锁有利于授予对等待时间最长的线程的访问权限。否则,此锁不保证任何特定的访问顺序。

根据本文的ReentrantLock关键功能

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

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

看看Benjamen的这篇文章,了解不同类型的ReentrantLocks的用法

于 2016-04-24T18:29:07.177 回答
3

同步锁不提供任何等待队列机制,在该机制中,在一个线程执行后,任何并行运行的线程都可以获取锁。因此,系统中存在并运行较长时间的线程永远不会有机会访问共享资源,从而导致饥饿。

可重入锁非常灵活并且有一个公平策略,如果一个线程等待更长的时间并且在当前正在执行的线程完成后,我们可以确保更长等待的线程有机会访问共享资源,从而减少系统的吞吐量并使其更加耗时。

于 2018-10-06T12:34:41.897 回答
2

您可以使用带有公平策略或超时的可重入锁来避免线程饥饿。您可以应用线程公平策略。这将有助于避免线程永远等待获取您的资源。

private final ReentrantLock lock = new ReentrantLock(true);
//the param true turns on the fairness policy. 

“公平政策”选择下一个可运行的线程来执行。它基于优先级,自上次运行以来的时间,等等

另外,如果 Synchronize 无法逃脱阻塞,它可以无限期阻塞。可重入锁可以设置超时。

于 2016-06-18T18:59:15.150 回答
1

要记住的一件事是: “ ReentrantLock

” 这个名称给出了关于其他锁定机制的错误信息,即它们不能重入。这不是真的。通过“同步”获取的锁在 Java 中也是可重入的。

关键区别在于“同步”使用内部锁(每个 Object 都有),而 Lock API 没有。

于 2019-05-22T05:15:05.560 回答
0

让我们假设这段代码在一个线程中运行:

private static ReentrantLock lock = new ReentrantLock();

void accessResource() {
    lock.lock();
    if( checkSomeCondition() ) {
        accessResource();
    }
    lock.unlock();
}

因为线程拥有锁,所以它允许多次调用lock(),所以它重新进入锁。这可以通过引用计数来实现,因此它不必再次获取锁。

于 2018-11-14T07:39:39.207 回答
0

我认为 wait/notify/notifyAll 方法不属于 Object 类,因为它使用很少使用的方法污染了所有对象。它们在专用的 Lock 类上更有意义。所以从这个角度来看,也许最好使用专门为手头工作而设计的工具——即ReentrantLock。

于 2020-04-16T01:02:17.597 回答