suspend/resume、wait/notify、park/unpark

前言

“等待/唤醒”机制是java实现线程通信的方式之一,最常见的实现便是wait/notify。但是对于等待/唤醒机制,JDK实际上提供了多种实现方式,但是也废弃了一些,本篇主要讲解三种实现方式的区别、优缺点等。
除了suspend/resumewait/notifypark/unpark这三种,还有一种ReentrantLock结合Condition也可以实现“等待/唤醒”机制,写ReentrantLock的时候会写到。

suspend/resume

java.lang.Thread中定义了这两个方法

@Deprecated
public final void suspend() {
    checkAccess();
    suspend0();
}

@Deprecated
public final void resume() {
    checkAccess();
    resume0();
}

很清楚的看到这两个方法都被废弃了,废弃原因就是容易产生死锁。所以博主演示下这两个方法在什么情况下会产生死锁。

public class SuspendResumeDemo {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("thread suspend");
            // synchronized
            synchronized (SuspendResumeDemo.class) {
                Thread.currentThread().suspend();
            }
            System.out.println("thread finished");
        });

        thread.start();
        // 主线程休眠500毫秒,让thread抢到锁
        Thread.sleep(500);
        System.out.println("thread resume");
        // synchronized
        synchronized (SuspendResumeDemo.class) {
            thread.resume();
        }
    }
}

执行结果

thread suspend
thread resume

第8行suspend()方法执行时,子线程已经抢到了锁,并且该方法不会释放锁,第19行执行resume()前需要获取锁,而此时锁被子线程持有,但是resume()方法不执行,子线程就不能被唤醒,释放锁。这样就形成了死锁,第10行的代码也不会输出。suspend()方法不释放锁是导致死锁的主要原因,还有一种情况便是suspend()resume()执行顺序也可能导致线程永远被阻塞。

public class SuspendResumeDemo {

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            // thread休眠500毫秒,让主线程先执行resume()
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("thread suspend");
            Thread.currentThread().suspend();
            System.out.println("thread finished");
        });

        thread.start();

        System.out.println("thread resume");
        thread.resume();
    }
}

执行结果

thread resume
thread suspend

主线程先执行resume(),然后子线程再执行suspend(),之后便在没有线程唤醒子线程,所以子线程会被永久阻塞。
正是基于以上两点所以suspend/resume已经被废弃了。

wait/notify

wait/notify机制可以说是我们最熟悉的线程协作机制的。定义在java.lang.Object

// 阻塞当前线程,直到另一个线程调用该对象的notify()方法或者notifyAll()方法
public final void wait() throws InterruptedException {}
// 随机唤醒一个正在等待该对象的锁的线程
public final native void notify();
// 所有正在等待该对象的锁的线程
public final native void notifyAll();

其使用方式也比较简单,先来看下wait/notify的正常使用

public class WaitNotifyDemo {

    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            synchronized (lock) {
                try {
                    System.out.println("sub-thread get lock");
                    lock.wait();
                    System.out.println("sub-thread finished");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread.start();
        // 主线程休眠500毫秒,让子线程获取锁
        Thread.sleep(500);
        synchronized (lock) {
            System.out.println("main-thread notify sub-thread");
            lock.notify();
        }
    }
}

执行结果

sub-thread get lock
main-thread notify sub-thread
sub-thread finished

从输出结果来看,子线程获取锁后调用wait()方法处于阻塞状态,该方法会释放锁。所以主线程执行到第21行的时候可以获取到锁,唤醒子线程。主线程执行同步代码块完成后释放锁,此时子线程才能重新获取锁,继续执行。
wait/notify机制如果执行顺序不对,也会出现像suspend/resume一样的问题,导致线程永远被阻塞,将上述的代码稍微修改一下就可

public class WaitNotifyDemo {

    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            // 子线程休眠500毫秒,让主线程先获取锁
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock) {
                try {
                    System.out.println("sub-thread get lock");
                    lock.wait();
                    System.out.println("sub-thread finished");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        thread.start();
        synchronized (lock) {
            System.out.println("main-thread notify sub-thread");
            lock.notify();
        }
    }
}

执行结果

main-thread notify sub-thread
sub-thread get lock

根据执行结果可以知道,主线程先获取了锁,执行了notify()方法,之后子线程获取了锁,执行wait()方法,由于后续再也没有线程来唤醒子线程,所以子线程会一直被阻塞,第17行的代码也不会执行。
可以看到,wait/notify机制比suspend/resume机制有了一些改进,但是仍然存在着一些问题。

park/unpark

接下来看看park/unpark机制,park()unpark()都定义在java.util.concurrent.locks.LockSupport类中

/**
 * Disables the current thread for thread scheduling purposes unless the permit is available.
 * 禁止当前线程的调度,除非许可(permit)是可用的
 */
public static void park() {
    UNSAFE.park(false, 0L);
}

/**
 * Makes available the permit for the given thread, if it
 * was not already available.  If the thread was blocked on
 * {@code park} then it will unblock.  Otherwise, its next call
 * to {@code park} is guaranteed not to block. This operation
 * is not guaranteed to have any effect at all if the given
 * thread has not been started.
 */
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

通俗点说就是park/unpark具有以下特点:

  • park()阻塞线程,unpark()取消阻塞
  • park()unpark()不要求保持一定的顺序,即可以先执行unpark(),再执行park()
  • 多次调用unpark(),效果不会叠加。即多次调用unpark()后,执行一次park()不会阻塞,之后再执行park()还是会阻塞

以下是park/unpark代码示例

public class ParkUnparkDemo {

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                // 子线程休眠500毫秒,让主线程先执行unpark()
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("sub-thread park");
            LockSupport.park();
            System.out.println("sub-thread finished");
        });

        thread.start();

        System.out.println("main-thread unpark sub-thread");
        LockSupport.unpark(thread);
    }
}

执行结果

main-thread unpark sub-thread
sub-thread park
sub-thread finished

根据输出结果可以看出,主线程先执行了unpark(),子线程再执行park()后,子线程依然可以被唤醒。

park/unpark解决了顺序问题,并不代表它是完美的,它有着和suspend/resume同样的缺点:线程阻塞时,不会释放锁。具体示例代码如下

public class ParkUnparkDemo {

    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            synchronized (lock) {
                System.out.println("sub-thread park");
                LockSupport.park();
            }
            System.out.println("sub-thread finished");
        });

        thread.start();
        // 主线程休眠500毫秒,让子线程获取锁
        Thread.sleep(500);
        synchronized (lock) {
            System.out.println("main-thread unpark sub-thread");
            LockSupport.unpark(thread);
        }
    }
}

执行结果

sub-thread park

子线程获取锁,执行park()后被阻塞,但是并不会释放锁。所以主线程无法获取锁,不能执行unpark()方法唤醒子线程,导致子线程和主线程都处于阻塞状态。

总结

等待/唤醒机制 被阻塞时释放锁 需要考虑等待/唤醒顺序 由哪个类实现
suspend/resume java.lang.Thread
wait/notify java.lang.Object
park/unpark java.util.concurrent.locks.LockSupport

以上便是三种等待/唤醒机制的的使用方式及区别,第一种已经完全被弃用了,了解一下即可。第二种和第三种需要大家理解其区别后,在适当的场景选取适当是方式。

发布了52 篇原创文章 · 获赞 107 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Baisitao_/article/details/102218626