java知识点整理(整理中)
基础部分
1.HashMap 在java7和java8 中的实现以及区别
Jdk8之前,HashMap由数组+链表组成,数组是HashMap的主体,链表则主要是为了解决哈希冲突而存在的("拉链法" 解决散列冲突)
Jdk8之后,在解决哈希冲突是有较大变化,当链表长度大于阈值(默认是8)时,将链表转化为红黑树,以减少搜索时间
1.数组
HashMap 中的数组也被叫做哈希桶,本质上是一个 Node 类型的数组。
Node 中存放着 key,value 以及一个 hash 值。这个 hash 值的作用就是为了确定指定的 key 存放在数组中的哪个桶
static final int hash(Object key) {
int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
1)key 允许为null,为 null 时存放在数组的第一个桶(下标为0)中。
2)用到了 key 对象的 hashCode 方法获取对象的 hash 值,然后运用了一些位运算计算下标。
延申:
一.hashcode和equals,==
equals : 是否同一对象实例。 是实例,比如String s = new String ("test"); s.equals(s) ,这就是对同一对象实例的比较
等号(==) : 对比对象实例的内存地址(即对象实例id) 来判断是否是同一个对象实例 ;又可以说是判断对象实例是否物理相等
HashCode : 可以理解为并不是对象的内存地址,而是利用hash算法,对对象实例的一种描述, --对象实例的哈希码
1.如果两个对象equals, java 运行时环境一定认为他们的hashCode相等
2.如果两个对象equals 为false ,他们的hashCode有可能相等
3.如果两个hashCode相等,他们equals不一定为true
4.如果两个hashCode不等,他们equals一定为false
顺序是先判断hashCode 再判断equals
2.ConcurrentHashMap 的锁机制
数据结构 : 树组+ 树组 + 链表形式
读操作:HashEntry加了volatile关键字,可以保证读到最新的数据
同时读可以多个线程进行操作,即至少可以有16线程进行操作,至多无上限,只要在cpu承受范围内即可
写操作:它具有锁机制,可以同时让16(默认容量)个线程同时操作,每个线程进入各自寻找的Segment 树组中的HashEntry树组进行操作,大大提高了操作效率
线程进入put()方法时,会首先调用ReentrantLock.tryLock()方法试图获取锁。如果未能获取到锁(被其他线程持有中),就调用scanAndLockForPut()方法
可见是自旋执行tryLock()方法获取锁,最多会重试MAX_SCAN_RETRIES(多核环境下为64)次。如果重试达到上限还未成功,就直接调用lock()方法阻塞,等待锁被其他线程释放。注意在重试的最后会检测对应的HashEntry是否发生了变化,如果变化了,会重新开始自旋。
本线程插入完毕之后,调用ReentrantLock.unlock()方法释放锁,同时唤醒AQS队列中阻塞着的下一个线程(如果有的话)进行插入操作,执行完毕。
3.Jvm 内存模型
整个JVM占用的内存可分为两个大区,分别是线程共享区和线程私有区,线程共享区和JVM同生共死,所有线程均可访问此区域;而线程私有区顾名思义每个线程各自占有,与各自线程同生共死。这两个大区内部根据JVM规范定义又分为以下几个区:
线程独占 : 栈 ,本地方法栈 ,程序计数器
线程共享 : 堆 ,方法区
方法区(Method Area):
方法区主要是放一下类似定义,常量,编译后的代码,静态变量等, 在jdk1.7中 ,HotSpot VM 的实现就是将其放在永生带中,这样的好处是可以直接用堆中的GC 算法来进行管理,但是坏处就是经常会出现内存溢出, 所以在jdk 1.8 中HotSpot VM取消了永生带, 用元空间代替,元空间直接使用本地内存,理论上物理机有多少内存它就能使用多少内存,不会出现内存溢出异常
堆(Heap) :
几乎所有对象,树组等都是在此分配内存的,在jvm内存中占的比例也是极大的,也是GC回收的主要阵地,平时所说的新生代,老年代,永生带也是指代这片区域
虚拟机栈(java Stack):
当JVM在执行方法时,会在此区域中创建一个栈帧来存放方法的各种信息,比如返回值,局部变量和各种对象引用等,方法执行前就会创建栈帧入栈,执行完就出栈
本地方法栈 (Native Method Stack):
和虚拟机栈类似,不过是专门提供给Native方法使用的
程序计数器(Program Counter Regiter):
占用了很小一篇区域, JVM执行字节码是一行一行来进行执行的,所以需要一个程序计数器来记录当前执行行数, 每个线程工作时都有一个独立的计数器。程序计数器为执行 Java 方法服务,执行 native 方法时,程序计数器为空。
jvm分带 :新生代,老年代 ,持久代
4.jvm回收器
1.引用计数算法(已被淘汰的算法)
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
目前主流的java虚拟机都摒弃掉了这种算法,最主要的原因是它很难解决对象
之间相互循环引用的问题。尽管该算法执行效率很高。
2.可达性分析算法
目前主流的编程语言(java,C#等)的主流实现中,都是称通过可达性分析(Reachability Analysis)来判定对象是否存活的。这个算法的基本思路**就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。**如下图所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
二.被GC判断为”垃圾”的对象一定会回收吗 ?
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。(即意味着直接回收)
如果这个对象被判定为有必要执行finalize()方法,那么**这个对象将会放置在一个叫做F-Queue的队列之中,并在稍后由一个由虚拟机自动建立的、低优先级的Finalizer线程去执行它。**这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能会导致F-Queue队列中其他对象永久处于等待,甚至导致整个内存回收系统崩溃。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。
可以主动调用finilized函数吗?
finalize()可以主动调用, 但不建议调用, 由于gc也会调用可能会发生某种异常导致资源释放出现问题。
java的四种引用类型
上面的分析可知,无论是通过引用计数还是可达性分析的判断都用到了引用,那么引用是否可以被回收就至关重要了,如果一个引用要么可以被回收,要么就不能被回收那对于一些“可回收”的对象就无能无力了,jdk1.2之后扩充了引用的概念,将引用分为强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom Reference),四种引用引用的强度依次逐渐减弱。
强引用:程序中的普通对象赋值就是强引用,只要引用还在垃圾回收器就永远不会回收被引用的对象。
软引用:描述还有用但并非必须的对象,在系统将要发生内存溢出异常之前,将会把这些对象放入回收范围内进行二次回收,如果还没有足够内存,才抛出异常。
弱引用:也是用来描述非必须对象,强度更弱,弱引用关联的对象只能生存到下一次垃圾收集发生之前,无论内存是否足够都会被回收掉。
虚引用:一个对象是否有虚引用的存在,不会对其生存时间产生任何影响,也无法通过虚引用获取对象实例,虚引用的唯一一个目的就是能在对象被回收时收到一个系统通知
6.mybatis的一二级缓存
一级缓存
1:一级缓存是默认开启的;
2:底层其实是基于hashmap的本地内存缓存;
3:作用域是session(其实就相当于一个方法);
4:当session关闭或者刷新的时候缓存清空;
5:不通sqlsession之间缓存互不影响;
问题一:其实一级缓存也有数据一致性问题:
比如:我有一个更新操作对同一条数据,
如果是sqlsessionA进行了更新操作,则sqlsessionA对应的一级缓存被清空;
如果是sqlsessionB进行了更新操作,则此更新操作对改sqlsessionA不可见;
那么其实这个时候sqlsessionA再查的数据就是过期失效数据了;
就出现了数据不一致现象;
建议:
1:单个sqlsession的生命周期不能过长;
2:如果是对同一个语句更新尽量使用同一个sql,也就是同一个sqlsession;
3:建议关闭一级缓存
二级缓存
1:首先mybatis默认是没有开启二级缓存的,
2:二级缓存需要我们手动开启,它是mapper级别的缓存;
3:同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。
开启二级缓存
使用二级缓存 ,在
二级缓存不建议使用 : 因为二级缓存是建立在同一个namespace下的,如果对某一个表的操作查询可能有多个namespace,那么得到的数据就是有问题的;
建议:
1:对某个表的操作和查询都写在同一个namespace下,其他的namespace如果有操作就会有问题,脏数据;
2:对表的关联查询,关联的所有表的操作都必须在同一个namespace下;(这点在实际生产中简直太垃圾了,怎么可能呢)
总结 : 建议统一使用第三方插件来做缓存,如redis,mamcache等,
关闭mybatis的一级缓存和二级缓存,
mybatis仅仅只限于orm框架,数据库和对象的映射,以及操作sql;
7.乐观锁和悲观锁
悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic
包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
两种锁的使用场景
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
乐观锁常见的两种实现方式
乐观锁一般会使用版本号机制或CAS算法实现。
CAS : CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
1. 版本号机制
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
举一个简单的例子:
假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。当需要对账户信息表进行更新的时候,需要首先读取version字段。
- 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
- 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
- 操作员 A 完成了修改工作,提交更新之前会先看数据库的版本和自己读取到的版本是否一致,一致的话,就会将数据版本号加1( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
- 操作员 B 完成了操作,提交更新之前会先看数据库的版本和自己读取到的版本是否一致,但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,而自己读取到的版本号为1 ,不满足 “ 当前最后更新的version与操作员第一次读取的版本号相等 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。
2. CAS算法
即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。
乐观锁的缺点
ABA 问题是乐观锁一个常见的问题
1 ABA 问题
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
JDK 1.5 以后的 AtomicStampedReference 类
就提供了此种能力,其中的 compareAndSet 方法
就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2 循环时间长开销大
自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。 如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3 只能保证一个共享变量的原子操作
CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类
来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类
把多个共享变量合并成一个共享变量来操作。
CAS与synchronized的使用情景
简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多)
- 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
- 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。
补充: Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的 偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
8.可重入锁
可重入锁,也叫做递归锁,指的是多次对同一个锁进行加锁操作,都不会阻塞线程。实现思路:记录当前锁正在被哪个线程使用,采用计数来统计lock和unlock的调用次数。正常情况下,lock和unlock的调用次数应该相等,如果不相等就会死锁。
9.线程状态
六种状态 :
- 初始状态
实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。
2.1. 就绪状态
-
就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
-
调用线程的start()方法,此线程进入就绪状态。
-
当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态。
-
当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
-
锁池里的线程拿到对象锁后,进入就绪状态。
2.2. 运行中状态
线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一一种方式。
- 阻塞状态
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
- 等待
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
- 超时等待
处于这种状态的线程不会被分配CPU执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
-
终止状态
-
当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
-
在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
使线程退出的方法:
- 使用退出标志,使线程正常退出,也就是当 run() 方法完成后线程中止。
- 使用 stop() 方法强行终止线程,但是不推荐使用这个方法,该方法已被弃用。
- 使用 interrupt 方法中断线程。
sleep 与 wait 的区别
- 这两个方法来自不同的类分别是Thread和Object
- 最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法(锁代码块和方法锁)。
- wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围)
- sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
10.线程池
1.线程池的优势
(1)、降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)、提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
(3)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
(4)提供更强大的功能,延时定时线程池。
2.线程池的主要参数
1、corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
2、maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
3、keepAliveTime(线程存活保持时间)当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
4、workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。
5、threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
5、handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略。
3.线程池主要流程
1、判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。已满则。
2、判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
3、判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和策略。
(1、判断线程池中当前线程数是否大于核心线程数,如果小于,在创建一个新的线程来执行任务,如果大于则
2、判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
3、判断线程池中当前线程数是否大于最大线程数,如果小于,则创建一个新的线程来执行任务,如果大于,则执行饱和策略。
4.线程池为什么需要使用队列
1、因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。
2、创建线程池的消耗较高。
5.线程池为什么使用阻塞队列而不是使用非阻塞队列
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。
当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。
使得在线程不至于一直占用cpu资源。
6.java 中提供的线程池
Executors类提供了4种不同的线程池:newCachedThreadPool, newFixedThreadPool, newScheduledThreadPool, newSingleThreadExecutor
1、newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
2、newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
3、newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
4、newScheduledThreadPool:适用于执行延时或者周期性任务。
7.excute() 和 submit() 方法
1、execute(),执行一个任务,没有返回值。
2、submit(),提交一个线程任务,有返回值。
submit(Callable
11.类加载器
1)类加载器工作流程
- 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自
java.lang.ClassLoader
。 - 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过
ClassLoader.getSystemClassLoader()
来获取它。
类加载器的代理模式
类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。在介绍代理模式背后的动机之前,首先需要说明一下 Java 虚拟机是如何判定两个 Java 类是相同的。Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。
双亲委派模型:
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
- 每一个层次的类加载器都是如此。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。
- 只有当父加载器反馈自己无法完成这个加载请求时(搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。
代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object
类,也就是说在运行的时候,java.lang.Object
这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object
类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
类加载的过程
加载阶段
类的加载阶段是将class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态存储结构转化为方法区中运行时的数据结构,并且在堆内存中生成一个该类的java.lang.class对象,作为方法区数据结构的入口。
类加载阶段的最终产物的堆内存中的class对象,对于同一个Classloader对象,不管某各类被加载多少次,对应堆内存中的class对象始终只有一个。
类加载阶段发生在连接阶段之前,但连接阶段不必等加载阶段结束才开始,可以交叉工作。
连接阶段
类的连接阶段包括三个小的过程:分别是验证、准备和解析。
(1)验证
验证在连接阶段中的主要目地是确保class文件的字节流所包含的内容符合JVM规范,并且不会出现危害JVM自身安全的代码。但验证不符合要求时,会抛出VerifyError这样的异常后其子异常。
主要验证内容有:
验证文件格式:包括文件头部的魔术因子、class文件主次版本号、class文件的MD5指纹、变量类型是否支持等
元数据的验证:对class的字节流进行语义分析,判断是否符合JVM规范。简单来说就是java语法的正确性
字符码验证:主要验证程序的控制流程,如循环、分支等
符号引用验证:验证符号引用转换为直接引用时的合法性,保证解析动作的顺利执行。比如不能访问引用类的私有方法、全限定名称是否能找到相关的类。
(2)准备
准备阶段主要做的事就是在方法区为静态变量发配内存以及赋初始默认值(区别于初始化阶段赋的程序指定的真实值)
注意:final修饰的静态常量在编译阶段就已经赋值,不会导致类的初始化,是一种被动引用,因此也不存在连接阶段。
(3)解析
解析就是在常量池中寻找类、接口、字段和方法的符号引用,并且将这些符号引用替换成直接引用的过程。
解析过程主要针对类接口、字段、类方法和接口方法四类进行。
初始化阶段
初始化阶段是类的加载过程的最后一个阶段,该阶段主要做一件事情就是执行< clinit>(),该方法会为所有的静态变量赋予正确的值。
框架部分
1.IOC AOP
2.Spring 框架中出现的设计模式
a) 工厂设计模式 (简单工厂,方法工厂)
Spring使用工厂模式可以通过BeanFactory或ApplicationContext创建bean对象。
两者对比:
- BeanFactory :延迟注入(使用到某个 bean 的时候才会注入),相比于BeanFactory来说会占用更少的内存,程序启动速度更快。
- ApplicationContext :容器启动的时候,不管你用没用到,一次性创建所有 bean 。BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory ,除了有BeanFactory的功能还有额外更多功能,所以一般开发人员使用ApplicationContext会更多。
b) 单例设计模式
Spring依赖注入Bean实例默认是单例的。
c) 代理设计模式
Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用JDK Proxy去进行代理了,这时候Spring AOP会使用Cglib,这时候Spring AOP会使用Cglib生成一个被代理对象的子类来作为代理
当然你也可以使用AspectJ,Spring AOP已经继承了AspectJ,AspectJ应该算的上是java生态系统中最完整的AOP框架了。
Spring AOP和AspectJ AOP有什么区别?
Spring AOP属于运行时增强,而AspectJ是编译时增强。Spring AOP基于代理,而AspectJ基于字节码操作。
Spring AOP已经集成了AspectJ,AsectJ应该算的上是Java生态系统中最完整的AOP框架了。AspectJ相比于Spring AOP功能更加强大,但是Spring AOP相对来说更简单,如果我们的切面比较少,那么两者的性能差异不大。但是当切面太多的话,最好选择AspectJ,它比Spring AOP快很多。
d) 观察者模式
观察者设计模式是一种对象行为模式。它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变时,这个对象锁依赖的对象也会做出反应。Spring事件驱动模型就是观察者模式很经典的应用。
事件角色:ApplicationEvent(org.springframework.context包下)充当事件的角色,这是一个抽象类。
事件监听者角色:ApplicationListener充当了事件监听者的角色,它是一个接口,里面只定义了一个onApplicationEvent()方法来处理ApplicationEvent。
事件发布者角色:ApplicationEventPublisher充当了事件的发布者,它也是个接口。
Spring事件流程总结:
- 定义一个事件: 实现一个继承自 ApplicationEvent,并且写相应的构造函数;
- 定义一个事件监听者:实现 ApplicationListener 接口,重写 onApplicationEvent() 方法;
- 使用事件发布者发布消息: 可以通过 ApplicationEventPublisher 的 publishEvent() 方法发布消息。
e) 适配器设计模式
适配器设计模式将一个接口转换成客户希望的另一个接口,适配器模式使得接口不兼容的那些类可以一起工作,其别名为包装器。在Spring MVC中,DispatcherServlet根据请求信息调用HandlerMapping,解析请求对应的Handler,解析到对应的Handler(也就是我们常说的Controller控制器)后,开始由HandlerAdapter适配器处理。为什么要在Spring MVC中使用适配器模式?Spring MVC中的Controller种类众多不同类型的Controller通过不同的方法来对请求进行处理,有利于代码的维护拓展。
f) 装饰者设计模式
装饰者设计模式可以动态地给对象增加些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。Spring 中配置DataSource的时候,DataSource可能是不同的数据库和数据源。我们能否根据客户的需求在少修改原有类的代码下切换不同的数据源?这个时候据需要用到装饰者模式。
g) 策略设计模式
Spring 框架的资源访问接口就是基于策略设计模式实现的。该接口提供了更强的资源访问能力,Spring框架本身大量使用了Resource接口来访问底层资源。Resource接口本身没有提供访问任何底层资源的实现逻辑,针对不同的额底层资源,Spring将会提供不同的Resource实现类,不同的实现类负责不同的资源访问类型。
Spring 为 Resource 接口提供了如下实现类:
UrlResource:访问网络资源的实现类。
ClassPathResource:访问类加载路径里资源的实现类。
FileSystemResource:访问文件系统里资源的实现类。
ServletContextResource:访问相对于 ServletContext 路径里的资源的实现类.
InputStreamResource:访问输入流资源的实现类。
ByteArrayResource:访问字节数组资源的实现类。
这些 Resource 实现类,针对不同的的底层资源,提供了相应的资源访问逻辑,并提供便捷的包装,以利于客户端程序的
3.SpringMvc执行流程
1)一个请求匹配前端控制器 DispatcherServlet 的请求映射路径(在 web.xml中指定), WEB 容器将该请求转交给 DispatcherServlet 处理
2)DispatcherServlet 接收到请求后, 将根据 请求信息 交给 处理器映射器 (HandlerMapping)
3)HandlerMapping 根据用户的url请求 查找匹配该url的 Handler,并返回一个执行链
4)DispatcherServlet 再请求 处理器适配器(HandlerAdapter) 调用相应的 Handler 进行处理并返回 ModelAndView 给 DispatcherServlet
5)DispatcherServlet 将 ModelAndView 请求 ViewReslover(视图解析器)解析,返回具体 View
6)DispatcherServlet 对 View 进行渲染视图(即将模型数据填充至视图中)
7)DispatcherServlet 将页面响应给用户
4.Spring Bean 生命周期 初始化流程
Bean实例生命周期的执行过程如下:
- Spring对bean进行实例化,默认bean是单例;
- Spring对bean进行依赖注入;
- 如果bean实现了BeanNameAware接口,spring将bean的id传给setBeanName()方法;
- 如果bean实现了BeanFactoryAware接口,spring将调用setBeanFactory方法,将BeanFactory实例传进来;
- 如果bean实现了ApplicationContextAware接口,它的setApplicationContext()方法将被调用,将应用上下文的引用传入到bean中;
- 如果bean实现了BeanPostProcessor接口,它的postProcessBeforeInitialization方法将被调用;
- 如果bean实现了InitializingBean接口,spring将调用它的afterPropertiesSet接口方法,类似的如果bean使用了init-method属性声明了初始化方法,该方法也会被调用;
- 如果bean实现了BeanPostProcessor接口,它的postProcessAfterInitialization接口方法将被调用;
- 此时bean已经准备就绪,可以被应用程序使用了,他们将一直驻留在应用上下文中,直到该应用上下文被销毁;
- 若bean实现了DisposableBean接口,spring将调用它的distroy()接口方法。同样的,如果bean使用了destroy-method属性声明了销毁方法,则该方法被调用;
5.Spring Bean 作用域
singletion、 prototype、 request、session 和 global session
request、session 和 global session 三种作用域仅在基于web应用中使用 ,只能在基于web的Spring ApplicationContext环境
(1)当一个bean的作用域为Singleton,那么Spring IoC容器中只会存在一个共享的bean实例,并且所有对bean的请求,只要id与该bean定义相匹配,则只会返回bean的同一实例。Singleton是单例类型,就是在创建起容器时就同时自动创建了一个bean的对象,不管你是否使用,他都存在了,每次获取到的对象都是同一个对象。注意,Singleton作用域是Spring中的缺省作用域。要在XML中将bean定义成singleton
(2)当一个bean的作用域为Prototype,表示一个bean定义对应多个对象实例。Prototype作用域的bean会导致在每次对该bean请求(将其注入到另一个bean中,或者以程序的方式调用容器的getBean()方法)时都会创建一个新的bean实例。Prototype是原型类型,它在我们创建容器的时候并没有实例化,而是当我们获取bean的时候才会去创建一个对象,而且我们每次获取到的对象都不是同一个对象。根据经验,对有状态的bean应该使用prototype作用域,而对无状态的bean则应该使用singleton作用域。在XML中将bean定义成prototype,可以这样配置:
<bean id="account" class="com.foo.DefaultAccount" scope="prototype"/>
//或者
<bean id="account" class="com.foo.DefaultAccount" singleton="false"/>
(3)当一个bean的作用域为Request,表示在一次HTTP请求中,一个bean定义对应一个实例;即每个HTTP请求都会有各自的bean实例,它们依据某个bean定义创建而成。该作用域仅在基于web的Spring ApplicationContext情形下有效。考虑下面bean定义:
<bean id="loginAction" class=cn.csdn.LoginAction" scope="request"/>
(3)当一个bean的作用域为Request,表示在一次HTTP请求中,一个bean定义对应一个实例;即每个HTTP请求都会有各自的bean实例,它们依据某个bean定义创建而成。该作用域仅在基于web的Spring ApplicationContext情形下有效。考虑下面bean定义:
<bean id="loginAction" class=cn.csdn.LoginAction" scope="request"/>
(4)当一个bean的作用域为Session,表示在一个HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。考虑下面bean定义:
<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
1
针对某个HTTP Session,Spring容器会根据userPreferences bean定义创建一个全新的userPreferences bean实例,且该userPreferences bean仅在当前HTTP Session内有效。与request作用域一样,可以根据需要放心的更改所创建实例的内部状态,而别的HTTP Session中根据userPreferences创建的实例,将不会看到这些特定于某个HTTP Session的状态变化。当HTTP Session最终被废弃的时候,在该HTTP Session作用域内的bean也会被废弃掉。
(5)当一个bean的作用域为Global Session,表示在一个全局的HTTP Session中,一个bean定义对应一个实例。典型情况下,仅在使用portlet context的时候有效。该作用域仅在基于web的Spring ApplicationContext情形下有效。考虑下面bean定义:
<bean id="user" class="com.foo.Preferences "scope="globalSession"/>
1
global session作用域类似于标准的HTTP Session作用域,不过仅仅在基于portlet的web应用中才有意义。Portlet规范定义了全局Session的概念,它被所有构成某个portlet web应用的各种不同的portlet所共享。在global session作用域中定义的bean被限定于全局portlet Session的生命周期范围内。
6.SpringBoot 核心注解 三个注解
- @Configuration (来自于spring3.0)
用来代替applicationContext.xml配置文件, 所有这个配置文件里面能做到的事都可一通过这个注解所在类来进行注册
相关注解:
@Bean 用来代替XML配置里面
@ImportResource 如果有些通过类的注册方式配置不了的,可以通过这个注解引入额外的xml配置文件,有些老的配置文件无法通过@Configuration的方式来配置 ,通过此注解非常管用
@Import 用来引入额外的一个或者多个@Configuration 修饰的配置文件类
@SpringBootConfiguration 这个注解是 @Configuration 注解的变体, 只是用来修饰Spring Boot配置而已
- @ComponentScan
这是spring3.1添加的一个注解 ,用来代替配置文件中的component-scan配置, 开启组件扫描, 即自动扫描包路径下的@Component注解进行注册bean实例到context中。 另外 @ComponentScans是可重复注解,即可以配置多个用来配置注册不同的子包
- @EnableAutoConfiguration
注: @SpringBootApplication 含有以上三个注解
7.springBoot 的特点以及启动流程
spring有四个特性:
特性一:更快的构建能力
SpringBoot提供更多的Starters用于快速构建业务框架,Starters可以理解为启动器,它包含一系列的可以集成到应用里面的依赖包,可以一站式集成spring以及其他技术,而不需要自己去找依赖
常见的 Starters 有以下几个:
spring-boot-starter-test
spring-boot-starter-web
spring-boot-starter-data-jpa
spring-boot-starter-thymeleaf
特性二:起步依赖
SpringBoot提供了起步依赖,在创建工程时可以直接勾选依赖模块,在项目初始化的时候就可以把相关依赖直接俄添加到项目中,大大缩短了查询并添加依赖的时间
特性三:内嵌容器支持
Spring Boot 内嵌了 Tomcat、Jetty、Undertow 三种容器,其默认嵌入的容器是 Tomcat。
特性四:Actuator 监控
Spring Boot 自带了 Actuator 监控功能,主要用于提供对应用程序监控,以及控制的能力,比如监控应用程序的运行状况,或者内存、线程池、Http 请求统计等,同时还提供了关闭应用程序等功能。
启动流程
1) 如果我们使用的是SpringApplication的静态run方法,那么,这个方法里面首先要创建一个SpringApplication对象实例,然后调用这个创建好的SpringApplication的实例方法。在SpringApplication实例初始化的时候,它会提前做几件事情:
- 根据classpath里面是否存在某个特征类(org.springframework.web.context.ConfigurableWebApplicationContext)来决定是否应该创建一个为Web应用使用的ApplicationContext类型。
- 使用SpringFactoriesLoader在应用的classpath中查找并加载所有可用的ApplicationContextInitializer。
- 使用SpringFactoriesLoader在应用的classpath中查找并加载所有可用的ApplicationListener。
- 推断并设置main方法的定义类。
2) SpringApplication实例初始化完成并且完成设置后,就开始执行run方法的逻辑了,方法执行伊始,首先遍历执行所有通过SpringFactoriesLoader可以查找到并加载的SpringApplicationRunListener。调用它们的started()方法,告诉这些SpringApplicationRunListener,“嘿,SpringBoot应用要开始执行咯!”。
3) 创建并配置当前Spring Boot应用将要使用的Environment(包括配置要使用的PropertySource以及Profile)。
4) 遍历调用所有SpringApplicationRunListener的environmentPrepared()的方法,告诉他们:“当前SpringBoot应用使用的Environment准备好了咯!”。
5) 如果SpringApplication的showBanner属性被设置为true,则打印banner。 【banner:英文广告横幅,在这里面指的是运行时输出的SpringBoot,还可以进行修改
6) 根据用户是否明确设置了applicationContextClass类型以及初始化阶段的推断结果,决定该为当前SpringBoot应用创建什么类型的ApplicationContext并创建完成,然后根据条件决定是否添加ShutdownHook,决定是否使用自定义的[BeanNameGenerator],决定是否使用自定义的[ResourceLoader],当然,最重要的,将之前准备好的Environment设置给创建好的ApplicationContext使用。
7) ApplicationContext创建好之后,SpringApplication会再次借助[Spring-FactoriesLoader],查找并加载classpath中所有可用的[ApplicationContext-Initializer],然后遍历调用这些ApplicationContextInitializer的initialize(applicationContext)方法来对已经创建好的ApplicationContext进行进一步的处理。
8) 遍历调用所有[SpringApplicationRunListener]的contextPrepared()方法。
9) 最核心的一步,将之前通过@EnableAutoConfiguration获取的所有配置以及其他形式的IoC容器配置加载到已经准备完毕的ApplicationContext。
10) 遍历调用所有SpringApplicationRunListener的contextLoaded()方法。
11) 调用ApplicationContext的refresh()方法,完成IoC容器可用的最后一道工序。
12) 查找当前ApplicationContext中是否注册有CommandLineRunner,如果有,则遍历执行它们。
13) 正常情况下,遍历执行SpringApplicationRunListener的finished()方法、(如果整个过程出现异常,则依然调用所有SpringApplicationRunListener的finished()方法,只不过这种情况下会将异常信息一并传入处理)
分布式微服务部分
1.什么是分布式 ?( 集群思想)
所谓分布式,无非就是将一个系统拆分成多个子系统并分布到多个服务器上.
简单的说,是指将用户界面、控制台服务、数据库管理三个层次部署在不同的位置上。其中用户界面是客户端实现的功能,控制台服务是一个专门的服务器,数据管理是在一个专门的数据库服务器上实现的。
分布式常用框架:Dubbo,MQ消息队列,Zookeeper等.
关于分布式服务框架Dubbo
1.服务注册中心,不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的IP地址,并且能够平滑添加或删除服务提供者。
2.透明化的远程方法调用,就像调用本地方法一样调用远程方法,只需简单配置,没有任何API侵入。
3.软负载均衡及容错机制,可在内网替代F5等硬件负载均衡器.
关于MQ消息队列(RabbitMQ、ZeroMQ、ActiveMQ等.)
在高并发分布式环境下,由于来不及同步处理,请求往往发生堵塞,比如说,大量的insert、update之类的请求
同时到达数据库,直接导致无所的行锁和表锁,甚至最后请求会堆积过多,从而触发too many connections错误。
通过使用消息队列,我们可以异步处理请求,从而缓解系统的压力。
ACK机制-当消费者拿到消息的瞬间,队列中的消息立即删除.同时删除 前台页面,缓存库,数据库 .(ACK机制保证了性能的高效.)
3.消息中间件 (mq)原理
4.微服务框架(SpringCloud 组件,和组件原理 熔断机制)
sql部分
1.说说对sql的优化
1.MySQL数据库作发布系统的存储,一天五万条以上的增量,预计运维三年,怎么优化?
a. 设计良好的数据库结构,允许部分数据冗余,尽量避免join查询,提高效率。
b. 选择合适的表字段数据类型和存储引擎,适当的添加索引。
c. mysql库主从读写分离。
d. 找规律分表,减少单表中的数据量提高查询速度。
e。添加缓存机制,比如memcached,apc等。
f. 不经常改动的页面,生成静态页面。
g. 书写高效率的SQL。比如 SELECT * FROM TABEL 改为 SELECT field_1, field_2, field_3 FROM TABLE.
2.实践中如何优化MySQL
最好是按照以下顺序优化:
1.SQL语句及索引的优化
\2. 数据库表结构的优化
3.系统配置的优化
4.硬件的优化
5.说说对SQL语句优化有哪些方法?(选择几条)
(1)Where子句中:where表之间的连接必须写在其他Where条件之前,那些可以过滤掉最大数量记录的条件必须写在Where子句的末尾.HAVING最后。
(2)用EXISTS替代IN、用NOT EXISTS替代NOT IN。
(3) 避免在索引列上使用计算
(4)避免在索引列上使用IS NULL和IS NOT NULL
(5)对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
(6)应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描
(7)应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描
什么情况下会导致索引失效:
1.某个表中如果有两列 ,都创建了单独索引 下列查询会导致索引失效 :
select * from test where id=c_id; (这种情况下会被认定为不如走全表扫描)
2.在索引上使用 IS NULL 或者 IS NOT NULL
索引在设计数据库时尽量避免NULL值得出现,如果不可避免,则最好给定DEFALT值, 字符串空串有时候也会存在问题,也尽量给空格或者其他
3.like 通配符 , 前置通配符%xx会导致索引失效, 索引尽量使用后置通配符 xx%
4.or 语句 前后没有同时使用 索引 , 会导致索引失效
5.组合索引,不是使用第一索引, 索引失效
6.数据类型出现隐式转化, 如varchar 不加单引号的话可能会自动转换为int类型,使索引失效, 产生全表扫面
7、对索引字段进行计算操作、字段上使用函数。
2020.10.12 面试相关
springboot配置文件加载顺序
(1)bootstrap.yml(bootstrap.properties)先加载
(2)application.yml(application.properties)后加载
(3)同一种类型的.yml的配置文件加载顺序优先于.properties的配置文件
项目中 数据库事务, shrio (怎么实现的权限控制) 一二级缓存,什么时候默认走一级缓存不走数据库
线程池 必会 数据库索引必会
jdcbType 是必须的吗
ESeacsh(周六学习) redis(周末学习)
@Primary @Qualifier @Autowired 注解使用
mybatis相关
1.$ 和 #de区别
${}
是 Properties 文件中的变量占位符,它可以用于 XML 标签属性值和 SQL 内部,属于字符串替换。例如将 ${driver}
会被静态替换为 com.mysql.jdbc.Driver
${}
也可以对传递进来的参数原样拼接在 SQL 中。实际场景下,不推荐这么做。因为,可能有 SQL 注入的风险。
#{}
是 SQL 的参数占位符,Mybatis 会将 SQL 中的 #{}
替换为 ?
号,在 SQL 执行前会使用 PreparedStatement 的参数设置方法,按序给 SQL 的 ?
号占位符设置参数值,比如 ps.setInt(0, parameterValue)
。 所以,#{}
是预编译处理,可以有效防止 SQL 注入,提高系统安全性。
2.jdbcType 是必须的吗
MyBatis 插入空值时,需要指定JdbcType
mybatis insert空值报空值异常,但是在pl/sql不会提示错误,主要原因是mybatis无法进行转换
3.实体类和数据库字段不一致
第一种, 通过在查询的 SQL 语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
第二种,如果数据库字段名和实体类中的属性名差,主要是前者为下划线风格,后者为驼峰风格。在这种情况下,可以直接配置如下,实现自动的下划线转驼峰的功能。
<setting name="logImpl" value="LOG4J"/> <setting name="mapUnderscoreToCamelCase" value="true" /></settings>
第三种,通过
4.引用
5.Mapper 接口里的方法能否被重载
Mapper 接口里的方法,是不能重载的,因为是全限名 + 方法名的保存和寻找策略
6.不同的mapper.xml中 ,id 是否可以重复
如果配置了
7.如何获取自动生成的主键
方式一 : 使用 useGeneratedKeys + keyProperty 属性
<insert id="insert" parameterType="Person" useGeneratedKeys="true" keyProperty="id"> INSERT INTO person(name, pswd) VALUE (#{name}, #{pswd})</insert>
方式二 :mysql 中 使用
<insert id="insert" parameterType="Person"> <selectKey keyProperty="id" resultType="long" order="AFTER"> SELECT LAST_INSERT_ID() </selectKey> INSERT INTO person(name, pswd) VALUE (#{name}, #{pswd})</insert>
8.mybatis 执行批量插入,能返回数据库主键列表吗 ?
可以
9.mybatis 能否映射枚举类型
可以
10.mybatis 是否支持延迟加载
mybatis 仅支持一对一和一对多查询延迟加载
使用
它的原理是,使用 CGLIB 或 Javassist( 默认 ) 创建目标对象的代理对象。当调用代理对象的延迟加载属性的 getting 方法时,进入拦截器方法。比如调用 a.getB().getName()
方法,进入拦截器的 invoke(...)
方法,发现 a.getB()
需要延迟加载时,那么就会单独发送事先保存好的查询关联 B 对象的 SQL ,把 B 查询上来,然后调用a.setB(b)
方法,于是 a
对象 b
属性就有值了,接着完成a.getB().getName()
方法的调用。这就是延迟加载的基本原理。
11.如果A标签通过include引用了B标签, 请问B标签是否可以定义在A标签的后面
都可以,Mybaits 解析A标签,发现A标签引用了B标签, 但是B标签未解析到,就会把A标签定义为未解析,继续剩余的解析,包含B标签,等全部解析完毕之后,Mybatis 会重新解析那些被标记未解析的标签,此时解析A标签时,B标签已经存在,A标签也就顺利的解析完成了
- mybatis 失效
mybatis的一级缓存生效的范围是sqlsession,是为了在sqlsession没有关闭时,业务需要重复查询相同数据使用的。一旦sqlsession关闭,则由这个sqlsession缓存的数据将会被清空。
resultType 和 resultMap的区别
spring 多个相同类型的bean 注入
pojo 和 实体不对应 解决方案
springboot的特点以及 加载流程
mybatis 映射
HashMap与HashTable区别
tcp 与 udp 与 socket 等要做复习
java 五种io模型
B数 和 B+数
mybatis映射
书籍 : 《剑指offer》
五大常用算法思想 需要看
动态规划算法
redis 相关 :
最常用数据类型 :String
Redis 持久化
提供了两种方式,将数据持久化到硬盘
Redis 为什么快
Redis 是非阻塞 IO ,多路复用。
1)C语言实现
2)纯内存操作
3)基于非阻塞i/o ,多路复用机制
4)单线程,避免了多线程频繁的上下文切换
Redis 是单线程,如何提高多核CPU利用率
可以在同一服务器部署多个Redis 实例,并把他们当作不同的服务器使用
1)(全量)RDB持久化,是指在指定的时间间隔内,将指定内存中的数据集快照写入磁盘
RDB优点:
(1) 灵活设置备份频率和周期。
(2)适合冷备份,可以将一个单独的文件压缩后,转移到其他存储介质上,例如云服务器
(3)性能最大化,在持久化的最开始它唯一需要做的就是fork出一个子进程,然后由这个子进程完成持久化,这样就避免了大量的io操作,RDB对redis的对外提供的读写服务影响很小,可以保持高性能运作
(4)恢复更快,与AOF相比,RDB恢复速度更快,更适合恢复数据,特别是数据集特别大的时候
RDB缺点:
(1)如果想保证数据的高可用性,即最大限度的避免数据丢失,最好不要用RDB,因为系统一旦在定时持久化之前出现宕机现象,此前没来得及写入磁盘的数据都会丢失
(2)由于RDB是通过fork出子进程来完成持久化的,如果数据集过大,可能会导致整个服务器短暂停止服务
2)(增量)AOF持久化,以日志形式记录服务器所处理的每一个写、删除 操作,查询记录不会记录,以文本的方式记录,可以打开文件看到详细的操作记录
Redis 数据结构(五种)
String(字符串) 、Hash(字典)、List(列表)、Set(集合)、SortedSet(有序集合)
各种数据结构的区别
redis 的穿透 击穿 雪崩
1.击穿: 指的是单个key在缓存中查询不到,去数据库查询,这样如果数据量不大或者并发不大的情况是没什么问题的,如果数据量过大,或者是高并发情况下,会导致数据库压力过大导致崩溃
解决方法:
1)通过synchronized+双重检查机制:某个key只让一个线程查询,阻塞其它线程
-
设置value永不过期
-
使用互斥锁(mutex key)
布隆过滤器
2 .雪崩
雪崩指的是多个key查询并且出现高并发,缓存中失效或者查不到,然后都去db查询,从而导致db压力突然飙升,从而崩溃。
出现原因:
1) key同时失效
2) redis本身崩溃了
解决方案:
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。(跟击穿的第一个方案类似,但是这样是避免不了其它key去查数据库,只能减少查询的次数)
- 可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存
- 不同的key,设置不同的过期时间,具体值可以根据业务决定,让缓存失效的时间点尽量均匀
- 做二级缓存,或者双缓存策略。A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。(这种方式复杂点)
3.穿透
一般是出现这种情况是因为恶意频繁查询才会对系统造成很大的问题: key缓存并和数据库不存在,所以每次查询都会查询数据库从而导致数据库崩溃。
解决方案:
1)使用布隆过滤器: 热点数据等场景(具体看使用场景)
2 ) 将击透的key缓存起来,但是时间不能太长,下次进来是直接返回不存在,但是这种情况无法过滤掉动态的key,就是说每次请求进来都是不同额key,这样还是会造成这个问题
什么是布隆过滤器 ?
可以理解为有点儿傻的set结构,当使用它的contains 方法判断某个对象是否存在时,他可能会误判,
当布隆过滤器说某个值存在时不一定存在,但是当它说某个值不存在时就一定不存在
分布式锁概念 以及redis 实现
由于分布式的原因 某个变量可能存在于JVM1,JVM2,JVM3 等多个JVM内存中,所以会让单机部署并发控制锁的策略失败,为了解决这个问题,就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁
分布式锁应该具备哪些条件?
1.在分布式系统下,一个方法同一时间只能被一个机器的同一线程执行
2. 高可用的获取锁与释放锁;
3. 高性能的获取锁与释放锁;
4. 具备可重入特性;
5. 具备锁失效机制,防止死锁;
6. 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
分布式锁的三种实现方式:
相关***
CAP理论:
任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。
互联网大都会牺牲强一致性换取系统的高可用,系统往往只需要保持最终一致性就可以了, 只要这个最终时间是在用户可以接受的范围内即可。
为了保持最终一致性 我们往往需要分布式事务,分布式锁等技术来实现 保证一个方法在同一时间内只能被同一个线程执行
1.基于数据库实现分布式锁;
1)基于数据库实现排他锁:
2.基于缓存(Redis等)实现分布式锁;
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
3.基于Zookeeper实现分布式锁;
分布式 唯一序列号
数据库 和 redis 不一致问题
QPS (每秒请求 次数 10W+)
redis Scan (分页查询)
数据接口
mysql 存储引擎 (即表类型 table_type)
mysql 支持多种存储引擎 ,包括 MyISAM、InnoDB、BDB、MEMORY、MERGE、EXAMPLE、NDB Cluster、 ARCHIVE 等, 其中MyISAM支持事务安全 ,他还支持一些第三方存储引擎,例如TokuDB(高写能能高压缩存储引擎)、 Infobright (列式存储引擎)
分布式事务
ArrayList 与 LinkedList区别
ArrayList : ArrayList实现了List接口,它是以数组的方式来实现的,数组的特性是可以使用索引的方式来快速定位对象的位置,因此对于快速的随机取得对象的需求,使用ArrayList实现执行效率上会比较好.
LinkedList : LinkedList是采用链表的方式来实现List接口的,它本身有自己特定的方法,如: addFirst(),addLast(),getFirst(),removeFirst()等. 由于是采用链表实现的,因此在进行insert和remove动作时在效率上要比ArrayList要好得多!适合用来实现Stack(堆栈)与Queue(队列),前者先进后出,后者是先进先出.
在删除可插入对象的动作时,为什么ArrayList的效率会比较低呢?
因为ArrayList是使用数组实现的,若要从数组中删除或插入某一个对象,需要移动后段的数组元素,从而会重新调整索引顺序,调整索引顺序会消耗一定的时间,所以速度上就会比LinkedList要慢许多. 相反,LinkedList是使用链表实现的,若要从链表中删除或插入某一个对象,只需要改变前后对象的引用即可!
LinkedList Api 错误场景
addFirst () 失败会抛出异常
removeFirst () 失败抛出异常
getFirst() 失败抛出异常
offerFirst() 成功返回true
pollFirst() 失败 失败返回null
peekFirst() 失败返回null