Java理论与实践: 有状态Web应用程序都有漏洞吗?(4)

时间:2008-10-26 14:26:19  来源:互联网  作者:  字号:【

了解不可修改性

    要创建线程安全的应用程序,一个有用的技巧便是尽可能多地使用不可修改的数据。清单 4 展示了重写后的最高分示例,它使用了 HighScore 的不可修改的 实现,从而避免了原子性故障(允许调用方看见不存在的玩家/分数对)和可见性故障(阻止 getHighScore() 的调用方看见在调用 updateHighScore() 时写的最新值):

    清单 4. 使用不可修改的 HighScore 对象修复原子性和可见性漏洞

 

Public class HighScore {
    public final String name;
    public final int score;

    public HighScore(String name, int score) {
        this.name = name;
        this.score = score;
    }
}

public PlayerScore getHighScore() {
    ServletContext ctx = getServletConfig().getServletContext();
    return (PlayerScore) ctx.getAttribute("highScore");
}

public void updateHighScore(PlayerScore newScore) {
    ServletContext ctx = getServletConfig().getServletContext();
    PlayerScore hs = (PlayerScore) ctx.getAttribute("highScore");
    if (newScore.score > hs.score)
        ctx.setAttribute("highScore", 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() {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    return holder.get();
}

public void updateHighScore(PlayerScore newScore) {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    while (true) {
        HighScore old = holder.get();
        if (old.score >= newScore.score)
            break;
        else if (holder.compareAndSet(old, newScore))
            break;
    }
}

    对于放置在范围容器中的可修改数据,应该将它们的状态转换变成原子性的,这可以通过同步或 java.util.concurrent 中的原子变量类来实现。

0

顶一下

0

埋一下

引用地址: