使用 Google Guava Striped 实现基于 Key 的并发锁
写 Java 代码至今,在应对可能冲突的共享资源操作时会尽量用 JDK 1.5 开始引入的并发锁(如 Lock 的各类实现类, ReentrantLock 等) 进行锁定,而不是原来的 synchronized
关键字强硬低性能锁。
这里是应用 JDK 1.5 的 Lock
的基本操作步骤
private Lock lock = new ReentrantLock();
private void operate() {
// 安全操作 ....
lock.lock();
try {
// 对共享资源的操作 ...
} finally {
lock.unlock();
}
}
如此,operate()
就是一个线程安全的方法,任何对它的调用都安排到了一个队列里等着。但有时候上锁需要考虑更细的粒度,下面是一个演示案例,引出第一个问题
一、为何需要细粒度锁
private void merge(String filePath, List<String> deltaLines) { lock.lock(); try { Path path = Paths.get(filePath); List<String> fileLines = Files.exists(path) ? Files.readAllLines(path) : new ArrayList<>(); fileLines.addAll(deltaLines); fileLines.sort(Comparator.naturalOrder()); Files.write(path, fileLines); } catch (Exception ex) { ex.printStackTrace(); } finally { lock.unlock(); } } |
|
被保护的操作序列是读取原文件内容,合并新行并排序,再回写文件; 如果原文件不存在则生成新文件,并含有已排序的新行。如果不保护该系列操作,文件内容将会被不同线程相互覆盖,因为两个线程可能读入相同的内容再加上各自不同的新行写回,而不是全部内容叠加。
二、用 ConcurrentHashMap 改进的细粒度锁
如果继承采用与前面一样保证整个方法绝对安全的方式,效率上就会变得很差,因为无论是操作相同还是不同的文件,统统得排着队进行。而实际上只有是操作不同的文件(filePath 不同), 是允许并发的。这就引出了要对锁的粒度进一步细化,只在文件路径相同时才需要获取锁。有一种实现方式是为不一样的 filePath
创建各自的锁,用 ConcurrentHashMap
缓存起来,看接来的改进:
private Map<String, Lock> cachedLocks = new ConcurrentHashMap<>(); private void merge(String filePath, List<String> deltaLines) { Lock lock = cachedLocks.computeIfAbsent(filePath, key -> new ReentrantLock()); lock.lock(); try { Path path = Paths.get(filePath); // ... 以下省略 } finally { lock.unlock(); } } |
改进后的代码在应对并发性的性能是大大提高了,有一个问题是如果应用中要操作百万,千万个不同的文件,那么势必在内存中创建相应数量的锁实例,对内存将是个不小的负担。即使线程池大小只有几个的时候锁实例的数量也与文件个数相同,并且长时间不再使用的锁实例都无法被回收。进一步的优化也许可以采用弱引用,或定时清理长时间不使用的锁实例,而且要兼顾到避免瞬间高并发时生成大量锁实例耗用内存的情形。
三、采用 Guava Striped 实现细粒度锁
这儿提及到了锁实例量与线程池大小关系,所以可以考虑把创建的锁实例放到一个固定大小(如使用它的线程池大小)的 ConcurrentHashMap
中,比如创建锁时清除缓存中最早未使用的锁,这样做对内存不会产生负担,就是清理工作必须做到高效。其实这一思考惯性正好引出了今天的主角: Google Guava 库的 Striped 类,Guava 当前版本是 27.1, 在 Guava 库中 Striped 类仍然被标记为 @Beta
不稳定版本,所以使用它的一起后果自负(可能造成死锁:使用guava Striped中的lock导致线程死锁的问题分析,该文发表于 2016-11-19)。
Guava 对 @Beta
的解释见 https://github.com/google/guava#important-warnings, 标记为 @Beta
的类或方法会被随时修改甚至是移除,如果使用它再次作为类库发布的话强烈建议用 Guava Beta Checker 检测并确保不要用 @Beta
的类。可怜 Striped 自从 13.0 加入后直至今天的 27.1 都未转正。
还继续往下阅读吗?
先来感受一下怎么用 Striped
,而后再来了解它的 API 和实现原理:
private Striped<Lock> stripedLocks = Striped.lock(20); private void merge(String filePath, List<String> deltaLines) { Lock lock = stripedLocks.get(filePath) ; lock.lock(); try { Path path = Paths.get(filePath); // ... 以下省略 } finally { lock.unlock(); } } |
看上去就是替代了我们用 ConcurrentHashMap
部分的代码,代码方法并没什么简洁,但是它省内存啊,不管不同的文件名有多少个就只要预建 20 个锁,当然 20 这个数字也是基于 merge
方法可能被多少个线程并发执行(如线程池的大小) 来设置的。
Striped 比用 ConcurrentHashMap 缓存的锁实例的好处是锁可被重用,Striped 中同一个锁第一次由 key1 引用,第二次还能被 key2 引用,ConcurrentHashMap 中的锁呢, key 1 用过的就不再被 key2 再次使用。
Striped 实现细粒度锁是基于它自己在 Striped Javadoc 中提出的一个真理,简单说来就以下三条
- 相同的 key (hashCode()/equals()) 时, striped.get(key) 总会得到相同的锁实例
- 但是不同的 key 却可能调用 striped.get(key) 获得相同的锁实例
- 基于上一条,预建更多的锁实例数量能减低锁碰撞的可能性
第一条保证被保护的代码是线程安全的,第二条会出现不同 key 的两个任务会排在同一个队列上,性能上会有所降低,但能够在锁数量(内存)与并发规模之间平衡。比如线程池大小为 20,预建 80 个锁对内存来说毫无压力,比瞬间百万,千万个锁好多了。Guava 建议是,对于计算密集型的任务创建 4 倍于可用处理器数目的锁。
紧接着来看下 Striped 提供的 API,它支持创建 Lock, Semaphore 和 ReadWriteLock,并且提供创建 eager 和 lazyWeak 两个版本
- public static Striped<Lock> lock(int strips)
- public static Striped<Lock> lazyWeakLock(int stripes)
- public static Striped<Semaphore> semaphore(int stripes, int permits)
- public static Striped<Semaphore> lazyWeakSemaphore(int stripes, int permits)
- public static Striped<ReadWriteLock> readWriteLock(int stripes)
- public static Striped<ReadWriteLock> lazyWeakReadWriteLock(int stripes)
以上返回的 Lock 或 ReadWriteLock 都是可重入锁,lazyWeakXxx() 版本的选择也是基于节约内存的考虑,如果并发大小是可控且不大的情况不一定需要 lazyWeekXxx() 的版本,比如前面说的线程池大小为 20 的情况初始化 80 个锁直接用 Striped.lock(80) 就行。
对于 Striped 的使用也就差不多了,如果用 semaphore(...) 的话需要了解 JDK 中 Semaphore 信号量的使用,其实是在同一把锁的情况下次一层次的控制。举个例子,Lock 控制了同一个帐号只能同时一个地方登陆,Semaphore(信号量) 放宽一些,可以控制同一个帐号最多在几个地方同时登陆。
使用 Google Guava Striped 实现基于 Key 的并发锁相关推荐
- Google Guava Striped 实现细粒度锁
首先不谈Striped能做什么,我们来看下如下的代码 https://my.oschina.net/lis1314/blog/664142?fromerr=8CDQbye9 /*** 购买产品* @p ...
- Java类库Google Guava学习
参考 官网 https://github.com/google/guava Google Guava官方教程(中文版) | 并发编程网 – ifeve.com 一篇让你熟练掌握Google Guava ...
- 一致性Hash(基于google Guava实现)
背景 一般我们使用的hash就是md5 sha 之类的工具类,在负载均衡会要求类似同一个ip在增加节点时还是定位到之前的节点,这时就要用到一致性hash.具体实现代码参考(基于google Guava ...
- Google Guava Collections 使用介绍
原帖http://www.open-open.com/lib/view/open1325143343733.html 简介: Google Guava Collections 是一个对 Java Co ...
- [Google Guava] 3-缓存
原文地址 译文地址 译者:许巧辉 校对:沈义扬 范例 01 LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() ...
- Google Guava BloomFilter
当Guava项目发布版本11.0时,新添加的功能之一是BloomFilter类. BloomFilter是唯一的数据结构,用于指示元素是否包含在集合中. 使BloomFilter有趣的是,它将指示元素 ...
- 【Guava】Google Guava本地高效缓存
1.Google,Guava本地高效缓存 Guva是google开源的一个公共java库,类似于Apache Commons,它提供了集合,反射,缓存,科学计算,xml,io等一些工具类库.cache ...
- Google,Guava本地高效缓存
Guva是google开源的一个公共java库,类似于Apache Commons,它提供了集合,反射,缓存,科学计算,xml,io等一些工具类库. cache只是其中的一个模块.使用Guva cac ...
- Google Guava之--cache
一.简介 Google Guava包含了Google的Java项目许多依赖的库,如:集合 [collections] .缓存 [caching] .原生类型支持 [primitives support ...
最新文章
- Python---哈夫曼树---Huffman Tree
- python非官方的二进制扩展包下载地址
- Oneproxy 读写分离
- Python3转义字符
- linux java 查询mysql_Linux Java连接MySQL数据库
- android 单元测试 多线程,多线程之单元测试(Junit)
- 证明LDU分解的唯一性
- 自建 bitwarden 密码管理服务
- Java 后台验证码汉字拼音校验
- 性能优化-图片压缩格式的选择(ETC和ASTC)
- 华为OD(外包)社招技术二面,总结复盘
- 提问的智慧 (How To Ask Questions The Smart Way)
- 电脑安装哪款linux系统好,四款linux操作系统总有一款适合你
- linux编译ace tao,ACE_TAO的编译
- 开发常用镜像站 - 阿里云镜像站
- 朋友圈图片评论功能,来了!
- 有谁知道怎么处理微信用户头像过期问题,除了本地保存,因为不会用七牛云远程附件
- 论二级域名收集的各种姿势
- wifi网络为什么总是断线 (by quqi99)
- 【NOIP2013提高组day1】货车运输