要求是只允许单线程执行用户管理(创建/更新/导入)操作,但不允许多个线程同时为同一用户执行用户操作。例如,当线程A创建用户A时,同时不允许线程B导入用户A或创建用户A但允许线程B导入用户B.以下代码线程是否可以满足这些要求?
public class UserManagement {
ConcurrentHashMap<Integer, Lock> userLock = new ConcurrentHashMap<>();
public void createUser(User user, Integer userId) {
Lock lock = userLock.putIfAbsent(userId, new ReentrantLock());
try {
lock.lock();
//create user logic
} finally {
lock.unlock();
}
}
public void importUser(User user, Integer userId) {
Lock lock = userLock.putIfAbsent(userId, new ReentrantLock());
try {
lock.lock();
//import user logic
} finally {
lock.unlock();
}
}
public void updateUser(User user, Integer userId) {
Lock lock = userLock.putIfAbsent(userId, new ReentrantLock());
try {
lock.lock();
// update user logic
} finally {
lock.unlock();
}
}
}
答案 0 :(得分:5)
您的代码符合有关安全访问用户操作的要求,但它不是完全线程安全的,因为它不保证所谓的初始化安全性。如果您创建UserManagement
的实例并在多个线程之间共享它,那么这些线程在某些情况下可以看到未初始化的userLock
。虽然不太可能,但仍有可能。
为了使您的类完全是线程安全的,您需要将final
修饰符添加到userLock
,在这种情况下,Java内存模型将保证在多线程环境中正确初始化该字段。将不可变字段定为最终也是一种很好的做法。
重要更新: @sdotdi在评论中指出,在构造函数完成工作后,您可以完全依赖对象的内部状态。实际上,它不是真的,事情也更复杂。
评论中提供的链接仅涵盖Java代码编译的早期阶段,并且没有说明进一步发生的事情。但进一步说,优化就会发挥作用,并开始按照自己的意愿重新排序说明。 JMM是代码优化器唯一的限制。根据JMM,使用构造函数中发生的事情更改指向对象新实例的指针的赋值是完全合法的。所以,nothings阻止它优化这个伪代码:
UserManagement _m = allocateNewUserManagement(); // internal variable
_m.userLock = new ConcurrentHashMap<>();
// constructor exits here
UserManagement userManagement = _m; // original userManagement = new UserManagement()
到这一个:
UserManagement _m = allocateNewUserManagement();
UserManagement userManagement = _m;
// weird things might happen here in a multi-thread app
// if you share userManagement between threads
_m.userLock = new ConcurrentHashMap<>();
如果您想阻止此类行为,则需要使用某种同步:synchronized
,volatile
或更柔和的final
,如本例所示。
例如,您可以在“Java Concurrency in Practice”一书的第3.5节“安全发布”中找到更多详细信息。
答案 1 :(得分:3)
除了Andrew Lygin提到的那个之外,你的程序还有另一个bug。
如果以前没有看到lock
,这会将null
设置为userId
,因为`putIfAbsent(...)不会返回新值,它会返回 previous 值:
Lock lock = userLock.putIfAbsent(userId, new ReentrantLock());
请改为:
Lock lock = userLock.computeIfAbsent(userId, k -> new ReentrantLock());
computeIfAbsent(...)
会返回 new 值。此外,除非实际需要,否则它实际上没有创建新的Lock对象。 (感谢@bowmore提出建议。)
这个程序线程是否安全?
假设您修复了错误,我们仍然无法分辨程序。我们所能说的是,UserManagement
类的实例不允许对同一userId
的这三种方法中的任何一种进行重叠调用。
是否能使您的程序线程安全取决于您如何使用它。例如,您的代码不允许两个线程同时更新相同的userId
,但如果他们尝试,它将允许它们一个接一个地进行。你的代码将无法控制哪一个首先出现---操作系统会这样做。
您的锁定可能会阻止两个线程将用户记录置于无效状态,但是它们会将其保留在右状态吗?这个问题的答案超出了你向我们展示的一个班级的范围。
线程安全不是 composeable 属性。也就是说,完全用线程安全类构建一些东西不保证整个东西都是线程安全的。
答案 2 :(得分:1)
只要您不在锁定的逻辑代码块中创建任何新线程,就可以看到线程安全。如果您确实在锁定的逻辑代码块中创建了线程,并且这些线程会为同一个用户调用任何UserManagement方法,那么您最终会遇到死锁。
您还需要确保只有一个UserManagement实例。如果创建多个实例,则可以让多个线程更新同一用户。我建议让userLock静态以避免这个问题。
使用应用程序逻辑的另一个小的nitpik。传入用户时,您需要确保不使用不同的userId传入同一用户(不确定为什么要将userId与用户对象分开传递)。这需要此类之外的其他逻辑来创建/导入新用户。否则你最终可能会调用createUser(userA,1)和createUser(userA,2)或import(userA,3)。
答案 3 :(得分:0)
有一些问题:
现在修复第一个问题并不容易 - 仅仅调用userLock.remove(userId);
是不够的:
public class UserManagement {
private final ConcurrentHashMap<Integer, Lock> userLock = new ConcurrentHashMap<>();
public void createUser(User user, Integer userId) {
Lock lock = userLock.computeIfAbsent(userId, k -> new ReentrantLock());
lock.lock();
try {
// do user logic
} finally {
lock.unlock();
}
// Danger: this could remove the lock although another thread is still inside the 'user logic'
userLock.remove(userId);
}
}
根据我目前的知识,您可以解决所有问题,甚至可以节省一些内存并避免显式锁定。根据javadocs的唯一要求是“用户逻辑”很快:
// null is forbidden so use the key also as the value to avoid creating additional objects
private final ConcurrentHashMap<Integer, Integer> userLock = ...;
public void createUser(User user, Integer userId) {
// Call compute if existing or absent key but do so atomically:
userLock.compute(userId, (key, value) -> {
// do user logic
return key;
});
userLock.remove(rowIndex);
}