如何基於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 之類的鎖性能要好的。當然,我們這里針對的並發數都是極少的,我們只是想要保證這極少情況下的線程安全性。所以,其實也還好。

 

嘮叨: 靜下心來。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM