基础
HashMap底层
JDK1.7中,HashMap是由数组+链表实现的,使用一个Entry数组来存储数据,用key的hashcode取模来决定key会被放到数组里的位置。如果hashcode相同,或者hashcode取模后的结果相同,那么这些key会被定位到Entry数组的同一个格子里,这些Entry对象以链表的形式存储。
JDK1.8中,HashMap是由数组+链表+红黑树实现的,存储容器从Entry换成了Node,存储在一个格子里的Node数量小于8个的时候以链表存储,当Node数量达到8个以红黑树存储。这样的好处是当存储一批hashcode相同或者hashcode取模之后的结果一样的情况下,HashMap的读取操作时间复杂度从O(n)降到了O(logn)。
HashMap的长度为什么要是2的n次方
HashMap为了存取高效,要尽量减少碰撞,就是要尽量把数据分配均匀,使每个链表长度大致相同,这种算法实际就是取模。计算机中直接求余的效率不如位运算,HashMap源码中做了优化,用hash&(length-1)来代替取模运算。只有当 length-1每一位都是1的时候,才能保证hash的后几位都被取到而不浪费空间,所以HashMap的长度要是2的n次方 。
ConcurrentHashMap的实现
JDK1.7中ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。(有时间再看吧)
Synchronized原理
synchronized加锁的同步代码块中在字节码引擎中执行时,其实是通过锁对象monitor的取用和释放来实现的。同步代码块在字节码文件中被编译为
monitorenter;//获取monitor许可证,进入同步块;
同步代码...;
monitorexit;//离开同步块后,释放monitor许可证
根据虚拟机规范要求,在执行monitorenter指令时,首先要尝试获取对象锁(monitor对象)。如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器(_count)加1。当然与之对应执行monitorexit指令时,锁的计数器(_count)也会减1。如果当前线程获取锁失败,那么就会被阻塞住,进入_WaitSet 中,等待锁被释放为止。而且一个monitorenter指令会对应两个monitorexit指令,因为编译器要确保程序中调用过的每条monitorenter指令都要执行对应的monitorexit指令,当程序异常时monitorenter和monitorexit指令也能正常配对执行,编译器会自动产生一个异常处理器,它的目的就是用来执行异常的monitorexit指令。而字节码中多出的monitorexit指令,就是异常结束时,被执行用来释放monitor的。
同步方法在被编译成字节码文件中会加上一个ACC_SYNCHRONIZED标识,JVM通过ACC_SYNCHRONIZED标识,就可以知道这是一个需要同步的方法,进而执行和同步代码块一样的操作。
Lock
Lock是一个接口,有三个实现类,一个是ReentrantLock,另两个是ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。
实现Lock接口的基本思想
实现锁的功能需要两个必备元素,一个是表示锁状态的变量(假设0表示线程没有获取锁,1表示线程占有锁),该变量必须声明为volitale类型;另一个是队列,队列中的节点表示因未能获取锁而阻塞的线程。
线程获取锁的大致过程(这里没有考虑可重入和获取锁过程被中断或超时的情况)
1. 读取表示锁状态的变量
2. 如果表示状态的变量的值为0,那么当前线程尝试将变量值设置为1(通过CAS操作完成),当多个线程同时将表示状态的变量值由0设置成1时,仅一个线程能成功,其它线程都会失败:
2.1 若成功,表示获取了锁,
2.1.1 如果该线程(或者说节点)已位于在队列中,则将其出列(并将下一个节点则变成了队列的头节点)
2.1.2 如果该线程未入列,则不用对队列进行维护
然后当前线程从lock方法中返回,对共享资源进行访问。
2.2 若失败,则当前线程将自身放入等待(锁的)队列中并阻塞自身,此时线程一直被阻塞在lock方法中,没有从该方法中返回(被唤醒后仍然在lock方法中,并从下一条语句继续执行,这里又会回到第1步重新开始)。
3. 如果表示状态的变量的值为1,那么将当前线程放入等待队列中,然后将自身阻塞(被唤醒后仍然在lock方法中,并从下一条语句继续执行,这里又会回到第1步重新开始)
注意: 唤醒并不表示线程能立刻运行,而是表示线程处于就绪状态,仅仅是可以运行而已
线程释放锁的大致过程
1. 释放锁的线程将状态变量的值从1设置为0,并唤醒等待(锁)队列中的队首节点,释放锁的线程从就从unlock方法中返回,继续执行线程后面的代码
2. 被唤醒的线程(队列中的队首节点)和可能和未进入队列并且准备获取的线程竞争获取锁,重复获取锁的过程
注意:可能有多个线程同时竞争去获取锁,但是一次只能有一个线程去释放锁,队列中的节点都需要它的前一个节点将其唤醒,例如有队列A<-B-<C ,即由A释放锁时唤醒B,B释放锁时唤醒C
Lock和synchronized的区别:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
Volitale
volatile是一个类型修饰符,一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
适用场景
1)volatile修饰布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
2)单例模式中的double check,防止获得残缺对象。
使用条件(必须同时满足下面两个条件才能保证在并发环境的线程安全):
(1)对变量的写操作不依赖于当前值。
(2)该变量没有包含在具有其他变量的不变式中。
原理
用valitale修饰的变量生成汇编代码后,会多出一个lock前缀指令
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
CAS
CAS是compare and swap的缩写,CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。CAS通过比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。
CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量时,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程。
CAS存在的问题
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。
1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
JVM内存划分
堆: 是java虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域, 该内存区域存放了对象实例及数组(但不是所有的对象实例都在堆中)
方法区:方法区也称"永久代",它用于存储虚拟机加载的类信息、常量、静态变量、是各个 线程共享的内存区域
虚拟机栈:描述的是java方法执行的内存模型:每个方法被执行的时候都会创建一个"栈帧 ",用于存储局部变量表(包括参数)、操作栈、方法出口等信息。
本地方法栈:与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而 本地方法栈则是为Native方法服务。(栈的空间大小远远小于堆)
程序计数器:是最小的一块内存区域,它的作用是当前线程所执行的字节码的行号指示器, 在虚拟机的模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需 要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器 完成。
直接内存:直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区 域。jdk1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法 直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小.
Java堆内存划分
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。
默认情况下,
新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2
Eden : from : to = 8 : 1 : 1
Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。
Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-整理算法。
垃圾对象的判定方法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减一;任何时候计数器为0的对象就是不可能再被使用的。
这种方法特点就是,实现简单,判定效率也很高,大部分情况下都是一个不错的选择。但是很多主流的Java虚拟机没有选择使用引用计数法类管理内存,主要原因它很难解决对象之间相互循环引用的问题。
Javabean和class的区别
JavaBean实质也是一个类,这个类遵循:
1.类必须是具体的和公共的。
2.具有无参数的构造器。
3.通过提供符合一致性设计模式的公共方法将内部域暴露成员属性(即getters和setters)。
4.实现java.io.Serializable接口(这个目前是默认实现的,不需要特殊申明)。
框架
Spring
IOD:控制反转,将对象的创建过程交给容器,让容器管理对象的生命周期如创建,初始化,销毁等。
AOP:面向切面编程,对关注点进行模块化,通过对某一功能点进行编程,比如记录日志,有很多个类都需要记录日志的方法,则创建记录日志的代理方法,需要调用该功能是只需要调用代理方法,这就是AOP。
Bean注入属性有哪几种方式:接口注入、构造器注入、set注入
SpringMVC工作流程
1、用户发送请求至前端控制器DispatcherServlet
2、DispatcherServlet收到请求调用HandlerMapping处理器映射器。
3、处理器映射器找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
4、DispatcherServlet调用HandlerAdapter处理器适配器
5、HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
6、Controller执行完成返回ModelAndView
7、HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet
8、DispatcherServlet将ModelAndView传给ViewReslover视图解析器
9、ViewReslover解析后返回具体View
10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
11、DispatcherServlet响应用户
SpringMVC与Struts2的主要区别
①springmvc的入口是一个servlet即前端控制器,而struts2入口是一个filter过虑器。
②springmvc是基于方法开发,传递参数是通过方法形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。
③Struts采用值栈存储请求和响应的数据,通过OGNL存取数据, springmvc通过参数解析器是将request对象内容进行解析成方法形参,将响应数据和页面封装成ModelAndView对象,最后又将模型数据通过request对象传输到页面。 Jsp视图解析器默认使用jstl。
Kafka为什么快
Kafka速度的秘诀在于,它把所有的消息都变成一个的文件。通过mmap(Memory Mapped Files(内存映射文件))提高I/O速度,写入数据的时候它是末尾添加所以速度最优;读取数据的时候配合sendfile直接暴力输出。阿里的RocketMQ也是这种模式,只不过是用Java写的。
Redis集群策略
主从复制、哨兵、集群
主从复制
将数据进行读写分离,可以有效分摊Master的压力,不过主从复制不具备自动容错和恢复功能,主机从机的宕机都会导致部分读写请求失败,需要等待机器重启或者手动切换IP才能恢复。
哨兵模式
在主从的基础上增加了哨兵功能,主从可以自动切换,可用性更高。缺点就是每个redis实例都保存着全量数据。
集群模式
在redis的每一个节点上,都有这么两个东西,一个是插槽(slot),它的的取值范围是:0-16383。还有一个就是cluster,可以理解为是一个集群管理的插件。当我们的存取的key到达的时候,redis会根据crc16的算法得出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作。
为了保证高可用,redis-cluster集群引入了主从模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点A1都宕机了,那么该集群就无法再提供服务了。
redis和memcached的区别
1 Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,hash等数据结构的存储。
2 Redis支持数据的备份,即master-slave模式的数据备份。
3 Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。
Mapper中传参数的方式
- 直接传递,参数用#{num}来接收
- 使用@param注解
- 多个参数封装成map传递
场景
人事