Method and steps of realizing synchronous lock based on string
In some cases, we may want to do something based on strings. For example, it is more reasonable to use lock strings for concurrent synchronization operations of the same user. Because concurrent operations are not allowed only in the case of the same string. If we lock all of them indiscriminately, the overall performance will be greatly reduced.
Because of the diversity of strings, it seems that string locks are natural and have more advantages than advanced locks such as segmented locks.
Because the assignment of variables of type string is as follows: string a = "Hello world."; All of them often have a wrong image, and the string object is immutable.
Well, let's not elaborate on the debate on this issue. In short, "a"= "A" is possible.
In addition, for locking, we all know that locking is meaningful only for the same object. So, roughly, we can use string locks as follows:
public void method1() { String str1 = "a"; synchronized (str1) { // do sync a things... } } public void method2() { String str2 = "a"; synchronized (str2) { // do sync b things... } }
At first glance, this is really convenient and simple. However, as mentioned earlier, "a" may not be equal to "a" (this is most cases. String variables with the same value are equal only when string is stored in the constant pool).
Therefore, we can slightly optimize:
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... } }
It seems very convenient and simple. The principle is to put the string object into the constant pool. But there will be a problem. How to clean up the data of these constant pools?
Anyway, can we implement a lock based on string?
It must be OK! Directly on the code!
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); } }
When using, just pass in the lockkey.
// 加锁 StringBasedMutexLock.lock(linkKey); // 解锁 StringBasedMutexLock.unlock(linkKey);
Is there any advantage in doing so?
1. Using concurrenthashmap to achieve lock acquisition, the performance is still good;
2. Each string corresponds to a lock and is deleted after use, which will not cause memory overflow;
3. It can be used as an external tool, and the access of business code is convenient. There is no need to wrap the whole code like synchronized;
Deficiencies?
1. Using concurrenthashmap to achieve lock acquisition, the performance is still good;
2. Each string corresponds to a lock and is deleted after use, which will not cause memory overflow;
3. It can be used as an external tool, and the access of business code is convenient. There is no need to wrap the whole code like synchronized;
4. This article just wants to show the implementation of string lock, which is not suitable for concurrent processing in distributed scenarios;
Extension: if string is not used as a lock, how to ensure thread safety in low probability concurrency scenarios under the premise of large concurrency?
We know that the efficiency of CAS is relatively high. We can use atomic classes to operate CAS.
For example, we add a status field and operate it to ensure thread safety:
/** * 运行状态 * * 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); } }
In the actual test, the CAS performance is better than that of locks such as synchronized. Of course, the number of concurrencies we target here is very small. We just want to ensure thread safety in this rare case. So, it's actually okay.
summary
The above is my method of dealing with the real IP of the client. I hope the content of this article has a certain reference value for your study or work. Thank you for your support.