看了同步阻塞《你知道同步阻塞是什么吗》,我当然知道。“那你怎么看呢”,这个。..在同步阻塞的世界里,代码执行到哪里,数据就跟到哪里。如果数据太慢跟不上,代码就停在那里等待数据到达,然后用数据执行。可以说代码执行和数据如影随形,形影不离。执子之手,与子白头偕老。这很感人。如果不太理解,可以认为代码执行其实就是一些行为动作,而这些行为动作的目的是获取/操作数据。比如加法,这里的动作是做加法,数据就是加数和被加数。运算的结果是另一个数据,两个数的和。只是在这个加法中,数据跑的很快(CPU寄存器,能不能快点),我们几乎察觉不到等待数据执行action的过程。怎么办,再看一个能把他们拉开的例子。自然是数据库查询。既有网络I/O,也有磁盘I/O,肯定会慢一些。假设我的业务是这样的,代码先去数据库查询一个用户,然后修改用户的密码,再更新回数据库,最后代码成功返回。如果互联网速度和数据库很慢,可能会出现这种情况。代码执行一个查询数据库的动作,然后等啊等啊等。等待的时间没有了,最后数据库返回了用户。然后,代码快速更改密码,执行更新数据库的操作,然后再次等待。等待,等待,等待的花朵再次绽放,数据库终于有了答复,更新成功。然后代码返回成功,全部执行。所以,同步阻塞代码最大的特点就是取数据在路上,数据不到位就阻塞。最后一个小升华:所谓同步,就是快的等待慢的,然后一起前进,表达了目的。所谓堵,就是想办法拦住快的,等慢的来,意思就是手段。总之,同步是目的,阻塞是手段。看过异步非阻塞《你知道什么是异步非阻塞吗》,当然知道,但是不知道怎么读。“哦,恭喜你,你会很快回答的……”。我们生活在一个异步的世界里,但我们是最不异步的人。你去饭店吃饭,服务员给你写好菜单,交给厨房给别人上菜。厨房准备好后,会按铃通知服务员,服务员会把饭菜送到你的地方。服务员是主(或I/O)线程,任务交给厨房的工作线程去执行。厨房接到任务的时候,也要记住送任务的服务员。然后厨房执行任务,服务员也去执行其他任务。就是这样。厨房完成任务后,届时会通知服务员。服务员收到通知后,会进行下一步的内容,比如将餐点送到客人的餐桌上。这是一个很常见的异步场景。既然一方不愿意一直等着(或者关注)对方,又不知道对方什么时候做完,所以只能希望对方做完了告诉自己,然后自己进行后续工作。这就是我们常说的异步回调(或通知)。早上项目经理开完会,就给大家分配了任务,把测试用例的代码给大家,说谁做完谁跑测试用例,就可以了。然后会议休会,各去各忙各的。下午5:00完成,开始运行测试用例,幸运的是一次全部通过。就算你的任务完成了,那你就可以做你想做的事了,比如看《编程新谈》公众号。项目经理是主(或I/O)线程,它给每个开发人员的这些工作线程分配任务,给每个人一段逻辑代码,告诉他们在他们的任务完成后执行这段逻辑代码。开发人员完成任务后,再执行逻辑代码。逻辑代码执行完,就认为结束了。不再需要告诉项目经理。这也是一种常见的异步场景。一方给另一方安排好任务后,给对方一段逻辑代码,然后就分道扬镳了。以后的日子,你走你的阳关路,我过我的独木桥。这段逻辑代码一般是通过一个Runnable接口传入,任务完成后执行,暂且称其为“执行完成”。所以异步非阻塞代码最大的特点就是我给你分配任务,你完成了再回复我,我们不会互相耽误。最后一个小升华:所谓异步,就是你走你的路,我走我的路,大家各自往前走,代表了一种事实形式。所谓无阻塞,就是走得快,走得慢,不为你停留片刻,代表一种直观的现象。总之,异步是一种形式,非阻塞是一种现象。异步非阻塞本身并没有什么明显的显着特点。请注意,我说的是“本身”。因为我们整个世界都在异步非阻塞模式下运行。上厕所玩手机,等车玩手机,上班玩手机,等晚饭玩手机,回家玩手机,边玩手机边睡觉做梦。第二天也是一样。哈哈。当一个人没有被阻止时。不可否认,我们所处的社会是非常复杂的,主要是因为人与人之间的交流、沟通和协调有时并不是一件容易的事情。同理,异步非阻塞“本身”并不难,难的是如何实现。毕竟,一群听不懂人类语言的二手线,要相互沟通协调,可不是一件容易的事。我认为所谓的响应性的响应性就是外部世界发生了变化,你必须做出响应。因此,反应式编程是围绕变化而构建的。如何收集原来的变化,如何将这个变化通知相关的处理器,处理器如何响应,反应的过程实际上触发了一个新的变化,这个新的变化应该如何收集,以及如何通知下一个处理器,以及依此类推,直到一切结束。可以说,整个自然界都是有反应的,因为它们都对外界的变化或自身的变化作出反应。先说人类,冷了就加衣服,饿了就吃饭,生病了就去医院。看到绿色让人放松,看到蓝色让人平静,看到红色让人兴奋。除了动植物,向日葵绕太阳转称为趋光性,植物的根向水分多的地方生长称为趋水性。鸽子可以利用磁场来辨别方向,鲸鱼和海归可以利用磁场来记住它们的路径。所以响应式“本身”是一个很简单的模型,你给我找零,我做出响应。动植物都有一套完整的感觉器官,可以感知外界的变化。同时,他们拥有超高的智商或一整套能够应对这种变化的生物系统。这是几万年甚至几千万年进化的结果,是由基因决定的,所以看起来很自然。再来看看编程世界的响应式,也是这两个问题。一是如何知道外界的变化,二是如何应对这种变化。代码是没有生命的,只能简单粗暴。怎么知道变化,那就让别人告诉你吧。如何反应,然后执行一段逻辑代码。别人告诉你等于异步回调/通知。执行的逻辑代码可以是外界传入的,也可以是自己的方法。现在明白了,异步非阻塞就是响应式的。最后,一个小小的升华:所谓响应式是一个概念,或者说是一种编程模型,它不是一种知识,也不是一种技术。但是它需要用到一个技术,那就是实现异步非阻塞技术。我觉得Reactor在传统编码中把逻辑处理代码写成方法,需要的数据通过方法参数传入,处理后的数据通过方法的返回值返回。执行时以main方法为入口,这些方法按照一定的顺序执行,数据依次从各个方法流入流出。当所有的方法都执行完了,数据也处理完了,就结束了。整个过程基于逻辑代码的执行。数据只是必要的参与者,因为代码需要处理数据。如果数据没有到位,代码会停止不执行,等待数据的到来。这是一个典型的同步阻塞执行过程,非常简单,容易理解,代码也很容易写。至此,我们讲到的都是响应性的理论,那么如何实现呢,我还真是一时半会儿没有头绪。Reactive是异步非阻塞的,应该是相对于同步阻塞而言的。那我们不妨利用同步阻塞的响应性,看看能得到哪些有价值的发现。响应性着重于变化和反应两点,变化在前,反应在后。同步阻塞也关注两点,执行逻辑和数据,执行逻辑在前,数据在后。然后开始建立通信。因为“反应”是一系列的行为动作,应该对应“执行逻辑”。“变化”只能对应“数据”。事实上,这是正确的。从“数据”到可用性的变化本身就是一种“变化”。这种对应关系是完美成立的,但逻辑顺序却完全冲突。响应式是变化主导的响应,这个很容易理解。我没有任何变化,你也不需要回应。同步阻塞以执行逻辑和数据为主,这个也很好理解。我的代码还没有执行,根本不需要数据。可以看出他们的对应关系是完美的,但是主导顺序却完全相反,这是一个非常非常有价值的发现。因为我们只需要将同步阻塞逆向即可,这是实现响应式的大方向。这种推理似乎是正确的,但实际上是否如此呢?好吧,是的。现在请和我一起扭转你的想法。本来以逻辑代码执行为主线,数据为参与者。现在数据是主线程,逻辑代码执行是参与者。说白了,原来是数据传入逻辑代码,现在逻辑代码传入数据。可能有人会问,逻辑代码怎么传?哈哈,Lambda表达式,函数式编程。想象一根长长的水管,水从中流过。如果你想把水变成橙色,你只要在管道上开一个洞,加上一个不断注入橙色染料的装置,流过它的水就会变成橙色。如果你想把橙水变甜,只要在后面的管道上开一个洞,加上一个会不断加入白糖的装置,让流过它的水变甜。同理,可以在后面继续安装加柠檬酸的装置使水变酸,在后面继续安装压二氧化碳的装置使水起泡。最后发现,自来水经过多道工序加工成了芬达。如果把水流看成是数据流,把投递器看成是逻辑码,就变成数据先流入第一个逻辑码,处理后流入第二个逻辑码,再往下流到最后。这就是数据为主线,逻辑代码只是参与者,这也是Reactor实现响应式编程的原则。Spring官方使用的响应式类库是Reactor。其中,Reactor库已经实现了“以数据为主线”和“变化时通知处理器”这两个功能,而我们要做的是“对变化做出反应”,即插入逻辑代码。Reactor入门在Reactor中,有两个非常重要的类,Mono和Flux,它们都是数据源,实现了“以数据为主线”和“变化时通知处理器”这两个功能.函数,同时也为我们提供了插入“响应变化”逻辑代码的方法。Mono代表0个或1个数据,Flux代表0个或多个数据。从一个简单的Mono开始。设计一个简单的例子,先创建一个数据源,里面只包含一个数据10,第一次处理是加1,第二次处理是奇偶校验过滤,第三次处理是消费这条数据,然后就结束了。为了清楚的看到哪些代码是主线程执行的,哪些代码是工作线程执行的,特意打印了很多信息。publicstaticvoidmain(String[]args){displayCurrTime(1);displayCurrThreadId(1);//创建一个数据源Mono.just(10)//延迟5秒再传输数据.delayElement(Duration.ofSeconds(5))//对数据进行转换.map(n->{displayCurrTime(2);displayCurrThreadId(2);displayValue(n);delaySeconds(2);returnn+1;})//对数据进行过滤。filter(n->{displayCurrTime(3);displayCurrThreadId(3);displayValue(n);delaySeconds(3);returnn%2==0;})//如果数据没有了,使用默认值.defaultIfEmpty(9)//订阅一个消费者消费data.subscribe(n->{displayCurrTime(4);displayCurrThreadId(4);displayValue(n);delaySeconds(2);System.out.println(n+"consumed,workerThreadover,exit.");});displayCurrTime(5);di??splayCurrThreadId(5);pause();}//显示当前时间staticvoiddisplayCurrTime(intpoint){System.out.println(point+":"+LocalTime.now()));}//显示当前线程IdstaticvoiddisplayCurrThreadId(intpoint){System.out.println(point+":"+Thread.currentThread().getId());}//显示当前值staticvoiddisplayValue(intn){System.out.println("input:"+n);}//延时几秒staticvoiddelaySeconds(intseconds){try{TimeUnit.SECONDS.sleep(seconds);}catch(InterruptedExceptione){e.printStackTrace();}}//主线程暂停staticvoidpause(){try{System.out.println("mainThreadover,暂停。”);System.in.read();}catch(IOExceptione){e.printStackTrace();}}输出如下:1:15:00:39.8091:15:15:00:40.1585:1mainThreadover,paused.2:15:00:45.1582:9input:103:15:00:47.1603:9input:114:15:00:50.1624:9input:99consumed,workerThreadover,退出。可以看到主线程执行完不到1秒,5秒后从数据源发出数据进入第一步处理,2秒后进入第二步处理,3秒后秒进入第三步处理,数据被消费,结束。主线程Id为1,工作线程Id为9。这段代码实际上建立了一个数据通道,在通道的指定位置插入处理逻辑,等待数据的到来。主线程执行建立通道的代码,主线程很快执行,通道建立。此时它只是一个没有任何数据的空通道。当数据到达时,工作线程执行每个节点的逻辑代码来处理数据,然后将数据传输到下一个节点,如此循环直到结束。所以,在写响应式代码的时候,一定要记住,我做的是创建一个数据通道,并在通道的指定位置插入适当的逻辑处理代码。同时要记住,主线程执行的时候,只是建立了通道,并没有数据。如果您不理解本文的内容,请多读几遍以确保您理解了它。如果你都明白了,那么恭喜你,你已经开始响应式编程了。