高级JAVA工程师面试题(陆续完善答案)

2018-12-06  本文已影响0人  为梦想奋斗的晖

    昨天去深圳某互联网企业应聘了高级JAVA工程师岗位,面试问题如下,为了方便给自己去看,会将答案陆续给加到问题底下,方便一次性查看


一、高并发与锁

1.synchronized 和 lock 的区别,和reentrantlock又有什么区别?

ReenTrantLock可重入锁(和synchronized的区别)总结

可重入性:从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 

锁的实现:Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。 

性能的区别:在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。 

功能区别:便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized 

ReenTrantLock独有的能力:

1.      ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。

2.      ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

3.      ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。 

简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。 

什么情况下使用ReenTrantLock:

答案是,如果你需要实现ReenTrantLock的三个独有功能时。

参考链接:https://blog.csdn.net/qq838642798/article/details/65441415

2.synchronized 是可重入的吗?synchronized可以作用于什么上面?CAS无锁算法

1.可重入

1.1. 定义

    关于可重入这一概念,我们需要参考维基百科。若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。

1.2. 可重入的条件

    不在函数内使用静态或全局数据。不返回静态或全局数据,所有数据都由函数的调用者提供。使用本地数据(工作内存),或者通过制作全局数据的本地拷贝来保护全局数据。不调用不可重入函数。

1.3. 可重入与线程安全

    一般而言,可重入的函数一定是线程安全的,反之则不一定成立。在不加锁的前提下,如果一个函数用到了全局或静态变量,那么它不是线程安全的,也不是可重入的。如果我们加以改进,对全局变量的访问加锁,此时它是线程安全的但不是可重入的,因为通常的枷锁方式是针对不同线程的访问(如Java的synchronized),当同一个线程多次访问就会出现问题。只有当函数满足可重入的四条条件时,才是可重入的。

2. synchronized的可重入性

2.1. synchronized是可重入锁

    回到引言里的问题,如果一个获取锁的线程调用其它的synchronized修饰的方法,会发生什么?从设计上讲,当一个线程请求一个由其他线程持有的对象锁时,该线程会阻塞。当线程请求自己持有的对象锁时,如果该线程是重入锁,请求就会成功,否则阻塞。我们回来看synchronized,synchronized拥有强制原子性的内部锁机制,是一个可重入锁。因此,在一个线程使用synchronized方法时调用该对象另一个synchronized方法,即一个线程得到一个对象锁后再次请求该对象锁,是永远可以拿到锁的。在Java内部,同一个线程调用自己类中其他synchronized方法/块时不会阻碍该线程的执行,同一个线程对同一个对象锁是可重入的,同一个线程可以获取同一把锁多次,也就是可以多次重入。原因是Java中线程获得对象锁的操作是以线程为单位的,而不是以调用为单位的。

2.2. synchronized可重入锁的实现

    之前谈到过,每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。

参考链接:https://www.cnblogs.com/cielosun/p/6684775.html

3.并发包你还用过其他什么?CopyOnWriteArrayList 跟ArrayList有啥区别,CopyOnWriteArrayList底层是怎么实现的?ConcurrHashMap为什么要用红黑树,红黑树有什么特征?红黑树的优势是什么?红黑树的时间复杂度是多少?

  Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

什么是CopyOnWrite容器

    CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWriteArrayList的实现原理

    在使用CopyOnWriteArrayList之前,我们先阅读其源码了解下它是如何实现的。以下代码是向CopyOnWriteArrayList中add方法的实现(向CopyOnWriteArrayList里添加元素),可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。 

CopyOnWrite的应用场景

    CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。实现代码如下: 

CopyOnWrite的缺点

    CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

    内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。

    针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。

    数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。 

参考链接:https://www.cnblogs.com/dolphin0520/p/3938914.html

4.线程池有哪些线程池,核心线程和最大线程有什么区别?你们的线程池大小是怎么设置的?IO密集型怎么设置线程数?

二、JVM

5.说下JVM,说下类加载器,类是怎么加载的?类加载器有线程安全问题吗?

6.说下你知道的垃圾回收器,说下G1回收器,说下垃圾回收算法。

7.说下你做了哪些JVM的优化?OOM排查步骤

三、数据库

8.MySQL有哪些优化步骤?为什么要用join替换子查询?

9.Mysql索引有哪些索引?为什么要用B+树而不用B树?

从数据结构角度

1、B+树索引(O(log(n))):关于B+树索引,可以参考 MySQL索引背后的数据结构及算法原理

2、hash索引: a 仅仅能满足"=","IN"和"<=>"查询,不能使用范围查询 b 其检索效率非常高,索引的检索可以一次定位,不像B-Tree 索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,所以 Hash 索引的查询效率要远高于 B-Tree 索引 c 只有Memory存储引擎显示支持hash索引

3、FULLTEXT索引(现在MyISAM和InnoDB引擎都支持了)

4、R-Tree索引(用于对GIS数据类型创建SPATIAL索引)

从物理存储角度

1、聚集索引(clustered index)

2、非聚集索引(non-clustered index)

从逻辑角度

1、主键索引:主键索引是一种特殊的唯一索引,不允许有空值

2、普通索引或者单列索引

3、多列索引(复合索引):复合索引指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用复合索引时遵循最左前缀集合

4、唯一索引或者非唯一索引

5、空间索引:空间索引是对空间数据类型的字段建立的索引,MYSQL中的空间数据类型有4种,分别是GEOMETRY、POINT、LINESTRING、POLYGON。MYSQL使用SPATIAL关键字进行扩展,使得能够用于创建正规索引类型的语法创建空间索引。创建空间索引的列,必须将其声明为NOT NULL,空间索引只能在存储引擎为MYISAM的表中创建CREATE TABLE table_name[col_name data type] [unique|fulltext|spatial][index|key][index_name](col_name[length])[asc|desc]

1、unique|fulltext|spatial为可选参数,分别表示唯一索引、全文索引和空间索引;

2、index和key为同义词,两者作用相同,用来指定创建索引

3、col_name为需要创建索引的字段列,该列必须从数据表中该定义的多个列中选择;

4、index_name指定索引的名称,为可选参数,如果不指定,MYSQL默认col_name为索引值;

5、length为可选参数,表示索引的长度,只有字符串类型的字段才能指定索引长度;

6、asc或desc指定升序或降序的索引值存储

参考链接:https://www.cnblogs.com/-619569179/p/6525684.html

10.Mysql主从同步系统底层是怎么同步的?MySQL的悲观锁怎么实现?Mysql的隔离级别?二阶段提交了解过吗?

mysql主从复制原理 

从库生成两个线程,一个I/O线程,一个SQL线程; i/o线程去请求主库 的binlog,并将得到的binlog日志写到relay log(中继日志) 文件中;主库会生成一个 log dump 线程,用来给从库 i/o线程传binlog; SQL 线程,会读取relay log文件中的日志,并解析成具体操作,来实现主从的操作一致,而最终数据一致; 

参考链接:https://www.cnblogs.com/Aiapple/p/5792939.html

MySQL的悲观锁怎么实现?

要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。我们可以使用命令设置MySQL为非autocommit模式:set autocommit=0;设置完autocommit后,我们就可以执行我们的正常业务了。

具体如下:

//0.开始事务begin;/begin work;/start transaction; (三者选一就可以)

//1.查询出商品信息select status from t_goods where id=1 for update;

//2.根据商品信息生成订单insert into t_orders (id,goods_id) values (null,1);

//3.修改商品status为2update t_goods set status=2;

//4.提交事务commit;/commit work;

注:上面的begin/commit为事务的开始和结束,因为在前一步我们关闭了mysql的autocommit,所以需要手动控制事务的提交,在这里就不细表了。上面的第一步我们执行了一次查询操作:select status from t_goods where id=1 for update;与普通查询不一样的是,我们使用了select…for update的方式,这样就通过数据库实现了悲观锁。此时在t_goods表中,id为1的 那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。注:需要注意的是,在事务中,只有SELECT ... FOR UPDATE 或LOCK IN SHARE MODE 同一笔数据时会等待其它事务结束后才执行,一般SELECT ... 则不受此影响。拿上面的实例来说,当我执行select status from t_goods where id=1 for update;后。我在另外的事务中如果再次执行select status from t_goods where id=1 for update;则第二个事务会一直等待第一个事务的提交,此时第二个查询处于阻塞的状态,但是如果我是在第二个事务中执行select status from t_goods where id=1;则能正常查询出数据,不会受第一个事务的影响。

 补充:MySQL select…for update的Row Lock与Table Lock上面我们提到,使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。 

参考链接:https://www.cnblogs.com/laoyeye/p/8228467.html

Mysql的隔离级别? 

1、事务的并发问题

1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据

2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。

3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表 

2、MySQL事务隔离级别

事务隔离级别                                   脏读          不可重复读           幻读

读未提交(read-uncommitted)        是                    是                    是

不可重复读(read-committed)        否                    是                    是

可重复读(repeatable-read)            否                    否                    是

串行化(serializable)                       否                    否                    否    

 mysql默认的事务隔离级别为repeatable-read

参考链接:https://www.cnblogs.com/huanongying/p/7021555.html


四、各类框架

10.Kafka的数据存储是怎么样的?

11.Spring的源码,Spring的事务是怎么做的?

12.Struts2和SpringMVC那个是单例哪个是多例?那个有并发和线程安全问题

SringMVC默认是单例还是多例?

1、默认单例

    SpringMVC默认是单例的。与Struts2不同,SpringMVC没有默认处理方法,也就是说SpringMVC是基于方法的开发,都是用形参接收值,一个方法结束参数就销毁了,多线程访问都会有一块内存空间产生,里面的参数也是不会共用的。由于SpringMVC默认使用了单例,所以Controller类中不适合定义属性,只要controller中不定义属性,那么单例完全是安全的。单例模式可以提高SpringMVC性能,不需要每次相应请求都创建一个对象。 此外,Spring的Ioc容器管理的bean默认是单实例的。

2、多例

    在特殊情况,需要在Controller类定义属性时,单例肯定会出现竞争访问,需要在类上面加上注解@Scope(“prototype”)改为多例的模式。

3、Struts2 与SpringMVC不同,Struts2是基于类的属性进行发的,定义属性可以整个类通用。所以Struts2的Action是多实例的并非单例,也就是每次请求产生一个Action的对象。Action类中往往包含了数据属性,例如在页面填写的form表单的字段,Action中有对应的的属性来绑定页面form表单字段。显然如果Action是单实例的话,那么多线程的环境下就会相互影响,例如造成别人填写的数据被你看到了。 但是什么有人说Struts2的Action 默认是单例的?而且还可以进行配置呢? 因为在和Spring一起使用的时候,Action交给Spring进行管理,默认的就是单例,所以才会有人说Struts2默认是单例的。 所以在Spring整合Struts2开发时,如果需要用使用Struts2多例,就在spring的action bean配置的时候设置scope=”prototype”。

参考链接:https://blog.csdn.net/chengyuqiang/article/details/78776767

13.Hibernate和Mybatis的区别。

14.Spring的两大特性?AOP用到了什么设计模式?两个代理模式的区别?如果实现了接口可以指定Cglib来代理吗?AOP是在JVM哪部分做处理的?

15.Redis的如何保证热点数据的?过期策略是怎么样的?Redis的集群是怎么同步的?如果一个节点宕掉其他集群会怎么处理?缓存击穿是怎么处理的?缓存雪崩怎么处理?

为什么会有淘汰?

Redis可以看作是一个内存数据库,可以通过Maxmemory指令配置Redis的数据集使用指定量的内存。设置maxmemory为0,则表示无限制(这是64位系统的默认行为,而32位系统使用3GB内隐记忆极限)。 maxmemory 100mb 

当内存使用达到maxmemory极限时,需要使用某种淘汰算法来决定清理掉哪些数据,以保证新数据的存入。

常用的淘汰算法:

FIFO:First In First Out,先进先出。判断被存储的时间,离目前最远的数据优先被淘汰。

LRU:Least Recently Used,最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰。

LFU:Least Frequently Used,最不经常使用。在一段时间内,数据被使用次数最少的,优先被淘汰。

Redis提供的淘汰策略:

noeviction:达到内存限额后返回错误,客户尝试可以导致更多内存使用的命令(大部分写命令,但DEL和一些例外)

allkeys-lru:为了给新增加的数据腾出空间,驱逐键先试图移除一部分最近使用较少的(LRC)。

volatile-lru:为了给新增加的数据腾出空间,驱逐键先试图移除一部分最近使用较少的(LRC),但只限于过期设置键。

allkeys-random: 为了给新增加的数据腾出空间,驱逐任意键

volatile-random: 为了给新增加的数据腾出空间,驱逐任意键,但只限于有过期设置的驱逐键。

volatile-ttl: 为了给新增加的数据腾出空间,驱逐键只有秘钥过期设置,并且首先尝试缩短存活时间的驱逐键

参考链接:https://blog.csdn.net/gongm24/article/details/79624641

缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

解决方案

有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

缓存雪崩

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

解决方案

缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案

1.使用互斥锁(mutex key)业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。在redis2.6.1之前版本未实现setnx的过期时间

2. "提前"使用互斥锁(mutex key):在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。

3. "永远不过期":  这里的“永远不过期”包含两层意思:(1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期        从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。

4. 资源保护:采用netflix的hystrix,可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。

参考链接:https://blog.csdn.net/zeb_perfect/article/details/54135506

16.Zookeeper的高可用性是怎么保证的?分布式锁是怎么做的?跟Redis的分布式锁有什么区别?

17.SpringCloud的组件和作用?eureka 跟zookeeper的区别?你还用过什么其他的注册中心吗?

18.用过mongodb吗?说说和mysql的区别

五、集合类及其他

19.常见的集合类有哪些?HashMap为什么不是线程安全的?Hash碰撞是怎么回事?

20.对Socket了解多吗?对Delay ACK了解吗?

1.基本概念

先了解一下,TCP传输的数据流的分类:

    TCP交互数据流:一般情况下数据总是以小于MSS的分组发送,做的是小流量的数据交互,常见的应用比如SSH,Telnet等

    TCP成块数据流:TCP尽最大能力的运载数据,数据基本都是按照MSS发送,常见的应用有FTP,Iperf等

2.交互数据

交互式的数据传输其实应用于日常生活中比较多一些,比如说聊天,远程登录等,我们通过一个实验来观察下交互数据流,我这里使用一个127.0.0.1让server和client进行通信,server不断的回显client发送过去的数据。

1. Client 使用port 57600 连接到 Server 6666  IP 127.0.0.1

2. Client 发送s到Server

3. Server 收到s,并将s回显给Client

上述的过程明显是一个交互式的输入,看下tcpdump的抓包,其中4/5/6/7 四个包是客户端发送s,然后回显的过程,简单的分析下

a. 4号包表示,客户端发送一个s数据到 服务器

b. 5号包表示,单独的一个ACK,即告诉客户端已经收到s了

c. 6号包表示,服务器回显数据s

d. 7号包表示,客户端确认已经收到回显

不难看出,5/6号包可以将确认和数据完全一起发送,这样可以减少网络中ACK的小包了,提高这类数据的发送效率。其实这种技术就叫delay ACK

Delay Ack

简单的说,Delay Ack就是延时发送ACK,在收到数据包的时候,会检查是否需要发送ACK,如果需要的话,进行快速ACK还是延时ACK,在无法使用快速确认的条件下,就会使用Delay Ack。

TCP在何时发送ACK的时候有如下规定:

1.当有响应数据发送的时候,ACK会随着数据一块发送

2.如果没有响应数据,ACK就会有一个延迟,以等待是否有响应数据一块发送,但是这个延迟一般在40ms~500ms之间,一般情况下在40ms左右,如果在40ms内有数据发送,那么ACK会随着数据一块发送,对于这个延迟的需要注意一下,这个延迟并不是指的是收到数据到发送ACK的时间延迟,而是内核会启动一个定时器,每隔200ms就会检查一次,比如定时器在0ms启动,200ms到期,180ms的时候data来到,那么200ms的时候没有响应数据,ACK仍然会被发送,这个时候延迟了20ms.

3.如果在等待发送ACK期间,第二个数据又到了,这时候就要立即发送ACK!

    优点:减少了数据段的个数,提高了发送效率

    缺点:过多的delay会拉长RTT

    先通过一组实验来看下效果: 分别让服务器在0ms/30ms/40ms/50ms延时的情况下进行回显,看看数据的交互情况

    0ms的状况:第一个字符没有Delay ACK的状况,后面的就一直出现data和ACK一起发送的状况,也就是说ACK不是立即回复,然后在定时器到达之前有数据发送和数据一块发送

    30ms的状况:我们看到第一个字符和0ms的状况一样,也是快速回复ACK,之后的字符就是delay ACK,接收到数据不会立马回复ACK,等了30ms左右,有数据需要发送!看起来30ms定时器也没有到期

    40ms的状况:这个就不在有delay ACK的状况,比较奇怪,可能是kernel有些优化吧,基本都是立即回复ACK了,感觉像是满足了快速回复ACK的条件

    50ms的状况:50ms的情况和40ms的情况是一致的。

    通过做实验我们可以得到这样的一个结论:

    1.一般情况下载40ms内基本都会等待data,如果有data就会一块发送,但是超出了这个时间,都是使用的快速ACK,具体原因目前未知

    2.无论多少ms的延时,第一个包都是使用快速ACK,具体原因未知所以我们这个地方还留有2个疑问,等到后续弄明白再来补充。

关闭Delay ACK

    如果需要立即回复ACK,那么可以使用TCP_QUICKACK这个选项来启动快速ACK,先看下Linux关于这个option的描述:Enable quickack mode if set or disable quickack mode if cleared.  In quickack mode, acks are sent immediately, rather than delayed if needed in  accordance  to  normal  TCP operation.   This  flag  is not permanent, it only enables a switch to or from quickack mode.  Subsequent operation of the TCP protocol will once again enter/leave quickack  mode depending on internal protocol processing and factors such as delayed ack timeouts occurring and data transfer.  This option should not be used in code intended to  be portable.

总结一下要点:

1. 如果在快速的ACK模式下,ACK被立即发送

2. 这个flag并不是永久的,系统会判定是交互数据流,仍然会启动delay ACK,所以这个flag在recv之后需要重新设置我们直接看下实验结果从结果来看:每一个字符回显过程,服务器都是data和ACK分开发送的,收到数据之后第一时间立即回复ACK包!如果不设置QIUCKACK的话,从30ms实验的结果来看,就是数据包和ACK一起发送。

参考链接:https://blog.csdn.net/wdscq1234/article/details/52430382

21.CAP是什么?

22.知道哪些设计模式?深拷贝和浅拷贝有什么区别?

上一篇下一篇

猜你喜欢

热点阅读