基础
JDK1.8的新特性(阿里)
①引入了lambda表达式,可以简化匿名内部类的代码,允许将方法作为参数。②方法引用,可以进一步简化lambda表达式的书写,可以引用类的构造方法,静态方法,特定类的方法和某个对象的方法。③可以在接口中使用default定义默认方法和静态方法,引入默认方法方便了接口升级的维护,之前如果在接口中新增一个方法必须修改所有实现类。④引入了stream类,支持链式编程,为集合类和数组提供了一些方便的操作方法,例如filter、skip、limit和map、concat等。⑤可以通过类型自动推测泛型参数。⑥允许重复使用注解,扩大了注解的使用范围,可以用在局部变量和泛型,方法异常上。⑦引入了Optional类解决空指针异常,更新了日期类的API等。
HashMap的数据结构,源码原理,线程安全问题,红黑树
数据结构(招行)
JDK1.8之前使用的是数组+链表的数据结构,每一个元素是一个Entry结点,包含key、value、hash值、指向下一个元素的next指针四个属性。JDK1.8之后使用的是数组+链表或红黑树的数据结构,每一个元素是一个Node结点,Node实现了Entry接口,Node有一个子类TreeNode,代表树结点。
源码原理(阿里)
put方法:
①在存放数据时会先通过hash方法计算key的hashCode,JDK1.8之前的计算比较复杂,但是效率并不高,JDK1.8将计算出的hashCode高低16位进行异或运算,可以保证尽可能多的位数参与运算,并且让结果中的0和1尽量分布均匀,降低哈希冲突的概率,使键值尽可能分散,提高查询效率。
②计算出hash值后,再将hash值与数组的长度-1进行与操作,这样可以保证索引的范围在数组的范围之内,由于数组长度必须是2的幂次方,-1后必然是011…11这样的形式,进行与运算就可以保证结果的0和1分别更加均匀。
③计算出索引后,JDK1.8之前如果结点为空就创建新的Entry结点,否则遍历链表根据hash值和key决定覆盖value值还是创建新的结点。JDK1.8中,如果结点为空直接增加一个链表结点,如果是一个树结点就增加一个树结点,如果都不是则代表是链表结点,就遍历链表,根据key和hash值判断是否重复以决定替代value值还是新增结点,如果添加链表后,达到建树阈值TREEIFY_THRESHOLD-1时,就会将链表转为红黑树,TREEIFY_THRESHOLD是一个值为8常量。由于链表查找时间复杂度为O(n),红黑树为O(logn),当数值太小时查找效率相差无几,因此设有一个阈值。
resize方法:
①重新规划table的长度和阈值,如果达到扩容阈值,就把table的容量增加到旧容量的2倍。如果新的容量小于默认的初始化容量16就置为16,阈值重新设置为新容量和加载因子之积。如果新的table容量超出或等于最大容量(1<<30),将阈值调整为最大整形数,并且return终止过程。
②重新排列数据结点,遍历table上的每一个结点分别处理。如果是null则跳过,如果不为null且没有next结点,重新计算hash值并存入新的table。如果结点为树结点,调用split方法处理,如果红黑树太小就退化回链表。如果都不是,则说明是链表结点,如果hash值和oldCap与的结果为0则不处理,否则存放到新的下标。
线程安全问题(阿里)
①JDK1.8之前,链表结点的插入使用头插法,在多线程操作的时候可能会产生链表死循环问题。②JDK1.8起,链表结点的插入改为尾插法,不会形成环,但是多线程操作时可能会存在值丢失的问题。③如果要解决线程安全,可以使用ConcurrentHashMap,是线程安全的HashMap,数据结构是Segment数组和Entry数组,采用了减小锁粒度的思想使用分段锁来保证线程安全,Segment的数量就是锁的并发度,默认为16,一个Segment包含一个HashEntry链表,HashEntry用来存储数据,当修改HashEntry的时候必须先获取对应的Segment锁。
红黑树(招行)
①红黑树就是一种自平衡二叉树,实现原理和平衡二叉树类似,但性能要优于平衡二叉树。
②红黑树的特性:每个结点是红色或黑色、根结点是黑色、每个叶子结点是黑色、每个红色结点的两个子结点是黑色、从任意结点到每个叶子的路径包含数目相同的黑色结点。
③红黑树的插入过程主要操作有两种,变色,用于调整两个红色结点相邻的情况,适应性质4,旋转,用于调整左右子树黑色结点数目不同的情况,适应性质5。
④在HashMap中putTreeVal用于保存树结点,执行二叉树查找,每一次都比较当前结点和待插入结点的大小,如果小就在左子树查找,否则往右子树查找。找到空位后,执行两个方法,balanceInsertion平衡插入,将结点插入红黑树并使之平衡,moveRootToFront重置红黑树的根结点。
并发
线程的实现方式(阿里)
①直接继承Thread类,重写run方法,缺点是无法继承其他类。②实现Runnable接口,重写run方法,将实现类作为参数传入Thread的构造方法。优点是可以继承其他类,实现了解耦操作,适合多个线程访问同一个共享资源。③实现Callable接口,重写call方法,将实现类包装成一个FutureTask对象作为参数传入Thread的构造方法。优点是可以带有返回值,缺点是相对复杂。④还可以提高线程池来创建线程。
synchronized关键字
作用:
①用于为Java对象、方法、代码块提供线程安全的操作,属于排它的悲观锁,也属于可重入锁。②被synchronized修饰的方法和代码块在同一时刻只能有一个线程访问,其他线程只有等待当前线程释放锁资源后才能访问。③Java中的每个对象都有一个monitor监视器对象,加锁就是在竞争monitor,对代码块加锁是通过在前后分别加上monitorenter和monitorexit指令实现的,对方是否加锁是通过一个标记位来判断的。
内部结构:
synchronized内部包括6个区域,每个区域的数据都代表锁的不同状态。①ContentionList:锁竞争队列,所有请求锁的线程都被放在竞争队列中。②EntryList:竞争候选列表,在锁竞争队列中有资格成为候选者来竞争锁资源的线程被移动到候选列表中。③WaitSet:等待集合,调用wait方法后阻塞的线程将被放在WaitSet。④OnDeck:竞争候选者,在同一时刻最多只有一个线程在竞争锁资源,该线程的状态被称为OnDeck。⑤Owner:竞争到锁资源的线程状态。⑥!Owner:释放锁后的状态。
流程:
①收到新的锁请求时首先自旋,如果通过自旋也没有获取锁资源,被放入ContentionList(该做法对于已经进入队列的线程是不公平的,体现了synchronized的不公平性)。②为了防止ContentionList尾部的元素被大量线程进行CAS访问影响性能,Owner线程会在是释放锁时将ContentionList的部分线程移动到EntryList并指定某个线程(一般是最先进入的)为OnDeck线程。Owner并没有将锁直接传递给OnDeck线程而是把锁竞争的权利交给他,该行为叫做竞争切换,牺牲了公平性但提高了性能。③获取到锁的OnDeck线程会变为Owner线程,未获取到的仍停留在EntryList中。④Owner线程在被wait阻塞后会进入WaitSet,直到某个时刻被唤醒再次进入EntryList。⑤ContentionList、EntryList、WaitSet中的线程均为阻塞状态。⑥当Owner线程执行完毕后会释放锁资源并变为!Owner状态。
volatile关键字
作用:
①保证被修饰的变量对所有线程可见,在一个线程修改了变量的值后,新的值对于其他线程是可以立即获取的。②禁止指令重排序,被修饰的变量不会被缓存在寄存器中或者对其他处理器不可见的地方,因此在读取volatile修饰的变量时总是会返回最新写入的值。③不会执行加锁操作,不会导致线程阻塞,主要适用于一个变量被多个线程共享,多个线程均可对这个变量执行赋值或读取的操作。④volatile可以严格保证变量的单次读写操作的原子性,但并不能保证像i++这种操作的原子性,因为i++在本质上是读、写两次操作。
显式锁和synchronized、volatile的区别(阿里)
volatile和synchronized的区别:
①volatile只能修饰实例变量和类变量,而synchronized可以修饰方法以及代码块。②volatile只能保证数据的可见性,但是不保证原子性,synchronized是一种排它机制,可以保证原子性。只有在特殊情况下才适合取代synchronized:对变量的写操作不依赖于当前值(例如i++),或者是单纯的变量赋值;该变量没有被包含在具有其他变量的不等式中,不同的volatile变量不能互相依赖,只有在状态真正独立于程序内的其它内容时才能使用volatile。③volatile是一种轻量级的同步机制,在访问volatile修饰的变量时并不会执行加锁操作,线程不会阻塞,使用synchronized加锁会阻塞线程。
synchronized和ReentrantLock的区别:
①synchronized是隐式锁,ReentrantLcok是显式锁,使用时必须在finally代码块中进行释放锁的操作。②synchronized是非公平锁,ReentrantLock可以实现公平锁。③ReentrantLock可响应中断,可轮回,为处理锁提高了更多灵活性。④synchronized是一个关键字,是JVM级别,ReentrantLock是一个接口,是API级别。⑤synchronized采用悲观并发策略,ReentrantLock采用的是乐观并发策略,会先尝试以CAS方式获取锁。
如何实现线程安全的计数器,原子类安全的原理(阿里)
①可以使用atomic包下的原子类AtomicInteger类。②原子类中的成员变量value是volatile修饰的,可以保证可见性和有序性,其中的操作方法都是以CAS方式进行的。
什么是CAS
①CAS指Compare And Swap,比较并交换。CAS(V,E,N)算法包括三个参数,V表示要更新的变量的值,E表示预期的值,N表示新值。在且仅在V的值和E相等时才会将V的值设置为N,如果不同则说明已经有其他线程做了更改,当前线程就什么也不做。最后CAS返回当前V的真实值。②CAS操作采用了乐观锁的思想,有多个线程同时使用CAS操作一个共享变量时只有一个线程会成功,失败的线程不会被挂起仅会被告知失败,并且允许再次尝试,或者放弃操作。基于这样的原理虽然CAS没有使用锁,也可以及时发现其他线程的操作进行适当地并发处理。
CAS有什么问题
①CAS算法地实现有一个重要前提:需要取出内存中某时刻的数据,然后在下一刻进行比较、替换,但在这个时间差内数据可能已经发生了变化,导致ABA问题。②ABA问题指线程1从内存V位置取出A,这时线程2也从内存中取出A,并将其首先修改为B,接着又修改为A,这时线程1在进行CAS操作时会发现内存中数据仍是A,然后线程1操作成功。尽管从操作角度来说线程1成功了,但是在该过程中实际上数据已发生了变化但并未被感知到,某些应用场景下可能会出现数据不一致的问题。③乐观锁通过版本号来解决ABA问题,具体的操作是每次执行数据修改操作时都会带上一个版本号,如果预期版本号和数据版本号一致就进行操作,并将版本号加1,否则执行失败。
JVM
JVM的内存区域(阿里)
JVM的内存区域分为线程私有区域(程序计数器、虚拟机栈、本地方法区)、线程共享区域(堆、方法区)和直接内存。①程序计数器是一块很小的内存空间,用于存储当前线程执行字节码文件的行号指示器。②虚拟机栈是描述Java方法执行过程的内存模型,帧栈中存储了局部变量表,操作数栈,动态链接,方法出口等信息。③本地方法栈,和虚拟机栈作用类似,区别是虚拟机栈为Java方法服务,本地方法栈为Native方法服务。④JVM运行过程中创建的对象和生成的数据都存储在堆中,堆是被线程共享的内存区域,也是垃圾回收最主要的内存区域。⑤方法区用来存储常量,静态变量、类信息、即时编译器编译后的机器码、运行时常量池等数据。
JVM怎么调优
类加载机制和双亲委派模型,如何判断两个类是不是一个(阿里)
类加载机制:
类加载到内存中主要有5个阶段,分别为①加载:将Class文件读取到运行时数据区的方法区内,在堆中创建Class对象,并封装类在方法区的数据结构的过程。②验证:主要用于确保Class文件符合当前虚拟机的要求,保障虚拟机自身的安全,只有通过验证的Class文件才能被JVM加载。③准备:主要工作是在方法区中为类变量分配内存空间并设置类中变量的初始值。④解析:将常量池中的符号引用替换为直接引用。⑤初始化:主要通过执行类构造器的方法为类进行初始化,该方法是在编译阶段由编译器自动收集类中静态语句块和变量的赋值操作组成的。JVM规定,只有在父类的方法都执行成功后,子类的方法才可以被执行。在一个类中既没有静态变量赋值操作也没有静态语句块时,编译器不会为该类生成方法。
类加载器和双亲委派模型:
①主要有启动类加载器,负责加载JAVA_HOME/lib中的类库;扩展类加载器,负责加载JAVA_HOME/lib/ext中的类库;应用程序类加载器,也称系统类加载器,负责加载用户类路径上指定的类库;也可以自定义类加载器。②类加载器之间的层次关系叫做双亲委派模型,要求除了顶层的启动类加载器外其余的类加载器都应当有自己的父类加载器。一个类收到类加载请求后会层层找父类加载器去尝试加载,因此所有的加载请求最终都会被传送到顶层的启动类加载器,只有当父类加载器反馈自己无法完成加载时子加载器才会尝试自己去加载。③双亲委派模型的好处是保障类加载的唯一性和安全性,例如加载rt.jar包中的java.lang.Object,无论哪一个类加载最终都会委托给启动类加载器,这样就保证了类加载的唯一性。如果存在包名和类名都相同的两个类,那么该类就无法被加载。
如何判断两个类是否相同:
根据全限定类名和类加载器,如果都相同则是同一个。
什么时候会发生GC?(阿里)
内存分配和回收策略
①对象优先在Eden区分配:大多数情况下对象在新生代Eden区分配,当Eden区没有足够空间时,虚拟机将发起一次MinorGC。②大对象直接进入老年代:大对象是指需要大量连续内存空间的Java对象,如很长的字符串及数组。虚拟机提供了一个参数-XX:PretenureSizeThreshold,大于该值的对象会直接进入老年代,防止它在新生代之间来回复制。③长期存活的对象进入老年代:虚拟机给每个对象定义了一个年龄计数器,若对象在Eden区出生、经过第一次MinorGC后仍存活且能被Survivor容纳,将被移到Survivor区并且对象年龄设为1。每经过一次MinorGC,年龄就加1。默认在年龄增加到15时晋升到老年代,可通过-XX:MaxTenuringThreshold设置晋升老年代的年龄阈值。④动态对象年龄判定:如果在Survivor空间中相同年龄所有对象大小超过了该空间的一半,大于等于该年龄的对象就可以直接进入老年代而不用等到达到阈值。⑤空间分配担保:发生MinorGC前,先判断老年代最大可用连续空间是否大于新生代所有对象的总空间,如果成立那么MinorGC是安全的。如果不成立会查看HandlePromotionFailure是否允许担保,如果允许会冒险进行MinorGC,否则改为一次FullGC。
新生代是怎么分区的
①JVM新创建的对象(除了大对象外)会被存放在新生代,默认占1/3堆内存空间。由于JVM会频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。②新生代又分为Eden区,ServivorFrom区和ServivorTo区。③Eden区:Java新创建的对象首先会被存放在Eden区,如果新创建的对象属于大对象,则直接将其分配到老年代。大对象的定义和具体的JVM版本、堆大小和垃圾回收策略有关,一般为2KB~128KB,可通过-XX:PretenureSizeThreshold设置其大小。在Eden区的内存空间不足时会触发MinorGC,对新生代进行一次垃圾回收。②ServivorTo区:保留上一次MinorGC时的幸存者。③ServivorFrom区:将上一次MinorGC时的幸存者作为这一次MinorGC的被扫描者。
新生代的垃圾回收机制
新生代的GC过程叫做MinorGC,采用复制算法实现,具体过程如下:①把在Eden区和ServivorFrom区中存活的对象复制到ServivorTo区,如果某对象的年龄达到老年代的标准,则将其复制到老年代,同时把这些对象的年龄加1。如果ServivorTo区的内存空间不够,则也直接将其复制到老年代。如果对象属于大对象,则也直接复制到老年代。②清空Eden区和ServivorFrom区中的对象。③将ServivorFrom区和ServivorTo区互换,原来的ServivorTo区成为下一次GC时的ServivorFrom区。
老年代的垃圾回收机制
①老年代主要存放有长生命周期的对象和大对象,老年代的GC叫MajorGC。②在老年代,对象比较稳定,MajorGC不会频繁触发。在进行MajorGC前,JVM会进行一次MinorGC,过后仍然出现老年代空间不足或无法找到足够大的连续内存空间分配给新创建的大对象时,会触发MajorGC进行垃圾回收,释放JVM的内存空间。③MajorGC采用标记清除算法,该算法首先会扫描所有对象并标记存活的对象,然后回收未被标记的对象,并释放内存空间。因为要先扫描老年代的所有对象再回收,所以MajorGC的时间较长。容易产生内存碎片,在老年代没有内存空间可分配时,会出现内存溢出异常。
永久代
①永久代指内存的永久保存区域,主要存放Class和Meta(元数据)的信息。Class在类加载时被放入永久代。②永久代和老年代、新生代不同,GC不会在程序运行期间对永久代的内存进行清理,这也导致了永久代的内存会随着加载的Class文件的增加而增加,在加载的Class文件过多时会出现内存溢出异常,比如Tomcat引用jar文件过多导致JVM内存不足而无法启动。③在JDK1.8中,永久代已经被元数据区取代。元数据区的作用和永久代类似,二者最大的区别在于:元数据区并没有使用虚拟机的内存,而是直接使用操作系统的本地内存。因此元空间的大小不受JVM内存的限制,只和操作系统的内存有关。④在JDK1.8中,JVM将类的元数据放入本地内存中,将常量池和类的静态常量放入Java堆中,这样JVM能够加载多少元数据信息就不再由JVM的最大可用内存空间决定,而由操作系统的实际可用内存空间决定。
什么情况下会触发fullGC
①fullGC会清理整个堆空间,包括老年代和新生代,会造成很大的系统资源开销,应对尽量避免。②调用System.gc时会触发fullGC,尽量避免调用该方法。③老年代空间不足时会触发fullGC,由于老年代主要存放新生代转入的对象,大对象和大数组,因此应该尽量做到让对象在minorGC阶段被回收,不要创建过大的对象及数组。由于在minorGC时survivor区放不下的对象会进入老年代,老年代也放不下时会触发fullGC,因此可以根据实际情况增大survivor区、老年代空间或调低并发垃圾回收的比率。③永久代满了时会触发fullGC,可以增加永久代的空间,例如-XX:MaxPermSize=16m,也可以开启CMS回收永久代选项。不过JDK1.8已经移除了永久代,新增了一个叫元数据区的native内存区,所以大部分类的元数据都在本地内存中分配。
如何确定对象是否是垃圾
①Java采用引用计数法和可达性分析来确定对象是否应该被回收。引用计数法容易产生循环引用的问题,可达性分析通过根搜索算法实现。根搜索算法以一系列GC Roots的点作为起点向下搜索,在一个对象到任何GC Roots都没有引用链相连时,说明其已经死亡。根搜索算法主要针对栈中的引用、方法区的静态引用和JNI中的引用展开分析。②引用计数法:在Java中如果要操作对象,就必须先获取该对象的引用,因此可以通过引用计数法来判断一个对象是否可以被回收。在为对象添加一个引用时,引用计数加1;在为对象删除一个引用时,引用计数减1;如果一个对象的引用计数为0,则表示此刻该对象没有被引用,可以被回收。引用计数法容易产生循环引用问题,循环引用指两个对象相互引用,导致它们的引用一直存在,而不能被回收。③可达性分析:为了解决引用计数法的循环引用问题,Java还采用了可达性分析来判断对象是否可以被回收。具体做法是首先定义一些GC Roots对象,然后以这些GC Roots对象作为起点向下搜索,如果在GC Roots和一个对象之间没有可达路径,则称该对象是不可达的。不可达对象要经过至少两次标记才能判断其是否可被回收,如果两次标记后该对象仍然不可达,则将被垃圾回收器回收。
GC算法
①标记清除算法:标记出所有需要回收的对象,然后清除可回收的对象。效率较低,并且因为在清除后没有重新整理可用的内存空间,如果内存中可被回收的小对象居多,会引起内存碎片化问题。②复制算法:将可用内存分为区域1和区域2,将新生成的对象放在区域1,在区域1满后对区域1进行一次标记,将标记后仍然存活的对象复制到区域2,然后清除区域1。效率较高并且易于实现,解决了内存碎片化的问题,缺点是浪费了大量内存,同时在系统中存在长生命周期对象时会在两区域间来回复制影响系统效率。③标记清除算法:结合了标记清除算法和复制算法的优点,标记过程和标记清除算法一样,标记后将存活的对象移动到一端,清理另一端。④分代收集算法:根据对象不同类型把内存划分为不同区域,把堆划分为新生代和老年代。由于新生代的对象生命周期较短,主要采用复制算法。将新生代划分为一块较大的Eden区和两块较小的Survivor区,Servivor区又分为ServivorTo和ServivorFrom区。JVM在运行过程中主要使用Eden和SurvivorFrom区,进行垃圾回收时将这个两个区域存活的对象复制到SurvivorTo区并清除这两个区域。老年代主要存储长生命周期的大对象,因此采用标记清除或标记整理算法。
垃圾回收器
①Serial:单线程,基于复制算法,JVM运行在Client时默认的新生代垃圾收集器。②ParNew:Serial的多线程实现,基于复制算法,JVM运行在Server时默认 的新生代垃圾收集器。③Paraller Scavenge:多线程,基于复制算法,以吞吐量最大化为目标,允许较长时间的STW换取吞吐量。④Serial Old:单线程,基于标记整理算法,是JVM运行在Client模式下默认的老年代垃圾回收器,可和Serial搭配使用。⑤Parall Old:多线程,基于标记整理算法,优先考虑系统的吞吐量。⑥CMS:多线程,基于标记清除算法,为老年代设计,追求最短停顿时间。主要有四个步骤:初始标记、并发标记、重新标记、并发清除。⑥G1:将堆内存分为几个大小固定的独立区域,在后台维护了一个优先列表,根据允许的收集时间回收垃圾收集价值最大的区域。相比CMS不会产生内存碎片,并且可精确控制停顿时间。分为四个阶段:初始标记、并发标记、最终标记、筛选回收。
Spring
IoC思想和DI实现方式(招行)
IOC和DI
IOC即控制反转,简单来说就是把对象的控制权委托给spring框架,作用是降低代码的耦合度。②DI即依赖注入,是IOC的一种具体实现方式。假设一个Car类需要Engine的对象,那么一般需要new一个Engine,利用IOC就是只需要定义一个私有的Engine引用变量,容器会在运行时创建一个Engine的实例对象并将引用自动注入给变量。
DI的实现方式
①可以注入的数据类型有基本数据类型、String、Bean以及集合等复杂数据类型。②有三种注入方式,第一种是通过构造器注入,通过constructor-arg标签实现,缺点是即使不需要该属性也必须注入;第二种是通过Set方法注入,通过property标签实现,优点是创建对象时没有明确限制,缺点是某个成员变量必须有值,在获取对象时set方法可能还没有执行;第三种是通过注解注入,利用@Autowired自动按类型注入,如果有多个匹配则按照指定bean的id查找,查找不到会报错;@Qualifier在自动按照类型注入的基础之上,再按照 Bean 的 id 注入,给成员变量注入时必须搭配@Autowired,给方法注入时可单独使用;@Resource直接按照 Bean 的 id 注入;@Value用于注入基本数据类型和String。
BeanFactory、FactoryBean、ApplicationContext的区别(阿里)
①BeanFactory是一个Factory顶层接口,是用来管理bean的IOC容器或对象工厂,较为古老,不支持spring的一些插件。BeanFactory使用了延迟加载,适合多例模式。②FactoryBean是一个Bean接口,是一个可以生产或者装饰对象的工厂Bean,可以通过实现该接口自定义的实例化Bean的逻辑。③ApplicationConext是BeanFactory的子接口,扩展了其功能,ApplicationContext是立即加载,适合单例模式。一般推荐使用ApplicationContext。
Spring中bean对象的生命周期
①Spring对bean进行实例化。②Spring将值和bean的引用注入到其对应的属性中。③调用BeanNameAware的setBeanName方法。④调用BeanFactoryAware的setBeanFactory方法。⑤调用AppicationContxtAware的setApplicationContext方法。⑥调用BeanPostProcessor的post-ProcessBeforeInitialization方法。⑦调用InitializingBean的after-PropertiesSet方法。如果bean使用init-method声明了自定义初始化方法,该方法也会被调用。⑧调用BeanPostProcessor的post-ProcessAfterInitialization方法。⑨使用bean。⑩调用DisposableBean的destroy方法,如果bean使用destroy-method声明了自定义销毁方法,该方法也会被调用。
bean的作用范围
通过scope指定bean的作用范围,有①singleton:单例的,每次容器返回的对象是同一个。②prototype :多例的,每次返回的对象是新创建的实例。③request:仅作用于HttpRequest,每次Http请求都会创建一个新的bean。④session:仅作用于HttpSession,不同的Session使用不同的实例,相同的Session使用同一个实例。⑤global session :仅作用于HttpSession,所有的Session使用同一个实例。
使用XML配置创建Bean对象的方式
①通过默认无参构造器。使用bean标签,只使用id和class属性,如果没有无参构造器会报错。②使用静态工厂,通过bean标签中的class指明静态工厂,factory-method指明静态工厂方法。③使用实例工厂,通过bean标签中的factory-bean指明实例工厂,factory-method指明实例工厂方法。
Aop的基本思想,原理,相关的注解(阿里、招行)
思想和原理
①Aop即面向切面编程,简单地说就是将代码中重复的部分抽取出来,在需要执行的时候使用动态代理的技术,在不修改源码的基础上对方法进行增强。优点是可以减少代码的冗余,提高开发效率,维护方便。Spring会根据类是否实现了接口来判断动态代理的方式,如果实现了接口会使用JDK的动态代理,核心是InvocationHandler接口和Proxy类,如果没有实现接口会使用cglib的动态代理,cglib是在运行时动态生成某个类的子类,如果某一个类被标记为final,是不能使用cglib动态代理的。
②JDK的动态代理主要通过重组字节码实现,首先获得被代理对象的引用和所有接口,生成新的类必须实现被代理类的所有接口,动态生成Java代码后编译新生成的.class文件并重新加载到JVM运行。JDK代理直接写Class字节码,CGLib是采用ASM框架写字节码,生成代理类的效率低。但是CGLib调用方法的效率高,因为JDK使用反射来调用方法,CGLib使用FastClass机制为代理类和被代理类各生成一个类,这个类会为代理类或被代理类的方法生成一个index,这个index可以作为参数直接定位要调用的方法。
相关注解
①@Before前置通知,@AfterThrowing异常通知,@AfterReturning后置通知,@After最终通知,@Around环绕通知。②最终通知会在后置通知之前执行,为解决此问题一般使用环绕通知。
专业术语
①Joinpoint(连接点):指那些被拦截到的点,在 spring 中这些点指的是方法,因为 spring 只支持方法类型的连接点。例如业务层实现类中的方法都是连接点。②Pointcut(切入点):指我们要对哪些 Joinpoint 进行拦截的定义。例如业务层实现类中被增强的方法都是切入点,切入点一定是连接点,但连接点不一定是切入点。③Advice(通知/增强):指拦截到 Joinpoint 之后所要做的事情。④Introduction(引介):引介是一种特殊的通知,在不修改类代码的前提下可以在运行期为类动态地添加一些方法或 Field。⑤Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。⑥Proxy(代理):一个类被 AOP 织入增强后,就产生一个结果代理类。⑦Target(目标):代理的目标对象。⑧Aspect(切面):是切入点和通知(引介)的结合。
单例模式(阿里)
①单例模式是保证系统实例唯一性的重要手段。单例模式首先通过将类的实例化方法私有化来防止程序通过其他方式创建该类的实例,然后通过提供一个全局唯一获取该类实例的方法帮助用户获取类的实例,用户只需也只能通过调用该方法获取类的实例。②单例模式的设计保证了一个类在整个系统中同一时刻只有一个实例存在,主要被用于一个全局类的对象在多个地方被使用并且对象的状态是全局变化的场景下。同时单例模式为系统资源的优化提供了很好的思路,频繁创建或销毁对象都会增加系统的资源消耗,而单例模式保障了整个系统只有一个对象能被使用,很好地节约了资源。
饿汉式:线程安全,没有加锁执行效率高,但类加载时就创建了实例,可能浪费内存资源。
懒汉式:线程不安全,在使用时才创建实例,可以通过双重锁机制保证线程安全,或使用静态内部类优化效率。
反射和序列化会破坏单例模式,在序列化添加readResolve可以解决该问题,但实际上还是实例化了两次只是新创建的对象没有被返回,如果创建对象的频率很快,内存的开销也会更大。
还可以使用注册式单例模式,可以使用枚举式单例模式,反编译后可以发现在静态代码块中实例化,属于饿汉式,同时不会被反射破坏,当修饰符是枚举类型时会抛出异常。也可以使用容器式单例模式,使用HashMap,适用于实例非常多的情况,方便管理但是线程不安全,可以使用ConcurrentHashMap。
MySQL
Select语句的执行顺序(阿里)
查询涉及的关键字 ----------执行顺序
select 查询列表 -------------------⑦
from 表 ------------------------------①
连接类型 join 表2----------------②
on 连接条件-------------------------③
where 筛选条件--------------------④
group by 分组列表----------------⑤
having 分组后筛选----------------⑥
order by 排序列表-----------------⑧
limit 偏移,条目数 --------------⑨
MySQL的存储引擎(阿里)
MyISAM
MyISAM存储引擎是MySQL5.5之前默认的存储引擎,管理非事务表,提供高速存储和检索,以及全文搜索能力。该引擎插入数据快,空间和内存使用比较低。
①存储组成:在磁盘上存储成三个文件,文件名就是表名,不缓存数据文件,只缓存索引文件。表定义的扩展名为.frm,数据文件的扩展名为.MYD,索引文件的扩展名为.MYI,数据文件可以和索引文件放在不同目录,平均分别IO,获得更快的速度,而且索引是压缩的,能加载更多索引,提高内存使用率。
②特点:不支持事务,但支持全文索引,极大优化LIKE查询效率。锁是表级别,锁成本小但是并发性能差,不支持行级锁,只支持并发插入的表锁,主要用于高负载查询。读写相互阻塞,但读读不阻塞。不缓存数据,只缓存索引,可以通过key_buffer缓存大大提高访问性能,减少磁盘IO。读取速度快,并发量小,不适合大量update。
③适用场景:不需要事务支持的业务,一般为读比较多的网站应用。并发相对较低的业务,修改数据相对较少的业务,对数据一致性、完整性要求不高的业务。
④调优:尽量使用索引,优先使用MySQL缓存机制。调整读写优先级,启用延迟插入改善大批量写入性能。尽量顺序操作让INSERT数据都写入到尾部减少阻塞。分解大的操作降低单个操作的阻塞时间。降低并发数,高并发场景可以使用排队机制。对于相对静态数据,充分利用QueryCache提高访问效率。主从同步的主库使用InnoDB,从库使用MyISAM。
InnoDB
InnoDB用于事务处理应用程序,把数据放在一个逻辑表空间内,通过多版本并发控制来获得高并发性,实现了四种隔离级别,默认为可重复读,使用next-key locking避免幻读。如果对事物的完整性要求比较高,或者需要频繁地更新删除数据可以选择InnoDB,因为InnoDB提供了事物的提交、回滚、崩溃恢复。
①存储组成:只有ibd文件,分为数据区和索引区,有较好的读写并发能力。物理文件有日志文件、数据文件和索引文件。
②特点:支持事务,支持4个隔离级别,支持多版本读取。行级锁,通过索引实现,更新时锁定当前行,全表扫描仍然是表锁。支持崩溃恢复能力和MVCC。具有非常高地缓存特性,可以缓存索引也能缓存数据。支持分区、表空间,支持外键约束,自增长列。
③优点:支持事务,并发量大,适合大量update。缺点:对比MyISAM写效率较差,并且占用更多的磁盘空间以保留数据和索引,更消耗资源,速度较慢。
④适用场景:需要事务锁定,适合高并发场景,数据更新较为频繁的场景,数据一致性要求较高的场景。硬件设备内存较大,可以利用InnoDB的缓存能力提高内存利用率,减少磁盘IO。
⑤调优:主键尽可能小,避免给SECONDARY INDEX带来过大的空间负担。避免全表扫描,因为会使用表锁。尽可能缓存所有索引和数据,减少磁盘IO。执行大量插入操作时,尽量手动控制事务。避免主键更新,这会带来大量数据移动。
Memory
如果需要很快的读写速度,对数据安全性的要求比较低,可以选择Memory。对表大小有要求,只适用于较小的数据库表。如果mysqld进程异常,那么数据库就会重启或崩溃,丢失数据,表的生命周期很短,一般只使用一次比较适合存储临时数据。
①存储组成:所有数据保存在内存RAM中,可以提供极快的访问速度。每个表对应一个磁盘文件,文件名和表名相同,类型为.frm,只存储表的结构,数据保存在内存,有利于快速处理。
②特点:默认使用hash索引,速度比BTree要快,但安全性不高。
③适用场景:如果需要很快的读写速度,并且对数据的安全性要求较低,可以使用Memory。
Mysql的隔离级别,对应的问题,可重复读如何实现
隔离级别(阿里,招行)
①未提交读,一个事务会读取到另一个事务没有提交的数据,存在脏读、不可重复读、幻读的问题。②已提交读,一个事务可以读取到另一个事务已经提交的数据,解决了幻读的问题,存在不可重复读、幻读的问题。③可重复读,MySQL默认的隔离级别,在一次事务中读取同一个数据结果是一样的,解决了不可重复读的问题,存在幻读问题。④可串行化,每次读都需要获得表级共享锁,读写互相阻塞,效率低,解决了幻读问题。
可重复读如何实现(阿里)
①使用MVCC多版本并发控制方式,类似于乐观锁的一种实现方式。②InnoDB在每行记录后面保存两个隐藏的列,分别保存了这个行的创建时间和行的删除时间。这里存储的并不是实际的时间值,而是系统版本号,当数据被修改时,版本号加1。③在读取事务开始时,系统会给当前读事务一个版本号,事务会读取版本号<=当前版本号的数据。此时如果其他写事务修改了这条数据,那么这条数据的版本号就会加1,从而比当前读事务的版本号高,读事务自然而然的就读不到更新后的数据了。
读取数据库时可能出现哪些问题(招行)
①脏读,一个事务中会读取到另一个事务中还没有提交的数据,如果另一事务最终回滚了数据,那么所读取到的数据就是无效的。②不可重复读,一个事务中可以读取到另一个事务中已经提交的数据,在同一次事务中对同一数据读取的结果可能不同。③幻读,一个事务在读取数据时,当另一个事务在表中插入了一些新数据时再次读取表时会多出几行,如同出现了幻觉。
SQL语句的优化,SQL执行性能要关注哪些字段(阿里)
如何优化SQL语句
①在MySQL5.5及以下避免使用子查询,因为内部执行计划时先查询外表再查内表,如果外表数据很大,查询就会很慢。MySQL5.6使用了JOIN关联方式对其进行了优化。②避免使用函数索引,MySQL不像Oracle一样支持函数索引,会进行全表扫描。③用IN来代替OR。④在LIKE中双%无法使用索引,避免使用。⑤禁止不必要的排序,分组统计时可以使用order by null禁止排序。⑥尽量使用批量INSERT插入。
如何提高INSERT的性能
①合并多条INSERT语句为一句,写日志的数量减少,降低了日志刷盘的数据量和频率,减少SQL语句解析次数。②修改buffer_size,调大批量插入的缓存。③设置innodb_flush_log_at_trx_commit=0,可以明显提升导入速度。设置为0时,log_buffer的数据将以每秒一次的频率写入log文件,同时会进行文件系统到磁盘的同步操作。设置为1时,在每次事务提交的时候才会写入log文件,同时触发同步。设置为2时,事务提交时会写入但是不会触发同步,此外每秒会有一次文件系统到磁盘的同步。④手动提交事务,减少事务的消耗,一般执行1000条提交一次。
如何分析SQL语句的性能,要关注哪些字段
①通过EXPLAIN查看SQL语句的查询执行计划,如果作用在表上,效果等同DESC。MySQL5.6.3之前只能对SELECT生成执行计划,5.6.3之后可以对INSERT、DELETE、UPDATE等都可以生成执行计划。通过type列可以得知是否使用了全表扫描以及索引的使用形式,通过key可以得知实际上使用了哪个索引,通过key_len可以得知索引是否使用完成,通过rows可以得知扫描的行数是否过多,通过extra可以得知是否使用了临时表或者额外的排序操作。
②使用show profile分析SQL语句性能消耗,例如可以查询到SQL会执行多少时间,并显示CPU、内存使用量、执行过程中系统锁及表锁的花费时间等信息。它可以帮助我们查询时间都耗费到了哪里,从MySQL5.6开始可以通过trace文件进一步获取优化器是如何选择执行计划的。
EXPLAIN字段含义
id:表示查询中执行的顺序,从大到小执行,id一样时从上往下执行。
select_type:表锁查询中每个select字句的类型,例如simple表示不包含子查询、表连接等其他复杂查询,primary表示查询中包含子查询,subquery表示select或where中包含子查询。
type:表示MySQL在表中找到行的方式,性能从差到好分为:all全表扫描、index索引全扫描、range索引范围扫描、ref返回匹配某个单独值得所有行、eq_ref唯一性索引扫描、const常量、system表中只有一行或者是空表、null执行时不用访问表或索引就能得到结果。
possible_keys:查询时可能用到的索引
key:查询中实际使用到的索引,如果没有使用索引则显示null。
key_len:使用到索引字段的长度,该值对于确认索引的有效性以及多列索引中用到的列数目很重要。
ref:表示表的连接匹配条件,即那些列或常量被用于查找索引列上的值
rows:表示MySQL根据表统计信息及索引情况估计出找到所需记录需要读取的行数
Extra:包含一些重要信息,using where表示MySQL在存储引擎收到记录后进行后过滤,using index强调只需要使用索引就可以满足查询表的要求,不需要直接访问表数据,说明正在使用覆盖索引,impossible where表示where语句会导致没有符合条件的行。
Redis
Redis的数据结构,sorted-set的使用场景(招行)
string类型的基本操作和注意事项
①存储的数据:单个数据,最简单常用的数据存储类型。存储数据的格式:一个存储空间保存一个数据。存储内容:通常使用字符串,如果字符串以整数的形式展示,可以作为数字操作使用。②添加/修改数据:set key value、获取数据:get key、删除数据:del key、添加/修改多个数据:mset key1 value1 key2 value2 …、获取多个数据:mget key1 key2 …、获取数据字符个数(字符串长度):strlen key、追加信息到原始信息后部(如果原始信息存在就追加,否则新建):append key value。③string在redis内部存储默认就是一个字符串,当遇到增减类操作incr,decr时会转成数值型进行计算。redis所有的操作都是原子性的,采用单线程处理所有业务,命令是一个一个执行的,因此无需考虑并发 带来的数据影响。注意:按数值进行操作的数据,如果原始数据不能转成数值,或超越了redis 数值上限范围(java中long型数据最大值,Long.MAX_VALUE)将报错。
hash类型的基本操作和注意事项
①存储需求:对一系列存储的数据进行编组,方便管理,一般存储对象信息。存储结构:一个存储空间保存多个键值对数据。底层使用哈希表结构实现数据存储。②如果field数量较少,存储结构优化为类数组结构;如果field数量较多,存储结构使用HashMap结构。③添加/修改数据:hset key field value、获取数据:hget key field,hgetall key、 删除数据:hdel key field1 [field2]、添加/修改多个数据:hmset key field1 value1 field2 value2 …、 获取多个数据:hmget key field1 field2 …、获取哈希表中字段的数量:hlen key、获取哈希表中是否存在指定的字段:hexists key field。③hash类型下的value只能存储字符串,不允许存储其他数据类型,不存在嵌套现象。如果数据未获取到, 对应的值为(nil)。每个 hash 可以存储 2^32 - 1 个键值对。hash类型十分贴近对象的数据存储形式,并且可以灵活添加删除对象属性。但hash设计初衷不是为了存储大量对象而设计的,不可滥用,更不可以将hash作为对象列表使用。hgetall 操作可以获取全部属性,如果内部field过多,遍历整体数据效率就很会低,有可能成为数据访问瓶颈。
list类型的基本操作和注意事项
①存储需求:存储多个数据,并对数据进入存储空间的顺序进行区分。存储结构:一个存储空间保存多个数据,且通过数据可以体现进入顺序。保存多个数据,底层使用双向链表存储结构实现。②添加/修改数据:lpush key value1 [value2] …,rpush key value1 [value2] …、获取数据:lrange key start stop,lindex key index,llen key、获取并移除数据:lpop key,rpop key。获取数据时可以设置等待时间,list为空时等待获取。移除指定数据:lrem key count value。③list中保存的数据都是string类型的,数据总容量是有限的,最多2^32- 1 个元素(4294967295)。list具有索引的概念,但是操作数据时通常以队列的形式进行入队出队操作,或以栈的形式进行入栈出栈操作。获取全部数据操作结束索引设置为-1。list可以对数据进行分页操作,通常第一页的信息来自于list,第2页及更多的信息通过数据库的形式加载。
set类型的基本操作和注意事项
①存储需求:存储大量的数据,在查询方面提供更高的效率。存储结构:能够保存大量的数据,高效的内部存储机制,便于查询。与hash存储结构完全相同,仅存储键,不存储值(nil),并且值是不允许重复的。②添加数据:sadd key member1 [member2]、获取全部数据:smembers key、删除数据:srem key member1 [member2]、获取集合数据总量:scard key、判断集合中是否包含指定数据:sismember key member。③set 类型不允许数据重复,如果添加的数据在 set 中已经存在,将只保留一份。set 虽然与hash的存储结构相同,但是无法启用hash中存储值的空间。
sorted-set类型的相关操作和注意事项
①存储需求:数据排序有利于数据的有效展示,需要提供一种可以根据自身特征进行排序的方式。存储结构:新的存储模型,可以保存可排序的数据,在set的存储结构基础上添加可排序字段。②添加数据:zadd key score1 member1 [score2 member2]、获取全部数据:zrange key start stop [WITHSCORES],zrevrange key start stop [WITHSCORES]、删除数据:zrem key member [member …]。③score保存的数据存储空间是64位,超过该范围的话score保存的数据也可以是一个双精度的double值,但可能会丢失精度,使用时候要慎重。sorted_set 底层存储还是基于set结构的,因此数据不能重复,如果重复添加相同的数据,score值将被反复覆盖,保留最后一次修改的结果。
Zookeeper
Zookeeper的应用场景(阿里)
维护配置信息
Java编程经常会遇到配置项,例如数据库的user、password等,通常配置信息会放在配置文件中,再把配置文件放在服务器上。当需要修改配置信息时,要去服务器上修改对应的配置文件,但在分布式系统中很多服务器都需要使用该配置文件,因此必须保证该配置服务的高可用性和各台服务器上配置的一致性。通常会将配置文件部署在一个集群上,但一个集群涉及的服务器数量是很庞大的,如果一台台服务器逐个修改配置文件是效率很低且危险的,因此需要一种服务可以高效快速且可靠地完成配置项的更改工作。
zookeeper就可以提供这种服务,使用Zab一致性协议保证一致性。hbase中客户端就是连接zookeeper获得必要的hbase集群的配置信息才可以进一步操作。在开源消息队列Kafka中,也使用zookeeper来维护broker的信息。在dubbo中也广泛使用zookeeper管理一些配置来实现服务治理。
分布式锁服务
一个集群是一个分布式系统,由多台服务器组成。为了提高并发度和可靠性,在多台服务器运行着同一种服务。当多个服务在运行时就需要协调各服务的进度,有时候需要保证当某个服务在进行某个操作时,其他的服务都不能进行该操作,即对该操作进行加锁,如果当前机器故障,释放锁并fall over到其他机器继续执行。
集群管理
zookeeper会将服务器加入/移除的情况通知给集群中其他正常工作的服务器,以及即使调整存储和计算等任务的分配和执行等,此外zookeeper还会对故障的服务器做出诊断并尝试修复。
生成分布式唯一ID
在过去的单库单表系统中,通常使用数据库字段自带的auto_increment熟悉自动为每条记录生成一个唯一的id。但分库分表后就无法依靠该属性来标识一个唯一的记录。此时可以使用zookeeper在分布式环境下生成全局唯一性id。每次要生成一个新id时,创建一个持久顺序结点,创建操作返回的结点序号,即为新id,然后把比自己结点小的删除。