如何使此代码线程安全

时间:2017-01-06 11:33:41

标签: java multithreading

以下代码中的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;
    }
}

3 个答案:

答案 0 :(得分:6)

您的代码不是线程安全的。这导致了以下问题,其中存在令人讨厌的缺点,即大部分时间它都能正常工作:

  1. 一个线程的更新可能永远不会显示给其他线程。
  2. 两个线程可能会检查名字,找不到任何内容,并且都添加了lastname(在更改显示给彼此之前)。
  3. Theads可能会看到“不完整”的对象。
  4. 使用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)或在地图上进行同步。 / p>

    使用私有对象的原因是,您可以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));
    
            }
        }
    }
    

    如您所见,这可以使实现非常优雅。但是,如果您在地图上有更多(和更复杂的)操作,则可能会遇到麻烦。

答案 1 :(得分:0)

您可以使用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;
}

}

答案 2 :(得分:0)

IMO您只需要同步尝试访问共享资源(例如集合)的代码部分。

在你的代码中除了你正在调用的api(我们不知道任何事情)之外,唯一的共享资源是你的姓氏映射的第一个名字,所以如果你把它作为并​​发集合(Concurrent hashMap),你的数据在你的地图中会很好(在两个线程进入“ testMethod ”的情况下,并且无法在地图和竞争条件中找到名称,其中一个成功首先调用put方法并添加姓氏到map,然后其他线程调用put方法使用相同的键/值,但最终你的地图有正确的值)。

但是在你的代码中, testMethod 的整体操作是意外的,例如在一个线程中可能找不到密钥并调用api来生成姓氏,而另一个线程正在使用同样的关键。