避免使用状态变量副本导致阻塞?

3
我希望能就我最近的一个多线程想法得到一些智慧。具体如下:
假设我有以下(伪代码)类,其run()方法在某个线程上永远运行。其他线程将在随机时间使用setState()更改Foo实例的状态。run()正在执行的工作仅涉及读取状态变量,没有写入,并且在while语句的一次执行期间状态不得更改(例如:在位图上绘制)。
在这种情况下,拥有2个状态变量的副本似乎可以防止很多潜在的阻塞(因为如果我只有一个共享状态变量的副本,我必须在while循环中同步所有内容(使用stateLock),并且外部线程可能无法获得更改状态的机会)。 请参考以下代码后的问题。
class Foo {
  Object stateLock = new Object();

  private float my1, my2, my3;
  private float sh1, sh2, sh3;  // sh stands for shared

  public void setState(...) {
    synchronized (stateLock) {
      // modify sh1, sh2, or sh3 here
    }
  }

  private void updateState() {
    synchronized (stateLock) {
      // set my1=sh1, my2=sh2, my3=sh3
    }
  }

  public void run() {
    while(true) {
      updateState();
      // then do tons of stuff that uses my1,my2,my3 over and over...
      ...
    }
  }
}

这个逻辑是否存在漏洞?有没有“标准化”的或更智能的方法来处理这个问题?如果状态变量很多怎么办?更糟糕的是,如果状态变量是不容易复制的自定义对象(例如在Java中,自定义对象的变量是引用),该怎么办?

顺便说一下,这是我在Android上使用SurfaceView时遇到的问题。

2个回答

3
这个问题有一个更简单的解决方法:
private volatile float sh1, sh2, sh3;  // note "volatile"

在Java内存模型中,线程允许从其他线程缓存值。关键字volatile表示所有线程必须使用相同的变量值(即所有引用变量的内存位置相同)。当与基本类型一起使用时,这意味着您不需要同步(尽管对于64位基本类型,其中float不是一个,取决于您的JVM是32位还是64位,您可能不需要同步)。
您可能希望观察部分/不一致的更新 - 即在另一个线程读取它们时,某些sh变量被更新。您可能希望同步更新以维护一致的状态,通过使您对多个sh变量的更新“原子化”。

很遗憾,我认为这并没有帮助。删除my1、my2、my3,转而使用共享的volatile变量,违反了(本地)状态在while循环迭代期间不得更改的要求。虽然使用volatile可以保证读/写不会交错,但这并不意味着状态变量不能在while迭代的中途被另一个线程更改。Esko提供了一个好的解决方案,恰好来自于《Java并发实践》。 - heycosmo
抱歉,也许您并不想删除我的 my1、my2、my3。然而,在这种情况下,将 sh1、sh2、sh3 设为 volatile 是多余的,因为 stateLock 原子化了状态设置和获取。 - heycosmo

2
为了保持所有变量同步并避免同步问题,您可以将变量放入不可变对象中,并将其作为一个整体进行更新。在读取状态时,保持一个这样的状态对象作为局部变量,您将确保在读取它时没有其他人会更新它。
以下是一些示例代码(未经测试等)。如果旧值在setState中没有被读取,或者它仅从一个线程访问,则易失性字段就足够了。但在一般情况下(多个线程调用setState且新状态取决于旧状态的值),使用AtomicReference可以确保不会错过任何更新。
class Foo {
    private final AtomicReference<State> state = new AtomicReference<State>(new State(0, 0, 0));

    private void setState(float x1, float x2, float x3) {
        State current;
        State updated;
        do {
            current = state.get();
            // modify the values
            float sh1 = current.sh1 + x1;
            float sh2 = current.sh2 + x2;
            float sh3 = current.sh3 + x3;
            updated = new State(sh1, sh2, sh3);
        } while (!state.compareAndSet(current, updated));
    }

    public void run() {
        while (true) {
            State snapshot = state.get();
            // then do tons of stuff that uses sh1, sh2, sh3 over and over...
        }
    }

    private class State {
        public final float sh1, sh2, sh3;

        State(float sh1, float sh2, float sh3) {
            this.sh1 = sh1;
            this.sh2 = sh2;
            this.sh3 = sh3;
        }
    }
}

以下是一个特殊情况的示例代码,即更新状态不依赖于状态的旧值:

class Foo {
    private volatile State state = new State(0, 0, 0);

    private void setState(float sh1, float sh2, float sh3) {
        state = new State(sh1, sh2, sh3);
    }

    public void run() {
        while (true) {
            State snapshot = state;
            // then do tons of stuff that uses sh1, sh2, sh3 over and over...
        }
    }

    private class State {
        public final float sh1, sh2, sh3;

        State(float sh1, float sh2, float sh3) {
            this.sh1 = sh1;
            this.sh2 = sh2;
            this.sh3 = sh3;
        }
    }
}

这是一种很好的打包处理方式(而且没有任何显式同步 - 奖励!)。谢谢!(我碰巧在你的帖子之后读了《Java并发实践》中关于此章节的内容。) - heycosmo
我还没有读过《Java并发编程实践》,但是可能我已经从其他一百个地方收集了相同的信息。 :) - Esko Luontola

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接