使用缓存时对于加锁的思考

原文:https://www.cnblogs.com/rongdi/p/9034886.html

  突然发现之前写的自己实现XXX的话题不是很被大众关注,可能是真的写的不行,也可能是大多都是一些吃了饭没事做瞎写的一些东西,大家都没兴趣,之后可能会尽量写一些真正实用的东西,大家一起学习。言归正传,促使我研究这个话题的原因是在工作中遇到需要自己实现多级缓存的情况。比如在springboot中我们虽然可以随意替换缓存技术,可以使用redis也可以使用ehcache,但是据我所知,这些缓存默认都是只能使用一种。假设现在我需要同时使用ehcache和redis,其中ehcache做本地的第一级缓存。这里忽略可能用到的mybatis,hibernate那些操作数据库的orm框架的缓存,单独只考虑应用层面的缓存。当然在已经有springboot,ehcache,redis的情况下,通过自定义注解,切面拦截,组合ehcache和redis的两级缓存,其实并不是很困难。难点只在于细节部分,比如怎么让自己定义的注解和切面可以被springboot中的缓存注解开关@EnableCaching控制,又比如多点部署时怎么让ehcache本地缓存和redis集中式缓存实现同步,当然也有使用缓存的时候怎么加锁的问题。本话题重点讨论,在缓存使用时的加锁问题。

  其实当我们刚开始学java的线程中同步工具时,我们就接触到synchronized,锁,读写锁等。其中在api文档的ReentrantReadWriteLock类里有一个例子,专门演示了读写锁,在一个缓存对象中的使用的例子如下:

class CachedData {Object data;volatile boolean cacheValid;ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();void processCachedData() {rwl.readLock().lock();if (!cacheValid) {// Must release read lock before acquiring write lockrwl.readLock().unlock();rwl.writeLock().lock();// Recheck state because another thread might have acquired//   write lock and changed state before we did.if (!cacheValid) {data = ...cacheValid = true;}// Downgrade by acquiring read lock before releasing write lockrwl.readLock().lock();rwl.writeLock().unlock(); // Unlock write, still hold read}use(data);rwl.readLock().unlock();}}

  里面的读写锁,double check,锁降级等用的炉火纯青自是不用说的,我当时也是对此记忆犹新,如获至宝啊,感觉这段代码很适合装B,然后我在我的缓存实现代码里也准备这么玩,当我把这段代码复制过去,改成我需要的逻辑后,我就发现了问题。瞬间感觉这要是这么玩,可能装B不成那个啥的。然后我搜了搜网上别人的这种玩法,想看看别人是不是也有这种尴尬。然后搜索出来一大把如下面代码(百度搜索:java读写锁实现缓存)

package test;import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;/*** 设计一个缓存系统* 读写锁的应用。* JDK1.5自带的读写锁特性,读与读不互斥,读与写互斥,写与写互斥。* 为什么要使用读写锁?一句话概括那就是提高系统性能,如何提高呢?* 试想,对于所有对读的操作是不需要线程互斥的,而如果方法内* 使用了synchronized关键字同步以达到线程安全,对于所有的线程不管是读还是写的操作都要同步。* 这时如果有大量的读操作时就会又性能瓶颈。* * 所以,当一个方法内有多个线程访问,并且方法内有读和写读操作时,* 提升性能最好的线程安全办法时采用读写锁的机制对读写互斥、写写互斥。这样对于读读就没有性能问题了* @author zhurudong**/
public class CacheTest {// 缓存的mapprivate Map<String, Object> map = new HashMap<String, Object>();// 读写锁对象private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();/*** 从缓存中获取数据的方法* @param key* @return*/public Object getData(String key) {readWriteLock.readLock().lock();//读锁,只对写的线程互斥Object value = null;try {// 尝试从缓存中获取数据value = map.get(key);if (value == null) {readWriteLock.readLock().unlock();//发现目标值为null,释放掉读锁readWriteLock.writeLock().lock();//发现目标值为null,需要取值操作,上写锁try {value = map.get(key);// 很严谨这一步。再次取目标值if (value == null) {//很严谨这一步。再次判断目标值,防止写锁释放后,后面获得写锁的线程再次进行取值操作// 模拟DB操作value = new Random().nextInt(10000) + "test";map.put(key, value);System.out.println("db completed!");}readWriteLock.readLock().lock();//再次对读进行锁住,以防止写的操作,造成数据错乱} finally {/** 先加读锁再释放写锁读作用:* 防止在43行出多个线程获得写锁进行写的操作,所以在写锁还没有释放前要上读锁*/readWriteLock.writeLock().unlock();}}} finally {readWriteLock.readLock().unlock();}return value;}/***  test main* @param args*/public static void main(String[] args) {final CacheTest cache = new CacheTest();final String key = "user";for (int i = 0; i < 1000; i++) {new Thread(){public void run() {System.out.println(cache.getData(key));};}.start();}}
}

  估计很多人看了上面代码,也会和我当时改完api文档的例子后一样觉得这代码真他妈严谨。改完后,我又仔细想想(我习惯把逻辑用现实中的场景去验证,毕竟程序世界各种场景处理方法等都是是现实世界的抽象),想到了生活中去超市门口那种储存柜子存取包裹,读操作可以类比为取柜子里的包裹,写操作可以类比为去超市存包裹,map可以类比成那个柜子。使用上面代码在现实中的这种场景下还原出来就是:当我拿着小票去取包裹,别人可以同时去取包裹,但是当我去存包裹的时候,我让除了我以外的所有人必须在我后面等着,等我存好了,别人才能靠近柜子。这种霸道的做法,不就是上面那段代码的表现吗?怎么样看了这段描述,你们还要像上面那样装B吗?有人会问,为啥api文档里推荐那样写呢,其实仔细看下文档里的那段代码,它加锁的那个方法是没有入参的,缓存的数据直接作为了成员变量,这个成员变量作为缓存的值对于所有调用processCachedData方法的线程来说都是共享的,有共享数据就要考虑多线程环境下同步的问题。而上面那段自己的代码是有入参key的,也就是说根据key获取的缓存值有可能被所有调用线程共享,也有可能不共享,当key不同的时候,这个数据其实是各不影响的。所以这种情况加锁最好是根据这个入参区别对待。然后我就有了下面的想法,不知道是不是也有人想到呢?

public Object getData1(String key) {if(key == null) return null;Object data = null;synchronized (key.intern()) {data = dataMap.get(key);if(data == null) {data = "query object";dataMap.put(key, data);}}return data;
}

  如上代码,我们可以把key当成监视器,但如果仅仅只是key有点不严谨,因为可能字符串内容一样,一个是返回字符串池的引用,一个返回堆里的引用,根本不是一个对象。表现出来的结果可能就等同于没有加锁。所以在这里使用key.intern(),都返回池里的引用。这样控制相当于不同的人拿着不同的小票可以同时存取包裹,不同的人要是拿着一样的小票进来存取,就应该一个人存取的时候,另外一个人等候。虽然现实场景中每个人的小票应该都是唯一的,但是程序中是合理的,我们可以假设小票有重复。回到程序中:如果多个线程如果拿着不同的key值操作数据不需要等待其它线程的锁,如果拿着同样的key值操作数据就需要等待其它线程释放锁。这样的结果貌似很合理,但是在一般缓存应用场景中,缓存最大的作用是在短时间大量重复的获取相同的key的值能够快速的从缓存获取到数据,假设不加读写请求的区分,统一加锁,会导致多个线程不能同时去读取相同的key的值,也需要互相等待,同样有损性能。以上两种加锁方案:第一种进行了读写分开考虑,多线程中读读不互斥,读写互斥,但是没有考虑获取不同的key值不需要互斥;第二种考虑了不同的key不需要互斥的情况,但是没有考虑读读不需要互斥的情况。可惜的是java的api并没有提供根据不同的参数获取不同的读写锁的方法,这一点很尴尬。相信所有的自称高性能的缓存都会遇到这种尴尬,于是我找了纯java实现的缓存ehcache的源码看了下,发现核心思路就是先默认生成了2048个长度的ReentrantReadWriteLock数组,然后使用hash算法对key进行计算得到一个int的值,然后每个key进来去拿数组里hash值对应的索引的那个ReentrantReadWriteLock,然后再使用这个读写锁来控制,这样就可以达到不同的key读写互不影响,相同的key读读不互斥,读写互斥。抽离出来大概代码如下(适当简化过了)。

    //源码里实际上是把ReentrantReadWriteLock封装在一个ReadWriteLockSync类里的,我不喜欢拐弯抹角,直接简单的提取出来了final ReadWriteLock[] rwlocks = new ReentrantReadWriteLock[2048];//这里是自己为了简单,源码里并不是这么简陋的{for(int i = 0;i < rwlocks.length;i++) {rwlocks[i] = new ReentrantReadWriteLock();}}//这里是源码里的方法public static int selectLock(final Object key, int numberOfLocks) throws CacheException {/*** 这里用自己和自己小1的数字取了一个与运算,目的就是保证numberOfLocks是2的n次方,比如8和7与1000  & 0111= 0000如果结果不等于0必然不是2的n次方  */int number = numberOfLocks & (numberOfLocks - 1);if (number != 0) {throw new CacheException("Lock number must be a power of two: " + numberOfLocks);}if (key == null) {return 0;} else {/**这里也很有意思,一般我们做估计就是hash(key) % numberOfLocks来定位数组的索引,他这样玩其实在numberOfLocks是2的n次方的情况下,是与%操作等效,如下8 % 4 = 8 & 31000& 0011= 0000 = 011 % 8 = 11 & 71011& 0111= 0011= 3每次看到别人用位运算,只能感叹,这尼玛才叫装B啊!*/int hash = hash(key) & (numberOfLocks - 1);return hash;}}//这里是源码里的hash函数,表示看不懂,就知道传一个对象进去可以出来一个int类型的值public static int hash(Object object) {int h = object.hashCode();h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);}

  然后再修改一下最开始说的装B的读写锁代码如下

    /**这里使用支持并发的map是为了保证容器本身没有并发问题,容器的存取操作是原子的,后面getData2的同步控制是为了防止业务数据同时读写出现脏读等情况,比如一个key在读,同时也在写,到底读到的是写前还是写后的数据呢?这里是属于业务上的并发控制,至于容器上的并发控制就是明明两个不同的key去写因为抢同一个位置导致数据不一致等问题,就像存包裹,有两个人非要去存在一个箱子里就会出现争抢了。并不是说使用了线程并发库就不需要进行并发控制了,毕竟线程并发库里的读写,也只能保证单独的读写是原子操作,假设多个组合起来就不能保证原子性了*/private Map<String,Object> dataMap = new ConcurrentHashMap<>();public Object getData2(String key) {int locknum = selectLock(key,2048);ReadWriteLock rwlock = rwlocks[locknum];rwlock.readLock().lock();try {Object data = dataMap.get(key);if (data == null) {rwlock.readLock().unlock();rwlock.writeLock().lock();if(data == null) {data = "query object";dataMap.put(key, data);}rwlock.readLock().lock();rwlock.writeLock().unlock();}return data;} finally {rwlock.readLock().unlock();}}

  其实这样的思路就是尽量降低锁的粒度,把一个读写锁换成了2048个小锁,像jdk1.8以前的ConcurrentHashMap采用的锁分段,分成了16端,其实和这个的思想是一样的,都是降低锁的粒度,达到高效的并发控制。不过,估计当读写达到一定两级后仅仅这16端的切分,估计也起不到多大的效果了。所以官方估计意识到了这一点,在1.8后把实现方式改成了CAS无锁算法。我突然想到新版的ehcache(这篇话题谈到的ehcache版本是2.10.4)会不会也使用CAS呢?这个新版的源码,我暂时也没有研究过,人类对性能的追求是永无止境的,这个就等有兴趣的朋友再去探究了。

  好吧,就这样结束吧,如果文章有错误的地方,欢迎指正。下一个话题再讨论如何在springboot的基础上结合ehcache和redis无缝实现自定义二级缓存。

转载于:https://www.cnblogs.com/yelongsan/p/9037239.html

使用缓存时对于加锁的思考(* * * * *)相关推荐

  1. 使用缓存时对于加锁的思考

    突然发现之前写的自己实现XXX的话题不是很被大众关注,可能是真的写的不行,也可能是大多都是一些吃了饭没事做瞎写的一些东西,大家都没兴趣,之后可能会尽量写一些真正实用的东西,大家一起学习.言归正传,促使 ...

  2. 如何防止android app被误删除,如何避免手机清理缓存时误删了重要文件【注意事项】...

    如何避免手机清理缓存时误删了重要文件? 缓存只是内存中少部分数据的复制品,所以CPU到缓存中寻找数据时,也会出现找不到的情况(因为这些数据没有从内存复制到缓存中去),这时CPU还是会到内存中去找数据, ...

  3. 当我们在谈论HTTP缓存时我们在谈论什么

    前言 在浏览器众多缓存中的HTTP缓存可能很多人对这个的概念并没有很清晰,每个人都知道进入一次网页之后再刷新一次页面,加载速度会比首次加载快非常多,每个人都知道这是浏览器缓存的magic,但是对此背后 ...

  4. 谈谈使用Redis缓存时批量删除的几种实现

    谈谈使用Redis缓存时批量删除的几种实现 原文:谈谈使用Redis缓存时批量删除的几种实现 前言 在使用缓存的时候,我们时不时会遇到这样一个需求,根据缓存键的规则去批量删除这些数据,比较常见的就是按 ...

  5. 基于北向资金指数择时策略验证与思考

    基于北向资金指数择时策略验证与思考 本文思路华泰金工研量化资产配置7月月报<北向资金走向预示市场短期或震荡>关于北上资金的择时思路,构建基于北向资金的股市择时策略,并进行历史回测.结果表明 ...

  6. Dreamweaver8加载站点缓存时卡死,解决办法:

    Dreamweaver8加载站点缓存时卡死,解决办法: 无论中文版的 dreamweaver 8,还是英文版本的dw8或绿色版本的DW8,都可能出现无法启动的情况,这个bug的出现是因为先前你曾经在使 ...

  7. 基于A股周内效应择时策略验证与思考(附代码)

    基于A股周内效应择时策略验证与思考 本文思路来自于东吴证券研报<A股市场的周内效应>内容,对A股市场的日历效应在周内表现进行探索. 上述研报的核心内容简述: 1.A股市场在股票指数和个股上 ...

  8. npm强制清除缓存时无法清除问题

    npm强制清除缓存时无法清除问题 使用webpack时遇到的问题,npm强制清除缓存时无法清除. 强制清除npm指令 npm cache clean -f 在执行以上指令后,命令一直没有终止,具体忘了 ...

  9. 构建iOS稳定应用架构时方案选择的思考,主要涉及工程结构,数据流思想和代码规范...

    工程结构架构,减少耦合混乱以及防治需求大改造成结构重构,如何构建稳定可扩展可变换的工程结构的思考 我打算采用Information flow的方式自上而下,两大层分为基础层和展现层的结构.基础层分为多 ...

最新文章

  1. IP地址基础网络知识—Vecloud微云
  2. 单片机模块学习之键盘
  3. Linux进程间通信一 System V 共享内存简介与示例
  4. Java8 - 自定义实现体会Future的原理
  5. C#和NewSQL更配 —— TiDB入门
  6. CF1286D-LCC【动态dp,数学期望】
  7. WebSphere中连接池的配置
  8. 卡尔曼滤波,高斯函数
  9. 谈一下今天的网络赛。。。这次是真的弱爆了。。。。
  10. 解二元一次方程组的c语言编程,如何利用C语言求二元一次方程的解
  11. 【IDEA类注释模板和方法注释模板】
  12. QQ在线客服代码演示-asp源代码
  13. 乐高wedo搭建图纸_乐高wedo2.0搭建图纸
  14. 黑冰客防骗子—常见网络骗子骗术防御要点
  15. java PDF模板生成并导出(文字、表格、图片)
  16. 数学建模竞赛网站汇总
  17. Java 在Word中创建多级项目符号列表和编号列表
  18. Jamie Zawinski访谈:C++之恶
  19. 盘古BPM工作流平台(DMN)引擎完全开源了
  20. mysql导出表数据到文件的几种方法

热门文章

  1. mysql数据库怎么比较时间_【数据库】mysql可以比较时间吗
  2. c语言结构体变量所占字节计算,【C语言】结构体占用字节数及存储与空间分配...
  3. QT中的中英文翻译(ts和qm使用方法)
  4. 单播、组播、广播区别与联系
  5. 骁龙730g处理器怎么样 骁龙730g相当于麒麟多少 骁龙730g是什么级别
  6. 学习理发去哪里_【学理发应该去哪里是理发店还是学校】
  7. Android 十年总结:1.5 到 10.0 都有哪些新特性?面试常问题
  8. 手机浏览器css差异问题,手机和桌面之间的CSS行高属性差异
  9. java-php-python-ssmB饮品公司管理计算机毕业设计
  10. VHDL程序的基本结构