searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

Java基于String实现同步锁

2024-05-28 02:19:30
27
0

1. 前言

在分布式应用中,能够通过 Redis、Zookeeper 等组件实现分布式锁。针对一个订单号、任务 ID 进行加锁,避免其它进程或者线程操作相同的资源。

但是,如果项目不依赖这些组件,单进程的情况下,针对不同订单号、任务 ID 进行加锁就变得比较困难。像是 ReentrantLock 或者 CountDownLatch 这种 JUC 实现的锁,都是针对单个资源的锁,不能用于变换莫测的 String。

2. 需要考虑的问题

  • 基于 String 的锁字典
  • 内存泄漏问题

我们需要做的,是根据 String 获取一把锁,来锁定该 String 代表的资源。既然不同的 String 代表着不同的锁,我们需要的是使用一个池子,来缓存 String 及其对应的锁,即 String 锁的字典。

这个锁字典的生命周期基本上是伴随着整个进程,因为我们使用的场景就是单个进程下根据 String 来获取锁。那么,如果大量地根据不同的 String 获取锁,就会在池子中缓存了大量的 key,value,不及时释放可能会导致内存泄漏问题。

3. 实现方法

1、基于 JVM 的字符串常量池

JVM里面有一块内存是用于储存字符串常量的,从这里面拿出来的字符串都是同一个对象。

但是我们自己创建的字符串是不同的对象,不在这个字符串常量池里面,而 synchronized是向对象加锁的,所以直接对字符串对象加锁,可能加到两个两个不同的对象上,就会出问题了。

因此可以使用 String 类的 intern() 方法,将字符串加到字符串常量池中,调用方法后,返回的是字符串常量池中的字符串对象。

实现

public final class StringMutexLock {  
    public void operation(String bizId){
        String lock = bizId; // 业务ID
		lock = lock.intern();
        synchronized (lock){
            // 实际业务
        }
    }
}

存在的问题

这种方法问题很大,因为字符串常量池中的对象不一定会被垃圾回收器回收,因此可能会造成内存泄露的问题。

2 、基于 ConcurrentHashMap 和 CountDownLatch

使用 ConcurrentHashMap 来储存 key 和对应的锁,加锁时判断 ConcurrentHashMap 是否已经存在对应的锁对象,解锁时将锁对象从 ConcurrentHashMap 中移除。

实现

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

@Slf4j
public final class StringMutexLock {
    private static final ConcurrentHashMap<String, CountDownLatch> lockHolder = new ConcurrentHashMap<>();

    /**
     * 基于lockKey上锁,同步执行
     * @param lockKey
     */
    public static void lock(String lockKey) throws InterruptedException {
        while (!tryLock(lockKey)){
            try {
                log.info("Get lock[{}] failed, waiting...", lockKey);
                blockOnLock(lockKey);
            }catch (InterruptedException e){
                String errMsg = String.format("Error occur while get lock[%s]: %s", lockKey, e.getMessage());
                throw new InterruptedException(errMsg);
            }
        }
    }

    /**
     * 基于lockKey, 释放锁, 只要调用者传入正确的lockKey, 锁就会被释放
     * @param lockKey
     */
    public static void unlock(String lockKey){
        CountDownLatch lock = lockHolder.remove(lockKey);
        lock.countDown();
        log.info("Release lock[{}] success.", lockKey);
    }
    
    
    /**
     * 尝试给指定字符串上锁
     * @param lockKey 字符串
     * @return true: 上锁成功  false:上锁失败
     */
    private static boolean tryLock(String lockKey){
        // 这里每次调用都会创建一个 CountDownLatch 对象,对GC不太友好
        return lockHolder.putIfAbsent(lockKey, new CountDownLatch(1)) == null;
    }

    /**
     * 获取实际锁对象,并阻塞等待
     * @param lockKey
     * @throws InterruptedException
     */
    private static void blockOnLock(String lockKey) throws InterruptedException {
        CountDownLatch lock = lockHolder.get(lockKey);
        if (lock != null){
            lock.await();
        }
    }
}

存在的问题

  • 每次调用 tryLock 都会创建一个 CountDownLatch 对象,对 GC 不太友好。
  • 不会记录获取锁的线程,其他线程通过 String 获取锁对象之后,可能会误释放锁。
  • 不支持的超时设置,需要主动释放锁。

3、Guava包的常量池

Google 的第三方工具包 Guava 里面提供了常量池的实现,能够创建​基于弱引用的常量池​​,避免 GC 问题。

实现

import com.google.common.collect.Interner;
import com.google.common.collect.Interners;

public final class StringMutexLock {
	private final Interner<String> internPool = Interners.newWeakInterner();
    
    public void operation(String bizId){
        String lock = internPool.intern(bizId);
        synchronized (lock){
            // 具体业务
        }
    }
}

存在的问题

  • 不会记录获取锁的线程,其他线程通过 String 获取锁对象之后,可能会误释放锁。
  • 不支持的超时设置,需要主动释放锁。
0条评论
作者已关闭评论
l****n
2文章数
0粉丝数
l****n
2 文章 | 0 粉丝
l****n
2文章数
0粉丝数
l****n
2 文章 | 0 粉丝
原创

Java基于String实现同步锁

2024-05-28 02:19:30
27
0

1. 前言

在分布式应用中,能够通过 Redis、Zookeeper 等组件实现分布式锁。针对一个订单号、任务 ID 进行加锁,避免其它进程或者线程操作相同的资源。

但是,如果项目不依赖这些组件,单进程的情况下,针对不同订单号、任务 ID 进行加锁就变得比较困难。像是 ReentrantLock 或者 CountDownLatch 这种 JUC 实现的锁,都是针对单个资源的锁,不能用于变换莫测的 String。

2. 需要考虑的问题

  • 基于 String 的锁字典
  • 内存泄漏问题

我们需要做的,是根据 String 获取一把锁,来锁定该 String 代表的资源。既然不同的 String 代表着不同的锁,我们需要的是使用一个池子,来缓存 String 及其对应的锁,即 String 锁的字典。

这个锁字典的生命周期基本上是伴随着整个进程,因为我们使用的场景就是单个进程下根据 String 来获取锁。那么,如果大量地根据不同的 String 获取锁,就会在池子中缓存了大量的 key,value,不及时释放可能会导致内存泄漏问题。

3. 实现方法

1、基于 JVM 的字符串常量池

JVM里面有一块内存是用于储存字符串常量的,从这里面拿出来的字符串都是同一个对象。

但是我们自己创建的字符串是不同的对象,不在这个字符串常量池里面,而 synchronized是向对象加锁的,所以直接对字符串对象加锁,可能加到两个两个不同的对象上,就会出问题了。

因此可以使用 String 类的 intern() 方法,将字符串加到字符串常量池中,调用方法后,返回的是字符串常量池中的字符串对象。

实现

public final class StringMutexLock {  
    public void operation(String bizId){
        String lock = bizId; // 业务ID
		lock = lock.intern();
        synchronized (lock){
            // 实际业务
        }
    }
}

存在的问题

这种方法问题很大,因为字符串常量池中的对象不一定会被垃圾回收器回收,因此可能会造成内存泄露的问题。

2 、基于 ConcurrentHashMap 和 CountDownLatch

使用 ConcurrentHashMap 来储存 key 和对应的锁,加锁时判断 ConcurrentHashMap 是否已经存在对应的锁对象,解锁时将锁对象从 ConcurrentHashMap 中移除。

实现

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

@Slf4j
public final class StringMutexLock {
    private static final ConcurrentHashMap<String, CountDownLatch> lockHolder = new ConcurrentHashMap<>();

    /**
     * 基于lockKey上锁,同步执行
     * @param lockKey
     */
    public static void lock(String lockKey) throws InterruptedException {
        while (!tryLock(lockKey)){
            try {
                log.info("Get lock[{}] failed, waiting...", lockKey);
                blockOnLock(lockKey);
            }catch (InterruptedException e){
                String errMsg = String.format("Error occur while get lock[%s]: %s", lockKey, e.getMessage());
                throw new InterruptedException(errMsg);
            }
        }
    }

    /**
     * 基于lockKey, 释放锁, 只要调用者传入正确的lockKey, 锁就会被释放
     * @param lockKey
     */
    public static void unlock(String lockKey){
        CountDownLatch lock = lockHolder.remove(lockKey);
        lock.countDown();
        log.info("Release lock[{}] success.", lockKey);
    }
    
    
    /**
     * 尝试给指定字符串上锁
     * @param lockKey 字符串
     * @return true: 上锁成功  false:上锁失败
     */
    private static boolean tryLock(String lockKey){
        // 这里每次调用都会创建一个 CountDownLatch 对象,对GC不太友好
        return lockHolder.putIfAbsent(lockKey, new CountDownLatch(1)) == null;
    }

    /**
     * 获取实际锁对象,并阻塞等待
     * @param lockKey
     * @throws InterruptedException
     */
    private static void blockOnLock(String lockKey) throws InterruptedException {
        CountDownLatch lock = lockHolder.get(lockKey);
        if (lock != null){
            lock.await();
        }
    }
}

存在的问题

  • 每次调用 tryLock 都会创建一个 CountDownLatch 对象,对 GC 不太友好。
  • 不会记录获取锁的线程,其他线程通过 String 获取锁对象之后,可能会误释放锁。
  • 不支持的超时设置,需要主动释放锁。

3、Guava包的常量池

Google 的第三方工具包 Guava 里面提供了常量池的实现,能够创建​基于弱引用的常量池​​,避免 GC 问题。

实现

import com.google.common.collect.Interner;
import com.google.common.collect.Interners;

public final class StringMutexLock {
	private final Interner<String> internPool = Interners.newWeakInterner();
    
    public void operation(String bizId){
        String lock = internPool.intern(bizId);
        synchronized (lock){
            // 具体业务
        }
    }
}

存在的问题

  • 不会记录获取锁的线程,其他线程通过 String 获取锁对象之后,可能会误释放锁。
  • 不支持的超时设置,需要主动释放锁。
文章来自个人专栏
文章 | 订阅
0条评论
作者已关闭评论
作者已关闭评论
0
0