Java线程:wait和notify方法

3

我有一个线程调用了wait方法,只有在某个其他类中调用notify方法时才能唤醒它:

 class ThreadA {
     public static void main(String [] args) {
         ThreadB b = new ThreadB();
         b.start();

         synchronized(b) {
             try {
                 System.out.println("Waiting for b to complete...");
                 b.wait();
             } catch (InterruptedException e) {}
             System.out.println("Total is: " + b.total);
         }
     }
 }

class ThreadB extends Thread {
    int total;
    public void run() {
        synchronized(this) {
            for(int i=0;i<100;i++) {
                total += i;
            }
            notify();
        }
    }
}

在上面的代码中,如果main中的synchronized块,如果ThreadA没有先执行而是另一个同步块执行并完成,那么ThreadA将执行其自己的synchronized块并调用wait,会发生什么以及如何再次通知它?

@templatetypedef:感谢您重新格式化(我刚开始做这个)。 - Paŭlo Ebermann
3
notify() 是为神明保留的。凡人应该使用notifyAll()。 - Julius Musseau
@Julius Davies,ParkSuppot.park/unpark更接近于神,notify仍然很菜 :) - bestsss
@bestsss,请解释一下。您的意思是park/unpark比wait/notify更有效吗? - Pacerier
@Pacerier,有点类似,尽管在某个时候进行了调整,现在它需要额外的内存屏障。Park/Unpark 不需要显式同步(这很好,尽管它仍然可能使用本地互斥锁),但是适当的使用可能需要队列。对于简单情况,park/unpark 通常优于 wait/notify,但没有 notifyAll(请参见关于队列的注释)。也不需要将所有内容都包装在 try/catch(InterruptedException) 中。 - bestsss
7个回答

10
如果ThreadBThreadA之前执行完其synchronized块,则ThreadA在调用wait时将无限期地被阻塞。它不会因为另一个线程已经完成而得到通知。
问题在于你试图以一种它们没有被设计用来使用的方式来使用waitnotify。通常,waitnotify用于使一个线程等待直到某个条件成立,然后让另一个线程通知该条件可能已经成立。例如,它们经常按以下方式使用:
/* Producer */
synchronized (obj) {
    /* Make resource available. */
    obj.notify();
}

/* Consumer */
synchronized (obj) {
    while (/* resource not available */)
        obj.wait();

    /* Consume the resource. */
}
以上代码之所以有效,是因为无论哪个线程先运行都没有关系。如果生产者线程创建了一个资源,并且没有任何人在等待 obj,那么当消费者运行时,它将进入 while 循环,注意到已经生产了资源,然后跳过调用 wait。然后它就可以消耗该资源。另一方面,如果消费者先运行,则会在 while 循环中注意到资源尚未可用,并等待其他对象通知它。然后另一个线程就可以运行,生产资源,并通知消费者线程该资源可用。一旦原始线程被唤醒,它将注意到循环的条件不再成立,并消耗资源。
更普遍地说,Java 建议您始终在循环中调用 wait,因为存在虚假通知,在这种通知中,线程可能从对 wait 的调用中醒来,而没有收到任何通知。使用上述模式可以防止这种情况发生。
在您特定的情况下,如果您希望确保 ThreadB 运行完成后 ThreadA 执行,您可能希望使用 Thread.join(),它明确地阻塞调用线程,直到某些其他线程执行。更一般地,您可能需要研究 Java 提供的一些其他同步原语,因为它们通常比 wait 和 notify 更易于使用。

先生,我有一个疑问,如何知道哪个线程拥有特定对象的锁。您能为我澄清吗? - satheesh

1
你可以使用循环并等待直到总数被计算出来:
synchronized(b) {
   while (total == 0) {
       b.wait();
   }
}

你也可以使用更高级的抽象,比如 CountDownLatch


1
new CountDownLatch(1)通常是我最喜欢的等待条件。 - bestsss

1

ThreadB的run方法在进入ThreadA.main中的同步块之前可能已经完成。在这种情况下,由于notify调用已经发生在您开始等待之前,ThreadA将永远阻塞在wait调用上。

一个简单的解决方法是在启动第二个线程之前,在主线程中获取b的锁,以确保wait先发生。

ThreadB b = new ThreadB();
synchronized(b) {
    b.start();
    ...
    b.wait();
}

这是一个很好的建议,确保ThreadB在A获得锁之前不开始工作。然而,您仍然希望在同步块内部有一个循环来检查某些成功条件是否为真,而不是依赖于notify()调用。这可能会被虚假唤醒所触发。 - Jesse Barnum

0
你需要添加一些用于线程间通信的标志,以便B在完成时向A发出信号。一个简单的布尔变量就可以了,只要它在同步块中进行读写操作。
synchronized(this) {
    for(int i=0;i<100;i++) {
        total += i;
    }
    isDone = true;
    notify();
}

2) A需要在等待时循环。因此,如果您的布尔变量名为isDone,并且由threadB设置为true,则threadA应该有以下代码:

synchronized(b) {
    System.out.println("Waiting for b to complete...");
    while( ! isDone ) b.wait();
}

在这种情况下,实际上没有理由在A中使用同步块 - 因为threadB在运行完成后不做任何事情,而A除了等待B之外什么也不做,因此threadA可以简单地调用b.join()来阻塞直到它完成。我假设你的实际用例比这更复杂。

第二段代码应该是 while(!b.isDone) b.wait(); - bestsss
我认为它不需要是b.isDone。布尔变量不需要存在于b线程中,可以在父类中拥有它也是可以的。它也不需要被标记为volatile,因为所有对它的读写都发生在具有相同锁对象的同步块中。如果我对此有误,请告诉我详细原因。[我刚刚重新阅读了我的帖子,我想我看到了混淆之处 - 我假设isDone在父类中定义,而不是在b中定义] - Jesse Barnum
父类是java.lang.Thread。除非您使用定义了isDone的某个外部类,否则代码将无法编译。除此之外,只要'b'实例和'isDone'在双射中,代码就可以正常工作(因此最好将isDone放在b类中)。如果您在同一锁下访问它们,则无需volatile。 - bestsss
好的,我理解你的意思。我一开始以为ThreadB是ThreadA的内部类,在这种情况下,ThreadA中的实例变量可以工作,但是重新阅读代码后,没有迹象表明ThreadB是内部类。 - Jesse Barnum

0

为什么要那么复杂?只需使用Thread的join()函数即可。

ThreadB b = new ThreadB();
b.start();
b.join();
// now print b.total

0
你可能想要使用java.util.concurrent.Semaphore来实现这个功能。

-1

不要使用synchronized(thread),不要这样做,不要使用synchronized(thread).. 重复:不要使用synchronized(thread) :)

如果你需要等待线程'b'完成,请使用b.join(),现在你的代码可以自由地挂起在b.wait()中。

--

希望下面的源代码能给你提供一些见解,而在使用同步(线程)/通知(notify())时我认为是不好的实践。(剪切-剪切)
祝愉快

在继续之前,请确保您已经接受了Oracle的许可协议,可以在以下链接中找到: https://cds.sun.com/is-bin/INTERSHOP.enfinity/WFS/CDS-CDS_Developer-Site/en_US/-/USD/ViewLicense-Start?LicenseUUID=7HeJ_hCwhb4AAAEtmC8ADqmR&ProductUUID=pGqJ_hCwj_AAAAEtB8oADqmS&cnum=&evsref=&sln=

Java源代码(包括在内)在init()中被调用,自Java 1.5以来,任何Java构造函数都可以有效地调用它。

private static **synchronized int** nextThreadNum() {
return threadInitNumber++;
}

//join (该方法仅使用纳秒增加毫秒,如果纳秒>500000,则毫秒为0且纳秒>0

public final **synchronized** void join(long millis) 
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;

if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
}

if (millis == 0) {
    while (isAlive()) {
    wait(0);
    }
} else {
    while (isAlive()) {
    long delay = millis - now;
    if (delay <= 0) {
        break;
    }
    wait(delay);
    now = System.currentTimeMillis() - base;
    }
}
}


public **synchronized** void start() {
    /**
 * This method is not invoked for the main method thread or "system"
 * group threads created/set up by the VM. Any new functionality added 
 * to this method in the future may have to also be added to the VM.
 *
 * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    group.add(this);
    start0();
    if (stopBeforeStart) {
    stop0(throwableFromStop);
}
}

//stop1 在 stop 之后被调用以确保适当的权限

private final **synchronized** void stop1(Throwable th) {
SecurityManager security = System.getSecurityManager();
if (security != null) {
    checkAccess();
    if ((this != Thread.currentThread()) ||
    (!(th instanceof ThreadDeath))) {
    security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
    }
}
    // A zero status value corresponds to "NEW"
if (threadStatus != 0) {
    resume(); // Wake up thread if it was suspended; no-op otherwise
    stop0(th);
} else {

        // Must do the null arg check that the VM would do with stop0
    if (th == null) {
    throw new NullPointerException();
    }

        // Remember this stop attempt for if/when start is used
    stopBeforeStart = true;
    throwableFromStop = th;
    }
}

2
这个答案没有提供任何理由说明为什么你不应该在其他线程对象上进行同步,也没有详细回答原始问题。 - templatetypedef
如果您使用sync(线程)/ notify()操作,将会影响到任何等待该条件的join操作。 - bestsss
@templatetypedef 开始/停止/加入所有同步线程对象。新线程在Thread.class上同步。查看源代码,即使没有明确说明,所有的happens-before都需要在某个东西上同步才能发生,自JDK 0.9以来,它就是线程对象。事实上,这已经成为了一种标准。 - bestsss
1
@bestsss- 在Thread类对象上同步不同于在单个Thread对象上同步;两者是不同的实体。此外,我查看了Thread类的源代码,但startjoin方法被标记为native,因此我无法看到其实现。我也无法在Google上找到有关线程锁定Thread.class对象的信息,并且JLS将Thread.join特殊处理为强制执行happens-before的一种方式,因此我不确定在Thread类上同步是否是happens-before的原因。 - templatetypedef
2
也许可以这样写:由于timed join中的一个bug,不建议将Thread对象用作其他用途的锁对象,而不是你的绝对说法*不要synchronized(thread),不要这样做,不要synchronized(thread)..重申:不要synchronized(thread)*。 - Paŭlo Ebermann
显示剩余10条评论

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