【问题标题】:Threadsafe bidirectional association in JavaJava中的线程安全双向关联
【发布时间】:2011-07-01 11:53:19
【问题描述】:

什么是实现线程安全双向关联的好方法?有没有好的库或代码生成器?

这是一个非线程安全的例子:

class Foo {

    private Foo other;

    public Foo getOther() {
        return other;
    }

    public void setOther(Foo other) {
        this.setOtherSecretly(other);
        other.setotherSecretly(this);
    }

    void setOtherSecretly(Foo other) {
        if (this.other != null) this.other.other = null;
        this.other = other;
    }
}

我对线程安全的要求是:

  • 没有死锁
  • 最终一致性(当所有线程停止修改对象时,最终会达到一致状态。即,当另一个线程同时执行 setOther 时,assert foo.getOther().getOther() == foo 失败是可以接受的。
  • 顺序行为。如果一个线程执行setOther 并且没有其他线程覆盖该值,getOther 立即返回该线程的新值。
  • 没有时光倒流。一旦一个线程用getOther 观察到一个新值,它就再也不会接收到旧值(除非它被再次设置)。

也很高兴拥有:

  • 低争用,尤其是没有全局锁。该解决方案应该可以很好地扩展。
  • 尽可能少的同步开销。对于单线程,它应该具有合理的性能。
  • 内存开销低。当一个对象有 5 个关联时,我不希望每个关联有 3 个附加字段。设置器中的局部变量是可以的。

我的应用程序将有 16 个线程处理多个类的大约 5000 个对象。

我还没有提出解决方案(不,这不是家庭作业),因此欢迎提供任何意见(想法、文章、代码)。

【问题讨论】:

  • 这似乎很困难,如果可能的话。如果你用全局锁来做,看看性能是否可以接受?

标签: java multithreading thread-safety scalability bidirectional


【解决方案1】:

事实证明这是一个非常困难的问题! (很好!)使用全局锁太容易了,而且可能太慢了。我想我有一个无锁版本——我将在下面介绍——但我不会太相信它是完美的。很难推断所有可能的交错。

事实证明,这是transactional memory 的完美用例!只需将整个块标记为原子并修改您想要的任何内容!你可以看看Deuce STM,虽然我不知道它有多快。要是最好的系统不需要定制硬件就好了……

无论如何,在考虑了这个问题一段时间后,我想我想出了一个使用Java的AtomicReference绕过锁的版本。一、代码:

class Foo {

    private AtomicReference<Foo> oRef = new AtomicReference<Foo>;

    private static final AtomicInteger order = new AtomicInteger(0);
    private final int id = order.incrementAndGet();

    private static bool break(Foo x, Foo y) {
        if (x.id > y.id)
            return break(y, x);

        return x.oRef.compareAndSet(y, null) &&
               y.oRef.compareAndSet(x, null);
    }

    public void setOther(Foo f) {
        if (f != null && f.id > id) {
            f.setOther(this);
            return;
        }
        do {
            Foo other = oRef.get();
            if (other == f)
                break;

            if (other != null && !break(this, other))
                continue;

            if (f == null)
                break;

            Foo fother = f.oRef.get();
            if (fother != null && !break(f, fother))
                continue;

            if (!f.oRef.compareAndSet(null, this))
                continue;
            if (!oRef.compareAndSet(null, f)) {
                f.oRef.set(null);
                continue;
            }
        } while (false);
    }
}

关键点:

  • 如果没有对任何受影响的 Foo 的并发访问(最多 4 个),setter 将通过循环修改相关指针。
  • 如果存在并发设置器,一些设置器可能会失败并重试。
  • 如果多个线程同时尝试中断关系,则只有一个线程会成功执行x.oRef.compareAndSet(y, null)
  • 如果f.oRef.compareAndSet(null, f)成功,其他线程将无法打破break()中半建立的关系。那么如果oRef.compareAndSet(null, f)成功,则操作完成。如果失败,f.oRef 可以重置,每个人都重试。

【讨论】:

  • 如果您有一个保证唯一且一致的排名(我倾向于使用long 而不是int 以避免任何溢出的可能性),那么您可以放心锁定要调整的项目(如果锁是按顺序获取锁,按与获取相反的顺序释放,则不可能出现死锁)。使用锁推理代码可能比使用 compare-and-set 推理代码更容易。
【解决方案2】:

Google Guava 为您执行此操作:BiMap

例如:

BiMap<Integer, String> bimap = Synchronized.biMap(HashBiMap.create(), someMutexObject);
bimap.put(1, "one");
bimap.put(2, "two");

bimap.get(1); // returns "one"
bimap.inverse().get("one") // returns 1

someMutexObject 可以是您想要synchronize 的任何对象。

【讨论】:

  • HashBiMap 内部依赖于 HashMaps,它不是线程安全的。
  • Synchronized.biMap( 来自哪里?
【解决方案3】:

我可以想到一个静态成员来充当监视器。但也许这就是你认为的“全局”锁。

class Foo {

    private static final Object MONITOR = new Object();
    private Foo other;

    public Foo getOther() {
        synchronized(MONITOR){
            return other;
        }
    }

    public void setOther(Foo other) {
        synchronized(MONITOR){
            this.setOtherSecretly(other);
            other.setotherSecretly(this);
        }
    }

    void setOtherSecretly(Foo other) {
        if (this.other != null) this.other.other = null;
        this.other = other;
    }
}

【讨论】:

    【解决方案4】:

    另一种选择是简单地使other 引用可变。这将满足您的要求和您的优点。

    【讨论】:

    • 这足以防止以下情况发生吗?三个实例 A、B、C。 Thread1 调用 A.setOther(B); Thread2 调用 A.setOther(C)。在没有同步或锁定的情况下,分配可能按以下顺序发生:A.other=B; A.其他=C; C.其他=A; B.其他=A。当 A 指向 C 作为它的另一个时,让 B 错误地指向 A 作为它的另一个。
    • 这绝对不足以确保一致性。
    • 不,这样做不安全。根据您的问题,我没想到实际上不止 1 个线程会调用 setOther() - 这样做似乎是做作和荒谬的 - 也就是说,它表明设计可能被破坏。
    【解决方案5】:

    您可以将每个对象关联到它们自己的锁,然后在获取两个锁的同时设置另一个。例如。为避免死锁,您可以使用锁排序

    class Foo extends ReentrantLock {
    
        private static final AtomicInteger order = new AtomicInteger(0);
    
        final int id = order.incrementAndGet();
    
        private Foo other;
    
        public Foo getOther() {
            return other;
        }
    
        public void setOther(Foo other) {
            if (id > other.id) {
                other.lock();
                try {
                    this.lock();
                    try {
    
                        // assign here
                    } finally {
                        this.unlock();
                    }
                } finally {
                    other.unlock();
                }
            } else if (id < other.id) {
                this.lock();
                try {
                    other.lock();
                    try {
    
                        // assign here
                    } finally {
                        other.unlock();
                    }
                } finally {
                    this.unlock();
                }
            }
        }
    }
    

    【讨论】:

    • 基本想法是合理的,但请记住,如果您还需要撤消现有关联(正如原始示例代码所暗示的那样),您需要获取可能另外两个对象的锁。并且由于需要在获取锁之前读取数据,以确定获取锁的顺序,因此需要在获取锁之后重新验证关系,然后再进行任何更改。我认为这一切都是可行的。
    【解决方案6】:

    试试这个,不写就可以读。

    ReentrantReadWriteLock

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-01-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多