1. 前言
在分布式应用中,能够通过 Redis、Zookeeper 等组件实现分布式锁。针对一个订单号、任务 ID 进行加锁,避免其它进程或者线程操作相同的资源。
但是,如果项目不依赖这些组件,单进程的情况下,针对不同订单号、任务 ID 进行加锁就变得比较困难。像是 ReentrantLock 或者 CountDownLatch 这种 JUC 实现的锁,都是针对单个资源的锁,不能用于变换莫测的 String。
2. 需要考虑的问题
- 基于 String 的锁字典
- 内存泄漏问题
我们需要做的,是根据 String 获取一把锁,来锁定该 String 代表的资源。既然不同的 String 代表着不同的锁,我们需要的是使用一个池子,来缓存 String 及其对应的锁,即 String 锁的字典。
这个锁字典的生命周期基本上是伴随着整个进程,因为我们使用的场景就是单个进程下根据 String 来获取锁。那么,如果大量地根据不同的 String 获取锁,就会在池子中缓存了大量的 key,value,不及时释放可能会导致内存泄漏问题。
3. 实现方法
1、基于 JVM 的字符串常量池
JVM里面有一块内存是用于储存字符串常量的,从这里面拿出来的字符串都是同一个对象。
但是我们自己创建的字符串是不同的对象,不在这个字符串常量池里面,而 synchronized是向对象加锁的,所以直接对字符串对象加锁,可能加到两个两个不同的对象上,就会出问题了。
因此可以使用 String 类的 intern() 方法,将字符串加到字符串常量池中,调用方法后,返回的是字符串常量池中的字符串对象。
实现
public final class StringMutexLock {
public void operation(String bizId){
String lock = bizId; // 业务ID
lock = lock.intern();
synchronized (lock){
// 实际业务
}
}
}
存在的问题
这种方法问题很大,因为字符串常量池中的对象不一定会被垃圾回收器回收,因此可能会造成内存泄露的问题。
2 、基于 ConcurrentHashMap 和 CountDownLatch
使用 ConcurrentHashMap 来储存 key 和对应的锁,加锁时判断 ConcurrentHashMap 是否已经存在对应的锁对象,解锁时将锁对象从 ConcurrentHashMap 中移除。
实现
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
@Slf4j
public final class StringMutexLock {
private static final ConcurrentHashMap<String, CountDownLatch> lockHolder = new ConcurrentHashMap<>();
/**
* 基于lockKey上锁,同步执行
* @param lockKey
*/
public static void lock(String lockKey) throws InterruptedException {
while (!tryLock(lockKey)){
try {
log.info("Get lock[{}] failed, waiting...", lockKey);
blockOnLock(lockKey);
}catch (InterruptedException e){
String errMsg = String.format("Error occur while get lock[%s]: %s", lockKey, e.getMessage());
throw new InterruptedException(errMsg);
}
}
}
/**
* 基于lockKey, 释放锁, 只要调用者传入正确的lockKey, 锁就会被释放
* @param lockKey
*/
public static void unlock(String lockKey){
CountDownLatch lock = lockHolder.remove(lockKey);
lock.countDown();
log.info("Release lock[{}] success.", lockKey);
}
/**
* 尝试给指定字符串上锁
* @param lockKey 字符串
* @return true: 上锁成功 false:上锁失败
*/
private static boolean tryLock(String lockKey){
// 这里每次调用都会创建一个 CountDownLatch 对象,对GC不太友好
return lockHolder.putIfAbsent(lockKey, new CountDownLatch(1)) == null;
}
/**
* 获取实际锁对象,并阻塞等待
* @param lockKey
* @throws InterruptedException
*/
private static void blockOnLock(String lockKey) throws InterruptedException {
CountDownLatch lock = lockHolder.get(lockKey);
if (lock != null){
lock.await();
}
}
}
存在的问题
- 每次调用 tryLock 都会创建一个 CountDownLatch 对象,对 GC 不太友好。
- 不会记录获取锁的线程,其他线程通过 String 获取锁对象之后,可能会误释放锁。
- 不支持的超时设置,需要主动释放锁。
3、Guava包的常量池
Google 的第三方工具包 Guava 里面提供了常量池的实现,能够创建基于弱引用的常量池,避免 GC 问题。
实现
import com.google.common.collect.Interner;
import com.google.common.collect.Interners;
public final class StringMutexLock {
private final Interner<String> internPool = Interners.newWeakInterner();
public void operation(String bizId){
String lock = internPool.intern(bizId);
synchronized (lock){
// 具体业务
}
}
}
存在的问题
- 不会记录获取锁的线程,其他线程通过 String 获取锁对象之后,可能会误释放锁。
- 不支持的超时设置,需要主动释放锁。