java基础之深入原理(重点)

危机感源于今天的差距,恐惧感来自未来5年、十年、十几年的差距。

java基础总结强化

一、从底层理解java反射机制

了解java程序从编译到执行的全过程,从底层理解学习,java代码编译和执行主要包含以下3个重要机制,有助于我们更好的理解学习程序的运行机制和原理。

(一)java源码编译机制

1、java源码编译是通过java源码编译器完成,将源码文件(.java)编译成字节码文件(.class即二进制文件)。

2、java源码编译由以下三个过程组成:

(1)分析和输入到符号表;

(2)注解处理;

(3)语义分析和生成class文件。

3、详细流程如下:

源代码文件*.java -> 词法分析器 -> tokens流 -> 语法分析器 -> 语法树/抽象语法树 -> 语义分析器 -> 注解抽象语法树 -> 字节码生成器 -> JVM字节码文件*.class

4、class文件主要包括两部分:

(1)常量池:记录的是代码出现过的所有token(类名,成员变量名等等)以及符号引用(方法引用,成员变量引用等等);编译器将源程序编译成class文件后,会用一部分字节分类存储类字段的名字/所属类型、类方法的名字/返回类型/参数名与所属类型、常量,还有在程序中出现的大量的字面值。

注意:在编译器编译Java源代码时,就已经在字节码中为每个方法都设置好了局部变量区和操作数栈的数据和大小。当JVM首次加载方法所属的Class文件时,就将这些数据放进了方法区。(从这可以看出JVM是一种解释执行class文件的规范技术)。

       因此在线程调用方法时,只需要根据方法区中的局部变量区和操作数栈的大小来分配一个新的栈帧的内存大小,并压入Java栈。

(2)方法区:类中各个方法的字节码。

注意:事实上,只有JVM加载class后,在方法区中为它们开辟了空间才更像一个“池”。

(二)类加载机制

1、JVM(java虚拟机)的类加载是通过ClassLoader及其子类来完成的,默认采用的是双亲委派机制。

2、双亲委派机制:类的加载是自顶向下加载,检测该类是否加载是从下往上检查是否已加载,即就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次往上传递加载请求 (本质上就是loadClass函数的递归调用)。因此,所有的加载请求最终都会传到顶层的启动类加载器中。如果父类加载器已完成这个类加载请求,就成功返回;只有当父类加载器无法完成此加载请求时,子加载器才会尝试自己去加载。

注意:双亲委派机制保证了类加载过程的安全性,避免黑客利用同名类植入程序。

3、JVM预定义的三种类型的类加载器:

(1)启动(Bootstrap)类加载器:负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类;

(2)扩展(Extension)类加载器:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将 <JAVA_HOME >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。

(3)系统(System)类加载器:系统类加载器是由 Sun的AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径下的类库 加载到内存中。开发者可以直接使用系统类加载器。

(4)Custom ClassLoader(自定义类加载器):程序员可以根据自身需要自定义类加载器。

注意:JVM主要在程序第一次主动使用类的时候,才会去加载该类。也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。

(三)类执行机制

1、JVM是基于栈的体系结构来执行class字节码文件。

2、当线程创建后,JVM根据字节码为类的各种信息分配内存区域,分别为程序计数器、栈、堆和方法区。

(1)程序计数器(PC)存放下一条要执行的指令在方法内的偏移量即当前线程所执行的字节码的行号指示器,存储着下一条被执行的指令地址。

(2)堆用于存储所有类实例和数组对象。

注意:堆内存中类实例有指向方法区中类方法的指针。

(3)栈用来存放基本类型的对象和自定义对象的引用。每启动一个线程,JVM都会为它分配一个Java栈,用于存放方法中的局部变量,操作数以及异常数据等。

(4)方法区又称为静态区,和堆一样被所有线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

注意:方法区属于堆内存。

注意:(1)String str = "hello"; 先在内存中找是不是有"hello"这个对象,如果有,就让str指向那个"hello".如果内存里没有"hello",就创建一个新的对象保存"hello",即这样定义的变量所有引用指向同一对象。(2)String str=new String ("hello") 就是不管内存里是不是已经有"hello"这个对象,都新建一个对象保存"hello"。

3、详解栈内存数据结构分为三部分:局部变量区、操作数栈和帧数据区。

(1)局部变量区:用来存放方法中的所有局部变量值,包括传递的参数。

(2)操作数栈:用来存储在执行指令时产生和使用的中间结果数据。

(3)帧数据区:用来存储常量池的解析、正常方法返回以及异常派发机制的信息数据

(四)实例说明java源码编译执行的过程

1、在编译好java程序得到MainApp.class文件后,在命令行上敲java AppMain。系统就会启动一个jvm进程,jvm进程从classpath路径中找到一个名为AppMain.class的二进制文件,将MainApp的类信息加载到运行时数据区的方法区内,这个过程叫做MainApp类的加载。

2、然后JVM找到AppMain的主函数入口,开始执行main函数。

3、main函数的第一条命令是Animal  animal = new Animal("Puppy");就是让JVM创建一个Animal对象,但是这时候方法区中没有Animal类的信息,所以JVM马上加载Animal类,把Animal类的类型信息放到方法区中。

4、加载完Animal类之后,Java虚拟机做的第一件事情就是在堆区中为一个新的Animal实例分配内存, 然后调用构造函数初始化Animal实例,这个Animal实例持有着指向方法区的Animal类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用。

5、当使用animal.printName()的时候,JVM根据animal引用找到Animal对象,然后根据Animal对象持有的引用定位到方法区中Animal类的类型信息的方法表,获得printName()函数的字节码的地址。

6、开始运行printName()函数。

(五)反射机制原理

1、反射机制原理:在程序运行状态下,可以动态获取或者动态调用任意一个类的方法和属性。

注意:java反射本质:通过Class类实例获取指定类的字节码信息,进而获取或者调用该类的属性和方法。底层就是Class实例存储指向方法区中存储着该类方法的指针,进而调用该类的方法。

2、获取Class类实例的三种方法:

(1)利用Object的getClass()方法,返回Object运行时的实例。

(2)利用类字面常量:通过类名.class获取class对象。

(3)利用Class类的forName(String 类全名)方法获取指定类的实例,其中,类全名=包路径+类名。(常用方法)

总结:第一种已经创建了对象,那么这个时候就不需要去进行反射了,显得有点多此一举。第二种需要导入类的包,依赖性太强。所以我们一般选择第三种方式。

3、利用java反射机制获取指定类的方法并调用该方法:

(1)getMethod(String 方法名,Class 方法参数)方法返回指定方法名的方法对象。

(2)getMethods()方法返回指定类的方法对象数组。

(3)Method类的invoke(Object obj,Object... args):对带有指定参数的指定对象调用由此 Method 对象表示的底层方法。

注意:调用本质:Method类实例即方法对象存储着指向该类内存方法区方法的指针,进而直接调用该方法。

  • java绑定机制

       JVM中方法的调用是通过静态和动态绑定机制实现的。其中,java的多态和向上转型都是借助动态绑定实现的,当我们理解了动态绑定就能搞定多态和向上转型。

(一)静态绑定机制

静态绑定是在程序执行前方法就已经被绑定即在编译过程中就已经知道被调用的方法到底是哪个类中的方法。

1、java当中的方法只有final、static、private修饰的的方法和构造方法是静态绑定的。

(1)private修饰的方法:private修饰的方法是不能被继承的,因此子类无法访问父类中private修饰的方法。所以只能通过父类对象来调用该方法体。因此可以说private方法和定义这个方法的类绑定在了一起。

(2)final修饰的方法:可以被子类继承,但是不能被子类重写(覆盖),所以在子类中调用的实际是父类中定义的final方法。

注意:使用final修饰方法的两个好处:(1)防止方法被覆盖;(2)关闭java中的动态绑定。

(3)static修饰的方法:可以被子类继承,但是不能被子类重写(覆盖),但是可以被子类隐藏。

注意:当子类对象向上类型转换为父类对象时,不论子类中有没有定义这个静态方法,该对象都会使用父类中的静态方法,因此这里说静态方法可以被隐藏而不能被覆盖。这与子类隐藏父类中的成员变量是一样的。隐藏和覆盖的区别在于,子类对象转换成父类对象后,能够访问父类被隐藏的变量和方法,而不能访问父类被覆盖的方法。

(4)构造方法:构造方法也是不能被继承的,因此编译时也可以知道这个构造方法方法到底是属于哪个类的。

注意:因为子类是通过super方法调用父类的构造函数,或者是jvm自动调用父类的默认构造方法。

2、静态绑定总结:

       在编译器阶段就已经指明了调用方法在常量池中的符号引用,JVM运行的时候只需要进行一次常量池解析即可。

(二)动态绑定

动态绑定是指在运行时期根据具体对象的类型进行绑定。

1、若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。

注意:编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。不同的语言对后期绑定的实现方法是有所区别的,但我们至少可以这样认为:它们都要在对象中安插某些特殊类型的信息进行标识。

2、动态绑定的过程:

(1)JVM虚拟机获取对象实际类型的方发表;

(2)JVM通过常量池解析获取在方法表中指定方法的定位;

(3)通过直接地址找到该方法字节码所在的内存空间,调用方法。

注意:java中重载的方法使用静态绑定,重写的方法使用动态绑定。

3、动态绑定总结:

根据对象的声明类型(对象引用的类型)找到“合适”的方法。具体步骤如下:

(1)如果能在声明类型中匹配到方法签名完全一样(参数类型一致)的方法,那么这个方法就是最合适的。

(2)在第1条不能满足的情况下,寻找可以“凑合”的方法。标准就是通过将参数类型进行自动转型之后再进行匹配。如果匹配到多个自动转型后的方法签名f(A)和f(B),则用下面的标准来确定合适的方法:传递给f(A)方法的参数都可以传递给f(B),则f(A)最合适。反之f(B)最合适 。

注意:第二个条件表达的是方法参数范围小的最合适。

(3)如果仍然在声明类型中找不到“合适”的方法,则编译阶段就无法通过。

(三)方法表

1、当JVM使用类装载器定位Class文件,并将其输入到内存中时。会提取Class文件的类型信息,并将这些信息存储到方法区中。其中,有一种数据结构就是方法表,方法表以数组的形式存储当前类及其所有超类的可见方法字节码在内存中的直接地址。

2、方法表存储类信息的特点:

(1)子类方法表继承了父类的方法;

(2)相同的方法名(即方法名和参数列表相同)在所有类的方发表中的索引相同。

3、示例图如下:Father f = new Son();f.f1();

注意:父类引用指向子类对象在调用方法时,首先在父类方发表中查找该方法,如果没有找到该方法,则编译时便无法通过;如果有该方法,保留该方法的索引号,在根据子类对象引用找到子类的方法表,通过方法索引号找到子类的同名方法,然后通过直接地址找到该方法字节码所在的内存空间。即根据对象(father)的声明类型(Father)还不能够确定调用方法f1的位置,必须根据father在堆中实际创建的对象类型Son来确定f1方法所在的位置。

三、JDK动态代理原理

1、JDK动态代理是通过JDK的Proxy类和一个调用处理器InvocationHandler接口来实现的,通过Proxy来生成代理类实例,而这个代理实例通过调用处理器InvocationHandler接收不同的参数灵活调用真实对象的方法。

(一)Proxy类

1、通过Proxy类的静态方法Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)生成代理类实例。参数说明:

(1)loader表示获取当前被代理类的类加载器,作用是用来生成代理类的。

(2)interfaces表示获取真实对象的所有接口,作用是代理类要实现所有接口。

(3)h表示调用处理器,作用生成的代理不能直接调用真实对象的方法,而是通过调用处理器来调用真实对象的方法,即重写的invoke(Object proxy, Method method, Object[] args)方法。

注意:JDK动态代理只能代理接口,不能代理类。

2、区分开equals和==:

(1)equals比较的是对象的内容;

(2)==比较的是存储对象的地址。

3、回调更像是一个约定:就是如果我调用了a()方法,那么就必须要回调,而不需要显示调用。

四、进程和线程

(一)进程

1、产生进程概念的原因:

       由于cpu与其他pc资源(RAM,显卡、光驱、键盘等)之间速度的不协调,假如有两个程序A和B,程序A在执行到一半的过程中,需要读取大量的数据输入(I/O操作),而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源。为了提高cpu资源利用率,在程序A读取数据的过程中,让程序B去执行,当程序A读取完数据之后,让程序B暂停。为实现程序间的切换,就需要状态的保存和恢复。

       由于任务执行需要具备执行环境即程序执行所需的资源,称之为程序上下文。要实现程序间切换,轮流使用cpu资源即“程序同时执行”,就需要保存程序上下文状态信息,因此,推出进程的概念来表述程序上下文的状态信息即各种资源状态信息。

2、进程和程序区分开

(1)程序是一个静态概念,机器上的一个class文件或者.exe文件就是一个程序。

(2)进程是程序的一次动态执行,进程是资源分配的基本单位,拥有独立的内存区域。

3、进程定义

       进程就是一个程序在一个数据集上的一次动态执行过程。进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。

4、进程实例讲解:

       有一手好厨艺的计算机科学家正在为他的女儿烘制生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需的原料:面粉、鸡蛋、糖、香草汁等。在这个比喻中,做蛋糕的食谱就是程序(即用适当形式描述的算法)计算机科学家就是处理器(cpu),而做蛋糕的各种原料就是输入数据。进程就是厨师阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和。现在假设计算机科学家的儿子哭着跑了进来,说他的头被一只蜜蜂蛰了。计算机科学家就记录下他照着食谱做到哪儿了(保存进程的当前状态),然后拿出一本急救手册,按照其中的指示处理蛰伤。这里,我们看到处理机从一个进程(做蛋糕)切换到另一个高优先级的进程(实施医疗救治),每个进程拥有各自的程序(食谱和急救手册)。当蜜蜂蛰伤处理完之后,这位计算机科学家又回来做蛋糕,从他离开时的那一步继续做下去。

(二)线程

1、产生线程概念的原因

       当程序增多时,程序间状态保存和切换耗时就会增大,为进一步提高进程对资源的利用率,在进程中引出线程的概念,线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,使到进程内并发成为可能。

       假设,一个文本程序,需要接受键盘输入,将内容显示在屏幕上,还需要保存信息到硬盘中。若只有一个进程,势必造成同一时间只能干一样事的尴尬(当保存时,就不能通过键盘输入内容)。若有多个进程,每个进程负责一个任务,进程A负责接收键盘输入的任务,进程B负责将内容显示在屏幕上的任务,进程C负责保存内容到硬盘中的任务。这里进程A,B,C间的协作涉及到了进程通信问题,而且有共同都需要拥有的东西—文本内容,不停的切换造成性能上的损失。若有一种机制,可以使任务A,B,C共享资源,这样上下文切换所需要保存和恢复的内容就少了,同时又可以减少通信所带来的性能损耗,那就好了。这种机制就是线程。

2、线程定义

       线程是调度的基本单元,是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。线程没有自己的系统资源。

(三)创建线程的三种方法

1、第一种方法:继承Thread类,重写run()方法。

注意:Thread类的常用构造方法:Thread(Runnable target,String threadName);Thread(Runnable target);

2、第二种方法:实现Runnable接口,重写run()方法。(推荐)

(1)创建Runnable接口的实现类;

(2)重写run()方法;

(3)利用Thread(Runnable target,String threadName)构造方法创建线程对象,其中,以Runnable接口的实现类作为参数是确定所要执行run()方法的所属对象。

注意:可通过Thread类的静态方法currentThread()方法获取当前正在执行的线程对象的引用。

3、第三种方法:利用Callable接口和FutureTask类实现。

(四)线程进入阻塞状态的方法

1、sleep(long millis)方法是线程Thread类的静态方法:在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。

注意:该方法只是让出CPU资源,并不会释放同步资源锁。

2、wait()是Object的方法,是指当前线程释放同步资源锁,进入等待状态。

注意:wait()方法则只能在同步方法或同步块中使用,直到线程对象执行notify()或者notifyAll()方法时,被唤醒的线程才能以常规方式与在该对象上主动同步的其他所有线程进行竞争。

3、yield()方法是Thread类的静态方法,暂停当前正在执行的线程对象,并执行其他线程。

4、join()方法表示优先执行完当前线程,再去执行其他线程。

(五)实现线程同步

1、第一种方法:使用synchronized关键字修饰方法。

由于java中每个对象都有一个内置锁,使用此关键字修饰方法时,内置锁会保护整个方法。

例如:public synchronized void save(){ //执行代码}。

注意:同步方法无须显式指定同步监视器,因为同步方法的同步监视器是this即调用该方法的对象。

2、第二种方法:使用synchronized修饰语句块。(推荐)

被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。

语法如下:synchronized(object){ //执行代码 },其中object为同步监视器。object可以是this,代表获取当前对象的锁;也可以是类中的一个属性,代表获取该属性的锁。

注意:java程序允许使用任何对象作为同步监视器,通常推荐使用可能被并发访问的共享资源充当同步监视器。

总结:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,建议使用synchronized代码块同步关键代码即可。

3、第三种方法:使用volatile关键字修饰变量。

volatile修饰变量保证了可见性和有序性。

  1. 可见性:在多线程环境下,某个共享变量如果被其中一个线程给修改了,其他线程能够立即知道这个共享变量已经被修改了,当其他线程要读取这个变量的时候,最终会去内存中读取,而不是从自己的工作空间中读取。
  2. 有序性:一个变量被声明volatile的话,那么这个变量不会被进行重排序,也就是说,虚拟机会保证这个变量之前的代码一定会比它先执行,而之后的代码一定会比它慢执行。

注意:volatile并不是线程安全的,因为java运行程序并不是原子操作,无法保证一致性。

4、必须满足以下两个条件的情况下,volatile就能保证变量的线程安全问题:

  1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  2. 变量不需要与其他状态变量共同参与不变约束。

5、第四种方法:使用锁机制(推荐)

(1)JDK5中添加了新的java.util.concurrent包,相对同步容器而言,并发容器通过一些机制改进了并发性能。因为同步容器将所有对容器状态的访问都串行化了,这样保证了线程的安全性,所以这种方法的代价就是严重降低了并发性,当多个线程竞争容器时,吞吐量严重降低。

 (2)Lock,ReadWriteLock是Java5提供的两个根接口,并为 Lock提供了ReentrantLock实现类,为ReadWriteLock提供了ReentrantReadWriteLock实现类。在 Java8中提供了新型的StampLock类,在大多数场景下它可以替代传统的ReentrantReadWriteLock。

注意:采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

6、总结:在性能上来说,如果竞争资源不激烈,synchronized和Lock两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。

7、第五种方法:ThreadLocal类管理变量。

(1)使用ThreadLocal类管理变量,每一个使用该变量的线程都会获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

(2)ThreadLocal() : 创建一个线程本地变量 

(3)get() : 返回此线程局部变量的当前线程副本中的值 

(4)initialValue() : 返回此线程局部变量的当前线程的"初始值" 

(5)set(T value) : 将此线程局部变量的当前线程副本中的值设置为value

注意:如果业务逻辑强依赖于副本变量,则不适合用ThreadLocal解决。

注意:ThreadLocal一般都是声明在静态变量中,如果不断地创建ThreadLocal而没有调用其remove方法,将导致内存泄露,特别是在高并发的Web容器当中。

五、IO学习

(一)传统BIO(Blocking IO)模型

1、传统的BIO是同步阻塞,基于流的模型。

注意:BIO模拟场景:叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。

2、传统BIO存在的问题:

       针对网络通信采用的是一个请求对应一个应答的方式,虽然简化了应用层的开发,但是在性能和可靠性方面存在巨大瓶颈,在高并发场景下,机器的资源很快就被耗尽即服务端无法为多个客户端提供服务。虽然,采用线程池的方式可以优化,但即使这样,也无法改变传统IO阻塞的根本问题。

3、传统BIO模型API:java.io包包含了所有BIO操作的输入和输出流。

4、BIO操作流的分类:起初只有字节操作流(InputStream和OutputStream),但是字节操作流不利于中文的阅读,因此,推出了字符操作流(Reader和Writer)。

(1)字节输入流和输出流抽象类:InputStream和OutputStream。

(2)字符输入流和输出流抽象类:Reader和Writer。

5、为实现字节和字符之间的转换,可通过转换流实现:利用InputStreamReader和OutputStreamWriter两个类的构造函数分别以InputStream和OutputStream为参数创建字符流。为避免频繁的读写硬盘,提高性能,推出了字节和字符缓冲流。

(1)字节缓冲流:BufferedInputStream和BufferedOutputStream。

(2)字符缓冲流:BufferedReader和BufferedWriter。

6、为实现对文件的读写,推出了文件字节输入流和输出流(FileInputStream和FileOutputStream)。为方便中文的读写,推出了文件字符输入流和输出流(FileReader和FileWriter)。

7、在使用OutputStreamWriter时,当关闭此字符输出流时,要先调用flush()方法刷新该流的缓冲即将已缓冲的字符强制输出到指定目的地,再关闭此流。这样做的目的是关闭该流时避免丢失已缓冲的字节。

注意:flush()方法的常规协定是:如果此输出流的实现已经缓冲了以前写入的任何字节,则调用此方法指示应将这些字节立即写入它们预期的目标。

(二)NIO(new IO或者non-blocking io)模型

1、由于传统BIO同步阻塞的缺点,导致java在网络通信模块一直是短板,为加强java在服务端的市场,在JDK1.4推出了NIO支持非阻塞IO,并增加了java.nio包,提供很多异步开发的API和类库。

注意:NIO的模拟场景:叫一个线程不停的循环观察每一个水壶,根据每个水壶当前的状态去处理。

2、NIO是同步非阻塞,基于缓存的模型,是Buffer、Channel和Selector三种技术的整合。通过Buffer取得数据,每一个客户端通过Channel在Selector(多路复用器)上进行注册。服务端不断轮询Channel来获取客户端的信息。Channel上有connect,accept(阻塞)、read(可读)、write(可写)四种状态标识。根据标识来进行后续操作。所以一个服务端可接收无限多的Channel。不需要一个通道开启一个线程,大大提升了性能。

3、Buffer(缓冲区)是一个对象,包含一些要写入或者读出的数据。

(1)在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。

(2)缓冲区实际上是一个数组,并提供了对数据结构化访问以及维护读写位置等信息。

(3)通常使用ByteBuffer来操作,利用allocate(int capacity)创建一个指定大小的缓存。

(4)flip()方法将Buffer从写模式切换为读模式。使用clear()方法清除缓冲区。

4、Channel(通道)对数据的读取和写入要通过Channel,它就像水管一样,是一个通道。通道不同于流的地方就是通道是双向的,可以用于读、写和同时读写操作。

(1)底层的操作系统的通道一般都是全双工的,所以全双工的Channel比流能更好的映射底层操作系统的API。

(2)Channel主要分两大类:SelectableChannel:用户网络读写;FileChannel:用于文件操作。

注意:ServerSocketChannel和SocketChannel都是SelectableChannel(多路复用器)的子类。

5、Selector(选择器)提供选择已经就绪的任务的能力:Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

注意: 一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。

6、NIO的处理流程:

(1)创建利用Selector选择器的open()方法创建一个选择器实例:

Selector selector = Selector.open(); 

(2)将通道设置为非阻塞方式,SocketChannel和ServerSocketChannel都是SelectableChannel(多路复用器)的子类,可以使用configureBlocking()方法和register()方法:

channel.configureBlocking(false); 

(3)将通道以SelectionKey 的OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT四种标记中的一个标记注册到选择器上:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ); 

while(true) { 

(4)使用选择器Selector的select()方法轮询已准备好的通道状态,进行遍历处理:

  int readyChannels = selector.select();

(5)如果没有准备好的通道,就退出本次循环,进行下一次循环: 

  if(readyChannels == 0) continue; 

(6)利用选择器Selector的SelectedKeys()方法获取已准备好的通道状态Set集合:

  Set selectedKeys = selector.selectedKeys(); 

(7)利用Set集合的迭代器Iterator遍历通道状态:

  Iterator keyIterator = selectedKeys.iterator(); 

  while(keyIterator.hasNext()) { 

SelectionKey key = keyIterator.next();

//判断通道是哪种状态,然后进行处理

    if(key.isAcceptable()) { 

        // a connection was accepted by a ServerSocketChannel. 

    } else if (key.isConnectable()) { 

        // a connection was established with a remote server. 

    } else if (key.isReadable()) { 

        // a channel is ready for reading 

    } else if (key.isWritable()) { 

        // a channel is ready for writing 

}

(8) 将已处理的通道状态删除,否则,下一次 select() 返回时 我们还可以在 SelectedKeys 集合中获取到该通道状态:

keyIterator.remove();

}

7、因为NIO封装度不高且使用麻烦,因此可以使用非常好用的NIO框架Netty。

(三)AIO(Asynchronous IO):异步IO

1、AIO是对NIO的优化改进,在JDK7的版本中发布,引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。AIO的处理过程是将IO操作的两个阶段都全部交给内核系统完成,用户线程只需要告诉内核,我要读取一块数据,请你帮我读取,读取完了放在我给你的地址里面,然后告诉我一声就可以了。

注意:AIO的实现需要操作系统层面支持,在windows上通过IOCP实现了真正的异步io;在Linux系统中,仍然用的是epoll模式,并不是纯正的AIO。

2、AIO的模拟场景类似:每个水壶上装一个开关,当水开了以后会提醒对应的线程去处理。

3、AIO的API比NIO的使用要简单很多,主要就是监听、读、写等各种CompletionHandler。

(四)序列化

1、序列化是解决对象在读写和传输操作引发的问题,解决对象的状态保存问题。

2、序列化的实现:将需要被序列化的类实现Serializable接口(标记接口),该接口没有需要实现的方法,implements Serializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象;接着,使用ObjectOutputStream对象的writeObject(Object obj)方法就可以将参数为obj的对象写出(即保存其状态),要恢复的话则用输入流。

六、网络编程

网络编程是实现网络中主机上应用程序之间的通信,这就涉及通信协议。

(一)通信协议

1、通信分层意义

       由于底层的通信比较复杂,需要进行抽象分离,为了方便开发和扩展,通信协议采用分层的思想。即同层之间可以通信,上一层可以调用下一层,隔层之间不会发生关系,各层之间互不影响,有利于系统的开发和扩展。

2、通信协议分层模型TCP/IP四层模型由上往下:应用层、传输层、网络层、数据链路层和物理层。

(1)应用层协议:http、ftp、telnet(远程登录服务协议)、dns(域名解析服务);

(2)传输层协议:tcp和udp;数据包装上端口。

(3)网络层协议:ip协议、DHCP(动态主机控制协议);

(4)数据链路层:arp(地址解析协议:根据ip获取MAC地址);

3、ip地址是一个32位的地址,分为网络地址和主机地址,通过子网掩码进行划分。

4、子网掩码的作用就是把ip地址分为两部分网络地址和主机地址。

5、网关是指具有路由功能的设备的ip地址,可以有多个,默认网关是指没有可用的网关就用默认的网关发送数据。网关将两个使用不同协议的网络段连接在一起的设备。它的作用就是对两个网络段中的使用不同传输协议的数据进行互相的翻译转换。

6、DNS服务器是域名解析服务器,用于把网址解析为ip地址。

7、TCP是一种面向连接的协议,提供可靠的字节流通信协议。

8、UDP是向相应程序提供了一种发送封装的原始IP数据报的方法,发送时不需要建立连接,是一种不可靠的连接。

9、端口的作用用来区分同一台电脑上的不同应用程序,端口号在系统中占2个字节,共65536个端口,端口号的理论值范围是从0到65535,公认的是0-1023 ,注册端口是1024-49152,还有随机动态端口是1024-65535,共是65536个端口。

(二)网络资源常用类

1、InetAddress类用于获取互联网协议ip地址信息,封装计算机的ip地址和DNS(没有端口信息)。

(1)InetSocketAddress是SocketAddress的子类,包含IP地址和端口信息,常用于socket通信,不依赖于任何协议。

2、域名和主机名的区别:

(1)域名是Internet网络上的一个服务器或一个网络系统的名字,在全世界,没有重复的域名。域名的形式是以若干个英文字母和数字组成,由“.”分隔成几部分。

(2)一个域名下可以有多个主机名,域名下还可以有子域名。例如,域名abc.com下,有主机server1和server2,其主机全名就是server1.abc.com和server2.abc.com。

3、URL类表示统一资源定位符,通过URL可以直接读取或写入网络上的数据。

(1)通过URL对象的openStream()方法可以得到指定资源的输入流,利用io相关类读取或访问网页上的资源。

4、Socket类用来创建客户端的套接字,创建一个流套接字并将其连接到指定主机上的指定端口(即指定服务器的ip地址和端口号)。

       Socket编程需要实现服务器端和客户端,因为这两个设备通讯时,需要知道对方的IP和端口号。通常服务器端有个固定的端口号,客户端直接通过服务器的IP地址和端口号进行访问服务器端,同时告知客户端的端口号,于是他们之间就可以通过socket进行通信。

(1)通过getInputStream()和getOutputStream()方法获取输入和输出流。

5、ServerSocket类用于创建服务器端的套接字,创建绑定到特定端口的服务器套接字(即指定要监听的端口),通过accept()方法侦听并接受客户端的套接字,返回一个和客户端Socket对象相连接的Socket对象。同样,通过Socket对象的getInputStream()和getOutputStream()方法获取输入和输出流进行交互。

6、DatagramSocket类用于创建数据报套接字并将其绑定到本地主机上的指定端口。由于UDP协议不需要建立连接,可以发送或者接受数据报即DatagramSocket的send()和receive()方法就可以发送和接收数据,UDP协议接收和发送数据没有IO流接口。

(1)DatagramPacket类用来创建数据报包,数据报包包含要发送的数据、IP地址和端口号。数据报包可用来实现无连接包投递服务。每条报文仅根据该包中包含的信息(ip地址和端口信息)从一台机器路由到另一台机器。

七、集合框架

(一)匿名内部类

1、匿名内部类的作用:使用匿名内部类可以简化代码编写,代码更加简洁清晰。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。

2、匿名内部类的使用:使用匿名内部类能够在实现父类或者接口中的方法情况下同时产生一个相应的对象,但是前提是这个父类或者接口必须先存在才能这样使用。

(二)Comparable和Comparator排序接口

1、java是面向对象的语言,如果对自定义的对象实现排序功能,可以让该对象实现Comparable接口或者定义一个实现Comparator接口的比较器类。

2、利用集合工具类Collections或者数组工具类Arrays的sort()方法对集合或者数组排序。

3、实现Comparable接口,要重写compareTo(T object)方法,返回结果:当前对象小于参数对象,等于参数对象,大于参数对象,分别返回负整数,零,正整数。

注意:若当前对象减去参数对象,默认是升序排列;参数对象减去当前对象,则为降序排列。

4、定义一个实现Comparator接口的比较器类,要重写compare(T o1,T o2)方法,返回结果:o1小于o2,等于o2,大于o2,分别返回负整数,零,正整数。

注意:若o1减去o2,则为升序;o2减去o1,则为降序。

5、利用集合工具类或者数组工具类的sort(List list,Comparator c)方法对集合或者数组按照比较器排序。

(三)集合框架

1、集合框架主要包括两种类型的容器,分别为集合Collection和视图Map。

(1)Collection存储一种元素集合,Collection又有三个子接口,分别为List、Set和Queue,其中List采用类似数组的形式存储;Set采用散列表的形式存储;Queue采用队列先进先出的形式存储。

(2)Map是存储键值对映射。

2、Collection集合通过add()方法添加元素,即List、Set和Queue都继承了该方法。

3、List集合可以通过get()方法获取指定元素。

4、Set集合通过迭代器获取指定元素。

5、Queue集合通过peek()获取但不移除队列头;poll()方法获取并移除队列头。

6、Map集合通过put()方法添加元素,通过get()方法获取指定键对应的值。

7、通过集合工具类Collections和数组工具类Arrays对集合和数组进行操作,提供了填充、排序和搜索等操作的静态方法。

(四)泛型

1、泛型的本质就是将数据类型参数化,在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

 

  • 2
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一位远方的诗人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值