java并发编程之三:wait/notify/sleep/yield/join

1.线程的状态

Java中线程中状态可分为五种:New(新建状态),Runnable(就绪状态),Running(运行状态),Blocked(阻塞状态),Dead(死亡状态)。

New:新建状态,当线程创建完成时为新建状态,即new Thread(...),还没有调用start方法时,线程处于新建状态。

Runnable:就绪状态,当调用线程的的start方法后,线程进入就绪状态,等待CPU资源。处于就绪状态的线程由Java运行时系统的线程调度程序(thread scheduler)来调度。

Running:运行状态,就绪状态的线程获取到CPU执行权以后进入运行状态,开始执行run方法。

Blocked:阻塞状态,线程没有执行完,由于某种原因(如,I/O操作等)让出CPU执行权,自身进入阻塞状态。

Dead:死亡状态,线程执行完成或者执行过程中出现异常,线程就会进入死亡状态。


线程状态转换图

1.wait/notify/notifyAll方法的使用:

JDK中一共提供了这三个版本的方法,

(1)wait()方法的作用是将当前运行的线程挂起(即让其进入阻塞状态),直到notify或notifyAll方法来唤醒线程.

(2)wait(long timeout),该方法与wait()方法类似,唯一的区别就是超过指定时间时,如果还没有notify或notifAll方法的唤醒,也会自动唤醒。

(3)至于wait(long timeout,long nanos),本意在于更精确的控制调度时间,不过从目前版本来看,该方法貌似没有完整的实现该功能,其源码如下:

//Object类:
    public final void wait() throws InterruptedException {
        wait(0);
    }
// Android-changed: Implement wait(long) non-natively.
    // public final native void wait(long timeout) throws InterruptedException;
    public final void wait(long timeout) throws InterruptedException {
        wait(timeout, 0);
    }
   @FastNative
    public final native void wait(long timeout, int nanos) throws InterruptedException;

通过源码可以看到最终调用的是底层native方法wait(long timeout, int nanos)。
其实wait方法底层也是通过对象锁监视器monitor实现的,可以通过代码验证下:

//方法没有使用synchronized修饰,调用method方法时会抛出IllegalMonitorStateException异常
public void method() throws InterruptedException {
        wait();
    }

//异常
Thrown to indicate that a thread has attempted to wait on an object's monitor or to notify other threads waiting on an object's monitor without owning the specified monitor.
这句话的意思大概就是:线程试图等待对象的监视器或者试图通知其他正在等待对象监视器的线程,但本身没有对应的监视器的所有权。wait方法是一个本地方法,其底层是通过一个叫做监视器锁的对象来完成的。所以上面之所以会抛出异常,是因为在调用wait方式时没有获取到monitor对象的所有权,那如何获取monitor对象所有权?Java中只能通过Synchronized关键字来完成,修改上述代码,增加Synchronized关键字:

public synchronized void method() throws InterruptedException {
        wait();
    }

有了对wait方法原理的理解,notify方法和notifyAll方法就很容易理解了。既然wait方式是通过对象的monitor对象来实现的,所以只要在同一对象上去调用notify/notifyAll方法,就可以唤醒对应对象monitor上等待的线程了。notify和notifyAll的区别在于前者只能唤醒monitor上的一个线程,对其他线程没有影响,而notifyAll则唤醒所有的线程.
注意:调用wait方法后,线程是会释放对monitor对象的所有权的,即会释放对象锁,这点和sleep不同。

2. sleep/yield/join方法解析

这三个方法都位于Thread类中是静态方法,而上面三个方法都位于Object类中。

2.1 sleep(long timeout)

sleep方法的作用是让当前线程暂停指定的时间(毫秒),sleep方法是最简单的方法,在上述的例子中也用到过,比较容易理解。唯一需要注意的是其与wait方法的区别。最简单的区别是,wait方法依赖于同步,而sleep方法可以直接调用。而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则需要释放锁。sleep暂停期间一直持有monitor对象锁,其他线程是不能进入的。而wait方法则不同,当调用wait方法后,当前线程会释放持有的monitor对象锁,因此,其他线程还可以进入到同步方法,线程被唤醒后,需要竞争锁,获取到锁之后再继续执行

2.2 yield

yield方法的作用是暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止。yield方法只是将Running运行状态转变为Runnable就绪状态。

2.3 join()/join(long timeout)

join方法的作用是父线程等待子线程执行完成后再执行,换句话说就是将异步执行的线程合并为同步的线程。其实现与wait方法类似,join()方法实际上执行的join(0),而join(long millis)也与wait(long millis)的实现方式一致,join内部是通过wait来实现的,可以在源码中查看:

/* @throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          interrupted status of the current thread is
     *          cleared when this exception is thrown.
     */
    public final void join() throws InterruptedException {
        join(0);
    }

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

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

        if (millis == 0) {
            while (isAlive()) {
                lock.wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                lock.wait(delay);//调用wait方法,但是哪里调用的lock.notify/notifyAll()唤醒的呢
                now = System.currentTimeMillis() - base;
            }
        }
        }
    }

大家重点关注一下join(long millis)方法的实现,可以看出join方法就是通过wait方法来将线程的阻塞,如果join的线程还在执行,则将当前线程阻塞起来,直到join的线程执行完成,当前线程才能执行。不过有一点需要注意,这里的join只调用了wait方法,却没有对应的notify方法,原因是Thread的start方法中做了相应的处理,所以当join的线程执行完成以后,会自动唤醒主线程继续往下执行。

总结

wait/notify/notifyAll方法的作用是实现线程间的协作,那为什么这三个方法不是位于Thread类中,而是位于Object类中?位于Object中,也就相当于所有类都包含这三个方法(因为Java中所有的类都继承自Object类)。要回答这个问题,还是得回过来看wait方法的实现原理,大家需要明白的是,wait等待的到底是什么东西?如果对上面内容理解的比较好的话,我相信大家应该很容易知道wait等待其实是对象monitor监视器对象,因为Java中的每一个对象都有一个内置的monitor对象,自然所有的类都理应有wait/notify方法。

区别
notify:只会唤醒等待该锁的其中一个线程。
notifyAll:唤醒等待该锁的所有线程。
既然notify会唤醒一个线程,并获取锁,notifyAll会唤醒所有线程并根据算法选取其中一个线程获取锁,那最终结果不都是只有一个线程获取锁吗?那JDK为什么还需要做出来这两个方法呢?这两种同步方法本质上会有什么区别?

这还要从对象内部锁的调度说起。

对象内部锁
其实,每个对象都拥有两个池,分别为锁池(EntrySet)和(WaitSet)等待池。

锁池:假如已经有线程A获取到了锁,这时候又有线程B需要获取这把锁(比如需要调用synchronized修饰的方法或者需要执行synchronized修饰的代码块),由于该锁已经被占用,所以线程B只能等待这把锁,这时候线程B将会进入这把锁的锁池。
等待池:假设线程A获取到锁之后,由于一些条件的不满足(例如生产者消费者模式中生产者获取到锁,然后判断队列为满),此时需要调用对象锁的wait方法,那么线程A将放弃这把锁,并进入这把锁的等待池。
如果有其他线程调用了锁的notify方法,则会根据一定的算法从等待池中选取一个线程,将此线程放入锁池。
如果有其他线程调用了锁的notifyAll方法,则会将等待池中所有线程全部放入锁池,并争抢锁。

锁池与等待池的区别:等待池中的线程不能获取锁,而是需要被唤醒进入锁池,才有获取到锁的机会。

你可能感兴趣的:(java并发编程之三:wait/notify/sleep/yield/join)