面经题目汇总

文章目录


分为三大类:Java、中间件、算法

Java

1. 用过哪些list?它们的区别?使用的场景?

  1. LinkedList和ArrayList的差别主要来自于Array和LinkedList数据结构的不同。ArrayList是基于数组实现的,LinkedList是基于双链表实现的。另外LinkedList类不仅是List接口的实现类,可以根据索引来随机访问集合中的元素,除此之外,LinkedList还实现了Deque接口,Deque接口是Queue接口的子接口,它代表一个双向队列,因此LinkedList可以作为双向对列,栈(可以参见Deque提供的接口方法)和List集合使用,功能强大。

  2. 因为Array是基于索引(index)的数据结构,它使用索引在数组中搜索和读取数据是很快的,可以直接返回数组中index位置的元素,因此在随机访问集合元素上有较好的性能。Array获取数据的时间复杂度是O(1),但是要插入、删除数据却是开销很大的,因为这需要移动数组中插入位置之后的的所有元素。

  3. 相对于ArrayList,LinkedList的随机访问集合元素时性能较差,因为需要在双向列表中找到要index的位置,再返回;但在插入,删除操作是更快的。因为LinkedList不像ArrayList一样,不需要改变数组的大小,也不需要在数组装满的时候要将所有的数据重新装入一个新的数组,这是ArrayList最坏的一种情况,时间复杂度是O(n),而LinkedList中插入或删除的时间复杂度仅为O(1)。ArrayList在插入数据时还需要更新索引(除了插入数组的尾部)。

  4. LinkedList需要更多的内存,因为ArrayList的每个索引的位置是实际的数据,而LinkedList中的每个节点中存储的是实际的数据和前后节点的位置。

使用场景:

(1)如果应用程序对数据有较多的随机访问,ArrayList对象要优于LinkedList对象;

( 2 ) 如果应用程序有更多的插入或者删除操作,较少的数据读取,LinkedList对象要优于ArrayList对象;

(3)不过ArrayList的插入,删除操作也不一定比LinkedList慢,如果在List靠近末尾的地方插入,那么ArrayList只需要移动较少的数据,而LinkedList则需要一直查找到列表尾部,反而耗费较多时间,这时ArrayList就比LinkedList要快。

2. 说一下 HashMap 的结构,为什么非线程安全,为什么容量是 2 的次幂

  • 数组+链表(1.8引入红黑树)
  • HashMap底层是一个Entry数组,当发生hash冲突的时候,hashmap是采用链表的方式来解决的,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。
    在hashmap做put操作的时候,现在假如A线程和B线程同时对同一个数组位置调用addEntry,两个线程会同时得到现在的头结点,然后A写入新的头结点之后,B也写入新的头结点,那B的写入操作就会覆盖A的写入操作造成A的写入操作丢失
    当多个线程同时操作同一个数组位置的时候,也都会先取得现在状态下该位置存储的头结点,然后各自去进行计算操作,之后再把结果写会到该数组位置去,其实写回的时候可能其他的线程已经就把这个位置给修改过了,就会覆盖其他线程的修改
  • hashmap求hashcode是通过对数组长度取余得到的,当容量一定是2^n时,h & (length - 1) == h % length,它俩是等价不等效的,位运算效率非常高

HashMap并发环境下会有什么问题

死循环和更新(覆盖)丢失

3. 说一下 Volatile 关键字,聊到底层原理

这里主要说底层原理:

将Java代码

private static volatile Singleton instance = new Singleton();

转变为汇编代码,如下。

0x01a3de1d: movb $0*0,0*1104800(%esi);0x01a3de24: lock addl $0*0,(%esp);

可以发现转变为的汇编代码是有lock前缀的,Lock前缀的指令在多核处理器下会引发两件事情。

  1. 将当前处理器缓存行的数据写回到系统内存。
  2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会出现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中。

volatile的两条实现原则:

  • Lock前缀指令会引起处理器缓存回写到内存。
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

4. java内存泄漏场景(什么情况会导致内存泄漏)

给出一个Java内存泄漏的典型例子,

ArrayList list = new ArrayList();

for (int i = 1; i < 100; i++) {

    Object o = new Object();

    v.add(o);

    o = null;

}

内存泄漏的根本原因
内存泄漏的根本原因在于生命周期长的对象持有了生命周期短的对象的引用

1、静态集合类,如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。

2、各种连接,如数据库连接、网络连接和IO连接等。在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。

3、变量不合理的作用域。一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。

4、内部类持有外部类,如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。

5、改变哈希值,当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露

5. GC root有哪些

JVM垃圾回收的根对象的范围有以下几种:

(1)虚拟机(JVM)栈中引用对象

(2)方法区中的类静态属性引用对象

(3)方法区中常量引用的对象(final 的常量值)

(4)本地方法栈JNI的引用对象

6. 一个老年代也放不下的大对象会存在哪里

如果老年代也放不下,会发生full gc。。。

7. 新生代和老年代的分区是怎么分的,是逻辑分区还是物理分区?

在这里插入图片描述
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )

没有找到确切的答案,不过从下面看应该是逻辑分区:

根据Java虚拟机规范的规定,Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的。

8. java泛型有哪些

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。
通常我们常用的就只有泛型类,一个最普通的泛型类:

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{ 
    //key这个成员变量的类型为T,T的类型由外部指定  
    private T key;

    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}

又或者写链表时,每个node我们不知道他的值是int还是string还是什么,就可以定义为node,然后在使用/调用时传入具体的类型(类型实参)。

什么是泛型?为什么要使用泛型?

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

9. 在JDK8中移除永久代,并把方法区移至元空间,这么设计的原因是什么

随着JDK8的到来,JVM不再有PermGen。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中。

  • 永久代的大小是在启动时固定好的——很难验证并进行调优。-XX:MaxPermSize(默认64M)
  • 简化垃圾回收:对每一个回收集使用专门的元数据迭代器。
  • 可以在GC不进行暂停的情况下并发地释放类数据。
  • 元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
      -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
      -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

元空间和永久代最大的区别是元空间并不在虚拟机中,而是使用本地内存。
这样可以提升对元数据的处理,提升gc效率。

答案链接

10. 元空间需要进行GC么?需要的话,元空间的GC是young gc还是full gc

如果Metaspace的空间占用达到了设定的最大值(可以通过-XX:MetaspaceSize设定),那么就会触发GC来收集死亡对象和类的加载器。

应该都不是,因为元空间既不是新生代也不是老年代。。。(不确定)

11. 说一下JUC包

java.util.concurrent,JUC是JDK5才引入的并发类库。它的基础就是AbstractQueuedSynchronizer抽象类,Lock,CountDownLatch等的基础就是该类,而该类又用到了CAS操作。常用类如下:

  • JUC的atomic包下运用了CAS的AtomicBoolean、AtomicInteger、AtomicReference等原子变量类

  • JUC的locks包下的AbstractQueuedSynchronizer(AQS)以及使用AQS的ReentantLock(显式锁)、ReentrantReadWriteLock

    附:运用了AQS的类还有:Semaphore、CountDownLatch、ReentantLock(显式锁)、ReentrantReadWriteLock

  • JUC下的一些同步工具类:CountDownLatch(闭锁)、Semaphore(信号量)、CyclicBarrier(栅栏)、FutureTask

  • JUC下的一些并发容器类:ConcurrentHashMap、CopyOnWriteArrayList

  • JUC下的一些Executor框架的相关类: 线程池的工厂类->Executors 线程池的实现类->ThreadPoolExecutor/ForkJoinPool

  • JUC下的一些阻塞队列实现类:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue

附:ForkJoinPool:使用work-stealing的工作方式运行

12. java如何实现一个BlockingQueue

阻塞队列是对普通队列的一种扩展,在普通队列功能上增加了一些额外功能。

普通队列其实主要就是入队、出队操作。阻塞队列接口(BlockingQueue)继承自普通队列。

其实主要在Queue基础上增加了阻塞的入队(put())和出队(take())操作,即当队列已满时调用put()入队时,当前线程会阻塞,直到队列有空间时才会继续入队;当队列为空时,调用take()出队操作时,当前线程会阻塞,直到队列中有元素时才会继续出队。这就是阻塞队列的核心。

Java阻塞队列-BlockingQueue介绍及实现原理
【图解JDK源码】BlockingQueue的基本原理

13. 利用反射调用类的私有方法

可以通过反射获取对象的类的getDeclaredFieldgetDeclaredMethod方法访问类的私有属性、方法,还可以重新设置私有属性的值,调用私有方法。如下面的field1 = e.getClass().getDeclaredField("field1");Method method1 = e.getClass().getDeclaredMethod("fun1");

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

//Exam.java
class Exam{
	private String field1="私有属性";
	public String field2="公有属性";
	public void fun1(){
		System.out.println("fun1:这是一个public访问权限方法");
	}
	
	private void fun2(){
		System.out.println("fun2:这是一个private访问权限方法");
	}
	
	private void fun3(String arg){
		System.out.println("fun3:这是一个private访问权限且带参数的方法,参数为:"+arg);
	}
	
} 

public class Test02 {
	public static void main(String args[]){
		Exam e=new Exam();
		try {
			field1 = e.getClass().getDeclaredField("field1");
			field2 = e.getClass().getDeclaredField("field2");
			field1.setAccessible(true);
			System.out.println("field1: "+field1.get(e));
			field1.set(e,"重新设置一个field1值");
			System.out.println("field1: "+field1.get(e));
			System.out.println("field2: "+field2.get(e));
			field2.set(e,"重新设置一个field2值");
			System.out.println("field2: "+field2.get(e));
		} catch (NoSuchFieldException e1) {
			e1.printStackTrace();
		}catch (IllegalArgumentException e1) {
			e1.printStackTrace();
		} catch (IllegalAccessException e1) {
			e1.printStackTrace();
		}
		
		try {
			
			Method method1 = e.getClass().getDeclaredMethod("fun1");
			method1.invoke(e);
			
			Method method2 = e.getClass().getDeclaredMethod("fun2");
			method2.setAccessible(true);
			method2.invoke(e);
			
			Method method3 = e.getClass().getDeclaredMethod("fun3",String.class);
			method3.setAccessible(true);
			method3.invoke(e,"fun3的参数");
		} catch (NoSuchMethodException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} catch (SecurityException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}catch (IllegalAccessException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} catch (IllegalArgumentException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		} catch (InvocationTargetException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
	}
}

14. 如何找到 java 程序 CPU 使用率100%的原因

先用 top 命令,找到 java 进程的 pid:
如 pid 为 1000;
再用 top -H -p 1000 命令查看在这个进程中,消耗 cpu 最多 的线程,如 1003;
最后使用 jstack 1000 > dump_file 把这个进程的堆栈信息 dump 到文件中,
打开 dump_file,找到 id 为1003的线程(要转化为16进制),就能发现是哪个方法占用了 cpu,分析自己的代码,
改正 bug!

答案链接

15. protected修饰的类能被继承吗?

可以。

访问修饰符定义了类、属性和方法的访问权限,Java 中包含四种,访问权限从小到大为 private、default、protected 和 public。

  • public,公共修饰符,被其修饰的类、属性或方法在项目中任意类中访问。
  • protected,保护修饰符,被其修饰的类、属性或方法在当前类所属包或当前类的子类中可访问。
  • default,默认修饰符,没有明确声明修饰符时默认采用此修饰符,被其修饰的类、属性或方法只能被当前类所属包中的类访问。
  • private,私有修饰符,被其修饰的类、属性或方法仅在当前类中可访问。

16. 讲一下AQS

简单总结:

  • AQS(抽象队列同步器,或者简称同步器)其实就是一个可以给我们实现锁的框架

  • 内部实现的关键是:先进先出的队列(CLH队列,双向队列)、state状态(volatile实现可见性,修改时CAS操作保证原子性)

  • 定义了内部类ConditionObject

  • 拥有两种线程模式

    • 独占模式
      -共享模式
  • 在LOCK包中的相关锁(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建

  • 一般我们叫AQS为同步器

  • 获取独占锁的过程就是在acquire定义的,该方法用到了模板设计模式,由子类实现的~

    过程:acquire(int)尝试获取资源,如果获取失败,将线程插入等待队列。插入等待队列后,acquire(int)并没有放弃获取资源,而是根据前置节点状态状态判断是否应该继续获取资源,如果前置节点是头结点,继续尝试获取资源,如果前置节点是SIGNAL状态,就中断当前线程,否则继续尝试获取资源。直到当前线程被park()或者获取到资源,acquire(int)结束。

  • 释放独占锁的过程就是在acquire定义的,该方法也用到了模板设计模式,由子类实现的~

    首先调用子类的tryRelease()方法释放锁,然后唤醒后继节点,在唤醒的过程中,需要判断后继节点是否满足情况,如果后继节点不为且不是作废状态,则唤醒这个后继节点,否则从tail节点向前寻找合适的节点,如果找到,则唤醒.

独占式同步状态获取流程:
在这里插入图片描述

AQS实现了一个同步器的基本结构,下面以独占锁与共享锁分开讨论,来说明AQS怎样实现获取、释放同步状态。
独占模式

  • 独占获取acquire: tryAcquire 本身不会阻塞线程,如果返回 true 成功就继续,如果返回 false 那么就阻塞线程并加入阻塞队列。
  • 独占且可中断模式获取acquireInterruptibly:支持中断取消
  • 独占且支持超时模式获取tryAcquireNanos: 带有超时时间,如果经过超时时间则会退出。
  • 独占模式释放release:释放成功会唤醒后续节点

共享模式

  • 共享模式获取acquireShared
  • 可中断模式共享获取acquireSharedInterruptibly
  • 共享模式带定时获取tryAcquireSharedNanos
  • 共享锁释放releaseShared

注意以上框架只定义了一个同步器的基本结构框架,的基本方法里依赖的 tryAcquire 、 tryRelease 、tryAcquireShared 、 tryReleaseShared 四个方法在 AQS 里没有实现,这四个方法不会涉及线程阻塞,而是由各自不同的使用场景根据情况来定制。

有别于wait和notiry。这里利用 jdk1.5 开始提供的 LockSupport.park() 和 LockSupport.unpark() 的本地方法实现,实现线程的阻塞和唤醒。

AQS虽然实现了acquire,和release方法,但是里面调用的tryAcquire和tryRelease是由子类来定制的。可以认为同步状态的维护、获取、释放动作是由子类实现的功能,而动作成功与否的后续行为时有AQS框架来实现

https://blog.csdn.net/varyall/article/details/80381626
https://blog.csdn.net/vernonzheng/article/details/8275624

17. AQS在各同步器内的Sync与State实现

也是上面的博客里讲的。

State机制
提供 volatile 变量 state; 用于同步线程之间的共享状态。通过 CAS 和 volatile 保证其原子性和可见性。

基于AQS构建的Synchronizer包括ReentrantLock,Semaphore,CountDownLatch, ReetrantRead WriteLock,FutureTask等,这些Synchronizer实际上最基本的东西就是原子状态的获取和释放,只是条件不一样而已。

1. ReentrantLock

需要记录当前线程获取原子状态的次数,如果次数为零,那么就说明这个线程放弃了锁(也有可能其他线程占据着锁从而需要等待),如果次数大于1,也就是获得了重进入的效果,而其他线程只能被park住,直到这个线程重进入锁次数变成0而释放原子状态。

2. Semaphore

则是要记录当前还有多少次许可可以使用,到0,就需要等待,也就实现并发量的控制,Semaphore一开始设置许可数为1,实际上就是一把互斥锁。

3. CountDownLatch

闭锁则要保持其状态,在这个状态到达终止态之前,所有线程都会被park住,闭锁可以设定初始值,这个值的含义就是这个闭锁需要被countDown()几次,因为每次CountDown是sync.releaseShared(1),而一开始初始值为10的话,那么这个闭锁需要被countDown()十次,才能够将这个初始值减到0,从而释放原子状态,让等待的所有线程通过。

4. FutureTask

需要记录任务的执行状态,当调用其实例的get方法时,内部类Sync会去调用AQS的acquireSharedInterruptibly()方法,而这个方法会反向调用Sync实现的tryAcquireShared()方法,即让具体实现类决定是否让当前线程继续还是park,而FutureTask的tryAcquireShared方法所做的唯一事情就是检查状态,如果是RUNNING状态那么让当前线程park。而跑任务的线程会在任务结束时调用FutureTask 实例的set方法(与等待线程持相同的实例),设定执行结果,并且通过unpark唤醒正在等待的线程,返回结果。

18. java集合 fail-fast & fail-safe 机制

fail-fast ( 快速失败 )机制是集合世界中比较常见的错误检测机制,通常出现在遍历集合元素的过程中。它是一种对集合遍历操作时的错误检测机制,在遍历中途出现意外的修改时,通过 unchecked 异常暴力地反馈出来。这种机制经常出现在多线程环境下,当前线程会维护一个计数比较器, 即 expectedModCount, 记录已经修改的次数。在进入遍历前,会把实时修改次数 modCount 赋值给 expectedModCount,如果这两个数据不相等,则抛出异常。 java.util 下的所有集合类都是 fail-fast,而 concurrent 包中的集合类都是 fail-safe。

fail-fast ( 快速失败 )

  • 在使用迭代器遍历一个集合对象时,比如增强for,如果遍历过程中对集合对象的内容进行了修改(增删改),会抛出 ConcurrentModificationException 异常.
  • 查看ArrayList源代码,在next方法执行的时候,会执行checkForComodification()方法
@SuppressWarnings("unchecked")  
public E next() {  
    checkForComodification();  
    int i = cursor;  
    if (i >= size)  
        throw new NoSuchElementException();  
    Object[] elementData = ArrayList.this.elementData;  
    if (i >= elementData.length)  
        throw new ConcurrentModificationException();  
    cursor = i + 1;  
    return (E) elementData[lastRet = i];  
}  

//...............省略.............

final void checkForComodification() {  
    if (modCount != expectedModCount)  
        throw new ConcurrentModificationException();  
}  

原理:

  1. 迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个modCount变量,
  2. 集合中在被遍历期间如果内容发生变化,就会改变modCount的值,
  3. 每当迭代器使用 hashNext()/next()遍历下一个元素之前,都会检测modCount变量和expectedmodCount值是否相等,
  4. 如果相等就返回遍历,否则抛出异常,终止遍历.

举例

//会抛出ConcurrentModificationException异常
for(Person person : Persons){
    if(person.getId()==2)
        student.remove(person);
}

注意

这里异常的抛出条件时检测到modCount = expectedmodCount 这个条件.

如果集合发生变化时修改modCount值, 刚好有设置为了expectedmodCount值, 则异常不会抛出.(比如删除了数据,再添加一条数据)

所以不能依赖于这个异常是否抛出而进行并发操作的编程, 这个异常只建议检测并发修改的bug.

使用场景 :

java.util包下的集合类都是快速失败机制的, 不能在多线程下发生并发修改(迭代过程中被修改).

fail-safe ( 安全失败 )

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先copy原有集合内容,在拷贝的集合上进行遍历.

原理:

  • 由于迭代时是对原集合的拷贝的值进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException

缺点:

  • 基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地, 迭代器并不能访问到修改后的内容 (简单来说就是, 迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的)

使用场景:

java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改.

19. 有4个线程分别获取C、D、E、F盘的大小,第5个线程统计总大小。请实现相关代码,体现主要逻辑。(要能写出来)

磁盘类已经给出。

public class DiskMemory {
    private int totalSize ;
    public int getSize(){
        return (new Random().nextInt(3)+1)*100;//加一是为了防止获取磁盘大小为0,不符合常理
    }

    public synchronized void setSize(int size){
        totalSize += size;
    }

    public int getTotalSize(){
        return totalSize; // 这里可以返回四个磁盘总大小。
    }
}

主要用到的方法是:CountDownLatch,
CountDownLatch类是一个同步倒数计数器,构造时传入int参数,该参数就是计数器的初始值,每调用一次countDown()方法,计数器减1,计数器大于0 时, await()方法会阻塞后面程序执行,直到计数器为0,后面被阻塞的方法才会得以实行。await(long timeout, TimeUnitunit),是等待一定时间,然后执行,不管计数器是否到0了。

	public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(4);
        ExecutorService service = Executors.newFixedThreadPool(6);
        DiskMemory diskMemory = new DiskMemory();
        for (int i = 0 ; i < 4 ; i ++) {
            service.execute(new Runnable() {
                @Override
                public void run() {
                    int timer = new Random().nextInt(3) + 1;
                    try {
                        Thread.sleep(timer * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    int diskSize = diskMemory.getSize();
                    System.out.printf("完成磁盘的统计任务,耗时%d秒。磁盘大小为%d。\n",timer,diskSize);
                    diskMemory.setSize(diskSize);
                    // 任务完成之后,计数器减一
                    countDownLatch.countDown();
                    System.out.println("count num = " + countDownLatch.getCount());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        // 主线程一直被阻塞,直到count的计数器被置为0
        countDownLatch.await();
        System.out.printf("全部磁盘都统计完成,所有磁盘总大小。\n" + ", totalSize = " + diskMemory.getTotalSize());
        service.shutdown();

    }

输出(因为并行,顺序不定)

完成磁盘的统计任务,耗时1秒。磁盘大小为200。
count num = 3
完成磁盘的统计任务,耗时3秒。磁盘大小为100。
完成磁盘的统计任务,耗时3秒。磁盘大小为300。
count num = 1
count num = 2
完成磁盘的统计任务,耗时3秒。磁盘大小为300。
count num = 0
全部磁盘都统计完成,所有磁盘总大小。
, totalSize = 900

原博链接:https://blog.csdn.net/zhujiangtaotaise/article/details/60570882

20. Integer、new Integer() 和 int

基本概念的区分:

  1. Integer 是 int 的包装类,int 则是 java 的一种基本数据类型
  2. Integer 变量必须实例化后才能使用,而int变量不需要
  3. Integer 实际是对象的引用,当new一个 Integer时,实际上是生成一个指针指向此对象;而 int 则是直接存储数据值
  4. Integer的默认值是null,int的默认值是0

Integer、new Integer() 和 int 的比较

  1. 两个 new Integer() 变量比较 ,永远是 false 因为new生成的是两个对象,其内存地址不同
    Integer i = new Integer(100);
    Integer j = new Integer(100);
    System.out.print(i == j);  //false
    
  2. Integer变量 和 new Integer() 变量比较 ,永远为 false。因为 Integer变量 指向的是 java 常量池 中的对象, 而 new Integer() 的变量指向 堆中 新建的对象,两者在内存中的地址不同。
    Integer i = new Integer(100);
    Integer j = 100;
    System.out.print(i == j);  //false
    
  3. 两个Integer 变量比较,如果两个变量的值在区间-128到127 之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为 false 。
    Integer i = 100;
    Integer j = 100;
    System.out.print(i == j); //true
    Integer i = 128;
    Integer j = 128;
    System.out.print(i == j); //false
    

分析:Integer i = 100 在编译时,会翻译成为 Integer i = Integer.valueOf(100),而 java 对 Integer类型的 valueOf 的定义如下:

在这里插入图片描述

java对于-128到127之间的数,会进行缓存。所以 Integer i = 127 时,会将127进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了。

  1. int 变量 与 Integer、 new Integer() 比较时,只要两个的值是相等,则为true
    因为包装类Integer 和 基本数据类型int 比较时,java会自动拆包装为int ,然后进行比较,实际上就变为两个int变量的比较。
    Integer i = new Integer(100);//自动拆箱为 int i=100; 此时,相当于两个int的比较
    int j = 100;
    System.out.println(i == j);//true
    

问题链接:https://mp.weixin.qq.com/s/7PGSaVoss077bGpUrCDg8A

21. CopyOnWriteArrayList原理,优缺点,使用场景(未答)

https://blog.csdn.net/u010002184/article/details/90452918

22. 线程池常见参数

先看构造方法定义:

public ThreadPoolExecutor(int corePoolSize,
                        int maximumPoolSize,
                        long keepAliveTime,
                        TimeUnit unit,
                        BlockingQueue<Runnable> workQueue,
                        ThreadFactory threadFactory,
                        RejectedExecutionHandler handler) 
  • 1.corePoolSize 指定了线程池里的线程数量
  • 2.maximumPoolSize 指定了线程池里的最大线程数量
  • 3.keepAliveTime 当线程池线程数量大于corePoolSize时候,多出来的空闲线程,多长时间会被销毁。
  • 4.unit 时间单位
  • 5.workQueue 任务队列,用于存放提交但是尚未被执行的任务。
  • 6.threadFactory 线程工厂,用于创建线程,一般可以用默认的
  • 7.handler 拒绝策略,当任务过多时候,如何拒绝任务。

23. 线程池的拒绝策略有哪些

  • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
  • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

24. spring 事务实现方式有哪些?

  • 编程式事务管理对基于 POJO 的应用来说是唯一选择。我们需要在代码中调用beginTransaction()、commit()、rollback()等事务管理相关的方法,这就是编程式事务管理。

  • 基于 TransactionProxyFactoryBean 的声明式事务管理

  • 基于 @Transactional 的声明式事务管理

  • 基于 Aspectj AOP 配置事务

25. Lock和TryLock的区别

1: lock拿不到锁会一直等待。tryLock是去尝试,拿不到就返回false,拿到返回true。

2: tryLock是可以被打断的,被中断 的,lock是不可以。

原博链接

26. lock的公平锁和非公平锁的怎么实现的

  • 非公平:
    1.调用lock()方法时,首先去通过CAS尝试设置锁资源的state变量,如果设置成功,则设置当前持有锁资源的线程为当前请求线程
    2.调用tryAcquire方法时,首先获取当前锁资源的state变量,如果为0,则通过CAS去尝试设置state,如果设置成功,则设置当前持有锁资源的线程为当前请求线程

    以上两步都属于插队现象,可以提高系统吞吐量

  • 公平:
    1.调用lock()方法时,不进行CAS尝试
    2.调用tryAcuqire方法时,首先获取当前锁资源的state变量,如果为0,则判断该节点是否是头节点可以去获取锁资源,如果可以才通过CAS去尝试设置state

    上面通过判断该线程是否是队列的头结点,从而保证公平性

在这里插入图片描述
在这里插入图片描述

27. 类加载机制(不是类加载几个过程)

加载过程主要完成三件事情:

  1. 通过类的全限定名来获取定义此类的二进制字节流
  2. 将这个类字节流代表的静态存储结构转为方法区的运行时数据结构
  3. 在堆中生成一个代表此类的java.lang.Class对象,作为访问方法区这些数据结构的入口。

这个过程主要就是类加载器完成。

28. 如果多个类加载器加载同一个类,会出现什么情况(未答)

29. tomcat的加载过程(未答)

30.

框架

1. Hibernete和MyBatis的区别

hibernate:
是一个标准的ORM框架(对象关系映射)。入门门槛较高,不需要程序写sql,sql语句自动生成。对sql语句的优化修改比较困难。

应用场景:
适用于需求变化不多的中小型项目,比如后台管理系统,ERP,ORM,OA。

mybatis:
专注sql本身,需要程序员自己编写sql语句,sql语句修改优化比较方便。mybatis是一个不完全的ORM框架,虽然程序员自己写sql,mybatis也可以实现映射(输入映射,输出映射)。

应用场景:
适用于需求变化较多的项目,比如:互联网项目

2. spring的启动过程

spring的启动过程

2-1. bean的加载(未答)

bean的加载

3. Spring Context初始化流程

Spring在初始化过程中要做的事情很多,下面我们就根据ClassPathXmlApplicationContext初始化看看我们的应用走了哪些步骤,我用debug模式下一步步来展现初始化过程。
首先我们看一下ClassPathXmlApplicationContext类的继承关系

ClassPathXmlApplicationContext
AbstractXmlApplicationContext
AbstractRefreshableConfigApplicationContext
AbstractRefreshableApplicationContext
AbstractApplicationContext
DefaultResourceLoader
I ResourceLoad
ApplicationContext context = new ClassPathXmlApplicationContext("conf/applicationContext.xml");
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
            throws BeansException {
        super(parent);
        setConfigLocations(configLocations);
        if (refresh) {
            refresh();
        }
    }

初始化应用上下文,使用ClassPathXmlApplicationContext类新建实例,初始化这个类,我们可以看到使用super方法初始化了父类,上面继承树中所有的类都初始化了;然后把传送的配置文件目录设置为配置文件;调用refresh方法,这个方法是AbstractApplicationContext类实现的方法。

public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            // Prepare this context for refreshing.
//准备上下文的刷新,
            prepareRefresh();
            // Tell the subclass to refresh the internal bean factory.
//得到新的Bean工厂,应用上下文加载bean就是在这里面实现的
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
//准备bean工厂用在上下文中
            // Prepare the bean factory for use in this context.
            prepareBeanFactory(beanFactory);
            try {
                // Allows post-processing of the bean factory in context subclasses.
//允许子类上下问处理bean工厂
                postProcessBeanFactory(beanFactory);
                // Invoke factory processors registered as beans in the context.
//请求工厂处理器作为beans注册在上下文
                invokeBeanFactoryPostProcessors(beanFactory);
                // Register bean processors that intercept bean creation.
//注册bean处理器拦截bean创建
                registerBeanPostProcessors(beanFactory);
                // Initialize message source for this context.
//初始化上下文中消息源
                initMessageSource();
                // Initialize event multicaster for this context.
//初始化上下文中事件广播
                initApplicationEventMulticaster();
                // Initialize other special beans in specific context subclasses.
//初始化其他具体bean
                onRefresh();
                // Check for listener beans and register them.
//检查监听bean并注册
                registerListeners();
                // Instantiate all remaining (non-lazy-init) singletons.
//实例化未初始化单例
                finishBeanFactoryInitialization(beanFactory);
                // Last step: publish corresponding event.
//最后一步发布相应事件
                finishRefresh();
            }

obtainFreshBeanFactory方法把配置文件中的bean加载到容器中。调用AbstractRefreshableApplicationContext的refreshBeanFactory方法,然后调用loadBeanDefinitioans(beanFactory)方法,这个方法的实现类是XmlWebApplicationContext的loadBeanDefinitions方法,方法调用Reader中的loadBeanDefinitions(configLocations)把配置文件传入Reader。

protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
//这里定义了Reader来读取Bean,在loadBeanDifinitions方法中传入reader
        XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
......
        // then proceed with actually loading the bean definitions.
        initBeanDefinitionReader(beanDefinitionReader);
        loadBeanDefinitions(beanDefinitionReader);
    }

XmlBeanDefinitionReader中调用AbstractBeanDefinition中的loadBeanDefinitions(location)方法。把配置文件读取成Resource后,调用XmlBeanDefinitionReader中loadBeanDefinitions(Resource)方法,在方法中调用doLoadBeanDefinitions(InputSource inputSource, Resource resource)传入参数是文件流和Resource,方法内调用registerBeanDefinitions(Document doc,Resource resource)方法

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
        BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
        documentReader.setEnvironment(this.getEnvironment());
        int countBefore = getRegistry().getBeanDefinitionCount();
        documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
        return getRegistry().getBeanDefinitionCount() - countBefore;
    }

在DocumentReader调用注册bean方法中进行注册,调用DefaultBeanDefinitionDocumentReader的doRegisterBeanDefinitions方法,在parseBeanDefinitions方法中调用parseDefaultElement(ele, delegate);方法,判断如果获取到的是个bean的话执行,processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate)方法最后在DefaultListableBeanFactory类中调用registerBeanDefinition进行注册,注册的容器是this.beanDefinitionMap.put(beanName, beanDefinition);,这个是初始化的CurrentHashMap类。
容器的初始化以及Bean的注册就完成了。

原博链接:https://www.jianshu.com/p/a569aae8b722

4. Spring中bean的生命周期(未答)

https://www.processon.com/special/template/5c9c2cd1e4b08cb4bb00691f

ClassPathXmlApplication 调用 它的父类 AbstractApplicationContext 的 refresh 方法,refresh里调用了obtainFreshBeanFactory方法,obtainFreshBeanFactory调用了 getBeanFactory 方法(这是一个抽象方法,这里用的他的实现类是AbstractRefreshableApplicationContext),getBeanFactory返回了一个beanFactory对象,它是DefaultListableBeanFactory类的实例,使用xmlbeandefinitionreader 将配置文件转换为 document 后,通过 parseBeanDefinition 解析出一个个BeanDefinition 通过 registerBeanDefinition 注册进 beanfactory中(发生在返回beanFactory之前),就是构造好一个beanFactory 在上面的getBeanFactory中返回。
在这里插入图片描述

4-1. spring的生命周期(不是bean)(未答)

5. Spring Boot的启动流程

在这里插入图片描述
总览:

启动流程主要分为三个部分,第一部分进行SpringApplication的初始化模块,配置一些基本的环境变量、资源、构造器、监听器,第二部分实现了应用具体的启动方案,包括启动流程的监听模块、加载配置环境模块、及核心的创建上下文环境模块,第三部分是自动化配置模块,该模块作为springboot自动配置核心,在后面的分析中会详细讨论。在下面的启动程序中我们会串联起结构中的主要功能。

启动:

每个SpringBoot程序都有一个主入口,也就是main方法main里面调用SpringApplication.run()启动整个spring-boot程序,该方法所在类需要使用**@SpringBootApplication注解**,以及@ImportResource注解(if need),@SpringBootApplication包括三个注解,功能如下:

@EnableAutoConfiguration:SpringBoot根据应用所声明的依赖来对Spring框架进行自动配置

@SpringBootConfiguration(内部为@Configuration):被标注的类等于在spring的XML配置文件中(applicationContext.xml),装配所有bean事务,提供了一个spring的上下文环境

@ComponentScan组件扫描,可自动发现和装配Bean,默认扫描SpringApplication的run方法里的Booter.class所在的包路径下文件,所以最好将该启动类放到根包路径下

首先进入run方法,run方法中去创建了一个SpringApplication实例,在该构造方法内,我们可以发现其调用了一个初始化的initialize方法,这里主要是为SpringApplication对象赋一些初值。构造函数执行完毕后,我们回到run方法,

该方法中实现了如下几个关键步骤:

  1. 创建了应用的监听器SpringApplicationRunListeners并开始监听

  2. 加载SpringBoot配置环境(ConfigurableEnvironment),如果是通过web容器发布,会加载StandardEnvironment,其最终也是继承了ConfigurableEnvironment,类图如下
    在这里插入图片描述
    可以看出,*Environment最终都实现了PropertyResolver接口,我们平时通过environment对象获取配置文件中指定Key对应的value方法时,就是调用了propertyResolver接口的getProperty方法

  3. 配置环境(Environment)加入到监听器对象中(SpringApplicationRunListeners)

  4. 创建run方法的返回对象:ConfigurableApplicationContext(应用配置上下文),我们可以看一下创建方法:
    在这里插入图片描述
    方法会先获取显式设置的应用上下文(applicationContextClass),如果不存在,再加载默认的环境配置(通过是否是web environment判断),默认选择AnnotationConfigApplicationContext注解上下文(通过扫描所有注解类来加载bean),最后通过BeanUtils实例化上下文对象,并返回

  5. 回到run方法内,prepareContext方法将listeners、environment、applicationArguments、banner等重要组件与上下文对象关联

  6. 接下来的refreshContext(context)方法(初始化方法如下)将是实现spring-boot-starter-*(mybatis、redis等)自动化配置的关键,包括spring.factories的加载,bean的实例化等核心工作。
    在这里插入图片描述

配置结束后,Springboot做了一些基本的收尾工作,返回了应用环境上下文。回顾整体流程,Springboot的启动,主要创建了配置环境(environment)、事件监听(listeners)、应用上下文(applicationContext),并基于以上条件,在容器中开始实例化我们需要的Bean,至此,通过SpringBoot启动的程序已经构造完成,接下来我们来探讨自动化配置是如何实现。

自动化配置:

之前的启动结构图中,我们注意到无论是应用初始化还是具体的执行过程,都调用了SpringBoot自动配置模块
在这里插入图片描述
该配置模块的主要使用到了SpringFactoriesLoader,即Spring工厂加载器,该对象提供了loadFactoryNames方法,入参为factoryClass和classLoader,即需要传入上图中的工厂类名称和对应的类加载器,方法会根据指定的classLoader,加载该类加器搜索路径下的指定文件,即spring.factories文件,传入的工厂类为接口,而文件中对应的类则是接口的实现类,或最终作为实现类,所以文件中一般为如下图这种一对多的类名集合,获取到这些实现类的类名后,loadFactoryNames方法返回类名集合,方法调用方得到这些集合后,再通过反射获取这些类的类对象、构造方法,最终生成实例
在这里插入图片描述
下图有助于我们形象理解自动配置流程

在这里插入图片描述
mybatis-spring-boot-starter、spring-boot-starter-web等组件的META-INF文件下均含有spring.factories文件,自动配置模块中,SpringFactoriesLoader收集到文件中的类全名并返回一个类全名的数组,返回的类全名通过反射被实例化,就形成了具体的工厂实例,工厂实例来生成组件具体需要的bean。

mybatis-spring-boot-starter、spring-boot-starter-web等组件的META-INF文件下均含有spring.factories文件,自动配置模块中,SpringFactoriesLoader收集到文件中的类全名并返回一个类全名的数组,返回的类全名通过反射被实例化,就形成了具体的工厂实例,工厂实例来生成组件具体需要的bean。

更多看原博吧 太多了会比较晦涩难懂。。。

答案原博:https://www.jianshu.com/p/87f101d8ec41

6. Spring Boot的注解(未答)

主要说@SpringBootApplication里面最主要的三个注解及作用

7. 简单介绍下Netty及使用场景(未答)

8. Netty线程模型(未答)

9. 简介RPC

https://zhuanlan.zhihu.com/p/50616871 (写的非常好,吹爆!)

  • 全称是:远程过程调用(Remote Procedure Call)
  • 在本地调用远程方法,就好像是调用本地方法一样
  • 举例:
    远程机器上有一段代码:HelloImpl.java(实现类)
    你本地有一段代码:Hello.java (接口)
    
    然后:
    你在本地使用:
    
         @Reference
         private Hello hello;
    
    就可以为Hello这个接口注入实现类HelloImpl了。(是不是像spring的Ioc一样?)
    至于怎么注入的,以及在本地怎么找到远程我要的实现类的先不管,
    再多了解一点下面的RPC知识后就自然而然地知道了。
    
  • 为什么是RPC不是http。
    存在这个问题是因为在经典的MVC RESTful开发中,前端只需要点击一个连接,就可以调用远程主机上的某些代码,执行服务,最后返回数据给前端。所以会思考,可否在后端里面像前端一样通过http调用,拿到远程主机返回的数据呢?
    答案是:为了提高传输效率与安全,采用RPC而不是http
    HTTP接口由于受限于HTTP协议,需要带HTTP请求头,这个请求头里面往往会携带很多无用的数据,导致传输起来效率或者说安全性不如RPC。
    RPC自己定义了一种TCP协议进行通讯,它是一种技术,一种思想,可以使用很多技术来实现,比如gRPC可以通过http2来实现。
    http的三次握手四次挥手等一些规矩增加了网络开销,http只适用于交互不大的系统,像淘宝这种亿级的系统,http扛不住。
  • 实现原理:(代理模式)
    扫描到@Reference注解后,就给它生成一个代理对象,将这个代理对象放进容器中。而这个代理对象的内部,就是通过httpClient来实现RPC调用的。
    RPC将需要调用的对象序列化后传输过来,然后反序列化。
  • 技术要点:
    Call ID映射
    序列化与反序列化
    网络传输
    要实现一个RPC框架,只需要把以上三点实现了就基本完成了
  • 原理,看图解释,按序号看调用过程
    在这里插入图片描述
    客户端要调用服务端的方法,客户端先告诉他的“小助手”stub,我要什么方法,参数是什么。

然后这个“小助手”会与服务端建立网络通讯传递方法调用信息,服务端的“小助手”接收到了客户端的请求,知道他要哪个方法,传递的什么参数,然后服务端的“小助手”就找到对应的方法,并执行,最后将结果通过网络返回给客户端的“小助手”,它再传给发起调用的地方。

客户端的“小助手”要将调用信息发送到服务端就要序列化请求,便于在网络中传输。

服务端的“小助手”需要反序列化后才知道参数到底是啥。

同样数据返回时依然需要序列化与反序列化

UML时序图
在这里插入图片描述
影响RPC调用的因素主要是:1.客户端能否与服务端【快速】建立连接、2.序列化与反序列化的速度是否够快。

再结合这篇:https://www.jianshu.com/p/fd0d4bf85c97
在这里插入图片描述

  1. 服务消费方(client)调用以本地调用方式调用服务;
  2. client stub接收到调用后负责将方法、参数等组装成能够1. 进行网络传输的消息体;
  3. client stub找到服务地址,并将消息发送到服务端;
  4. server stub收到消息后进行解码;
  5. server stub根据解码结果调用本地的服务;
  6. 本地服务执行并将结果返回给server stub;
  7. server stub将返回结果打包成消息并发送至消费方;
  8. client stub接收到消息,并进行解码;
  9. 服务消费方得到最终结果。

RPC的目标就是要2~8这些步骤都封装起来,让用户对这些细节透明。

其中的技术细节包括:

  • 对象的序列化反序列化
  • 通信

rpc和Rest Api的区别
REST是一种设计风格,它的很多思维方式与RPC是不一样的。

RPC的思想是把本地函数映射到API,也就是说一个API对应的是一个function,我本地有一个getAllUsers,远程也能通过某种约定的协议来调用这个getAllUsers。至于这个协议是Socket、是HTTP还是别的什么并不重要;

RPC中的主体都是动作,是个动词,表示我要做什么。

而REST则不然,它的URL主体是资源,是个名词。而且也仅支持HTTP协议,规定了使用HTTP Method表达本次要做的动作,类型一般也不超过那四五种。这些动作表达了对资源仅有的几种转化方式。

下面图文可能与此问题无关。。。
在这里插入图片描述

ZK使用长连接推送方式,consul使用心跳方式。

10. 如何防止sql注入(未答)

11. Mybatis处理流程(未答)

在这里插入图片描述

12. 框架问题整理(未答)

在这里插入图片描述
在这里插入图片描述

13. spring cloud负载均衡说一下

使用的是ribbon,它默认的策略是轮询策略,也支持随机、权重等策略。

14. springboot如何加载

探索SpringApplication启动Spring

15. @Component和@Bean注解的区别

@Component在类上使用,表明这个类是个组件类,需要Spring为这个类创建Bean。@Bean注解使用在方法上,告诉Spring这个方法将会返回一个Bean对象,需要把返回的对象注册到Spring应用的上下文中。

数据库

1. Innodb 下如何解决幻读的问题?

Innodb通过MVCC(Multi-version Concurrency Control多版本并发控制)解决了幻读的问题。

MySQL的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制(MVCC)。

可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。

MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,这个事务看到的数据都是一致的。

下面我们通过InnoDB的简化版行为来说明MVCC是如何工作的。

InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号(systemversionnumber)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。下面看一下在REPEATABLEREAD隔离级别下,MVCC具体是如何操作的。

SELECT InnoDB会根据以下两个条件检查每行记录:InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。只有符合上述两个条件的记录,才能返回作为查询结果。

INSERT InnoDB为新插入的每一行保存当前系统版本号作为行版本号。

DELETE InnoDB为删除的每一行保存当前系统版本号作为行删除标识。

UPDATE InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。MVCC只在REPEATABLEREAD和READCOMMITTED两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容(4),因为READUNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。而SERIALIZABLE则会对所有读取的行都加锁。

2. sql优化的几种方式

  • 为搜索字段创建索引。

  • 避免使用 select *,列出需要查询的字段。

  • 垂直分割分表。

  • 选择正确的存储引擎。

在这里插入图片描述

3. 联合索引的最左匹配原则

最左匹配原则
在这里插入图片描述
成因
mysql创建复合索引的规则是首先会对复合索引的最左边,也就是索引中的第一个字段进行排序,在第一个字段排序的基础上,在对索引上第二个字段进行排序,其实就像是实现类似order by 字段1,字段2这样的排序规则,那么第一个字段是绝对有序的,而第二个字段就是无序的了,因此一般情况下直接只用第二个字段判断是用不到索引的,这就是为什么mysql要强调联合索引最左匹配原则的原因。

4. postgresql与mysql的区别

一、开源方面(psql更好)

  • PostgreSQL基于自由的BSD/MIT许可,组织可以使用、复制、修改和重新分发代码,只需要提供一个版权声明即可。
  • MySQL的开源协议是基于GPL协议,任何公司都可以免费使用,不允许修改后和衍生的代码做为闭源的商业软件发布和销售,MySQL的版权在甲骨文手中,甲骨文可以推了其商业闭源版本。

二、ACID支持方面(psql更好)

  • PostgreSQL支持事务的强一致性,事务保证性好,完全支持ACID特性。

  • MySQL只有innodb引擎支持事务,事务一致性保证上可根据实际需求调整,为了最大限度的保护数据,MySQL可配置双一模式,对ACID的支持上比PG稍弱弱。

三、SQL标准的支持方面(psql更好)

  • PostgreSQL几乎支持所有的SQL标准,支持类型相当丰富。

  • MySQL只支持部分SQL标准,相比于PG支持类型稍弱。

四、复制

  • MySQL的复制是基于binlog的逻辑异步复制,无法实现同步复制
  • PostgreSQL可以做到同步,异步,半同步复制,以及基于日志逻辑复制,可以实现表级别的订阅和发布。

五、并发控制

  • PostgreSQL通过其MVCC实现有效地解决了并发问题,从而实现了非常高的并发性。
  • innodb的基于回滚段实现的MVCC机制,但是MySQL的间隙锁影响较大,锁定数据较多。

六、性能

  • PostgreSQL

    ① PostgreSQL广泛用于读写速度高和数据一致性高的大型系统。此外,它还支持各种性能优化,当然这些优化仅在商业解决方案中可用,例如地理空间数据支持,没有读锁定的并发性等等。

    ② PostgreSQL性能最适用于需要执行复杂查询的系统。

    ③ PostgreSQL在OLTP/ OLAP系统中表现良好,读写速度以及大数据分析方面表现良好,基于PG的GP数据库,在数据仓库领域表现良好。

    ④ PostgreSQL也适用于商业智能应用程序,但更适合需要快速读/写速度的数据仓库和数据分析应用程序。

  • MySQL

    ① MySQL是广泛选择的基于Web的项目,需要数据库只是为了简单的数据事务。 但是,当遇到重负载或尝试完成复杂查询时,MySQL通常会表现不佳。

    ② MySQL的读取速度,在OLTP系统中表现良好。

    ③ MySQL + InnoDB为OLTP场景提供了非常好的读/写速度。总体而言,MySQL在高并发场景下表现良好。

    ④ MySQL是可靠的,并且与商业智能应用程序配合良好,因为商业智能应用程序通常读取很多。

PostgreSQL与MySQL优劣对比

  • PostgreSQL相对于MySQL的优势

    1、在SQL的标准实现上要比MySQL完善,而且功能实现比较严谨;

    2、存储过程的功能支持要比MySQL好,具备本地缓存执行计划的能力;

    3、对表连接支持较完整,优化器的功能较完整,支持的索引类型很多,复杂查询能力较强;

    4、PG主表采用堆表存放,MySQL采用索引组织表,能够支持比MySQL更大的数据量。

    5、PG的主备复制属于物理复制,相对于MySQL基于binlog的逻辑复制,数据的一致性更加可靠,复制性能更高,对主机性能的影响也更小。

    6、MySQL的存储引擎插件化机制,存在锁机制复杂影响并发的问题,而PG不存在。

    7、PG对可以实现外部数据源查询,数据源的支持类型丰富。

    8、PG原生的逻辑复制可以实现表级别的订阅发布,可以实现数据通过kafka流转,而不需要其他的组件。

    9、PG支持三种表连接方式,嵌套循环,哈希连接,排序合并,而MySQL只支持嵌套循环。

    10、 PostgreSQL源代码写的很清晰,易读性比MySQL强太多了。

    11、 PostgreSQL通过PostGIS扩展支持地理空间数据。 地理空间数据有专用的类型和功能,可直接在数据库级别使用,使开发人员更容易进行分析和编码。

    12、可扩展型系统,有丰富可扩展组件,作为contribute发布。

    13、 PostgreSQL支持JSON和其他NoSQL功能,如本机XML支持和使用HSTORE的键值对。 它还支持索引JSON数据以加快访问速度,特别是10版本JSONB更是强大。

    14、 PostgreSQL完全免费,而且是BSD协议,如果你把PostgreSQL改一改,然后再拿去卖钱,也没有人管你,这一点很重要,这表明了PostgreSQL数据库不会被其它公司控制。 相反,MySQL现在主要是被Oracle公司控制。

  • MySQL相对于PG的优势

    1、innodb的基于回滚段实现的MVCC机制,相对PG新老数据一起存放的基于XID的MVCC机制,是占优的。新老数据一起存放,需要定时触 发VACUUM,会带来多余的IO和数据库对象加锁开销,引起数据库整体的并发能力下降。而且VACUUM清理不及时,还可能会引发数据膨胀;

    2、MySQL采用索引组织表,这种存储方式非常适合基于主键匹配的查询、删改操作,但是对表结构设计存在约束;

    3、MySQL的优化器较简单,系统表、运算符、数据类型的实现都很精简,非常适合简单的查询操作;

    4、MySQL相对于PG在国内的流行度更高,PG在国内显得就有些落寞了。

    5、MySQL的存储引擎插件化机制,使得它的应用场景更加广泛,比如除了innodb适合事务处理场景外,myisam适合静态数据的查询场景。

总结

总体上来说,开源数据库都不是很完善,商业数据库oracle在架构和功能方面都还是完善很多的。从应用场景来说,PG更加适合严格的企业应用场景(比如金融、电信、ERP、CRM),但不仅仅限制于此,PostgreSQL的json,jsonb,hstore等数据格式,特别适用于一些大数据格式的分析;而MySQL更加适合业务逻辑相对简单、数据可靠性要求较低的互联网场景(比如google、facebook、alibaba),当然现在MySQL的在innodb引擎的大力发展,功能表现良好。

5. 介绍下数据库的主从同步及容灾(未答)

6. MySQL如何根据索引字段找到数据的?

https://blog.csdn.net/tongdanping/article/details/79878302

索引是一个排序的列表,在这个列表中存储着索引的值和包含这个值的数据所在行的物理地址,在数据十分庞大的时候,索引可以大大加快查询的速度,这是因为使用索引后可以不用扫描全表来定位某行的数据,而是先通过索引表找到该行数据对应的物理地址然后访问相应的数据

7. 事务隔离级别和他们解决的问题并介绍

8. 哪些情况下不应该使用索引?

  1. 数据唯一性差的字段不要使用索引
    比如性别,只有两种可能数据。意味着索引的二叉树级别少,多是平级。这样的二叉树查找无异于全表扫描。
  2. 频繁更新的字段不要使用索引
    比如logincount登录次数,频繁变化导致索引也频繁变化,增大数据库工作量,降低效率。
  3. 字段不在where语句出现时不要添加索引
    只有在where语句出现,mysql才会去使用索引
  4. 数据量少的表不要使用索引
    使用了改善也不大
    另外。如果mysql估计使用全表扫描要比使用索引快,则不会使用索引。

缓存

1. 说一下Redis的几种数据结构,问我 zset 的底层数据结构(就知道跳跃表)

String:
字符串类型的内部编码有3种:

  • int:8个字节的长整型。
  • embstr:小于等于39个字节的字符串。
  • raw:大于39个字节的字符串。

Rdis会根据当前值的类型和长度决定使用哪种内部编码实现。

Hash
哈希类型的内部编码有两种:

  • ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以再节省内存方面比hashtable更加优秀。
  • hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。

list
列表类型的内部编码有两种。

  • ziplist(压缩列表):当列表的元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时(默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使用。
  • linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。

set
集合类型的内部编码有两种:

  • intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用。
  • hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。

zset

有序集合类型的内部编码有两种:

  • ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplist-entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配置(默认64字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存的使用。
  • skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist的读写效率会下降。

有序集合比较典型的使用场景就是排行榜系统。

2. 说一下 Redis 的数据淘汰策略

Redis在已使用内存达到设定的上限时,提供了6种数据淘汰策略(也就是maxmemory-policy可能的值):

volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用 的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数 据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据 淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据

何时触发淘汰数据的动作

1、一个客户端执行指令,导致数据的增加时。

2、Redis检测到内存的使用已经达到上限。

3、Redis自身执行指令时,等等

注意:

Redis为了避免反复触发淘汰策略,每次会淘汰掉一批数据。

当Redis指令产生数据比较大时,淘汰掉的数据量也相应也比较大。

为了节省内存,LRU的策略并不是严格执行的,Redis是在整体中随机抽样取出一小部分数据,在这部分数据中严格执行LRU策略,在Redis3.0以后的版本对此算法做了改进,但仍然也是近似的LRU的策略,只是离真正的LRU更近了。

另外用户可以动态的设定随机抽取的样本数,例如:maxmemory-samples 5

LRU(Least recently used,最近最少使用)

3. Redis的哈希环最多有16384个槽,为什么是16384?

CRC16算法产生的hash值有16bit,该算法可以产生2^16-=65536个值。换句话说,值是分布在0~65535之间。那作者在做mod运算的时候,为什么不mod65536,而选择mod16384?

这里是取了一个权衡点:
1)如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。myslots[CLUSTER_SLOTS/8]。65536÷8÷1024=8kb,改为16384就是2kb。所以为了减少心跳时带宽占有率,应该缩减槽点数量。
2) redis的集群主节点数量基本不可能超过1000个。
3) 槽位越小,节点少的情况下,压缩率高。所以为了压缩率,槽位也不能太少(槽位少,则槽位大)。
综上,为了平衡压缩率和心跳时带宽,取值16384。

4. 当哈希环出现“数据倾斜” 该如何解决?

暂无答案,猜测是使用虚拟槽分区。虚拟槽分区巧妙地使用了哈希分区,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中。

使用虚拟节点

5. 描述一下Redis中的哨兵机制,主从切换具体是如何实现的?

6. Redis里面使用到了Raft协议,你有了解么?

Raft协议是用来解决分布式系统一致性问题的协议,但是redis内并没有用来实现一些分布式锁以及分布式事务,仅仅是用来做master宕机时的选主。

了解了raft协议,我们就看一下redis的一致性解决方案sentinel的架构设计。

sentinel集群内的每一个节点都会监控集群内部的每一个节点的状态,并将定时交换监控的信息。

我们来看下sentinel是怎么用raft协议来实现节点宕机处理的:

  • 如果一台master节点,比如master1节点宕机下线,sentinel1发现master1没有汇报自己的状态,在sentinel内部有一个节点没有汇报的最长时间上线,当一个节点超出了这个时间上限,就会在本机标记此节点主观下线,就是说在本sentinel节点的视角来看,此节点是下线状态。
  • 由于sentinel集群中有三个sentinel节点,只有sentinel1认为master1下线,这就仅仅是主观下线,还不能处理故障转移。然后过了一段时间,sentinel2也发现master1好久没有汇报信息,也把master1标记未主观下线。sentinel集群中的三台机器会定时交流自己的监控信息,当sentinel1发现sentinel2也认为master1主观下线了,就是说集群中有超过一半的节点认为节点下线了,这时sentinel集群就会达成一个一致,认为master1已经客观下线了。
  • 此时sentinel就会开始执行故障转移,在slave11和slave12中选出新的master节点。首先slave11和slave12变成候选者,等待一个0到1s内的随机值,然后向sentinel集群的每一个节点发送求票信息,希望能选举自己成为master,每一个sentinel只能投一次票,最终必然有一个节点成为新的master。比如slave11获取了sentinel1和sentinel2的选票,slave12获取了sentinel3的选票,最终的结果就是slave11成为master。
  • 然后slave11成为master,slave12成为slave11的slave,转而向slave11进行主从复制。
  • 同时sentinel也会监视master1,如果master1经过修复后重新上线,这时的master就会变成slave11的从节点,转而向master进行主从复制。

至此,redis的故障转移就完成了,redis利用比较易于实现的raft协议实现了节点宕机的自动化处理,保障了集群的高可用性。

答案博客链接

7. 消息队列怎么保证可靠性传递(消息丢失问题)(未答)

消息队列持久化

8. redis缓存 穿透、并发、雪崩场景介绍及解决方案(未答)

穿透分为前后、前:布隆过滤器
缓存并发:加锁阻塞(分布式锁)
缓存雪崩:设置随机过期时间

9. 如何保证redis/DB一致性

过期时间和回写局限性太大,回写的话又要连接数据库,又要连接redis。
使用消息队列
使用binlog

消息队列

1. 消息队列的好处

2. 如何保证消息队列的可靠性传输

算法

1. 给一个数组,求最大的连续递增子数组的长度?

本题最好的解法是使用动态规划,首先要想明白一个问题:如何找到以a[n]作为最后一个元素的最长子列。

要找到以a[n]作为最后一个元素的最长子列,我们需要找出数组中所有的a[n]之前并且比a[n]小的元素集合,设为S(a[s]), 设在S(a[s])中以元素a[s]为最后一个元素的子列为Ls(a[s]), 选择Ls(a[s])中最长的一个加上a[n]自身就得到了以a[n]元素为队尾的最长子列。

要得到最终答案只需要再次遍历数组,比较所有以a[k](0 < k <= n)为队末的队列长度并选择最长的一个即可。

leetcode第三百题:

package lc201_400;

/**
 * 最长上升子序列
 * https://leetcode-cn.com/problems/longest-increasing-subsequence/submissions/
 * 给定一个无序的整数数组,找到其中最长上升子序列的长度。
 *
 * @author binzhang
 * @date 2019-08-17
 */
public class LeetCode300 {
    public int lengthOfLIS(int[] nums) {
        if (nums.length <= 1) {
            return nums.length;
        }
        int[] dp = new int[nums.length];
        dp[0] = 1;
        for (int i = 1 ; i < nums.length ; i ++) {
            int max = 1;
            for (int j = 0 ; j < i ; j ++) {
                if (nums[i] > nums[j]) {
                    max = Math.max(max, dp[j] + 1);
                }
            }
            dp[i] = max;
        }
        int res = 0;
        for (int i = 0 ; i < dp.length ; i ++) {
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

2. 给你一个二叉树,返回每一层的数值相加计算得到的平均值的数组(其实就是一个层序遍历)

leetcode637

通过栈或者队列来解决。

3. 实现一个二叉树的中序遍历(不用递归做)

leetcode94
题解链接:https://blog.csdn.net/coder__666/article/details/80349039

中序遍历(LDR)是二叉树遍历的一种,也叫做中根遍历、中序周游。在二叉树中,先左后根再右。巧记:左根右。
中序遍历首先遍历左子树,然后访问根结点,最后遍历右子树
若二叉树为空则结束返回,
否则:

(1)中序遍历左子树
(2)访问根结点
(3)中序遍历右子树

import java.util.Stack;
public class Test 
{
	public static void main(String[] args)
	{
		TreeNode[] node = new TreeNode[10];//以数组形式生成一棵完全二叉树
		for(int i = 0; i < 10; i++)
		{
			node[i] = new TreeNode(i);
		}
		for(int i = 0; i < 10; i++)
		{
			if(i*2+1 < 10)
				node[i].left = node[i*2+1];
			if(i*2+2 < 10)
				node[i].right = node[i*2+2];
		}
		
		midOrderRe(node[0]);
		System.out.println();
		midOrder(node[0]);
	}
	
	public static void midOrderRe(TreeNode biTree)
	{//中序遍历递归实现
		if(biTree == null)
			return;
		else
		{
			midOrderRe(biTree.left);
			System.out.println(biTree.value);
			midOrderRe(biTree.right);
		}
	}
	
	
	public static void midOrder(TreeNode biTree)
	{//中序遍历费递归实现
		Stack<TreeNode> stack = new Stack<TreeNode>();
		while(biTree != null || !stack.isEmpty())
		{
			while(biTree != null)
			{
				stack.push(biTree);
				biTree = biTree.left;
			}
			if(!stack.isEmpty())
			{
				biTree = stack.pop();
				System.out.println(biTree.value);
				biTree = biTree.right;
			}
		}
	}
}
 
class TreeNode//节点结构
{
	int value;
	TreeNode left;
	TreeNode right;
	
	TreeNode(int value)
	{
		this.value = value;
	}
}

或者看这个:https://blog.csdn.net/weixin_43852903/article/details/91466111

解题思路
递归和非递归底层实现都是栈。

  • 对于递归:先一直向左遍历,直到为空时返回并且打印最左的左孩子,然后再判断该元素是否有右孩子,此时的元素再递归判断左孩子~~~和之前情形一致。
  • 对于非递归:将所有的左孩子都入栈,定义一个top指向栈顶元素,当遍历cur到null时,可以将cur指向新的top,这样下来再遍历top.right,直到遍历完二叉树为止。

递归代码实现

void binaryTreeInOrder(TreeNode root) {
    if (root == null){
        return;
    }
    binaryTreeInOrder(root.left);
    System.out.print(root.value+" ");
    binaryTreeInOrder(root.right);
}

非递归代码实现

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        //用栈实现
        Stack<TreeNode> stack = new Stack<>();
        TreeNode cur = root;
        TreeNode top = null;
        while (cur != null || !stack.isEmpty()){
            //内层循环将当前数据入栈
            while (cur != null){
                stack.push(cur);
                cur = cur.left;
            }
            //出栈并将该元素放入到链表中
            top = stack.pop();
            list.add(top.val);
            //将cur指向栈顶元素的右孩子
            cur = top.right;
        }
        return list;
    }
}

4. 找到数组中只出现一次的数字

LeetCode136

暴力解法:遍历数组,map中没有,put(arr[i], 1),有的话value+1。

如果其他数字都是偶数次数:直接遍历,如果没出现过放入ArrayList中,有的话从ArrayList中移除。

快速解法:用异或的特性,A^A=0 0^X=X;以及异或的交换律特性。

class Solution {
    public static int singleNumber(int[] nums) {        
        int result=0;
        for(int i=0;i<nums.length;i++){
        	// 因为其他的都是出现双数次,所以其他的都抵消掉了,最后只剩唯一元素的 A^0=A
            result^=nums[i];
        }
        return result;
    }
}

5. 合并二叉树

给定两棵二叉树,把它们合并成一个二叉树,合并规则如下:如果两棵树的对应节点不为空,把这两个节点的和作为新树的节点,如果有一个节点为空,则把非空的节点作为新树的节点。

leetcode617

AB两棵树 同步递归 用B更新A并返回A

递归过程:

  • 如果A当前节点为空 返回B的当前节点
  • 如果B当前节点为空 返回A的当前节点
  • (此情况已经包含在上述两种)AB的两个当前节点都为空 返回null
  • 都不为空 则将B的val 加到A的val上 返回当前节点
/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
class Solution {
    public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
        if (t1 == null && t2 == null) {
            return null;
        }
        if (t1 != null && t2 == null) {
            return t1;
        }
        if (t1 == null && t2 != null) {
            return t2;
        }
        t1.val += t2.val;
        t1.left = mergeTrees(t1.left, t2.left);
        t1.right = mergeTrees(t1.right, t2.right);
        return t1;
    }
}

6. 给你一个循环链表让你找入口节点。考虑空间复杂度和不考虑空间复杂度两种做法

leetcode142

下面的解法空间复杂度是O(1),还可以直接用Set判断重复元素,时间复杂度是O(n)。

package lc1_200;

/**
 * 环形链表 II
 * https://leetcode-cn.com/problems/linked-list-cycle-ii/
 *
 * @author binzhang
 * @date 2019-09-08
 */

class ListNode {
    int val;
    ListNode next;
    ListNode(int x) {
        val = x;
        next = null;
    }
}

public class LeetCode142 {
    public ListNode detectCycle(ListNode head) {
        if (head == null || head.next == null) {
            return null;
        }
        ListNode p1 = head;
        ListNode p2 = head;
        // 这里要注意判断p2.next 不然p2.next.next会报错
        while (p1 != null && p2 != null && p2.next != null) {
            p1 = p1.next;
            p2 = p2.next.next;
            if (p1 == p2) {
                // p1 == p2 已经能证明有环,此时p2走过的路等于p1走过的路+环一圈的路,即从出发点到相遇点的路程等于环一圈的路程,所以相交点到入环点的距离等于出发点到入环点的距离。
                p1 = head;
                while (p1 != p2) {
                    p1 = p1.next;
                    p2 = p2.next;
                }
                if (p1 == p2) {
                    return p1;
                }

            }
        }
        return null;
    }
}

7. 设计模式的原则

设计模式有六大基本原则:

一、单一职责原则

单一职责原则:不要存在多于一个导致类变更的原因

顾名思义,就是职责单一,只做自己要的职责,其他的东西我不干,比如说一个互联网公司技术开发有android开发,ios开发,前端开发,后端开发 等 ,每一个职位都需要要专业的人来开发,才能开发出优秀的产品。如果公司为了节约成本,做android的又要搞ios开发,又要搞后端开发,所有的事都由一个人来干,这样不但忙不过来,而且不可能每一项技术都是那么专业,这就表明了单一职责的重要性。

二、里氏替换原则

里氏替换原则:子类可以扩展父类的功能,但不能改变父类原有的功能

里氏替换原则本质就是继承和多态的应用
这里面的意思是有
1、子类可以实现父类的抽象方法,也可以扩展添加自己的方法
2、当子类重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松,比如父类方法的形参为String,那么子类的方法形参为String或String父类
3、当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。比如父类的方法的返回值为String,那么子类方法返回值为String或String的子类

三、依赖倒置原则

依赖倒置原则:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象

说白了,就是面向接口
比如我们就常常把访问数据库的代码写成了函数,在访问数据库反复调用,这就叫做高层模块依赖低层模块,但是后来想用其他数据库,问题就来了,我们就不能直接利用高层,解决方法就是面向接口

四、接口隔离原则

接口隔离原则:客户端不应该被强迫地依赖那些根本用不上的方法。

和单一职责原则的区别,其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架

五、迪米特原则

迪米特法则:一个对象应该对其他对象保持最少的了解

这个就是面向对象的一个特征,对象之前尽量减少耦合,在一个类中应该少出现其他类

六、开闭原则

开闭原则:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

不要改你以前写的代码,你应该加一些代码去扩展原来的功能,来实现新的需求,对于新功能,不要动不动就改别人的代码,而是在之前代码的基础上添加去扩展新功能

8. 如何找到两个相交链表的相交点?

依次遍历的话是O(n^2)级别的,肯定不是面试官想要的答案。。。

我们可以先通过遍历算出两个链表的长度,然后得到长度的差值,先让长的走差值的长度,这样因为相交点之后的数据两个链表是一样的,所以在此时两个链表同步向下走,在为null之前有相等的地方就是他们的相交点。
在这里插入图片描述

答案链接
这里考虑了两种情况,链表无环和链表有环两种。

9. 判断给定字符串中的括号是否匹配

leetcode20
解题思路:

  1. 使用栈
  2. 遇左括号入栈
  3. 遇右括号出栈,判断出栈括号是否与右括号成对

10. TopK问题:找出N个数中最小的K个数(N非常大)

解法:

  1. 用前K个数创建大小为K的大根堆
  2. 剩余N-K个数跟堆顶进行比较

时间复杂度N*logK

11. TopK变种:从N有序队列中找到最小的K个值

解法:

  1. 用N个队列的最小值组成大小为K的小根堆
  2. 取堆顶值
  3. 将堆顶值所在队列的下一个值加入堆,并调整
  4. 重复步骤2,直到K次

时间复杂度(N+K-1)*logK

12. 常用算法题解决思路及场景

在这里插入图片描述
在这里插入图片描述

13. 常见算法题目

在这里插入图片描述
在这里插入图片描述

项目

0. 项目中的难点亮点!!!!!

可以从两方面着手回答:

  • 分布式系统:服务间调用,熔断器,分布式事务。。。
  • 系统的高并发和高可用:数据库主从同步,容灾;Redis集群。。。

1. 如何设计一个秒杀系统?

秒杀系统的整体架构可以概括为“稳、准、快”。

  • 稳(高可用):整个系统架构要满足高可用,流量符合预期时肯定要稳定,超出预期时也同样不能掉链子,你要保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这个是最基本的前提。
  • 准(可靠性):你的业务需求是秒杀10台iPhone XS,那就只能成交10台,多一台少一台都不行。一旦库存不对,那平台就要承担损失。
  • 快(高并发):就是说系统的性能要足够高,否则你怎么支撑这么大的流量呢?不光是服务端要做极致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方快一点,整个系统就完美了。

设计思路:将请求拦截在系统上游,降低下游压力。在一个并发量大,实际需求小的系统中,应当尽量在前端拦截无效流量,降低下游服务器和数据库的压力,不然很可能造成数据库读写锁冲突,甚至导致死锁,最终请求超时。

  • 限流:前端直接限流,允许少部分流量流向后端。

  • 削峰:瞬时大流量峰值容易压垮系统,解决这个问题是重中之重。常用的消峰方法有异步处理、缓存和消息中间件等技术。

  • 异步处理:秒杀系统是一个高并发系统,采用异步处理模式可以极大地提高系统并发量,其实异步处理就是削峰的一种实现方式。

  • 内存缓存:秒杀系统最大的瓶颈一般都是数据库读写,由于数据库读写属于磁盘IO,性能很低,如果能够把部分数据或业务逻辑转移到内存缓存,效率会有极大地提升。

  • 消息队列:消息队列可以削峰,将拦截大量并发请求,这也是一个异步处理过程,后台业务根据自己的处理能力,从消息队列中主动的拉取请求消息进行业务处理。

  • 可拓展:当然如果我们想支持更多用户,更大的并发,最好就将系统设计成弹性可拓展的,如果流量来了,拓展机器就好了,像淘宝、京东等双十一活动时会临时增加大量机器应对交易高峰。

2. 谈谈对分布式事务的理解(未答)

3. 熟悉的开源项目(看spring和netty吧)

3.1 spring

3.2 netty

计算机网络

1. Http 1.0 和 Http 1.1 区别

1. 长连接
HTTP 1.0需要使用keep-alive参数来告知服务器端要建立一个长连接,而HTTP1.1默认支持长连接。

HTTP是基于TCP/IP协议的,创建一个TCP连接是需要经过三次握手的,有一定的开销,如果每次通讯都要重新建立连接的话,对性能有影响。因此最好能维持一个长连接,可以用个长连接来发多个请求。

2. 节约带宽
HTTP 1.1支持只发送header信息(不带任何body信息),如果服务器认为客户端有权限请求服务器,则返回100,否则返回401。客户端如果接受到100,才开始把请求body发送到服务器。

这样当服务器返回401的时候,客户端就可以不用发送请求body了,节约了带宽。

另外HTTP还支持传送内容的一部分。这样当客户端已经有一部分的资源后,只需要跟服务器请求另外的部分资源即可。这是支持文件断点续传的基础。

3. HOST域
现在可以web server例如tomat,设置虚拟站点是非常常见的,也即是说,web server上的多个虚拟站点可以共享同一个ip和端口。

HTTP1.0是没有host域的,HTTP1.1才支持这个参数。

2. HTTP1.1 HTTP 2.0主要区别

1. 多路复用
HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。

当然HTTP1.1也可以多建立几个TCP连接,来支持处理更多并发的请求,但是创建TCP连接本身也是有开销的。

TCP连接有一个预热和保护的过程,先检查数据是否传送成功,一旦成功过,则慢慢加大传输速度。因此对应瞬时并发的连接,服务器的响应就会变慢。所以最好能使用一个建立好的连接,并且这个连接可以支持瞬时并发的请求。

关于多路复用,可以参看学习NIO

2. 数据压缩
HTTP1.1不支持header数据的压缩,HTTP2.0使用HPACK算法对header的数据进行压缩,这样数据体积小了,在网络上传输就会更快。

3. 服务器推送
意思是说,当我们对支持HTTP2.0的web server请求数据的时候,服务器会顺便把一些客户端需要的资源一起推送到客户端,免得客户端再次创建连接发送请求到服务器端获取。这种方式非常合适加载静态资源。

服务器端推送的这些资源其实存在客户端的某处地方,客户端直接从本地加载这些资源就可以了,不用走网络,速度自然是快很多的。

3.

操作系统

1. 输出文件内容

  • cat:cat filename # -n 打印出行号
  • tac:是 cat 的反向操作,从最后一行开始打印。
  • more:和 cat 不同的是它可以一页一页查看文件内容,比较适合大文件的查看。
  • less:和 more 类似,但是多了一个向前翻页的功能。
  • head:取得文件前几行。
  • tail:是 head 的反向操作,只是取得是后几行。tail -fn 999 xxx.log

2. 指令与文件搜索

  • which:指令搜索。which [-a] command # -a :将所有指令列出,而不是只列第一个
  • whereis:文件搜索。速度比较快,因为它只搜索几个特定的目录。whereis [-bmsu] dirname/filename
  • locate:文件搜索。可以用关键字或者正则表达式进行搜索。locate [-ir] keyword # -r:正则表达式
  • find:文件搜索。find / -name xxx

3. 查看进程

  • ps:查看某个时间点的进程信息。ps aux | grep xxx
  • pstree:查看进程树。示例:查看所有进程树 pstree -A
  • top:实时显示进程信息。示例:两秒钟刷新一次 top -d 2
  • netstat:查看占用端口的进程。示例:查看特定端口的进程 netstat -anp | grep port

设计模式

1. 设计模式分类

  • 创建型模式,共五种:单例模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式。
  • 结构型模式,共七种:适配器模式、装饰者模式、代理模式、门面模式(外观模式)、桥梁模式、组合模式、享元模式。
  • 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
  • 扩展模式:规则模式、对象池模式、雇工模式、黑板模式、空对象模式。

原博链接:https://blog.csdn.net/afei__/article/details/80412746

2. 六大设计原则

  • 总原则:开闭原则

    一个软件实体如类、模板或函数应该对扩展开放,对修改关闭。

  • 1.单一职责原则:应该有且仅有一个原因引起类的变更。
  • 2.里氏替换原则:所有引用基类的地方都必须能透明地使用其子类的对象。
  • 3.依赖倒置原则:高层模块不应该依赖底层模块,两者都要改依赖其抽象。
  • 4.接口隔离原则:客户端不应该依赖他不需要的接口,类之间的依赖关系应该建立在最小的接口上。
  • 5.迪克特法则(最少知道原则):一个对象应该对其他对象有最少的了解(低耦合)。
  • 6.合成复用原则:是在一个新的对象里面使用一些已有的对象,使其成为新对象的一部分。新对象通过委派达到复用已有功能的效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值