19年秋招整理的笔记,尘封了许久…当时整理也花了挺多的心思。双手奉上,给大家参考。希望大家都能成为offer收割机!!! btw 最后有彩蛋!!!有彩蛋!!!彩蛋!!!蛋!!!
子进程和父进程之间有不同的代码和数据空间,多个线程之间共享数据空间。
* 一个进程包含多个线程
* 资源分配给进程,有多个线程共享资源
* 进程是程序执行的最小单位
* 进程由于和相互独立,所以通信机制相对复杂,而线程有共享数据块,通信相对方便。
* 套接字、共享内存、文件、管道、信号,消息队列等
* 继承Thread类
* 实现Runnable接口
* 实现Callable接口, 并把实例对象传给FutureTask,并把FutureTask传给Thread,能够返回一个异步处理结果Future对象,并能抛出异常。
* 使用线程池
* new:新建一个线程
* runnable:可运行
* running: 运行
* block: 阻塞
* 等待阻塞: 运行的线程执行了wait方法,jvm把该线程放入了等待队列。
* 同步阻塞: 运行的线程在获取对象的同步锁时,锁被其他线程占用,则把该线程放入锁池中。
* 其他阻塞: 运行的线程执行了sleep或join或IO操作,最终线程会重新转入可运行状态。
* dead:线程run方法执行结束或意外停止。
* 同步方法可以用this或者当前class对象进行加锁。
* 同步代码块可以选择任意对象来加锁,更加灵活。
* 两只都需要用synchronized关键字修饰。
* 监视器和锁一块使用,监视器监视同步代码块,确保一次只有一个线程执行同步代码块,每个监视器都与一个对象的引用相关联,线程在获取锁之前不允许执行同步代码块。
* 死锁就是两个或多个线程为了获取对方的锁导致一直等待的状态。
* 规定所有线程获取锁的顺序,按照顺序加锁和释放锁,就不会导致死锁的发生。
* 调用了start方法,才是多线程,如果只调用run方法,那么还是在本线程中执行。
* Runnable的run方法没有返回值,Callable有返回一个泛型,和Future和FutureTask组合能够得到异步处理结果,能够取消线程任务。
* CyclicBarrier是等所有线程同时到达栅栏位置才能继续执行。使用await阻塞线程。
* CountDownLatch是一个或多个线程等待一组事件发生。使用countdown方法使的计数器减一。await阻塞等待计数器为0。
* CyclicBarrier当线程到达某个点上,就停止运行了,直到所有线程都到达这个点,所有线程才重新运行。
* CountDownLatch当线程运行到某个点上时,只是数值-1,线程继续运行。
* CyclicBarrier只能唤起一个任务,而CountLatch可以唤起多个任务。
* CyclicBarrier是每次加值,CountDownLatch是每次计数值减1,所以CountDownLatch计数值到0之后就不能重用了。
* 多线程围绕可见性和原子性而展开,使用volatile修饰的变量,能够保证其在多线程中的可见性,即每次读到该变量,一定是最新的数据。
* 阻止jvm对指令进行重排序。
* 和CAS结合,保证原子性。
* 如果程序在多线程环境下运行,和在单线程环境下运行结果相同,就是线程安全的。
* 获取线程的pid: 使用ps命令或者使用jps命令
* 获取堆栈: 使用jstack pid或者kill - 3 pid(打印java线程的调用栈)
* 线程停止,如果线程持有某个对象的监视器,那么监视器也会被释放。
* 使用阻塞队列BlockingQueue。
* wait是Object的方法, 而sleep是线程的方法,如果线程持有某个对象的监视器,sleep不会放弃这个监视器,而wait会放弃这个监视器。
* 通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率。
* 给生产者消费者之间解耦。
* ThreadLocal是为了线程封闭实现的,维护了一个Map结构,可以给每个线程分配一个对象,那么线程之间就不会出现并发访问的问题。
* JDK强制的,这些方法在调用前都必须先获得对象的锁。
* wait方法会立即释放对象的监视器,而notify和notifyAll方法会等线程剩余代码执行结束之后再释放。
* 比如在客户端服务器中,如果每次收到一个请求就新创建一个线程,会浪费很多资源,并且线程的创建和销毁也会浪费很多资源,回收器的压力也会增大,所以,就可以
使用线程池,达到线程的重用,还可以灵活控制线程的并发条目。
* 核心线程数: 当线程小于核心线程数,就会创建新的线程
* 最大线程数:当线程大于最大线程数的时候,就会执行拒绝策略
* 线程空闲时间: 指的是超过核心线程数时线程达到空闲时间,就销毁线程。
* 时间单位: 空闲时间的单位
* 等待队列: 存储等待执行线程的工作队列
* 线程工厂: 创建线程的工厂
* 决绝策略: 工作队列,线程池满的时候就执行拒绝策略
* newFixedThreadPool: 固定长度的线程池,核心线程数和最大线程数相等,空闲时间为0,等待队列为Integer的最大值。
* newCachedThreadPool: 带缓存的线程池,核心线程数为0, 最大线程数为Integer的最大值,当线程空闲时间超过60秒销毁。使用SynchorizedQueue,是一个直接提交的队列,说明,有任务直接线程执行,
没线程创建线程执行。一般建议执行速度较快较小的线程。不然最大线程池边界容易造成内存溢出。
* newSingleThreadPool: 单线程线程池, 核心线程数和最大线程数都是1,亦为之每次执行一个线程,多余的放到等待队列中。
* newScheduledThreadPool: 调度线程池,按一定的周期执行任务,即定时任务。
* Thread中有一个holdsLock(Obj)方法,当前线程持有对象监视器的时候返回true。
* synchonized是关键字,需要定义在方法上,或方法体中,必须在调用的地方释放锁,并且因为是内置锁,所以无法中断一个正在获取锁的线程,也不会一直等到一个线程获取锁。
* ReentrantLock是一个类,所有就可以有属性和方法,更灵活,允许可定时,可轮询,可中断的获取锁的操作。还支持公平队列。但是需要在finally块中释放锁。
* 就是segment的大小,默认是16。也就是最多同时允许16个线程的并发修改。
* ReentrantLock在某些方面会有局限,比如在读写方面,如果用ReentrantLoc本省是防止一个线程在写数据的时候,另一个数据在读数据导致数据不一致,但是如果两个线程都在读数据就没必要加锁。
* 所以出现了读写锁,读锁共享,写锁独占,读和读之间不会排斥,增加了读取的速率。
* FutureTask表示一个异步运算的任务,可以传一个Callable类,对这个任务的结果进行获取,或者取消任务。FutureTask实现了Runnable接口,所以也可以放到线程池中。
线程1先获取A对象的锁,然后休眠一会,此时线程二获取B对象的锁,让线程1 休眠结束之后获取线程二的锁,线程二获取线程一的锁,因为两个线程都想获取对方持有的锁,所以导致了死锁。
* 如果是因为IO阻塞的,没办法唤醒,因为IO是操作系统实现的。
* 如果是因为调用的wait、sleep、join方法的化,可以中断线程,并且抛出InterruptedException唤醒它。
* 这些对象保证了内存可见性,不需要进行额外的同步手段,提升了代码效率。
* 上下文切换是指当前cpu的控制权由当前线程切换到另一个就绪的线程的过程。
* 如果是无界队列LinkedBlockingQueue,继续添加任务到阻塞队列中等待执行
* 如果是有界队列A仍然有BlockingQueue,会根据最大线程数,如果小于最大线程数,创建线程,如果大于,执行拒绝策略,默认是抛异常。
* 抢占式,会根据线程的优先级,饥饿情况等数据算出一个总的优先级,分配下一个时间片给线程执行。
* 因为Java采用优先级的抢占式调度,所以如果一个优先级高的线程常常获取到CPU的控制权,为了让某些优先级低的线程也能被执行,所以就用sleep(0),手动触发一次操作系统分配时间片的操作,平衡
CPU控制权。
* 因为很多synchorized里面的代码都是一些很简单的代码,让等待的线程都加锁就会降低效率,因为线程阻塞涉及用户态到内核态的转变,可以让等待锁的线程不要被阻塞,而是在synchorized周围做忙循环
,循环几次之后如果还没有获取锁,再阻塞。
* Java内存模型将内存分为主内存和工作内存, 类的状态,也就是类之间共享的变量,是存储在主内存中的,Java线程调用的时候会将主内存中的变量在自己的工作内存中复制一份,操作自己的工作内存中的变量,
执行完操作,把最新的变量存入主内存。
* 定义了一些原子操作,用来操作主内存和工作内存中的变量
* 定义了volatile变量的使用规则。
* happens-before: 先行发生原则,定义了操作A必须先行发生于操作B的规则。
* Compare and Swap: 比较-替换,是一种乐观锁技术, 有三个值,内存地址,旧的值,新的值,当且仅当内存值等于旧的预期值的时候,才将内存值修改为新的值。否则什么都不做。比同步锁效率高。
* 乐观锁对并发操作产生的线程安全问题持乐观态度,认为竞争状态永远不会发生,将比较替换作为了原子操作,如果失败则产生冲突,有一定的重试机制。
* 悲观锁认为线程安全问题总是会发生,因此对资源进行操作的时候都会持有独占锁,就像synchonized,直接上锁。
* 抽象队列同步器,维护了一个volatile int的state变量,代表共享资源, 和一个FIFO的队列存储所有争夺共享资源的线程,定义了两种获取资源的方式,独占或者共享,需要自定义的同步器实现tryAcquire
和tryRelease这种获取获释放资源的操作。
* Semaphore用来限制某段代码块的并发数。有一个构造函数,传入int,管理一组permit。当线程来调用方法,使用acquire方法,没有许可就阻塞。执行完成后使用release方法返回一个许可。
如果n=1就是Synchronized。
* 在其他线程执行put方法时,可能size方法获取到的值为旧值。
* 虽然只有一句,但是底层可能需要三句汇编语句,所以也会产生多线程并发问题。
* 构造方法和静态块是调用的线程调用的,只有run方法才是线程自己调用的。
* 高并发,任务执行时间短的任务,可以设置线程数为cpu+1,防止CPU的频繁上下文切换。
* 低并发,任务执行时间长的任务分为计算密集型和IO密集型:
* IO密集型: 不占用CPU,所以线程数可以多一点,让CPU处理更多的业务。
* 计算密集型: 线程数少一点,减小上下文的切换。
* HashTable封装内部状态,对所有共有方法加独占锁,所以只能允许一个线程的并发修改,而且迭代器是快速失败的,不允许遍历时候修改复合操作需要客户端加锁。
* ConcurrentHashMap提出了分段锁的概念,默认16个segment,也就是最多允许16个线程的并发修改,而且迭代器是安全失败的,允许遍历的时候并发修改,而且复合操作作为原子操作实现。
* JDK8之前采用分段锁,默认16个segment,之后取消了分段锁,直接用HashEnrty的table数组,并且采用了数组加链表加红黑树的结构,链表长度超过8,采用红黑树。
* 对变量的写入操作不依赖于当前值, 或者确保只有单线程更新变量的值。
* 该变量不会与其他状态变量一起纳入不变性条件中。
* 在访问变量时不需要加锁。
* 修改源代码: 基本行不通,很多类的源代码都不开放不能被修改。
* 继承原来类,写新的方法: 基本行不通, 并不是所有的类都和Vector一样将内部状态公开,并且当内部类更改了同步机制之后就失效了。
* 客户端加锁,通过将扩展代码放入一个辅助类中,判断类是否支持客户端加锁,并用同样的锁加锁。也会破坏同步策略的封装性。
* 组合模式: 为原来的线程安全类新加一层锁。
* Vector封闭所有内部状态,对所有共有方法通过synchronized加同步,导致方法只能串行执行,但是复合操作需要用客户端加锁方式自己实现
* Inverse of control 控制反转, 在Spring之前,不同层之间的依赖通过new对象完成,这样就导致了层级之前的强耦合关系,
而且还是编译依赖,如果团队合作的话,别人没写那一层,自己只能等带别人写完再写自己的层。所以为了减弱这种耦合关系,
Spring提出了容器的概念,底层就是一个Map集合,Spring在创建容器的时候通过XML配置或者注解依赖反射创建所有的Bean对象的实例,保存到容器中,
这样就把原本属于本类的控制权移交给了容器处理,就减小了这种耦合关系,而且还是运行时依赖。
* Dependency Injection 依赖注入,Spring通过IOC创建类的实例,但是类的成员变量却没有赋值,所以可以通过XML配置或者注解模式对对象进行赋值初始化操作。即依赖注入。
* 一种是最底层的BeanFactory,是延迟加载的,当使用到的时候才创建类的实例。
* 第二种就是ApplicationContext,不进行延迟加载,创建容器的时候就加载了所有Bean对象的实例。
ApplicationContext ac = new ClassPathApplicationContext("bean.xml");
ac.getBean("");
* 通过默认无参构造器创建
(用的最多)
* 通过静态工厂创建
* 通过实例工厂创建
* bean对象默认创建出来都是单例的,但是有些情况下需要用到多例(比如容器接管structs2的action创建时就必须多例)
bean.xml中可以配置scope属性,可配置的值为singleton(单例)、prototype(多例)、request(一次请求)、session(一次会话)、globalsession(一次全局会话)
* 基本数据类型和String
* 其他bean类型(需要在bean.xml里面配置)
* 复杂类型(集合类型)
* 构造函数注入
涉及的标签的属性:
* type 指定参数类型
* index 指定参数索引
* name 指定参数名称(最常用)
* value (指定基本数据类型和String)
* ref 指定其他bean类型
* set方法注入
=======================tag==============================
复杂类型的注入:
dasdsad
dasdsad
dasdsad
dasdsad
========================================================
涉及的标签的属性:
* name 指定set参数的名称(方法名: setName, name即name)
* value 指定基本数据类型和String
* ref 指定其他bean类型
* 使用注解注入
* 用在类上的注解
* Component : 相当于配置了一个bean标签 属性为value,即bean的id
** Controller 控制层
** Service 业务层
** Repository 数据层
* 用在注入数据上的
* 注入其他bean对象
** Autowired 按照类型注入,类型相同按照名称注入
** Qualifier 在自动按照类型注入的基础上按照id注入, value指定bean的id
** Resource 按照名称注入
* 注入基本数据类型
** Value()
* 用于改变作用范围的
* Scope
* 值和xml配置相同
* 把xml配置中的base-package扫描变成注解配置
* @Configuration
@ComponentScan(base-package={类})
* 扫描加载配置文件的类的注解
* Import({类})
* 加载配置文件地址
* PropertySource("classpath:加文件地址")
* 在spring4.3.5之前资源文件如果想通过Value注入,需要用el表达式,并且且配置SpringConfiguration类
配置静态PropertySourcesPlaceholderConfigurer方法,并作为Bean注入
* Qualifier作为参数注解,直接可以单独使用按照beanid匹配
* Junit原理:
即创建一个Test接口注解类,使用反射寻找对应方法执行。
* 注解类的生命周期:
* 自定义注解时需要添加Retenntion()注解
* 三种状态 : resource(源码) class(编译之后有的就没了) runtime(运行时有)
* 面向切面编程,因为使用oop就会存在很多事务处理的代码,安全和日志的一些代码的冗余,因为调用每个方法都要写这些处理的代码,所以引用了aop,使用动态代理模式,可以在不改变这些业务代码
的基础上增强这个方法,将事务处理等业务放到代理类中进行处理。
* xml配置
* 配置业务层的bean对象
* 配置通知类(增强的功能类),交由Spring管理
* 使用aop:config配置aop
* 使用aop:aspect配置切面
* 配置通知的类型,何时执行,增强的方法,切入点表达式
===================tag===========================
pointcut可以使用全通配方式 * *..*.*(..)
pointcut也可以通用,放到外面, 然后通过pointcut-ref="id" 进行依赖(必须写在切面之前)
通知类型:前置通知、后置通知、异常通知 、最终通知、环绕通知(Spring为我们提供的一种在代码中手动添加通知方式的方式)
环绕通知配置方法:
public Object around(ProceedingJoinPoint pjp) {
Object rtValue = null;
.......前置通知
rtValue = pjp.proceed();
后置通知、异常通知、最终通知
}
================================================
* 注解模式配置
* 在bean.xml中写 (全注解模式就需要加enableAspectjAutoProxy注解)
* 通知类加入Aspect注解,配置PointCut、通知方法根据通知类型加入对应的注解即可。
* 配置事务管理器
* 注入数据源
* 配置事务的通知
* 配置事务的属性
isolation: 配置事务的隔离级别。默认值:DEFAULT,使用数据库默认,mysql是可重复读,Orecal是读已提交事件
propagation: 配置事务的传播行为。默认值:REQUIRED。一般的选择。(增删改方法)。当是查询的方法,使用SUPPORTS
timeout: 配置事务的超时时间,默认-1
read-lony:配置是否为只读事务。默认值false,读写型事务。true,查询操作。
rollback-for: 指定一个异常,当执行产生该异常,事务回滚,其他不回滚,没有默认值,都回滚
no-rollback-for: 指定一个异常,当执行产生该异常,事务不回滚,其他回滚,没有默认值,都回滚
* 配置aop
* 配置切入点表达式
* 配置事务通知和切入点表达式的关联
* 开启Spring对事物注解的支持
* 使用TransactionManager注解即可。
* 由java代码编译生成.class字节码文件,然后通过jvm解释执行,生成对应平台的可执行代码。
* 创建类的实例(new)、访问类的静态变量,对静态变量进行赋值,调用静态方法时
* 反射
* 初始化子类,父类也被初始化
* jvm启动时被表明启动类的类,直接使用java.exe运行的某个主类
* 当使用jdk1.7动态语言支持时
* 使用类加载器
* 启动类加载器,加载一些rt.jar里面的class,由c++实现,不是ClassLoader的子类
* 扩展类加载器,加载一些扩展功能的jar
* 应用类加载器,加载classpath中制定的jar和目录中的class
* 当类加载器在加载某个类时,并不会先自己加载,而是将请求委托给父加载器加载。
* 作用就是防止内存中出现多份相同的字节码(安全性考虑)。
=======================tag=============================
类在加载一遍之后,会把得到的Class类的实例缓存起来,下次再请求加载,直接使用缓存中的实例,不会再次尝试加载。
=======================================================
* 加载: 加载类的二进制文件,在堆中创建Class类的对象
* 连接
* 验证: 验证文件格式、元数据、字节码、符号引用是否正确
* 准备: 对类的静态成员开辟内存,初始化为默认值
* 解析: 把类中的符号引用转为直接引用
* 初始化:为静态成员赋值
* JVM在加载class文件时,并不是所有的都是用解析器解析。而是对热点代码的字节码重新编译优化,生成机器码,由CPU直接执行,这样就提升了加载的效率。
所以对热点代码进行编译执行,其他代码解释执行。
* 热点代码就是多次调用的循环体和方法。
* 两种方式检测:1采样 2计数器 计数器分为方法调用计数器和回边计数器,当计数器超过阈值,则触发JIT即时编译。
* 为新生对象分配内存
* 由五部分组成
* 堆:分为新生代和老年代,存放对象的实例和常量池
* 虚拟机栈:里面保存了方法调用的栈帧,当前方法即栈顶元素,栈帧中保存了方法的局部变量表,操作数,动态链接,返回地址等。
* 本地方法栈:为虚拟机使用Native方法提供服务
* 方法区:存储已经被虚拟机加载的类元数据信息(元空间)
* 程序计数器:当前线程字节码执行的行号指示器
* jdk1.7后,常量池,运行时常量池,字符串常量池都位于堆中
* 常量池存放了字面量(用双引号定义的字面量都在里面)和符号引用(类和接口的全限定名,字段和方法的名称和描述)
* 字符串常量池只存字符串的引用,不存内容。
* 引用计数器(无法解决循环引用的问题)
* 可达性分析
! jvm在使用了一组OOpmap的数据结构来存储所有对象的引用,所以可以快速准确的完成GC ROOT的枚举。
* 标记清除:通过可达性分析,从GC ROOT开始枚举,对可达对象进行标记,没标记的对象直接清除(会产生内存碎片)
* 标记整理:标记部分与标记清除相同,整理部分让所有存活对象移到一边,直接清理端边界以外的内存。
* 复制算法:将内存分为两部分,把所有对象存到一边,将存货对象移到另一面,然后清除另一边。
* 分代回收:根据年轻代和老年代的特点,用不同的回收算法。老年代的生命周期较长,所以直接标记清除,年轻代生命周期短,使用复制算法。
* Serial回收器: 串行收集器是最古老也是最稳定和效率高的收集器,但会产生较长停顿,只能适用单线程环境
* ParNew收集器: Serial回收器的多线程版本
* Paraller收集器: 类似ParNew收集器,但更关注系统吞吐量
* Oaraller Old收集器,是Paraller收集器的老年代版本,使用多线程标记整理算法。
* CMS收集器: 为了获取最短回收的停顿时间,需要消耗额外的CPU和内存资源,采用的标记清除算法,会产生内存碎片
* G1收集器: 面向服务器的收集器,适用于多颗处理器及大容量内存的机器。可以同时兼顾停顿时间和高吞吐量的要求。
* 1.7之前有PermGen区(永久代),替换成了Metaspace(元空间)。
* 永久代中存储的字面量转移到了堆区,符号引用转移到了native heap区,类静态变量转移到了堆区。
* 元空间存储了类的元数据信息。
* 最大区别是元空间不在虚拟机中,而在本地内存中。
* 好处就是原先字符串存到永久代中,容易导致性能瓶颈或内存溢出,永久代为GC带来了不必要的复杂性。
* 一句话:如果一个对象是可达的(一直被引用),但是不会再被使用,就产生了内存泄漏。举例,set集合中存入Object对象,之后使object=null。则原onject对象一直被引用,但不会被使用了。
* 内存溢出出现的原因及解决方法:
* 是否出现了内存泄漏导致堆栈空间不断增大,导致溢出? 查程序,看是否有内存泄露问题。
* 是否加载了大量class和jar包,导致装载器空间不够,导致溢出? 增大虚拟机参数
* 是否操作了大量大的对象导致堆内存满了从而溢出? 看看有没有死循环或者循环产生大量重复对象
* nio直接操控内存,导致溢出? 看nio是否直接操控了内存
* 对象在surival的from和to区来回复制,复制15次直接进入老年代。
* 新对象大于elden区一半,直接进入老年代。
* surival区中大于某一年龄的对象超过surival区的一半,进入老年去。
* 一次minor gc之后,surival区还放不下,进入老年区
* 是不是频繁创建了大量大对象?
* 一次FULL GC之后老年带对象变化很多,就是1/2问题,新生代设置小了,变化不大,老年代设置小了。
自己定义一个ClassLoader,重写父类的loadClass方法。
* 父类的静态变量和静态代码块
* 子类的静态变量和静态代码块
* 父类的实例成员和实例代码块
* 父类的构造方法
* 子类的实例成员和实例代码块
* 子类的构造方法
* elden区不够放
* 当对象大小小于elden区1/2,存入elden区,存不下,就将elden区和surival from区数据放到to区,from变成to,如果surival区也放不下,放入老年区,还放不下,full gc。
* 当前请求的栈深大于虚拟机栈的栈深。
* 大量加载class,常量池溢出
* 强引用: 普通对象的创建都是强引用,只要被引用,回收器就不会进行回收
* 软引用: 非必须的引用,在内存溢出之前被回收,主要用在类似缓存的功能中。
* 弱引用:第二次垃圾回收器回收,主要用在监控对象是否已经被垃圾回收器标记为即将回收的垃圾。
* 虚引用:每次垃圾回收时都会回收,用于检测是否已经从内存中清除。
* 首先根据域名会让本地域名服务器尝试解析该域名,如果解析不了就传给根域名服务器,解析出服务器的ip地址,与对方ip地址进行TCP连接(三次握手协议)
连接成功后发送http请求,服务器收到请求之后返回响应,由浏览器渲染加载页面,(四次挥手协议)连接结束。
* TCP是面向连接的传输
* 端到端的通信
* 可靠性、能够确保传输数据的正确性、不会出现丢失和乱序问题
* 采用字节流方式,即以字节为单位传输字节序列
------------------------------------------------------------
* UDP是无连接的协议传输,数据之前源端与终端之间不建立连接
* 点到点的通信
* 字节开销小,传输的是数据报
* 吞吐量主要受应用软件生成数据的速率、传输宽带、源端和终端主机性能等因素的限制
* TCP适用于客户端服务器、文件传输,重要状态的更新。
* UDP适用于视频传输、即时通信、点到点的传输。
* TCP对应的协议:FTP Telnet SMTP POP3 HTTP
* UDP对应的协议:DNS SNMP TFTP
* OSI 7层:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层
* TCP\IC 四层:网络接口层、网际层、运输层、应用层
* 五层协议:物理层、数据链路层、网络层、传输层、应用层
* 每一层的协议:
* 物理层: IEEE802.3 RJ45, CLOCK(中继器、集线器)
* 数据链路层: PPP、FR、HDLC、VLAN、MAC (网桥、交换机)
* 网络层:IP、ICMP、ARP、RARP、OSPF、IPX、RIP、IGRP (路由器)
* 传输层:TCP、UDP、SPX
* 会话层:NFS、SQL、NETBIOS、RPC
* 表示层:JPEG、MPEG、ASII
* 应用层协议:FTP、DNS、Telnet、SMTP、HTTP、WWW、NFS
* 物理层:通过媒介传输比特,确定机械和电气规范
* 数据链路层:将比特组装成帧和点到点的传递(帧Frame)
* 网络层:数据包从源到宿的传递和网际互连(包Packet)
* 传输层:提供端到端的可靠报文传输和错误恢复(段Segment)
* 会话层:建立、管理和终止会话(会话协议数据单元SPDU)
* 表示层:对数据进行翻译、加密和压缩(表示协议数据单元PPDU)
* 应用层:允许访问OSI环境的手段(应用协议数据单元APDU)
* A类地址:以0开头、第一个字节范围0-126
* B类地址:以10开头、第一个字节范围128-191
* C类地址:以110开头,第一个字节范围192-223
* D类地址:以1110开头,第一个字节范围224-239
* E类地址:保留
* 每个主机都有一个ARP高速缓冲区用来保存IP地址和MAC地址的映射。当源主机要发送数据时,首先检查自己的缓冲区中是否有对应的IP地址的MAC地址,如果有,直接发送,如果没有,
就向本网段中所有的主机广播发送ARP数据包,该数据包包括源主机IP,源主机MAC,目的主机ARP,当收到ARP数据包之后检查IP地址是不是自己的IP地址,如果是,先取出数据包中的IP和MAC
存入自己的列表中,重复则覆盖,并发送自己MAC地址封装的响应包(单播),源主机收到后存入自己的缓冲区,并发送数据。
* ICMP:因特网控制报文协议,是TCP/IP协议的一个子协议,用于IP主机、路由器之间传递控制消息。
* TFTP: 是TCP/IP协议族中一个用来在客户机和服务器之间进行简单文件传输的协议,提供不复杂,开销不大的文件传输服务。
* HTTP: 超文本传输协议,属于应用层面向对象的协议,适用于分布式超媒体传输系统
* NAT:网络地址转换协议,属于接入广域网(WAN)技术,是一种将私有地址转化为合法的IP地址的技术
* DHCP:动态主机配置协议,是一种让系统得以连接到网络上,并获取所需配置参数的手段,使用UDP协议工作。具体用途:给内部网络或者网络服务供应商自动分配IP地址,给用户或者网络管理员
作为对所有计算机作中央管理的手段。
* 逆地址解析协议,作用是完成硬件地址到IP地址的映射,主要用在无盘工作站,因为给无盘工作站配置IP地址不能保存。工作流程:在网络中配置一台RARP服务器,保存MAC和IP的映射关系,
当无盘工作站启动,就封装一个RARP数据包,里面有其MAC地址,然后广播到网络,当服务器收到请求包,就查看对应MAC地址的IP地址装入响应报文,发回请求者。因为需要广播发送报文,所以RARP只能
用在具有广播能力的网络。
* 三次握手: 1服务器建立传输控制快TCB,准备客户端的连接请求,进入监听状态
2客户端建立传输控制快TCB,向服务端发送连接请求报文,进入发送状态(SYN=1 seq=x)
3服务器收到连接请求报文,同意连接,发送确认报文,进入同步收到状态(ACK=1 SYN=1 ack=x+1 seq=y)
4客户端收到服务器的确认,向服务器给出确认,发送确认报文,TCP连接(ACK=1 ack=y+1 seq=x+1)
5服务器收到确认进入连接状态
* 四次挥手: 1客户端发送连接释放报文,停止发送数据,进入终止等待1状态(FIN=1 seq=u:前一个传送数据的最后一个字节的序号+1)
2服务器收到连接释放报文,发送了确认报文,进入了关闭等待状态(ACK=1 ack=u+1 seq=v)
3客户端收到确认报文,进入终止等待2状态
4服务器发送完最后的数据,发送连接释放报文,进入最终确认状态(FIN=1 ack=u+1 seq=w)
5客户端收到服务器的连接释放报文后发出确认,进入时间等待状态(ACK=1 ack=w+1 seq=u+1) 开启保活计时器2mls时间
6服务端收到客户端的确认请求,进入close状态,撤销传输控制快TCB,断开连接。
* 握手为什么是三次?
一句话:防止失效的连接请求发送到服务器,产生错误连接,浪费资源
* 挥手为什么是四次?三次不行吗?
客户端发送了连接释放报文后,停止了数据的发送,但是服务端还有数据没发完,所以服务端的响应ACK和FIN分两次,所以一共要进行四次。
* BIO是面向流的、阻塞IO : 只能通过一个字节一个字节进行读取,通过输入流产生一个字节数据,输出流消费一个字节数据,字节处理速度缓慢。并且在调用wirte和read方法时线程阻塞,直到所有数
据被写入或者有数据被读取,阻塞期间不能进行其他操作。
* NIO是面向缓冲区的,非阻塞同步IO:除去boolean外,其他基本类型都有对应的Buffer对象来存取数据。通道使用write和read用来接收和发送缓冲区的数据。通道注册在选择器上,用来管理通道,采用多路复用技术,轮询选择一个
当前准备就绪的通道。以块的形式读取数据,每次产生一个数据块或者消费一个数据块,数据处理速度快。当线程从通道中读取数据时,只会读到当前已有的数据或者读不到数据,写入数据时不需要等到全部写入。
期间线程可以去执行其他操作,不会阻塞。
* AIO是异步非阻塞IO,真正实现了异步IP请求,将IO请求从应用程序交由操作系统完成,完成后返回通知。
* 首先,阻塞与不阻塞应该看write和read方法,NIO的write和read方法是交给通道去执行,而通道是注册在selector上的,选择器是单线程去轮询寻找通道的变化,当read操作时,会读取当前已经存在的数据,
没有数据,也会返回,写操作也不会等到全部写入之后再返回,期间,线程可以执行其他操作。并且select操作可以通过设置select(100)等设置阻塞时间,也可以调用wakeup使得select不是阻塞的。
* 产生原因就是客户端和服务器没有约定好一个稳定的数据结构,导致服务器一次性读取全部buffeer中的数据。
* Netty中首先要确定一个稳定的数据结构,可以是length+str,然后让服务端的Decode继承自FrameDecoder,首先判断buffer数据的长度,长度不足就返回null(FrameDecoder内维护了一个缓存数组,会缓存
未读取完的数据)。长度够了在读取指定长度的数据,剩下的还是会进入缓存。
* 动态代理模式是在不改变原有方法的业务代码的基础上对原方法进行增强。
* 基于接口的动态代理:
要求: 被代理类至少实现一个接口
涉及类:Proxy
创建代理对象的方法:newProxyInstance(ClassLoader, Class[], InvocationHandle)
* 参数的含义:
* ClassLoader: 被代理对象的类加载器
* Class[]: 字节码数组,被代理对象实现的接口
* InvocationHandle: 是一个就扣,一般用这个接口的实现类,用于提供增强代码。
含义是:如何进行代理:使用了设计模式的策略模式:
策略模式的使用要求:
数据已经有了
目的明确
达成目标的过程不相同
* 基于子类的动态代理:
要求: 被代理类不能是最终类,不能被final修饰
涉及类: Enhancer(使用第三方CGLIB)
创建代理对象的方法: create(Class, Callback)
* 参数的含义:
* Class:被代理对象的字节码
* Callback: 如何进行处理,也是一个接口,一般用子接口MethodInterceptor.
* 单例模式就是程序执行过程自始至终只有一个类的实例。
* 单例模式根据创建时间不同分为懒汉式,饿汉式,双重校检锁三个版本。
* 创建就是: 构造函数私有,创建静态的公有获取实例的方法。
* 懒汉式:即用即加载,一开始不实例化,等到调用公有方法在进行实例化。
* 饿汉式:加载类时进行实例化。
* 双重校验锁:懒汉式的基础上加双重检查
* 创建型
* 工厂方法模式
* 抽象工厂模式
* 建造者模式
* 单例模式
* 原型模式
* 行为型
* 观察者模式
* 策略模式
* 责任链模式
* 状态模式
* 命令模式
* 中介者模式
* 结构型
* 装饰者模式
* 适配器模式
* 享元模式
* 代理模式
* 桥接模式
* 组合模式
* 观察者模式定义了一种一对多的依赖关系,观察目标发生改变,所有被观察者都收到通知。
* 包含角色
* 抽象主题 : 定义了一些被观察者统一的接口,包括增删提醒观察者
* 具体主题: 实现了抽象被观察者的接口方法。
* 抽象观察者:为所有观察者角色定义的抽象类,在收到通知的时候更新自己
* 具体观察者: 抽象观察者的实现
* SOLID
* 单一职责原则
* 开闭原则
* 里氏替换原则
* 接口隔离原则
* 依赖倒置原则
* 最大区别就是编译不同,C++会直接编译成可执行代码,即exe文件。而Java因为有跨平台的特性,所以会先编译生成字节码文件,
即class文件,然后根据系统环境的不同由jvm生成对应的机器码,即可执行文件。
* 第二个大的不同点就是垃圾回收机制的不同,在C时期,还是malloc和free对内存直接操作,C++开始用new和delete,虽然把垃圾回收做到了语言的层面,但是
Java提供了自动垃圾回收的机制,可以通过回收算法和回收期,自动对不用的对象进行回收。
* 第三个大的不同点就是C++支持多继承,但是Java不支持多继承,只允许类进行单继承,但是可以继承多个接口
* 还有Java只有引用,没有指针
byte 1,short 2, int 4, float 4, long 8, double 8, char 2 boolean未知,Interger等包装类为对象,所以大小为四个字节(对象的内存地址的大小为4个字节)
* 封装
* 继承
* 多态
* 方法的重载,重写,运行时的动态调用(实际就是重写)
* 当实现Serializabled接口对对象进行序列化操作时,用transient修饰的域可以不进行持久化操作。
* 枚举里面只有同类型的属性变量,而结构体可以有结构函数
为了适用集合等场景, Java对于每种基本类型都有对应的包装类,从JDK5之后就可以进行自动拆装箱,即将基本类型与对应的对象类型之间的自动转换。
* 类型不同,Java封装了地址,可以转成字符串查看,C++的指针为一个存地址的变量,为int值, 一般长度为计算机字长。
* 所占内存不同, Java的引用没有实体,不占内存,C++的指针声明之后才会赋值,不用不会分配内存。
* 初始值不同, Java的引用初始值为null, C++的地址初始值为int,如果不改值就很危险。
* 计算, 引用不可以计算,指针可以计算进行++ -- 操作。
* 控制能力不同, 引用不能计算,所以可以被控制,C++的地址可以计算,所以就可能指向不是与自己程序的内存,不容易被控制。
* 内存泄漏, Java引用不存在内存泄漏问题,C++指针容易产生内存泄漏,所以用后要及时回收。
* 作为方法参数: Java方法参数只是传值,引用做参数时穿的是引用的COPY值(不是对象的copy值), 更改引用的属性同时会更改对象的属性值,C++指针直接指向内存对象,直接更改对象值。
总结: Java引用和C++的指针都是想通过一个机制找放操作的目标,Java的引用算是对指针的封装,灵活性降低,安全性提高。
==对基本类型来说就是比较值是否相同,对于对象来说比较对象的地址是否相同,所以对象要比较值是否相同就要用到equals方法。
静态是属于类的,非静态是属于类的实例的。当类被加载的时候,静态方法,静态代码块,静态成员都会被初始化,会与类来说,他们只有一份,所有类实例都能访问他们。
* 相同点是都不能直接被实例化
* 最大不同是一个实例类只能继承自一个抽象类,但是可以实现多个接口。
* 第二个不同是抽象类中可以有属性和抽象方法和非抽象的方法,而接口中的所有方法都是抽象的,可以有静态变量值,但是静态变量值不能修改。
* 接口一般用来实现某种特性,抽象类就是一个类的抽象实现。
* 静态内部类: 相当于外部类的静态成员,使用static修饰,隶属于外部类,使用起来可以独立使用。
* 成员内部类: 相当外部类的一个成员,隶属于外部类的具体对象,创建时先创建外部类的对象才能创建成员内部类的实例。
* 局部内部类: 定义在一个方法的方法体中,一般用于执行某方法是才使用,只能调用final修饰的局部变量。
* 匿名内部类: 定义在方法体中,灵活性强,没有名字,工作本质与局部内部类相似。
* Java类中有四种访问控制符,分别为private(只能被本类访问和修改)、protect(能被自身类,子类,同包下的类访问和修改)、public(能被所有其他类访问)、默认的friendly(同包类访问)
* 注意: 访问控制只停留在编译成,即只在编译层检查,在运行时可以通过反射访问所有的成员包括private修饰的成员。
* 运用设计模式中享元模式,字符串在定义时,如果用""直接定义字面量,jvm会在常量池中检查是否存在该字面量,如果存在,直接返回其引用,如果不存在,在常量池中创建对应字面量,并返回引用。
常量池的出现为了防止频繁地创建和销毁对象。
* String对象有一个特性,就是不变性,只能被创建,不能被修改。如果对String对象频繁进行加减操作,效率很低,会出现性能瓶颈,甚至内存溢出。所以提供了StringBuffer和StringBuilder来对
String进行操作,StirngBuffer是线程安全的,如果涉及线程安全问题,应该用StringBuffer,而StringBuilder不是线程安全的。
* Java中,一个数组即一个对象,通过new创建,通过[下标访问这些数据。所以new Object[5]并没有创建5个对象,只是创建了一个数组实例,长度为五,每个元素都为null,当为每个数
组元素赋值之后才是创建了5个对象。
* Collection、Set、List、Map
* 和具体实现相关,应该由具体实现来决定如何进行克隆和序列化。
* Iterator可以用来遍历List和Set集合,但是ListIterator只能用来遍历List集合。Iterator只能通过next进行单向遍历(前向),而ListIterator可以进行双向遍历,并提供了添加元素,替换元素
,获取索引等操作。
* Enumeration接口速度和内存占用都比Iterator小,但是不是线程安全的。而Iterator对集合进行操作时不允许其他线程修改该集合,是线程安全的。并且允许调用者删除底层元素。
* java.util包下的集合类都是快速失败的。即如果在遍历过程中对集合元素进行了删除或修改,就会导致exceptedModCount!= ModCount,导致ConcurrentModifiedExcption异常出现,
但是用iterator的remove方法则不会导致该异常出现,因为该方法执行后会将ModCount的值赋值给exceptedModCount。所以这些集合不能进行多线程的并发访问和修改。
* java.concurrent包下的集合都是安全失败的。即在进行遍历操作之前,并不会直接遍历集合元素,而是先把集合元素复制一份,遍历copy集合,所以修改时与遍历的集合不是同一个,
所以不会产生异常,但是不能遍历到修改后的值,可以在多线程环境中使用。
* 如果想让一个集合是可比较的,可以让集合实现Comparable接口,并重写compareTo()方法,也可以在外部通过匿名内部类的方式实现Comparator接口,有两个重写方法:conpare(和compareTo相同),
equals(判断两个comparator是否相同)。
* 两者都通过对象数组的方式存储元素,但Vector对大多数方法加了synchronized锁,保证了线程的安全,但是降低了效率。所以Vector是线程安全的,效率不高。ArrayList不是线程安全的,效率高。
* 一般认为,HashTable是一个遗留类,HashTable的方法是同步的,是线程安全的,而HashMap不是。
* HashTable不允许键值为空,HashMap可以有空的键或值。
* HashTable使用Enumeration,HashMap使用Iterator。
* HashTable直接使用对象的hashcode,而HashMap会重新计算hash值。
* HashMap存放了key-value的键值对。在jdk1.8之前采用了数组+链表的方式存放,jdk1.8之后采用数组+链表+红黑树。
* HashTable重要参数: 负载因子(默认0.75),容量, 阈值
* 其中数组为一个Node[]节点数组,也叫hash桶节点,默认16个。里面存放了key-value键值对节点。HashMap用hash表存储,采用链地址法来处理冲突。
* put方法即先判断hashmap是否为空,空就扩容,然后根据key的hash值i去到table[i]中,判断key是否相等,相等则覆盖,不相等则判断是否是红黑树节点,如果是,直接插入键值对,
如果不是,则遍历链表,key相等则覆盖,链表长度大于8则转为红黑树。放入之后看是否超过阈值,超过则扩容为原来size的两倍。
* 为什么扩容都是2的幂: 因为在计算插入元素在hash表中的位置的时候,我们希望能够使得节点分布平均,但是采用模运算消耗巨大,所以就采用和hash值与(length-1)做与运算的方式,如果length总是
2的幂,length-1的所有二进制位都是1,使得节点平均分布。如果不是2的幂,肯定存在有的二进制位是0,那么无论和谁与结果都是0,这也个位置就永远不可能存节点了。
* 在很多地方都需要用到hashcode(),hashcode()用来定位元素的位置,equals()方法来判断两个元素是否相等。在HashMap中,先用key的hashcode来定位到hash表的位置,然后用equals比较是否相等,
相等则覆盖。
* Array可以存放基本类型,也可以存放对象类型,而ArrayList只能用来存放对象类型。
* Array是定长的,ArrayList是自动扩容的。
* ArrayList有更多的操作方法,例如addAll, removeAll等。
* 存储结构不同, ArrayList是基于索引的数据接口, 底层是数组, Linklist底层是链表。
* ArrayList可以在O(1)内对元素进行随机访问,LinkList为O(n),且不支持随机访问,但是增删改方便。
* ListList占内存更多,因为一个节点存了两个引用。
* HashSet采用hash表,因此元素无序,添加删除操作复杂度O(1)。
* TreeSet采用属性结构,有序,添加删除操复杂度O(logn)。
* PriorityQueue是一个基于优先级堆的无界队列,元素按照自然顺序排序,创建的时候可以给他提供一个负责排序的元素比较器,不允许null值,不是线程安全的,入队和出队时间复杂度O(logn)。
* 11个, hashcode()、equals()、 notify()、notifyAll()、wait()、wait(long)、 wait(long, int)、 finalized()、clone()、toString()、getClass()
* 序列化本质上就是把对象内存中的数据按照一定的规则,变成一系列的字节数据,写入流中。需要实现Serializable接口,必要时需要提供seriaVersionUid(为了区分两个相同类名的类)。
* 反序列化与之过程相反,但是如果不想对某个域对象进行序列化,需要用transient关键字修饰。
* 方法很多,最重要的一个就是反射。
* 反射就是为了能够在运行时动态加载一个类,动态调用一个方法,动态访问一个属性。因为JVM在加载类时会在堆中为每个类创建一个Class类的实例,通过这个对象可以获取类的全部信息。
* Class.forName("类路径");
* 类名.class
* 对象.getClass()。
//获取类对象
Class clazz = Class.forName("User");
//获取类实例(无参构造)
User user = (User) clazz.newInstance();
//带参数构造
Constructor constructor = clazz.getConstructor(String.class, String.class);
User user1 = (User) constructor.newInstance("admin","123");
//获取对象的属性
Field username = clazz.getDeclaredField("username");
//修改对象的属性
username.setAccessible(true);//如果属性为私有,则要设置为true
username.set(user1,"admini");
//获取类的普通方法
Method setUsername = clazz.getMethod("setUsername", String.class);
//设置user1的username为aaa(如果是静态方法,实例对象为null)
setUsername.invoke(user1,"aaa");
jre是Java运行时环境,包括虚拟机和核心类库和支持文件
jdk是开发工具包, 包含jre,编译器、调试器和其他工具。
* static为静态,表示成员变量或者成员方法可以在没有类实例的情况下被访问。
* static方法不能被覆盖,因为方法覆盖是运行时绑定的,static实在编译时静态绑定的,而且static不与任何类的实例相关联。
* private方法不能被覆盖,只能本类使用,子类获取不到。
* 很明显不行,static方法会在类加载时初始化,那时候没有任何类的实例,就不存在任何非静态的属性,就会报错。
* 方法重载发生在同一个类中,相同方法名,参数列表不同。
* 方法覆盖发生在继承关系里面,子类重写了父类的方法,方法名、参数列表,返回值都要相同。
* 构造方法就是为了初始化类的对象的,方法名和类名相同。
* 构造方法重载和普通的方法重载类似,相同方法名,不同参数列表。
* Java中不存在拷贝构造函数的概念。
* 受检查的异常,可以通过throws抛出异常,或者try-catc捕获异常,通常可以处理。
* 不受检查的异常,运行时异常,Java编译器不会去检查他,通常是执行了错误操作(/0)。
=================tag======================
除去异常(实现Thorwable接口)之外还有一类为Error,通常是虚拟机或动态链接等出现错误导致的。
==========================================
* throw在方法内部,后面只能跟一个异常对象。
* throws用在方法声明上,后跟多个异常类型。
* 没有任何相同点。
* finally无论出不出现异常都会执行,主要是一些清理工作。
* finalize()方法为Object方法,负责垃圾回收之前的清理工作。
* 200 成功
* 302 重定向 响应头包含Location
* 304 调用缓存 响应头中有Last-Modify最后修改时间,如果请求头中的If-Modify与LM相同,则直接调用缓存中数据
* 404 客户端资源搜索不到
* 500 服务器解析错误
* 包含请求行、请求头、空行、请求体(post)
* refer:请求头,包含网页来源信息,用于防盗链
* refresh: 刷新 格式"num;url"
* WEB-INF
* web.xml
* classes
* lib
* bin 二进制运行文件,包含startup.exe 和 shutdown.exe
* conf 配置文件, web.xml server.xml
* lib jar包
* temp 临时文件
端口
<引擎>
主机
引擎>
* driver
* url
* name
* password
1. 加载驱动类
Class.forname("com.mysql.jdbc.Driver");
!加载驱动类会执行类的静态代码块,Driver的静态代码块为DirverManager.regist(new Driver());即注册这个Driver
* 生成连接对象
Connection connection = DirverManager.getConnection("url","name","pass");
* 通过connection对象获取预处理对象
Statment stat = connection.createStatment();
PreraredStatment stat = connection.createPreraredStatment(); 通过sql模板加参数,防止sql注入
* 执行sql语句execute
* 处理结果集 ResultSet
* 关闭事务自动提交
con.setAutoCommit(false);
* 如果事务执行成功commit,执行失败rollback。
* A原子性: 事物不可再分,要么全部执行成功,要么全部执行失败
* C一致性: 事务执行前后业务逻辑不变
* I隔离性: 事务的并发访问不受影响
* D持久性: 事物执行完成后数据库数据不会变,不管发生任何问题都不变
* 脏读: 读到另一个事务的未提交更新数据
* 不可重复读: 对同一记录的两次读取不同,因为其他事物修改了该记录
* 幻读: 对同一张表的两次查询不同,因为另一个事务插入了数据
* 串行化:效率最低,不可能出现并发问题, 容易死锁
* 可重复度: Mysql默认,防止了脏读和不可重复读
* 读已提交数据: Oracle默认, 防止脏读
* 读未提交数据: 出现事务并发问题
* 参数
* 初始大小
* 最小空闲数
* 最大空闲数
* 最长等待时间
* 最大连接数
* 增量
1. dbcp(装饰者模式) BasicDateSource dbcp = new BasicDateSource();
2. c3p0(动态代理) ComboPooledDataSource c = new ComboPooledDataSource();
3. JNDI配置
* 去Tomcat配置
* Context context = new InitContext();
* context.lookup("java:comp/env/jdbc/datasource");
* 三大域对象,每个对象都有一个生命周期监听器和属性监听器(ServletContext, HttpSession, HttpServletRequest)
* JavaBean监听器
* JavaBeanSession钝化、活化监听器(实现序列化接口)
* 获取初始化参数
* 获取ServletContext
* 获取ervlet名称
* 请求、转发、包含、错误
* post
* encrypt="multipart/form-data"
* 获取工厂 DiskFileItemFactory
* 获取解析器 ServletFileUpload
* 获取FILEiTEM列表
* content-Type
* content-Disposition(inline, attachment;filename=xxx)
* SMTP(发)
* POP3(收)
* IMAP(收发)
* mail.jar
* activition.jar
* Session(Prop, Auth)
* MimeMessage
* Transport
* 创建XMLHttpRequest对象
try{
return new XMLHttpRequest();
}catch(e) {
try{
return new ActiveXObject("Msxml2.XMLHTTP");
} catch(e) {
try{
return new ActiveXObject("Microsoft.XMLHTTP");
}catch(e) {
alert("哥们,你用的什么浏览器呀?");
e;
}
}
}
* 打开与服务器的连接
xmlHttp.open("请求方式","url", "true");请求是否为异步
* xmlHttp.send(null);//请求体
* 监听事件 (onreadystatechange)
* 0 初始化,还没调用open
* 1 请求开始,调用了open方法,没调用send方法
* 2 调用了send方法
* 3 服务器已经开始响应,但不表示响应结束了
* 4 完成响应
重点!!!!重点!!!!分界线!!!!!!!!重点!!!!!!重点!!!!
内推
简历发邮箱 [email protected] !!!
简历发邮箱 [email protected] !!!
简历发邮箱 [email protected] !!!