Java 细粒度锁续篇
在上篇文章中大概介绍了 Java 中细粒度锁的几种实现方式,并且针对每种方式都做了优缺点说明,在使用的时候就需要根据业务需求选择更合适的一种。上篇文章中的最后一种弱引用锁的实现方式,我在里面也说了其实还有更优雅的实现,其实也算不上更优雅,只是看起来更优雅,原理还是一样的,今天我打算用一篇文章的篇幅来好好说下。
首先,我们来再次回顾一下,这里为什么可以利用弱引用的特性拿掉分段锁呢?分段锁在这里主要是为了保证每次在创建和移除锁时的线程安全,而采用了弱引用之后,我们不需要每次创建之后都进行移除,因为当弱引用指向的对象引用被释放之后 Java 会在下一次的 GC 将这弱引用指向的对象回收掉,在经过 GC 之后,当弱引用指向的对象被回收时,弱引用将会进入创建时指定的队列,然后我们通过队列中的值来将这些存放在 Map 中的弱引用移除掉,所以我们才能够顺利的拿掉分段锁。
WeakHashMap
你注意看弱引用锁的代码实现,里面在我们获取锁的时候有个手动去清理 Map 中被回收的锁的过程,如果你看过之前的 谈谈 Java 中的各种引用类型 这篇文章的话,你应该知道 Java 提供了一个 WeakHashMap 类,他是使用弱引用作为 key,它在 GC 决定将弱引用所指向的 key 对象回收之后,会将当前保存的 entry 也自动移除,这个是怎么实现的呢?
其实原理也是一样的,利用弱引用指向的对象被回收时,弱引用将会进入创建时指定的队列这一特性,然后通过轮询队列来移除元素。只不过将移除的操作完全包裹在 WeakHashMap 类里面了,你可以看到里面所有的 public 的增删改查方法都直接或间接调用了expuntgeStaleEntries() 方法,而 expuntgeStaleEntries 方法中就是在轮询队列移除被回收的 key 所对应的元素。
private void expungeStaleEntries() {
for (Object x; (x = queue.poll()) != null; ) {
synchronized (queue) {
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) x;
int i = indexFor(e.hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> p = prev;
while (p != null) {
Entry<K,V> next = p.next;
if (p == e) {
if (prev == e)
table[i] = next;
else
prev.next = next;
// Must not null out e.next;
// stale entries may be in use by a HashIterator
e.value = null; // Help GC
size--;
break;
}
prev = p;
p = next;
}
}
}
}
既然 Java 已经给我们提供了相应功能的类,那我们是不是可以在弱引用锁的实现中直接使用 WeakHashMap 呢?这样我们就不用在获取锁的时候做手动移除的操作了,WeakHashMap 内部已经帮我们做了。
但如果你稍微看一下 WeakHashMap 类的描述就能发现他不是线程安全的,在该类里面有这样一段描述:
Like most collection classes, this class is not synchronized. A synchronized {@code WeakHashMap} may be constructed using the {@link Collections#synchronizedMap Collections.synchronizedMap} method.
正因为如此,在弱引用的实现中才采用 ConcurrentHashMap 来保存锁,只不过 ConcurrentHashMap 类没有提供弱引用的实现,也就没有提供自动为我们移除元素的功能,所以才会在获取锁的时候做一个移除元素的操作,相信看到这里你应该大概明白了使用弱引用作为 key 的 WeakHashMap 是怎么做到当弱引用被回收的时候自动把对应的元素给移除了。
那如果说按照上面描述里面所说的通过 Collections 工具类的 synchronizedMap 方法来实现线程安全呢?先来看代码实现:
public class WeakHashLock<T> {
public final Map<T, WeakReference<ReentrantLock>> weakHashMap =
Collections.synchronizedMap(new WeakHashMap<>());
public ReentrantLock get(T key){
return this.weakHashMap.computeIfAbsent(key, lock -> new WeakReference<>(new ReentrantLock())).get();
}
}
上面代码中 WeakHashLock 类中只有一个 get 方法根据 key 获取锁对象,不存在的话创建一个新的锁对象返回,看起来是不是很简单,但不幸的是通过 Collections 工具类的 synchronizedMap 方法来实现的线程安全方式性能不是很好,为什么这么说呢,我们可以看下 synchronizedMap 方法实现:
// synchronizedMap 方法实现
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
// SynchronizedMap 类构造方法
SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
mutex = this;
}
SynchronizedMap(Map<K,V> m, Object mutex) {
this.m = m;
this.mutex = mutex;
}
public int size() {
synchronized (mutex) {return m.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return m.isEmpty();}
}
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
}
public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
public V remove(Object key) {
synchronized (mutex) {return m.remove(key);}
}
从代码实现可以看出,synchronizedMap 方法会创建一个SynchronizedMap 实例返回,在该实例的构造方法中将自己赋值给用来同步的对象,然后 SynchronizedMap 类中的方法都使用该同步的对象进行同步,以致于我们做的每一个操作都需要进行同步,其实就相当于给 WeakHashMap 类中实例方法都加上了 synchronized 关键字,这种实现方式性能难免会大打折扣。
ConcurrentReferenceHashMap
这种方式不可取的原因主要是因为 WeakHashMap 不是线程安全的,那有没有线程安全的并且实现了弱引用来保存元素的 Map 呢?当然上篇文章中的实现是一种方式,那如果也想像 WeakHashMap 一样将这些移除的操作完全封装到 Map 类里面呢。我们可以看下 org.springframework.util 包下的 ConcurrentReferenceHashMap 类,该类就很好的实现了我们想要的效果,在该类的描述中就提到了这样一段话:
This class can be used as an alternative to {@code Collections.synchronizedMap(new WeakHashMap<K, Reference<V>>())} in order to support better performance when accessed concurrently. This implementation follows the same design constraints as {@link ConcurrentHashMap} with the exception that {@code null} values and {@code null} keys are supported.
从描述中可以看到 ConcurrentReferenceHashMap 类可以用来替代使用 synchronizedMap 方法保证线程安全的 WeakHashMap 类,以便在并发访问时提供更好的性能。那就来看下采用 ConcurrentReferenceHashMap 类的实现方式:
public class WeakHashLock<T> {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
private static final ConcurrentReferenceHashMap.ReferenceType DEFAULT_REFERENCE_TYPE =
ConcurrentReferenceHashMap.ReferenceType.WEAK;
private final ConcurrentReferenceHashMap<T, ReentrantLock> referenceHashMap;
/**
* Create mutex factory with default settings.
*/
public WeakHashLock() {
this.referenceHashMap = new ConcurrentReferenceHashMap<>(DEFAULT_INITIAL_CAPACITY,
DEFAULT_LOAD_FACTOR,
DEFAULT_CONCURRENCY_LEVEL,
DEFAULT_REFERENCE_TYPE);
}
public WeakHashLock(int concurrencyLevel,
ConcurrentReferenceHashMap.ReferenceType referenceType) {
this.referenceHashMap = new ConcurrentReferenceHashMap<>(DEFAULT_INITIAL_CAPACITY,
DEFAULT_LOAD_FACTOR,
concurrencyLevel,
referenceType);
}
public ReentrantLock get(T key) {
return this.referenceHashMap.computeIfAbsent(key, lock -> new ReentrantLock());
}
}
上面代码实现同样非常简单,相比上面 WeakHashMap 的方式多了两个构造方法而已,但不同于使用 synchronizedMap 方法来保证线程安全的方式,性能会提高很多。如果你感兴趣的话可以去看下这个类的内部实现,原理都是利用了弱引用的特性,只不过实现方式有点不同而已。
这里我想要提醒两点,一个是 ConcurrentReferenceHashMap 中默认的引用类型是软引用。
private static final ReferenceType DEFAULT_REFERENCE_TYPE = ReferenceType.SOFT;
另外一个要注意的是 ConcurrentReferenceHashMap 中有的方法返回的结果是 GC 之后但还没有清理被回收元素之前的结果,什么意思呢,我们来看一个示例:
ConcurrentReferenceHashMap<String, String> referenceHashMap = new ConcurrentReferenceHashMap<>(16, 0.75f, 1, ConcurrentReferenceHashMap.ReferenceType.WEAK);
referenceHashMap.put("key", "value");
// 经过 GC 标记之后,弱引用已经进入创建时指定的队列中,这时可以去轮询队列移除元素了
System.gc();
// isEmpty 和 size 方法返回的结果是还没有移除元素的结果
System.out.println(referenceHashMap.isEmpty()); // false
System.out.println(referenceHashMap.size()); // 1
// get 方法中调用了移除元素的方法
System.out.println(referenceHashMap.get("key")); // null
System.out.println(referenceHashMap.isEmpty()); // true
System.out.println(referenceHashMap.size()); // 0
上面测试结果可以看到,在 GC 标记之后调用 isEmpty 和 size 方法得到的返回结果都表明集合中是还有元素,而调用 get 方法得到的却是个 null,然后再调用 isEmpty 和 size 方法得到的结果表示集合为空,这其实是因为前面两个方法里面没有做移除元素的操作,而 get 方法是先做了一次移除元素然后再去获取值,这里提醒下这个细节问题,避免以为 ConcurrentReferenceHashMap 没有实现移除元素的功能。
好了,上面都是利用弱引用特性再配合 ReentrantLock 实现了细粒度锁,这里就再顺便看下利用弱引用特性配合 synchronized 关键字的实现方式吧。同样,原理是一样,只不过从 ReentrantLock 再回到 synchronized,前面说了这么多的原理,就不再赘述了,直接看代码实现吧:
// 用于同步的对象
public class Mutex<T> {
private final T key;
public Mutex(T key) {
this.key = key;
}
public static <T> Mutex<T> of(T key) {
return new Mutex<>(key);
}
public T getKey() {
return key;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Mutex<?> xMutex = (Mutex<?>) o;
return Objects.equals(key, xMutex.key);
}
@Override
public int hashCode() {
return Objects.hash(key);
}
}
public class MutexFactory<T> {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
private static final ConcurrentReferenceHashMap.ReferenceType DEFAULT_REFERENCE_TYPE =
ConcurrentReferenceHashMap.ReferenceType.WEAK;
private final ConcurrentReferenceHashMap<T, Mutex<T>> referenceHashMap;
public MutexFactory() {
this.referenceHashMap = new ConcurrentReferenceHashMap<>(DEFAULT_INITIAL_CAPACITY,
DEFAULT_LOAD_FACTOR,
DEFAULT_CONCURRENCY_LEVEL,
DEFAULT_REFERENCE_TYPE);
}
public MutexFactory(int concurrencyLevel,
ConcurrentReferenceHashMap.ReferenceType referenceType) {
this.referenceHashMap = new ConcurrentReferenceHashMap<>(DEFAULT_INITIAL_CAPACITY,
DEFAULT_LOAD_FACTOR,
concurrencyLevel,
referenceType);
}
public Mutex<T> getMutex(T key) {
return this.referenceHashMap.computeIfAbsent(key, Mutex::new);
}
// 提供强制移除已经被回收的弱引用元素
public void purgeUnreferenced() {
this.referenceHashMap.purgeUnreferencedEntries();
}
}
由于我们一般实现的细粒度基本上是基于用户或者其他的需要同步的对象,上面是通过构建一个互斥对象作为 ConcurrentReferenceHashMap 的 value,然后我们就可以使用 synchronized 关键字来锁定该 value 对象达到同步的效果,使用方式如下:
MutexFactory<String> mutexFactory = new MutexFactory<>();
public void save(String userId) throws InterruptedException {
synchronized (mutexFactory.getMutex(userId)){
// do something
}
}
这种同步方式业务代码看起来简单些,对于一些简单的需求就可以直接使用这种方式,当然如果需要提供 API 级别的加锁方式或者需要构建带条件的加锁方式那还是使用 ReentrantLock。
对于加锁这一块虽然说了这么多,也许你已经打算采用这些方式去实现你想要的效果了,可是呢随着微服务大行其道,一个系统往往启动了好几个实例,每个实例对应一个 JVM 虚拟机,而我们前面说的这些都是在只有一个虚拟机的前提下才有用,这就意味着我们前面说的这些加锁方式基本上已经派不上用场了。
那随之而来的解决方案就是我们经常听到并且感觉很高大上,却很少用到的分布式锁了,这一块我虽然使用过,也去查阅过相关资料,但我自认为没有完全真正掌握底层的原理,还需要进一步的实践,只好再找机会整理整理后再输出了。
微信公众号:rookiedev,Java 后台开发,励志终身学习,坚持原创干货输出,你可选择现在就关注我,或者看看历史文章再关注也不迟。长按二维码关注,我们一起努力变得更优秀!
Java 细粒度锁续篇的更多相关文章
- Java细粒度锁实现的3种方式
最近在工作上碰见了一些高并发的场景需要加锁来保证业务逻辑的正确性,并且要求加锁后性能不能受到太大的影响.初步的想法是通过数据的时间戳,id等关键字来加锁,从而保证不同类型数据处理的并发性.而java自 ...
- Java 中常见的细粒度锁实现
上篇文章大致说了下 ReentrantLock 类的使用,对 ReentrantLock 类有了初步的认识之后让我们一起来看下基于 ReentrantLock 的几种细粒度锁实现. 这里我们还是接着用 ...
- lesson3:java的锁机制原理和分析
jdk1.5之前,我们对代码加锁(实际是对象加锁),都是采用Synchronized关键字来处理,jdk1.5及以后的版本中,并发编程大师Doug Lea在concurrrent包中提供了Lock机制 ...
- Java 各种锁的小结
一. synchronized 在 JDK 1.6 之前,synchronized 是重量级锁,效率低下. 从 JDK 1.6 开始,synchronized 做了很多优化,如偏向锁.轻量级锁.自旋锁 ...
- java的锁机制
一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在Java里边就是拿到某个同步对象的锁(一个对象只有一把锁): 如果这个时候同步对象的锁被其他线程拿走了,他(这个线 ...
- JAVA线程锁-读写锁
JAVA线程锁,除Lock的传统锁,又有两种特殊锁,叫读写锁ReadWriteLock 其中多个读锁不互斥,读锁和写锁互斥,写锁和写锁互斥 例子: /** * java线程锁分为读写锁 ReadWri ...
- Java线程锁一个简单Lock
/** * @author * * Lock 是java.util.concurrent.locks下提供的java线程锁,作用跟synchronized类似, * 单是比它更加面向对象,两个线程执行 ...
- paip.提升性能----java 无锁结构(CAS, Atomic, Threadlocal, volatile, 函数式编码, 不变对象)
paip.提升性能----java 无锁结构(CAS, Atomic, Threadlocal, volatile, 函数式编码, 不变对象) 1 锁的缺点 2 CAS(Compare ...
- Java偏向锁实现原理(Biased Locking)
http://kenwublog.com/theory-of-java-biased-locking 阅读本文的读者,需要对Java轻量级锁有一定的了解,知道lock record, mark wor ...
随机推荐
- LCCUP 2020 秋季编程大赛 补题
果然是力扣杯,难度较于平时周赛提高了不少,个人感觉最后两题并不太容易QAQ LCP 18.早餐组合 #二分思想 题目链接 题意 你获得了每种主食的价格,及每种饮料的价格,你需要选择一份主食和一份饮料, ...
- dubbo起停之服务消费
ReferenceAnnotationBeanPostProcessor继承了AnnotationInjectedBeanPostProcessors其实现了InstantiationAwareBea ...
- High-Resolution Image Inpainting using Multi-Scale Neural Patch Synthesis
论文来源:CVPR 2017 摘要 之前方法的缺点:之前的方法是基于语义和上下文信息的,在填充较大holes的表现得很好,能够捕获更高级的图像特征,但是由于内存限制和难以训练网络的因素,只能处理分辨率 ...
- 数据共享-spring batch(9)上下文处理
目录 1 引言 2 开发环境 3 基于 Mybatis-plus 集成多数据源的数据库访问 3.1 pom 文件中引入 Mybatis-plus 3.2 配置及使用多数据源 4 ExecutionCo ...
- Spring Boot 2.4.0 发布,配置文件重大调整,不要乱升级!!
前段时间 Spring Boot 2.4.0 发布了,栈长作了一个新特性全盘解读,其中介绍了一个很重要的变革,那就是配置文件. 配置文件可是每个框架的核心,不得不搞清楚,所以,这篇栈长就带大家深入实战 ...
- vm虚拟机安装centos7。克隆镜像以及快照
为了方便下次安装配置,保存一篇安装centos的文章 https://blog.csdn.net/wsq119/article/details/80635558 步骤非常详细,一看就会. 这一篇是关于 ...
- PyQt(Python+Qt)学习随笔:QMdiArea多文档界面部件的subWindowActivated信号
专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 QMdiArea的subWindowActivated在一个窗口激活( ...
- PyQt(Python+Qt)入门:Designer组件属性编辑界面中QWidget类相关属性详解
本文适用人员:本文比较长,适合不理解Qt Designer部件属性的人员阅读或资料查找. 声明: 1.如果有人认为本文是简单的复制粘贴+翻译而成,敬请读本文最后的后记: 2.本文为老猿Python学习 ...
- ATT&CK 实战 - 红日安全 vulnstack (二) 环境部署(劝退水文)
靶机下载地址:http://vulnstack.qiyuanxuetang.net/vuln/detail/3/ 靶场简述 红队实战系列,主要以真实企业环境为实例搭建一系列靶场,通过练习.视频教程.博 ...
- (四)一个bug的生命周期
Bug的属性 Bug重现环境 这个应该是我们重现BUG的一个前提,如果没有这个前提,我们可能会无法重现问题,或者根本就无从下手. • 操作系统 这个是一般软件运行的一大前提,基本上所有的软件都依赖于操 ...