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

我试图理解,如果可以使用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;
}
181669 次浏览

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

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

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

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

顺便说一下,使用synchronized(this)——以及使用公共类实例的一般锁定——可能会有问题,因为它会使你的代码陷入潜在的死锁,因为其他不知情的人可能会试图在程序中的其他地方锁定你的对象。

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

private ReentrantLock lock;


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


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

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


除此之外,ReentrantLock支持锁轮询支持超时的可中断锁等待ReentrantLock也支持可配置fairness policy,允许更灵活的线程调度。

该类的构造函数接受一个可选的公平形参。当在争用中设置true时,锁倾向于将访问权限授予等待时间最长的线程。否则,此锁不能保证任何特定的访问顺序。使用公平锁被多个线程访问的程序可能会显示较低的总体吞吐量(即更慢;通常比使用默认设置的那些慢得多),但在获得锁和保证没有饥饿方面的时间差异较小。但是请注意,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的多个线程中的一个可以连续多次获得该锁,而其他活动线程没有进展,当前也没有持有该锁。还要注意,untimed tryLock方法不支持公平性设置。如果锁可用,即使其他线程正在等待,它也会成功。


ReentrantLock 五月也是< >强更加可伸缩< / >强,在更高的争用下表现得更好。你可以阅读更多关于在这里

然而,这一说法遭到了质疑;请看下面的评论:

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


什么时候应该使用ReentrantLocks?根据developerWorks的文章…

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


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

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

如果你有一个由synchronized保护的常见I/O操作,用ReentrantLock替换监视器,让你的应用程序在我们修复监视器固定之前就充分受益于Loom的可伸缩性提升(或者,如果可以的话,更好的是使用性能更高的StampedLock)。

从oracle文档页关于ReentrantLock:

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

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

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

根据此文章 . ReentrantLock关键特性

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

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

看看Benjamen关于不同类型的下reentrantlock的使用的文章

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

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

“公平策略”选择下一个要执行的可运行线程。它是基于优先级,从上次运行到现在的时间等等

< p >, 同步如果不能脱离阻塞,可以无限阻塞。Reentrantlock可以设置超时。< / p >

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

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

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

private static ReentrantLock lock = new ReentrantLock();


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

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

有一件事要记住:

名称'ReentrantLock'给出了关于其他锁定机制的错误消息,即它们不是可重入的。这是不对的。通过'synchronized'获得的锁在Java中也是可重入的。< / p >

关键的区别是“同步”使用内在锁(每个对象都有),而锁API没有。

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