重复尝试获取锁是防止死锁的一个好解决方案吗?

huangapple go评论58阅读模式
英文:

Is repeatedly trying to get locks a good solution to prevent deadlocks?

问题

我的问题是关于在使用线程时如何同步和避免死锁。在这个例子中,一个对象简单地持有一个整数变量,多个线程调用这些对象的swapValue方法。

public class Data {

    private long value;

    public Data(long value) {
        this.value = value;
    }

    public synchronized long getValue() {
        return value;
    }

    public synchronized void setValue(long value) {
        this.value = value;
    }

    public void swapValue(Data other) {
        long temp = getValue();
        long newValue = other.getValue();
        setValue(newValue);
        other.setValue(temp);
    }
}

swapValue方法应该是线程安全的,并且如果资源不可用,不应该跳过交换值。简单地在方法签名上使用synchronized关键字会导致死锁。我想出了这个(显然)有效的解决方案,它只基于一个线程在其资源上解锁,而另一个线程在资源仍然未解锁时尝试获取它的概率。

private Lock lock = new ReentrantLock();

...

public void swapValue(Data other) {
    lock.lock();
    while (!other.lock.tryLock()) {
        lock.unlock();
        lock.lock();
    }

    long temp = getValue();
    long newValue = other.getValue();
    setValue(newValue);
    other.setValue(temp);

    other.lock.unlock();
    lock.unlock();
}

对我来说,这看起来像是一个技巧。这是这类问题的常见解决方案吗?是否有更“确定性”的行为并且在实践中也适用的解决方案?

英文:

my question is about synchronisation and preventing deadlocks when using threads. In this example an object simply holds an integer variable and multiple threads call swapValue on those objects.

public class Data {

    private long value;

    public Data(long value) {
        this.value = value;
    }
    public synchronized long getValue() {
        return value;
    }
    public synchronized void setValue(long value) {
        this.value = value;
    }

    public void swapValue(Data other) {
        long temp = getValue();
        long newValue = other.getValue();
        setValue(newValue);
        other.setValue(temp);
    }
}

The swapValue method should be thread safe and should not skip swapping the values if the resources are not available. Simply using the synchronized keyword on the method signature will result in a deadlock. I came up with this (apparently) working solution, which is only based on the probability that one thread unlocks its resource and the other tries to claim it while the resource is still unlocked.

private Lock lock = new ReentrantLock();

...

public void swapValue(Data other) {
    lock.lock();
    while(!other.lock.tryLock())
    {
        lock.unlock();
        lock.lock();
    }

    long temp = getValue();
    long newValue = other.getValue();
    setValue(newValue);
    other.setValue(temp);
    
    other.lock.unlock();
    lock.unlock();

}

To me this looks like a hack. Is this a common solution for these kind of problems? Are there solutions that are "more deterministic" in their behaviour and also applicable in practice?

答案1

得分: 2

有两个问题需要解决:

  • 首先,将Data.locksynchronized关键字使用的内置锁混合使用
  • 其次,在四个锁中存在不一致的锁定顺序 - this.lock,other.lock,this的内置锁和other的内置锁

即使没有synchronizeda.swapValue(b)b.swapValue(a)也可能发生死锁,除非您使用您的方法尝试在锁定和解锁时进行自旋,但这是低效的。

你可以采取的一种方法是为每个Data对象添加一个带有某种final 唯一 ID的字段 - 在交换两个对象的数据时,先锁定ID较低的对象,然后再锁定ID较高的对象,而不管哪个是this,哪个是other。请注意,System.identityHashCode不幸地不是唯一的,所以在这里不能轻松使用它。

解锁的顺序在这里不是关键的,但是在可能的情况下,按照锁定的相反顺序解锁通常是一个好的实践

英文:

There are two issues at play here:

  • First, mixing Data.lock with the built-in lock used by the synchronized keyword
  • Second, inconsistent locking order among four (!) locks - this.lock, other.lock, the built-in lock of this, and the built-in lock of other

Even without synchronized, a.swapValue(b) and b.swapValue(a) can deadlock unless you use your approach to try to spin while locking and unlocking, which is inefficient.

One approach that you could take is add a field with some kind of final unique ID to each Data object - when swapping data of two objects, lock the one with a lower ID before the one with the higher ID, regardless of which is this and which is other. Note that System.identityHashCode is unfortunately not unique so it can't be easily used here.

The unlock ordering isn't critical here, but unlocking in the reverse order of locking is generally a good practice to follow where possible.

答案2

得分: 2

@Nanofarad 的想法是正确的:为每个 Data 实例分配一个唯一、永久的数字 ID,然后使用这些 ID 来决定首先锁定哪个对象。在实践中,这可能看起来像这样:

private static void lockBoth(Data a, Data b) {
    Lock first = a.lock;
    Lock second = b.lock;
    if (a.getID() < b.getID()) {
        first = b.lock;
        second = a.lock;
    }
    first.lock();
    second.lock();
}

private static void unlockBoth(Data a, Data b) {
    a.lock.unlock();
    b.lock.unlock();

    // 注意:在下面的评论中,@Queeg 建议在一般情况下,最好使这个例程总是以与 `lockBoth()` 锁定它们的顺序相反的顺序解锁这两个锁。
    // 请参阅 https://stackoverflow.com/a/8949355/801894 了解解释。
}

public void swapValue(Data other) {
    lockBoth(this, other);
    ...交换它们...
    unlockBoth(this, other);
}
英文:

@Nanofarad has the right idea: Give every Data instance a unique, permanent numeric ID, and then use those IDs to decide which object to lock first. Here's what that might look like in practice:

private static void lockBoth(Data a, Data b) {
    Lock first = a.lock;
    Lock second = b.lock;
    if (a.getID() &lt; b.getID()) {
        first = b.lock;
        second = a.lock;
    }
    first.lock();
    second.lock();
}

private static void unlockBoth(Data a, Data b) {
    a.lock.unlock();
    b.lock.unlock();

    // Note: @Queeg suggests in comments below that in the general case,
    // it would be good practice to make this routine always unlock the
    // two locks in the order opposite to which `lockBoth()` locked them.
    // See https://stackoverflow.com/a/8949355/801894 for an explanation.
}

public void swapValue(Data other) {
    lockBoth(this, other);
    ...swap &#39;em...
    unlockBoth(this, other);
}

答案3

得分: -1

在你的情况下,只需使用AtomicInteger或AtomicLong,而不是重新发明轮子。关于你问题中的同步和死锁部分,一般来说,“不要依赖概率”——这太棘手了,很容易搞错,除非你是一个有经验的数学家,确切地知道自己在做什么——但即使是这样,也是有风险的。一个使用概率的例子是UUID,但如果计算机变得足够快,那么本不应该在地球存在的代码可能在毫秒或更短的时间内就会出现问题,最好编写不依赖概率的代码,尤其是并发代码。

英文:

In your case, just use AtomicInteger or AtomicLong instead inventing the wheel again. About the synchronization and deadlocks part of your question in general - DO NOT RELY ON PROBABILITY -- it is way too tricky and too easy to get it wrong, unless you're experienced mathematician knowing exactly what youre doing - but even then it is risky. One example when probability is used is UUID, but if computers will get fast enough then the code that shouldn't reasonably break till the end of universe can break in matter of milliseconds or faster, it is better to write code that do not rely on probability, especially concurrent code.

huangapple
  • 本文由 发表于 2023年2月10日 05:01:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/75404387.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定