java开发工程师面试常问问题
博主开源项目,欢迎意见和star
https://gitee.com/xuelingkang/spring-boot-demo
https://gitee.com/xuelingkang/react-demo
https://gitee.com/xuelingkang/websocket
https://gitee.com/xuelingkang/zookeeper
网站
https://demo.xzixi.com
程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器,这里不会出现OutOfMemoryError。
线程私有;生命周期和线程一致,每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程;存储局部变量表、操作数栈、动态链接、方法出口等信息。(局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址);会出现的异常:1、StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度;2、OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。)
与虚拟机栈类似,区别是虚拟机栈运行java方法,本地方发展运行的是native方法,也会有StackOverflowError和OutOfMemoryError。
线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量,运行时常量池,在老版jdk,方法区也被称为永久代【因为没有强制要求方法区必须实现垃圾回收,HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。
线程共享,jvm管理的内存中最大的一块区域,多线程下需要同步,会抛出OutOfMemoryError,如果堆中没有内存完成实例分配,且堆无法再扩展时抛出该异常。
程序计数器、本地方法栈、虚拟机栈生命周期与线程一致,由线程而生,随线程而灭,当线程执行结束后,内存会自动回收,所以无需关心。
而堆和方法区是线程共享的,不会随着线程执行完毕而回收,gc关注的也正是这部分的内存。
早期判断对象是否存活大多都是以这种算法,这种算法判断很简单,简单来说就是给对象添加一个引用计数器,每当对象被引用一次就加1,引用失效时就减1。当为0的时候就判断对象不会再被引用。
优点:实现简单效率高,被广泛使用与如python何游戏脚本语言上。
缺点:难以解决循环引用的问题,就是假如两个对象互相引用已经不会再被其它其它引用,导致一直不会为0就无法进行回收。
目前主流的商用语言[如java、c#]采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。
它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的。
可作为GC Roots的对象有四种
①虚拟机栈(栈桢中的本地变量表)中的引用的对象,就是平时所指的java对象,存放在堆中。
②方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中。
③方法区中的常量引用的对象。
④本地方法栈中JNI(native方法)引用的对象。
即使可达性算法中不可达的对象,也不是一定要马上被回收,还有可能被抢救一下。
要真正宣告对象死亡需经过两个过程。
1.可达性分析后没有发现引用链
2.查看对象是否有finalize方法,如果有重写且在方法内完成自救[比如再建立引用],还是可以抢救一下,注意这边一个类的finalize只执行一次,这就会出现一样的代码第一次自救成功第二次失败的情况。[如果类重写finalize且还没调用过,会将这个对象放到一个叫做F-Queue的序列里,这边finalize不承诺一定会执行,这么做是因为如果里面死循环的话可能会时F-Queue队列处于等待,严重会导致内存崩溃,这是我们不希望看到的。]
新生代采用复制算法,复制算法比较适合用于存活率低的内存区域,它优化了标记/清除算法的效率和内存碎片问题,且JVM不以5:5分配内存【由于存活率低,不需要复制保留那么大的区域造成空间上的浪费,因此不需要按1:1【原有区域:保留空间】划分内存区域,而是将内存分为一块Eden空间和From Survivor、To Survivor【保留空间】,三者默认比例为8:1:1,优先使用Eden区,若Eden区满,则将对象复制到第二块内存区上。但是不能保证每次回收都只有不多于10%的对象存货,所以Survivor区不够的话,则会依赖老年代年存进行分配】。
由于老年代存活率高,没有额外空间给他做担保,必须使用这两种算法。
年轻代收集器
Serial、ParNew、Parallel Scavenge
老年代收集器
Serial Old、Parallel Old、CMS收集器
特殊收集器
G1收集器[新型,不在年轻、老年代范畴内]
最基本、发展最久的收集器,在jdk3以前是gc收集器的唯一选择,Serial是单线程收集器,Serial收集器只能使用一条线程进行收集工作,在收集的时候必须得停掉其它线程,等待收集工作完成其它线程才可以继续工作。
优点:对于Client模式下的jvm来说是个好的选择。适用于单核CPU【现在基本都是多核了】
缺点:收集时要暂停其它线程,有点浪费资源,多核下显得。
可以认为是Serial的升级版,因为它支持多线程[GC线程],而且收集算法、Stop The World、回收策略和Serial一样,就是可以有多个GC线程并发运行,它是HotSpot第一个真正意义实现并发的收集器。默认开启线程数和当前cpu数量相同【几核就是几个,超线程cpu的话就不清楚了 - -】,如果cpu核数很多不想用那么多,可以通过-XX:ParallelGCThreads来控制垃圾收集线程的数量。
优点:
1.支持多线程,多核CPU下可以充分的利用CPU资源
2.运行在Server模式下新生代首选的收集器【重点是因为新生代的这几个收集器只有它和Serial可以配合CMS收集器一起使用】
缺点: 在单核下表现不会比Serial好,由于在单核能利用多核的优势,在线程收集过程中可能会出现频繁上下文切换,导致额外的开销。
Java1.8默认的收集器,采用复制算法的收集器,和ParNew一样支持多线程。
但是该收集器重点关心的是吞吐量【吞吐量 = 代码运行时间 / (代码运行时间 + 垃圾收集时间) 如果代码运行100min垃圾收集1min,则为99%】
对于用户界面,适合使用GC停顿时间短,不然因为卡顿导致交互界面卡顿将很影响用户体验。
对于后台
高吞吐量可以高效率的利用cpu尽快完成程序运算任务,适合后台运算
Parallel Scavenge注重吞吐量,所以也成为"吞吐量优先"收集器。
和新生代的Serial一样为单线程,Serial的老年代版本,不过它采用"标记-整理算法",这个模式主要是给Client模式下的JVM使用。
如果是Server模式有两大用途
1.jdk5前和Parallel Scavenge搭配使用,jdk5前也只有这个老年代收集器可以和它搭配。
2.作为CMS收集器的后备。
支持多线程,Parallel Scavenge的老年版本,jdk6开始出现, 采用"标记-整理算法"【老年代的收集器大都采用此算法】
在jdk6以前,新生代的Parallel Scavenge只能和Serial Old配合使用【而Parallel Scavenge又不能和CMS配合使用】,而且Serial Old为单线程Server模式下会拖后腿【多核cpu下无法充分利用】,这种结合并不能让应用的吞吐量最大化。
Parallel Old的出现结合Parallel Scavenge,真正的形成“吞吐量优先”的收集器组合。
CMS收集器(Concurrent Mark Sweep)是以一种获取最短回收停顿时间为目标的收集器。【重视响应,可以带来好的用户体验,被sun称为并发低停顿收集器】
它的运作分为4个阶段
1.初始标记:标记一下GC Roots能直接关联到的对象,速度很快
2.并发标记:GC Roots Tarcing过程,即可达性分析
3.重新标记:为了修正因并发标记期间用户程序运作而产生变动的那一部分对象的标记记录,会有些许停顿,时间上一般 初始标记 < 重新标记 < 并发标记
4.并发清除
以上初始标记和重新标记需要stw(停掉其它运行java线程)
之所以说CMS的用户体验好,是因为CMS收集器的内存回收工作是可以和用户线程一起并发执行。
缺点:
1.cms对cpu特别敏感,cms运行线程和应用程序并发执行需要多核cpu,如果cpu核数多的话可以发挥它并发执行的优势,但是cms默认配置启动的时候垃圾线程数为 (cpu数量+3)/4,它的性能很容易受cpu核数影响,当cpu的数目少的时候比如说为为2核,如果这个时候cpu运算压力比较大,还要分一半给cms运作,这可能会很大程度的影响到计算机性能。
2.cms无法处理浮动垃圾,可能导致Concurrent Mode Failure(并发模式故障)而触发full GC
3.由于cms是采用"标记-清除“算法,因此就会存在垃圾碎片的问题,为了解决这个问题cms提供了 -XX:+UseCMSCompactAtFullCollection选项,这个选项相当于一个开关【默认开启】,用于CMS顶不住要进行full GC时开启内存碎片合并,内存整理的过程是无法并发的,且开启这个选项会影响性能(比如停顿时间变长)
G1(garbage first:尽可能多收垃圾,避免full gc)收集器是当前最为前沿的收集器之一(1.7以后才开始有),同cms一样也是关注降低延迟,是用于替代cms功能更为强大的新型收集器,因为它解决了cms产生空间碎片等一系列缺陷。
摘自甲骨文:适用于 Java HotSpot VM 的低暂停、服务器风格的分代式垃圾回收器。G1 GC 使用并发和并行阶段实现其目标暂停时间,并保持良好的吞吐量。当 G1 GC 确定有必要进行垃圾回收时,它会先收集存活数据最少的区域(垃圾优先)
g1的特别之处在于它强化了分区,弱化了分代的概念,是区域化、增量式的收集器,它不属于新生代也不属于老年代收集器。
用到的算法为标记-清理、复制算法
g1是区域化的,它将java堆内存划分为若干个大小相同的区域【region】,jvm可以设置每个region的大小(1-32m,大小得看堆内存大小,必须是2的幂),它会根据当前的堆内存分配合理的region大小。
g1通过并发(并行)标记阶段查找老年代存活对象,通过并行复制压缩存活对象【这样可以省出连续空间供大对象使用】。
g1将一组或多组区域中存活对象以增量并行的方式复制到不同区域进行压缩,从而减少堆碎片,目标是尽可能多回收堆空间【垃圾优先】,且尽可能不超出暂停目标以达到低延迟的目的。
g1提供三种垃圾回收模式 young gc、mixed gc 和 full gc,不像其它的收集器,根据区域而不是分代,新生代老年代的对象它都能回收。
几个重要的默认值
g1是自适应的回收器,提供了若干个默认值,无需修改就可高效运作
-XX:G1HeapRegionSize=n 设置g1 region大小,不设置的话自己会根据堆大小算,目标是根据最小堆内存划分2048个区域
-XX:MaxGCPauseMillis=200 最大停顿时间 默认200毫秒
第一次Minor GC,将Eden区存活对象复制到Survivor0区(如果Survivor区放不下则直接进入老年代),清空Eden区,所有存活对象年龄+1
第二次Minor GC,将Eden区和Survivor0区存活对象复制到Survivor1区(如果Survivor区放不下则直接进入老年代),清空Eden区,所有存活对象年龄+1
。。。
第n次Minor GC,将年龄15(默认15,可配置)的对象移到老年代,年龄小于15的对象继续重复1,2步骤
增加年轻代比例会减少Minor GC频率,增加单次Minor GC时间,增加老年代比例会减少Full GC频率,增加单次Full GC时间,jvm优化就是要达到一个平衡,合格的GC,括号中的值仅做参考
Minor GC执行非常迅速(50ms以内)
Minor GC没有频繁执行(大约10s执行一次)
Full GC执行非常迅速(1s以内)
Full GC没有频繁执行(大约10min执行一次)
复制对象的成本要远高于扫描成本,所以,单次Minor GC时间更多取决于GC后存活对象的数量,而非Eden区的大小,因此如果堆中短期对象很多,那么扩容新生代,单次Minor GC时间不会显著增加,但是Minor GC的频率会变低。
InputStream类是字节输入流的抽象类,是所有字节输入流的父类,InputStream类具有层次结构如下图所示。
java中的字符是Unicode编码的,是双字节的。InputStream是用来处理字节的,在处理字符文本时很不方便。Java为字符文本的输入提供了专门的一套类Reader。Reader类是字符输入流的抽象类,所有字符输入流的实现都是它的子类。
输出流OutputStream类是字节输入流的抽象类,此抽象类表示输出字节流的所有类的超类。
Writer类是字符输出流的抽象类,所有字符输出类的实现都是它的子类。
有序,可重复
ArrayList用数组实现,随机访问速度快,增删元素速度慢
扩容:默认大小是10,每次扩容后长度变为原来1.5倍,调用Arrays.copyOf()
增删元素:在末尾增加元素,如果有空余空间,速度也很快,空间不足时需要先扩容,所以速度较慢
指定其他位置增加元素,需要调用System.arraycopy()方法,速度较慢
LinkedList用双相链表实现,随机访问速度慢,增删元素速度快
不存在扩容问题
增删元素:增删元素只需要改变指针指向,速度快
线程安全的List,速度慢
无序,不重复
用HashMap实现,基本上都是调用HashMap的方法
有序,用二叉排序
无序,key不重复,value可以重复
数组加链表实现,默认大小16,默认负载因子0.75,key和value可以为null。
为什么默认大小时16,当数组长度是2的整数次幂时,通过hashCode计算数组下标的到的结果碰撞概率最小。
为什么默认负载因子是0.75,扩容阈值=初始化大小*负载因子,当负载因子过小时容易浪费空间,过大是影响扩容效率,设置为0.75可以达到时间和空间上的平衡。
put方法:调用key的hashCode()方法,根据返回的hashCode值对bucket数组长度取模,找到bucket位置来存储Entry对象
get方法:调用key的hashCode()方法,根据返回的hashCode值对bucket数组长度取模,找到bucket位置,获取Entry对象的value
当key的hashCode值相同但是equals比较结果不同时,发生hash碰撞,HashMap使用链表存储Entry对象,调用get方法时,首先找到bucket位置,然后调用equals()方法比较链表中的第一个Entry对象,如果不是则遍历剩余的Entry对象。
当bucket数组元素个数大于阈值是会对HashMap扩容,调用resize()方法,将数组长度扩容为原来的两倍,然后根据key的hashCode重新将Entry对象散列到bucket数组中。
jdk1.8底层是数组+链表+红黑树,jdk1.7是数组+链表。
用红黑树遍历查找的时间复杂度比链表低,插入删除的时间负责度比链表高,提高查询性能。
扩容策略:1.7中是只要不小于阈值就直接扩容2倍;而1.8的扩容策略会更优化,当数组容量未达到64时,以2倍进行扩容,超过64之后若链表中元素个数不小于8就将链表转换为红黑树,但如果红黑树中的元素个数不大于6就会还原为链表,当红黑树中元素不小于32的时候才会再次扩容。
key和value都不能为null,线程安全,对整个表上锁,性能较差,一般不用。
采用锁分段技术,每次只对操作的段上锁,其它段不上锁,并发场景下性能好。
String不可变字符序列,StringBuffer可变字符序列,线程安全,StringBuilder可变字符序列,线程不安全
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
步骤:
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
步骤:
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
步骤:
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
步骤:
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
步骤:
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
步骤:
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
步骤:
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
步骤:
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
步骤:
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
步骤:
主要参数:
参数计算:
核心线程数 = CPU可用核心数 / (1 - 阻塞系数)
阻塞系数 = 阻塞时间/总时间
也就是说,当任务是计算密集型,那么线程数=CPU可用核心数,当任务是io密集型,增加线程数可以在进行IO操作时让出CPU时间给其他线程。
任务队列容量需要根据实际场景最多允许线程阻塞的时间和单个任务执行时间确定,任务队列容量 = 核心线程数 / 单个任务耗时 * 最大阻塞时间,也就是计算核心线程在最大阻塞时间内能处理多少个任务。
最大线程数 = 高峰期单位时间任务数量 * 处理单个任务所需时间 / 最大阻塞时间,同时也需要综合考虑服务器性能。
常用设计模式:单例模式、代理模式、适配器模式、策略模式、观察者模式、责任链模式。
设计模式总的来说就是通过使用接口和抽象类使程序更好扩展和维护,只要遵循这样的原则就不必拘泥于模式。设计模式篇幅较大,请参考文末链接。
基于tcp的无状态协议,可以使用cookie记录状态。
http1.1新特性:
http请求流程:
建立连接三次握手:
第一次握手:客户端发送一个带SYN的TCP报文到服务器,表示客户端想要和服务器端建立连接。
第二次握手:服务器端接收到客户端的请求,返回客户端报文,这个报文带有SYN和ACK确认标示,访问客户端是否准备好。
第三次握手:客户端再次响应服务端一个ACK确认,表示客户端已经准备好了。
为什么是三次握手:
三次可以让双方都确定连接到对方后能收到对方的回复,双方都准备好了,保证连接的可靠性。
断开连接四次挥手:
第一次挥手:客户端发送一个FIN(结束),用来关闭客户端到服务器端的连接。
第二次挥手:服务器端收到这个FIN后发回一个ACK确认标示,确认收到。
第三次挥手:服务器端发送一个FIN到客户端,服务器端关闭客户端的连接。
第四次挥手:客户端发送ACK报文确认。
为什么是四次挥手:
因为客户端主动关闭连接,发送FIN,服务器接收到后回应ACK,表示服务器知道客户端已经发送完数据,准备端开,但是服务器可能还有数据没有发送完,所以服务器也需要发送一个FIN包,表示数据已经发送完成,可以断开了,客户端响应ACK或者超时后连接断开。
https和http的区别:
对称密钥加密:
所谓的对称密钥,就是加密和解密都用同一个密钥,因此也叫做共享密钥加密。
优点:处理速度快。
缺点:但是容易被第三方盗取。
非对称密钥加密:
所谓的非对称密钥,就是这是一对密钥,一个是私钥,一个是公钥。私钥不能让其他任何人知道,而公钥可以随意发布,任何人都可以获得到,因此这种加密方式也叫公开密钥加密。公钥用来加密,私钥用来解密。
优点:更加安全,不容易被盗取。
缺点:处理效率相比对称密钥加密要慢,如果在通信时用这种方式加密,效率很低。
https使用混合加密:
使用非对称加密的方式交换密钥,然后使用对称加密进行通信。
交换密钥步骤:服务器将公钥发送给客户端,客户端生成私钥,使用服务器公钥加密发送给服务器,服务器接收到加密后的私钥,使用自己的私钥解密,得到客户端的私钥,然后使用这个私钥进行通信。
工作原理:
四种隔离级别:
mysql默认隔离级别是可重复读。
索引其实是一种数据结构,能够帮助我们快速的检索数据库中的数据,InnoDB引擎,默认的是B+树。
B+树索引和Hash索引的区别:
查询优化器:
一条SQL语句的查询,可以有不同的执行方案,至于最终选择哪种方案,需要通过优化器进行选择,选择执行成本最低的方案。
在一条单表查询语句真正执行之前,MySQL的查询优化器会找出执行该语句所有可能使用的方案,对比之后找出成本最低的方案。
这个成本最低的方案就是所谓的执行计划。优化过程大致如下:
MySQL表级锁分为读锁和写锁,开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。
开锁大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
大量key同时过期,导致数据库压力过大甚至宕机。
解决方案:
一般是攻击者不断请求一个不存在的数据,导致数据库压力过大。
解决方案:
由于某个key过期,同时有大量请求并发访问该数据,导致数据库压力过大。
解决方案:
集群脑裂是指master节点网络故障,导致sentinel认为master节点宕机,将其他slave节点提升为master节点,当master节点网络恢复后出现了两个master节点,旧master节点降级为slave节点,网络故障期间写入的数据会丢失。
解决方案:
设置连接到master的最少slave数量和slave连接到master的最大延迟时间,如果连接到master的slave数量不足或者连接超时,master会拒绝写入数据,减少数据丢失。
先删除缓存,再修改数据库。如果删除缓存成功,修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致,如果删除缓存失败不执行修改数据库,在并发量不是特别高,且对于双写不一致问题要求不严格的情况下可以使用。
数据库与缓存更新与读取操作进行异步串行化:
多个线程同时写一个key,如果不能保证顺序执行,有可能会使旧数据覆盖新数据。
数据库中用时间戳做版本号,写缓存时先获取分布式锁,获取到锁之后检查缓存中的数据是不是比当前数据旧,是就写,否则取消。
目前主流的消息队列,RabbitMQ,RocketMQ,Kafka
为什么要用MQ,异步,削峰,解耦
消费消息之前使用内存set或redis的set判重,在数据库中使用唯一键约束
轮询 | 默认策略 |
weight | 权重策略 |
ip_hash | 根据客户端ip负载均衡 |
least_conn | 最少连接方式 |
fair(第三方) | 响应时间方式 |
url_hash(第三方) | 依据URL分配方式 |
在轮询中,如果服务器down掉了,会自动剔除该服务器。
缺省配置就是轮询策略。
此策略适合服务器配置相当,无状态且短平快的服务使用。
权重越高分配到需要处理的请求越多。
此策略可以与least_conn和ip_hash结合使用。
此策略比较适合服务器的硬件配置差别比较大的情况。
在nginx版本1.3.1之前,不能在ip_hash中使用权重(weight)。
ip_hash不能与backup同时使用。
此策略适合有状态服务,比如session。
当有服务器需要剔除,必须手动down掉。
此负载均衡策略适合请求处理时间长短不一造成服务器过载的情况。
分布式事务
jvm:https://www.jianshu.com/p/76959115d486
jvm:https://blog.csdn.net/m0_37524661/article/details/87432999
io:https://www.jianshu.com/p/8316ec8e185d
数据结构:https://blog.csdn.net/u010775025/article/details/79315361
数据结构:https://www.cnblogs.com/codingblock/p/7712596.html
数据结构:https://www.cnblogs.com/xuxinstyle/p/9537249.html
数据结构:https://www.cnblogs.com/williamjie/p/9358291.html
数据结构:https://blog.csdn.net/kingmax54212008/article/details/79662407
排序算法:https://www.cnblogs.com/nankeyimengningchenlun/p/9151701.html
多线程:https://www.cnblogs.com/Jansens520/p/8624708.html
多线程:https://www.cnblogs.com/syp172654682/p/9383335.html
多线程:https://www.cnblogs.com/jpfss/p/11016169.html
设计模式:https://blog.csdn.net/jason0539/article/details/44956775
http:https://my.oschina.net/u/3242075/blog/2245520
http:https://blog.csdn.net/Boring_Wednesday/article/details/83189743
http:https://www.cnblogs.com/Java3y/p/8444033.html
http:https://blog.csdn.net/lvyibin890/article/details/82462041
get/post:https://www.cnblogs.com/logsharing/p/8448446.html
mysql:https://www.cnblogs.com/frankielf0921/p/5930743.html
mysql:https://www.cnblogs.com/williamjie/p/11187470.html
mysql:https://blog.csdn.net/hxpjava1/article/details/80908763
mysql:https://www.liangzl.com/get-article-detail-20113.html
缓存:https://blog.csdn.net/l2009103205/article/details/83305825
缓存:https://blog.csdn.net/kongtiao5/article/details/82771694
缓存:https://blog.csdn.net/LO_YUN/article/details/97131426
缓存:https://blog.csdn.net/lzhcoder/article/details/79469123
一致性hash算法:https://blog.csdn.net/bntX2jSQfEHy7/article/details/79549368
一致性hash算法:https://blog.csdn.net/gerryke/article/details/53939212
nginx:https://www.cnblogs.com/1214804270hacker/p/9325150.html