假设我有一个对象如下:
Map<String, String> m = new HashMap<>();
然后我按如下方式在这个对象上同步并更改它的引用:
synchronize(m){
m = new HashMap<>();
}
使用此代码,m 上的锁会发生什么?更新 m 表示的新对象是否仍然安全?还是锁定本质上是在旧对象上?
假设我有一个对象如下:
Map<String, String> m = new HashMap<>();
然后我按如下方式在这个对象上同步并更改它的引用:
synchronize(m){
m = new HashMap<>();
}
使用此代码,m 上的锁会发生什么?更新 m 表示的新对象是否仍然安全?还是锁定本质上是在旧对象上?
从JLS 17.1开始:
同步语句(第 14.19 节)计算对对象的引用;然后它会尝试在该对象的监视器上执行锁定操作,并且在锁定操作成功完成之前不会继续进行。执行完锁定操作后,将执行同步语句的主体。如果主体的执行完成,无论是正常的还是突然的,都会在同一个监视器上自动执行解锁操作。
现在的问题。
m上的锁会发生什么?
没有什么。这有点令人困惑。实际上,线程在它试图获取锁的时候持有对象所引用的锁。 m
同步块中的分配m
不会自动“切换”正在执行的线程持有的锁。
更新 m 表示的新对象是否仍然安全?
这不安全。写入m
未在同一个锁上同步。
还是锁定本质上是在旧对象上?
是的
要安全地更改对对象的引用,您可以:
AtomicReference<Map<String, String>>
synchronized
在包含此地图的对象上使用,或者在其他一些锁定对象上使用更好。
class A {
private final Object lock = new Object();
private Map<String, String> m = new HashMap<>();
public void changeMap() {
synchronized(lock){
m = new HashMap<>();
}
}
}
至少添加volatile
private volatile Map<String, String> m = new HashMap<>();
另请参阅有关此主题的其他答案
锁在对象上,而不是在变量上。
当一个线程试图进入一个同步块时,它会在 synchronized 关键字之后计算括号中的表达式,以确定要获取锁定的对象。
如果覆盖引用以指向新对象,则尝试进入同步块的下一个线程将获取新对象上的锁,因此可能会出现两个线程在同一同步块中执行代码的情况相同的对象(当另一个线程开始执行块时,可能不会完成对旧对象的锁定)。
为了使互斥起作用,您需要线程共享相同的锁,您不能让线程交换锁对象。将一个专用对象用作锁是个好主意,使其成为最终对象以确保没有任何更改,如下所示:
private final Object lock = new Object();
这样,由于锁对象不用于其他任何用途,因此不会有去更改它的诱惑。
内存可见性在这里似乎无关紧要。在推理交换锁如何产生问题时,您不需要考虑可见性,并且添加代码以让锁对象以可见的方式更改无助于解决问题,因为解决方案是避免更改完全锁定对象。
你的方法不安全。您需要在所有协调线程之间使用相同m
的锁来保护某些资源(在本例中为映射),但正如您直观地理解的那样,这在这里失败了,因为对象m
在不断变化。
具体来说,一旦你在临界区内写了一个新的引用m
,另一个线程就可以进入临界区(因为他们获得了新 Map
的锁定,而不是另一个线程持有的旧的锁定),并访问新的部分构造的地图。
另请参阅安全出版物。