蚂蚁金服2020春招面经以及沉淀反思,论面试官的套路模式

文章目录

4.29一面

1.先来聊聊多线程吧,先说一下为什么要用到多线程?

  在刚开始解除到多线程的时候,我简单认为多线程就是为了提高性能的。后来了解的多了才发现,多线程其实并不一定能提升性能,甚至有的时候为了保证线程安全还会影响性能。多线程的应用主要有以下几个优点:

  • 避免阻塞浪费大量时间: 这里可以利用多线程BIO实现思想来解释一下多线程这个优点。假设现在场景是在BIO模式下客户端通过HTTP请求服务端,经过tcp连接之后,服务端一直在等待客户端进行数据的传输,如果客户端一直没有发送数据,那么在单线程模式下,这个响应会被阻塞住,直到接收到客户端的数据为止,这一段时间处理器的大量运行时间被闲置了。而如果我们利用多线程模型,在链接建立完成进入阻塞状态的时候,就可以创建一个新的线程对该连接进行监控,在子线程获取到CPU的时间时对客户端传输的数据进行处理,一但发生阻塞就再次切换线程,执行别的事务。此时就可以大大提高CPU的运行效率。
  • 异步调用: 单线程模型中事件的执行是顺序执行的,下一个操作必须等待前一个操作完成后才能执行,而如果前一个操作非常耗时,就会影响下一个操作的效率。此时可以使用多线程进行异步调用,比如我们创建一个工单时,需要向表中插入数据,如果数据级联的表很多,数据量很大,可能较为耗时,此时我们可以创建一个新的线程去执行插入的操作,当前线程先返回创建结果,继续下一个操作,以此来提高执行效率。
  • 提高性能: 在一定的前提下,多线程确实可以提高程序的性能,但不是绝对的。主要需要满足一下几种情况:
    1. 任务需要具备可拆分性,也就是并发性。一个任务可以拆分成多个子任务,才有多线程的必要。可拆分的条件是比较严格的,如果一个操作依赖另外一个操作的结果,那么即使使用多线程也不会带来性能上的提升。
    2. 只有当CPU性能是处理任务的瓶颈时,多线程才能带来性能的提升。如果一系列操作的性能瓶颈是磁盘的IO速度,那么即使使用多线程,也是无法提高性能的。
    3. 需要CPU有多个核心。多线程和多核心CPU的关系有点像厨师和灶台,一个线程是一个厨师,一个CPU核心是一个灶台,只有多个灶台的情况下,厨师才可以并发炒菜。否则还是需要一堆厨师排队等待一个灶台进行使用。

2.多线程有哪些实现方式?

  这个问题比较基础,我回答了继承Thread,实现Runnable接口,线程池,Callable 这几种。然后面试官说,你说的这几种除了线程池,别的其实很少单独使用,都是在学习的时候才直接用。有些尴尬。
  【重要更新】重新查证了Oracle官方文档,发现实现多线程的方式其实只有两种:继承Thread,实现Runnable接口,文档地址:Oracle官方文档
  实现多线程的几种方式:

  • 继承Thread类:直接新建一个类继承Thread,覆盖其run()方法为自己的业务逻辑即可。
/*
 * 实现多线程的一个方法,继承Thread类
 */
public class MyThread extends Thread {

	private String name;
	
	public MyThread(String name) {
		this.name = name;
	}
	
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println("Thread start : " + this.name + " ,i=" + i);
		}
	}
	
}

  • 实现Runnable接口:Thread类实际上是一个实现了Runnable接口的类。Runnable接口是一个函数式接口,其中只有一个抽象的run()方法,其本身是不具备任何多线程的特点的。但是Thread类有一个构造函数接收Runnable的实现类,完成run方法的构造。所以可以实现一个Runable接口类,传参进Thread类中来实现一个线程。
/* 
 * 通过实现Runnable接口,完成一个Runnable类
 */
public class MyRunnable implements Runnable {

	private String name;
	
	public MyRunnable(String name) {
		this.name = name;
	}
	
	@Override
	public void run() {
		for (int i = 0; i < 10; i++) {
			System.out.println("Thread start : " + this.name + " ,i=" + i);
		}
	}
}

/*
 1. 传参进Thread实现类中
 */
 public class RunnableDemo {
	public static void main(String[] args) {
		MyRunnable mr1 = new MyRunnable("Runnable1");
		MyRunnable mr2 = new MyRunnable("Runnable2");
		MyRunnable mr3 = new MyRunnable("Runnable3");
		Thread t1 = new Thread(mr1);
		Thread t2 = new Thread(mr2);
		Thread t3 = new Thread(mr3);
		t1.start();
		t2.start();
		t3.start();
	}
}
  1. 利用线程池创建:Executors类创建线程池。
  2. 实现Callable接口创建线程:实现Callable重写call方法。实现Callable和实现Runnable类似,但是功能更强大,具体表现在
    • 可以在任务结束后提供一个返回值,Runnable不行
    • call方法可以抛出异常,Runnable的run方法不行
    • 可以通过运行Callable得到的Fulture对象监听目标线程调用call方法的结果,得到返回值,(fulture.get(),调用后会阻塞,直到获取到返回值)

3.那你说一下你了解哪些线程池,还有线程池的一些关键参数吧。

  线程池的考点最近越来越喜欢问了。

3.1 首先,线程池有几种分类,适合不同场景下的使用:

  1. 1.newFixedThreadPool(int nThreads) :一个指定工作线程数量的线程池。
  2. 2.newCachedThreadPool() :一个用来处理大量短时间工作任务的线程池,其特点如下:
    • 2.1 试图缓存线程并重用,当无缓存线程可用的时候,就会创建新的工作线程。
    • 2.2 如果线程限制的时间超过阈值,就会被终止并移除缓存。
    • 2.3 系统长时间闲置的时候,newCachedThreadPool()不会消耗什么资源
  3. 3.newSingleThreadExecutor() :创建唯一的工作线程来执行任务,如果线程异常结束,创建另一个线程来取代它。
  4. 4.newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize):定时或者周期性的工作调度,两者的区别在于前者是单一工作线程,后者是多个线程。
  5. 5.newWorkStealingPool() :该种线程池内部会创建ForkJoinPool,利用working-stealing算法,并行地处理任务,不保证处理顺序。
    • 5.1 Fork/Join框架:是一种把大任务分割成若干个小任务并行执行,最终将每个小任务的结果汇总成大任务记过的框架。
    • 5.2 working-stealing算法:一种窃取算法,可以允许某个线程从其他线程的队列中窃取任务来执行。

3.2 线程池的关键参数(就是ThreadPoolExecutor的构造函数):

  1. corePoolSize :核心线程池数量。
  2. maximumPoolSize :线程不够用时所允许创建的最大线程数。
  3. workQueue :任务等待队列。
  4. keepAliveTime :线程池维护线程所允许的最大时间,即空闲线程存活时间。
  5. TimeUnit :时间单位
  6. threadFactory :线程池创建线程使用的工厂,默认是传Executors.defaultThreadFactory()。
  7. handler :线程池的饱和策略,即对拒绝任务的处理策略。
    在这里插入图片描述

4. 线程池execute一个任务之后,线程池的执行过程

  1. 如果线程池中运行的线程少于corePoolSize设定值,则创建新的线程来执行任务,即使线程池中其他线程是空闲的。
  2. 如果线程池中的线程数量大于等于corePoolSize并且小于maximumPoolSize,队列workQueue未满,则将新添加的任务放到workQueue中。
  3. 如果线程池中的线程数量大于等于corePoolSize并且小于maximumPoolSize,队列workQueue已满,则会创建新的线程来处理被添加的任务。
  4. 如果线程池设置的corePoolSize和maximumPoolSize相同,则线程池的大小是固定的,此时新任务提交,若队列workQueue未满,则将新添加的任务放到workQueue中。
  5. 如果线程池中的线程数量大于等于了maximumPoolSize,就执行拒绝策略handler 。

5.线程安全是什么?为什么会出现线程安全问题?

  • 线程安全:当多个线程同时访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行文都可以获得正确的结果,那就称这个对象时线程安全的。
  • 为什么会出现线程安全问题:多线程操作共享资源时,导致共享资源出现错乱就是线程安全问题。其本质上是因为每个线程都有自己独立的工作空间。
    这里先介绍下JMM,即Java内存模型(注意不是运行时数据区模型,不一样)。JMM中将内存分为主内存和工作内存,主内存属于数据共享的区域,存储的实例对象和数据信息。每个线程会有自己的工作内存,存储当前方法的所有本地变量信息,线程的工作内存对其他线程不可见。线程在工作时并不是直接在主存上直接操作的,而是会将数据从主存拷贝一份到工作内存中,操作完成后刷新回主内存。
    所以在这一过程中,多个线程对数据的操作都是在自己的工作内存当中的,当多个线程操作同一对象,最后刷新回主存时,可能发生数据不一致现象,就会出现线程安全问题。

6.如果我自己写了一个类,把对象放入HashMap中,需要注意什么?

  • 用自定义类作为key,必须重写equals()和hashCode()方法:因为自定义类中的equals() 和 hashCode()都继承自Object类。Object类的hashCode()方法返回这个对象存储的内存地址的编号。而equals()比较的是内存地址是否相等。

7.那给你个场景,如果我两个对象的hashcode一样,equals不一样,put进Map会发生什么,拿出来的时候呢?hashcode不一样,equals一样会发生什么?

  • 相等的概念:Object版本的equal只是简单地判断是不是同一个实例,也就是内存地址是否相同。但是有的时候,我们想要的的是逻辑上的相等,比如一个Book类,我们想利用其中的prices属性来判断两本书是否相等,此时就需要重写equals。如果涉及到了HashMap,那么重写了equals就需要重写hashcode。
  • 重写equals()和hashcode()的原则:
    • 1.同一个对象,在没有发生修改的情况下,无论何时调用hashcode()得到的返回值必须一致:
      • 因为HshMap在put元素的时候首先利用hash值(调用hashcode()得到的返回值再进行散列处理)来判断当前元素应该存放的桶的位置。如果get的时候hash值计算发生了变化,那么就无法再映射到原来的桶中,取出元素。
        在这里插入图片描述
        在这里插入图片描述
    • 2.hashCode()的返回值相等的对象不一定相等,通过hashCode()和equals()必须能唯一确定一个对象:由于一个桶中所挂的所有节点的hash值都是一样的,所以必须再次通过equals()值来找到想要的元素。
    • 3.一旦重写了equals()函数(重写equals的时候还要注意要满足自反性、对称性、传递性、一致性),就必须重写hashCode()函数。而且hashCode()的生成哈希值的依据应该是equals()中用来比较是否相等的字段,即equals()相同的的元素其hashCode()也要相同才能安全放入HashMap中。
  • 回答面试官的问题:
    • 1.如果我两个对象的hashcode一样,equals不一样,put进Map会发生什么,拿出来的时候呢?
      • 如果两个对象的hashcode一样,equals不一样,会正常put进Map,只不过会发生hash碰撞,以链表或者红黑树的形式挂在同一个桶中,而且因为二者的equals结果不一样,也可以正常取出。
    • 2.hashcode不一样,equals一样会发生什么?
      • 如果两个对象的equals一样的话,从业务逻辑上讲我们是想要把这两个对象认为是同一个对象的。但是由于二者的hashcode不一样,在put进Map中的时候,二者会被映射到不同的位置,即不同的桶中,没有调用equals比较的机会。所以这时两个业务上相等的对象会被放进不同的位置,出现错误。这就是为什么在重写hashCode()时,生成的哈希值依据应该是equals()中用来比较是否相等的字段。

10.HashMap为什么线程不安全,想要线程安全怎么办?

  • 1.多线程put数据不一致问题
    • 假设有两个线程A和B,都在执行put任务,线程A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后遍历该桶的链表直到尾结点(JDK1.8之后HashMap变成了尾插),就在要插入数据之前,线程A的CPU时间片用完了,此时调度器调度线程B开始执行,与A不同的是,线程B成功将元素put进了Map中,而且线程Bput的元素和线程A的hash值是一样的。那么当线程A再次被调度的时候,它持有过期的链表尾,但是线程A对此一无所知,直接进行了插入操作,此时线程Bput的数据就会被覆盖掉,造成了数据不一致问题。
  • 2.并发导致的死循环问题
    • 在JDK1.8之前,由于HashMap使用的是头插法,在多线程resize()扩容的时候,有可能会造成链表成环的死循环。因为在 JDK1.7 中采取的是头插法,遍历一个节点就插入一个节点到新的哈希桶数组,所以才会导致出现循环链表。不过这个问题已经在JDK1.8中得到了解决,JDK8使用两个头结点来保持旧链表的引用,直到该索引处对应的链表全部遍历完之后再分别把头结点放在新的哈希桶数组对应的位置。而不是遍历一个节点就插入一个节点到新的哈希桶数组。所以不会出现死链。
    • 但是JDK1.8真的不会出现死循环了么?实际上还是会的,只不过出现的位置不一样,JDK1.8中的死循环一般出现在对红黑树的操作中,比如链表树化的时候还有红黑树的子节点父节点相互引用死循环问题,可以看下这个链接:HashMap在jdk1.8也会出现死循环的问题(实测)
    • 3.想要线程安全就加锁,或者直接使用ConcurrentHashMap。

11.ConcurrentHashMap怎么保证线程安全的。

  • ConcurrentHashMap是利用锁来保证线程安全的,ConcurrentHashMap的锁经历了两个阶段的优化,首先是第一个阶段:分段锁。然后优化成了当前的CAS+synchronized机制的锁。
  • 分段锁(Segment)阶段:早期的ConcurrentHashMap还是数据+链表的数据结构,没有引入红黑树,当时的加锁机制是锁住整个桶,如果有线程对桶进行操作,就会锁住整个桶,其他线程无法操作。线程释放锁之后,其他线程才能再次进入这个桶。以此来保证线程安全。
  • CAS机制阶段:在当前的ConcurrentHashMap中,数据结构和HashMap一样,已经变成了数组+链表+红黑树的结构。锁的机制也升级成了CAS+synchronized,如果元素要put的桶是空的,那么就利用CAS进行元素的添加,失败就循环尝试。如果桶不为空,发生了hash碰撞,就对头结点加synchronized锁来进行后续的操作。
    在这里插入图片描述

12.LinkedList了解么?底层是什么样的?各种操作的时间复杂度。

  • LinkedList是JDK提供的一个链表类,其底层是一个双向链表。
  • 操作时间复杂度:
    • add(E):尾插法,O(1)。
    • remove():指针指向操作,O(1)。
    • get(index):这个在面试的时候我说是O(n),面试小哥告诉我不是的,比这个快。当时我想了半天,链表都是要遍历才能找到元素的啊,后来在面试官的提示下,我想到了双向链表的get,可以先计算位置离头结点近还是尾结点近,然后从比较近的那个节点遍历。时间复杂度是O(n/2)。虽然也是O(n)范畴的,但这里面试官还是想考察有没有看过源码,我没有看过,但是后面想出来了,所以面试官还挺开心的。

13.Sping的AOP,AOP底层怎么实现的?

  这个可以写一大篇了,各位看官自行查询吧。

14.数据库相关:了解数据库引擎么?InnoDB的索引数据结构了解么?如果不是主键索引,二级索引的结构是什么样子?数据库事务,特点和隔离级别,会出现的各种问题,幻读是什么?

  这个可以看我的数据库知识总结专栏:Java后端面试学习知识总结指南——数据库:MySQL

15.计算机网络相关:三次握手过程,cookie和session是什么?

  这个可以看我的计网知识总结专栏:(还没写,挖坑)

16.为什么想转专业?(我本科化学,研究生电子)讲一下项目中遇到的困难,吸取的教训,有什么可以改进的地方?讲一下笔试题吧,当时怎么想的,现在有思路么?下来有优化么?

  个人相关问题,就不详细展开了。

5.9二面

1.什么时候开始学java?为什么学java?怎么学习的,学习历程讲一下?平时写过什么代码?关注过什么开源社区?看过什么开源项目?

  个人相关问题,就不详细展开了。

2.数据库索引结构,为什么用B+树,还有其他的索引结构可以使用么?

  这个可以看我的数据库知识总结专栏:Java后端面试学习知识总结指南——数据库:MySQL

3.JVM内存模型,新生代和老年代之间有什么关系?什么时候对象会放进老年代?一个static的hashmap会放进新生代还是老年代?一个放进Threadlocal的hashmap会放在新生代还是老年代?(套路问题,我被面试官挖坑埋了)

  碍于个人水平,这里是我查阅了很多资料后自行分析的答案,可能存在错误,如果有错误还请读者及时在评论区指正,大家一起探讨。

  • 新生代和老年代的关系:新生代和老年代是Java分代垃圾回收算法划分出的不同逻辑区域,而实际上,新生代和老年代在JVM内存模型中,都是放在堆内存中的,只不过按照分代收集的理论思想,将堆内存划分成不同的区域,这些区域划分只不过是一部分垃圾收集器的设计风格,而并非JAVA虚拟机具体实现的内存布局,更不是《Java虚拟机规范》中的规定。
    • 《Java虚拟机规范》中对Java堆的的描述是:所有对象示例以及数组都应当在对上分配。虽然随着Java虚拟机技术的发展,栈上分配的优化手段导致了变化的发生,但这一规定绝大多数时候还是适用的。
    • 从新生代到老年代:在分代回收理论中,对象的建立会优先分配在新生代的Eden区中,当Eden区没有足够的分配空间,就会发生一次Minor GC,而如果新建立的对象是个非常大的对象,新生代放不下,则会由分配担保机制直接放到老年代中。如果一个对象在新生代存活超过15岁(默认值,每活过一次Minor GC,年龄就加一岁),则将对象转移到老年代当中。
  • 一个static的hashmap会放进新生代还是老年代:这个问题其实容易被表面的问法迷惑,当时我就被面试官问懵逼了。当时有点慌张,其实只需要仔细一分析,答案就出来了,首先一个static的HashMap分成两个部分,一个部分是静态变量部分,一个部分是new出来的HashMap实例部分。
  • public static HashMap<Integer> map = new HashMap<>();以这行代码为例,new HashMap<>();是创建了一个新的对象,会直接在堆内存中开辟空间,那么放在年轻代还是老年代就要看这个对象的大小了,如果特别大就会直接放进老年代,否则就放在年轻代。
  • static HashMap<Integer> map 这一部分,是一个静态变量,其放置的位置和JDK版本有关:
    • JVM运行时数据区中,有一块内存区域叫做方法区,在JDK7之前的HotSpot虚拟机中,方法区的实现叫做永久代,永久代和堆一样是线程共享的区域,该区域负责存储已经被虚拟机加载的类型信息、常量(final)、静态变量(static)、字符串常量池、代码缓存等数据。JDK7之前,静态变量会放进永久代。
    • 而JDK7之后,JVM开发团队逐渐意识到使用永久代设计的不合理之处,于是将字符串常量池、静态变量等数据移出永久代,放入了堆中,在JDK8之后,永久代被移除,取而代之的是实现了方法区的元空间(MetaSpace),元空间的内存是开辟在本地内存中的,理论上其空间取决于你内存条的大小。所以JDK8之后,静态变量也会随着对象一起放进堆中。
  • 一个放进Threadlocal的hashmap会放在新生代还是老年代:这个问题也是很让人懵逼的,其实和上一个问题是一样的,面试官只不过是通过加入各种知识点来轰炸迷惑你,仔细分析一下答案也可以出来。
    • Threadlocal是什么:在线程当中,有一片属于线程自己的私家领地,就是ThreadlocalMap,线程会将Threadlocal实例化出来的对象当做key,存放进Threadlocal的数据当做value,放进这个Map中,这样数据就私有化了。当调用Threadlocal对象的set方法时,会先获取当前线程,再拿到当前线程的ThreadlocalMap,get也一样,以此来防止别的线程访问数据。
    • 放进Threadlocal的hashmap:由上面的介绍可以分析出,一个对象如果被放进Threadlocal中,其实只是将其引用放进了线程的ThreadlocalMap的Value中,必须通过当前线程的ThreadlocalMap才能访问到,别的线程访问不到。实际上这个hashmap在内存中的位置是没有变化的,只是其引用被放到了一个Map中,以此来阻隔其他线程的访问。那么这个问题的答案就出来了,该放哪就放哪,出生地还是新生代,如果活过15岁,就进入了老年代。

4.怎么停止一个线程?如果你一个线程链接服务器的时候,我服务端无限挂起,用interrupt()可以正常停止线程么?

  • 怎么停止一个线程:
    • 在旧版本的Java中,提供了stop,suspend,resume方法用来停止线程,不过这些方法都已经被废弃了,JDK不推荐开发者使用,这主要是因为作为开发者的程序员其实并不能很好把握线程的停止时机,特别是多线程和多人协作开发的情况下,很容易由于强制停止线程导致数据丢失问题。
    • 后来Java实现了一个让线程自己停止的机制interrupt。interrupt机制是用来通知线程需要停止,而不是强制其停止。
  • 服务端无限挂起,用interrupt可以正常停止线程么?
    • 是可以的。因为如果interrup机制在被阻塞的时候依然可以响应中断。

5.HashMap怎么防止哈希冲突的,还有什么方法?

  • HashMap是使用链表发来防止哈希冲突的,每个哈希桶都对应一条链表。如果在put的时候发生了Hash冲突,那么就会将元素挂在链表的后面。
  • HashMap还使用了扰动函数,通过散列的过程来减少Hash冲突的概率:当获取到key的hashcode()值之后,会进一步处理,将其hashcode()值无符号右移16位,将高位信息右移到低位信息上,在与其hashcode()值进行异或运算(^)得到散列后的Hash值,最后再用Hash值与HashMap的数组容量长度-1相与(&),得到key应该放置的桶的位置。

6.装饰器模式和代理模式?用装饰器模式和代理模式同时去实现一个对象的get方法,这两个是有什么区别?JDK中哪里用到了装饰器模式,哪里用带了代理模式?

  • 装饰器模式和代理模式都是用来对类进行增强的。二者其实很相似,对装饰器模式来说,装饰者(decorator)和被装饰者(decoratee)都实现同一个 接口。对代理模式来说,代理类(proxy class)和真实处理的类(real class)都实现同一个接口。此外,不论我们使用哪一个模式,都可以很容易地在真实对象的方法前面或者后面加上自定义的方法。
  • 然而,实际上,在装饰器模式和代理模式之间还是有很多差别的。装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问。换句话说,用代理模式,代理类(proxy class)可以对它的客户隐藏一个对象的具体信息。因此,当使用代理模式的时候,我们常常在一个代理类中创建一个对象的实例。并且,当我们使用装饰器模式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。
  • 我们可以用另外一句话来总结这些差别:使用代理模式,代理和真实对象之间的的关系通常在编译时就已经确定了,而装饰者能够在运行时递归地被构造。
  • 用装饰器模式和代理模式同时去实现一个对象的get方法,这两个是有什么区别?
    • 代理模式实现get方法,我们可以不关注这个对象的细节,利用代理在代理类中创造这个对象的实例,用户可以直接拿代理类去使用。
    • 装饰器模式,我们一般将原始对象作为参数传递给装饰者,如果有多个对象,用户可以决定装饰哪个对象的get方法。

NIO的过程,过程中使用的东西都是什么?

tcp粘包了解么?

  • tcp的粘包:TCP粘包就是指发送方发送的若干包数据到达接收方时粘成了一包,从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾,出现粘包的原因是多方面的,可能是来自发送方,也可能是来自接收方。

    • 发送方原因导致粘包:发送端为了将多个发往接收端的包,更加高效的的发给接收端,于是采用了优化算法(Nagle算法),将多次间隔较小、数据量较小的数据,合并成一个数据量大的数据块,然后进行封包,造成了发送方可能会出现粘包问题
    • 接收方原因:TCP接收到数据包时,并不会马上交到应用层进行处理,会将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。
  • 如何解决tcp中的粘包:

    • 对于发送方导致的tcp粘包,可以关闭Nagle算法来防止粘包。
    • 对于接收方导致的tcp粘包,在传输层是无法解决的,只能交由应用层来解决。
    • 应用层解决tcp粘包:1.格式化数据:每条数据有固定的格式(开始符,结束符),这种方法简单易行,但是选择开始符和结束符时一定要确保每条数据的内部不包含开始符和结束符。2.发送长度:发送每条数据时,将数据的长度一并发送,例如规定数据的前4位是数据的长度,应用层在处理时可以根据长度来判断每个分组的开始和结束位置。

tcp四次挥手,哪个环节可以连接重置?链接重置是什么?

写题:实现一个LRU。

写题:多线程赛跑问题。用多线程实现,5个运动员赛跑,一个裁判,满足:5个运动员必须同时起跑,裁判必须等所有线程跑完才能进行下一步操作,只要有一个运动员摔倒就直接全部线程停止。

  • 5
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值