如果可以使用 synchronized(this),为啥还要使用 ReentrantLock?

Posted

技术标签:

【中文标题】如果可以使用 synchronized(this),为啥还要使用 ReentrantLock?【英文标题】:Why use a ReentrantLock if one can use synchronized(this)?如果可以使用 synchronized(this),为什么还要使用 ReentrantLock? 【发布时间】:2012-08-03 00:59:53 【问题描述】:

如果可以使用synchronized (this),我试图了解是什么让并发锁定如此重要。在下面的虚拟代码中,我可以做到:

    同步整个方法或同步漏洞区域(synchronized(this)...) 或使用 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;

【问题讨论】:

顺便说一句,所有 java 内在锁本质上都是可重入的。 @pongapundit 所以synchronized(this)synchronized(this)//some code 不会导致死锁。对于内在锁,如果他们获得对资源的监视器,并且如果他们再次想要它,他们可以在没有死锁的情况下获得它。 object.lock;......;object.unlock 等于 synchronized(this.class) 它是类级别的锁而不是对象级别的 【参考方案1】:

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

private ReentrantLock lock;

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


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

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


除此之外,ReentrantLock 还支持lock polling 和interruptible lock waits that support time-out。 ReentrantLock 还支持configurable fairness policy,允许更灵活的线程调度。

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


ReentrantLock 可能也可以是more scalable,在更高的竞争下表现更好。你可以阅读更多关于这个here的信息。

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

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


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

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


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

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

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

【讨论】:

应该删除指向 lycog.com 的“已知更具可扩展性”链接。在重入锁测试中,每次都会创建一个新锁,因此没有排他锁,结果数据无效。此外,IBM 链接没有提供底层基准测试的源代码,因此无法确定测试是否正确进行。就个人而言,我只是删除了关于可扩展性的整条线,因为整个声明基本上不受支持。 我根据您的回复修改了帖子。 如果性能是您高度关注的问题,请不要忘记寻找一种完全不需要同步的方法。 性能这件事对我来说根本没有意义。如果可重入锁的性能更好,那么为什么不同步不只是在内部以与可重入锁相同的方式实现? @user2761895 Lycog 链接中的ReentrantLockPseudoRandom 代码在每次调用setSeednext 时都使用全新的非竞争锁【参考方案2】:

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

【讨论】:

【参考方案3】:

要记住的一点是: 名称“ReentrantLock”给出了关于其他锁定机制的错误消息,即它们不是可重入的。 这不是真的。 通过“同步”获取的锁在 Java 中也是可重入的。

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

【讨论】:

【参考方案4】:

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

您是对的,您可以使用synchronized(this) 而不是ReentrantReadWriteLock,但并非总是如此。

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

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

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

【讨论】:

为了防止潜在的死锁,因为其他人可能会在游戏中的其他地方尝试锁定您的对象,使用私有对象实例作为同步监视器,如下所示:public class MyLock private final Object protectedLongLockingMonitor = new Object(); private long protectedLong = 0L; public void incrementProtectedLong() synchronized(protectedLongLockingMonitor) protectedLong++; 【参考方案5】:

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

private static ReentrantLock lock = new ReentrantLock();

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

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

【讨论】:

synchronized 块具有完全相同的重入行为(引用计数)。这不是ReentrantLock 的优点/特点之一。【参考方案6】:

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

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

【讨论】:

【参考方案7】:

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

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

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

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

ReentrantLock 这个article的关键功能

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

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

看看 Benjamen 的 article 了解不同类型的 ReentrantLocks 的用法

【讨论】:

【参考方案8】:

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

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

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

还有, Synchronize 如果无法逃脱阻塞,则可以无限阻塞。可重入锁可以设置超时。

【讨论】:

以上是关于如果可以使用 synchronized(this),为啥还要使用 ReentrantLock?的主要内容,如果未能解决你的问题,请参考以下文章

[MethodImpl(MethodImplOptions.Synchronized)]lock(this)与lock(typeof(...))

synchronized(this)与synchronized(class)(转载)

java 并发synchronized使用

synchronized修饰普通方法和静态方法

synchronized 加在java方法前面是啥作用

[Java] synchronized在代码块中修饰.class与this的区别