簡體   English   中英

如何使此代碼線程安全

[英]How can I make this code thread-safe

以下代碼中的testMethod()在運行時由很多線程訪問。 如果在地圖中找到該條目,它接受'firstName'並立即返回'lastName'。 如果沒有,它會在API中查找姓氏,更新地圖並返回相同的名稱。 現在這個方法確實在相同的地圖數據結構中放置和獲取操作,我認為這不是“線程安全的”。 我現在很困惑是使函數'同步'還是使用ConcurrentHashMap而不是HashMap

public class Sample {

    Map<String, String> firstNameToLastName = new HashMap<>();

    public String testMethod(String firstName) {
        String lastName = firstNameToLastName.get(firstName);

        if (lastName!= null)
            return lastName;

        String generateLastName = SomeAPI.generateLastName(firstName);

        firstNameToLastName.put(firstName, generateLastName);

        return generateLastName;
    }
}

您的代碼不是線程安全的,這是正確的。 這導致了以下問題,其中存在令人討厭的缺點,大部分時間它都可以正常工作:

  1. 來自一個線程的更新可能永遠不會顯示給其他線程。
  2. 兩個線程可能會檢查名字,找不到任何內容,並且都添加了lastname(在更改之前顯示相應名稱)。
  3. Theads可能會看到“不完整”的對象。

使用synchronized基本修復

一個非常基本的修復是允許同時只允許一個線程使用synchronized關鍵字訪問函數的並發部分(這可以添加到函數定義中,但是您應該使用私有對象進行同步)。

public class Sample {

    Map<String, String> firstNameToLastName = new HashMap<>();
    private final Object nameMapLock = new Object();

    public String testMethod(String firstName) {
        synchronized(nameMapLock){
            String lastName = firstNameToLastName.get(firstName);

            if (lastName!= null)
                return lastName;

            String generateLastName = SomeAPI.generateLastName(firstName);

            firstNameToLastName.put(firstName, generateLastName);

            return generateLastName;
        }
    }
}

如果多個線程同時嘗試訪問數據,則必須等到另一個線程完成。 您還必須確保在鎖定中不引入死鎖。

在私有Object上同步

在回復評論時,我將添加一些解釋,說明為什么在私有對象上進行同步而不是在完整方法上(通過向方法定義添加synchronized )或在地圖上進行synchronized

使用私有對象的原因是,您可以100%確定沒有其他類也使用您的對象(讀取鎖定)進行同步。

當您在方法上使用synchronized關鍵字時,您實際上正在同步this (當前對象),這意味着任何使用您的類的人也可以這樣做。 在地圖上同步時,地圖本身也可能同步該對象,或者您將地圖傳遞到的其他類。

請注意,在一些非常罕見的情況下,您確實希望其他人能夠使用相同的鎖,但這意味着您需要執行大量額外的文檔操作,並且存在其他人濫用鎖定的風險。

我在上面的例子中展示的方式是大多數人這樣做的方式。 然而,還有很多其他方法可以做到這一點。

使用ConcurrentHashMap修復

使用ConcurrentHashMap將解決問題1和3(如上編號)。 但你還是要對第二點采取特別措施。 從Java 8開始,您可以使用ConcurrentHashMap.computeIfAbsent()優雅地執行此操作。 這將工作如下:

public class Sample {

    ConcurrentHashMap<String, String> firstNameToLastName = new ConcurrentHashMap<>();

    public String testMethod(String firstName) {
        return firstNameToLastName.computeIfAbsent(firstName, 
                    name -> SomeAPI.generateLastName(name));

        }
    }
}

如您所見,這可以使實現非常優雅。 但是,如果您在地圖上有更多(更復雜)的操作,則可能會遇到麻煩。

您可以使用ReentrantReadWriteLock ,這樣您就可以讀取多個線程。

public class Sample {

final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
Map<String, String> firstNameToLastName = new HashMap<>();

public String testMethod(String firstName) {
    rwl.readLock().lock();
    String lastName = firstNameToLastName.get(firstName);
    rwl.readLock().unlock();

    if (lastName!= null)
        return lastName;

    lastName = SomeAPI.generateLastName(firstName);

    // Must release read lock before acquiring write lock, it is already released
    rwl.writeLock().lock();
    //now another thread could already put a last name, so we need to check again
    lastName = firstNameToLastName.get(firstName);
    if (lastName== null)
        firstNameToLastName.put(firstName, lastName);

    rwl.writeLock().unlock();
    return lastName;
}

}

IMO您只需要同步嘗試訪問共享資源(例如集合)的代碼部分。

在您的代碼中,除了您正在調用的api(我們不知道任何事情)之外,唯一的共享資源是您的姓氏映射的第一個名稱,因此如果您將其作為並發集合(Concurrent hashMap),那么您的地圖中的數據將是好的(在兩個線程進入“ testMethod ”並且無法在map中找到名稱的情況下,在競爭條件中,其中一個成功先調用put方法並添加姓氏以映射然后另一個線程調用put方法具有相同的鍵/值,但最終你的映射具有正確的值)。

但是在您的代碼中, testMethod的整體操作是意外的,例如,在一個線程中可能找不到密鑰並調用api生成姓氏,而另一個線程正在使用相同的密鑰更新映射。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM