拓展内容

编程方式

  • 命令式编程:命令“机器”如何去做事情*(how),这样不管你想要的是什么(what)*,它都会按照你的命令实现。比如说面向过程编程

  • 声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做*(how)。比如说SQL语句,编程语言中的接口

  • 函数式编程:一切皆是函数,函数可以作为参数作为返回值,详见kotlin的高阶函数

  • 面向切面编程:指对于一段代码我们从中间织入语句,常用的就是为一个函数调用前后插入语句做操作。

在java中这些都可以被从形式上支持

函数式相关

lambda表达式

如何从签名的角度将一个方法与其他方法分离

方法有三个特性表现自己和其余方法的不同

1,调用者,从OOP的角度来看就是这个方法归属于哪个类

2,方法的返回值,即预期返回类型

3,方法形参,即预期的输入类型

另外在java中声明的异常也算是方法的一部分,这个不在讨论范围内

Method method = HttpVerticle.class.getMethod("start", Promise.class);
System.out.println(method);
//public void verxtx.HttpVerticle.start(io.vertx.core.Promise) throws java.lang.Exception

从数学的角度来说函数就是值对值的映射关系,再从OOP的角度来说,如果我声明一个int到Srting的函数,即

int -> String的映射,这只是一种关系,即声明签名。其余符合这个映射关系的函数就可以视为这个声明的实现

即如下伪代码也是成立的

void invoke((int-> String)  function){
   String s = function(1);
//.......
}
String transfer(int a){
//....
return a.toString;
}
invoke(transfer);

这个就是函数可以如同对象一样传递

java如何书写lambda表达式

首先java lambda表达式比较受限,只支持sam(Single Abstract Method)接口,而且是以对象的形式支持的

其核心就在于形参,方法体和返回值

即()->{ }形式

观察这个接口

public interface Handler<String> {
  int handle(String s);
}

首先确定形参 String s,其次确定返回值int

Handler lambda  = (String s) -> {
//do something
return 1
}

若方法体只有单行且返回值为int,简化版本 (s) -> s.length

lambda实质

通过字节码可以看出,调用lambda方法时使用了invokedynamic,该字节码命令是为了支持动态语言特性而在Java7中新增的。Java的lambda表达式实现上也就借助于invokedynamic命令。

字节码中每一处含有invokeDynamic指令的位置都称为动态调用点,这条指令的第一个参数不再是代表方法调用符号引用的CONSTANT_Methodref_info常量,而是变成为JDK7新加入的CONSTANT_InvokeDynamic_info常量,从这个新常量中可得到3项信息:引导方法(Bootstrap Method,此方法存放在新增的BootstrapMethods属性中)、方法类型和名称。引导方法是有固定的参数,并且返回值是java.lang.invoke.CallSite对象,这个代表真正要执行的目标方法调用。根据CONSTANT_InvokeDynamic_info常量中提供的信息,虚拟机可以找到并执行引导方法,从而获得一个CallSite对象,最终调用要执行的目标方法。

以上看不懂?没关系

我们看反射结果

以我的项目中一个例子,所以实际上就是生成了一个函数而已

for (Method method : HttpVerticle.class.getDeclaredMethods()) {
      System.out.println(method);
    }
//private static void verxtx.HttpVerticle.lambda$start$3(io.vertx.ext.web.RoutingContext)

面向切面编程

从代理模式讲起

何为代理模式?即一个代理类包含一个被代理的引用,实际操作还是由被代理的对象操作

代理类可以在被代理的方法执行前后执行一些代码

其伪代码如下

class DelegateClass{
    T realClass;
    void doSomething(int a,String s){
        //do something before calling real method
        realClass(a,s);
        //do something after calling real method
    } 
}

这个就是典型的静态代理,这个在调用前后的执行操作就是对这个函数的切面操作

动态代理

对于大部分情况下我们并不能完全确认是哪个需要被代理,而且代理类和被代理类的代码混合在一起不利于后期维护,有没有一种方法可以解决这个问题呢?

是有的,动态代理,区别于静态代理,其更侧重于运行时将两者代码编织在一起

以面向接口的jdk自带的动态代理代码举例

public class Aop<T> implements InvocationHandler {

  private Object realObject;
  private Class<T> interfaceType;

  public Aop(Object realObject, Class<T> interfaceType) {
    this.realObject = realObject;
    this.interfaceType = interfaceType;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("参数为"+ Arrays.toString(args));
    Object res = method.invoke(realObject, args);
    System.out.println("返回值为"+res);
    return res;
  }

  public <T> T getProxy(){
    return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), realObject.getClass().getInterfaces(), this);
  }

}

核心原理就将原类中方法抽象为Method类,以反射的方式调用,这个代理类为运行时字节码生成的实例

除此之外还有以类继承形式实现的动态代理库CGLIB

也可以了解一下编译期静态代理AspectJ

完整解析+源码参考基于jdk动态代理的Aop实现

IO方式

IO分类

  • 阻塞角度:阻塞IO非阻塞IO这两个概念是程序级别的。主要描述的是程序请求操作系统IO操作后,如果IO资源没有准备好,那么程序该如何处理的问题:前者等待;后者继续执行(挂起)

  • 线程角度:同步IO异步IO,这两个概念是操作系统级别的。主要描述的是操作系统在收到程序请求IO操作后,如果IO资源没有准备好,该如何响应程序的问题:前者不响应,直到IO资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当IO资源准备好以后,再用事件机制返回给程序。

网络IO

同步阻塞模式(Blocking IO)

伪代码如下

{
    // 阻塞,直到有数据
	read(socket, buffer);
	process(buffer);
}

也就说若当前数据没有准备好则当前线程会阻塞在这里,即一个线程只能同时处理一次请求

非阻塞IO(Non-Blocking IO)

伪代码实现

{
while(true){
for(Soket s: Sockets){
 if(s.isAccepted){
 accept();
 }
 id(s.readable){
 read();
 }
 ...
}
}
}

非阻塞 IO 的核心在于使用一个 Selector 来管理多个通道,可以是 SocketChannel,也可以是 ServerSocketChannel,将各个通道注册到 Selector 上,指定监听的事件。

之后可以只用一个线程来轮询这个 Selector,看看上面是否有通道是准备好的,当通道准备好可读或可写,然后才去开始真正的读写,这样速度就很快了。我们就完全没有必要给每个通道都起一个线程。

NIO 中 Selector 是对底层操作系统实现的一个抽象,管理通道状态其实都是底层系统实现的,这里简单介绍下在不同系统下的实现。

select:上世纪 80 年代就实现了,它支持注册 FD_SETSIZE(1024) 个 socket,在那个年代肯定是够用的

1.6上windows的实现就是这个

poll:1997 年,出现了 poll 作为 select 的替代者,最大的区别就是,poll 不再限制 socket 数量。

select 和 poll 都有一个共同的问题,必须全遍历一遍才知道哪个准备好了

epoll:2002 年随 Linux 内核 2.5.44 发布,epoll 能直接返回具体的准备好的通道,时间复杂度 O(1)

这个e就是event的意思,对应连接时间,读写事件都直接投递到队列中,只要遍历这个队列就行了,直接处理事件

异步IO(Asynchronous IO)

简单来说就是直接交由内核处理,并且添加一个回调。

即提交任务后线程可以直接返回。

在 Unix/Linux 等系统中,JDK 使用了并发包中的线程池来管理任务,具体可以查看 AsynchronousChannelGroup 的源码。

在 Windows 操作系统中,提供了一个叫做 I/O Completion Ports 的方案,通常简称为 IOCP,操作系统负责管理线程池,其性能非常优异,所以在 Windows 中 JDK 直接采用了 IOCP 的支持,使用系统支持,把更多的操作信息暴露给操作系统,也使得操作系统能够对我们的 IO 进行一定程度的优化。

而对于Linux平台,目前稳定版本上的AIO实现其实就是NIO,这就是为什么要用JUC的线程池管理

文件IO

在同步文件IO中,线程启动一个IO操作然后就立即进入等待状态,直到IO操作完成后才醒来继续执行。而异步文件IO方式中,线程发送一个IO请求到内核,然后继续处理其他的事情,内核完成IO请求后,将会通知线程IO操作完成了

总结

最简单判断哪个模型最节约资源的方法,就是看自己java程序的线程是不是会被挂起,就是实际运行时间占比

如何判断是否是async,一般来说默认是同步,而后缀async或者前缀async或者方法含有回调的一般是异步

即如果我们要做到最大吞吐量(本机性能的顶点)必须尽可能使用户态线程高效运行,减少阻塞时间

Last updated