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

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

Web 应用程序的线程风险

    如果 Web 应用程序将购物车等可变会话数据存储在 HttpSession 中,就有可能出现两个请求试图同时访问该购物车。这可能导致以下几种故障:

    原子性故障。在数据不一致的状态下,一个线程正在更新多个数据项,而另一个线程正在读取这些数据读线程和写线程之间的可见性故障。一个线程修改购物车,但另一个线程看到的购物车内容的状态是过时的或不一致的原子性故障

    清单 2 展示了一个用于在游戏应用程序中设置和获取最高分的不恰当的方法实现。它使用一个 PlayerScore 对象来表示最高分,这实际上是一个具有 name 和 score 属性的普通 JavaBean 类,存储在内嵌于应用程序的 ServletContext 中(假设在应用程序启动时,初始最高分在 ServletContext 中被设置为 highScore 属性,因此 getAttribute() 调用不会失败)。

    清单 2. 在范围容器中存储相关项的不恰当模式

 

public PlayerScore getHighScore() {
    ServletContext ctx = getServletConfig().getServletContext();
    PlayerScore hs = (PlayerScore) ctx.getAttribute("highScore");
    PlayerScore result = new PlayerScore();
    result.setName(hs.getName());
    result.setScore(hs.getScore());
    return result;
}

public void updateHighScore(PlayerScore newScore) {
    ServletContext ctx = getServletConfig().getServletContext();
    PlayerScore hs = (PlayerScore) ctx.getAttribute("highScore");
    if (newScore.getScore() > hs.getScore()) {
        hs.setName(newScore.getName());
        hs.setScore(newScore.getScore());
    }
}

    清单 2 中的代码不够好。这里采用的方法是在 ServletContext 中存储一个包含最高分玩家的名字和分数的可变容器。当打破记录时,必须更新名字和分数。

    假如当前的最高分玩家是 Bob,他的分数为 1000,但是 Joe 以 1100 分打破了这个记录。在正要设置 Joe 的分数时,另一个玩家又发出获得最高分的请求。getHighScore() 将从 servlet 上下文获取 PlayerScore 对象,然后从该对象获取名字和分数。如果不慎出现计时失误,就有可能获取 Bob 的名字和 Joe 的分数,显示 Bob 取得了 1100 分,而实际上 Bob 并没有获得这个分数(这种故障在免费游戏站点上是可以接受的,因为 “分数” 并不是 “银行存款”)。这是一种原子性故障,因为彼此之间本应该是原子关系的两个操作 — 获取名字/分数对和更新名字/分数对 — 实际上并没有按照原子关系执行,而且其中一个线程可以在不一致状态下查看共享数据。

    另外,由于分数更新逻辑遵循 check-then-act 模式,因此可能出现两个线程 “争夺” 更新最高分,从而导致难以预料的结果。假设当前的最高分是 1000,有两个玩家同时注册更高的分数 1100 和 1200.如果出现计时失误,这两个分数都能够通过 “高于现有分数的最高分” 检查,并且都进入到更新最高分的代码块中。和前面一样,根据计时的实际情况,最后的结果可能不一致(采用一个玩家的名字和另一个玩家的分数),或者出现错误(分数为 1100 的玩家可能覆盖分数为 1200 的玩家)。

    可见性故障

    比原子性故障更复杂的是可见性 故障。没有同步时,如果一个线程读取另外一个线程正在写的变量,读的线程将看到过时的数据。更糟糕的是,读线程还可能会看到 x 变量的最新数据和 y 变量的过时数据,即使先写 y 变量也是这样。可见性故障非常复杂,因为它的发生是随机的,甚至是频繁的,这会导致罕见的难以调试的间发性故障。可见性故障是由数据争夺引起的 — 访问共享变量时不能正确同步。争夺数据的程序,不管它是什么样的,都属于有漏洞的程序,因为它们的行为不能可靠预测。

    Java Memory Model(JMM)定义一些条件,它们保证读变量的线程能够看到另一个线程的写入结果(详细讲解 JMM 超出了本文的范围;。JMM 在一个称为 happens-before 的程序的操作上定义一个排序。只有在通用锁上执行同步或访问一个通用的可变变量时,才能创建跨线程的 Happens-before 排序。在没有 happens-before 排序的情况下, Java 平台可以延迟或更改顺序,按照这个顺序,一个线程的写操作对于另一个读取同一变量的线程是可见的。

    清单 2 中的代码不仅有原子性故障,还有可见性故障。updateHighScore() 方法从 ServletContext 获取 HighScore 对象,然后修改它的状态。这样做的目的是让其他调用 getHighScore() 的线程看见这些修改,但是如果 updateHighScore() 的 name 和 score 属性的写操作和其他调用 getHighScore() 的线程的 name 和 score 属性的读操作之间没有 happens-before 排序,我们只能期盼运气好些,让读线程能够看到正确的值。

0

顶一下

0

埋一下

引用地址: