线程同步之wait和notify、notifyall原理

线程同步的时候比较普世的方法就是wait和notify/notifyall来搭配使用,如下所示,这段非常的经典,用于同步过程绝对安全。

package com.company;

public class A {
    private boolean condition;
    private Object lock;

    public void work() {
        AThead thead = new AThead();
        thead.start();
        lock = new Object();

        synchronized (lock) {
            while (!condition) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println("work");
    }

    public class AThead extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println("start");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock) {
                condition = true;
                lock.notifyAll();
            }
        }
    }
}

但是我有几个疑问
1、wait的真正含义是什么?
2、wait和notify为什么放在同步块里执行?
3、为什么wait的时候需要一个while循环确认条件满足?
4、condition需要加volatile吗?

如果这几个问题,看官很清楚的话,就不需要继续往下看了。

wait

首先来看wait到底有什么用?下面是官方的解释

public final void wait(long timeout)
                throws InterruptedException
Causes the current thread to wait until either another thread invokes the notify() method or the notifyAll() method for this object, or a specified amount of time has elapsed.
The current thread must own this object's monitor.

This method causes the current thread (call it T) to place itself in the wait set for this object and then to relinquish any and all synchronization claims on this object. Thread T becomes disabled for thread scheduling purposes and lies dormant until one of four things happens:

Some other thread invokes the notify method for this object and thread T happens to be arbitrarily chosen as the thread to be awakened.
Some other thread invokes the notifyAll method for this object.
Some other thread interrupts thread T.
The specified amount of real time has elapsed, more or less. If timeout is zero, however, then real time is not taken into consideration and the thread simply waits until notified.
The thread T is then removed from the wait set for this object and re-enabled for thread scheduling. It then competes in the usual manner with other threads for the right to synchronize on the object; once it has gained control of the object, all its synchronization claims on the object are restored to the status quo ante - that is, to the situation as of the time that the wait method was invoked. Thread T then returns from the invocation of the wait method. Thus, on return from the wait method, the synchronization state of the object and of thread T is exactly as it was when the wait method was invoked.
A thread can also wake up without being notified, interrupted, or timing out, a so-called spurious wakeup. While this will rarely occur in practice, applications must guard against it by testing for the condition that should have caused the thread to be awakened, and continuing to wait if the condition is not satisfied. In other words, waits should always occur in loops, like this one:

     synchronized (obj) {
         while ()
             obj.wait(timeout);
         ... // Perform action appropriate to condition
     }
 
(For more information on this topic, see Section 3.2.3 in Doug Lea's "Concurrent Programming in Java (Second Edition)" (Addison-Wesley, 2000), or Item 50 in Joshua Bloch's "Effective Java Programming Language Guide" (Addison-Wesley, 2001).
If the current thread is interrupted by any thread before or while it is waiting, then an InterruptedException is thrown. This exception is not thrown until the lock status of this object has been restored as described above.

Note that the wait method, as it places the current thread into the wait set for this object, unlocks only this object; any other objects on which the current thread may be synchronized remain locked while the thread waits.

This method should only be called by a thread that is the owner of this object's monitor. See the notify method for a description of the ways in which a thread can become the owner of a monitor.

总结下,wait会让当前线程进入阻塞状态,并且释放object的锁。只有在触发了notify/notifyAll的时候 或者线程被interrupts ,这个方法才会返回。

为什么需要synchronize

wait和notify都必须处于synchronize块中,来保持同步,为什么需要这样?
如果不放synchronize块中,可能会出现死锁。
举个例子

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();

    public void give(String data) {
        buffer.add(data);
        notify();                   // Since someone may be waiting in take!
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty())    // don't use "if" due to spurious wakeups.
            wait();
        return buffer.remove();
    }
}

这是使用wait,notify写的一个阻塞队列,如果没有同步,那么可能出现这样的场景
A线程调用take,发现buffer.isEmpty为true
B线程执行give
A线程执行wait

此时A线程等不到notify了,因为B线程在wait之前就调用notify了,所以A线程只能一直等下去,直到天荒地老。

为什么需要一个while循环确认条件满足

主要2个原因
1、有可能其他线程调用了notify/notifyAll,但是和当前业务无关,所以必须进行条件判断
2、存在spurious wakeup(虚假唤醒)的情况,可能根本没有通知,线程也被唤醒了。这个我也不太懂,可以参考
https://stackoverflow.com/questions/1050592/do-spurious-wakeups-in-java-actually-happen

使用notify还是notifyall

为了唤醒等待的线程,你应该使用notify还是notifyall呢? 按理说notify是唤醒单个正在等待的线程,而notifyall是唤醒所有等待的线程。一种常见的说法是,你总是应该使用notifyall,这是合理而保守的建议。他总是会产生正确的结果,因为他可以保证你将会唤醒所有需要被唤醒的线程。你可能也会唤醒其他一些线程,但是这不会影响程序的正确性。这些线程醒来之后,会检查他们正在等待的条件,如果条件不满足,会继续等待。

condition需要加volatile吗

Synchronized能够实现原子性和可见性;在Java内存模型中,synchronized规定,线程在加锁时,先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。

有没有更好的实现方法

上述的解决方案非常的经典,既优雅又稳定,但java的concurrent包实际上提供了更高级的语言来实现同步方法,比如CountDownLatch、Atomic、Semaphore、CyclicBarrier、ConcurrentHashMap等,能用高阶的方法尽量用高阶的。比如上述代码,我们就可以用CountDownLatch来解决,不用考虑同步问题,使用起来超级简单。

package com.company;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;

public class B {
    private CountDownLatch countDownLatch;

    public void work() {
        countDownLatch = new CountDownLatch(1);
        AThead thead = new AThead();
        thead.start();

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("work");
    }

    public class AThead extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println("start");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end");
            countDownLatch.countDown();
        }
    }
}

ref

https://docs.oracle.com/javase/6/docs/api/java/lang/Object.html#wait(long)
https://stackoverflow.com/questions/2779484/why-must-wait-always-be-in-synchronized-block
https://stackoverflow.com/questions/1050592/do-spurious-wakeups-in-java-actually-happen
java effective 69

你可能感兴趣的:(java)