基于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 之类的锁性能要好的。当然,我们这里针对的并发数都是极少的,我们只是想要保证这极少情况下的线程安全性。所以,其实也还好。

总结

以上就是我在处理客户端真实IP的方法,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

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

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

  • 基于Redis实现分布式锁的方法(lua脚本版)

    1.前言 在Java中,我们通过锁来避免由于竞争而造成的数据不一致问题.通常我们使用synchronized .Lock来实现.但是Java中的锁只能保证在同一个JVM进程内中可用,在跨JVM进程,例如分布式系统上则不可靠了. 2.分布式锁 分布式锁,是一种思想,它的实现方式有很多,如基于数据库实现.基于缓存(Redis等)实现.基于Zookeeper实现等等.为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件 互斥性:在任意时刻,只有一个客户端能持有锁. 不会发生死锁:即使客户端

  • 基于docker搭建nginx文件服务器的方法步骤

    1.在本机新建配置文件docker_nginx.conf server { listen 7070; server_name localhost; charset utf-8; location /files { #在docker内nginx的目录 alias /home/files; expires 1d; allow all; autoindex on; } 2.启动命令 docker run --name nginx -d -p 7070:7070 -v D:\dev\nginx-1.13

  • 基于VS2019配置opencv4.0的方法步骤

    前言: 不得不说网上搜到的垃圾配置方案真是多,基本上没几个能用的,东拼西凑花了一上午弄好了自己的环境 opencv4.0下载,vs2019下载 不说废话,直接上图干净利落 1.创建新空白项目 2.添加一个主文件 3.配置opencv环境 4. 链接器配置 5. 将opencv添加到计算机环境中 6. 将文件复制到C盘的C:\Windows\SysWOW64和C:\Windows\System32文件夹中 7.最后一步:运行测试环境,测试图片自己加进去,要用双斜杠. #include <openc

  • Redis主从同步配置的方法步骤(图文)

    一丶主从概念 一个master可以拥有多个slave,一个slave又可以拥有多个slave,如此下去,形成了强大的多级服务器集群架构 master用来写数据,slave用来读数据,经统计:网站的读写比率是10:1 通过主从配置可以实现读写分离 master和slave都是一个redis实例(redis服务) 二丶主从配置 说明:搭建redis主服务和从服务可以在同一台电脑上搭建,也可以在不同电脑上搭建,博主这里使用一台电脑进行搭建 1.配置主 step1 查看电脑中的ip地址 step2 编辑

  • 搭建基于express框架运行环境的方法步骤

    一.Express简介 Express提供了一个轻量级模块,把Node.js的http模块功能封装在一个简单易用的接口中.Express也扩展了http模块的功能,使你轻松处理服务器的路由.响应.cookie和HTTP请求的状态.使用Express可以充当Web服务器. 二.搭建基于express框架运行环境  开发后端的node服务 1.安装express ① 安装全局变量 npm install express-generator -g (全局变量会在C盘node文件下) ②查看安装成功:e

  • Java对象级别与类级别的同步锁synchronized语法示例

    目录 1.对象级别的同步锁 2.类级别的同步锁 3.总结 Java synchronized 关键字 可以将一个代码块或一个方法标记为同步代码块.同步代码块是指同一时间只能有一个线程执行的代码,并且执行该代码的线程持有同步锁.synchronized关键字可以作用于 一个代码块 一种方法 当一个方法或代码块被声明为synchronized时,如果一个线程正在执行该synchronized 方法或代码块,其他线程会被阻塞,直到持有同步锁的线程释放.根据锁定的范围可以分为 类级别的锁可以防止多个线程

  • 基于redis实现分布式锁的原理与方法

    前言 系统的不断扩大,分布式锁是最基本的保障.与单机的多线程不一样的是,分布式跨多个机器.线程的共享变量无法跨机器. 为了保证一个在高并发存场景下只能被同一个线程操作,java并发处理提供ReentrantLock或Synchronized进行互斥控制.但是这仅仅对单机环境有效.我们实现分布式锁大概通过三种方式. redis实现分布式锁 数据库实现分布式锁 zk实现分布式锁 今天我们介绍通过redis实现分布式锁.实际上这三种和java对比看属于一类.都是属于程序外部锁. 原理剖析 上述三种分布

  • java同步锁的正确使用方法(必看篇)

    同步锁分类 对象锁(this) 类锁(类的字节码文件对象即类名.class) 字符串锁(比较特别) 应用场景 在多线程下对共享资源的安全操作. 需求:启动5个线程对共享资源total进行安全操作. 同步锁在多线程单例模式下的使用 以上三类同步锁都可以. package cn.myThread; public class MyThread implements Runnable { private static int total = 10; @Override public void run()

  • Java线程使用同步锁交替执行打印奇数偶数的方法

    对同一个对象进行多线程操作时,如何保证线程执行结果的一致性?我们需要对线程操作对象加同步锁.(这是一道面试题) 需求描述 1-20个数字 A线程打印奇数:1,3,5,7,9,11,13,15,17,19 B线程打印偶数:2,4,6,8,10,12,14,16,18,20 C线程在AB两个线程执行完了之后打印结果:"success". 线程代码实现 Num.java package com.boonya.thread.test; /** * @ClassName: Num * @Desc

随机推荐