Java面试题

OOP的理解

类,对象,还有面向对象的三大特征:继承,多态和封装。

Java 泛型的好处及实现原理

泛型好处:

泛型简单易用

类型安全 泛型的主要目标是实现java的类型安全。 泛型可以使编译器知道一个对象的限定类型是什么,这样编译器就可以在一个高的程度上验证这个类型

消除了强制类型转换 使得代码可读性好,减少了很多出错的机会

Java语言引入泛型的好处是安全简单。泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

泛型的实现原理

泛型的实现是靠类型擦除技术 类型擦除是在编译期完成的 也就是在编译期 编译器会将泛型的类型参数都擦除成它的限定类型,如果没有则擦除为object类型之后在获取的时候再强制类型转换为对应的类型。 在运行期间并没有泛型的任何信息,因此也没有优化。

什么是线程池?

线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。如果每个请求都创建一个线程去处理,那么服务器的资源很快就会被耗尽,使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。

常见线程池

newSingleThreadExecutor
单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务
newFixedThreadExecutor(n)
固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行
newCacheThreadExecutor(推荐使用)
可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。
newScheduleThreadExecutor
大小无限制的线程池,支持定时和周期性的执行线程

几个常用的操作系统进程调度算法

一、先来先服务和短作业(进程)优先调度算法


1.先来先服务调度算法

2.短作业(进程)优先调度算法

二、高优先权优先调度算法


1.优先权调度算法的类型

2.高响应比优先调度算法

三、基于时间片的轮转调度算法


1.时间片轮转法

2.多级反馈队列调度算法

Java线程池七个参数详解

一、corePoolSize 线程池核心线程大小

二、maximumPoolSize 线程池最大线程数量

三、keepAliveTime 空闲线程存活时间

四、unit 空闲线程存活时间单位

五、workQueue 工作队列

六、threadFactory 线程工厂

七、handler 拒绝策略

线程池的三种工作队列

SynchronousQueue

     无缓冲无界等待队列,超出核心线程个数的任务时,创建新的线程执行任务,直到线程数达到最大线程数,触发拒绝策略,可缓存任务数:0

ArrayBlockingQueue

     基于数组的先进先出的有界队列,超出核心线程个数的任务时,将任务加入在此数组,数组大小为创建时指定大小,当任务队列塞满时,创建新的线程执行任务,直到线程数达到最大线程数,触发拒绝策略,可缓存任务数:创建时指定队列大小

LinkedBlockingQueue

     基于链表的先进先出可选有界队列,超出核心线程个数的任务时,将任务加入在此队列,队列大小为创建时指定大小不指定时为Integer.MAX_VALUE,当任务队列塞满时,创建新的线程执行任务,直到线程数达到最大线程数,触发拒绝策略,可缓存任务数:默认为Integer.MAX_VALUE否则为创建时指定队列大小

线程池的四种拒绝策略

AbortPolicy:ThreadPoolExecutor中默认的拒绝策略就是AbortPolicy。直接抛出异常也不处理

CallerRunsPolicy:CallerRunsPolicy在任务被拒绝添加后,会调用当前线程池的所在的线程去执行被拒绝的任务

DiscardPolicy:采用这个拒绝策略,会让被线程池拒绝的任务直接抛弃,不会抛异常也不会执行

DiscardOldestPolicy:DiscardOldestPolicy策略的作用是,当任务呗拒绝添加时,会抛弃任务队列中最旧的任务也就是最先加入队列的,再把这个新任务添加进去

线程的五大状态及其转换

线程的五大状态分别为:创建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)、死亡状态(Dead)。

1)创建状态:即单纯地创建一个线程,创建线程有三种方式,

2)就绪状态:在创建了线程之后,调用Thread类的start()方法来启动一个线程,即表示线程进入就绪状态!

3)运行状态:当线程获得CPU时间,线程才从就绪状态进入到运行状态!

4)阻塞状态:线程进入运行状态后,可能由于多种原因让线程进入阻塞状态,如:调用sleep()方法让线程睡眠,调用wait()方法让线程等待,调用join()方法、suspend()方法(它现已被弃用!)以及阻塞式IO方法。

5)死亡状态:run()方法的正常退出就让线程进入到死亡状态,还有当一个异常未被捕获而终止了run()方法的执行也将进入到死亡状态!

 

什么是线程安全?如何保证线程安全?

对线程安全的定义:

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

多线程编程中的三个核心概念:

原子性

可见性

有序性

实现线程安全:

第一种 : 互斥同步

最常使用的就是Synchronized 关键字。

synchronized实现同步的基础就是:Java中的每一个对象都可以作为锁。

具体表现为:1.普通同步方法,锁是当前实例对象

                      2.静态同步方法,锁是当前类的Class对象

                      3.同步方法块,锁是Synchronized括号里匹配的对象

第二种方法就是:非阻塞同步

先进行操作,如果没有其他线程争用共享数据,那么操作就成功了,如果共享数据有争用,就采取补偿措施(不断地重试)。

CAS是实现非阻塞同步的计算机指令,它有三个操作数:内存位置,旧的预期值,新值,在执行CAS操作时,当且仅当内存地址的值符合旧的预期值的时候,才会用新值来更新内存地址的值,否则就不执行更新。

     使用方法:使用JUC包下的整数原子类decompareAndSet()和getAndIncrement()方法

  缺点 ABA 问题  版本号来解决

  只能保证一个变量的原子操作,解决办法:使用AtomicReference类来保证对象之间的原子性。可以把多个变量放在一个对象里。

第三种:无同步方案

将共享数据的可见范围限制在一个线程中。这样无需同步也能保证线程之间不出现数据争用问题。

经常使用的就是ThreadLocal

其实引起线程不安全最根本的原因 就是 :线程对于共享数据的更改会引起程序结果错误。线程安全的解决策略就是:保护共享数据在多线程的情况下,保持正确的取值。

进程间通信的五种方式

管道、系统IPC(信号量、消息队列、共享内存)和套接字(socket)。

链接

进程和线程的主要区别

根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位

在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)

内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。

包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

进程间切换与线程间切换的区别

进程切换分两步

1.切换页目录以使用新的地址空间
2.切换内核栈和硬件上下文。

对于linux来说,线程和进程的最大区别就在于地址空间。
对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。 所以明显是进程切换代价大

如何在Java实现线程?

由于线程类本身就是调用的Runnable接口所以你可以继承java.lang.Thread 类或者直接调用Runnable接口来重写run()方法实现线程。

通过CallableFuture接口创建线程

通过这两个接口创建线程,你要知道这两个接口的作用,下面我们就来了解这两个接口:通过实现Runnable接口创建多线程时,Thread类的作用就是把run()方法包装成线程的执行体,那么,是否可以直接把任意方法都包装成线程的执行体呢?从JAVA5开始,JAVA提供提供了Callable接口,该接口是Runnable接口的增强版,Callable接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大,call()方法的功能的强大体现在:

1call()方法可以有返回值;

2call()方法可以声明抛出异常;

从这里可以看出,完全可以提供一个Callable对象作为Threadtarget,而该线程的线程执行体就是call()方法。但问题是:Callable接口是JAVA新增的接口,而且它不是Runnable接口的子接口,所以Callable对象不能直接作为Threadtarget。还有一个原因就是:call()方法有返回值,call()方法不是直接调用,而是作为线程执行体被调用的,所以这里涉及获取call()方法返回值的问题。

于是,JAVA5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该类实现了Future接口,并实现了Runnable接口,所以FutureTask可以作为Thread类的target,同时也解决了Callable对象不能作为Thread类的target这一问题。

Runnable还是Thread

通过继承Thread类实现多线程:

优点:

1、实现起来简单,而且要获取当前线程,无需调用Thread.currentThread()方法,直接使用this即可获取当前线程;

缺点:

1、线程类已经继承Thread类了,就不能再继承其他类;

2、多个线程不能共享同一份资源(如前面分析的成员变量 i );

通过实现Runnable接口或者Callable接口实现多线程:

优点:

1、线程类只是实现了接口,还可以继承其他类;

2、多个线程可以使用同一个target对象,适合多个线程处理同一份资源的情况。

缺点:

1、通过这种方式实现多线程,相较于第一类方式,编程较复杂;

2、要访问当前线程,必须调用Thread.currentThread()方法。

Thread 类中的start() 和 run() 方法有什么区别?

start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。

用户态切换到内核态主要有三种方式

a. 系统调用
这是用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,比如前例中fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,例如Linuxint 80h中断。
b. 异常
CPU在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,比如缺页异常。
c. 外围设备的中断
当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。
3种方式是系统在运行时由用户态转到内核态的最主要方式,其中系统调用可以认为是用户进程主动发起的,异常和外围设备中断则是被动的。

索引底层实现原理

索引(Index)是帮助MySQL高效获取数据的数据结构,这些数据结构以某种方式引用(指向)数据。

链接

什么情况下应不建或少建索引

表记录太少

经常插入、删除、修改的表

数据重复且分布平均的表字段

三大范式

第一范式(1NF):

字段具有原子性,不可再分。所有关系型数据库系统都满足第一范式) 数据库表中的字段都是单一属性的,不可再分。例如,姓名字段,其中的姓和名必须作为一个整体,无法区分哪部分是姓,哪部分是名,如果要区分出姓和名,必须设计成两个独立的字段。

第二范式(2NF):

第二范式(2NF)是在第一范式(1NF)的基础上建立起来的,即满足第二范式(2NF)必须先满足第一范式(1NF)。
要求数据库表中的每个实例或行必须可以被惟一地区分。通常需要为表加上一个列,以存储各个实例的惟一标识。这个惟一属性列被称为主关键字或主键。

第二范式(2NF)要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性,如果存在,那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体,新实体与原实体之间是一对多的关系。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。简而言之,第二范式就是非主属性非部分依赖于主关键字。

第三范式(3NF):

满足第三范式(3NF)必须先满足第二范式(2NF)。简而言之,第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。
所以第三范式具有如下特征:
1,每一列只有一个值
2,每一行都能区分。
3,每一个表都不包含其他表已经包含的非主关键字信息。

Hash索引和B+树索引有什么区别或者说优劣势?

1Hash索引不能进行范围查询,而B+树可以。

这是因为Hash索引指向的数据是无序的,而B+ 树的叶子节点是个有序的链表。

2Hash索引不支持联合索引的最左侧原则(即联合索引的部分索引无法使用),而B+树可以。

对于联合索引来说,Hash索引在计算Hash值的时候是将索引键合并后再一起计算Hash值,所以不会针对每个索引单独计算Hash值。因此如果用到联合索引的一个或多个索引时,联合索引无法被利用。

3Hash索引不支持Order BY排序,而B+树支持。

因为Hash索引指向的数据是无序的,因此无法起到排序优化的作用,而B+树索引数据是有序的,可以起到对该字段Order By 排序优化的作用。

4Hash索引无法进行模糊查询。而B+ 树使用 LIKE 进行模糊查询的时候,LIKE后面前模糊查询(比如%开头)的话可以起到优化的作用。

B树与B+树的区别

  1. B树每个节点都存储数据,所有节点组成这棵树。B+树只有叶子节点存储数据(B+数中有两个头指针:一个指向根节点,另一个指向关键字最小的叶节点),叶子节点包含了这棵树的所有数据,所有的叶子结点使用链表相连,便于区间查找和遍历,所有非叶节点起到索引作用。
  2. B树中叶节点包含的关键字和其他节点包含的关键字是不重复的,B+树的索引项只包含对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
  3. B树中每个节点(非根节点)关键字个数的范围为[m/2(向上取整)-1,m-1](根节点为[1,m-1]),并且具有n个关键字的节点包含(n+1)棵子树。B+树中每个节点(非根节点)关键字个数的范围为[m/2(向上取整),m](根节点为[1,m]),具有n个关键字的节点包含(n)棵子树。
  4. B+树中查找,无论查找是否成功,每次都是一条从根节点到叶节点的路径。

B树的优点

B树的每一个节点都包含keyvalue,因此经常访问的元素可能离根节点更近,因此访问也更迅速。

B+树的优点

单次请求涉及的磁盘IO次数少(出度d大,且非叶子节点不包含表数据,树的高度小);

查询效率稳定(任何关键字的查询必须走从根结点到叶子结点,查询路径长度相同);

遍历效率高(从符合条件的某个叶子节点开始遍历即可);

Mysql 存储引擎的区别和比较

InnoDB :如果要提供提交、回滚、崩溃恢复能力的事务安全(ACID兼容)能力,并要求实现并发控制,InnoDB是一个好的选择

MyISAM:如果数据表主要用来插入和查询记录,则MyISAM(但是不支持事务)引擎能提供较高的处理效率

Memory:如果只是临时存放数据,数据量不大,并且不需要较高的数据安全性,可以选择将数据保存在内存中的Memory引擎,MySQL中使用该引擎作为临时表,存放查询的中间结果。数据的处理速度很快但是安全性不高。

Archive:如果只有INSERTSELECT操作,可以选择ArchiveArchive支持高并发的插入操作,但是本身不是事务安全的。Archive非常适合存储归档数据,如记录日志信息可以使用Archive

什么是事务?

指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。

A:原子性(Atomicity
    事务中的操作要么都不做,要么就全做。
C:一致性(Consistency
    事务执行的结果必须是数据库从一个一致性状态转换到另一个一致性状态。
I:隔离性(Isolation
    一个事务的执行不能被其他事务干扰
D:持久性(Durability
    一个事务一旦提交,它对数据库中数据的改变就应该是永久性的

事务的隔离级别

第一种隔离级别:Read uncommitted(读未提交)
如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据,该隔离级别可以通过排他写锁,但是不排斥读线程实现。这样就避免了更新丢失,却可能出现脏读,也就是说事务B读取到了事务A未提交的数据

解决了更新丢失,但还是可能会出现脏读

第二种隔离级别:Read committed(读提交)
如果是一个读事务(线程),则允许其他事务读写,如果是写事务将会禁止其他事务访问该行数据,该隔离级别避免了脏读,但是可能出现不可重复读。事务A事先读取了数据,事务B紧接着更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。

解决了更新丢失和脏读问题

第三种隔离级别:Repeatable read(可重复读取)
可重复读取是指在一个事务内,多次读同一个数据,在这个事务还没结束时,其他事务不能访问该数据(包括了读写),这样就可以在同一个事务内两次读到的数据是一样的,因此称为是可重复读隔离级别,读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务(包括了读写),这样避免了不可重复读和脏读,但是有时可能会出现幻读。(读取数据的事务)可以通过共享读镜排他写锁实现。

解决了更新丢失、脏读、不可重复读、但是还会出现幻读

第四种隔离级别:Serializable(可序化)
提供严格的事务隔离,它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行,如果仅仅通过行级锁是无法实现序列化的,必须通过其他机制保证新插入的数据不会被执行查询操作的事务访问到。序列化是最高的事务隔离级别,同时代价也是最高的,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻读

spring 事务实现方式有哪些?

  1. 编程式事务管理对基于 POJO 的应用来说是唯一选择。我们需要在代码中调用beginTransaction()commit()rollback()等事务管理相关的方法,这就是编程式事务管理。
  2. 基于 TransactionProxyFactoryBean 的声明式事务管理
  3. 基于 @Transactional 的声明式事务管理
  4. 基于 Aspectj AOP 配置事务

mysql中SQL执行过程详解

  • 客户端发送一条查询给服务器;
  • 服务器先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段。
  • 服务器段进行SQL解析、预处理,在优化器生成对应的执行计划;
  • mysql根据优化器生成的执行计划,调用存储引擎的API来执行查询。
  • 将结果返回给客户端。

Redis数据类型

Redis支持五中数据类型:

StringRedis最基本的数据类型, String类型是二进制安全的,意味着可以包含任何数据,比如jpg图片或者序列化的对象。String类型的最大能存储512M

Hash(哈希)是fieldvalue之间的映射,即键值对的集合,所以特别适合用于存储对象。

List(列表)Redis列表是简单的字符串列表,按照插入顺序排序。支持添加一个元素到列表头部(左边)或者尾部(右边)的操作。

Set(集合)String类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。

Zset(sortedset:有序集合)有序集合是在集合的基础上为每一个元素关联一个分数,这就让有序集合不仅支持插入,删除,判断元素是否存在等操作外,还支持获取分数最高/最低的前N个元素。有序集合中的每个元素是不同的,但是分数却可以相同。有序集合使用散列表和跳跃表实现,即使读取位于中间部分的数据也很快,时间复杂度为O(log(N)),有序集合比列表更费内存。

Redis持久化的两种方式

1snapshotting(快照),这个是redis默认持久化的方式

快照是redis默认的持久化的方式,这种方式在规定的时间将内存中数据以快照的方式写入到二进制文件中,默认的文件名是:dump.rdb;可以通过配置设置自动持久化的方式,我们可以修改redis.conf文件,来配置redisn秒如果超过mkey被修改,则自动做快照操作。

2Append-only file,简称aof的方式

由于快照的方式有一定的间隔时间,所以如果redis在间隔时间内意外down掉后,就会丢失最后一次快照后的所有数据。

aof比快照方式有更好的持久化性,是由于在使用aof时,redis会将每一个收到的命令通过write函数追加到文件中,当redis重启后,会通过重新执行aof文件中的内容,来在内存中重建整个数据库的内容。

Redis为什么是单线程?为什么有如此高的性能?

注意:redis 单线程指的是网络请求模块使用了一个线程,即一个线程处理所有网络请求,其他模块仍用了多个线程。

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMapHashMap的优势就是查找和操作的时间复杂度都是O(1)

2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4、使用多路I/O复用模型,非阻塞IO

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

redis与mysql的区别

前者是内存数据库也是非关系数据库,数据保存在内存,当然速度快

而后者是关系型数据库,将数据存储在硬盘中,数据访问也就慢。

Spring IOC的理解

传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。

 DI—Dependency Injection,即依赖注入组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。   

 理解DI的关键是:谁依赖谁,为什么需要依赖,谁注入谁,注入了什么,那我们来深入分析一下:

  谁依赖于谁:当然是应用程序依赖于IoC容器;

  为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源;

  谁注入谁:很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;

  注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

什么是 aop?

aop是面向切面编程,不同于java原始的oop是面向对象编程,使用aop可以实现不需要修改原功能代码,只需要修改一下配置,即可实现功能的扩展
aop采用的是横向抽取机制,取代了传统的纵向继承体系,减少了重复性代码
运行期间通过代理方式向目标类植入增强代码
经典的应用场景有事务的管理,安全检查,缓存,性能监控等等

Spring AOP 原理

      AspectJ的底层技术是静态代理,即用一种AspectJ支持的特定语言编写切面,通过一个命令来编译,生成一个新的代理类,该代理类增强了业务类,这是在编译时增强,相对于下面说的运行时增强,编译时增强的性能更好。

      Spring AOP采用的是动态代理,在运行期间对业务方法进行增强,所以不会生成新类,对于动态代理技术,Spring AOP提供了对JDK动态代理的支持以及CGLib的支持。

      JDK动态代理只能为接口创建动态代理实例,而不能对类创建动态代理。需要获得被目标类的接口信息(应用Java的反射技术),生成一个实现了代理接口的动态代理类(字节码),再通过反射机制获得动态代理类的构造函数,利用构造函数生成动态代理类的实例对象,在调用具体方法前调用invokeHandler方法来处理。

HashMap和HashTable的区别

HashMap线程不安全,HashTable是线程安全的。HashMap内部实现没有任何线程同步相关的代码,所以相对而言性能要好一点。如果在多线程中使用HashMap需要自己管理线程同步。HashTable大部分对外接口都使用synchronized包裹,所以是线程安全的,但是性能会相对差一些。

二者的基类不一样。HashMap派生于AbstractMapHashTable派生于Dictionary。它们都实现Map, Cloneable, Serializable这些接口。AbstractMap中提供的基础方法更多,并且实现了多个通用的方法,而在Dictionary中只有少量的接口,并且都是abstract类型。

keyvalue的取值范围不同。HashMapkeyvalue都可以为null,但是HashTablekeyvalue都不能为null。对于HashMap如果get返回null,并不能表明HashMap不存在这个key,如果需要判断HashMap中是否包含某个key,就需要使用containsKey这个方法来判断。

算法不一样。HashMapinitialCapacity16,而HashTableinitialCapacity11HashMap中初始容量必须是2的幂,如果初始化传入的initialCapacity不是2的幂,将会自动调整为大于输入的initialCapacity最小的2的幂。HashMap使用自己的计算hash的方法(会依赖keyhashCode方法)HashTable则使用keyhashCode方法得到。

Hashmap的put过程

判断数组是否为空,如果是空,则创建默认长度位 16 的数组。

通过与运算计算对应 hash 值的下标,如果对应下标的位置没有元素,则直接创建一个。

如果有元素,说明 hash 冲突了,则再次进行 3 种判断。
 

  1. 判断两个冲突的key是否相等,equals 方法的价值在这里体现了。如果相等,则将已经存在的值赋给变量e。最后更新evalue,也就是替换操作。
  2. 如果key不相等,则判断是否是红黑树类型,如果是红黑树,则交给红黑树追加此元素。
  3. 如果key既不相等,也不是红黑树,则是链表,那么就遍历链表中的每一个key和给定的key是否相等。如果,链表的长度大于等于8了,则将链表改为红黑树,这是Java8 的一个新的优化。

最后,如果这三个判断返回的 e 不为null,则说明key重复,则更新key对应的value的值。

对维护着迭代器的modCount 变量加一。

最后判断,如果当前数组的长度已经大于阀值了。则重新hash

哈希冲突

由于哈希算法被计算的数据是无限的,而计算后的结果范围有限,因此总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。

1、开放地址法(再散列法)

发生哈希冲突后,按照某一次序找到下一个空闲的单元,把冲突的元素放入。

2、链地址法(拉链法)

   将哈希值相同的元素构成一个链表,head放在散列表中。一般链表长度超过了8就转为红黑树,长度少于6个就变为链表。 

3、再哈希法

   同时构造多个不同的哈希函数,Hi = RHi(key) i= 1,2,3 … k; 
H1 = RH1(key) 发生冲突时,再用H2 = RH2(key) 进行计算,直到冲突不再产生,这种方法不易产生聚集,但是增加了计算时间。

4、创建公共溢出区

把哈希表分为公共表和溢出表,如果发生了溢出,溢出的数据全部放在溢出区。

TCP连接拥塞控制四种方法

总共四种:慢开始,拥塞避免,快重传,快恢复

 慢启动算法的思路:主机开发发送数据报时,如果立即将大量的数据注入到网络中,可能会出现网络的拥塞。慢启动算法就是在主机刚开始发送数据报的时候先探测一下网络的状况,如果网络状况良好,发送方每发送一次文段都能正确的接受确认报文段。那么就从小到大的增加拥塞窗口的大小,即增加发送窗口的大小。

  拥塞避免的思路:是让cwnd缓慢的增加而不是加倍的增长,每经历过一次往返时间就使cwnd增加1,而不是加倍,这样使cwnd缓慢的增长,比慢启动要慢的多。

快重传:

      快重传算法要求首先接收方收到一个失序的报文段后就立刻发出重复确认,而不要等待自己发送数据时才进行捎带确认。

   快恢复:

       1. 当发送发连续接收到三个确认时,就执行乘法减小算法,把慢启动开始门限(ssthresh)减半,但是接下来并不执行慢开始算法。

       2. 此时不执行慢启动算法,而是把cwnd设置为ssthresh的一半, 然后执行拥塞避免算法,使拥塞窗口缓慢增大

UDP&TCP的区别

UDP协议和TCP协议都是传输层协议。

TCPTransmission Control Protocol,传输控制协议)提供的是面向连接,可靠的字节流服务。即客户和服务器交换数据前,必须现在双方之间建立一个TCP连接,之后才能传输数据。并且提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。

UDPUser Data Protocol,用户数据报协议)是一个简单的面向数据报的运输层协议。它不提供可靠性,只是把应用程序传给IP层的数据报发送出去,但是不能保证它们能到达目的地。由于UDP在传输数据报前不用再客户和服务器之间建立一个连接,且没有超时重发等机制,所以传输速度很快。

说一下 session 的工作原理?

其实session是一个存在服务器上的类似于一个散列表格的文件。里面存有我们需要的信息,在我们需要用的时候可以从里面取出来。类似于一个大号的map吧,里面的键存储的是用户的sessionid,用户向服务器发送请求的时候会带上这个sessionid。这时就可以从中取出对应的值了。

session 和 cookie 有什么区别?

Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。

HTTP 和 HTTPS 的基本概念

HTTP超文本传输协议HTTPHyperText Transfer Protocol)互联网上应用最为广泛的一种网络协议。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。它可以使浏览器更加高效。HTTP 协议是以明文方式发送信息的,如果黑客截取了 Web 浏览器和服务器之间的传输报文,就可以直接获得其中的信息。

HTTP 原理:

  客户端的浏览器首先要通过网络与服务器建立连接,该连接是通过 TCP 来完成的,一般 TCP 连接的端口号是80 建立连接后,客户机发送一个请求给服务器,请求方式的格式为:统一资源标识符(URL)、协议版本号,后边是 MIME 信息包括请求修饰符、客户机信息和许可内容。

  服务器接到请求后,给予相应的响应信息,其格式为一个状态行,包括信息的协议版本号、一个成功或错误的代码,后边是 MIME 信息包括服务器信息、实体信息和可能的内容。

HTTPS:是以安全为目标的 HTTP 通道,是 HTTP 的安全版。HTTPS 的安全基础是 SSLSSL 协议位于 TCP/IP 协议与各种应用层协议之间,为数据通讯提供安全支持。SSL 协议可分为两层:SSL 记录协议(SSL Record Protocol),它建立在可靠的传输协议(如TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。SSL 握手协议(SSL Handshake Protocol),它建立在 SSL 记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。

HTTP 与 HTTPS  的区别

1HTTPS  协议需要到 CA Certificate Authority,证书颁发机构)申请证书,一般免费证书较少,因而需要一定费用。(以前的网易官网是http,而网易邮箱是 https )

2HTTP 是超文本传输协议,信息是明文传输,HTTPS 则是具有安全性的 SSL 加密传输协议。

3HTTP HTTPS 使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443

4HTTP 的连接很简单,是无状态的。HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。(无状态的意思是其数据包的发送、传输和接收都是相互独立的。无连接的意思是指通信双方都不长久的维持对方的任何信息。)

HTTPS 的连接过程

客户端的浏览器向服务器发送请求,并传送客户端 SSL 协议的版本号,加密算法的种类,产生的随机数,以及其他服务器和客户端之间通讯所需要的各种信息。

服务器向客户端传送 SSL 协议的版本号,加密算法的种类,随机数以及其他相关信息,同时服务器还将向客户端传送自己的证书。

客户端利用服务器传过来的信息验证服务器的合法性,服务器的合法性包括:证书是否过期,发行服务器证书 CA 是否可靠,发行者证书的公钥能否正确解开服务器证书的 "发行者的数字签名",服务器证书上的域名是否和服务器的实际域名相匹配。如果合法性验证没有通过,通讯将断开;如果合法性验证通过,将继续进行第四步。

用户端随机产生一个用于通讯的 "对称密码",然后用服务器的公钥(服务器的公钥从步骤中的服务器的证书中获得)对其加密,然后将加密后的预主密码传给服务器。

如果服务器要求客户的身份认证(在握手过程中为可选),用户可以建立一个随机数然后对其进行数据签名,将这个含有签名的随机数和客户自己的证书以及加密过的密钥一起传给服务器。

如果服务器要求客户的身份认证,服务器必须检验客户证书和签名随机数的合法性,具体的合法性验证过程包括:客户的证书使用日期是否有效,为客户提供证书的 CA  是否可靠,发行 CA 的公钥能否正确解开客户证书的发行 CA 的数字签名,检查客户的证书是否在证书废止列表(CRL)中。检验如果没有通过,通讯立刻中断;如果验证通过,服务器将用自己的私钥解开加密的私钥,然后执行一系列步骤来产生主通讯密码(客户端也将通过同样的方法产生相同的主通讯密码)。

服务器和客户端用相同的对称加密密钥,对称密钥用于 SSL 协议的安全数据通讯的加解密通讯。同时在 SSL 通讯过程中还要完成数据通讯的完整性,防止数据通讯中的任何变化。

 客户端服务器端发出信息,指明后面的数据通讯将使用的步骤 中的主密码为对称密钥,同时通知服务器客户端的握手过程结束。

服务器向客户端发出信息,指明后面的数据通讯将使用的步骤 中的主密码为对称密钥,同时通知客户端服务器端的握手过程结束。

SSL 的握手部分结束,SSL 安全通道的数据通讯开始,客户和服务器开始使用相同的对称密钥进行数据通讯,同时进行通讯完整性的检验。

为什么对称加密比非对称加密快

对称加密主要的运算是位运算,速度非常快,如果使用硬件计算,速度会更快

非对称加密计算一般都比较复杂,比如 RSA,它里面涉及到大数乘法、大数模等等运算

java 传递参数的两种方式

 值传递:方法调用时,实际参数把它的值传递给对应的形式参数,方法执行中形式参数值的改变不影响实际参数的值。

  引用传递:也称为传地址。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,在方法执行中,对形式参数的操作实际上就是对实际参数的操作,方法执行中形式参数值的改变将会影响实际参数的值。

 

       a.传递值的数据类型:八种基本数据类型和String(这样理解可以,但是事实上String也是传递的地址,只是string对象和其他对象是不同的,string对象是不能被改变的,内容改变就会产生新对象。那么StringBuffer就可以了,但只是改变其内容。不能改变外部变量所指向的内存地址)

    b.传递地址值的数据类型:除String以外的所有复合数据类型,包括数组、类和接口 

String 对象内存分配策略

1.1 创建对象的方式

String s = "abc" 方式创建的对象,存储在字符串常量池中,在创建字符串对象之前,会先在常量池中检查是否存在 abc 对象。如果存在,则直接返回常量池中 abc对象的引用,不存在会创建该对象,并将该对象的引用返回给对象 s

HotSpot 虚拟机为例,在 jdk1.8 之前,字符串常量池在方法区中,为了减小方法区内存溢出的风险,在 jdk1.8 之后就把字符串常量池转移到 java 堆中了。

String s = new String("abc") 这种方式,实际上 abc 本身就是字符串池中的一个对象,在运行 new String() 时,把字符串常量池中的字符串 abc 复制到堆中,因此该方式不仅会在堆中,还会在常量池中创建 abc 字符串对象。 最后把 java 堆中对象的引用返回给 s

上面的问题清楚了原理还很好理解,下面还有一个例子,理解起来就不那么容易了。

    @Test

    public void test() {

        String s1 = "abc";

        String s2 = "a";

       

        System.out.println(s1 == ("a" + "bc"));    // true

        System.out.println(s1 == (s2 + "bc"));     // false

    }

2.1 字符串常量重载 "+"

"a" + "bc" 是两个字符串常量的拼接,当一个字符串由多个字符串常量连接而成时,它自己也肯定是字符串常量。java 虚拟机对于字符串常量的 “+” 号连接,在程序编译期,java 虚拟机就将常量字符串的 “+” 连接优化为连接后的值。

这样一来,最终只会在常量池中创建一个 abc 对象,并没有创建临时字符串对象 a  bc,减轻了垃圾收集器的压力。s1 == ("a" + "bc"),因为 abcs1 在字符串常量池中已经存在了,因此返回值是 true

2.2 字符串引用重载 "+"

java 虚拟机对于有字符串引用存在的字符串连接,即 s2 + "bc" 在被编译器执行的时候,会自动引入 StringBuilder 对象,调用其 append() 方法,最终调用 toString() 方法返回其在堆中对象的引用。 下面是反编译后的部分字节码。

怎么防止死锁?

死锁的四个必要条件:

  • 互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
  • 请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
  • 不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
  • 环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系

volatile变量的特性

1)保证可见性,不保证原子性

2)禁止指令重排

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:

  a.重排序操作不会对存在数据依赖关系的操作进行重排序。

b.重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变

a.当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

b.在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放

到其前面执行。

即执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行。且前面语句的结果对volatile变量及其后面语句可见。

synchronized 和 volatile 的区别是什么?

  • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
  • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
  • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
  • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

synchronized和lock的区别

区别如下:

  1. lock是一个接口,而synchronizedjava的一个关键字,synchronized是内置的语言实现;
  2. 异常是否释放锁:
  3. synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
  4. 是否响应中断
  5. lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;
  6. 是否知道获取锁
  7. Lock可以通过trylock来知道有没有获取锁,而synchronized不能;
  8. Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)
  9. 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
  10. synchronized使用Object对象本身的wait notifynotifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,

synchronized 和 Lock 有什么区别?

  • 首先synchronizedjava内置关键字,在jvm层面,Lock是个java类;
  • synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
  • synchronized会自动释放锁(a 线程执行完同步代码会释放锁 b 线程执行过程中发生异常会释放锁)Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
  • synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
  • synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
  • Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。

什么是 java 序列化?什么情况下需要序列化?

简单说就是为了保存在内存中的各种对象的状态(也就是实例变量,不是方法),并且可以把保存的对象状态再读出来。虽然你可以用你自己的各种各样的方法来保存object states,但是Java给你提供一种应该比你自己好的保存对象状态的机制,那就是序列化。

什么情况下需要序列化:

a)当你想把的内存中的对象状态保存到一个文件中或者数据库中时候;
b)当你想用套接字在网络上传送对象的时候;
c)当你想通过RMI传输对象的时候;

深拷贝和浅拷贝区别是什么?

浅拷贝只是复制了对象的引用地址,两个对象指向同一个内存地址,所以修改其中任意的值,另一个值都会随之变化,这就是浅拷贝(例:assign()

深拷贝是将对象及值复制过来,两个对象修改其中任意的值另一个值不会改变,这就是深拷贝(例:JSON.parse()JSON.stringify(),但是此方法无法复制函数类型)

什么时候使用volatile

public class VolatileFalseUsage {

    private volatile int count = 0;



    public void incrementCount() {

        count++;

    }

    public int getCount() {

        return count;

    }

}

count++可以分解为三步操作,1. 读取count的值,2.count1 3.count写回内存。添加Volatile关键词只能够保证count的变化立马可见,而不能保证123这三个步骤的总体原子性。 要实现总体的原子性还是需要用到类似Synchronized的关键字。

多线程sleep()和wait()的区别

sleep()

   1、属于Thread类,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态

   2sleep方法没有释放锁

   3sleep必须捕获异常

   4sleep可以在 任何地方使用

 


wait()

   1、属于Object,一旦一个对象调用了wait方法,必须要采用notify()notifyAll()方法唤醒该进程

   2wait方法释放了锁

   3wait不需要捕获异常

   4waitnotifynotifyAll只能在同步控制方法或者同步控制块里面使用


 

sleep(1000)wait(1000)的区别:

     Thread.Sleep(1000) 意思是在未来的1000毫秒内本线程不参与CPU竞争,1000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束,即使这个时候恰巧轮到操作系统进行CPU 分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去。

    wait(1000)表示将锁释放1000毫秒,到时间后如果锁没有被其他线程占用,则再次得到锁,然后wait方法结束,执行后面的代码,如果锁被其他线程占用,则等待其他线程释放锁。注意,设置了超时时间的wait方法一旦过了超时时间,并不需要其他线程执行notify也能自动解除阻塞,但是如果没设置超时时间的wait方法必须等待其他线程执行notify

spring、springboot和springMVC之间的关系

Spring
Spring是一个轻量级的java开发框架,其核心是控制反转(IOC)和面向切面(AOP),它像是一辆汽车的发动机(把框架比作汽车的话),是框架最核心的东西。
springMVC
springMVC
是用来开发web应用的一套轻度耦合的web框架,其涵盖了前端视图开发、后台接口逻辑开发等,它的优点有很多比如清晰的角色划分,配置方式很直接,业务代码可重用等。但它的缺点在于配置文件XML,config这些比较繁琐。于是大家为了让其配置不再那么繁琐,于是就有了springboot
springboot
springboot
是约定大于配置,可以简化spring的配置流程。(注意这里是基于spring框架而不是springMVC)。所以springboot是在spring基础上的一种更简洁的开发应用的方式,当然springboot之于springMVC就像spring之于springMVC,它只是一个承载者,让开发web应用的时候配置更简单,并且集成了Tomcat这类服务器,让开发者更加专注于代码的业务逻辑上。

什么是 JDBC

JDBC 规范定义接口,具体的实现由各大数据库厂商来实现 

JDBC  Java 访问数据库的标准规范,真正怎么操作数据库还需要具体的实现类,也就是数据库驱动。每个

数据库厂商根据自家数据库的通信格式编写好自己数据库的驱动。所以我们只需要会调用 JDBC 接口中的方法即

可,数据库驱动由数据库厂商提供。

Mybatis与JDBC相比有什么优势?

1.sql语句与java代码分离

2.对结果集的映射,mybatis会帮我们将数据进行封装

3.mybatis底层用到了连接池,不用每次都从数据库获取以及关闭资源,节省资源

4.动态sql语句方便,如果是传统的JDBC要进行关键字查询时需要写多条代码

mybatis常用标签

select 标签、 insert标签、delete标签、update标签、if 标签、foreach 标签、choose标签

JVM内存模型

首先要说一下JVM内存空间分为五部分,分别是:方法区、堆、Java虚拟机栈、本地方法栈、程序计数器

方法区主要用来存放类信息、类的静态变量、常量、运行时常量池等,方法区的大小是可以动态扩展的,

堆主要存放的是数组、类的实例对象、字符串常量池等。

Java虚拟机栈是描述JAVA方法运行过程的内存模型,Java虚拟机栈会为每一个即将执行的方法创建一个叫做栈帧的区域,该区域用来存储该方法运行时需要的一些信息,包括:局部变量表、操作数栈、动态链接、方法返回地址等。比如我们方法执行过程中需要创建变量时,就会将局部变量插入到局部变量表中,局部变量的运算、传递等在操作数栈中进行,当方法执行结束后,这个方法对应的栈帧将出栈,并释放内存空间。栈中会发生的两种异常,StackOverFlowErrorOutOfMemoryError,StackOverFlowError表示当前线程申请的栈超过了事先定好的栈的最大深度,但内存空间可能还有很多。 OutOfMemoryError是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。

本地方法栈结构上和Java虚拟机栈一样,只不过Java虚拟机栈是运行Java方法的区域,而本地方法栈是运行本地方法的内存模型。运行本地方法时也会创建栈帧,同样栈帧里也有局部变量表、操作数栈、动态链接和方法返回地址等,在本地方法执行结束后栈帧也会出栈并释放内存资源,也会发生OutOfMemoryError

最后是程序计数器,程序计数器是一个比较小的内存空间,用来记录当前线程正在执行的那一条字节码指令的地址。如果当前线程正在执行的是本地方法,那么此时程序计数器为空。程序计数器有两个作用,1、字节码解释器通过改变程序计数器来一次读取指令,从而实现代码的流程控制,比如我们常见的顺序、循环、选择、异常处理等。2、在多线程的情况下,程序计数器用来记录当前线程执行的位置,当线程切换回来的时候仍然可以知道该线程上次执行到了哪里。而且程序计数器是唯一一个不会出现OutOfMeroryError的内存区域。

 

方法区和堆都是线程共享的,在JVM启动时创建,在JVM停止时销毁,而Java虚拟机栈、本地方法栈、程序计数器是线程私有的,随线程的创建而创建,随线程的结束而死亡。

浅谈Java锁机制

链接

浏览器输入url​​​​​​​

输入网址敲回车-> DNS->找到 IP 地址->通过 TCP 三次握手建立连接->浏览器发送 Http 请求->收到回应->浏览器渲染页面->结束连接

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值