繁体   English   中英

如果可以使用 synchronized(this),为什么要使用 ReentrantLock?

[英]Why use a ReentrantLock if one can use synchronized(this)?

如果可以使用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;
}

ReentrantLock非结构化的,不像synchronized结构——也就是说,你不需要使用块结构来锁定,甚至可以跨方法持有一个锁。 一个例子:

private ReentrantLock lock;

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

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

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


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

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


ReentrantLock可能更具可扩展性,更高的竞争下进行好得多。 您可以在此处阅读更多相关信息。

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

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


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

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


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

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

如果您有一个由synchronized保护的公共 I/O 操作,请用ReentrantLock替换监视器,即使在我们修复监视器固定之前,您的应用程序也可以充分受益于 Loom 的可扩展性提升(或者,更好的是,使用更高性能的StampedLock如果你可以)。

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

你是对的,你可以使用synchronized(this)而不是ReentrantReadWriteLock但相反的情况并不总是如此。

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

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

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

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

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

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

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

根据本文的ReentrantLock关键功能

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

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

看看 Benjamen 关于不同类型ReentrantLocks 的使用的这篇文章

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

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

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

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

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

此外,如果无法逃脱阻止,Synchronize 可以无限期阻止。 Reentrantlock 可以设置超时。

要记住的一件事是:

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

主要区别在于“同步”使用内在锁(每个对象都有),而 Lock API 没有。

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

private static ReentrantLock lock = new ReentrantLock();

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM