Java, 使用同步方法进行多线程操作

5

我有些困难,无法保证我的程序不会死锁。我认为我需要添加第三个同步方法release,在ping被调用后可以用来释放另一个线程。

以下是代码:

// Attempt at a simple handshake.  Girl pings Boy, gets confirmation.
// Then Boy pings girl, get confirmation.
class Monitor {
    String name;

    public Monitor (String name) { this.name = name; }

    public String getName() {  return this.name; }

     // Girl thread invokes ping, asks Boy to confirm.  But Boy invokes ping,
    // and asks Girl to confirm.  Neither Boy nor Girl can give time to their
    // confirm call because they are stuck in ping.  Hence the handshake 
    // cannot be completed.
    public synchronized void ping (Monitor p) {
      System.out.println(this.name + " (ping): pinging " + p.getName());
      p.confirm(this);
      System.out.println(this.name + " (ping): got confirmation");
    }

    public synchronized void confirm (Monitor p) {
       System.out.println(this.name+" (confirm): confirm to "+p.getName());
     }
}

class Runner extends Thread {
    Monitor m1, m2;

    public Runner (Monitor m1, Monitor m2) { 
      this.m1 = m1; 
      this.m2 = m2; 
    }

    public void run () {  m1.ping(m2);  }
}

public class DeadLock {
    public static void main (String args[]) {
      int i=1;
      System.out.println("Starting..."+(i++));
      Monitor a = new Monitor("Girl");
      Monitor b = new Monitor("Boy");
      (new Runner(a, b)).start();
      (new Runner(b, a)).start();
    }
}

那么你究竟想要同步什么?目前,您已设置为防止监视器同时ping和确认。您想要避免哪种竞争条件? - Thomas
请纠正我,但是按照现在的代码,ping 在调用 confirm 时不会立即挂起吗? - Dennis Meng
大约有1/10的时间我会遇到死锁(两个线程都在等待对方确认)。我需要代码是确定性的,这样它就永远不会死锁。我收到了提示“向Monitor类添加第三个同步方法,称为release。一个线程可以使用此方法释放另一个线程。将ping更改为wait,然后确认给另一个线程。然后释放另一个线程。” - user1311286
1
@DennisMeng 不会立即执行,因为ping的同步锁定在“this”上,而p.confirm的同步锁定在“p”上。问题在于第二个线程中的ping调用也锁定了“p”。 - Thomas
2个回答

6
当某个操作需要获取两个不同的锁时,确保没有死锁的唯一方法是确保尝试执行这些操作的每个线程以相同的顺序获取多个对象上的锁。要解决死锁问题,您需要像这样修改代码-虽然不太美观,但它可以实现。
 public void ping (Monitor p) {
  Monitor one = this;
  Monitor two = p;
  // use some criteria to get a consistent order
  if (System.identityHashCode(one) > System.identityHashCode(two)) {
    //swap
    Monitor temp = one;
    one = two;
    two = one;
  }
  synchronized(one) {
       synchronized(two) {
           System.out.println(this.name + " (ping): pinging " + p.getName());
           p.confirm(this);
           System.out.println(this.name + " (ping): got confirmation");
        }
  }
}

1
我喜欢你的方法。你看到这里有任何性能瓶颈吗?有更好的方法可以实现吗? - Sid
1
代码有点丑陋,但不应该有任何性能开销。我在答案中找到了一个更详细的类似问题 - https://dev59.com/22HVa4cB1Zd3GeqPjyXe。可能在util.concurrent包中有更好的解决方案 - 但是使用同步锁,我想不出更好的方法。 - gkamal
谢谢回复。这会有所帮助。 - Sid
1
在名称相等的极低概率情况下要小心,因为在这种情况下仍可能出现死锁。您可以使用 System.identityHashCode() 来降低风险。为了确保完全安全,您可以添加一个子句,即如果两个哈希码相等,则使用一个单独的锁,只在这些情况下使用。 - assylias
谢谢,我已经更新了我的答案。我认为只有当对象相同时hashCode才可能相同。如果两个对象相同,则顺序无关紧要——第二个synchronized将是一个noop(synchronized获取可重入锁)。 - gkamal
@gkamal identityHashcode 是一个整数,所以如果你在一个使用足够内存的 64 位机器上,可能会发生冲突(但是可以承认非常罕见)。 - assylias

0

这是棘手的问题。我会将Monitor.name设为volatile,或者使用它自己的锁对象进行同步。最好的方法是:将其设为final。然后从你的两个方法中删除synchronized关键字。除了“name”之外,里面没有任何不安全的线程内容。

否则,在需要同步之前不要同步。使用同步块而不是方法。除非必须在同步块中放置任何内容。如果可以,请在单独的锁对象上分别同步字段。不要嵌套同步块,但如果必须始终以相同的顺序嵌套它们;具有同步对象层次结构。

同步所有方法是保持线程安全的简单有效的技术。但是,当您的线程开始交互时,很容易死锁或变得缓慢。然后就变得有趣了。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接