如何基于String实现同步锁?
在某些时候,我们可能想基于字符串做一些事情,比如:针对同一用户的并发同步操作,使用锁字符串的方式实现比较合理。因为只有在相同字符串的情况下,并发操作才是不被允许的。而如果我们不分青红皂白直接全部加锁,那么整体性能就下降得厉害了。
因为string的多样性,看起来string锁是天然比分段锁之类的高级锁更有优势呢。
因为String 类型的变量赋值是这样的: String a = "hello world."; 所有往往会有个错误的映象,String对象就是不可变的。
额,关于这个问题的争论咱们就不细说了,总之, "a" != "a" 是有可能成立的。
另外,针对上锁这件事,我们都知道,锁是要针对同一个对象,才会有意义。所以,粗略的,我们可以这样使用字符串锁:

public void method1() {
String str1 = "a";
synchronized (str1) {
// do sync a things...
}
}
public void method2() {
String str2 = "a";
synchronized (str2) {
// do sync b things...
}
}

乍一看,这的确很方便简单。但是,前面说了, "a" 是可能不等于 "a" 的(这是大部分情况,只有当String被存储在常量池中时值相同的String变量才相等)。
所以,我们可以稍微优化下:

public void method3() {
String str1 = "a";
synchronized (str1.intern()) {
// do sync a things...
}
}
public void method4() {
String str2 = "a";
synchronized (str2.intern()) {
// do sync b things...
}
}

看起来还是很方便简单的,其原理就是把String对象放到常量池中。但是会有个问题,这些常量池的数据如何清理呢?
不管怎么样,我们是不是可以自己去基于String实现一个锁呢?
肯定是可以的了!直接上代码!

import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch; /**
* 基于string 的锁实现
*/
public final class StringBasedMutexLock { private static final Logger logger = LoggerFactory.getLogger(StringBasedMutexLock.class); /**
* 字符锁 管理器, 将每个字符串 转换为一个 CountDownLatch
*
* 即锁只会发生在真正有并发更新 同一个 String 的情况下
*
*/
private static final ConcurrentMap<String, CountDownLatch> lockKeyHolder = new ConcurrentHashMap<>(); /**
* 基于lockKey 上锁,同步执行
*
* @param lockKey 字符锁
*/
public static void lock(String lockKey) {
while (!tryLock(lockKey)) {
try {
logger.debug("【字符锁】并发更新锁升级, {}", lockKey);
blockOnSecondLevelLock(lockKey);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.error("【字符锁】中断异常:" + lockKey, e);
break;
}
}
} /**
* 释放 lockKey 对应的锁选项,使其他线程可执行
*
* @param lockKey 要使用互斥的字符串
* @return true: 释放成功, false: 释放失败,可能被其他线程误释放
*/
public static boolean unlock(String lockKey) {
// 先删除锁,再释放锁,此处会导致后续进来的并发优先执行,无影响
CountDownLatch realLock = getAndReleaseLock1(lockKey);
releaseSecondLevelLock(realLock);
return true;
} /**
* 尝试给指定字符串上锁
*
* @param lockKey 要使用互斥的字符串
* @return true: 上锁成功, false: 上锁失败
*/
private static boolean tryLock(String lockKey) {
// 此处会导致大量 ReentrantLock 对象创建吗?
// 其实不会的,这个数量最大等于外部并发数,只是对 gc 不太友好,会反复创建反复销毁y
return lockKeyHolder.putIfAbsent(lockKey, new CountDownLatch(1)) == null;
} /**
* 释放1级锁(删除) 并返回重量级锁
*
* @param lockKey 字符锁
* @return 真正的锁
*/
private static CountDownLatch getAndReleaseLock1(String lockKey) {
return lockKeyHolder.remove(lockKey);
} /**
* 二级锁锁定(锁升级)
*
* @param lockKey 锁字符串
* @throws InterruptedException 中断时抛出异常
*/
private static void blockOnSecondLevelLock(String lockKey) throws InterruptedException {
CountDownLatch realLock = getRealLockByKey(lockKey);
// 为 null 说明此时锁已被删除, next race
if(realLock != null) {
realLock.await();
}
} /**
* 二级锁解锁(如有必要)
*
* @param realLock 锁实例
*/
private static void releaseSecondLevelLock(CountDownLatch realLock) {
realLock.countDown();
} /**
* 通过key 获取对应的锁实例
*
* @param lockKey 字符串锁
* @return 锁实例
*/
private static CountDownLatch getRealLockByKey(String lockKey) {
return lockKeyHolder.get(lockKey);
} }

使用时,只需传入 lockKey 即可。
// 加锁
StringBasedMutexLock.lock(linkKey);
// 解锁
StringBasedMutexLock.unlock(linkKey);
这样做有什么好处吗?
1. 使用ConcurrentHashMap实现锁获取,性能还是不错的;
2. 每个字符串对应一个锁,使用完成后就删除,不会导致内存溢出问题;
3. 可以作为一个外部工具使用,业务代码接入方便,无需像 synchronized 一样,需要整段代码包裹起来;
不足之处?
1. 使用ConcurrentHashMap实现锁获取,性能还是不错的;
2. 每个字符串对应一个锁,使用完成后就删除,不会导致内存溢出问题;
3. 可以作为一个外部工具使用,业务代码接入方便,无需像 synchronized 一样,需要整段代码包裹起来;
4. 本文只是想展示实现 String 锁,此锁并不适用于分布式场景下的并发处理;
扩展: 如果不使用 String 做锁,如何保证大并发前提下的小概率并发场景的线程安全?
我们知道 CAS 的效率是比较高的,我们可以使用原子类来进行CAS的操作。
比如,我们添加一状态字段, 操作此字段以保证线程安全:

/**
* 运行状态
*
* 4: 正在删除, 1: 正在放入队列中, 0: 正常无运行
*/
private transient volatile AtomicInteger runningStatus = new AtomicInteger(0); // 更新时先获取该状态:
public void method5() {
AtomicInteger runningStatus = link.getRunningStatus();
// 正在删除数据过程中,则等待
if(!runningStatus.compareAndSet(0, 1)) {
// 1. 等待另外线程删除完成
// 2. 删除正在更新标识
// 3. 重新运行本次数据放入逻辑
long lockStartTime = System.currentTimeMillis();
long maxLockTime = 10 * 1000;
while (!runningStatus.compareAndSet(0, 1)) {
if(System.currentTimeMillis() - lockStartTime > maxLockTime) {
break;
}
}
runningStatus.compareAndSet(1, 0);
throw new RuntimeException("数据正在更新,重新运行: " + link.getLinkKey() + link);
}
try {
// do sync things
}
finally {
runningStatus.compareAndSet(1, 0);
}
} public void method6() {
AtomicInteger runningStatus = link.getRunningStatus();
if (!runningStatus.compareAndSet(0, 4)) {
logger.error(" 数据正在更新中,不得删除,返回 ");
return;
}
try {
// do sync things
}
catch (Exception e) {
logger.error("并发更新异常:", e);
}
finally {
runningStatus.compareAndSet(4, 0);
}
}

实际测试下来,CAS 性能是要比 synchronized 之类的锁性能要好的。当然,我们这里针对的并发数都是极少的,我们只是想要保证这极少情况下的线程安全性。所以,其实也还好。
唠叨: 静下心来。
如何基于String实现同步锁?的更多相关文章
- 如何基于 String 实现同步锁?
如何基于String实现同步锁? 在某些时候,我们可能想基于字符串做一些事情,比如:针对同一用户的并发同步操作,使用锁字符串的方式实现比较合理. 因为只有在相同字符串的情况下,并发操作才是不被允许的. ...
- 如何基于String实现锁?
在某些时候,我们可能想基于字符串做一些事情,比如:针对同一用户的并发同步操作,使用锁字符串的方式实现比较合理.因为只有在相同字符串的情况下,并发操作才是不被允许的. 因为String 类型的变量赋值是 ...
- Java中String做为synchronized同步锁使用详解
Java中使用String作同步锁 在Java中String是一种特殊的类型存在,在jdk中String在创建后是共享常量池的,即使在jdk1.8之后实现有所不同,但是功能还是差不多的. 借助这个特点 ...
- 同步锁Lock
用于解决多线程安全问题有三种方式: 同步代码块(隐式锁,基于JVM) 同步方法(隐式锁,基于JVM) 同步锁(显式锁,jdk1.5后出现,相对于前两种方式,更加灵活) 下面通过一段程序来说明一下同步锁 ...
- java 多线程: Thread 并发访问-代码块同步synchronized {};String作为被锁的对象
方法同步的弊端 方法同步的时候,如果一个方法需要线程安全控制的代码速度其实很快,但是还有其他的业务逻辑代码耗时非常长(比如网络请求),这样所有的线程就在这一块就等待着了,这样造成了极大的资源浪费如果并 ...
- 基于redis 实现分布式锁的方案
在电商项目中,经常有秒杀这样的活动促销,在并发访问下,很容易出现上述问题.如果在库存操作上,加锁就可以避免库存卖超的问题.分布式锁使分布式系统之间同步访问共享资源的一种方式 基于redis实现分布式锁 ...
- 基于 Redis 的分布式锁
前言 分布式锁在分布式应用中应用广泛,想要搞懂一个新事物首先得了解它的由来,这样才能更加的理解甚至可以举一反三. 首先谈到分布式锁自然也就联想到分布式应用. 在我们将应用拆分为分布式应用之前的单机系统 ...
- 基于redis的分布式锁实现
1.分布式锁介绍 在计算机系统中,锁作为一种控制并发的机制无处不在. 单机环境下,操作系统能够在进程或线程之间通过本地的锁来控制并发程序的行为.而在如今的大型复杂系统中,通常采用的是分布式架构提供服务 ...
- 基于ZooKeeper实现——分布式锁与实现
引言 ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件.它是一个为分布式应用提供一致性服务的软件,提 ...
随机推荐
- 自定义字段的设计与实现(Java实用版)
前言 自定义字段又叫做"开放模型",用户可以根据自已的需求,添加需要的字段,实现个性化定制. 使用自定义字段的目的,使用自定义字段解决哪些问题 如现有一套CRM系统,客户模块中客户 ...
- maven项目转换为gradle项目
进入到项目更目录,运行 gradle init --type pom 上面的命令会根据pom文件自动生成gradle项目所需的文件和配置,然后以gradle项目重新导入即可.
- sql server update语句
update语句 --Update 语句用于修改表中的数据 语法:update 表名称 set 列名称 = 新值 where 列名称 = 某值 --更新某一行的若干列,set字句中用','隔开
- PHP扩展开发01:第一个扩展
我们先假设业务场景,是需要有这么一个扩展,提供一个叫ccvita_string的函数,他的主要作用是返回一段字符.(这个业务场景实在太假,大家就这么看看吧)对应的PHP代码可能是这样: functio ...
- hdu 6399 City Development
vjudge 读进来可能会有相同的\(n_i\),不过在相同的\(n_i\)中只有最后一个是有用的,所以其他的要缩起来,缩完后这些\(n\)的数量不会超过19个 可以发现一个城市的答案为所有城市初始权 ...
- ubuntu修改apt源
1.修改之前首先做好备份 sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak 2.编辑源列表文件 sudo vim /etc/apt/sou ...
- 基于 docker容器搭建机器学习环境
下载docker镜像 docker pull ubuntu:18.04 进入ubuntu系统命令 docker run -ti ubuntu /bin/bash 正确退出系统方式 先按,ctrl+p ...
- iOS去掉字符串中的HTML标签的方法
方法一.NSScanner去除标签 - (NSString *)removeTheHtmlFromString:(NSString *)htmlString { NSScanner * scanner ...
- Label 自适应文本(StoryBoard/xib)
To make your label automatically resize height you need to do following: Set layout constrains for l ...
- fiddler获取响应时间以及服务器IP
抓包工具fiddler实现http协议请求应答抓包.在接口测试.性能测试.安全测试等软件测试活动过程中,可能会遇到需要获取接口响应时间.接口服务器IP这样的情况.默认情况下fiddler不支持接口响应 ...