前言
“等待/唤醒”机制是java实现线程通信的方式之一,最常见的实现便是wait/notify
。但是对于等待/唤醒机制,JDK实际上提供了多种实现方式,但是也废弃了一些,本篇主要讲解三种实现方式的区别、优缺点等。
除了suspend/resume
、wait/notify
、park/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()
方法执行时,子线程已经抢到了锁,并且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 |
以上便是三种等待/唤醒机制的的使用方式及区别,第一种已经完全被弃用了,了解一下即可。第二种和第三种需要大家理解其区别后,在适当的场景选取适当是方式。