7

假设我有两个线程像这样运行:

  • 线程 A 在更新共享图像的像素时执行计算
  • 线程 B 定期读取图像并将其复制到屏幕

线程 A 快速执行工作,例如每秒 100 万次更新,所以我怀疑经常在锁/互斥/监视器上锁定和解锁是个坏主意。但是,如果没有锁并且无法建立从线程 A 到线程 B 的先发生关系,那么根据 Java 内存模型(JMM 规范),线程 B 根本不能保证看到 A 对图像的任何更新。

所以我认为最小的解决方案是线程 A 和 B 都在同一个共享锁上定期同步,但在同步块内实际上不执行任何工作 - 这就是使模式非标准和可疑的原因。用半真半伪代码来说明:

class ComputationCanvas extends java.awt.Canvas {

    private Object lock = new Object();
    private int[] pixels = new int[1000000];

    public ComputationCanvas() {
        new Thread(this::runThreadA).start();
        new Thread(this::runThreadB).start();
    }

    private void runThreadA() {
        while (true) {
            for (1000 steps) {
                update pixels directly
                without synchornization
            }
            synchronized(lock) {}    // Blank
        }
    }

    private void runThreadB() {
        while (true) {
            Thread.sleep(100);
            synchronized(lock) {}    // Blank
            this.repaint();
        }
    }

    @Override
    public void paint(Graphics g) {
        g.drawImage(pixels, 0, 0);
    }
}

这样添加空同步块是否正确实现了线程A向线程B传输数据的效果?还是有其他我无法想象的解决方案?

4

1 回答 1

1

是的,它有效。但它的工作原理很糟糕。

Happens before 只有在 writer 的 release 发生在 reader 的获取之前才有效。您的实现假定您正在编写的任何内容都将在后续读取/更新之前完成ThreadB。导致您的数据一直被同步刷新会导致性能问题,尽管我不能肯定地说到什么程度。当然,你已经让你的同步更细了,你测试了吗?

更好的解决方案可能使用单例/传输 SPSC(单生产者/单消费者)队列来存储写入线程的当前快照,并在您更新时使用它。

int[] data = ...
Queue<int[]> queue = new ...

// Thread A
while (true) {
    for (1000 iterations or so) {
        ...
    }
    queue.add(data);
}

// Thread B
while (true) {
    int[] snapshot = queue.take(); 
    this.repaint();
}

这样做的好处是你不需要忙等待,你可以等待队列阻塞或者直到下一次写入。您可以跳过没有时间更新的写入。您无需依赖任意线程调度程序来为您计划数据刷新。

请记住,线程安全的数据结构非常适合在线程之间传递数据。

编辑:哎呀,忘了说根据您的更新方式,您可能希望使用数组副本来防止您的数据因未缓存的随机写入而出现乱码。

于 2017-01-30T05:11:11.093 回答