Java给ID加锁的实现
服务端开发锁无处不在,有时就是需要对用户或者某个资源加锁,防止被并发访问,我这里分别介绍单体应用和分布式应用对锁ID的实现。
单体应用
单体应用,可以使用Jvm的锁,高效简单。
String.intern确保ID的唯一性,再锁该ID
stackoverflow上有人提出了简单的解决方案:simple-java-name-based-locks
All those answers I see are way too complicated. Why not simply use:
public void executeInNamedLock(String lockName, Runnable runnable) {
synchronized(lockName.intern()) {
runnable.run();
}
}
The key point is the method intern: it ensures that the String returned is a global unique object, and so it can be used as a vm-instance-wide mutex. All interned Strings are held in a global pool, so that’s your static cache you were talking about in your original question. Don’t worry about memleaks; those strings will be gc’ed if no other thread references it. Note however, that up to and including Java6 this pool is kept in PermGen space instead of the heap, so you might have to increase it.
There’s a problem though if some other code in your vm locks on the same string for completely different reasons, but a) this is very unlikely, and b) you can get around it by introducing namespaces, e.g. executeInNamedLock(this.getClass().getName() + “_” + myLockName);
public void internLock(Integer id) {
synchronized (String.valueOf(id).intern()) {
biz(id);
}
}
使用Java自带的synchronized进行锁ID,由于synchronized是基于对象进行加锁,不能保证同一个ID是一个Java对象,我们需要将ID转换为String,并放入常量池中,保证每个ID都是对应一个唯一的对象。关于intern的详细介绍,可以看美团技术团队写的深入解析String#intern
完整代码:
List<Integer> list = ImmutableList.of(10001, 10002, 10003, 10004, 10005, 10001, 10003, 10003);
/**
* 模拟5秒钟耗时操作
*/
public void biz(Integer id) {
try {
Thread.sleep(5000);
log.info("id:{}", id);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 使用 String.intern
*
* @param id
*/
public void internLock(Integer id) {
synchronized (String.valueOf(id).intern()) {
biz(id);
}
}
@Test
public void testStringIntern() {
log.info("使用 String.intern");
list.forEach(it -> new Thread(() -> internLock(it)).start());
}
运行结果:
15:15:31.355 [Test worker] INFO LockTest - 使用 String.intern
15:15:36.369 [Thread-21] INFO LockTest - id:10002
15:15:36.369 [Thread-23] INFO LockTest - id:10004
15:15:36.369 [Thread-27] INFO LockTest - id:10003
15:15:36.369 [Thread-20] INFO LockTest - id:10001
15:15:36.369 [Thread-24] INFO LockTest - id:10005
15:15:41.377 [Thread-25] INFO LockTest - id:10001
15:15:41.377 [Thread-22] INFO LockTest - id:10003
15:15:46.387 [Thread-26] INFO LockTest - id:10003
可以看出list中的前五个元素10001, 10002, 10003, 10004, 10005
没有被阻塞,15:15:36
时出的结果,第6个和第7个元素10001,100003
被阻塞,耗时5秒钟。最后一个100003
又被前一个100003
阻塞,耗时5秒钟。从结果上看符合我们的预期。
使用Guava的Interners.newWeakInterner
使用 intern()有性能问题,可以用 guava 的 Interners.newWeakInterner()一个弱引用的内部常量池
具体代码:
Interner<Integer> pool = Interners.newWeakInterner();
public void guavaInternLock(Integer id) {
synchronized (pool.intern(id)) {
biz(id);
}
}
运行结果
15:15:11.339 [Test worker] INFO LockTest - 使用 Guava Interners
15:15:16.357 [Thread-12] INFO LockTest - id:10001
15:15:16.357 [Thread-13] INFO LockTest - id:10002
15:15:16.358 [Thread-14] INFO LockTest - id:10003
15:15:16.360 [Thread-15] INFO LockTest - id:10004
15:15:16.360 [Thread-16] INFO LockTest - id:10005
15:15:21.372 [Thread-19] INFO LockTest - id:10003
15:15:21.372 [Thread-17] INFO LockTest - id:10001
15:15:26.385 [Thread-18] INFO LockTest - id:10003
使用Guava的Striped
如果想实现更细力度的锁,可以使用 JUC
中的Lock,但是单纯使用Lock是无法区分id的,需要我们自己实现判重逻辑。可以使用Guava Striped
很轻松实现该功能。
final Striped<Lock> striped = Striped.lock(10);
public void stripedLock(Integer id) {
Lock lock = striped.get(id);
lock.lock();
try {
biz(id);
} finally {
lock.unlock();
}
}
运行结果
15:14:51.323 [Test worker] INFO LockTest - 使用 Guava Striped
15:14:56.339 [Thread-5] INFO LockTest - id:10002
15:14:56.339 [Thread-7] INFO LockTest - id:10004
15:14:56.339 [Thread-8] INFO LockTest - id:10005
15:14:56.339 [Thread-4] INFO LockTest - id:10001
15:14:56.339 [Thread-6] INFO LockTest - id:10003
15:15:01.354 [Thread-9] INFO LockTest - id:10001
15:15:01.354 [Thread-10] INFO LockTest - id:10003
15:15:06.357 [Thread-11] INFO LockTest - id:10003
分布式应用
由于分布式环境中,需要部署多个节点,上文提到的锁是基于单个JVM,不能保证同一时间某个资源只有一个线程访问,所以需要借助其他中间件。
Redis
关于Redis加锁的原理可以看我之前写的Redis进阶,我这里直接使用redisson封装好的工具。redisson
借助lua
脚本,实现了可重入锁、读写锁等多种锁。具体可参考分布式锁和同步器
private void redisLock(Integer id) {
final RLock lock = RedisClient.INSTANCE.getLock(id.toString());
lock.lock();
try {
biz(id);
} finally {
lock.unlock();
}
}
运行结果
2022-07-05 09:45:33 INFO (Thread-6 LockTest:76)- id:10003
2022-07-05 09:45:33 INFO (Thread-9 LockTest:76)- id:10001
2022-07-05 09:45:33 INFO (Thread-8 LockTest:76)- id:10005
2022-07-05 09:45:33 INFO (Thread-5 LockTest:76)- id:10002
2022-07-05 09:45:33 INFO (Thread-7 LockTest:76)- id:10004
2022-07-05 09:45:38 INFO (Thread-4 LockTest:76)- id:10001
2022-07-05 09:45:38 INFO (Thread-10 LockTest:76)- id:10003
2022-07-05 09:45:43 INFO (Thread-11 LockTest:76)- id:10003