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 ...
随机推荐
- zabbix 用Telegram报警!!!
第一步:先在Telegram 注册个机器人!!! @BotFather在Telegram中添加联系人并按"开始",然后键入: /newbot输入你要新建的机器人名称在电报中@你的机 ...
- php filesize不能统计临时文件
文件上传时要统计上传的文件的大小,使用filesize('文件名')的时候,其中 的文件名就得是文件在本地的临时文件但是会出现一个错误显示成 filesize(): stat failed for D ...
- 教你C 语言简单编程速成
我们将所有的 C 语言要素放置到一份易读的备忘录上. 1972 年,丹尼斯·里奇Dennis Ritchie任职于贝尔实验室Bell Labs,在几年前,他和他的团队成员发明了 Unix .在创建了一 ...
- kafka入门之broker--日志存储设计
kafaka并不是直接将原省消息写入日志文件的,相反,它会将消息和一些必要的元数据信息大宝在一起封装成一个record写入日志.其实就是我们之前介绍的batch 具体对每个日志而言,kafka又将其进 ...
- CentOS6.5配置KVM
///确认cpu是否支持kvm egrep '(vmx|svm)' --color=always /proc/cpuinfo ///安装包 yum -y install qemu-kvm libvir ...
- Android多触点总结
文章部分内容参考: http://blog.csdn.net/barryhappy/article/details/7392326 总结: 1. event.getX()可以得到x的坐标,里面的参数0 ...
- 《技术男征服美女HR》—Fiber、Coroutine和多线程那些事
1.起点 我叫小白,坐在这间属于华夏国超一流互联网公司企鹅巴巴的小会议室里,等着技术面试官的到来. 令我感到不舒服的,是坐在我对面的那位HR美女一个劲儿的盯着我打量!虽说本人帅气,但是也不能这么毫无顾 ...
- 第8.23节 Python中使用sort/sorted排序与“富比较”方法的关系分析
一. 引言 <第8.21节 Python中__lt__.gt__等 "富比较"("rich comparison")方法用途探究>和<第8.2 ...
- 第8.34节 《Python类中常用的特殊变量和方法》总结
本章介绍了Python类中常用的特殊变量和方法,这些特殊变量和方法都有特殊的用途,是Python强大功能的基石之一,许多功能非常有Python特色.由于Python中一切皆对象,理解这些特殊变量和方法 ...
- PyQt(Python+Qt)学习随笔:复选框状态枚举类Qt.CheckState取值及含义
老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 老猿Python,跟老猿学Python! 老猿Python博文目录 专栏:使用PyQt开发图形界面P ...