本文来源于公众号【胖滚猪学编程】 转载请注明出处!

互斥锁ReentrantLock不好用?试试读写锁ReadWriteLock一文中,我们对比了互斥锁ReentrantLock和读写锁ReadWriteLock的区别,说明了读写锁在读多写少的场景下具有明显的性能优势,但是人的欲望是无穷的,还是不能被满足。。

数据库中的锁

由于大部分码农接触锁都是从数据库中的锁开始的,所以这里不妨先聊聊数据库中的锁。

我们以火车票售票的例子,假设如下场景,两处火车票售票点同时读取某一趟列车车票数据库中的余票数量,然后两处售票点同时卖出一张车票,同时修改余票为 X -1,写回数据库,这样就造成了实际卖出两张火车票而数据库中的记录却只减少了一张。

如果你阅读了公众号【胖滚猪学编程】的并发系列文章,包括:如何解决原子性问题ReentrantLock互斥锁读写锁ReadWriteLock,那么你一定知道出现原因和解决方案,对了,可以使用锁

锁可以分为两大类,乐观锁和悲观锁:

  • 悲观锁:顾名思义,就是很悲观,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改, 所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
  • 乐观锁:乐观锁,每次去拿数据的时候想法都是“没事,肯定没被改过”,于是就开心地获取到数据,不放心吗?那就在更新的时候判断一下在此期间别人有没有去更新过这个数据,可以使用版本号等机制。

一般情况下,数据库都会有读共享写独占的锁并发的方案,也就是说读读并发是没问题的,但在读写并发时,则有可能出现读取不一致情况,也就是常说的脏读,所以在悲观锁的模式下,在有写线程的时候,是不允许有任何其他的读和写线程的,也就是说写是独占的,这样会导致系统的吞吐明显下降。我们所说的ReadWriteLock的写锁就属于悲观锁。

如何避免这一情况,答案是使用乐观锁。每个线程都不会修改原始数据,而是从原始数据上拷贝上一份数据,同时记录版本号,不同的线程更新自己的数据,在最终写会时会判断版本号是否变更,如果变更则意味有人已经更改过了,那么当前线程需要做的就是自旋重试,如果重试指定的次数依然失败,那么就应该放弃更新,这种策略仅仅适合写并发并不强烈的场景,如果写竞争严重,那么多次自旋重试的开销也是非常耗性能的,如果竞争激烈,那么写锁独占的方式则更加适合。

那么具体怎么使用版本号机制呢?

很简单,对数据库表添加了一个version字段,设置为bigint类型。查询的时候我们需要查出版本信息,更新的时候,需要将版本信息+1。

1.查询数据信息
select xxx,version from xxx where id= #{id}
2.根据数据信息是判断当前数据库中的version是否还是刚才查出来的那个version
update xxx set xxx=xxx ,version = version+1 where id=#{id} and version= #{version};

由于update指定了where条件,可根据返回修改记录条数来判断当前更新是否生效,如果成功改动0条数据,说明version发生了变更,这时候可以根据自己业务逻辑来决定是否需要回滚事务。

数据库里的乐观锁,查询的时候需要把 version 字段查出来,更新的时候要利用 version 字段做验证。这个 version 字段就类似于今天我们要说的 StampedLock 里面的 stamp。基于上面谈到的这些内容,我们再来分析StampedLock类,就会非常比较容易理解。

本文来源于公众号【胖滚猪学编程】 以漫画形式让编程so easy and interesting !转载请注明出处!

StampedLock

Java 在 1.8 这个版本里,提供了一种叫 StampedLock 的锁,它的性能比读写锁还要好。

对比ReadWriteLock

我们先来看看StampedLock 和上一篇文章讲的 ReadWriteLock 有哪些区别。

ReadWriteLock 支持两种模式:一种是读锁,一种是写锁。而 StampedLock 支持三种模式,分别是:写锁、悲观读锁和乐观读

其中,写锁、悲观读锁的语义和 ReadWriteLock 的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。

不同的是:StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp,这里的stamp就类似刚刚我们说的数据库version,相信你已经明白了。

我们通过代码演示一下写锁、悲观读锁是如何使用的:

    // 锁实例
private final StampedLock sl = new StampedLock(); // 排它锁-写锁
void writeLock() {
long stamp = sl.writeLock();//获取写锁
try {
// 业务逻辑
} finally {
sl.unlockWrite(stamp);//释放写锁
}
} // 悲观读锁
void readLock() {
long stamp = sl.readLock();
try {
// 业务逻辑
} finally {
sl.unlockRead(stamp);
}
}

乐观读

StampedLock 的性能之所以比 ReadWriteLock 还要好,其关键是 StampedLock 支持乐观读的方式。所谓乐观读,即读的时候也能允许一个线程获取写锁,也就是说不是所有的写操作都被阻塞,自然而然的会比所有写都阻塞性能要强。

还是通过代码来说明一下乐观读是如何使用的:

    // 乐观读
double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead();//(1)
double currentX = x, currentY = y; // 检查在(1)获取到读锁票据后,锁有没被其他写线程排它性抢占
if (!sl.validate(stamp)) {
// 如果被抢占则获取一个共享读锁(悲观读锁)
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX*currentX + currentY*currentY);
}

tryOptimisticRead() 就是我们前面提到的乐观读。不过需要注意的是,由于 tryOptimisticRead() 是无锁的,所以共享变量 x 和 y 读入方法局部变量时,x 和 y 有可能被其他线程修改了。因此最后读完之后,还需要再次验证一下是否存在写操作,这个验证操作是通过调用 validate(stamp) 来实现的。

还有一个巧妙的地方:如果执行乐观读操作的期间,存在写操作,会把乐观读升级为悲观读锁。这个做法挺合理的,否则你就需要在一个循环里反复执行乐观读,直到执行乐观读操作的期间没有写操作(只有这样才能保证 x 和 y 的正确性和一致性),而循环读会浪费大量的 CPU。升级为悲观读锁,代码简练且不易出错。

锁的升级

在上一篇读写锁文章中,我们说到锁的升级和降级,ReadWriteLock是只允许降级而不允许升级的,而StampedLock 支持锁的降级(通过 tryConvertToReadLock() 方法实现)和升级(通过 tryConvertToWriteLock() 方法实现),读锁居然也可以升级为写锁,这也是它区别于读写锁的一大特性!

    // 读锁升级成写锁
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
// 尝试将获取的读锁升级为写锁
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
} else {
// 读锁升级写锁失败则释放读锁,显示获取独占写锁,然后循环重试
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}

StampedLock 使用注意事项

StampedLock真有这么完美吗?挑刺时间又来咯!

1、StampedLock 在命名上并没有增加 Reentrant,显然,StampedLock 不支持重入。这个是在使用中必须要特别注意的。

2、StampedLock 的悲观读锁、写锁都不支持条件变量(Condition),这个也需要你注意。

3、使用 StampedLock 一定不要调用中断操作,即不要调用interrupt() 方法,因为内部实现里while循环里面对中断的处理有点问题。如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。

总结

如何解决原子性问题为起点,我们初始了锁的概念,了解了synchronized锁模型,之后又走进了J.U.C Lock包,首先接触到了ReentrantLock互斥锁,由于互斥锁在读多写少场景的效率不高,因此接触了读写锁ReadWriteLock,而今天,又学习了一种比读写锁还要快的锁StampedLock。说明JAVA真是博大精深,连锁都有那么多种,需要根据实际情况合理选择才是!

关于StampedLock,重点应该了解它独特的思想:乐观的思想。就像人一样,不能总是悲观思想,乐观思想积极面对生活效率才更高!StampedLock通过一个叫做stamp的类似于数据库版本号的字段,实现了乐观读。当然永远乐观也是不行的,StampedLock也有它的缺陷,对于这些,你也需要特别注意。

本文来源于公众号【胖滚猪学编程】 以漫画形式让编程so easy and interesting !转载请注明出处!

本文转载自公众号【胖滚猪学编程】 用漫画让编程so easy and interesting!欢迎关注!形象来源于微信表情包【胖滚家族】喜欢可以下载哦~

【漫画】读写锁ReadWriteLock还是不够快?再试试StampedLock!的更多相关文章

  1. 【漫画】互斥锁ReentrantLock不好用?试试读写锁ReadWriteLock

    ReentrantLock完美实现了互斥,完美解决了并发问题.但是却意外发现它对于读多写少的场景效率实在不行.此时ReentrantReadWriteLock来救场了!一种适用于读多写少场景的锁,可以 ...

  2. 线程中的读写锁ReadWriteLock

    Lock锁还有两个非常强大的类 ReadWriteLock接口实现类ReentrantReadWriteLock(非常重要的锁) 想实现 读取的时候允许多线程并发访问,写入的时候不允许. 这种效果.. ...

  3. Java 读写锁 ReadWriteLock 原理与应用场景详解

    Java并发编程提供了读写锁,主要用于读多写少的场景,今天我就重点来讲解读写锁的底层实现原理@mikechen 什么是读写锁? 读写锁并不是JAVA所特有的读写锁(Readers-Writer Loc ...

  4. 显式锁(三)读写锁ReadWriteLock

    前言:   上一篇文章,已经很详细地介绍了 显式锁Lock 以及 其常用的实现方式- - ReetrantLock(重入锁),本文将介绍另一种显式锁 - - 读写锁ReadWriteLock.    ...

  5. 读-写锁 ReadWriteLock & 线程八锁

    读-写锁 ReadWriteLock: ①ReadWriteLock 维护了一对相关的锁,一个用于只读操作, 另一个用于写入操作. 只要没有 writer,读取锁可以由 多个 reader 线程同时保 ...

  6. C# 多线程编程之锁的使用【互斥锁(lock)和读写锁(ReadWriteLock)】

    多线程编程之锁的使用[互斥锁(lock)和读写锁(ReadWriteLock)] http://blog.csdn.net/sqqyq/article/details/18651335 多线程程序写日 ...

  7. 深入理解读写锁—ReadWriteLock源码分析

    转载:https://blog.csdn.net/qq_19431333/article/details/70568478 ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁.读锁可以在 ...

  8. 多线程编程_读写锁ReadWriteLock

    Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象.两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象. 读写锁:分为读 ...

  9. 读写锁ReadWriteLock和缓存实例

    读写锁:多个读锁不互斥,读锁与写锁互斥,写锁与写锁互斥.即:读的时候不允许写,写的时候不允许读,可以同时读.      synchronized关键字和普通的Lock构造的锁,会造成读与读之间的互斥, ...

随机推荐

  1. Apache jena SPARQL endpoint及推理

    一.Apache Jena简介 Apache Jena(后文简称Jena),是一个开源的Java语义网框架(open source Semantic Web Framework for Java),用 ...

  2. Fiddler抓取抖音视频

    目录 工具 Fiddler配置 手机端配置 工具 Android 或 ios手机均可 Fiddler 下载地址:https://www.telerik.com/fiddler Windows 操作系统 ...

  3. python进入adb shell交互模式

    import subprocess #方法一:进入某个环境执行语句(adb shell),注意shell内部命令需要带\n,执行完后一定记得执行exit命令退出,否则会阻塞 obj = subproc ...

  4. Java 解析 xml 常见的4中方式:DOM SAX JDOM DOM4J

    Java 四种解析 XML 的特点 1.DOM 解析: 形成了树结构,有助于更好的理解.掌握,且代码容易编写. 解析过程中,树结构保存在内存中,方便修改. 2.SAX 解析: 采用事件驱动模式,对内存 ...

  5. 苹果登录服务端JWT算法验证-PHP

    验证参数 可用的验证参数有 userID.authorizationCode.identityToken,需要iOS客户端传过来 验证方式 苹果登录验证可以选择两种验证方式 具体可参考这篇文章 htt ...

  6. ubuntu安装Python3并与Python2自由切换

    一.配置ssh链接安装openssh-server sudo apt-get install openssh-server 二.安装Python3及pip sudo apt-get install p ...

  7. 使用openmp进行共享内存编程

    预处理指令pragma:在系统中加入预处理器指令一般是用来允许不是基本c语言规范部分的行为.不支持pragma的编译器会忽略pragma指令提示的那些语句,这样就允许使用pragma的程序在不支持它们 ...

  8. JQ获取select上的option的data-start和data-id

    来源:https://zhidao.baidu.com/question/692142321436883524.html 静态的写法: 用jq的attr()函数,如: HTML: <select ...

  9. NC使用练习之通达OA-2017版本漏洞复现后续

    利用上一篇通达OA的漏洞环境,练习NC工具的使用. 步骤: 1.本机启动nc.exe监听端口: 确认端口是否成功监听成功: 2.用冰蝎将nc.exe上传至目标机: 3.用命令行在目标机启动nc.exe ...

  10. 单源最短路问题--朴素Dijkstra & 堆优化Dijkstra

    许久没有写博客,更新一下~ Dijkstra两种典型写法 1. 朴素Dijkstra     时间复杂度O(N^2)       适用:稠密图(点较少,分布密集) #include <cstdi ...