在之前的文章中我们讲到过Java SDK并发包的lock有别于synchronized的隐式锁的三个特性:能够响应中断、支持超时和非阻塞的获取锁。那么今天我们再来详细聊聊Java SDK并发包里的condition。Condition实现了管程模型里面的条件变量。
在之前我们提到过,Java语言内置的管程里只有一个条件变量,而lock &condition实现的管程是支持多个条件变量的,这是二者的一个重要区别。
在很多并发场景下,支持多个条件变量能够让我们的并发程序可读性更好,实现起来也更容易。例如实现一个阻塞队列就需要两个条件变量。
一个阻塞队列需要两个条件变量,一个是队列不空,空队列不允许出队,另一个是队列不满,队列已满不允许入队。这个例子我们在前面介绍完成的时候详细说过,这里就不再赘述。
不过这里你需要注意lock和condition实现的管程,线程等待和通知需要调用await single singleAll,他们的语义和wait notify notifyAll是相同的,但不一样的是lock & condition实现的管程里,只能使用前面的await single singleAll,而后面的wait notify notifyAll只有在synchronized实现的管程里才能使用。如果一不小心在lock & condition实现的管程里调用了wait notify notifyAll,那程序可就彻底玩完了。
Java SDK并发包里的lock和condition不过是管程的一种实现,管程你已经很熟悉了,那lock 和 condition的使用自然是小菜一碟,下面我们就来看看在知名项目Dubbo中,Lock和condition是怎么使用的?在不过在开始介绍源码之前,我还要先介绍两个概念:同步和异步。
我们平时写的代码基本都是同步的,但最近几年异步编程大火。那同步和异步的区别到底是什么呢?通俗点来讲,就是调用方是否需要等待结果,如果需要等待结果,那就就是同步,如果不需要等待结果就是异步。
比如在下面的代码里,有一个计算圆周率小数点后100万位的方法pai1M(),这个方法可能需要执行俩礼拜,如果调用之后,线程一直等待着计算结果,等俩礼拜之后返回,就可以执行printf(" hello world")了,这个属于同步;如果调用线程不需要等待计算结果,可就立刻可以执行printf(" hello world"),这个就属于异步。
//计算圆周率小数点后100万位
String pai1M(){
//省略代码无数
}
pai1M()
printf("hello woeld")
同步是Java代码默认的处理方式。如果你想让你的线程程序支持异步,可以通过下面两种方式来实现:
调用方创建一个子线程,在子线程里执行方法调用,这种调用我们称为异步调用;
方法实现的时候,创建一个新的线程执行主要逻辑,主线程直接return,这种方法我们一般称为异步方法。
其实在编程领域,异步的场景还是挺多的,比如TCP协议本身就是异步的,我们工作中经常用到的RPC调用,在TCP协议层面,发送完RPC请求后,线程是不会等待RPC的响应结果的,可能你会觉得奇怪,平时工作中的RPC调用大多数都是同步的,这是怎么回事呢?
其实很简单,一定是有人帮你做了异步转同步的事情,例如目前知名的RPC框架Dubbo就帮我们做了异步转同步的事情,那它是怎么做的呢?下面我们就来分析一下Dubbo的相关源码。
对于下面一个简单的RPC调用默认的方法下,sayHello方法是个同步方法,也就是说执行service.sayHello(“Dubbo”)的时候,线程会停下来等待结果。
DemoService service = 初始化部分省略
String message = service.sayHello("dubbo");
System.out.println(message);
如果此时你将调用线程dump出来的话,会是下图这个样子,你会发现调用线程阻塞了,调用线程的线程的状态是time_waiting,本来发送请求是异步的,但是调用线程却阻塞了。说明说明Dubbo帮我们做了异步转同步的事情,通过调用栈你可看到线程是阻塞在DefaultFuture.get()方法上可以推断:Dubbo异步转同步的功能应该是通过DefaultFuture这个类实现的。
不过为了理清前后关系,还是有必要分析一下DefaultFuture.get()之前发生了什么。DubboInvoker的第108行调用了DefaultFuture.get(),这一行很关键,我稍微修改了一下列在了下面。这一行先调用了request(inv,timeout),方法,这个方法其实就是发送RPC请求之后,通过调用get方法等待RPC返回结果。
public class DubboInvoker {
Result doInvoke(Invocation inv){
//下面这行就是源码中的108行
//为了方便展示做了修改
return currentClient.request(inv,timeout).get();
}
}
DefaultFuture这个类是很关键的,我把相关的代码精简之后列到了下面。不过在看代码之前,还是有必要重复一下我们的需求:当RPC返回结果之前,阻塞调用线程,让调用线程等待;当RPC返回结果后,唤醒调用线程,让调用线程重新执行。不知道你有没有似曾相识的感觉,这不是经典的等待-通知机制吗?这时候想必你的脑海里应该能浮现出缓存的解决方案了。有了自己的方案之后,我们再来看看Dubbo是怎么实现的。
//创建锁与条件变量
private final Lock lock = new ReentrantLock();
private final Cnodition done = new ReentrantLock();
//调用方通过该方法等待结果
Object get(int timeout) {
long start = System.nanoTime();
lock.lock;
try {
while(!isDone()) {
done.await(timeout);
long cur = System.nanoTime();
if(isDone() || cur - start > timeout){
break;
}
}
} finally {
lock.unlock();
}
if(!isDone()) {
throw TimeoutException();
}
return returnFromResponse();
}
//RPC结果是否返回
boolean isDone() {
return respoon != null;
}
//RPC结果返回时调用该方法
private void doReceived(Response res){
lock.lock();
try{
response = res;
if(!isDone()){
done.singal();
}
} finally {
lock.unlock();
}
}
调用线程通过调用get方法等待RPC返回结果,这个方法里你可以看到都是熟悉的面孔:调用lock获取锁,在finally里面调用unlock释放锁,获取锁后通过经典的在循环中调用await()方法来实现等待。
当RPC结果返回时,会调用doReceive的方法,在这个方法里调用lock获取锁,在finally里面调用unlock释放锁,获取锁后通过调用single来通知调用线程结果已经返回,不用继续等待。
至此,Dubbo里面的异步转同步的源码就分析完了,有没有觉得还挺简单的?最近这几年工作中需要的异步处理越来越多,其中有一个主要原因就是这些API本身就是异步API。例如websocket也是一个异步的通信协议,如果基于这个协议实现一个简单的RPC,你也会遇到异步转同步的问题。现在有很多公有云的API本身也是异步的,例如创建云主机就是一个。异步的API虽然调用成功了,但是云主机并没有创建成功,你需要调用另外一个API去轮询主机的状态。如果你需要在项目内部封装创建云主机的API,你也会面临异步转同步的问题,因为同步的API更易用。
Lock & condition是管程的一种实现,所以能否用好lock和condition,要看你对管程模型理解怎么样。管程的技术前面我们已经专门写了一篇文章做介绍,你可以结合着来学,理论联系实践也更有助于加深理解。