如何安全地深度复制不规则的2D阵列线程

时间:2011-03-28 01:14:57

标签: java arrays thread-safety copy

我在编写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],...,{{收集对象锁定1}})这样我可以调用像content[content.length - 1]这样的东西,并同时按照列表的顺序锁定所有对象。如果是这样,我可以安全地复制数组的内容。

如果没有,还有哪些其他类型的解决方案可以“阻止对数组的所有修改”,而不必更改实例化Frame或改变Frame的构造函数的类,这样它就不会占用数组,而只是实例不可变的收藏品(它本身就是另一个怪胎兔洞)。

感谢您对此领域的任何指导。

更新 我想做的事从根本上说是不可能的。我对通过synchronized锁定对象的理解也是错误的(t​​yvm glowcoder,Paulo和Brian)。我现在要尝试更改Frame上的接口以使用synchronized(objectsToLockSimultaneouslyList),这看起来好像效率低得多。或者我可以使用List<List<Boolean>>,其中XyCoordinate的存在意味着“真”。再次,这似乎是低效的,但线程安全。啊!

4 个答案:

答案 0 :(得分: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
    }
}

答案 1 :(得分:1)

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

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

因此,要在此处保持线程安全,您必须确保对阵列上的所有访问在同一个锁对象上同步。

答案 2 :(得分:1)

我建议您考虑使用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块更灵活,并且对于您的情况可能更快或更好,尽管很大程度上取决于您在实践中期望的那种争用。这样做的另一个好处是你不需要强制任何不可变的东西。

从根本上说,如果有两个线程争夺资源,他们必须就某种锁定机制达成一致。如果填充数据数组的线程不能很好地播放,那么唯一的选择是使用强制锁定的包装器。如果另一个线程表现良好,那么您也可以在两个线程中的顶级数组对象上进行同步,或者您可以使用expose-locks-and-data方法,或者只使用上面提到的类并调用{{ 1}}在你的copyData课程中。

这个答案比我预想的要长很多。希望它有意义。

编辑:请注意,将Frame调用放在unlock块中非常重要,以防finally(或RuntimeException}在{{}}内被抛出{1}}。在这种情况下,代码非常简单,因此它可能过度,但如果已经安装了这个安全网,它将使代码更易于维护。

EDIT2:我注意到你特意提到了一个不规则的数组。为了简洁起见,我把这个例子作为常规数组离开了:)

答案 3 :(得分:0)

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

如果某个其他线程对您传入Frame构造函数的数组有引用,那么您在Frame中执行的 nothing 可以帮助您。其他线程可以随意修改它。