以下代码中的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;
}
}
答案 0 :(得分:6)
您的代码不是线程安全的。这导致了以下问题,其中存在令人讨厌的缺点,即大部分时间它都能正常工作:
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来生成姓氏,而另一个线程正在使用同样的关键。