线程协作之 sleep/yield/join


线程协作之使用 wait/notify/notifyAll中我们讲解了 wait 和 notify 方法的使用和原理,现在我们再来看另外一组线程间协作的方法 sleep/yield/join。他们最明显区别是:这几个方法都位于 Thread 类中,而 wait/notify/notifyAll 三个方法都位于 Object 类中。现在我们逐个分析 sleep/yield/join 方法

sleep

sleep 方法的作用是让当前线程暂停指定的时间(毫秒),sleep 方法是最简单的方法,在前面的例子中也经常用到,比较容易理解。唯一需要注意的是其与 wait 方法的区别。

  • wait 方法依赖于同步,而 sleep 方法可以直接调用。
  • 更深层次的区别在于 sleep 方法只是暂时让出 CPU 的执行权,并不释放锁。而 wait 方法则需要释放锁。
public class SleepWaitTest {

    public synchronized void testSleep() {
        System.out.println(Thread.currentThread().getName() + " start!");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " end!");
    }

    public synchronized void testWait() {
        System.out.println(Thread.currentThread().getName() + " start!");
        try {
            wait(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " end!");
    }

    public static void main(String[] args) throws InterruptedException {
        SleepWaitTest sleepWaitTest = new SleepWaitTest();
        System.out.println("--- test sleep ---");
        Stream.generate(() -> new Thread(() -> {
            sleepWaitTest.testSleep();
        })).limit(3).collect(Collectors.toList()).forEach(Thread::start);

        Thread.sleep(5000);

        System.out.println("--- test wait ---");
        Stream.generate(() -> new Thread(() -> {
            sleepWaitTest.testWait();
        })).limit(3).collect(Collectors.toList()).forEach(Thread::start);

    }

}

输出如下,显然 sleep 的时候依然持有锁,因此其它线程无法进入到方法,只要一个线程执行完释放锁之后其它线程才能进入方法;而当调用 wait 方法后,当前线程会释放持有的 monitor 对象锁,因此,其他线程还可以进入到同步方法,线程被唤醒后,需要竞争锁,获取到锁之后再继续执行。

--- test sleep ---
Thread-0 start!
Thread-0 end!
Thread-2 start!
Thread-2 end!
Thread-1 start!
Thread-1 end!
--- test wait ---
Thread-3 start!
Thread-5 start!
Thread-4 start!
Thread-5 end!
Thread-4 end!
Thread-3 end!

yield

yield 方法的作用是暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止。yield 方法只是将 Running 状态转变为 Runnable 状态。我们还是通过一个例子来演示其使用:

public class YieldTest {

    public static void main(String[] args) {
        Stream.generate(() -> new Thread(() -> {
            Stream.iterate(1, n -> n +1).limit(5).forEach(i -> {
                try { Thread.sleep(500); } catch (InterruptedException e) {} // 减缓一下打印的速度便于观察
                System.out.println(Thread.currentThread().getName() + ": " + i);
                Thread.yield(); // 把 cpu 让给其它线程
            });
        })).limit(2).collect(Collectors.toList()).forEach(Thread::start);
    }

}


/**
 * A hint to the scheduler that the current thread is willing to yield
 * its current use of a processor. The scheduler is free to ignore this
 * hint.
 *
 * <p> Yield is a heuristic attempt to improve relative progression
 * between threads that would otherwise over-utilise a CPU. Its use
 * should be combined with detailed profiling and benchmarking to
 * ensure that it actually has the desired effect.
 *
 * <p> It is rarely appropriate to use this method. It may be useful
 * for debugging or testing purposes, where it may help to reproduce
 * bugs due to race conditions. It may also be useful when designing
 * concurrency control constructs such as the ones in the
 * {@link java.util.concurrent.locks} package.
 */
public static native void yield();

输出如下,我们通过 yield 方法来实现两个线程的交替执行。不过请注意,这种交替并不一定能得到保证,上面源码的注释中也对这个问题进行说明。

Thread-0: 1
Thread-1: 1
Thread-1: 2
Thread-0: 2
Thread-0: 3
Thread-1: 3
Thread-0: 4
Thread-1: 4
Thread-0: 5
Thread-1: 5

yield 仅做了解即可,由于其效果不好预测,生产环境很少使用。

  • 调度器可能会忽略该方法。
  • 使用的时候要仔细分析和测试,确保能达到预期的效果。
  • 很少有场景要用到该方法,主要使用的地方是调试和测试。

join

join 方法的作用是父线程等待子线程执行完成后再执行,换句话说就是将异步执行的线程合并为同步的线程。JDK 中提供三个版本的 join 方法,其实现与 wait 方法类似,join() 方法实际上执行的 join(0),而 join(long millis, int nanos) 也与 wait(long millis, int nanos) 的实现方式一致,暂时对纳秒的支持也是不完整的。我们可以看下 join 方法的源码,这样更容易理解

    /**
     * Waits at most {@code millis} milliseconds for this thread to
     * die. A timeout of {@code 0} means to wait forever.
     *
     * <p> This implementation uses a loop of {@code this.wait} calls
     * conditioned on {@code this.isAlive}. As a thread terminates the
     * {@code this.notifyAll} method is invoked. It is recommended that
     * applications not use {@code wait}, {@code notify}, or
     * {@code notifyAll} on {@code Thread} instances.
     *
     * @param  millis
     *         the time to wait in milliseconds
     *
     * @throws  IllegalArgumentException
     *          if the value of {@code millis} is negative
     *
     * @throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          <i>interrupted status</i> of the current thread is
     *          cleared when this exception is thrown.
     */
    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;
            }
        }
    }

    /**
     * Waits at most {@code millis} milliseconds plus
     * {@code nanos} nanoseconds for this thread to die.
     * If both arguments are {@code 0}, it means to wait forever.
     *
     * <p> This implementation uses a loop of {@code this.wait} calls
     * conditioned on {@code this.isAlive}. As a thread terminates the
     * {@code this.notifyAll} method is invoked. It is recommended that
     * applications not use {@code wait}, {@code notify}, or
     * {@code notifyAll} on {@code Thread} instances.
     *
     * @param  millis
     *         the time to wait in milliseconds
     *
     * @param  nanos
     *         {@code 0-999999} additional nanoseconds to wait
     *
     * @throws  IllegalArgumentException
     *          if the value of {@code millis} is negative, or the value
     *          of {@code nanos} is not in the range {@code 0-999999}
     *
     * @throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          <i>interrupted status</i> of the current thread is
     *          cleared when this exception is thrown.
     */
    public final synchronized void join(long millis, int nanos)
    throws InterruptedException {

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

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
            millis++;
        }

        join(millis);
    }

    /**
     * Waits for this thread to die.
     *
     * <p> An invocation of this method behaves in exactly the same
     * way as the invocation
     *
     * <blockquote>
     * {@linkplain #join(long) join}{@code (0)}
     * </blockquote>
     *
     * @throws  InterruptedException
     *          if any thread has interrupted the current thread. The
     *          <i>interrupted status</i> of the current thread is
     *          cleared when this exception is thrown.
     */
    public final void join() throws InterruptedException {
        join(0);
    }

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

public class SleepWaitTest {

    public static void main(String[] args) throws InterruptedException {
        SleepWaitTest sleepWaitTest = new SleepWaitTest();
        System.out.println("--- test join ---");
        Stream.generate(() -> new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " start!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " end!");
        })).limit(3).collect(Collectors.toList()).forEach(thread -> {
            thread.start();
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }

}

输出如下,可以看到我们在主线程里创建了三个子线程然后分别启动,并且线程启动后直接调用了 join。由上面的源码我们知道对子线程调用 join 会导致调用线程在子线程没完成的情况下进入到 wait 状态,直到子线程执行结束自动唤醒调用线程继续执行。所以这段代码三个子线程会顺序执行。

join 方法在多线程使用很多,总之只要记住一句话即可:join 表示等待此线程执行结束!Waits for this thread to die. 另外以下几个特性也很重要需要注意

  • When we invoke the join() method on a thread, the calling thread goes into a waiting state. It remains in a waiting state until the referenced thread terminates.

  • The join() method may also return if the referenced thread was interrupted.

  • if the referenced thread was already terminated or hasn't been started, the call to join() method returns immediately.

  • when a thread t1 calls t2.join(), then all changes done by t2 are visible in t1 on return

--- test join ---
Thread-0 start!
Thread-0 end!
Thread-1 start!
Thread-1 end!
Thread-2 start!
Thread-2 end!

参考

Copyright © jverson.com 2019 all right reserved,powered by Gitbook 11:01

results matching ""

    No results matching ""