1.TCP的连接过程
三次握手
序号:
seq序号,32位,发起方发送数据时对此进行标记
ack序号,确认序号,32位,只有ACK=1时,确认序号才有效,ack=seq+1
标志位:
URG:紧急指针有效
ACK:确认序号有效
PSH:接收方应该尽快将这个报文交给应用层
RST:重置连接
SYN:发起一个新连接
FIN:释放一个连接
第一步:客户端随机选取一个序号作为seq发送给服务器,SYN=1,ACK=0,seq=x
第二步:服务器接受到seq,将信息发送给客户端,SYN=1,ACK=1,seq=y,ack=x+1
第三步:客户端接受到消息,将确认信息发送给服务器,ACK=1,seq=x+1,ack=y+1
客户端为何还要发送确认信息?
防止已失效的连接请求又传到了服务器
已失效的连接请求是怎样产生的?
正常情况:客户端发出连接请求,但是由于连接请求报文丢失未收到确认,客户端会重传一次连接请求,收到服务器的确认,客户端发送确认,连接建立。
异常情况:客户端发出连接请求,但是连接请求报文并没有丢失,只是在某些网络节点滞留了,导致客户端和服务器的重连建立并释放以后才到达服务器,而这已经是一个失效的请求报文了。但是服务器接收到失效请求后,向客户端发送确认请求,同意建立连接。如果没有第三次客户端的确认,只要服务器发出确认请求连接就建立了,服务器会一直等待客户端发送数据,导致服务器资源的浪费。
2.TCP的断开过程
四次挥手
第一步:客户端发送FIN,序号M,客户端进入FIN_WAIT_1状态
第二步:服务器发送ACK=1,ack=M+1给客户端,服务器进入CLOSE_WAIT状态
第三步:服务器发送FIN,序号N,服务器进入LAST_ACK状态
第四步:客户端发送ACK=1,ack=N+1,客户端进入TIME_WAIT状态,2MSL后进入CLOSED状态,服务器进入CLOSED状态
为什么连接是三次握手,断开是四次挥手?
因为连接时SYN信号和ACK信号在一个报文里,直接建立连接即可。而断开时,客户端收到FIN信号后,仅表示客户端不再发送数据,而服务器可能并没有处理完,服务器只能先返回确认ACK信号,待全部数据处理完成后再发送FIN信号。
为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
防止客户端发送的确认信息服务器没有收到,这时服务器会再发送一个FIN请求,这时候客户端仍然处于TIME_WAIT,可以再次发送确认信息。
3.java容器
4.Iterator实现原理
通过内部类实现,每个集合类内部都含有Iterator<E> iterator()方法,该方法是返回集合中迭代器对象。获取迭代器对象后,就可以使用hasNext(),next,remove方法操作集合中的元素了
迭代器为什么定义了一个接口而不是一个类?
因为不同的集合,由于数据结构不一样,所以他们的存储方式也不一样,在迭代器获取的时候,获取的方法也是变化的。
java中的iterator迭代一个容器时,如果此时有别的方法在更改容器内容,那么Iterator就会抛出ConcurrentModificationException。
1.如果当前单个线程在更改容器(add,delete……),那么迭代的时候使用iterator.remove()可以确保迭代器在查找next的时候,指针不会丢失
2.如果当前有多个线程在操作容器,例如一个线程在向容器中写数据,另一个线程在迭代容器,必须考虑并发下的线程安全问题。可以采用java.util.concurrent包下的线程安全的容器解决此异常。
5.synchronized和ReentrantLock区别
1.可重入性:两者都是同一个线程每进入一次,锁的计数器自增1,要等到锁的计数器下降为0时才能释放锁。
2.锁的实现:Synchronized是依赖与JVM实现的,ReentrantLost是JDK实现的
3.性能的区别:在Synchronized优化前,synchronized的性能比ReentrantLock差很多。但自动Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了。
4.功能的区别:
便利性:synchronized由编译器去保证锁的加锁和释放,而ReentrantLock需要手工声明来加锁和释放,最好在finally中声明释放锁
粒度和灵活性:
ReentrantLock的粒度和灵活性更高
ReentrantLock独有的能力:
1.ReentrantLock可以指定是公平锁还是非公平锁,而synchronized只能是非公平锁。公平锁就是先等待的线程先获得锁。
2.ReentrantLock提供了一个Condition类,用来实现分组唤醒需要唤醒的线程,而synchronized要么随机唤醒一个线程要么唤醒全部线程
3.ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()实现
6.用过哪些线程池,ThreadPoolExecutor各个参数意义是什么
1.newCachedThreadPool:可缓存线程池,如果线程池长度超过处理需要,可以回收空闲线程,若无可回收,则 新建线程。
2.newFixedThreadPool:指定工作线程数量的线程池,如果工作线程数量达到初始最大值,就将提交的任务存入池队列中。
3.newSingleThreadExecutor:只有唯一的工作线程来执行任务,所有任务按照指定顺序(FIFO,LIFO,优先级)执行。
4.newScheduleThreadPool:固定大小的线程池,支持定时的以及周期性的任务执行。
为什么要使用线程池?
1.解决线程声明周期开销问题。为每个请求创建和销毁线程开销过大。
2.解决资源不足问题。在JVM里创建过多的线程可能导致资源不足。
使用线程池的风险?
1.死锁
所有线程都在等待已阻塞线程的结果
2.资源不足
如果线程池过大,线程会消耗大量系统资源,线程之间的切换也会浪费时间。
3.并发错误
线程池和其他排队机制依靠wait和notify,如果编码不正确,那么可能丢失通知,导致线程保持空间状态。
4.线程泄露
当线程执行一项任务,而任务完成后该线程却没有返回池中时,会发生这种情况。比如任务抛出一个RuntimeException或一个Error时,如果线程池没有捕捉到它们,那么线程只会退出而线程池的大小将会永久减1。当这种情况发生的次数足够多时,线程池最终为空。
或者是有些任务会等待某些资源如用户的输入,而这些资源又不能保证是可用状态,也会引起线程泄漏同样的问题。
5.请求过载
排在队列中等待执行的任务可能会消耗太多的系统资源引起资源缺乏。可以简单地抛弃请求,依靠更高级别的协议稍后重试请求。也可以用一个指出服务器暂时很忙的响应来拒绝请求。
线程池大小的设置
线程池的最佳大小取决于可用处理器的数目以及工作队列中的任务的性质。若在一个具有 N 个处理器的系统上只有一个工作队列,其中全部是计算性质的任务,在线程池具有 N 或N+1 个线程时一般会获得最大的 CPU 利用率。
对于那些可能需要等待 I/O 完成的任务(例如,从套接字读取 HTTP 请求的任务),需要让池的大小超过可用处理器的数目,因为并不是所有线程都一直在工作。通过使用概要分析,您可以估计某个典型请求的等待时间(WT)与服务时间(ST)之间的比例。如果我们将这一比例称之为 WT/ST,那么对于一个具有 N 个处理器的系统,需要设置大约 N*(1+WT/ST) 个线程来保持处理器得到充分利用。
处理器利用率不是调整线程池大小过程中的唯一考虑事项。随着线程池的增长,您可能会碰到调度程序、可用内存方面的限制,或者其它系统资源方面的限制,例如套接字、打开的文件句柄或数据库连接等的数目。
7.Java中的锁
悲观锁,乐观锁:
悲观锁:总是假设最坏的情况,每次操作数据时总是加锁
乐观锁:总是认为不会产生并发问题
1.version方式:
读取数据的同时会读取版本号,操作时版本号加1,提交更新时比较当前版本号与之前读取版本号
2.cas方式:
涉及三个操作数,数据所在的内存值,预期值,新值
当需要更新时,判断当前内存值与之前取到的值是否相等,若相等则用新值更新,若失败则重试。
markword
markword是java对象数据结构中的一部分,markword数据长度在32位和64位的虚拟机中分别为32bit和64bit,它的最后2bit是锁状态标志位,用来标记对象所处状态。
状态 | 标志位 | 存储内容 |
---|---|---|
未锁定 | 01 | 对象哈希码、对象分代年龄 |
轻量级锁定 | 00 | 指向锁记录的指针 |
膨胀(重量级锁定) | 10 | 执行重量级锁定的指针 |
GC标记 | 11 | 空(不需要记录信息) |
可偏向 | 01 | 偏向线程ID、偏向时间戳、对象分代年龄 |
32位虚拟机在不同状态下markword结构:
锁类型:
1.自旋锁
线程的阻塞和唤醒需要CPU从用户态转为内核态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作。如果持有锁的线程能在很短时间内释放锁资源,那么等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,避免用户线程和内核切换的消耗。
如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁,就不适合使用自旋锁。
2.重量级锁synchronized
3.偏向锁
若某一个锁被线程获取后,进入偏向模式,以后该线程再进入和退出同步块时不需要花费CAS操作来加锁和解锁,而只需要简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标志是否设为1(表示当前为偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前进程。
4.轻量级锁
加锁:
线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录Lock Rocord的空间,并将对象头中的MarkWord复制到锁记录中,官方称为Displace Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,则自旋获取锁,当自旋获取锁仍然失败时,表示存在其他线程竞争锁,则轻量级锁会膨胀成重量级锁。
解锁:
轻量级锁解锁时,会使用CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示同步过程已完成。如果失败,表示有其他线程尝试过获取该锁,则要在释放锁的同时唤醒被挂起的线程。
8.osi七层模型每一层做什么事情
应用层:是计算机用户,以及各种应用程序和网络之间的接口,其功能是直接向用户提供服务,完成用户希望在网络上完成的各种工作。它在其他6层工作的基础上,负责完成网络中应用程序与网络操作系统之间的联系,建立与结束使用者之间的联系,并完成网络用户提出的各种网络服务及 应用所需的监督、管理和服务等各种协议。此外,该层还负责协调各个应用程序间的工作。
表示层:对来自应用层的命令和数据进行解释,对各种语法赋予相应的含义,并按照一定的格式传送给会话层。其主要功能是“处理用户信息的表示问题,如编码、数据格式转换和加密解密”等。
会话层:是用户应用程序和网络之间的接口,主要任务是:向两个实体的表示层提供建立和使用连接的方法。将不同实体之间的表示层 的连接称为会话。因此会话层的任务就是组织和协调两个会话进程之间的通信,并对数据交换进行管理。 用户可以按照半双工、单工和全双工的方式建立会话。当建立会话时,用户必须提供他们想要连接的远程地址。而这些地址与MAC(介质访问控制子层)地址或网络层的逻辑地址不同,它们是为用户专门设计的,更便于用户记忆。
传输层:是通信子网和资源子网的接口和桥梁,起到承上启下的作用。该层的主要任务是:向用户提供可靠的端到端的差错和流量控制,保证报文的正确传输。传输层的作用是向高层屏蔽下层数据通信的细节,即向用户透明地传送报文。该层常见的协议:TCP/IP中的TCP协议和UDP协议。传输层提供会话层和网络层之间的传输服务,这种服务从会话层获得数据,并在必要时,对数据进行分割。然后,传输层将数据传递到网络层,并确保数据能正确无误地传送到网络层。因此,传输层负责提供两节点之间数据的可靠传送,当两节点的联系确定之后,传输层则负责监督工作。综上,传输层的主要功能如下:监控服务质量。
网络层:在下两层的基础上向资源子网提供服务。其主要任务是:通过路由选择算法,为报文或分组通过通信子网选择最适当的路径。具体地说,数据链路层的数据在这一层被转换为数据包,然后通过路径选择、分段组合、顺 序、进/出路由等控制,将信息从一个网络设备传送到另一个网络设备。一般地,数据链路层是解决同一网络内节点之间的通信,而网络层主要解决不同子网间的通信。例如在广域网之间通信时,必然会遇到路由(即两节点间可能有多条路径)选择问题。
数据链路层:负责建立和管理节点间的链路。该层的主要功能是:通过各种控制协议,将有差错的物理信道变为无差错的、能可靠传输数据帧的数据链路。在计算机网络中由于各种干扰的存在,物理链路是不可靠的。因此,这一层的主要功能是在物理层提供的比特流的基础上,通过差错控制、流量控制方法,使有差错的物理线路变为无差错的数据链路,即提供可靠的通过物理介质传输数据的方法。数据链路层的具体工作是接收来自物理层的位流形式的数据,并封装成帧,传送到上一层;同样,也将来自上层的数据帧,拆装为位流形式的数据转发到物理层;并且,还负责处理接收端发回的确认帧的信息,以便提供可靠的数据传输。
物理层:实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。“透明传送比特流”表示经实际电路传送后的比特流没有发生变化,对传送的比特流来说,这个电路好像是看不见的。
9.udp tcp区别
1.TCP面向连接,UDP是无连接的
2.TCP提供可靠的服务,数据无差错,不丢失,不重复,且按序到达。UDP尽最大努力交付,但不保证可靠交付
3.UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高要求的通信或广播通信
4.TCP是点到点的,UDP支持一对一,一对多,多对一和多对多
5.TCP对系统资源要求较高,UDP对系统资源要求较少
10.锁在操作系统层面如何实现的
在硬件层面, CPU提供了原子操作、关中断、锁内存总线的机制;操作系统基于这几个CPU硬件机制,就能够实现锁。多核计算机上提供了锁总线的支持来实现锁,然后再配合关闭中断的原子操作就能保证锁的正确性了。
11.死锁条件和如何解决死锁
死锁的必要条件:
1.互斥条件。某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占有。
2.不可抢占条件。进程所获得的资源在为使用完毕前,资源申请者不能强行地从资源占有者手中夺取资源,只能由占有该资源的进程自行释放。
3.占有且申请条件。进程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外进程占有,此时该进程阻塞;但是它在等待新资源的时候,仍继续占用已占有的资源。
4.循环等待条件。存在一个进程等待序列,形成循环等待环。
静态策略:使产生死锁的四个必要条件不能同时具备,从而对进程申请资源的活动加以限制,以保证死锁不会发生
死锁的预防:
1.打破互斥条件。允许进程同时访问某些资源。
2.打破不可抢占条件。允许进程强行从占有者那里夺取某些资源。
3.打破占有且申请条件。可以实行资源预先分配策略,即进程在运行前一次性地向系统申请它所需要的全部资源。
但这种策略也有缺点:
1)在许多情况下,一个进程在执行之前不可能知道它所需要的全部资源。
2)资源利用率低。无论所分配的资源何时用到,一个进程只有在占有所需的全部资源后才执行。即使有些资源在最后才被进程用到一次,但该进程声明周期却一直占有它们。
3)降低了进程的并发性。因为资源有限,又加上存在浪费,能分配到所需全部资源的进程个数减少。
4.打破循环等待条件。实行资源有序分配策略,即把资源事先分类编号,按号分配,使进程在申请,占用资源时不会形成环路。所有进程对资源的请求必须严格按照资源序号递增的顺序提出。进程占用了小号资源才能申请大号资源。这种策略与前面的策略相比,提升了资源的利用率和吞吐量。但有也缺点:
1)限制了进程对资源的请求,同时给系统中的所有资源合理编号是困难的,增加了系统开销。
2)为了遵循按编号申请的次序,暂不使用的资源也需要提前申请,增加了进程对资源的占用时间。
动态策略:不限制进程有关申请资源的命令,而是对进程所发出的每一个申请资源命令加以动态地检查,并根据检查结果决定是否进行资源分配
安全序列:
安全序列是指对当前申请资源的进程排出一个序列,保证按照这个序列分配资源完成进程,不会发生“酱油和醋”的尴尬问题。
我们假设有进程P1,P2,.....Pn
则安全序列要求满足:Pi(1<=i<=n)需要资源<=剩余资源 + 分配给Pj(1 <= j < i)资源
银行家算法:
银行家算法是从当前状态出发,逐个按安全序列检查各客户谁能完成其工作,然后假定其完成工作且归还全部贷款,再进而检查下一个能完成工作的客户,......。如果所有客户都能完成工作,则找到一个安全序列,银行家才是安全的。
缺点:
1)算法要求客户数保持固定不变,这在多程序系统中难以做到
2)算法保证所有客户在有限的时间内得到满足,但实时客户要求响应快,要考虑这个因素
3)由于要找到一个安全序列,实际上增加了系统的开销
12.数据库索引实现
数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用B树及其变种B+树。
创建索引可以大大提高系统的性能:
1.通过创建唯一索引,可以保证数据库表中每一行数据的唯一性
2.可以大大加快数据的检索速度,这也是创建索引的最主要的原因
3.可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义
4.在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间
5.通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能
为表设置索引要付出代价的:
1.创建和维护索引需要耗费时间,这种时间随着数据量的增加而增加
2.索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大
3.当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度
应该在这些列上创建索引:
1.经常需要搜索的列上,加快搜索速度
2.作为主键的列上,强制该列的唯一性和组织表中数据的排列结构
3.经常需要连接的列上,加快连接速度
4..经常需要范围搜索的列上
5.经常需要排序的列上
6.经常使用Where子句的列上
三种索引
1.唯一索引:不允许其中任何两行具有相同索引值的索引
2.主键索引:是唯一索引的特定类型
3.聚集索引:实际存储的顺序结构与数据存储的物理结构是一致的,一个表的聚集索引只能有一个,通常默认为主键
13.B树和B+区别
B树是为了磁盘或其他存储设备而设计的一种多叉平衡查找树。
m阶B树每个节点最多可以开m个叉。
m阶B树满足以下条件:
1.每个节点至多拥有m棵子树
2.根节点,只有2个节点(或者要么极端情况一棵树只有一个根节点)
3.非根非叶的节点至少有Ceil(m/2)个子树,(Ceil表示向上取整)
4.非叶节点中的信息包括[n, A0, K1, A1, K2, A2, ……, Kn, An],其中n表示该节点保存的关键字个数,K为关键字且Ki<Ki+1,A为指向子树根节点的指针。
5.从根到叶子的每一条路径都有相同的长度,也就是说,叶子节点在相同的层,并且这些节点不带信息,实际上这些节点就表示找不到指定的值,也就是指向这些节点的指针为空。
B+树
1.有n棵子树的节点含有n个关键字
2.所有的叶子结点包含了全部的关键字,及指向含有这些关键字记录的指针,且叶子节点本身根据关键字自小而大顺序连接
3.非叶子节点可以看成索引部分,节点中仅含有其子树中的最大(或最小)关键字
B+树优点:
1.B+树内部节点不包含数据信息,在内存页中能够存放更多的key。数据存放的更加紧密,具有更好的空间局部性。因此访问叶子节点上关联的数据也具有更好的缓存命中率。
2.B+树的叶子节点都是相连的,因此对整棵树只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
B树优点:
B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更新,因此访问也更迅速。
14.创建索引需要注意什么
一、数据库索引创建规则
1、表的主键、外键必须有索引;
2、数据量超过300的表应该有索引;
3、经常与其他表进行连接的表,在连接字段上应该建立索引;
4、经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;
5、索引应该建在选择性高的字段上;
6、索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
7、复合索引的建立需要进行仔细分析;尽量考虑用单字段索引代替:
A、正确选择复合索引中的主列字段,一般是选择性较好的字段;
B、复合索引的几个字段是否经常同时以AND方式出现在Where子句中?单字段查询是否极少甚至没有?如果是,则可以建立复合索引;否则考虑单字段索引;
C、如果复合索引中包含的字段经常单独出现在Where子句中,则分解为多个单字段索引;
D、如果复合索引所包含的字段超过3个,那么仔细考虑其必要性,考虑减少复合的字段;
E、如果既有单字段索引,又有这几个字段上的复合索引,一般可以删除复合索引;搜索
8、频繁进行数据操作的表,不要建立太多的索引;
9、删除无用的索引,避免对执行计划造成负面影响;
二、一般选择在这样的列上创建索引
1. 在经常需要搜索查询的列上创建索引,可以加快搜索的速度;
2. 在作为主键的列上创建索引,强制该列的唯一性和组织表中数据的排列结构;
3. 在经常用在连接的列上创建索引,这些列主要是一些外键,可以加快连接的速度;
4. 在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;
5. 在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询 时间;
6. 在经常使用在Where子句中的列上面创建索引,加快条件的判断速度;
7. 为经常出现在关键字order by、group by、distinct后面的字段,建立索引。
三、一般不选择具有这些特点的列上创建索引
1. 对于那些在查询中很少使用或者参考的列不应该创建索引。这是因为,既然这些列很少使用到,因此有索引或者无索引,并不能提高查 询速度。相反,由于增加了索引,反而降低了系统的维护速度和增大了空间需求;
2. 不要在有大量相同取值的字段上,建立索引。这是因为,由于这些列的取值很少,例如人事表的性别列,在查询的结果中,结果集的数据行占了表中数据行的很大比例,即需要在表中搜索的数据行的比例很大。增加索引,并不能明显加 快检索速度;
3. 对于那些定义为text, image和bit数据类型的列不应该增加索引。这是因为,这些列的数据量要么相当大,要么取值很少;
4. 当修改性能远远大于检索性能时,不应该创建索引。这是因为,修改性能和检索性能是互相矛盾的。当增加索引时,会提高检索性能,但是会降低修改性能。当减少索引时,会提高修改性能,降低检索性能。因此,当修改性能远远大于检索性能时,不应该创建索引。
四、创建索引需要注意的地方
1. 限制表上的索引数目。对一个存在大量更新操作的表,所建索引的数目一般不要超过3个,最多不要超过5个。索引虽说提高了访问速度,但太多索引会影响数据的更新操作。
2. 避免在取值朝一个方向增长的字段(例如:日期类型的字段)上,建立索引;对复合索引,避免将这种类型的字段放置在最前面。由于字段的取值总是朝一个方向增长,新记录总是存放在索引的最后一个叶页中,从而不断地引起该叶页的访问竞争、新叶页的分配、中间分支页的拆分。此外,如果所建索引是聚集索引,表中数据按照索引的排列顺序存放,所有的插入操作都集中在最后一个数据页上进行,从而引起插入“热点”。
3. 对复合索引,按照字段在查询条件中出现的频度建立索引。在复合索引中,记录首先按照第一个字段排序。对于在第一个字段上取值相同的记录,系统再按照第二个字段的取值排序,以此类推。因此只有复合索引的第一个字段出现在查询条件中,该索引才可能被使用。因此将应用频度高的字段,放置在复合索引的前面,会使系统最大可能地使用此索引,发挥索引的作用。
4. 删除不再使用,或者很少被使用的索引。表中的数据被大量更新,或者数据的使用方式被改变后,原有的一些索引可能不再被需要。数据库管理员应当定期找出这些索引,将它们删除,从而减少索引对更新操作的影响。
15.如何知道查询是否使用索引
explain显示了mysql如何使用索引来处理select语义以及连接表。可以帮助选择更好的索引和写出更优化的查询语句。
explain select last_name, first_name from a where id = XX
16.mysql存储引擎有哪些 区别
MySQL支持数个存储引擎作为对不同表的类型的处理器。MySQL存储引擎包括处理事务安全表的引擎和处理非事务安全表的引擎:
MyISAM管理非事务表。它提供高速存储和检索,以及全文搜索能力。MyISAM在所有MySQL配置里被支持,它是默认的存储引擎,除非你配置MySQL默认使用另外一个引擎。
MEMORY存储引擎提供“内存中”表。MERGE存储引擎允许集合将被处理同样的MyISAM表作为一个单独的表。就像MyISAM一样,MEMORY和MERGE存储引擎处理非事务表,这两个引擎也都被默认包含在MySQL中。
注释:MEMORY存储引擎正式地被确定为HEAP引擎。
InnoDB和BDB存储引擎提供事务安全表。BDB被包含在为支持它的操作系统发布的MySQL-Max二进制分发版里。InnoDB也默认被包括在所 有MySQL 5.1二进制分发版里,你可以按照喜好通过配置MySQL来允许或禁止任一引擎。
EXAMPLE存储引擎是一个“存根”引擎,它不做什么。你可以用这个引擎创建表,但没有数据被存储于其中或从其中检索。这个引擎的目的是服务,在 MySQL源代码中的一个例子,它演示说明如何开始编写新存储引擎。同样,它的主要兴趣是对开发者。
NDB Cluster是被MySQL Cluster用来实现分割到多台计算机上的表的存储引擎。它在MySQL-Max 5.1二进制分发版里提供。这个存储引擎当前只被Linux, Solaris, 和Mac OS X 支持。在未来的MySQL分发版中,我们想要添加其它平台对这个引擎的支持,包括Windows。
ARCHIVE存储引擎被用来无索引地,非常小地覆盖存储的大量数据。
CSV存储引擎把数据以逗号分隔的格式存储在文本文件中。
BLACKHOLE存储引擎接受但不存储数据,并且检索总是返回一个空集。
FEDERATED存储引擎把数据存在远程数据库中。在MySQL 5.1中,它只和MySQL一起工作,使用MySQL C Client API。在未来的分发版中,我们想要让它使用其它驱动器或客户端连接方法连接到另外的数据源。
区别
- InnoDB支持事务,MyISAM不支持,对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;
- InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
- InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
- Innodb不支持全文索引,而MyISAM支持全文索引,查询效率上MyISAM要高;
如何选择
- 是否要支持事务,如果要请选择innodb,如果不需要可以考虑MyISAM;
- 如果表中绝大多数都只是读查询,可以考虑MyISAM,如果既有读写也挺频繁,请使用InnoDB。
- 系统奔溃后,MyISAM恢复起来更困难,能否接受;
- MySQL5.5版本开始Innodb已经成为Mysql的默认引擎(之前是MyISAM),说明其优势是有目共睹的,如果你不知道用什么,那就用InnoDB,至少不会差。
17.公平锁 非公平锁
公平锁:公平锁和非公平锁的队列都基于锁内部维护的一个双向链表,表节点Node的值就是每一个请求当前锁的线程。公平锁就是保障了多线程下各线程获取锁的顺序,先到的线程优先获取锁。
非公平锁:在等待锁的过程中,如果有新的线程想要获取锁,有很大几率直接获得锁。其实对于非公平锁,只要线程进入了等待队列,队列里面依然是FIFO的原则,跟公平锁的顺序是一样的。
线程开销就是非公平锁效率高于公平锁的原因,因为为公平锁减少了线程挂起的几率,后来的线程有一定几率逃离被挂起的开销。
18.object的常用方法
Object是所有类的父类,若一个类没有明确指出继承于某个类,那么它默认继承Object类。
1.toString()
该方法在打印对象时被调用,将对象信息变为字符串返回,默认输出对象地址
2.equals()
该方法用于比较对象是否相等,必须被重写
3.hashCode()
该方法用来返回对象的物理地址(哈希值),常会和equals同时重写,确保相等的两个对象拥有相等的hashCode
19.如果只重写equals没有重写hashCode 会发生什么
Object规范[JavaSE6]
1.应用程序执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法都必须始终如一地返回同一个整数。但在同一个应用的多次执行过程中,每次执行所返回的整数可以不一致。
2.如果两个对象根据equals方法比较是相等的,那么调用这两个对象中任意一个的hashCode方法都必须产生同样的整数结果(相等的对象一定具有相等的散列码)
3.如果两个对象根据equals方法比较是不相等的,那么调用这两个对象中任意一个的hashCode方法,不一定要产生不同的整数结果。但是给不相等的对象产生截然不同的整数结果,有可能提高但列表的性能。(不相等的对象不一定具有不相等的散列码)
没有重写hashCode违反了第二条规定:相等的对象必须具有相等的散列码。
比如Set里是不能重复的,没有重写hashCode会导致集合中出现相同的元素。
20.hashmap原理 如何扩展 并发场景有什么问题 插入null如何处理
HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。
其允许存放null的key和null的value,当其key为null时,调用putForNullKey方法,放入到table[0]的这个位置
- 当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部。
- 从 HashMap 中 get 元素时,首先计算 key 的 hashCode,找到数组中对应位置的某一元素,然后通过 key 的 equals 方法在对应位置的链表中找到需要的元素。
当 HashMap 中的元素越来越多的时候,hash 冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对 HashMap 的数组进行扩容,数组扩容这个操作也会出现在 ArrayList 中,这是一个常用的操作,而在 HashMap 数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是 resize。
HashMap 底层数组的长度总是 2 的 n 次方。
那么 HashMap 什么时候进行扩容呢?当 HashMap 中的元素个数超过数组大小 *loadFactor
时,就会进行数组扩容,loadFactor的默认值为 0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为 16,那么当 HashMap 中元素个数超过 16*0.75=12
的时候,就把数组的大小扩展为 2*16=32
,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知 HashMap 中元素的个数,那么预设元素的个数能够有效的提高 HashMap 的性能。
hash函数计算出的值,通过indexFor进一步处理来获取实际的存储位置
/**
* 返回数组下标
*/
static int indexFor(int h, int length) {
return h & (length-1);
}
这个方法非常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,而 HashMap 底层数组的长度总是 2 的 n 次方,这是 HashMap 在速度上的优化。
当 length 总是 2 的 n 次方时,h& (length-1)运算等价于对 length 取模,也就是 h%length,但是 & 比 % 具有更高的效率。这看上去很简单,其实比较有玄机的,我们举个例子来说明:
假设数组长度分别为 15 和 16,优化后的 hash 码分别为 8 和 9,那么 & 运算后的结果如下:
h & (table.length-1) | hash | table.length-1 | ||
---|---|---|---|---|
8 & (15-1): | 0100 | & | 1110 | = 0100 |
9 & (15-1): | 0101 | & | 1110 | = 0100 |
8 & (16-1): | 0100 | & | 1111 | = 0100 |
9 & (16-1): | 0101 | & | 1111 | = 0101 |
从上面的例子中可以看出:当它们和 15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8 和 9 会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为 15 的时候,hash 值会与 15-1(1110)进行“与”,那么最后一位永远是 0,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!而当数组长度为16时,即为2的n次方时,2n-1 得到的二进制数的每个位上的值都为 1,这使得在低位上&时,得到的和原 hash 的低位相同,加之 hash(int h)方法对 key 的 hashCode 的进一步优化,加入了高位计算,就使得只有相同的 hash 值的两个值才会被放到数组中的同一个位置上形成链表。
所以说,当数组长度为 2 的 n 次幂的时候,不同的 key 算得得 index 相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
Fail-Fast 机制
原理
我们知道 java.util.HashMap 不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了 map,那么将抛出 ConcurrentModificationException,这就是所谓 fail-fast 策略。
ail-fast 机制是 java 集合(Collection)中的一种错误机制。 当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。
例如:当某一个线程 A 通过 iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程 A 访问集合时,就会抛出 ConcurrentModificationException 异常,产生 fail-fast 事件。
这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对 HashMap 内容(当然不仅仅是 HashMap 才会有,其他例如 ArrayList 也会)的修改都将增加这个值(大家可以再回头看一下其源码,在很多操作中都有 modCount++ 这句),那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。
Hashtable 与 HashMap 的简单比较
- HashTable 基于 Dictionary 类,而 HashMap 是基于 AbstractMap。Dictionary 是任何可将键映射到相应值的类的抽象父类,而 AbstractMap 是基于 Map 接口的实现,它以最大限度地减少实现此接口所需的工作。
- HashMap 的 key 和 value 都允许为 null,而 Hashtable 的 key 和 value 都不允许为 null。HashMap 遇到 key 为 null 的时候,调用 putForNullKey 方法进行处理,而对 value 没有处理;Hashtable遇到 null,直接返回 NullPointerException。
- Hashtable 方法是同步,而HashMap则不是。我们可以看一下源码,Hashtable 中的几乎所有的 public 的方法都是 synchronized 的,而有些方法也是在内部通过 synchronized 代码块来实现。所以有人一般都建议如果是涉及到多线程同步时采用 HashTable,没有涉及就采用 HashMap,但是在 Collections 类中存在一个静态方法:synchronizedMap(),该方法创建了一个线程安全的 Map 对象,并把它作为一个封装的对象来返回。
LinkedHashMap
LinkedHashMap 实现与 HashMap 的不同之处在于,LinkedHashMap 维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。
注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。
根据链表中元素的顺序可以分为:按插入顺序的链表,和按访问顺序(调用 get 方法)的链表。默认是按插入顺序排序,如果指定按访问顺序排序,那么调用get方法后,会将这次访问的元素移至链表尾部,不断访问可以形成按访问顺序排序的链表。
hashMap扩容
1.什么时候扩容:
扩容必须满足两个条件:
1、 存放新值的时候当前已有元素的个数必须大于等于阈值
2、 存放新值的时候当前存放数据发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)
因为上面这两个条件,所以存在下面这些情况
(1)、就是hashmap在存值的时候(默认大小为16,负载因子0.75,阈值12),可能达到最后存满16个值的时候,再存入第17个值才会发生扩容现象,因为前16个值,每个值在底层数组中分别占据一个位置,并没有发生hash碰撞。
(2)、当然也有可能存储更多值(超多16个值,最多可以存26个值)都还没有扩容。原理:前11个值全部hash碰撞,存到数组的同一个位置(这时元素个数小于阈值12,不会扩容),后面所有存入的15个值全部分散到数组剩下的15个位置(这时元素个数大于等于阈值,但是每次存入的元素并没有发生hash碰撞,所以不会扩容),前面11+15=26,所以在存入第27个值的时候才同时满足上面两个条件,这时候才会发生扩容现象。
2.扩容:调用resize方法
对于resize
的过程,相对来讲是比较简单清晰易于理解的。旧桶数组中的某个桶的外挂单链表是通过头插法插入新桶数组中的,并且原链表中的Entry结点并不一定仍然在新桶数组的同一链表。
示意图如下:
这里很容易就想到多线程情况下,隐约感觉这个transfer
方法在多线程环境下会乱套。事实上也是这样的,由于缺乏同步机制,当多个线程同时resize
的时候,某个线程t
所持有的引用next
(参考上面代码next
指向原桶数组中某个桶外挂单链表的下一个需要转移的Entry
),可能已经被转移到了新桶数组中,那么最后该线程t
实际上在对新的桶数组进行transfer
操作。
如果有更多的线程出现这种情况,那很可能出现大量线程都在对新桶数组进行transfer
,那么就会出现多个线程对同一链表无限进行链表反转的操作,极易造成死循环,数据丢失等等,因此HashMap
不是线程安全的,考虑在多线程环境下使用并发工具包下的ConcurrentHashMap
。
为什么是2的n次幂
通过限制length
是一个2的幂
数,h & (length-1)
和h % length
结果是一致的。这就是为什么要限制容量必须是一个2
的幂的原因。
举个简单的例子说明这两个操作的结果一致性:
假设有个hashcode
是311,对应的二进制是(1 0011 0111)
length
为16,对应的二进制位(1 0000)
-
%
操作:311 = 16*19 + 7;所以结果为7,二进制位(0111); -
&
操作:(1 0011 0111) & (0111) = 0111 = 7, 二进制位(0111)1 0011 0111 = (1 0011 0000) + (0111) = (12^4 + 1 2^5 + 02^6 + 02^7 + 12^8 ) + 7 = 2^4(1 + 2 + 0 + 0 + 16) + 7 = 16 * 19 + 7; 和%
操作一致。如果length
是一个2的幂的数,那么length-1
就会变成一个mask
, 它会将hashcode
低位取出来,hashcode
的低位实际就是余数,和取余操作相比,与操作会将性能提升很多。
hashMap的线程不安全
1.reSize死循环
为了思路更清晰,我们只将关键代码展示出来
while(null != e) {
Entry<K,V> next = e.next;
e.next = newTable[i];
newTable[i] = e;
e = next;
}
-
Entry<K,V> next = e.next;
——因为是单链表,如果要转移头指针,一定要保存下一个结点,不然转移后链表就丢了 -
e.next = newTable[i];
——e 要插入到链表的头部,所以要先用 e.next 指向新的 Hash 表第一个元素(为什么不加到新链表最后?因为复杂度是 O(N)) -
newTable[i] = e;
——现在新 Hash 表的头指针仍然指向 e 没转移前的第一个元素,所以需要将新 Hash 表的头指针指向 e -
e = next
——转移 e 的下一个结点
假设这里有两个线程同时执行了put()
操作,并进入了transfer()
环节
while(null!= e) {`
Entry<K,V> next = e.next;//线程1执行到这里被调度挂起了
e.next = newTable[i];
newTable[i] = e;
e = next;
}
那么现在的状态为:
从上面的图我们可以看到,因为线程1的 e 指向了 key(3),而 next 指向了 key(7),在线程2 rehash 后,就指向了线程2 rehash 后的链表。
然后线程1被唤醒了:
- 执行
e.next = newTable[i]
,于是 key(3)的 next 指向了线程1的新 Hash 表,因为新 Hash 表为空,所以e.next = null
, - 执行
newTable[i] = e
,所以线程1的新 Hash 表第一个元素指向了线程2新 Hash 表的 key(3)。好了,e 处理完毕。 - 执行
e = next
,将 e 指向 next,所以新的 e 是 key(7)
然后该执行 key(3)的 next 节点 key(7)了:
- 现在的 e 节点是 key(7),首先执行
Entry<K,V> next = e.next
,那么 next 就是 key(3)了 - 执行
e.next = newTable[i]
,于是key(7) 的 next 就成了 key(3) - 执行
newTable[i] = e
,那么线程1的新 Hash 表第一个元素变成了 key(7) - 执行
e = next
,将 e 指向 next,所以新的 e 是 key(3)
这时候的状态图为:
然后又该执行 key(7)的 next 节点 key(3)了:
- 现在的 e 节点是 key(3),首先执行
Entry<K,V> next = e.next
,那么 next 就是 null - 执行
e.next = newTable[i]
,于是key(3) 的 next 就成了 key(7) - 执行
newTable[i] = e
,那么线程1的新 Hash 表第一个元素变成了 key(3) - 执行
e = next
,将 e 指向 next,所以新的 e 是 key(7)
这时候的状态如图所示:
很明显,环形链表出现了!!当然,现在还没有事情,因为下一个节点是 null。当调用Get查找一个不存在的Key,
而这个Key的Hash结果恰好等于3的时候,由于位置3带有环形链表,所以程序将会进入死循环!
2.fail-fast
如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。
为什么String, Interger这样的wrapper类适合作为键?
1.因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。
2.不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。
3.不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。
21.concurrenthashmap原理
ConcurrentHashMap采用了分段锁的设计,只有在同一个分段内才存在竞态关系,不同的分段锁之间没有锁竞争。
并发度:
并发度可以理解为程序运行时能够同时更新ConccurentHashMap且不产生锁竞争的最大线程数,实际上就是ConcurrentHashMap中的分段锁个数,即Segment[]的数组长度。ConcurrentHashMap默认的并发度为16,但用户也可以在构造函数中设置并发度。当用户设置并发度时,ConcurrentHashMap会使用大于等于该值的最小2幂指数作为实际并发度(假如用户设置并发度为17,实际并发度则为32)。运行时通过将key的高n位(n = 32 – segmentShift)和并发度减1(segmentMask)做位与运算定位到所在的Segment。segmentShift与segmentMask都是在构造过程中根据concurrency level被相应的计算出来。
22.lru如何实现
1. LRU
1.1. 原理
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
1.2实现
最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:
新数据插入到链表头部;
每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
当链表满的时候,将链表尾部的数据丢弃。
1.3 分析
【命中率】
当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。
【复杂度】
实现简单。
【代价】
命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部。
- LRU-K
2.1. 原理
LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。
2.2. 实现
相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。详细实现如下:
数据第一次被访问,加入到访问历史列表;
如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;
当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序;
缓存数据队列中被再次访问后,重新排序;
需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。
LRU-K具有LRU的优点,同时能够避免LRU的缺点,实际应用中LRU-2是综合各种因素后最优的选择,LRU-3或者更大的K值命中率会高,但适应性差,需要大量的数据访问才能将历史访问记录清除掉。
2.3. 分析
【命中率】
LRU-K降低了“缓存污染”带来的问题,命中率比LRU要高。
【复杂度】
LRU-K队列是一个优先级队列,算法复杂度和代价比较高。
【代价】
由于LRU-K还需要记录那些被访问过、但还没有放入缓存的对象,因此内存消耗会比LRU要多;当数据量很大的时候,内存消耗会比较可观。
LRU-K需要基于时间进行排序(可以需要淘汰时再排序,也可以即时排序),CPU消耗比LRU要高。
3. Two queues(2Q)
3.1. 原理
Two queues(以下使用2Q代替)算法类似于LRU-2,不同点在于2Q将LRU-2算法中的访问历史队列(注意这不是缓存数据的)改为一个FIFO缓存队列,即:2Q算法有两个缓存队列,一个是FIFO队列,一个是LRU队列。
3.2. 实现
当数据第一次访问时,2Q算法将数据缓存在FIFO队列里面,当数据第二次被访问时,则将数据从FIFO队列移到LRU队列里面,两个队列各自按照自己的方法淘汰数据。详细实现如下:
新访问的数据插入到FIFO队列;
如果数据在FIFO队列中一直没有被再次访问,则最终按照FIFO规则淘汰;
如果数据在FIFO队列中被再次访问,则将数据移到LRU队列头部;
如果数据在LRU队列再次被访问,则将数据移到LRU队列头部;
LRU队列淘汰末尾的数据。
注:上图中FIFO队列比LRU队列短,但并不代表这是算法要求,实际应用中两者比例没有硬性规定。
3.3. 分析
【命中率】
2Q算法的命中率要高于LRU。
【复杂度】
需要两个队列,但两个队列本身都比较简单。
【代价】
FIFO和LRU的代价之和。
2Q算法和LRU-2算法命中率类似,内存消耗也比较接近,但对于最后缓存的数据来说,2Q会减少一次从原始存储读取数据或者计算数据的操作。
4. Multi Queue(MQ)
4.1. 原理
MQ算法根据访问频率将数据划分为多个队列,不同的队列具有不同的访问优先级,其核心思想是:优先缓存访问次数多的数据。
4.2. 实现
MQ算法将缓存划分为多个LRU队列,每个队列对应不同的访问优先级。访问优先级是根据访问次数计算出来的,例如
详细的算法结构图如下,Q0,Q1....Qk代表不同的优先级队列,Q-history代表从缓存中淘汰数据,但记录了数据的索引和引用次数的队列:
如上图,算法详细描述如下:
新插入的数据放入Q0;
每个队列按照LRU管理数据;
当数据的访问次数达到一定次数,需要提升优先级时,将数据从当前队列删除,加入到高一级队列的头部;
为了防止高优先级数据永远不被淘汰,当数据在指定的时间里访问没有被访问时,需要降低优先级,将数据从当前队列删除,加入到低一级的队列头部;
需要淘汰数据时,从最低一级队列开始按照LRU淘汰;每个队列淘汰数据时,将数据从缓存中删除,将数据索引加入Q-history头部;
如果数据在Q-history中被重新访问,则重新计算其优先级,移到目标队列的头部;
Q-history按照LRU淘汰数据的索引。
4.3 分析
【命中率】
MQ降低了“缓存污染”带来的问题,命中率比LRU要高。
【复杂度】
MQ需要维护多个队列,且需要维护每个数据的访问时间,复杂度比LRU高。
【代价】
MQ需要记录每个数据的访问时间,需要定时扫描所有队列,代价比LRU要高。
注:虽然MQ的队列看起来数量比较多,但由于所有队列之和受限于缓存容量的大小,因此这里多个队列长度之和和一个LRU队列是一样的,因此队列扫描性能也相近。
5. LRU类算法对比
由于不同的访问模型导致命中率变化较大,此处对比仅基于理论定性分析,不做定量分析。
| 对比点 | 对比 |
| 命中率 | LRU-2 > MQ(2) > 2Q > LRU |
| 复杂度 | LRU-2 > MQ(2) > 2Q > LRU |
| 代价 | LRU-2 > MQ(2) > 2Q > LRU |
实际应用中需要根据业务的需求和对数据的访问情况进行选择,并不是命中率越高越好。例如:虽然LRU看起来命中率会低一些,且存在”缓存污染“的问题,但由于其简单和代价小,实际应用中反而应用更多。
java中最简单的LRU算法实现,就是利用jdk的LinkedHashMap,覆写其中的removeEldestEntry(Map.Entry)方法即可
如果你去看LinkedHashMap的源码可知,LRU算法是通过双向链表来实现,当某个位置被命中,通过调整链表的指向将该位置调整到头位置,新加入的内容直接放在链表头,如此一来,最近被命中的内容就向链表头移动,需要替换时,链表最后的位置就是最近最少使用的位置。
23.强引用 软引用 弱引用 虚引用
提供四种引用目的:
1.可以让程序员通过代码的方式决定某些对象的生命周期
2.有利于JVM进行垃圾回收
1.强引用
如
String str = "hello";
只要某个对象与强引用与之关联,JVM必定不会回收这个对象,即使在内存不足的情况下,JVM宁愿抛出“OutOfMemory”。如果想中断某个强引用和对象的关联,可以显式地将引用赋值为null,JVM会在合适的时间回收该对象。
2.软引用
软引用描述一些有用但不是必须的对象,在Java中用java.lang.ref.SoftReference类表示。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。因此,这一点可以很好地用来解决OOM的问题,并且这个特性很适合用来实现缓存:比如网页缓存、图片缓存等。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。
3.弱引用
弱应用与对象的生命周期有关。在进行垃圾回收时,如果所引用的对象为null,则不论内存是否充足都会被回收,否则不会被回收。当然当内存不足时会直接被回收。在java中,用java.lang.ref.WeakReference类来表示。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被JVM回收,这个软引用就会被加入到与之关联的引用队列中。
4.虚引用
虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期,只要进行垃圾回收,虚引用就会被回收。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。
虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
24.springmvc处理请求流程
具体步骤:
第一步:发起请求到前端控制器(DispatcherServlet)
第二步:前端控制器请求HandlerMapping查找 Handler (可以根据xml配置、注解进行查找)
第三步:处理器映射器HandlerMapping向前端控制器返回Handler,HandlerMapping会把请求映射为HandlerExecutionChain对象(包含一个Handler处理器(页面控制器)对象,多个HandlerInterceptor拦截器对象),通过这种策略模式,很容易添加新的映射策略
第四步:前端控制器调用处理器适配器去执行Handler
第五步:处理器适配器HandlerAdapter将会根据适配的结果去执行Handler
第六步:Handler执行完成给适配器返回ModelAndView
第七步:处理器适配器向前端控制器返回ModelAndView (ModelAndView是springmvc框架的一个底层对象,包括 Model和view)
第八步:前端控制器请求视图解析器去进行视图解析 (根据逻辑视图名解析成真正的视图(jsp)),通过这种策略很容易更换其他视图技术,只需要更改视图解析器即可
第九步:视图解析器向前端控制器返回View
第十步:前端控制器进行视图渲染 (视图渲染将模型数据(在ModelAndView对象中)填充到request域)
第十一步:前端控制器向用户响应结果
25.spring的特性
1.IOC依赖注入
创建被调用者的实例不是由调用者完成,而是由Spring容器完成,并注入调用者。
2.AOP面向切面编程
允许通过分离应用的业务逻辑与系统级服务(如日志和事务管理)进行开发。
26.aop如何实现
AOP 实际上是由目标类的代理类实现的。AOP 代理其实是由 AOP 框架动态生成的一个对象,该对象可作为目标对象使用。AOP 代理包含了目标对象的全部方法,但 AOP 代理中的方法与目标对象的方法存在差异,AOP 方法在特定切入点添加了增强处理,并回调了目标对象的方法。
spring中实现方式:
1.基于xml方式
2.基于注解方式
27.解决哈希冲突的方法
1.开放定址法
(1)线性探查法(Linear Probing)
该方法的基本思想是:
将散列表T[0..m-1]看成是一个循环向量,若初始探查的地址为d(即h(key)=d),则最长的探查序列为:
d,d+l,d+2,…,m-1,0,1,…,d-1
即:探查时从地址d开始,首先探查T[d],然后依次探查T[d+1],…,直到T[m-1],此后又循环到T[0],T[1],…,直到探查到T[d-1]为止。
探查过程终止于三种情况:
(1)若当前探查的单元为空,则表示查找失败(若是插入则将key写入其中);
(2)若当前探查的单元中含有key,则查找成功,但对于插入意味着失败;
(3)若探查到T[d-1]时仍未发现空单元也未找到key,则无论是查找还是插入均意味着失败(此时表满)。
用线性探测法处理冲突,思路清晰,算法简单,但存在下列缺点:
① 处理溢出需另编程序。一般可另外设立一个溢出表,专门用来存放上述哈希表中放不下的记录。此溢出表最简单的结构是顺序表,查找方法可用顺序查找。
② 按上述算法建立起来的哈希表,删除工作非常困难。假如要从哈希表 HT 中删除一个记录,按理应将这个记录所在位置置为空,但我们不能这样做,而只能标上已被删除的标记,否则,将会影响以后的查找。
③ 线性探测法很容易产生堆聚现象。所谓堆聚现象,就是存入哈希表的记录在表中连成一片。按照线性探测法处理冲突,如果生成哈希地址的连续序列愈长 ( 即不同关键字值的哈希地址相邻在一起愈长 ) ,则当新的记录加入该表时,与这个序列发生冲突的可能性愈大。因此,哈希地址的较长连续序列比较短连续序列生长得快,这就意味着,一旦出现堆聚 ( 伴随着冲突 ) ,就将引起进一步的堆聚。
(2)线性补偿探测法
线性补偿探测法的基本思想是:
将线性探测的步长从 1 改为 Q ,即将上述算法中的 j = (j + 1) % m 改为: j = (j + Q) % m ,而且要求 Q 与 m 是互质的,以便能探测到哈希表中的所有单元。
(3)随机探测
随机探测的基本思想是:
将线性探测的步长从常数改为随机数,即令: j = (j + RN) % m ,其中 RN 是一个随机数。在实际程序中应预先用随机数发生器产生一个随机序列,将此序列作为依次探测的步长。这样就能使不同的关键字具有不同的探测次序,从而可以避 免或减少堆聚。基于与线性探测法相同的理由,在线性补偿探测法和随机探测法中,删除一个记录后也要打上删除标记。
2.拉链法
拉链法解决冲突的方法
拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于 1,但一般均取α≤1。
与开放定址法相比,拉链法有如下几个优点:
①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填入散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在 用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。
拉链法的缺点
拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。
3.再哈希法
4.建立一个公共溢出区
28.rpc
29.web工程哪些地方可能发生中文乱码 如何解决
30.error和异常的区别 碰到哪些常见的错误和异常
31.遇到error如何处理 遇到异常如何处理
32.垃圾回收机制
33.为什么年轻代采用复制算法 老年代采用标记算法
34.为什么只需要一个eden而需要两个survivor
35.如何设置年轻代的回收比率,这个比率的意义是谁和谁的比值
36.什么时候发生浮点数精度丢失,存储0.1会不会发生精度丢失
37.在要求高精度的场景 java有没有解决方案
38.post请求和get请求区别
39.https和http区别,有没有用过其他安全传输手段
40.线程状态
新建状态:没有调用start前
就绪状态:调用start后
运行状态:线程被设置为当前线程,开始执行run方法
阻塞状态:线程被暂停,如调用sleep后
死亡状态:线程执行结束