基于String实现同步锁的方法步骤

 更新时间:2019年09月24日 08:36:36   作者:等你归去来  
这篇文章主要给大家介绍了关于基于String实现同步锁的方法步骤,文中通过示例代码介绍的非常详细,对大家学习或者使用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 之类的锁性能要好的。当然,我们这里针对的并发数都是极少的,我们只是想要保证这极少情况下的线程安全性。所以,其实也还好。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对脚本之家的支持。

相关文章

  • 详解java.lang.reflect.Modifier.isInterface()方法

    详解java.lang.reflect.Modifier.isInterface()方法

    这篇文章主要介绍了详解java.lang.reflect.Modifier.isInterface()方法的相关资料,这里提供实例帮助大家理解这个方法的使用,需要的朋友可以参考下
    2017-09-09
  • java Spring MVC4环境搭建实例详解(步骤)

    java Spring MVC4环境搭建实例详解(步骤)

    spring WEB MVC框架提供了一个MVC(model-view-controller)模型-视图-控制器的结构和组件,利用它可以开发更灵活、松耦合的web应用。MVC模式使得整个服务应用的各部分(控制逻辑、业务逻辑、UI界面展示)分离开来,使它们之间的耦合性更低
    2017-08-08
  • flyway实现java 自动升级SQL脚本的问题及解决方法

    flyway实现java 自动升级SQL脚本的问题及解决方法

    大家在平时开发自己写SQL语句忘记在所有环境执行,需要新增环境做数据迁移,那么遇到这样的问题该如何解决呢?本文通过场景分析给大家介绍java 自动升级SQL脚本的策略,感兴趣的朋友一起看看吧
    2021-07-07
  • JAVA OutputStreamWriter流的实现

    JAVA OutputStreamWriter流的实现

    OutputStreamWriter是从字符流到字节流的桥接,它使用的字符集可以通过名称指定,也可以明确指定,或者可以接受平台的默认字符集,本文详细的介绍了JAVA OutputStreamWriter流的使用,感兴趣的可以了解一下
    2021-06-06
  • Struts2拦截器登录验证实例

    Struts2拦截器登录验证实例

    本篇文章主要介绍了Struts2拦截器登录验证实例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-05-05
  • java数据库操作类演示实例分享(java连接数据库)

    java数据库操作类演示实例分享(java连接数据库)

    java数据库操作类演示实例分享,大家参考使用吧
    2013-12-12
  • Java中switch的三种用法方式

    Java中switch的三种用法方式

    这篇文章主要介绍了Java中switch的三种用法方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • java 基础之final、finally和finalize的区别

    java 基础之final、finally和finalize的区别

    这篇文章主要介绍了java 基础之final、finally和finalize的区别的相关资料,需要的朋友可以参考下
    2017-05-05
  • Java基础之数组详解

    Java基础之数组详解

    这篇文章主要介绍了Java基础之数组详解,文中有非常详细的代码示例,对正在学习java的小伙伴们有很好的帮助,需要的朋友可以参考下
    2021-04-04
  • 如何获取java新IO的Path文件大小

    如何获取java新IO的Path文件大小

    这篇文章主要介绍了如何获取java新IO的Path文件大小,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-09-09

最新评论