wait/notify实现线程通信
实现线程通信呢,比较传统的办法就是使用synchronized关键字获取对象锁之后,结合Object自带的wait/notify方法来实现。
一个简单例子如下:
public static void main(String[] args) throws InterruptedException {
ObjectTar objectTar = new ObjectTar();
new WaitThread("WaitThread", objectTar).start();
Thread.sleep(1000);
new NotifyThread("NotifyThread", objectTar).start();
}
@Data
private static class ObjectTar {
private String name;
}
private static class WaitThread extends Thread {
final ObjectTar objectTar;
WaitThread(String name, ObjectTar tar) {
super(name);
this.objectTar = tar;
}
@Override
public void run() {
synchronized (objectTar) {
System.out.println(getName() + "获取锁,准备执行自己的业务逻辑");
if (StringUtils.isEmpty(objectTar.getName())) {
System.out.println(getName() + "发现目前的状况不能满足自己的要求,于是执行objectTar.wait()");
try {
objectTar.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(getName() + "从objectTar.wait()返回,完成自己的业务逻辑," + objectTar.getName());
}
}
}
private static class NotifyThread extends Thread {
final ObjectTar objectTar;
NotifyThread(String name, ObjectTar tar) {
super(name);
this.objectTar = tar;
}
@Override
public void run() {
synchronized (objectTar) {
System.out.println(getName() + "获取锁,完成了objectTar的name初始化");
objectTar.setName("黄焖鸡");
objectTar.notifyAll();
System.out.println(getName() + "执行完notify之后,继续执行自己的业务逻辑");
}
}
}
打印
WaitThread获取锁,准备执行自己的业务逻辑
WaitThread发现目前的状况不能满足自己的要求,于是执行objectTar.wait()
NotifyThread获取锁,完成了objectTar的name初始化
NotifyThread执行完notify之后,继续执行自己的业务逻辑
WaitThread从objectTar.wait()返回,完成自己的业务逻辑,黄焖鸡
wait和notify的通知实现机制如下图,因为这不是本文猪脚,所以就不展开说了:)
通过上边的示例呢,可以发现使用wait/notify来实现通信机制有三个比较明显的缺点:
- wait/notify都是Object中的方法,使用这两个方法之前必须是先获取到了这个对象的锁,这也就限制了其适用场合,那就是必须在同步代码块中。
- notify是随机唤醒一个线程,notifyAll是唤醒所有线程,没有办法去指定一个线程唤醒。
- 如果notify执行在前,wait执行在后,进入wait的判断逻辑如果有问题,那么线程可能会一直在阻塞状态
使用LockSupport实现线程通信
一个简单的示例实现wait/notify相同的效果
public static void main(String[] args) throws InterruptedException {
ObjectTar objectTar = new ObjectTar();
ParkThread parkThread = new ParkThread("ParkThread", objectTar);
parkThread.start();
Thread.sleep(1000);
//注意这里传入了parkThread
new UnParkThread("UnParkThread", objectTar, parkThread).start();
}
@Data
private static class ObjectTar {
private String name;
}
private static class ParkThread extends Thread {
final ObjectTar objectTar;
ParkThread(String name, ObjectTar tar) {
super(name);
this.objectTar = tar;
}
@Override
public void run() {
System.out.println(getName() + "获取锁,准备执行自己的业务逻辑");
if (StringUtils.isEmpty(objectTar.getName())) {
System.out.println(getName() + "发现目前的状况不能满足自己的要求,于是执行LockSupport.park()");
LockSupport.park();
}
System.out.println(getName() + "从LockSupport.park()返回,完成自己的业务逻辑," + objectTar.getName());
}
}
private static class UnParkThread extends Thread {
final ObjectTar objectTar;
Thread parkThread;
UnParkThread(String name, ObjectTar tar, Thread parkThread) {
super(name);
this.objectTar = tar;
this.parkThread = parkThread;
}
@Override
public void run() {
System.out.println(getName() + "获取锁,完成了objectTar的name初始化");
objectTar.setName("黄焖鸡");
//注意unpark传入了一个线程对象
LockSupport.unpark(parkThread);
System.out.println(getName() + "执行完LockSupport.unpark()之后,继续执行自己的业务逻辑");
}
}
打印
1:ParkThread获取锁,准备执行自己的业务逻辑
2:ParkThread发现目前的状况不能满足自己的要求,于是执行LockSupport.park()
3:UnParkThread获取锁,完成了objectTar的name初始化
4:ParkThread从LockSupport.park()返回,完成自己的业务逻辑,黄焖鸡
5:UnParkThread执行完LockSupport.unpark()之后,继续执行自己的业务逻辑
注意4、5的顺序是不一定的,这点和wait/notify是不一样的。
知识点
- LockSupport.park(),park是有停车的意思,那么这个方法就是相当于线程“停车”了,那就是“阻塞”了呗
- LockSupport.unpark(Thread thread),unpark那就是“不停车”了,滚蛋吧,参数有个线程,可以指定具体的线程从“阻塞”中释放出来
和wait/notify的区别
从上边的示例中呢,我们可以看出来下边的区别了:
- LockSupport实现线程之间的通信,不需要借助Object来达到目的
- LockSupport可以实现对指定线程从“阻塞”状态中释放
- 还有一点其实例子中没有表现出来,那就是LockSupport不需要担心阻塞和唤醒的操作顺序。什么意思呢,就是说我们代码中如果不是先执行park来阻塞线程,然后等另一个线程unpark来唤醒这个阻塞线程,而是先unpark,而后执行park,那么park是不会阻塞的,会接着执行后续业务逻辑;这点和wait/notify是不一样的,wait/notify实现线程的阻塞唤醒是有严格顺序要求的,必须先wait,而后才能notify。如果先notify,那么另一个线程wait之后可能永久阻塞住【当然这种情况在进入wait处业务逻辑判断没问题的时候是不会出现的】。
原理浅析
park和unpark都是native方法,底层都是C实现的。这里就简单来说下:
- 每个线程有一个Park实例,里边有个字段 “volatile int _counter”,它等于0的时候表示没有获取“凭证”,线程要阻塞。他等于1的时候,表示线程获取的“凭证”,也就是相当传统意义上的获取了锁标记,也就可以执行接下来的业务逻辑。
- park方法每执行一遍,都会消费一个“凭证”,相当于“_counter- - ”
- unpark方法如果连续执行,且期间没有park来消费“凭证”的话,只会生成一个凭证。也就是说不管unpark执行多少遍,每个线程都只有一个“凭证”。
总结
LockSupport是JDK中用来实现线程阻塞和唤醒的通信工具。相比于wait/notify,它更加灵活,不依赖对象的同时,可以指定任何线程进行唤醒,并且不担心阻塞和唤醒的操作顺序,但是需要注意,连续的多次唤醒的效果和一次唤醒是一样的。
JDK并发包下的锁和其他同步工具的底层中大量使用了LockSupport来实现线程之间的通信,掌握它的原理和用法对我们更好的理解这些JUC底层实现是有帮助的。