在某些时候,我们可能想基于字符串做一些事情,比如:针对同一用户的并发同步操作,使用锁字符串的方式实现比较合理。因为只有在相同字符串的情况下,并发操作才是不被允许的。而如果我们不分青红皂白直接全部加锁,那么整体性能就下降得厉害了。

  因为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实现锁?的更多相关文章

  1. 如何基于String实现同步锁?

    在某些时候,我们可能想基于字符串做一些事情,比如:针对同一用户的并发同步操作,使用锁字符串的方式实现比较合理.因为只有在相同字符串的情况下,并发操作才是不被允许的.而如果我们不分青红皂白直接全部加锁, ...

  2. 如何基于 String 实现同步锁?

    如何基于String实现同步锁? 在某些时候,我们可能想基于字符串做一些事情,比如:针对同一用户的并发同步操作,使用锁字符串的方式实现比较合理. 因为只有在相同字符串的情况下,并发操作才是不被允许的. ...

  3. RedLock.Net - 基于Redis分布式锁的开源实现

    工作中,经常会遇到分布式环境中资源访问冲突问题,比如商城的库存数量处理,或者某个事件的原子性操作,都需要确保某个时间段内只有一个线程在访问或处理资源. 因此现在网上也有很多的分布式锁的解决方案,有数据 ...

  4. c# 基于redis分布式锁

    在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量. 而同步的本质是通过锁来实现的.为了实现多个线程在 ...

  5. 基于Redis分布式锁(获取锁及解锁)

    目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题.分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency).可用性( ...

  6. 基于Redis分布式锁的正确打开方式

    分布式锁是在分布式环境下(多个JVM进程)控制多个客户端对某一资源的同步访问的一种实现,与之相对应的是线程锁,线程锁控制的是同一个JVM进程内多个线程之间的同步.分布式锁的一般实现方法是在应用服务器之 ...

  7. 基于 Redis 分布式锁

    1.主流分布式锁实现方案 基于数据库实现分布式锁 基于缓存(redis 等) 基于 Zookeeper 2.根据实现方式分类 : 类 CAS 自旋式分布式锁:询问的方式,类似 java 并发编程中的线 ...

  8. 基于redis分布式锁实现“秒杀”

    转载:http://blog.5ibc.net/p/28883.html 最近在项目中遇到了类似“秒杀”的业务场景,在本篇博客中,我将用一个非常简单的demo,阐述实现所谓“秒杀”的基本思路. 业务场 ...

  9. 基于AQS的锁

    锁分为独占锁和共享锁,它们的主要实现都是依靠AbstractQueuedSynchronizer,这个类只提供一系列公共的方法,让子类来调用.基于我了解不深,从这个类的属性,方法,和独占锁的获取方式去 ...

随机推荐

  1. cmd中,查询sqlcmd命令的选项

    像我这样的小白,有时候看到-d,-S,-P这些都不知道什么意思,后面知道了是一些命令的选项.如sqlcmd,打开cmd,输入sqlcmd -?        即可获得选项的含义. .

  2. VSCode 远程开发(带免密)

    VSCode 远程开发(带免密) 简介 Visual Studio Code(以下简称 VS Code)从1.35.0版本正式提供可以在本地编辑远程开发环境的文件的功能,具体实现如下图 安装完成Rem ...

  3. 《Java 8 in Action》Chapter 2:通过行为参数化传递代码

    你将了解行为参数化,这是Java 8非常依赖的一种软件开发模式,也是引入 Lambda表达式的主要原因.行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式.一言以蔽之,它意味 着拿出一个代码 ...

  4. Springboot源码分析之代理三板斧

    摘要: 在Spring的版本变迁过程中,注解发生了很多的变化,然而代理的设计也发生了微妙的变化,从Spring1.x的ProxyFactoryBean的硬编码岛Spring2.x的Aspectj注解, ...

  5. jQuery Validate 可选项

  6. getline()与get()(c++学习笔记)

    istream中的类(如cin)提供了一些面向行的类成员函数:getline()和get() 1.getline()函数 读取整行,使用回车键输入的换行符来确定输入结尾. 调用方法:cin.getli ...

  7. pyinstaller打包出错numpy.core.multiarray failed to import

    py原文件运行时正常,但用pyinstaller打包为exe后,在运行则报错: 这是因为cv2要求的numpy版本与你装的numpy版本不一样,导致冲突:网上很多说升级numpy,但你把numpy升的 ...

  8. Codeforces 976D

    题意略. 思路:构造题. 我们把全部的d[n]+1个点分作3部分来构造. 首先我们把原问题归约成构造d1.dn.和{d2 - d1,d3 - d1,.....,d[n-1] - d1}这样的问题,其中 ...

  9. Centos安装和配置Mysql5.7

    [root@localhost ~]# wget https://dev.mysql.com/get/mysql57-community-release-el7-11.noarch.rpm -bash ...

  10. 05_指针之New()函数的使用

    1.new函数是一个内置函数,表达式new(T)创建一个未命名的T类型变量,初始化为T类型的零值,并返回其地址(地址类型为*T)2.p:=new(int),q:=new(int)==>p!=q ...