同步块锁定对象和wait/notify

3

根据我的理解,当我使用同步块时,它会获取对象的锁,并在代码块执行完毕后将其释放。在以下代码中:

public class WaitAndNotify extends Thread{

    long sum;

    public static void main(String[] args) {
        WaitAndNotify wan = new WaitAndNotify();
        //wan.start();
        synchronized(wan){
            try {
                wan.wait();
            } catch (InterruptedException ex) {
                Logger.getLogger(WaitAndNotify.class.getName()).log(Level.SEVERE, null, ex);
            }
            System.out.println("Sum is : " + wan.sum);
        }
    }

    @Override
    public void run(){
        synchronized(this){
            for(int i=0; i<1000000; i++){
                sum = sum + i;
            }
            notify();
        }

    }  
}

如果运行方法内的同步块首先获取锁定,那么主方法内的同步块必须等待(不是因为wait(),而是因为另一个线程获取了锁定)。当运行方法执行完毕后,主方法将进入其同步块并等待通知,但它永远不会得到通知,这是我哪里误解了吗?

远离这些基础 API,转向像 java.util.concurrent.Executors 和 ExecutorService 这样的高级 API。请查看代码示例:http://examples.javacodegeeks.com/core-java/util/concurrent/executorservice/java-executorservice-example-tutorial/ - Ravindra babu
1
@sunrise76,java.util.concurrent中的类并不是“更高级”的:它们在_更高层次的抽象_上操作。编写生产代码的开发人员绝对应该使用更高级别的工具,但一个_学生_应该了解构建这些更高级别工具的基本原语。就像学习汇编语言的人在使用高级语言编写代码时做出更明智的决策一样,学习互斥锁、条件变量和原子操作的人在使用队列、线程池等工具时会更加得心应手。 - Solomon Slow
1
你的理解是正确的。如果没有其他线程在同一时刻被阻塞在foo.wait()调用中,foo.notify()将不会起作用。 - Solomon Slow
5个回答

3

wait()方法会暂时退出相应的监视器,然后在返回时重新进入:

请参见wait()

当前线程必须拥有此对象的监视器。线程释放此监视器的所有权,并等待另一个线程通过调用notify方法或notifyAll方法来唤醒正在等待该对象监视器的线程。然后,线程等待重新获取监视器的所有权并恢复执行。

这就是为什么这种同步方式能够起作用的原因和方式。


当调用 wait() 时,会释放对象监视器,而 notify()/notifyAll() 不会释放。"被唤醒的线程在当前线程放弃对该对象的锁定之前无法继续执行。" - user5132301

1

是的,可以在wait()之前执行notify(),导致线程挂起,因此需要小心确保它不会发生。

因此(以及其他原因),通常最好使用java.util.concurrent的高级结构,因为它们通常为您提供更少的犯错可能性。


1
在这里,您不会看到“永远等待”的问题,因为您正在调用带有超时的wait()版本;因此,即使没有收到通知,5秒后它也会返回。 wait()调用的“无限等待”版本确实可能会出现您描述的问题。

很抱歉我忘记删除5秒钟的部分。如果是wait(),那么notify()在wait()之前运行是否可能? - Lakshitha Ranasinghe

1
你有两个线程:你的WaitAndNotify(WAN)线程和Java的主执行线程。 两者都在争夺同一个锁。
如果WAN线程先获得锁,则主线程将被阻塞。 处于阻塞状态并不等同于处于等待状态。 等待状态的线程将在继续之前等待通知。 处于阻塞状态的线程将在锁变为可用时积极尝试获取锁(并一直尝试直到成功)。
假设run方法正常执行,它将调用notify(),但这将没有任何效果,因为当前没有其他线程处于等待状态。 即使有,WAN仍然持有锁,直到退出代码的同步块。 一旦WAN退出该块,Java将通知等待的线程(如果有的话,但现在没有)。
此时,主执行线程现在获得锁(不再被阻塞)并进入等待状态。 现在你已经使用了等待5000毫秒后继续的版本。 如果使用基本版本(wait()),它将永远等待,因为没有其他进程会通知它。

1
这是一个编程示例程序的版本,引入了一个测试条件变量的循环。这样可以避免在线程从等待中唤醒后重新获取锁定状态时对事物状态做出错误的假设,并且两个线程之间没有顺序依赖关系。请参考以下代码:

public class W extends Thread {
    long sum;
    boolean done;

    public static void main(String[] args) throws InterruptedException {
        W w = new W();
        w.start();
        synchronized(w) {
            while (!w.done) {
                w.wait();
            }
            // move to within synchronized block so sum
            // updated value is required to be visible
            System.out.println(w.sum);
        }
    }

    @Override public synchronized void run() {
        for (int i = 0; i < 1000000; i++) {
           sum += i;
        }
        done = true;
        // no notify required here, see nitpick at end
    }
}

你指出了等待通知的不足之处(顺序依赖,你需要依赖竞态条件,希望一个线程在另一个线程之前获取监视器),以及其他原因。一方面,一个线程可以在没有接收到通知的情况下从等待中唤醒,你不能假定有任何通知调用。

当一个线程等待时,它需要在循环中等待,在循环中测试检查某些条件。另一个线程应该设置这个条件变量,以便第一个线程可以检查它。Oracle教程建议:

注意:总是在测试等待条件的循环内调用wait方法。不要假设中断是为了您正在等待的特定条件,或者该条件仍为真。

其他细节问题:

  • 在你的示例中,JVM不需要将对sum变量的更改可见于主线程。如果添加一个同步实例方法来访问sum变量,或者在同步块内访问sum,则主线程将保证看到更新后的sum值。

  • 从日志记录来看,InterruptedException没有任何严重问题,它并不意味着有什么错误发生。当您调用线程的interrupt方法,设置其中断标志,并且该线程当前正在等待或休眠,或者使用标志仍然设置进入wait或sleep方法时,将触发InterruptedException。在我上面回答的玩具示例中,我将异常放在throws子句中,因为我知道这种情况不会发生。

  • 当线程终止时,它发出一个notifyAll,所有等待该对象的内容都将收到(这就是join是如何实现的)。最好使用Runnable而不是Thread,部分原因就是如此。

  • 在这个特定的示例中,更合理的做法是在计算线程上调用Thread#join,而不是调用wait。

下面是使用join进行重写的示例:

public class J extends Thread {
    private long sum;

    synchronized long getSum() {return sum;}

    public static void main(String[] args) throws InterruptedException {
        J j = new J();
        j.start();
        j.join();
        System.out.println(j.getSum());
    }

    @Override public synchronized void run() {
        for (int i = 0; i < 1000000; i++) {
           sum += i;
        }        
    }
}

Thread#join 调用 wait 方法,在线程对象上进行锁定。当求和线程终止时,它发送一个通知并将其 isAlive 标志设置为 false。同时,在 join 方法中,主线程正在等待求和线程对象,它接收到通知,检查 isAlive 标志,并意识到不必再等待,因此可以离开 join 方法并打印结果。


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