第十三章、显式锁
发布日期:2021-09-12 09:58:00 浏览次数:44 分类:技术文章

本文共 9709 字,大约阅读时间需要 32 分钟。

文章目录

显式锁

java5.0以前访问共享对象使用的机制只有synchronized和volatile。java5.0后提供了一种新的机制:ReentrantLock。ReentrantLock并不是代替内置加锁方法,而是当内置锁满足不了需求时,作为一种可高端的选择。

一、Lock与ReentrantLock

Lock接口中定义了一种无条件、可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显式的。其中tryLock();是轮询锁通过释放已获得的锁,并退回重新尝试获取所有锁,tryLock(long timeout, TimeUnit unit) 是通过定时释放已获得的锁,放弃本次操作。

public interfece Lock{
void lock();//显式加锁 void lockInterruptibly() throws InterruptedException; boolean tryLock();//轮询锁通过释放已获得的锁,并退回重新尝试获取所有锁 boolean tryLock(long timeout, TimeUnit unit) throw InterruptedException;//定时锁获取 void unlock(); Condition newCondition();}

为什么要创建一种与内置锁如此相似的加锁机制?

大多情况下,内置锁能很好的工作,但在功能上仍存在一些局限性。例如:无法中断一个正在等待获取锁的线程,或者无法在请求获取一个锁时无限等待下去(tryLock)。内置锁在获取的过程中无法中断。内置锁必须在获取该锁的代码块中释放,无法实现非阻塞结构的加锁规则,很难实现带有时间限制的操作。(Lock接口中对应每个方法就是解决内置锁不足)

下面给出了Lock接口的标准的使用方法。必须在finally中释放锁,否则,如果在被保护的代码中抛出了异常,那么这个锁将永远无法释放。

Lock lock = new ReentrantLock();...lock.lock();try {
// 更新对象状态 // 捕获异常,并在必要时恢复不变性条件} finally {
lock.unlock();//一定要记得在finally块里释放}

如果没有在finally中释放锁,那么相当于启动了一个定时炸弹,程序不会自动清除锁,千万不要忘记。

1、轮询锁和定时锁(避免死锁发生)

可定时的与可轮询的锁获取模式是由tryLock方法实现的,是除了顺序获得锁之外的一个新的避免死锁的方式

如果不能获得所有需要的锁,那么可以使用可定时的或者可轮询的锁获取方式,从而使你重新获得控制权,它会释放已经获得的锁,然后重新尝试获取所有锁。

public class DeadlockAvoidance {
private static Random rnd = new Random(); public boolean transferMoney(Account fromAcct, Account toAcct, DollarAmount amount, long timeout, TimeUnit unit) throws InsufficientFundsException, InterruptedException {
long fixedDelay = getFixedDelayComponentNanos(timeout, unit); long randMod = getRandomDelayModulusNanos(timeout, unit); long stopTime=System.nanoTime()+unit.toNanos(timeout); //使用tryLock来获取两个锁,如果不能同时获得,那么就回退并重新尝试 while(true){
if(fromAcct.lock.tryLock()){
//使用tryLock来获取锁 try{
if(toAcct.lock.tryLock()){
try{
if(fromAcct.getBalance().compareTo(amount)<0) throw new InsufficientFundsException(); else{
fromAcct.debit(amount); toAcct.credit(amount); return true; } }finally{
toAcct.lock.unlock(); } } }finally{
fromAcct.lock.unlock(); //无论成功与否都会释放所有锁 } } //如果在指定时间内不能获得所有需要的锁,那么transferMoney将返回一个失败状态,从而使该操作平缓地失败。 if(System.nanoTime()
{
public int compareTo(DollarAmount other) {
return 0; } DollarAmount(int dollars) {
} } class Account {
public Lock lock; void debit(DollarAmount d) {
} void credit(DollarAmount d) {
} DollarAmount getBalance() {
return null; } } class InsufficientFundsException extends Exception {
}}

在实现具有时间限制的操作时,定时锁非常有用。 当在带有时间限制的操作中调用了一个阻塞方法时,它能根据剩余时间来提供一个时限。如果操作不能在指定时间内给出结果,那么程序就会提前结束。 当使用内置锁时,在开始请求锁后,这个操作将无法取消,因此内置锁很难实现带有时间限制的操作。

例子:带有时间限制的锁操作例子。

public class TimedLocking {
private Lock lock = new ReentrantLock(); //定时的tryLock能够在这个带有时间限制的操作中实现独占加锁行为。 public boolean trySendOnSharedLine(String message, long timeout,TimeUnit unit) throws InterruptedException{
long nanosToLock=unit.toNanos(timeout) -estimatedNanosToSend(message); if(!lock.tryLock(nanosToLock,NANOSECONDS)) //如果不能再指定时间内获得锁,就失败 return false; try{
return sendOnSharedLine(message); }finally {
lock.unlock(); } } private boolean sendOnSharedLine(String message) {
//传送信息 return true; } long estimatedNanosToSend(String message) {
return message.length(); } }

2、可中断的锁获取操作(lockInterruptibly方法)

可中断的锁获取操作能在可取消的操作中使用加锁。 第七章中给出了几种不能响应中断的机制,例如请求内置锁。这些不可中断的阻塞机制将使的实现可取消的任务变得复杂。 lockInterruptibly方法能够在获得锁的同时保持对中断的响应,并且由于它包含在Lock中,因此无需创建其他类型的不可中断阻塞机制。

定时的tryLock同样能响应中断,因此当需要一个定时的和可中断的锁获取操作时,可以使用tryLock方法。

//   13-5   可中断的锁获取操作public class InterruptibleLocking {
private Lock lock = new ReentrantLock(); public boolean sendOnSharedLine(String message) throws InterruptedException {
lock.lockInterruptibly(); try {
return cancellableSendOnSharedLine(message); } finally {
lock.unlock(); } } private boolean cancellableSendOnSharedLine(String message) throws InterruptedException {
/* send something */ return true; }}

3、非块结构的加锁

在内置锁中,锁的获取和释放等操作都是基于代码块的——释放锁的操作总是与获取锁的操作处于同一个代码块,而不考虑控制权如何退出该代码块,可以避免可能的编码错误,但有时候需要更加灵活的加锁规则。

  在第11章中,通过降低锁的粒度提高了代码的可伸缩性。锁分段技术在基于散列的容器中实现了不同的散列链,以便使用不同的锁。
  我们可以采用类似原则来降低链表中锁的粒度,为每个链表节点使用一个独立的锁,使不同的线程能独立地对链表的不同部分进行操作。每个节点的锁将保护连接指针以及在该节点中存储的数据,因此当遍历或修改链表时,我们必须持有该节点上的这个锁,直到获得了下一个节点的锁,这样我们才能释放上一个节点的锁。

二、性能考虑因素

竞争性能是可伸缩性的关键要素:如果有越多的资源被消耗在锁的管理和调度上,那么应用程序可以得到的资源就越少。锁的实现方式越好,系统调用和上下文切换消耗的资源越少,在共享的内存总线的内存同步通信量也越少。

java5.0刚出显式锁时,ReentrantLock确实极大的与内置锁体现出吞吐率的差距,ReentrantLock能提供更高的吞吐量。但到了java6中,内置锁的性能得到极大改善,性能并不会由于竞争而急剧下降,并且与ReentrantLock可伸缩性基本相当。

二、公平性(公平锁和非公平锁)

在ReentrantLock的构造函数中提供了两种公平性选择:创建一个非公平的锁(默认)或者一个公平的锁。 在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许“插队”:当一个线程请求非公平的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有的等待线程并获得这个锁(在Semaphore中同样可以选择采用公平或非公平的获取顺序)。

  在激烈竞争的情况下,非公平锁的性能高于公平锁,其中的一个原因时:在恢复一个被挂起的线程与该线程真正开始运行之间存在着严重的延迟。 假设线程A持有一个锁,并且线程B请求这个锁。由于A持有这个锁,因此B挂起。当A释放锁时,B将被唤醒,因此会再次尝试获取锁。此时,如果C也请求这个锁,那么C很可能在B被完全唤醒之前获得,使用及释放这个锁。 这是一种“双赢”的局面:B获得锁的时刻并没有推迟,C更早地获得了锁,并且吞吐量也获得了提高。
  当持有锁的时间相对较长,或者请求锁的平均时间间隔较长,那么应该使用公平锁。

四、在Synchronized和ReentrantLock之间进行选择

在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的,可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用synchronized。

  
内置锁相比ReentrantLock优点在于:

  • 内置锁在线程转储中能给出哪些帧中获得了哪些锁,并能识别和检测发生死锁的线程。而ReentrantLock在java5.0时还不知道哪些线程持有ReentrantLock,但在java6.0中提供了一个接口,通过对接口注册可以访问ReentrantLock的加锁信息。
  • 内置锁自动加锁与释放锁,ReentrantLock需要在finally中手动释放锁。

五、读-写锁(ReentrantLock)

ReentrantLock和内置锁相同属于互斥锁,每次最多只能有一个线程持有ReentrantLock。互斥是一种保守的加锁策略,虽然可以避免“写/写”冲突和“写/读”冲突,但是也避免了“读/读”冲突,在许多情况下大多数的操作都是读操作,那么互斥这一保守的加锁策略会影响并发的读取性能。

  如果能够放宽加锁需求,允许多个执行读操作的线程同时访问数据结构,那么将提升程序的性能。只要每个线程都能确保读取到最新的数据,并且在读取数据时不会有其他线程修改数据,那么就不会发生问题。

public interface ReadWriteLock {
Lock readLock(); Lock writeLock();}

读写锁ReadWriteLock在读取锁和写入锁之间的交互可以采用多种实现方式。其中的实现需要考虑以下的问题:

  • 释放优先 :当一个写入操作释放写入锁时,并且队列中同时存在读线程和写线程,那么应该优先选择读线程、写线程、还是最先发出请求的线程?
  • 读线程插队 :如果锁是由读线程持有,但有写线程正在等待,那么新达到的读线程能否立即获得访问权,还是应该在写线程后面等待?如果允许读线程插队到写线程之前,那么将提高并发性,但却可能造成写线程发生饥饿问题。
  • 重入性 :读取锁和写入锁释放可重入?
  • 降级 :如果一个线程持有写入锁,那么它能否在不释放该锁的情况下获得读取锁?这可能会使得写入锁被降级为读取锁,同时不允许其他写线程修改被保护的资源,
  • 升级 :读取锁能够优先于其他正在等待的读线程和写线程而升级为一个写入锁?

与ReentrantLock类似,ReentrantReadWriteLock在构造的时候可以选择是一个非公平的锁(默认)还是一个公平的锁。在公平的锁中,等待时间最长的线程将优先获得锁。如何这个锁被读线程持有,而另外一个线程请求写入锁,那么其他读线程都不能获得锁,知道写线程使用完并且释放了写入锁。

写线程降级为读线程是可以的,但是从读线程升级为写线程则是不可以的。

import java.util.HashMap;import java.util.Map;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.ReentrantReadWriteLock;/** * 多线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。 * 但是 * 如果有一个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或者写。 * 小总结 *  读读能并存 *  读写不能并存 *  写写不能并存 * *  写操作:原子+独占,整个过程必须是一个完整的同一体,中间不许被分割被打断。 * */class MyCache //资源类{
private volatile Map
map = new HashMap<>(); private ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock(); public void put(String key,Object value) {
rwlock.writeLock().lock(); try {
System.out.println(Thread.currentThread().getName()+"\t 正在写入:"+key); try {
TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) {
e.printStackTrace(); } map.put(key, value); System.out.println(Thread.currentThread().getName()+"\t 写入完成"); } catch (Exception e) {
e.printStackTrace(); } finally {
rwlock.writeLock().unlock(); } } public void get(String key) {
rwlock.readLock().lock(); try {
System.out.println(Thread.currentThread().getName()+"\t 正在读取:"); try {
TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) {
e.printStackTrace(); } Object result = map.get(key); System.out.println(Thread.currentThread().getName()+"\t 读取完成:"+result); } catch (Exception e) {
e.printStackTrace(); } finally {
rwlock.readLock().unlock(); } }}public class ReadWirteLockDemo{
public static void main(String[] args) {
MyCache myCache = new MyCache(); for (int i=1;i<=5;i++) {
final int tempInt = i; new Thread(()->{
myCache.put(tempInt+"",tempInt+""); },String.valueOf(i)).start(); } for (int i=1;i<=5;i++) {
final int tempInt = i; new Thread(()->{
myCache.get(tempInt+""); },String.valueOf(i)).start(); } }}

小结:与内置锁相比,显式的Lock在处理锁上更加灵活,但是ReentrantLock不能完全替代synchronized。当访问被保护对象以读取操作为主,那么读/写锁才能提高程序的可伸缩性

转载地址:https://blog.csdn.net/weixin_38367817/article/details/103890694 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:计算机网络-第一章之计算机网络概述
下一篇:第十章、避免活跃性危险

发表评论

最新留言

很好
[***.229.124.182]2024年04月16日 16时41分45秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章

银河麒麟在VMware虚拟机中如何更改窗口分辨率大小? 2019-04-27
数据库外键使用所造成的影响有哪些? 2019-04-27
每天自我提升的8个好习惯 2019-04-27
jquery.nicescroll参数说明 2019-04-27
windows查看指定的端口是否开放 2019-04-27
微信小程序开发(三)——IE盒子,Flex弹性布局,色子六面 2019-04-27
Python 技术篇-pip安装tar.gz格式的离线资源包 2019-04-27
windows 技术篇-将本地主机加入域的方法实例演示 2019-04-27
Python 图像处理篇-利用opencv库展示本地图片实例演示 2019-04-27
Python 图像处理篇-利用opencv库和numpy库读取包含中文路径下的本地图片实例演示 2019-04-27
oracle 数据回滚,恢复误删的数据,闪回表功能的使用 2019-04-27
mac 系统新功能体验-根据时间变化的动态桌面背景,看壁纸演绎风景大片中的日出与日落 2019-04-27
ADB的安装和使用教程,小米手机连接adb实例演示 2019-04-27
windows 关闭粘滞键-解决Microsoft Remote Desktop输入自动变为快捷键问题 2019-04-27
测试工具 - Postman接口测试入门使用手册,Postman如何进行数据关联、自动更新cookies、简单编程 2019-04-27
PyQt5 技术篇-调用字体对话框(QFontDialog)获取字体,控件设置字体。 2019-04-27
Python 技术篇-将python项目打包成exe独立运行程序,pyinstaller库打包python代码实例演示 2019-04-27
Geany 权限问题:"Error opening file ... : permission denied.",原因及解决办法。 2019-04-27
CSDN博客主页增加赞赏码收钱模块,高端大气上档次! 2019-04-27
PyQt5 技术篇-调用文件对话框获取文件、文件夹路径。文件对话框返回选中的多个文件路径 2019-04-27