【问题标题】:Does this non-standard Java synchronization pattern work?这种非标准的 Java 同步模式有效吗?
【发布时间】:2017-06-15 03:47:53
【问题描述】:

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

  • 线程 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传输数据的效果?还是有其他我无法想象的解决方案?

【问题讨论】:

  • 为什么不能使用原子布尔值?什么都不同步仍然会使工作不同步
  • 为什么不使用volatile
  • pixels 数组被 A 更新时,画布仍然可以读取数据
  • Graphics 没有采用 int[] 数组的 drawImage 方法。因此,此示例中缺少一个基本步骤。此外,涉及三个线程,第三个线程是实际读取数组的事件调度线程(如果有接受int[]drawImage 方法)从不使用synchronized。由于repaint 只会向队列发布某种事件(但有一些陷阱),因此您可以摆脱线程 B 和 synchronized 并简单地发布一个自定义事件来解决所有问题。
  • synchronized 块已经超出了纯计算焦点。为什么任何其他也可以归结为一行代码的方法比这更糟糕?

标签: java multithreading synchronization java-memory-model happens-before


【解决方案1】:

是的,它有效。但是效果很差。

Happens before 仅在编写器的释放发生在读取器的获取之前发生时才有效。您的实现假定您正在编写的任何内容都将在从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();
}

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

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

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

【讨论】:

  • repaint 不带任何参数
  • @DavidConrad 你是对的,正在考虑下面的绘制方法。
  • 也可以通过 AtomicReference 传递像素数据,但由于您提到的原因,队列更胜一筹 - 没有繁忙的等待或调度问题。我已经多次使用这种模式并取得了巨大的成功。
  • 我会将队列的大小限制为两个。无需积累数千个等待重绘的数组。就像一个双缓冲
  • @efekctive 我在回答之前提到可以使用 TransferQueue 或单例队列。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-08-29
  • 2014-09-22
  • 1970-01-01
  • 1970-01-01
  • 2011-04-23
  • 1970-01-01
相关资源
最近更新 更多