如何基于 String 实现同步锁?

本文探讨如何使用String实现同步锁,强调在相同字符串情况下保证并发操作的同步。通过避免全量加锁提升性能,利用String常量池特性优化,并给出使用ConcurrentHashMap实现的自定义锁代码示例,该锁适用于小概率并发场景,但不适用于分布式环境。同时提到,对于大并发场景,可以考虑使用CAS操作确保线程安全。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

如何基于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变量才相等)。5个刁钻的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实现一个锁呢?关注微信公众号:Java技术栈,在后台回复:Java,可以获取我整理的 N 篇 Java 教程,都是干货。

肯定是可以的了!直接上代码!

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 即可。 
这样做有什么好处吗?

  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 之类的锁性能要好的。

当然,我们这里针对的并发数都是极少的,我们只是想要保证这极少情况下的线程安全性。所以,其实也还好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值