了解不可修改性
要创建线程安全的应用程序,一个有用的技巧便是尽可能多地使用不可修改的数据。清单 4 展示了重写后的最高分示例,它使用了 HighScore 的不可修改的 实现,从而避免了原子性故障(允许调用方看见不存在的玩家/分数对)和可见性故障(阻止 getHighScore() 的调用方看见在调用 updateHighScore() 时写的最新值):
清单 4. 使用不可修改的 HighScore 对象修复原子性和可见性漏洞
|
Public class HighScore { public HighScore(String name, int score) { public PlayerScore getHighScore() { public void updateHighScore(PlayerScore newScore) { |
清单 4 中的代码的潜在故障很少。在 setAttribute() 和 getAttribute()中使用同步保证了可见性。实际上,仅存储单个不可修改数据项消除了潜在的原子性故障,即 getHighScore()的调用方可以看见名字/分数对的不一致更新。
将不可修改对象放置在范围容器避免了许多原子性和可见性故障;将有效不可修改性对象放置在范围容器中也是安全的。有效不可修改性对象是指那些虽然理论上是可修改的,但实际上在发布之后再没有被更改过的对象,比如 JavaBean,将一个对象放置到 HttpSession 中之后,它的 setter 方法就不再被调用。
放置在 HttpSession 中的数据不仅被该会话的请求访问;它还可能被容器本身访问(如果容器进行状态复制的话)。
所有放置在 HttpSession 或 ServletContext 中的数据应该是线程安全的或有效不可修改的。
影响原子状态转换
但是 清单 4 中的代码仍然有一个问题 — updateHighScore() 中的 check-then-act 仍然使两个试图更新最高分数的线程之间存在潜在 “争夺”。如果计时失误,有一个更新可能会丢失。两个线程可能同时通过了 “高于现有分数的新最高分” 检查,造成它们同时调用 setAttribute()。不能确保两个分数中最高者获得调用,这取决于计时。要修复这个最后的漏洞,我们需要一种原子性地更新分数引用的方法,同时又要保证不受干扰。有几种方法可以实现这个目的。
清单 5 为 updateHighScore() 添加了同步,确保更新进程中固有的 check-then-act 不和另一个更新并发执行。如果所有条件修改逻辑获得 updateHighScore() 使用的同一个锁,用这种方法就可以了。
清单 5. 使用同步修复最后一个原子性漏洞
| public void updateHighScore(PlayerScore newScore) { ServletContext ctx = getServletConfig().getServletContext(); PlayerScore hs = (PlayerScore) ctx.getAttribute("highScore"); synchronized (lock) { if (newScore.score > hs.score) ctx.setAttribute("highScore", newScore); } } |
虽然清单 5 中的技术是可行的,但还有一个更好的技术:使用 java.util.concurrent 包中的 AtomicReference 类。这个类的用途就是通过 compareAndSet() 调用提供原子条件更新。清单 6 展示了如何使用 AtomicReference 来修复本示例的最后一个原子性问题。这个方法比清单 5 中的代码好,因为很难违背更新最高分数的规则。
清单 6. 使用 AtomicReference 来修复最后一个原子性漏洞
|
public PlayerScore getHighScore() { public void updateHighScore(PlayerScore newScore) { |
对于放置在范围容器中的可修改数据,应该将它们的状态转换变成原子性的,这可以通过同步或 java.util.concurrent 中的原子变量类来实现。
0
顶一下0
埋一下引用地址:



