1.常用的数据结构
ArrayList、LinkedList、HashMap
2. LinkedList结构原理
实现了List接口和Deque接口
LinkedList底层使用双向链表实现。
双向链表就是通过Node类来体现的,类中通过item变量保存了当前节点的值,通过next变量指向下一个节点,通过prev变量指向上一个节点。
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
3. HashSet和HashMap的原理
HashMap
HashMap是Map接口下的一个实现类,jdk1.7中采用数组+链表实现,jdk1.8之后采用数组+链表+红黑树实现。
HashMap支持存储key/value为null的元素。
HashMap被构造时默认初始容量为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
HashMap最大容量为2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
HashMap默认的负载因子为0.75(负载因子越大发上冲突的可能性越大,负载因子越小空间浪费较大)
static final float DEFAULT_LOAD_FACTOR = 0.75f;
链表树化的最小链表大小阈值8
,当HashMap中元素数量大于64
并且数组所挂链表的大小超过8
才会将数组所挂的链表转换为红黑树。
static final int TREEIFY_THRESHOLD = 8;
static final int MIN_TREEIFY_CAPACITY = 64;
红黑树链表化的的阈值是6
,当红黑树结点个数小于6
时,就会转换为链表。
static final int UNTREEIFY_THRESHOLD = 6;
HashMap中的hash方法是与我们直接根据key
生成hashCode值再取模的方式不同,在HashMap中的hash算法中,首先会key
计算出hashCode值,然后再将hashCode值与右移16
为的hashCode值进行按位异或操作,得到一个新的哈希值,这样做的目的其实就是为了增强哈希值的随机性,这个方法也叫做“扰动方法”。
索引计算方法
- 首先,计算对象的 hashCode()
- 再进行调用 HashMap 的 hash() 方法进行二次哈希
- 二次 hash() 是为了综合高位数据,让哈希分布更为均匀
- 最后在(与)& (capacity – 1) 得到索引
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
数组容量为何是 2 的 n 次幂
- 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
- 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
HashSet
HashSet是基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。它封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。
4.HashMap使用头插法会导致什么
(JDK1.7)在多线程下扩容的时侯可能会导致环形链表的出现,调用get()方法的时候可能导致死循环。
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
- e 和 next 都是局部变量,用来指向当前节点和下一个节点
5.线程安全的map
concurrentHashmap和HashTable
6.ConcurrentHashMap和HashTable的区别
Hashtable 对比 ConcurrentHashMap
- Hashtable 与 ConcurrentHashMap 都是线程安全的 Map 集合
- 底层数据结构:JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
- Hashtable 并发度低,整个 Hashtable 对应一把锁,同一时刻,只能有一个线程操作它
- ConcurrentHashMap 并发度高,整个 ConcurrentHashMap 对应多把锁,只要线程访问的是不同锁,那么不会冲突
- 实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
7.volatile、synchronized、reentrantlock的原理和区别
volatile可以保证有序性和可见性,不能保证原子性。
用 volatile 修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见。
用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果
volatile和synchronized的区别
(1)volatile只能作用于变量,使用范围较小。synchronized可以用在方法、类、同步代码块等,使用范围比较广。(要说明的是,java里不能直接使用synchronized声明一个变量,而是使用synchronized去修饰一个代码块或一个方法或类。)
(2)volatile只能保证可见性和有序性,不能保证原子性。而可见性、有序性、原子性synchronized都可以保证。
(3)volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。
reentrantlock和synchronized的区别:
锁的实现:Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的。使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
ReenTrantLock独有的能力:
1、ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
2、ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
3、ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
8.进程和线程的区别
进程是资源分配的最小单位,线程是资源调度的最小单位。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。
多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
9.产生死锁的条件
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不可剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之
一不满足,就不会发生死锁。
10.如何避免死锁
死锁避免的基本思想:系统对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配,这是一种保证系统不进入死锁状态的动态策略。
如果操作系统能保证所有进程在有限时间内得到需要的全部资源,则系统处于安全状态否则系统是不安全的。
利用银行家算法避免死锁:所谓银行家算法,是指在分配资源之前先看清楚,资源分配后是否会导致系统死锁。如果会死锁,则不分配,否则就分配。
11.GC算法
三种垃圾回收算法
标记清除法
解释:
- 找到 GC Root 对象,即那些一定不会被回收的对象,如正执行方法内局部变量引用的对象、静态变量引用的对象
- 标记阶段:沿着 GC Root 对象的引用链找,直接或间接引用到的对象加上标记
- 清除阶段:释放未加标记的对象占用的内存
要点:
- 标记速度与存活对象线性关系
- 清除速度与内存大小线性关系
- 缺点是会产生内存碎片
标记整理法
解释:
- 前面的标记阶段、清理阶段与标记清除法类似
- 多了一步整理的动作,将存活对象向一端移动,可以避免内存碎片产生
特点:
- 标记速度与存活对象线性关系
- 清除与整理速度与内存大小成线性关系
- 缺点是性能上较慢
标记复制法
解释:
- 将整个内存分成两个大小相等的区域,from 和 to,其中 to 总是处于空闲,from 存储新创建的对象
- 标记阶段与前面的算法类似
- 在找出存活对象后,会将它们从 from 复制到 to 区域,复制的过程中自然完成了碎片整理
- 复制完成后,交换 from 和 to 的位置即可
特点:
- 标记与复制速度与存活对象成线性关系
- 缺点是会占用成倍的空间
12.新生代和老年代各自的垃圾回收机制
新生代特点是内存空间相对老年代较小,对象存活率低。
标记复制算法的效率只和当前存活对象大小有关,因而很适用于年轻代的回收。而复制算法的内存利用率不高的问题,可以通过虚拟机中的两个Survivor
区设计得到缓解。
老年代的特点是内存空间较大,对象存活率高。
这种情况,存在大量存活率高的对象,标记复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。
13.IOC和AOP
IOC
IoC(Inverse of Control:控制反转)是一种设计思想 或者说是某种模式。这个设计思想就是 将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。 IoC 在其他语言中也有应用,并非 Spring 特有。IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
IoC 最常见以及最合理的实现方式叫做依赖注入(Dependency Injection,简称 DI)。
依赖注入:由IoC容器动态地将某个对象所需要的外部资源(包括对象、资源、常量数据)注入到组件(Controller, Service等)之中。简单点说,就是IoC容器会把当前对象所需要的外部资源动态的注入给我们。
Spring依赖注入的方式主要有四个,基于注解注入方式、set注入方式、构造器注入方式、静态工厂注入方式。推荐使用基于注解注入方式,配置较少,比较方便。
IoC 的思想就是两方之间不互相依赖,由第三方容器来管理相关资源。这样有什么好处呢?
- 对象之间的耦合度或者说依赖程度降低;
- 资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例。
AOP
AOP 即面向切面编程,简单地说就是将代码中重复的部分抽取出来,在需要执行的时候使用动态代理技术,在不修改源码的基础上对方法进行增强。
使用AOP的好处
- 降低模块的耦合度
- 使系统容易扩展
- 提高代码复用性
14. 依赖注入的方式
setter方法注入、构造方法注入、注解注入(@Autowried)
在Java代码中可以使用@Autowired或@Resource注解方式进行Spring的依赖注入。两者的区别是:@Autowired默认按类型装配,@Resource默认按名称装配,当找不到与名称匹配的bean时,才会按类型装配。
@Autowired(自动注入)修饰符有三个属性:Constructor,byType,byName。默认按照byType注入。
constructor:通过构造方法进行自动注入,spring会匹配与构造方法参数类型一致的bean进行注入,如果有一个多参数的构造方法,一个只有一个参数的构造方法,在容器中查找到多个匹配多参数构造方法的bean,那么spring会优先将bean注入到多参数的构造方法中。
byName:被注入bean的id名必须与set方法后半截匹配,并且id名称的第一个单词首字母必须小写,这一点与手动set注入有点不同。
byType:查找所有的set方法,将符合符合参数类型的bean注入。
主要有四种注解可以注册bean,每种注解可以任意使用,只是语义上有所差异:
- @Component:可以用于注册所有bean
- @Repository:主要用于注册dao层的bean
- @Controller:主要用于注册控制层的bean
- @Service:主要用于注册服务层的bean
15.Spring事务的传播机制
在Spring中对于事务的传播行为定义了七种类型分别是:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED
- REQUIRED(Spring默认的事务传播类型):支持当前事务,如果没有事务会创建一个新的事务
- SUPPORTS:支持当前事务,如果没有事务的话以非事务方式执行
- MANDATORY:支持当前事务,如果没有事务抛出异常
- REQUIRES_NEW:创建一个新的事务并挂起当前事务
- NOT_SUPPORTED:以非事务方式执行,如果当前存在事务则将当前事务挂起
- NEVER:以非事务方式进行,如果存在事务则抛出异常
- NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与REQUIRED类似的操作
16.SpringBoot的优点
- 独立运行
Spring Boot 而且内嵌了各种 servlet 容器,Tomcat、Jetty 等,现在不再需要打成war 包部署到容器中,Spring Boot 只要打成一个可执行的 jar 包就能独立运行,所有的依赖包都在一个 jar 包内。 - 简化配置
spring-boot-starter-web 启动器自动依赖其他组件,减少了 maven 的配置。 - 自动配置
Spring Boot 能根据当前类路径下的类、jar 包来自动配置 bean,如添加一个 spring-boot-starter-web 启动器就能拥有 web 的功能,无需其他配置。 - 无代码生成和XML配置
Spring Boot 配置过程中无代码生成,也无需 XML 配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是 Spring4.x 的核心功能之一。 - 避免大量的Maven导入和各种版本冲突
- 应用监控
Spring Boot 提供一系列端点可以监控服务及应用,做健康检测。
17.数据库事务隔离级别
SQL 标准定义了四个隔离级别:
- READ-UNCOMMITTED(读取未提交) : 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
- READ-COMMITTED(读取已提交) : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- REPEATABLE-READ(可重复读) : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- SERIALIZABLE(可串行化) : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏
18.MySQL默认的事务隔离级别
MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)
InnoDB的默认隔离级别RR(可重读)是可以解决幻读问题发生的,主要有下面两种情况:
- 快照读(一致性非锁定读):由 MVCC 机制来保证不出现幻读。
- 当前读(一致性锁定读):使用 Next-Key Lock 进行加锁来保证不出现幻读。
19.主键索引、唯一索引、聚簇(聚集)索引
MySQL数据库中innodb存储引擎,B+树索引可以分为:
聚簇索引(也称聚集索引,clustered index):找到了索引就找到了需要的数据,那么这个索引就是聚簇索引,所以主键就是聚簇索引,修改聚簇索引其实就是修改主键
非聚簇索引(辅助索引):索引的存储和数据的存储是分离的,也就是说找到了索引但没找到数据,需要根据索引上的值(主键)再次回表查询,非聚簇索引也叫做辅助索引。。
这两种索引内部都是B+树,聚集索引的叶子节点存放着一整行的数据。
主键索引是一种聚簇索引。
唯一索引是非聚簇索引
主键一定是聚簇索引,MySQL的InnoDB中一定有主键,即便研发人员不手动设置,则会使用unique索引,没有唯一索引,则会使用数据库内部的一个行的id来当作主键索引,其它普通索引需要区分SQL场景,当SQL查询的列就是索引本身时,我们称这种场景下该普通索引也可以叫做聚簇索引,MyisAM引擎没有聚簇索引。
20.tcp三次握手和四次挥手原理
所谓三次握手(Three-way Handshake),是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。
三次握手的目的是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号并交换 TCP 窗口大小信息.在socket编程中,客户端执行connect()时。将触发三次握手。
第一次握手:
客户端发送一个TCP的SYN标志位置1的包指明客户打算连接的服务器的端口,以及初始序号X,保存在包头的序列号(Sequence Number)字段里。
第二次握手:(服务器确认服务器可以接受到客户端的请求,但客户端还不知道)
服务器发回确认包(ACK)应答。即SYN标志位和ACK标志位均为1同时,将确认序号(Acknowledgement Number)设置为客户的I S N加1以.即X+1。
第三次握手:(客户端确认服务器可以收到自己的信息)
客户端再次发送确认包(ACK) SYN标志位为0,ACK标志位为1.并且把服务器发来ACK的序号字段+1,放在确定字段中发送给对方.并且在数据段放写ISN的+1
当三次握手都成功的时候,我们发现此时客户端发送的信息服务端能够收到并且服务端发送的信息客户端也能收到,通信双方连接成功。
1、图中的发送请求中的发送标识SYN、ACK表示的是发送报文中两个标识位!而Seq和Ack分别代表发送序号和确认号。
2、服务端在接收到了客户端的连接请求后,回复中同时发送了SYN、ACK两个标识位,将建立连接的请求和对客户端的确认应答在同一个数据包中发送了,这也是为什么只需要三次握手,就能建立连接。
TCP 四次挥手
TCP的连接的拆除需要发送四个包,因此称为四次挥手(four-way handshake)。客户端或服务器均可主动发起挥手动作,在socket编程中,任何一方执行close()操作即可产生挥手操作。
1.第一次挥手:客户端向服务端发送断开连接的请求,告诉服务端我这边不需要再请求你的数据了,tcp报文头中发送标识FIN=1(表示客户端请求跟服务端断开连接),序号Seq=u
2.第二次挥手:服务端在接收到客户端发送的断开请求后,需告诉客户端已收到请求,tcp报文头中发送标识ACK=1(ACK表示对客户端的断开连接的请求进行应答),序号Seq=v,确认号Ack=u+1(表示对客户端发送的序号Seq=u的请求进行确认)。
3.第三次挥手:当服务端数据传输完毕之后,向客户端发起断开连接的请求,告诉客户端我这边也不需要再发送数据了,tcp报文头中发送标识FIN=1,ACK=1(FIN表示服务端请求跟客户端断开连接,ACK表示对上一次客户端的断开连接的请求进行应答),序号Seq=w,确认号Ack=u+1(表示对客户端发送的序号Seq=u的请求进行确认)
4.第四次挥手:客户端接收到服务发送的断开连接请求后,需告诉服务端已收到信息,作出应答,tcp报文头中发送标识ACK=1(ACK表示对服务端的断开连接的请求进行应答),序号Seq=u+1,确认号Ack=w+1(表示对服务端发送的序号Seq=w的请求进行确认)
第二次挥手的时候。为什么不能像握手的时候一样,服务端对客户端断开连接的请求做确认应答的时候,同时向客户端发送断开连接的请求。这样“三次挥手”不就可以了么???
*:当服务端数据传输完毕之后,向客户端发起断开连接的请求,告诉客户端我这边也不需要再发送数据了,tcp报文头中发送标识FIN=1,ACK=1(FIN表示服务端请求跟客户端断开连接,ACK表示对上一次客户端的断开连接的请求进行应答),序号Seq=w,确认号Ack=u+1(表示对客户端发送的序号Seq=u的请求进行确认)
4.第四次挥手:客户端接收到服务发送的断开连接请求后,需告诉服务端已收到信息,作出应答,tcp报文头中发送标识ACK=1(ACK表示对服务端的断开连接的请求进行应答),序号Seq=u+1,确认号Ack=w+1(表示对服务端发送的序号Seq=w的请求进行确认)
第二次挥手的时候。为什么不能像握手的时候一样,服务端对客户端断开连接的请求做确认应答的时候,同时向客户端发送断开连接的请求。这样“三次挥手”不就可以了么???
解答:在实际的网络中,服务端在接收到客户端断开连接的请求的时候,此时服务端可能还有数据没有传输完毕,不能立即向客户端发送断开连接的请求!所以当客户端主动发起断开请求的时候,服务器先回应一个确认,等所有数据传输完毕后再发送服务器断开的请求。