【问题标题】:How do I deep copy an irregular 2D array threadsafely如何安全地深度复制不规则的二维数组
【发布时间】:2011-03-28 01:14:57
【问题描述】:

我在编写 Java 类时偶然发现了一个烦恼;我不知道如何使复制二维数组线程安全。这是该类的简单版本:

public class Frame {
  private boolean[][] content;

  public Frame(boolean [][] content) {
    boolean[][] threadSafeCopy = deepCopy(content);
    if (!(threadSafeCopy != null)) {
      throw new NullPointerException("content must not be null");
    }
    if (!(threadSafeCopy.length > 0)) {
      throw new IllegalArgumentException("content.length must be greater than 0");
    }
    if (!(threadSafeCopy[0] != null)) {
      throw new IllegalArgumentException("content[0] must not be null");
    }
    if (!(threadSafeCopy[0].length > 0)) {
      throw new IllegalArgumentException("content[0].length must be greater than 0");
    }
    for (int i = 1, count = threadSafeCopy.length; i < count; ++i) {
      if (!(threadSafeCopy[i].length == threadSafeCopy[0].length)) {
        throw new IllegalArgumentException
            (   "content[" + i + "].length [" + threadSafeCopy[i].length
              + "] must be equal to content[0].length [" + threadSafeCopy[0].length + "]"
            );
      }
    }
    this.content = threadSafeCopy;
  }

  private boolean[][] deepCopy(boolean[][] content) {
    boolean[][] result = null;
    if (content != null) {
      synchronized(content) { //do our best to make this as multi-threaded friendly as possible
        result = new boolean[content.length][];
        for (int i = 0, count = content.length; i < count; ++ i) {
          if (content[i] != null)
          {
            synchronized(content[i]) {
              result[i] = content[i].clone();
            }
          }
        }
      }
    }
    return result;
  }

  public boolean[][] getContent() {
    boolean[][] result = new boolean[this.content.length][]; //defensive copy
    for (int i = 0, count = result.length; i < count; ++i) {
      result[i] = this.content[i].clone(); //defensive copy
    }
    return result;
  }
}

然而,上述方法private boolean[][] deepCopy(boolean[][] content) 的实现实际上并不是线程安全的。在此方法尝试复制时,可能正在由另一个线程主动修改该数组。当然,我已经防范了最滥用的情况,在基本阵列上使用synchronized。但是,这不会导致二维数组实例的集合被锁定。并且可以在复制过程中对其进行修改。

是否有某种方法可以为每个基本数组 (content) 和子数组 (content[0], content[1], ..., content[content.length - 1]) 收集对象锁,这样我就可以调用类似synchronized(objectsToLockSimultaneouslyList) 的东西,它会同时按列表的顺序锁定所有对象。如果是这样,我可以线程安全地复制数组的内容。

如果不是,还有哪些其他类型的解决方案可用于“阻止对数组的所有修改”,而无需更改实例化 Frame 的类或更改 Frame 的构造函数,这样它就不会使用数组,而只需要实例不可变的集合(它本身就是另一个怪诞的兔子洞)。

感谢您在这方面的任何指导。

更新: 我想做的事情基本上是不可能的。而且我对通过同步锁定对象的理解也是错误的(t​​yvm glowcoder,Paulo 和 Brian)。我现在将尝试将 Frame 上的界面更改为使用List&lt;List&lt;Boolean&gt;&gt;,这肯定看起来效率要低得多。或者我可以使用Set&lt;XyCoordinate&gt;,其中 XyCoordinate 的存在意味着“真实”。再一次,这似乎非常低效,但线程安全。啊!

【问题讨论】:

  • 你想要的都是不可能的。替换数组,例如如果可以的话,请来自 Guava 的ImmutableList。或者,不要将它们暴露给其他线程,给它们一个副本以供使用。接受外部论据时使用防御性文案。
  • @maaartinus:知道了。只需阅读其他答案和 cmets。所以,基本上没有可能的线程安全方式来使用数组作为我的构造函数的参数。这意味着我只需要更改我的构造函数以不使用数组。当然,这意味着我必须找到某种方法来验证我正在传递一个不可变的布尔值列表的不可变列表,这似乎非常低效。
  • @chaotic3quilibrium 对,List> 很糟糕。 ImmutableList 呢?前者存在于 Guava 中,后者将是 BitSet 的简单包装器。
  • 也许你对“线程安全”的理解是罪魁祸首。假设您始终正确同步,您可以以线程安全的方式使用任何对象。不可变类使这一切变得微不足道,但它们并不总是必要的。不要将程序的其余部分视为您的敌人,试图同时修改您的对象以给您带来麻烦。
  • 实际上,您可以将 boolean[][] 包装在一个线程安全的包装器中,并使用 get(int x, int y)set(int x, int y, boolean value) 等同步方法。只是不要实现任何数组返回方法,因为它会返回一个可变的东西。或者可以这样做,但返回一个克隆,请参阅here

标签: java arrays thread-safety copy


【解决方案1】:

我认为最好的办法是将数组包装在一个对象中并提供对它的同步访问。然后你可以对访问器方法进行“关门”的深拷贝。

void deepCopy(boolean[][] orig) {
    synchronized(orig) {
        boolean[][] result = new boolean[orig.length][];
        deepCopy(orig,0);
        return result;        
    }
}

/**
 * recursive method to lock all rows in an array, and then copy
 * them in (backwards, by chance)
 */
void deepCopy(boolean[][] orig, int row) {
    if(row == orig.length) return; // end condition
    synchronized(orig[row]) { // lock the row first
        deepCopy(orig,row+1); // lock the next row
        content[row] = new boolean[orig[row].length];
        for(int i = 0; i < content[row].length; i++)
            content[row][i] = orig[row][i];
        // now, do row - 1
    }
}

【讨论】:

  • @glowcoder:我不明白你在说什么。 Frame 对象的目的是完全按照您所说的目的封装数组,即通过 getContent() 方法创建一个访问受限(只读)的不可变实例。我的挑战是,在尝试建立初始状态时,我在构造函数期间遇到了多线程暴露问题。您能否给我更多详细信息,或者用某种代码示例更新您的答案,说明您将如何修改我的代码?
  • @c3 那么不,没有。在数据受到保护之前,它不受保护!不过,您可以尝试在开始复制之前连续获取所有锁。我会在我的答案中添加一些东西。它会稍微偏离,但也许你可以从我离开的地方继续。
  • @c3 那里 - 这和我想象的一样好。您遍历并锁定每一行,然后在返回时填写它们。这将填满您的堆栈,因此它可能无法扩展到巨型数组。
  • @glowcoder:知道了。特维姆!即使我没有堆栈问题,它仍然存在一些线程安全问题。啊!我将转到列表和那里可用的不可靠不变性。
  • boolean[][] 可以正常工作,只要您只使用过Frame 对象。无论boolean[][] 来自何处,都将其替换为线程安全对象(例如您的Frame 对象),然后重构它用于使用它的位置。 这将引入性能开销 - 您最好的选择是使其无法从多个线程访问。
【解决方案2】:

您似乎对对象锁有一些误解。

某个对象上的synchronized 块(或方法)避免对此对象进行任何修改。它只是避免了其他线程同时处于同一对象的同步块或方法中(同一对象的wait()中的除外)。

因此,为了在这里实现线程安全,您必须确保对数组的所有访问都在同一个锁对象上同步。

【讨论】:

  • @Paulo:我对你所说的感到困惑。我在对象的引用上使用同步(内容)。鉴于我已经这样做了,我假设其他线程无法访问或修改内容。您是说当我的线程在 synchronized(content) 块内执行时其他线程可以访问和修改内容?如果是这样,同步命令的目的是什么?如果是这样,那么使数组多线程安全的方法是什么?
  • @chaotic3quilibrium:“鉴于我已经这样做了,我假设其他线程无法访问或修改内容”不。获取锁意味着没有其他线程可以获取锁,并不是说与锁关联的对象是不可修改的。如果您想要该属性,您有责任确保在没有先获取锁的情况下无法访问该对象。看看这个教程:download.oracle.com/javase/tutorial/essential/concurrency
  • @c3 他是对的。仅仅因为它是同步的并不意味着它不能被修改——这只是意味着它不能被再次等待。目的是防止它被等待,并由程序员确保所有访问都发生在同步块中。
  • 当然,其他线程可以随意修改。这不是synchronized 的工作方式。通过在某些东西上同步,您可以防止所有其他线程同时在同一个对象上同步。就这样。这与他们修改同一个对象或其他任何事情无关。
  • 同步合约只有在每个人都遵循它的情况下才有效。这就是封装如此重要的原因 - 同步很容易“打破你的交易目的”。
【解决方案3】:

我建议您考虑使用 java.util.concurrent 包。它有很多有用的类可能会对你有所帮助。

对于您的具体示例,您可以编写一个简单的ThreadSafe2DArray 类:

public class ThreadSafe2DArray {
    private ReadWriteLock readWriteLock;
    private Lock readLock;
    private Lock writeLock;
    private boolean[][] data;

    public ThreadSafe2DArray(int rows, int cols) {
        this.data = new boolean[rows][cols];
        this.readWriteLock = new ReentrantReadWriteLock();
        this.readLock = readWriteLock.readLock();
        this.writeLock = readWriteLock.writeLock();
    }

    public boolean get(int row, int col) {
        try {
            readLock.lock();
            return data[row][col];
        }
        finally {
            readLock.unlock();
        }
    }

    public void set(int row, int col, boolean b) {
        try {
            writeLock.lock();
            data[row][col] = b;
        }
        finally {
            writeLock.unlock();
        }
    }

    public boolean[][] copyData() {
        try {
            readLock.lock();
            // Do the actual copy
            ...
        }
        finally {
            readLock.unlock();
        }
    }
}

如果您想允许客户端进行自己的锁定,例如,如果客户端可以读取或更新一个块中的大量条目,您还可以公开原始数据数组和读/写锁。

这种方法比使用原始 synchronized 块灵活得多,并且可能更快或更适合您的情况,尽管很大程度上取决于您在实践中期望的争用类型。这样做的额外好处是您不需要强制任何内容不可变。

从根本上说,如果两个线程要争夺资源,它们必须就某种锁定机制达成一致。如果填充数据数组的线程不能很好地运行,那么唯一的选择是拥有一个强制锁定的包装器。如果另一个线程表现良好,那么您还可以在两个线程中同步顶级数组对象,或者您可以使用暴露锁和数据方法,或者只使用上面介绍的类并调用@987654324 @ 在你的 Frame 类中。

这个答案比我预期的要长得多。希望这是有道理的。

编辑:请注意,将unlock 调用放在finally 块内很重要,以防意外RuntimeException(或Error)在try 内的某处抛出。在这种情况下,代码非常简单,因此可能有点矫枉过正,但如果这个安全网已经到位,它将使代码更易于维护。

EDIT2:我注意到您特别提到了一个不规则数组。为了简洁起见,我将示例保留为常规数组:)

【讨论】:

  • Tyvm 如此明确地说明如何处理数组本身。我正在尝试创建一个封装数组的不可变类。我试图在复制参数时停止对参数的多线程滥用。 Per maaartinus,我只需要不那么担心。我现在计划将该构造函数标记为“非线程安全”以及为什么。然后提供一些线程安全的替代构造函数。
【解决方案4】:

我真的认为您没有掌握多线程的一些核心概念。

如果其他线程引用了您传递给Frame 构造函数的数组,那么您在Frame 中所做的任何操作 都可以帮助您。其他线程可以随意修改。

【讨论】:

  • @c3 - 如果你绝对需要二维数组是线程安全的,你想为它创建一个包装类。该类创建数组,并具有同步的 getter/setter 来访问它;所以你会喜欢public synchronized void set(int x, int y, boolean) 这样只有一个线程可以同时修改它。
猜你喜欢
  • 2010-09-30
  • 1970-01-01
  • 2011-07-30
  • 1970-01-01
  • 1970-01-01
  • 2015-05-05
  • 1970-01-01
  • 2014-02-16
  • 2021-06-19
相关资源
最近更新 更多