java多线程并发系列之锁的深入了解

时间:2020-12-13 17:31:50


上一篇博客中 : 

java多线程、并发系列之 (synchronized)同步与加锁机制

。介绍了java中Synchronized和简单的加锁机制,在加锁的模块中介绍了 轮询锁和定时锁,简单回顾下

  • 轮询锁:利用tryLock来获取两个锁,如果不能同时获得,那么回退并重新尝试。

  • 定时锁:索取锁的时候可以设定一个超时时间,如果超过这个时间还没索取到锁,则不会继续堵塞而是放弃此次任务。

锁的公平性

在公平的锁上,线程将按照它们发出请求的顺序来获取锁

上面似乎忘记了还有一种可中断锁和可选择粒度锁

可中断锁

正如定时的锁获取操作能在带有时间限制的操作中使用独占锁,可中断的锁获取操作同样能在可以取消的操作中使用加锁。lockInterruptibly方法能够在获得锁的同时保持对中断的响应,并且由于它包含在Lock中,因此无须创建其他类型的不可中断阻塞机制。

可中断的锁获取操作的标准结构比普通的锁获取操作略复杂一些,,因为需要两个try块(如果在可中断的锁获取操作中抛出InterruptedException,那么可以使用标准的try-finally加锁模式)。

如下程序中使用了lockInterruptibly来实现程序中的sendOnShareLine,以便在一个可取消的任务中调用他。


public boolean sendOnSharedLine(String message)
throws InterruptedException{
lock.lockInterruptibly();

try
{
return cancellableSendOnSharedLine(message);
}finally
{
lock.unlock();
}
}

private boolean cancellableSendOnSharedLine(String message)
throws InterruptedException{...}

性能考虑(内置锁和显示锁)

在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的Lock对象,性能更高一些。Brian Goetz对这两种锁在JDK1.5、单核处理器及双Xeon处理器环境下做了一组吞吐量对比的实验,发现多线程环境下,synchronized的吞吐量下降的非常严重,而ReentrankLock则能基本保持在同一个比较稳定的水平上。但与其说ReetrantLock性能好,倒不如说synchronized还有非常大的优化余地,于是到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronize的性能并不比Lock差。官方也表示,他们也更支持synchronize,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

二者的性能比较如下:图来自java并发编程实战

java多线程并发系列之锁的深入了解

java多线程并发系列之锁的深入了解

锁的公平性考虑

ReentrantLock的构造函数中提供了两种公平性选择:创建一个非公平的锁或者一个公平的锁。

公平的锁

线程将按照它们发出请求的顺序来获取锁,新发出的请求线程将被放入等待队列中。

非公平的锁

在公平锁的基础上面允许插队,当一个线程获取锁时,如果在发出请求的同时,该锁的状态变为可用,那么这个线程将跳过队列中的所有等待线程并获得锁。

我们并不希望所有的锁都是公平的。虽然公平是一种好的行为,但是对于系统的效率来说并不总是好的。看这样一个例子

等待队列     
对头 1-> 2 ->3 ->4 队尾 有四个等待线程,线程0正好执行结束,此时一个线程5来了,按照公平锁的方法,那么5将会进入等待队列 1-> 2 ->3 ->4 -> 5 ,然后再从对头中取出线程1, 但是如果,采用非公平的方法,在线程5来了的同时,将线程5直接执行,跳过等待队列,那么就会减少挂起线程5和恢复线程1的开销,吞吐量获得了提升

在激烈竞争的环境中,非公平锁的性能高于公平锁的性能的一个原因是:在恢复一个被挂起线程与该线程真正开始执行之间存在着严重的延迟。

两种锁的性能比较如下图,图来自java并发编程实战

java多线程并发系列之锁的深入了解

java多线程并发系列之锁的深入了解

在Synchronized和ReentrantLock之间进行选择

由上面可知ReentrantLock在加锁和内存提供的语义上面与内置锁相同,此外它还提供了一些其他的功能,包括定时的锁等待、可中断的锁等待、公平性,以及实现非块结构的加锁,ReentrantLock在性能上似乎优于内置锁。但是,内置锁在一些场合中仍然具有很大的优势

  • 内置锁为许多开发人员熟悉,并且简洁紧凑,而且在许多现有的程序中,如果两种机制混合使用,那么不仅仅容易令人困惑,也容易发生错误
  • ReentrantLock的危险性比同步机制要高,如果忘记在finally块中调用unlock,那么虽然代码表面能够运行,但是实际上已经埋上了一颗定时炸弹,并且很有可能危机其他的代码。

什么时候用ReentrantLock:仅当内置锁不能够满足需求时,才考虑使用ReentrantLock,这些需求包括:可定时、可轮询的与可中断的锁获取,公平队列,以及非块结构的锁