面试问题大全

1. Java基础

1.1 各种语言的特点

img

强弱是语言类型的严格程度,动静是变量与类型绑定方法,都有类型有关

1.2 StringBuilder和StringBuffer
  1. 他们两个字符串的修改都是对自身进行修改,不会创建新的内存空间,速度比string快
  2. stringbuilder是非线程安全的,buffer是线程安全的,因为常用的方法都使用了synchronized关键字进行同步,适用于多进程的环境
  3. 三个都被final修饰了,所以都不能被继承
1.3 内部类

成员内部类,局部内部类,静态内部类

作用是什么?

1.封装性

如果我们的内部类不想轻易被任何人访问,可以选择使用private修饰内部类,这样我们就无法通过创建对象的方法来访问,想要访问只需要在外部类中定义一个public修饰的方法,间接调用。

2.多继承

有提到可以用接口来实现多继承的效果,即一个接口有多个实现,但是这里也是有一点弊端的,那就是,一旦实现一个接口就必须实现里面的所有方法,有时候就会出现一些累赘,但是使用内部类可以很好的解决这些问题,用多个内部类来单继承,就实现的广义上的多继承

3.用匿名内部类实现回调功能

我们用通俗讲解就是说在Java中,通常就是编写一个接口,然后你来实现这个接口,然后把这个接口的一个对象作以参数的形式传到另一个程序方法中, 然后通过接口调用你的方法,匿名内部类就可以很好的展现了这一种回调

4.解决继承及实现接口出现同名方法的问题

内部类的作用 - 搜索结果 - 知乎 (zhihu.com)

1.4 JDK1.8新特性

主要是Lambda表达式和stream流,两个主要都是为了简化书写

Lambda表达式简化匿名内部类

stream流方便对集合一次性进行多次处理如过滤,所有元素单个处理,所有元素单个遍历,通过函数计算等

1.5 继承和多态

多态通常以有两种实现方法:子类继承父类、类实现接口

多态,实例化后只能用父类有的方法,子类特有的方法必须强转成子类后才能用,比如List list = new LinkedList<>(),List没有removeLast方法而LinkedList有,所以用不了,为什么要这么限制呢,你就做你自己的工作,防止把其他的方法暴露给你。而且可以重写父类的方法

最大的好处在于接口的灵活性:假如某一天我认为ArrayList的特性无法满足我的要求,我希望能够用LinkedList来代替它,那么只需要在对象创建的地方把new ArrayList()改为new LinkedList即可,其它代码一概不用改动。

如果是继承,那么父类子类的方法都能用,但是不能用父类的private方法,这点上多态反而重写后使用

1.6 异常

Exception异常是程序本身可以处理的异常,可以通过catch来进行捕获,又分为受检查异常(必须处理)和不受检查异常(可以不处理)

Error属于程序无法处理的错误,如Java虚拟机运行错误、虚拟机内存不够错误、类定义错误等,JVM一般会选择线程终止

除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常。

1.7 ==和equals

== 的作用:

基本类型:比较的就是值是否相同

引用类型:比较的就是地址值是否相同

equals 的作用:

引用类型:默认情况下,比较的是地址值,跟==效果一样

​ 但是意义不大,所以在一些类库中这个方法被重写了,如String、Integer、Date,重写为比较对象的成员变量值是否相同。

img

System.out.println(str1 == str2); // false
System.out.println(str1 == str3); // false
System.out.println(str2 == str3); // true
System.out.println(str1.equals(str2)); // true
  • ==:比较的是两个字符串内存地址(堆内存)的数值是否相等,属于数值比较;
  • equals():比较的是两个字符串的内容,属于内容比较。
1.8 抽象类和接口的区别

调用者使用动机不同,实现接口是为了使用他规范的某一个行为,自上而下

继承抽象类是为了使用这个类属性和行为,自下而上

例如:

猫、狗可以抽象成一个动物类抽象类,具备叫的方法;

鸟、飞机可以实现飞的接口,具备飞的行为,这里不能将鸟和飞机共用一个父类。接口的子类可以不存在任何关系。

接口不能定义成员变量,变量是公共的、静态的、最终的常量

抽象类除了不能实例化对象之外,类的其他功能依然存在,抽象类介于接口和具体类的中间

接口的设计目的,是对类的行为进行约束(更准确的说是一种“有”约束,因为接口不能规定类不可以有什么行为),也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无,但不对如何实现行为进行限制。对“接口为何是约束”的理解,我觉得配合泛型食用效果更佳。

而抽象类的设计目的,是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)。

1.9 基本类和包装类
  1. 包装类可以为null,基本类型不可以

  2. 包装类型可以用作泛型,基本类型不可以

  3. 基本类型比包装类型更高效,基本类型在栈中直接存储的具体数值,而包装类型则存储的是堆中的引用

    img

  4. 自动装箱和自动拆箱

    反编译后可知,自动装箱是通过Integer.valueOf()完成的,自动拆箱是通过Integer.intValue()完成的,好处是基本类型和引用类型直接运算

  5. 包装类可以在对象中定义更多的功能方法操作数据,比如说转成其他的进制,字符串

  6. 包装类继承了Number类,实现了Comparable接口

1.10 为什么重写equals还要重写hashCode

很可能某两个对象明明是“相等”,而hashCode却不一样。

if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))

两个所有属性都相等的对象,但是地址不同。没重写hashCode时,p.hash == hash 一定不相等。但是逻辑上这两个对象是相等的,并且equals也是相等的。

java是借助hashcode()方法和equals()方法来实现判断元素是否已经存在的

这就会导致,HashMap里面本来有这个key,但是你告诉我没有,导致了put操作成功。逻辑上是不符合规范的,get时取出来的也可能是自己另一个的value。

1.11 Object类中的方法

clone:浅拷贝,只有实现了Cloneable接口才可以调用该方法

主要是JAVA里除了8种基本类型传参数是值传递,其他的类对象传参数都是引用传递,我们有时候不希望在方法里将参数改变,这是就需要在类中复写clone方法。

getClass:获得运行时类型

toString:常用,一般子类都有覆盖

finalize:释放资源,由于无法确定该方法什么时候被调用,很少使用

equals:一般equals和==是不一样的,但是在Object中两者是一样的。子类一般都要重写这个方法

hashCode:该方法用于哈希查找,可以减少在查找中使用equals的次数

wait:

wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回

调用该方法后当前线程进入睡眠状态,直到以下事件发生

(1)其他线程调用了该对象的notify方法。

(2)其他线程调用了该对象的notifyAll方法。

(3)其他线程调用了interrupt中断该线程。

(4)时间间隔到了。

此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常

notify:该方法唤醒在该对象上等待的某个线程

nofifyAll:该方法唤醒在该对象上等待的所有线程

1.12 不可变类

概念:类实例一旦创建完后,就不能改变其成员变量值,基本类型、包装类和String类都属于不可变类,是线程安全的

如何设计

  1. 类声明为final,不可以被继承

  2. 所有成员变量定义为 private final

  3. 不提供改变成员变量的方法

  4. 通过构造器初始化成员,若构造器传入引用数据类型需要进行深拷贝(保护原来的数据)

    public final class MyImmutableDemo {  
        private final int[] myArray;  
        public MyImmutableDemo(int[] array) {  
            this.myArray = array.clone(); // right 
        }   
    }
    
  5. 在getter方法中不能返回对象本身,而是返回对象的拷贝。

final放在类上、成员变量上和方法上有什么不同

放在类上不能被继承,放在成员变量上成为常量,放在方法上可以被继承、重写,但是不能被重载

1.13 创建对象的几种方法
  1. new关键字,使用构造器

  2. 反射,也需要使用构造器

    当使用Class类里的newInstance()方法,调用的是无参构造方法。

    当使用java.lang.reflect.Constructor类里的newInstance方法,调用的是有参构造方法。

  3. Object类的clone方法,需要实现Cloneable接口,重写object类的clone方法,无论何时我们调用一个对象的clone方法,JVM就会创建一个新的对象,将前面对象的内容全部拷贝进去,用clone方法创建的对象并不会调用任何构造函数

  4. 反序列化,不会调用任何构造函数

1.14 IO流

IO流的分类

根据处理数据类型的不同分为:字符流和字节流

根据数据流向不同分为:输入流和输出流

字符流和字节流

字符流的由来: 因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。字节流和字符流的区别:

(1)读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。

(2)处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。

(3)字节流在操作的时候本身是不会用到缓冲区的,是文件本身的直接操作的;而字符流在操作的时候下后是会用到缓冲区的,是通过缓冲区来操作文件,我们将在下面验证这一点。

结论:优先选用字节流。首先因为硬盘上的所有文件都是以字节的形式进行传输或者保存的,包括图片等内容。但是字符只是在内存中才会形成的,所以在开发中,字节流使用广泛。

输入流和输出流

对输入流只能进行读操作,对输出流只能进行写操作,程序中需要根据待传输数据的不同特性而使用不同的流。

1.15 反射

不同阶段通过反射获取类的方法

image-20201023101812849

它允许程序在运行时(注意不是编译的时候)来进行自我检查并且对内部的成员进行操作

意义:

  1. 增加程序的灵活性,避免将程序写死到代码里,可以在程序运行过程中操作这些对象

    例:定义了一个接口,实现这个接口的类有20个,程序里用到了这个实现类的地方有好多地方,如果不使用配置文件手写的话,代码的改动量很大,因为每个地方都要改而且不容易定位,如果你在编写之前先将接口与实现类的写在配置文件里,下次只需改配置文件,利用反射(java API已经封装好了,直接用就可以用 Class.newInstance())创建对象就可完成。

  2. 代码简洁,提高代码的复用率,外部调用方便,解耦,提高程序的扩展性

    如果不用反射,接口是水果,实现类是具体的水果,那么我们如果再加一个西瓜类,就得在Factory里判断,每添加一个类都要修改一次Factory,但用了反射只用在调用的时候传入完整的类名就可完成。结果:用反射,修改一处代码;不用反射,修改两处代码。

        Fruit f = Factory.getInstance("cn.yonyong.reflection.testdemo.Apple") ;
        if(f!=null){ //判断是否取得接口实例
          f.eat() ;
    
    
  3. 对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法,那封装成private的意义是什么,所以其实反射在使用时,内部有安全控制,如果安全设置禁止了这些,那么反射机制就无法访问私有成员。

缺点:

  1. 性能问题,效率慢
  2. 内部暴露
1.16 引用类型

由高到低:强,软,弱,虚

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它:Object strongReference = new Object();

内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用对象来解决内存不足的问题,除非直接赋值为null

如果一个对象只具有软引用,则内存空间充足时,垃圾回收器不会回收它;如果内存空间不足了,就会回收这些对象的内存。虚拟机会尽可能优先回收长时间闲置不用软引用对象。对那些刚构建的或刚使用过的**"较新的"软对象会被虚拟机尽可能保留**,这就是引入引用队列ReferenceQueue的原因。应用场景是浏览器后退的缓存

弱引用软引用的区别在于:只具有弱引用的对象拥有更短暂生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定很快发现那些只具有弱引用的对象。

虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。

2. Java并发

2.1 并发锁

底层的锁只有两个,分别是“互斥锁”和“自旋锁”,其他锁都是在这两个锁上面衍生来的

两个线程竞争锁

互斥锁:没有竞争过的线程加入小黑屋,放弃cpu,阻塞等待,浪费时间且浪费cpu,适合运行线程持有锁时间比较长的业务

自旋锁:没有竞争过的线程不断自旋等待,继续争取锁,适合另一个线程运行比较短的场景

这两个锁只需要两种状态队列即可实现

img

可重入锁:获取了一个锁,在没释放锁的基础上还想获取同一把锁(同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁),但是由于锁被占用了只有释放才能重新获取,不实现可重入锁就会发生死锁,所以要加一个字段,看下加锁的线程是自己就可以获取锁了

img

读写锁:“读-读”这种业务是可以同时运行的,但是写不行,所以需要再增加一个属性,写锁只能一个一个线程持有,读锁可以多个线程

img

公平锁:老老实实排队,前面的线程获取锁

自旋锁和互斥锁在不同环境下优略性不同,比如竞争激烈和线程执行时间长,就适合使用互斥锁,反之使用自旋锁,所以对于什么环境下使用什么锁再分出了偏向锁、轻量级锁、重量级锁

偏向锁:一段同步代码一直被一个线程访问,那这个线程就会自动获取锁,降低获取锁的代价

轻量级锁(自旋锁):在偏向锁的情况下,被其他线程访问,就会升级为轻量级锁,其他线程就会通过自旋的方式获取锁,不会阻塞,提高性能

重量级锁:在轻量级锁的情况下,另一个线程自旋次数过多,就会进入阻塞,该锁碰撞为重量级锁,会让其他申请的线程进入阻塞,性能降低

分段锁:就是把锁的对象分成多段,每段独立控制,使得锁粒度更细,减少阻塞开销,从而提高并发性。像ConcurrentHashMap,容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率

2.2 synchronized关键字

如何理解

解决多个线程访问资源的同步性,保证其修饰的方法或代码块只有一个线程执行。在早期是重量级锁,因为java线程是映射到操作系统的原生线程上的,线程之间的切换需要从用户态转换到内核态,成本大。java6之后引入了大量优化,如自旋锁,轻量级锁等。

synchronized的使用

  • synchronized 关键字加到 static 静态方法和 synchronized(类.class) 代码块上都是是给 Class 类上锁。类锁是加载类上的,而类信息是存在 JVM 方法区的,并且整个 JVM 只有一份,方法区又是所有线程共享的,所以类锁是所有线程共享的。
  • synchronized 关键字加到实例方法上和synchronized(this)是给对象实例上锁。每个实例在 JVM 中都有自己的引用地址和堆内存空间,这时候,我们就认为这些实例都是独立的个体,很显然,在实例上加的锁和其他的实例就没有关系,互不影响了。
  • 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!

底层原理

synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

和lock的区别

这两个是Java提供的两种锁机制,Lock是一个接口,一般使用继承的ReentrantLock类,是Java写的控制锁的代码,其语义上与synchronized相同,二者都是可重入锁,但是提供了格外的功能。当业务结构复杂,如需要一些可定时、可中断、可沦陷的锁获取操作,或者希望使用公平锁就使用ReentrantLock,否则还是synchronized更好,synchronized是托管给JVM执行的。

2.3 volatile

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!

volatile关键字是线程同步的轻量级实现,性能更好,但是只能用于变量

volatile只能保证数据的可见性,及时通知其他线程,主物理内存的值已经被修改,但不能保证原子性

volatile主要是用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性

可见性底层

每个线程在将数据操作完store回主存之前,会加lock指令来锁定内存区域的缓存(缓存行锁定),根据MESI缓存一致性协议,总线通过侦听器发现数据被修改,会立即让其他线程工作内存中不一致的副本立即失效。等到当前线程将更改后的数据write回主存后,立即执行unlock指令。也是由读写屏障实现的,见下面2、3点

有序性底层

有序性由Java内存模型定义的「内存屏障」完成。内存屏障会提供3个功能:

1.它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;

2.它会强制将对缓存的修改操作立即写入主存;

3.如果是写操作,它会导致其他CPU中对应的缓存行无效。

JMM具备一些先天的有序性,通过Happens-Before原则就可以保证一定的有序性。

实际HotSpot虚拟机实现Java内存模型规范,汇编底层通过Lock指令来实现

volatile关键字不能保证原子性
当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去,这个写会操作会导致其他线程中的缓存无效。所以,如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。

举个栗子

一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。
线程A首先得到了i的初始值100,但是还没来得及修改,就阻塞了,这时线程B开始了,它也得到了i的值,由于i的值未被修改,即使是被volatile修饰,主存的变量还没变化,那么线程B得到的值也是100,之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中。根据可见性的原则,这个主存的值可以被其他线程可见。
问题来了,线程A已经读取到了i的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存,所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。

2.4 线程安全的三大特性

原子性 由synchronized保证

可见性 由synchronized和volatile保证

指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值

实际上,线程操作的是自己的工作内存,而不会直接操作主内存。如果线程对变量的操作没有刷写会主内存的话,仅仅改变了自己的工作内存的变量的副本,那么对于其他线程来说是不可见的,所以需要那两个关键字来保证。

在这里插入图片描述

有序性 由volatile保证,可以禁止指令重排

引申:happen before

在某些重要的场景下,这一组操作都不能进行重排序(前面一个操作结果对后序操作必须是可见的),于是JMM就提出了happen before这套规则,我们写的代码只要在这些规则下,就不会发生重排序

2.5 线程池的重要参数

ThreadPoolExecutor 3 个最重要的参数:

  • corePoolSize : 核心线程数定义了最小可以同时运行的线程数量。这是最小的线程数量,即使这些线程处于空闲状态,他们也不会销毁

    CPU密集型:核心线程数 = CPU核数 + 1

    IO密集型:核心线程数 = CPU核数 * 2

  • maximumPoolSize : 一个任务被提交到线程池以后,首先会找有没有空闲存活线程,如果有则直接将任务交给这个空闲线程来执行, 如果没有则会缓存到工作队列中,如果工作队列满了,当前可以同时运行的线程数量变为最大线程数(并不会直接填满),才会创建一个新线程, 然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。

  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。

其他参数

keepAliveTime:空闲线程存活时间,如果一个线程处于空闲,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁。

threadFactory:创建一个新进程时使用的工厂

handler:拒绝(饱和)策略,当工作队列中的任务已达到最大限制,并且线程池中的线程数量也达到做大限制,这时候如果有新任务提交进来,拒绝的处理策略。

  • ThreadPoolExecutor.AbortPolicy 抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy 调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy 不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy 此策略将丢弃最早的未处理的任务请求。
2.6 线程池的作用

创建和销毁线程所花费的时间和资源可能比处理的任务花费的时间和资源还多,线程池是为了提高线程的复用性以及固定的线程数量,降低资源消耗,提高线程的可管理型。

线程的生命周期非常短,所以线程池可以固定核心线程数量

提高响应速度,当任务到达时,任务可以不需要等线程创建就能立即执行

2.7 AQS和CAS

AbstractQueuedSynchronizer 抽象队列同步器,是要给抽象类,主要用来构建锁和同步器。ReenTrantLock等都是基于AQS的

AQS的原理:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9cmccgTA-1663937727218)(https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java 程序员必备:并发知识系统总结/CLH.png)]

AQS对资源的共享方式分为独占和共享,独占又分为公平锁和非公平锁

公平锁:按照线程在队列中的排队顺序,先到者先拿到锁

非公平锁:当线程要获取锁时,先通过两次CAS操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。如果两次CAS都不成功,那么后面和公平锁是一样的。相对来说非公平锁有更好的性能,吞吐量比较大,但是可能会导致在阻塞状态的线程长期处于饥饿状态

CAS(Compare & Set/Compare $ Swap):CAS操作包含三个操作数——内存位置(V)、预期原值(A)、新值(B)。

假设内存中的原数据V,旧的预期值A,需要修改的新值B

  • 比较 A 与 V 是否相等
  • 如果比较相等,将 B 写入 V
  • 返回操作是否成功

atomatic类底层 使用的CAS

乐观锁的实现方式主要有两种:CAS机制和版本号机制

2.8 i++如何保证线程安全
  1. 使用循环CAS+volatile,JUC下的atomic包提供线程安全的原子操作类,这些操作都是用CAS实现的,CAS是cpu指令原语,不可分割,配合volatile可见性和有序性就能保证线程安全了
  2. 使用锁机制,如ReentrantLock
  3. 使用 synchronized
  4. AtomicInteger(CAS乐观锁,效率更高)
2.9 创建线程的几种方式
  1. 继承Thread类,编写简单,直接this即可获取当前线程,不能再继承其他父类了
  2. 实现Runnable接口,与下面的实现接口一样,优劣与继承Thread相反,还有优点是多个线程可以共享一个target对象,非常适合多个相同线程来处理一份资源的情况
  3. 实现Callable接口,并结合Future实现,这两种方法比较推荐
  4. 通过线程池创建

本质上创建线程就只有一种方式,就是构造一个 Thread 类

2.10 创建线程池有哪几种方式

《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

Executors 返回线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OutOfMmoryError。
  • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

方式一:通过构造方法实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eGcxgnNL-1663937727218)(https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ThreadPoolExecutor构造方法.png)]

方式二:通过 Executor 框架的工具类 Executors 来实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qpg8VkEp-1663937727219)(https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/Executor框架的工具类.png)]

2.11 实现Runnable接口和Callable接口的区别

Runnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。Runnable 接口 不会返回结果或抛出检查异常,但是 Callable 接口 可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口 ,这样代码看起来会更加简洁。

2.12 ThreadLocal

是线程本地变量

操作他有三个方法:get、set和remove方法

同一个变量,在不同的线程中有不同的值,本身是不保存任何值

线程里面有一个threadlocalMap,key就是threadlocal(线程使用自身作为key),val就是threadlocal在这个线程里的值

内存泄漏问题:

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,如果线程没有被释放,或者执行完后放入线程池中,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法

ThreadLocal原理及引出的人生感慨 - 知乎 (zhihu.com)

三个主要场景

一:数据库连接

在我们平常的SpringWeb项目中,我们通常会把业务分成Controller、Service、Dao等等,也知道注解@Autowired默认使用单例模式。那有没有想过,当不同的请求线程进来后,因为Dao层使用的是单例,那么负责连接数据库的Connection也只有一个了,这时候如果请求的线程都去连接数据库的话,就会造成这个线程不安全的问题,比如线程在别的线程还没操完就直接close了,Spring是怎样来解决的呢?

在Dao层里装配的Connection线程肯定是安全的,解决方案就是使用ThreadLocal方法。当每一个请求线程使用Connection的时候,都会从ThreadLocal获取一次,如果值为null,那就说明没有对数据库进行连接,连接后就会存入到 ThreadLocal里,这样一来,每一个线程都保存有一份属于自己的Connection。每一线程维护自己的数据,达到线程的隔离效果。

如果一个请求中涉及多个 DAO 操作,而如果这些DAO中的Connection都是独立的话,就没有办法完成一个事务。但是如果DAO 中的 Connection 是从 ThreadLocal 中获得的(意味着都是同一个对象), 那么这些 DAO 就会被纳入到同一个 Connection 之下。互不干扰。

二:代替参数的显式传递

但是这个场景使用比较少,可以将做个参数封装为对象去传递,所以无所谓

三:保存用户信息

通常在用户登录后, 用户信息会保存在Session或者Token中。这个时候,我们如果使用常规的手段去获取用户信息会很费劲,拿Session来说,我们要在接口参数中加上HttpServletRequest对象,然后调用 getSession方法,且每一个需要用户信息的接口都要加上这个参数,才能获取Session,这样实现就很麻烦了。在实际的系统设计中,我们肯定不会采用上面所说的这种方式,而是使用ThreadLocal,我们会选择在拦截器的业务中, 获取到保存的用户信息,然后存入ThreadLocal,那么当前线程在任何地方如果需要拿到用户信息都可以使用ThreadLocal的get()方法 (异步程序中ThreadLocal是不可靠的)对于笔者而言,这个场景使用的比较多,当用户登录后,会将用户信息存入Token中返回前端,当用户调用需要授权的接口时,需要在header中携带 Token,然后拦截器中解析Token,获取用户信息,调用自定义的类(AuthNHolder)存 ThreadLocal中,当请求结束的时候,将ThreadLocal存储数据清空, 中间的过程无需在关注如何获取用户信息,只需要使用工具类的get方法即可。

2.13 让线程按顺序执行的方法

注意与线程安全的区别

  1. Thread.join()
  2. 使用单线程池 Executors.newSingleThreadExecutor()
  3. 使用volatile关键字修饰的信号量实现
  4. 使用Lock和信号量实现

3. Java集合

3.1 说一说HashMap

特点:顺序是不确定的,LinkedHashMap的顺序是按插入顺序来的,TreeMap的顺序是按键值升序

是非线程安全的,Collections的synchronizedMap方法可以使它线程安全,使用ConcurrentHashMap也行

是数组+链表+红黑树,一个个的键值对,初始有16对,主要使用get和put方法,想要存入节点,先计算这个key的hash值

在1.7中:index=HashCode(key)&(Length-1),默认Length-1=1111,其实就是获取hash值的后四位,这也是为什么初始大小是16位,如果index有元素了,使用头插法

在1.8中:优化了算法,是高位运算、取模运算,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销,如果有元素了,使用尾插法,防止多线程情况下变为环,超过8转为红黑树,将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树

几个字段,length:数组长度,loadFactor:负载因子,threshold:容纳键值对极限,size:实际键值对数量

threshold = length * Load factor,负载因子默认0.75,超过之后以2的n次方大小扩容

看源码过一遍流程

并发下的rehash会造成元素之间会形成一个循环链表,不过1.8之后解决了这个问题,但还是不建议在多并发的情况下使用HashMap,因为还存在数据丢失等问题,建议使用ConcurrentHashMap

在1.7中,首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问

在1.8中,取消了Segment分段锁,采用了CAS和synchronized来保证并发安全,数据结构跟HashMap1.8结构类似,synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍

红黑树:根节点和叶子节点是黑色,非严格的平衡二叉树,通过左旋右旋变色调整

3.2 有哪些集合是线程安全的

Vector:效率低,扩容一倍一倍扩,分配空间需要连续储存空间,只能在尾部进行插入删除,不建议使用

Stack继承Vector的

Hashtable:内部的方法都经过synchronized修饰,效率低基本被淘汰了

java.util.concurrent(JUC)包下的所有集合类

3.3 ConcurrentHashMap

在JDK1.7的时候,给数据加上分段锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。

JDK1.8的时候,使用Node数组+链表/红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作。如果头结点是空的,那么用CAS插入,否则使用synchronized

ConcurrentHashMap的key和value不能为null,有二义性问题,可能没有这个key或者这个value为null

HashMap的containsKey可以解决,如果key == null,对应hash值为0

因为ConcurrentHashMap是线程安全的,一般使用在并发环境下,你一开始get方法获取到null之后(有值就不用再判断了),再去调用containsKey方法,没法确保get方法和containsKey方法之间没有别的线程来捣乱,刚好把你要查询的对象设置了进去或者删除掉了。

3.4 ArrayList扩容机制

ArrayList是List的主要实现类,底层使用Object[]存储,适用于频繁的查找工作

初始大小是0,当有数据插入时,默认大小DEFAULT_CAPACITY = 10。

如果在创建时,可以指定initialCapacity为初始大小;ArrayList(initialCapacity)

扩容:

  1. 当添加元素时,如果元素个数+1> 当前数组长度 【size + 1 > elementData.length】时,进行扩容,扩容后的数组大小是: oldCapacity + (oldCapacity >> 1) 可以理解成1.5倍扩容
  2. 最后将旧数组内容调用Arrays.copyof(),里面是通过再调用System.arraycopy()方法(native修饰)进行复制,达到扩容的目的,此时新旧列表的size大小相同,但elementData的长度即容量不同(Arrays.copyOf() 方法对于基本数据类型和一维数组来说是深拷贝,对引用类型来说是浅拷贝)

为什么放大因子是1.5?

  1. 扩容容量不能太小,防止频繁扩容,频繁申请内存空间 + 数组频繁复制;
  2. 扩容容量不能太大,需要充分利用空间,避免浪费过多空间;
  3. 为了能充分使用之前分配的内存空间,最好把增长因子设为 1<k<2;
  4. 并且充分利用移位操作(右移一位),扩容后的数组大小是: oldCapacity + (oldCapacity >> 1),减少浮点数或者运算时间和运算次数;

4. JVM

4.1 GC
  1. 什么垃圾需要回收

    被引用链(GC Roots)直接或间接指向的对象

  2. 垃圾回收算法

    1. 标记清除,会产生很多内部碎片

    2. 复制,浪费了一半空间

    3. 标记整理,频繁移动

    4. 分代收集,融合了以上三种的优点

      新生代用复制(from,to必须有一块为空),老生代用标记清除和标记整理

    img

    Eden区满了就会gc,经历过一次gc没有回收的对象会进入到form或to,15次还没回收的进入到老生代,form和to交换,为了防止内存碎片

    大对象也会直接进入老生代

  3. GC分类

    1. Minor GC / Young GC,在年轻代中的Eden区被占满会触发,新生代对象朝生夕死,非常频繁,进入Suivivor区

    2. Full GC,指的是针对新生代、老年代、永久代/元空间的全体内存空间的垃圾回收,速度比Minor Gc慢10倍以上

      触发情况:System.gc()、可用内存不够

4.2 垃圾收集器
  1. Serial 收集器

    单线程的,没有线程交互的开销,新生代采用标记-复制,老年代采用标记-整理,适用于Client模式下的虚拟机

  2. ParNew收集器

    Serial收集器的多线程版本,适用于Server模式下的虚拟机,只有它能与CMS收集器配合工作

  3. Parallel Scavenge收集器

    也是多线程收集器,关注点是吞吐量,而CMS等更多关注用户线程的停顿时间

  4. Serial Old收集器

    Serial收集器的老年代版本呢,也是单线程的

  5. Parallel Old收集器

    Parallel Scavenge收集器的老年代版本

  6. CMS收集器

    非常注重用户体验,响应优先,第一次实现了让垃圾收集线程与用户线程基本上同时工作,仅用于老年代的收集,基于标记清除算法

    整个过程分为四个步骤

    初始标记:暂停所有其他线程,记录直接与root相连的对象,速度快

    并发标记:同时开启GC和用户进程,用一个闭包结构去记录可达对象,但并不能准确全部记录到,因为用户进程会不断更新引用域,无法保证实时性。这个阶段名称是“并发标记”,所以是不影响用户进程的

    重新标记:修正并发标记期间变动的那一部分对象的标记记录,时间比较短

    并发清除:开启用户进程,同时GC线程开始对未标记的区域做清扫

    优点:并发收集、低停顿

    缺点:1、对CPU资源敏感 2、无法处理浮动垃圾 3、所使用的标记-清除算法会导致大量空间碎片

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x8IZJa4k-1663937727220)(https://javaguide.cn/assets/CMS收集器.8a4d0487.png)]

  7. G1收集器,jdk1.9默认垃圾回收器

    面向服务器的,主要针对配置多处理器和大容量内存的机器,不仅满足GC停顿时间的要求,还具备高吞吐量的性能特征(全能)

    为什么叫G1?

    他把堆内存分割为很多不相关的区域,region大小相同物理上不连续,使用不同的region可以表示Eden,Serviver1…等,新生代老年代不需要物理上连续,可以通过Region的动态分配的方式实现逻辑上的连续,每次根据允许的收集时间,优先回收价值最大的Region,贪心

    对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象, 就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC。 G1的大多数行为都把H区作为老年代的一部分来看待。

    特点:

    1. 并发和并行:可以有多个GC线程同时工作,此时用户线程STW,还可以与应用程序交替执行,因此不会出现整个回收阶段完全阻塞的情况
    2. 分代收集:逻辑上属于分代垃圾收集器,将堆区域分为了Region,而且同时兼顾年轻代和老年代
    3. 空间整合:回收以Region为单位,Region之间是复制算法,但是整体上看是标记整理算法,都可以避免内存碎片,这种特性有利于程序的长期运行,分配大对象的时候不会因为无法找到连续内存空间而提前触发下一次GC。
    4. 可以建立可预测的停顿时间模型,使用者用参数可以明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒

    缺点:

    内存占用和程序运行时的格外负载都比CMS高,小内存上不占优势

    G1在回收内存后马上同时做合并空闲内存的工作,而CMS默认是在STW(stop the world)的时候做,在初始标记和重新标记阶段需要STW

    https://juejin.cn/post/7010034105165299725

  8. JDK1.8默认垃圾收集器是Parallel Scavenge和Old,提高了系统的吞吐量,如果重视效率,可以换为ParNew和CMS的组合,如果JDK版本高,也可以选择G1或者ZGC

4.3 GC Root在哪

特点:当前时刻存活的对象

哪些可以作为GC Root

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象

当对象不可达时,对象的finalize方法给对象一次垂死挣扎的机会,发生GC时会判断对象是否执行了finalize方法,如果未执行,则会先执行finalize方法,可以在此方法里将当前对象与GC Root关联,然后执行finalize方法之后GC会再次判断对象是否可达。(finalize方法只会执行一次,只有一次机会)

4.4 Java内存区域

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2epLBm1q-1663937727220)(https://javaguide.cn/assets/Java运行时数据区域JDK1.8.dbbe1f77.png)]

程序计数器:

  1. 通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理
  2. 在多线程的情况下,程序计数器用来记录当前线程执行的位置,切换回来的时候能够知道上次运行到了哪里

Java虚拟机栈:

线程私有,随着线程生命周期的创建和死亡,是JVM数据区域的一个核心,除了一些Native方法调用是通过本地方法栈实现的,其他所有的Java调用都是通过栈来实现的。

栈由一个个栈帧组成,每一次方法调用都会有一个对应的栈帧被压入栈中,调用结束后弹出,先进后出。

栈空间虽然不是无限的,但是如果程序调用陷入无限循环,就会导致栈压入太多,抛出StackOverFlowError

还可能会出现OutOfMemoryError错误,如果栈的内存大小可以动态扩展,虚拟机在动态扩展时无法申请到足够的内存空间

本地方法栈:

作用和虚拟机栈相似,虚拟机栈为虚拟机执行Java方法服务,而本地方法栈为虚拟机使用到的Native方法服务。在HotSpot虚拟机中和Java虚拟机栈合二为一。也会出现那两种错误。

  • java虚拟机栈用于管理java方法的调用,而本地方法栈用于管理本地方法的调用

**堆:**是所有线程共享的一块内存区域。此内存区域唯一的目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,分为新生代和老生代

堆最容易出现OutOfMemoryError错误

方法区:

当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。(非实例化的类和其中的方法,static变量改到堆了)

永久代不是Java堆内存的一部分。永久代存放JVM运行时使用的类。永久代同样包含了Java SE库的类和方法。永久代的对象在full GC时进行垃圾收集。

方法区在1.8中取消了永久代,改为了元空间,元空间使用的是直接内存。类似于永久代,元空间不属于JVM内存,直接使用本地内存,理论上可以无限制使用本地内推,也可以使用jvm调整参数大小

好处: 1. 永久代大小是有上限的,容易出现oom触发full gc 2. 永久代与老年代full gc同时进行的,变为元空间可以在不进行暂停的情况下并发释放类的数据

在元空间中保存的数据比永久代中纯粹很多,就是类的元数据,这些信息只对编译期或JVM的运行时有用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3fAr3WOx-1663937727221)(https://javaguide.cn/assets/method-area-implementation.68e9c9cd.png)]

运行时常量池:

存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)

字符串常量池:

主要目的是为了避免字符串的重复创建

JDK1.6的时候,运行时常量池和字符串常量池还有静态变量都在方法区中,1.7之后把静态变量和字符串常量池搬到了堆中,1.8把方法区改为了MetaSpace

4.5 变量和方法存放在哪

局部变量基本类型值和变量名存储在栈中,引用类型对象存储在推,对象的引用存储在栈
成员变量作为对象的属性,存放在堆里。
基本类型和引用类型的成员变量都在堆的这个对象的空间中,但是方法却是该类的所有对象共享的,对象使用方法的时候方法才被压入栈,方法不使用则不占用内存。对象实质上就是各种成员变量,不包括方法,因为方法存放在方法区
String是final修饰的,以一个个字符的方式存储在字符数组中,存储在字符串常量池,字符串常量池在堆中,在栈中引用

int a = 1;

a作为类的成员变量,存放于方法区中;1保存在堆(Heap)的实例中
a作为方法局部变量,存放于Java虚拟机栈(JVM Stacks)的局部变量表中;1也保存在栈内存中。

4.6 类加载器和加载过程

JVM中内置了三个重要的ClassLoader,除了BootstrapClassLoader其他类加载器均由Java实现且全部继承自java.lang.ClassLoader

  1. BootStrapClassLoader(启动类加载器):最顶层的加载类,负责加载%JAVA_HOME%/lib目录下的jar包和类
  2. ExtentionClassLoader(扩展类加载器):主要负责加载%JRE_HOME%lib/ext目录下的jar包和类
  3. AppClassLoader(应用程序(系统)类加载器):面向用户的加载类,负责加载当前应用classpath下的所有jar包和类
  4. 自定义ClassLoader

一个java文件从编码完成到最终执行,主要包括两个阶段:编译、运行

编译就是把我们写好的java文件,通过javac命令编译成字节码.class文件

运行就是把.class文件交给JVM执行

类加载过程就是JVM把.class文件加载进内存,并生成class对象的过程。JVM并不是一次性把所有的类都加载进内存,而是在第一个遇到需要运行的类时才会加载,且只加载一次

类加载主要分为三个部分:加载、链接和初始化。链接又可以细化为三个小部分

img

加载:把class字节码文件从各个来源通过类加载器装载入内存中。

验证:为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误。

准备:主要是为类变量(注意,不是实例变量)分配内存,并且赋予初值

解析:将常量池内的符号引用替换为直接引用的过程。

举个例子来说,现在调用方法hello(),这个方法的地址是1234567,那么hello就是符号引用,1234567就是直接引用。

初始化:只对static修饰的变量或语句进行初始化。

总结:

类加载过程只是类生命周期的一部分,在其前有编译为字节码文件,在其后使用完之后,还会在方法区垃圾回收的过程中卸载。

双亲委派:向上询问是否已加载,乡下逐层尝试是否可加载。向上是看是否看这个类是否已经被加载了,向下是确保安全性。

使用双亲委派模式的好处

  1. 可以避免类的重复加载
  2. 确保安全性,比如自己定义的String、Object类不会被加载

提问:new对象的过程中发生了什么

https://www.cnblogs.com/JackPn/p/9386182.html

4.7 java对象头的总体结构

在这里插入图片描述

1.对象头中的Mark Word(标记字)主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;

2.Klass Word是一个指向方法区中Class信息的指针,意味着该对象可随时知道自己是哪个Class的实例;

3.数组长度也是占用64位(8字节)的空间,这是可选的,只有当本对象是一个数组对象时才会有这个部分;

4.对象体是用于保存对象属性和值的主体部分,占用内存空间取决于对象的属性数量和类型;

5.对齐字是为了减少堆内存的碎片空间(不一定准确)。

4.8 JVM调优

目标值参考:

  • Heap 内存使用率 <= 70%;
  • Old generation内存使用率<= 70%;
  • avgpause <= 1秒;
  • Full gc 次数0 或 avg pause interval >= 24小时 ;

如何优化GC?

  1. 尽量不要创建过大的对象或数组。
  2. 通过虚拟机的 -Xmn 参数适当调大新生代的大小,让对象尽量在新生代中被回收掉。
  3. 通过 -XX:MaxTenuringThreshold 参数调大对象进入老年代的年龄,让对象尽量在新生代中被回收掉。
4.9 OOM

栈内存溢出:递归调用方法无结束语句 堆内存溢出:超大对象的创建,大量内存泄露堆积

5. Spring Boot

5.1 get和post

get:

  1. 一般从服务器获取资源,不导致服务器状态变化
  2. 请求参数放在url里面,只能进行url编码,有长度限制,会保留在浏览器记录里,不安全
  3. 浏览器会把http header和data一起发送出去,服务器响应200(返回数据)

post:

  1. 一般将实体提交到指定的资源
  2. 请求参数在request body里面,支持多种编码,没有长度限制(浏览器可能会限制),但是也不算安全,因为能被抓包,https才安全
  3. 浏览器发送header,服务器响应100,浏览器再发送data,服务器响应200

两者实质没有区别,都是同一个传输层协议

5.2 Spring事务

Spring支持两种方式的事务管理

  1. 编程式事务管理:通过类和方法手动管理事务,实际上很少使用
  2. 申明式事务管理:代码侵入性最小,是通过AOP实现(基于@Transactional的全注解方式使用最多)
5.3 常用注解

@SpringBootApplication是以下三个注解的集合

  • @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制,将所有符合自动配置条件的(@Confuguration配置)加载到IoC容器,仅此而已
  • @ComponentScan: 扫描被@Component (@Repository,@Service,@Controller)注解的 bean,注解默认会扫描该类所在的包下所有的类。
  • @SpringBootConfiguration:底层是Configuration注解,说白了就是支持JavaConfig的方式来进行配置(使用Configuration配置类等同于XML文件)

Spring Bean相关

@Autowired:自动带入对象到类中,被注入进的类同样要被Spring容器管理比如:Service类注入到Controller类中

我们一般使用@Autowired注解让Spring容器帮我们自动装配bean。要想把类标识成可用于@Autowired注解自动装配的bean的类,可以采用以下注解实现:@Component、@Repository、@Service、@Controller

是spring 的注解,默认按类型注入,有两个实现类会报错,可以结合@Qualifier注解指定使用哪个名称,(@Component(“name”))注解需要给自己起名字

@Resource 是J2EE注解,默认按照名称注入

@RestController是@Controller和@ResponseBody的合集,表示这是个控制器bean,并且是将函数的返回值直接填入HTTP响应体中,是REST风格的控制器

@Scope声明Spring Bean的作用域

@Configuration一般用来声明配置类,可以使用@Component注解代替,不过这个注解声明配置类更加语义化,@bean注解与它配合,不过放在方法上

@Requestparam接收请求头的参数,一般接受get,post也行,不支持批量插入,Content-Type : application/x-www-form-urlencoded 编码格式

@Requestbody接收请求体的参数,只能post,json,一般用于处理非 Content-Type: application/x-www-form-urlencoded编码格式的数据,比如:application/json、application/xml等

@Component和@Bean注解:两者的目的是一样的,都是注册bean到Spring容器中

1、@Component注解表明一个类会作为组件类,并告知Spring要为这个类创建bean。通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中。作用于类
2、@Bean注解通常是我们在标有该注解的方法中定义产生这个bean的逻辑。@Bean注解告诉Spring这个方法将会返回一个对象,这个对象要注册为Spring应用上下文中的bean。通常方法体中包含了最终产生bean实例的逻辑。作用于方法。

Spring帮助我们管理Bean分为两个部分

  • 一个是注册Bean(@Component , @Repository , @ Controller , @Service , @Configration),
  • 一个装配Bean(@Autowired , @Resource,可以通过byTYPE(@Autowired)、byNAME(@Resource)的方式获取Bean)。 完成这两个动作有三种方式,一种是使用自动配置的方式、一种是使用JavaConfig的方式,一种就是使用XML配置的方式。

那为什么有了@Compent,还需要@Bean呢?
如果你想要将第三方库中的组件装配到你的应用中,在这种情况下,是没有办法在它的类上添加@Component注解的,因此就不能使用自动化装配的方案了,但是我们可以使用@Bean,当然也可以使用XML配置。

都利用到了反射

https://javaniuniu.com/Annotation/ComponentAndBean

5.4 Spring的Bean生命周期

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HO0IhwH9-1663937727222)(https://segmentfault.com/img/remote/1460000040365134)]

Spring中Bean的生命周期是怎样的? - 知乎 (zhihu.com)

https://segmentfault.com/a/1190000040365130

5个阶段,创建前准备,创建实例化,依赖注入,容器缓存,销毁实例。

创建前准备:要从让下文和一些配置中去查找与解析bean相关的扩展实现,比如像init-method,容器在初始化bean的时候会调用的方法

第二阶段是创建实例阶段,这个阶段的主要作用是通过反射,去创建bean的实例对象,并且会扫描和解析bean声明的一些属性

第三阶段是依赖注入阶段,如果实例化的bean存在依赖其他bean对象的一些情况,则需要对这些依赖的bean进行对象注入,比如常见的@autoeired,以及setter注入的配置形式,同时在这个阶段会触发一些扩展的调用,比如说常见的扩展类beanpostProcessors,用来去实现bean初始化前,后的扩展回调,以及像beablnFactoryAware等等

第四个阶段是容器缓存阶段,主要作用是把bean保存在容器,以及spring的缓存中,到了这个阶段bean就可以被开发者使用了
这个阶段涉及到的操作,常见init-method,这个属性配置一些方法,或者这个阶段会被调用,以及像beanPostProcessors的后置处理器方法也会在这这个阶段被触发

第五个阶段销毁实例阶段,当spring的应用上下文被关闭的时候,这个上下文中所有的bean会被销毁,如果存在bean实现了像disposableBean接口,或者配置了destory-method属性的一些方法会在这个阶段被调用

5.5 SpringMVC原理

MCV是一种设计模式,Spring MVC是一款很优秀的MVC框架

https://www.jianshu.com/p/8a20c547e245

img

  1. 客户端(浏览器)发送请求,直接请求到 DispatcherServlet
  2. DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler
  3. 解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter 适配器处理。
  4. HandlerAdapter 会根据 Handler来调用真正的处理器开处理请求,并处理相应的业务逻辑。
  5. 处理器处理完业务后,会返回一个 ModelAndView 对象,Model 是返回的数据对象,View 是个逻辑上的 View
  6. ViewResolver 会根据逻辑 View 查找实际的 View
  7. DispaterServlet 把返回的 Model 传给 View(视图渲染)。
  8. View 返回给请求者(浏览器)
5.6 IoC

不是什么技术,是一种解耦的设计思想,目的是借助于“第三方”(Spring中的IOC容器)实现具有依赖关系的对象之间的解耦,IOC容器管理对象,你只管使用,从而降低代码之间的耦合度。

Spring IOC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。 IOC 容器负责创建对象,将对象连接在一起,配置这些对象,并从创建中处理这些对象的整个生命周期,直到它们被完全销毁。

IOC 的核心就是原先创建一个对象,我们需要自己直接通过 new 来创建,而 IOC 就相当于有人帮我们创建好了对象,需要使用的时候直接去拿就行

我们可以把IOC容器的工作模式看做是工厂模式的升华,可以把IOC容器看作是一个工厂,这个工厂里要生产的对象都在配置文件中给出定义,然后利用编程语言提供的反射机制,根据配置文件中给出的类名生成相应的对象。从实现来看,IOC是把以前在工厂方法里写死的对象生成代码,改变为由配置文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性。

控制反转怎么理解呢? 举个例子:“对象a 依赖了对象 b,当对象 a 需要使用 对象 b的时候必须自己去创建。但是当系统引入了 IOC 容器后, 对象a 和对象 b 之前就失去了直接的联系。这个时候,当对象 a 需要使用 对象 b的时候, 我们可以指定 IOC 容器去创建一个对象b注入到对象 a 中”。 对象 a 获得依赖对象 b 的过程,由主动行为变为了被动行为,控制权反转,这就是控制反转名字的由来。

这种就是说容器帮我们创建好了对象,我们需要使用的时候自己再主动去容器中查找,如:

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/application-context.xml");
Object bean = applicationContext.getBean("object");

依赖注入相比较依赖查找又是一种优化,也就是我们不需要自己去查找,只需要告诉容器当前需要注入的对象,容器就会自动将创建好的对象进行注入(赋值)。

DI(Dependecy Inject,依赖注入)是实现控制反转的一种设计模式,依赖注入就是将实例变量传入到一个对象中去。把底层类作为参数传入上层类,实现上层类对下层类的“控制”。

img

知乎 (zhihu.com)

如何注入的?

  • 基于属性注入
  • 基于 setter 方法注入
  • 基于构造器注入
5.7 自动配置原理

SpringBoot 定义了一套接口规范,这套规范规定:SpringBoot 在启动时会扫描外部引用 jar 包中的META-INF/spring.factories文件,将文件中配置的类型信息加载到 Spring 容器(此处涉及到 JVM 类加载机制与 Spring 的容器知识),并执行类中定义的各种操作。对于外部 jar 来说,只需要按照 SpringBoot 定义的标准,就能将自己的功能装置进 SpringBoot。

没有 Spring Boot 的情况下,如果我们需要引入第三方依赖,需要手动配置,非常麻烦。但是,Spring Boot 中,我们直接引入一个 starter 即可。比如你想要在项目中使用 redis 的话,直接在项目中引入对应的 starter 即可。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

自动装配可以简单理解为:通过注解或者一些简单的配置就能在 Spring Boot 的帮助下实现某块功能。

Spring Boot通过@EnableAutoConfiguration开启自动装配,通过SpringFactoriesLoader最终加载META-INF/spring.factories中的自动配置类实现自动装配

5.8 @Autowired和@Resource的区别
  • @Autowired 是 Spring 提供的注解,@Resource 是 JDK 提供的注解。
  • Autowired 默认的注入方式为byType(根据类型进行匹配),@Resource默认注入方式为 byName(根据名称进行匹配)。
  • 当一个接口存在多个实现类的情况下,@Autowired@Resource都需要通过名称才能正确匹配到对应的 Bean。Autowired 可以通过 @Qualifier 注解来显示指定名称,@Resource可以通过 name 属性来显示指定名称。
5.9 Mybatis中#和$的区别

最直接的区别:#相当于对数据加上单引号,$相当于直接显示数据(只讨论字符串类型)

#可以进行预编译、类型匹配等操作,可以防止sql注入,尽量多用#

$不进行类型匹配,直接替换,一般用于传入数据库对象,例如传入表名 ORDER BY ${columnName}

5.10 注解的本质

创建的注解是一个继承自Annotation的接口,里面每一个属性就是接口的一个抽象方法。

注解就像标签,是程序判断执行的依据。比如,程序读到@Test就知道这个方法是待测试方法,而@Before的方法要在测试方法之前执行

注解分为自定义注解、JDK内置注解和第三方注解(框架)。自定义注解一般要我们自己定义、使用、并写程序读取,而JDK内置注解和第三方注解我们只要使用,定义和读取都交给它们

5.11 Mybaits分页方式

数组分页,查到全部后再在service中截取,不推荐(逻辑分页)

sql分页,直接在sql语句里就分好,limit分页(物理分页)

使用PageHelper分页插件,或者MyBatis Plus分页(物理分页)

5.12 starter

starter 是一个启动器,一个入口,通过这个入口整合其他模块,不需要到处写配置或者加各种依赖
实现他需要 1. 引入模块所需的相关jar包 2.自动装配所需的配置到容器
官方命名:spring-boot-starter-xxx 自定义命名xxx-spring-boot-starter 自定义有点复杂

5.13 日志

日志分为操作日志(给用户看)和系统日志

注意事项

实战!日志打印的15个好建议 - 掘金 (juejin.cn)

5.14 AOP

应用场景:

记录日志,监控方法运行时间、权限控制、缓存优化(第一次调用查询数据库,将查询结果放入内存对象, 第二次调用, 直接从内存对象返回,不需要查询数据库 )、事务管理(调用方法前开启事务, 调用方法后提交关闭事务 )

如何编写一个AOP,基于注解

jdk动态代理为什么要实现接口,因为每个代理类已经继承了proxy类了,不能多继承

cglib动态代理会自动生成一个被代理类的子类,子类重写父类的所有非final修饰的方法,在子类去拦截父类所有方法的一个调用

5.15 Spring中的bean是线程安全吗

**「原型Bean」**对于原型Bean,每次创建一个新对象,也就是线程之间并不存在Bean共享,自然是不会有线程安全的问题。

**「单例Bean」**对于单例Bean,所有线程都共享一个单例实例Bean,因此是存在资源的竞争。

如果单例Bean,是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行**「查询」**以外的操作,那么这个单例Bean是线程安全的。比如Spring mvc 的 Controller、Service、Dao等,这些Bean大多是无状态的,只关注于方法本身。

但是如果Bean是有状态的 那就需要开发人员自己来进行线程安全的保证,最简单的办法就是改变bean的作用域 把 singleton 改为 protopyte, 这样每次请求Bean就相当于是 new Bean() 这样就可以保证线程的安全了。

有状态就是有数据存储功能

无状态就是不会保存数据

小结

  1. @Controller/@Service 等容器中,默认情况下,scope值是单例-singleton的,也是线程不安全的。
  2. 尽量不要在@Controller/@Service 等容器中定义静态变量,不论是单例(singleton)还是多实例(prototype)他都是线程不安全的。
  3. 默认注入的Bean对象,在不设置scope的时候他也是线程不安全的。
  4. 一定要定义变量的话,用ThreadLocal来封装,这个是线程安全的。
5.16 循环依赖

面试必杀技,讲一讲Spring中的循环依赖-阿里云开发者社区 (aliyun.com)

bean生命周期复杂,导致循环依赖的解决复杂

注入方式

注解属性注入,setter注入,构造器注入(三级缓存无法解决)

没有aop代理的情况下,二级缓存可以解决

img

https://zhuanlan.zhihu.com/p/84267654

5.17 bean的作用域

五种作用域中,request、session和global session三种作用域仅在基于web的应用中使用(不必关心你所采用的是什么web应用框架),只能用在基于web的Spring ApplicationContext环境。

img

6. 设计模式

6.1 单例模式

单例模式是在内存中只创建一次对象的设计模式,如果多次使用同一对象且作用相同的时候,就可以把这个对象设计为单例模式,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生

Spring中bean的默认作用域就是singleton的。

好处:

  • 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销;
  • 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。

有懒汉式和饿汉式两种类型

懒汉式是在程序使用该对象前,如果没有实例化就进行实例化,如果已经实例化了就直接返回该对象,关键是如何保证只创建一个实例化对象,需要加锁,普通直接在方法上加锁太慢了,优化的目标是:如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例

public static Singleton getInstance() {
	// if夹心饼干
    if (singleton == null) {  // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
        synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
            if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
                singleton = new Singleton();
            }
        }
    }
    return singleton;
}

链接:https://zhuanlan.zhihu.com/p/160842212

还有可能会出现指令重排,可以使用volatile防止,但是在高版本的jdk中已经解决好了

饿汉式是在类加载的时候就已经创建好了该单例对象,在获取的时候直接返回就行了,不会存在安全性和性能问题,如果对内存要求不高建议使用饿汉式

6.2 代理模式

代理模式就是使用代理对象来代替对真实对象的访问,这样就可以在不修改原目标对象的前提下提供格外的功能操作,扩展目标对象的功能,比如可以增加一些自定义操作。

静态代理:

比较死板,必须要一个实现接口的类,代理对象本身也要实现这个接口。每个目标对象对应一个代理对象,一改具改,在编译时就将接口,实现类和代理类变成了一个个class文件

动态代理:

JDK动态代理,如guide-rpc-framework

  1. 定义一个接口及其实现类;
  2. 自定义 InvocationHandler 并重写invoke方法,在 invoke 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;
  3. 通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;

只能代理实现了接口的类,CGLIB可以避免。但JDK效率更高

CGLIB动态代理,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理

  1. 定义一个类;
  2. 自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;
  3. 通过 Enhancer 类的 create()创建代理类;

如果目标对象实现了接口,则默认采用JDK动态代理,否则采用CGLIB动态代理。他是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为final类型的类和方法。

6.3 工厂模式

Spring使用工厂模式通过BeanFactory或ApplicationContext创建bean对象

  • BeanFactory :延迟注入(使用到某个 bean 的时候才会注入),相比于ApplicationContext 来说会占用更少的内存,程序启动速度更快。
  • ApplicationContext :容器启动的时候,不管你用没用到,一次性创建所有 bean 。BeanFactory 仅提供了最基本的依赖注入支持, ApplicationContext 扩展了 BeanFactory ,除了有BeanFactory的功能还有额外更多功能,所以一般开发人员使用 ApplicationContext会更多。
6.4 观察者模式

是一种对象行为模式,表示的是一种对象和对象之间具有依赖关系,当一个对象发生改变,这个对象所依赖的对象也会作出反应。比如Spring事件驱动模型就是观察者模型很经典的一个应用。比如添加商品的时候都需要重新更新商品索引。

6.5 装饰者模式

可以动态地给对象添加一些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。简单点儿说就是当我们需要修改原有的功能,但我们又不愿直接去修改原有的代码时,设计一个Decorator套在原有代码外面。其实在 JDK 中就有很多地方用到了装饰者模式,比如 InputStream家族,InputStream 类下有 FileInputStream (读取文件)、BufferedInputStream (增加缓存,使读取文件速度大大提升)等子类都在不修改InputStream 代码的情况下扩展了它的功能。

6.6 适配器模式

适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。

spring MVC中的适配器模式

在Spring MVC中,DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由HandlerAdapter 适配器处理。HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。

为什么要在 Spring MVC 中使用适配器模式? Spring MVC 中的 Controller 种类众多,不同类型的 Controller 通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet 直接获取对应类型的 Controller,需要的自行来判断

6.7 策略模式

7.计算机网络

7.1 从输入url到显示页面的过程

先是dns解析,有缓存查缓存,没有就dns服务器迭代+递归查询,本地DNS之间的查询方式是递归查询,找到ip地址,浏览器以一个随机端口(1024~65535)向服务器的web程序的80端口发起tcp连接请求,经过各种路由设备请求到达服务器端后,进入到网卡(MAC地址),然后进入到内核的TCP/IP协议栈,识别该连接请求,一层层剥开包,最终到达web程序,最终建立了TCP/IP连接,然后发起一个http请求,包括请求方法、请求头、请求正文,服务器也会返回一个HTTP响应,浏览器根据响应内容渲染到浏览器页面上,html,css,js,img

查询过程

img

7.2 http各个版本

http1.0 短连接,每次与服务器交互都需要新开一个连接

http1.1 持久化连接,建立一次连接,多次请求都由这个连接完成,还有缓存处理,断点续传功能

http2.0 多路复用,可以同时发起多重的请求-响应消息,服务器推送,头部压缩

HTTP1.0 定义了三种请求方法: GET, POST 和 HEAD 方法。
HTTP1.1 新增了六种请求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。

7.3 TCP流量控制与拥塞控制

TCP使用滑动窗口来进行流量控制,流量控制往往是指点对点的通信量的控制,即接收端控制发送端,它所要做的就是一直发送端发送数据的速率,以便接收端来得及接受。拥塞控制就是一个全局性的过程,设计所有的主机、路由器以及与降低网络传输性能有关的所有因素

为了更好地对传输层进行拥塞控制,有四种算法:慢开始、拥塞避免、快重传、快恢复

TCP协议要求发送方维护两个窗口:接收窗口和拥塞窗口,接收方总有足够大的缓存空间,所以把发送窗口等同于拥塞窗口cwnd

首先慢开始,拥塞窗口指数增长到慢开始门限ssthresh值然后开始改用拥塞避免算法线性扩大,如果发生拥塞就把ssthresh设置为此时的一半,cwnd重新设置为1,执行慢开始算法

快重传和快恢复是对满开始和拥塞避免算法的改进

快重传:发生网络拥塞,发送方收到3个冗余ACK报文,直接重传对方没有收到的报文段,而不必等报文段设置的重传计时器超时

快恢复:发送端收到3个冗余的ACK,直接把cwnd的值设置为ssthresh改变后的数值,直接线性增大

7.4 TIME-WAIT

time-wait开始的时间为tcp四次挥手中主动关闭连接方发送完最后一次挥手,也就是ACK=1的信号结束后,主动关闭连接方所处的状态。然后time-wait的的持续时间为2MSL

原因1:为了保证客户端发送的最后一个ack报文段能够到达服务器。因为这最后一个ack确认包可能会丢失,然后服务器就会超时重传第三次挥手的fin信息报,然后客户端再重传一次第四次挥手的ack报文。如果没有这2msl,客户端发送完最后一个ack数据报后直接关闭连接,那么就接收不到服务器超时重传的fin信息报(此处应该是客户端收到一个非法的报文段,而返回一个RST的数据报,表明拒绝此次通信,然后双方就产生异常,而不是收不到。),那么服务器就不能按正常步骤进入close状态。那么就会耗费服务器的资源。当网络中存在大量的timewait状态,那么服务器的压力可想而知。

原因2:在第四次挥手后,经过2msl的时间足以让本次连接产生的所有报文段都从网络中消失,这样下一次新的连接中就肯定不会出现旧连接的报文段了。也就是防止我们上一篇文章 为什么tcp是三次握手而不是两次握手? 中说的:已经失效的连接请求报文段出现在本次连接中。如果没有的话就可能这样:这次连接一挥手完马上就结束了,没有timewait。这次连接中有个迷失在网络中的syn包,然后下次连接又马上开始,下个连接发送syn包,迷失的syn包忽然又到达了对面,所以对面可能同时收到或者不同时间收到请求连接的syn包,然后就出现问题了。

7.5 TCP协议如何保证可靠传输
  1. 应用数据被分割成 TCP 认为最适合发送的数据块。
  2. TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
  3. 校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。
  4. TCP 的接收端会丢弃重复的数据。
  5. 流量控制: TCP 连接的每一方都有固定大小的缓冲空间,TCP 的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制)
  6. 拥塞控制: 当网络拥塞时,减少数据的发送。
  7. ARQ 协议: 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。
  8. 超时重传: 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。

tcp和udp的区别也包括上面的几点,另外,tcp面向字节流,只能点对点,udp面向报文

7.6 为什么要三次握手

两次会出问题,假如服务端ack丢了,客户端没收到服务端的确认报文,服务器已经准备好了接收数据,客户端不知道,这个时候客户端不会给服务端发数据,也会忽略服务端发送过来的数据,

三次就算出问题也没事,服务端在一段时间内没有收到确认ack报文的话就会重新进行第二次握手,也就是服务端会重发SYN报文段,客户端收到重发的报文段后会再次给服务端发送确认ack报文。

tcp设计机制的问题

7.7 GET和POST请求的区别

get一般取数据,也可以提交但是不建议,post一般提交数据

get参数放在url中,安全差,数据长度有限制,post请求数据放在body中,但是也不绝对安全,https才绝对安全

get请求只能进行url编码,post请求支持多种(multipart/form-data等)

对于 GET 方式的请求,浏览器会把 http header 和 data 一并发送出去,服务器响应 200(返回数据)表示成功;
而对于 POST,浏览器先发送 header,服务器响应 100, 浏览器再继续发送 data,服务器响应 200 (返回数据)

7.8 粘包

TCP是一个字节流的传输层通信协议,粘包就是由字节流切割和组装有误产生的

数据链路层MTU是1500,除去ip头和tcp头,MSS为1460byte,想要通过就必须切割,在TCP Recv Buffer中取走可能会取走不同快的导致解析数据错误

解决方法:加入特殊标志如头尾,头标志可以附带消息长度,由于数据可能与标志位的数据一样,还可以加一个校验字段

UDP是面向消息的协议,应用程序必须以消息为单位提取数据

8. 数据库

8.1 数据库的隔离级别

读未提交,读提交,可重复读和串行化,读未提交有三个问题会出现:脏读,不可重复读和幻读,读提交解决了脏读,不可重复度解决了可重复读,有next-key锁的可重复读(mysql)和串行化解决了幻读

读未提交:其他事务还没提交就能使用其他事务的数据,如果回滚了就会出现脏读

读提交:只读取别的事务提交过后的数据,但是读取其他事务中的数据,如果其他事务更新了,再次读取那个数据会改变为更新之后的值

可重复读:读取其他事务的数据,就算其他事务更新了那个数据,也不会更新,但是如果其他事务插入了同样行的数据,会以为没有更改成功

8.2 数据库的锁

乐观锁和悲观锁是两种并发控制的思想,其中乐观锁不是真正的锁,乐观锁会假定大概率不会发生冲突,所以执行过程中没有加锁,在提交更新的时候才会判断一下别人有没有更新这个数据,悲观锁就在访问处理数据的时候都加上排他锁(包括行锁和表锁),事务提交或回滚之后才释放锁,共享锁(读锁)也是悲观锁

分为行锁和表锁,行锁开销大,加锁慢,会出现死锁,冲突概率低,并发度高,表锁相反

行锁包括共享锁和排他锁,共享锁是允许多个事务读取一个资源,存在共享锁时任何其他事务都不能修改数据,排他锁是只允许被当前事务读和写,行锁算法有记录锁,间隙锁和临间锁(左闭右开)

表锁包括意向共享锁、意向排他锁

8.3 索引

常见的索引结构有 B树,B+树和Hash,在MySQL中,MyISAM和InnoDB引擎都使用B+Tree作为索引结构。但是两者的实现方式不一样,MyISAM引擎中叶节点的data域存放的是数据记录的地址,InnoDB表数据文件本身就是按B+Tree组织的一个索引结构,data域保存了完整的数据记录,这个索引的key是数据表的主键,因为无法同时把数据存放在两个不同的地方,所以一个表中只能有且有一个聚集索引。

img

img

MyISAM是非聚簇索引,data域存放的是数据记录的地址,InnoDB使用的是聚簇索引找到索引就找到了完整的数据记录

为什么 InnoDB 只在主键索引树的叶子节点存储了具体数据,但是其他索引树却不存具体数据呢,而要多此一举先找到主键,再在主键索引树找到对应的数据呢?

因为 InnoDB 需要节省存储空间。一个表里可能有很多个索引,InnoDB 都会给每个加了索引的字段生成索引树,如果每个字段的索引树都存储了具体数据,那么这个表的索引数据文件就变得非常巨大(数据极度冗余了)

MyISAM 直接找到物理地址后就可以直接定位到数据记录,但是 InnoDB 查询到叶子节点后,还需要再查询一次主键索引树,才可以定位到具体数据。等于 MyISAM 一步就查到了数据,但是 InnoDB 要两步,那当然 MyISAM 查询性能更高。

聚簇索引

数据按索引顺序存储,键值的逻辑顺序决定了表中相应行的物理顺序,找到包含第一个值的行后,便可以确保包含后续索引值的行在物理相邻处。具有唯一性,默认是主键。MyISAM没有聚集索引,都是二级索引

优点:速度很快

缺点:依赖于有序的数据,因为要在插入时排序,如果是字符串或者UUID这种数据,插入和查找的速度比较慢。更新代价大,还好对于主键索引来说,主键一般都是不可修改的。

非聚簇索引

将数据存储和索引分开的结构,索引结构的叶子节点指向了数据的对应行。存储指向真正数据行的指针。不过叶子结点也并不一定存放数据的指针,因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据

优点:更新代价较小

缺点:也依赖于有序的数据,最大的缺点是可能会二次查询(回表)。当查到索引对应的指针或者主键后,可能还需要根据指针或主键再到数据文件或表中查询。

主键索引:数据表的主键列使用的就是主键索引。是聚集索引(MyISAM没有主外键)

二级索引(辅助索引):二级索引的叶子节点存储的数据是主键。是非聚集索引

覆盖索引:如果查询的字段name正好建立了索引(联合索引),那么索引的key就是name的值,查到直接返回就行了,不需要再回表查询。

回表:索引查到的只是主键,还要通过这个主键再回表查其他的列。InnoDB引擎里自己建立的索引也是非聚集索引,同样是要回表的。

我们如果直接用主键查找,用的是聚集索引,能找到全部的数据。如果我们是用非聚集索引查找,如果索引里不包含全部要查找的字段,则需要根据索引叶子节点存的主键值,再到聚集索引里查找需要的字段,这个过程也叫做回表

MyISAM 的主键索引也需要回表, 因为它的主键索引的叶子节点存放的是指针

什么是索引回表,如何避免? - 知乎 (zhihu.com)

联合索引注意事项

应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。MySQL使用索引时需要索引有序,如建立了"name,age,school"的联合索引,索引的排序为: 先按照name排序,如果name相同,则按照age排序,如果age的值也相等,则按照school进行排序。

如果是>不一定走索引,如果发现全表扫描比较块可能就不走索引

如果条件和联合索引顺序不对,要看优化器,也可能走

8.4 Redis持久化

**快照持久化(默认持久化方式)RDB:**Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用

save 900 1           #900(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 300 10          #300(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 60 10000        #60(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

**只追加文件持久化AOF:**实时性更好,已经成为主流方案,

appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec  #每秒钟同步一次,显式地将多个写命令同步到硬盘(推荐,兼顾数据与写入性能)
appendfsync no        #让操作系统决定何时进行同步

还有AOF重写和Redis4.0对持久化机制的优化

8.5 Redis生产问题

缓存穿透:大量请求的key不在缓存中,导致请求直接到了数据库上,数据库也没有

最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。

1)缓存无效key,如果缓存和数据库都找不到某个key 的数据,就写一个到Redis中并设置过期时间,可以解决请求的key不频繁的情况,但是不能从根本上解决问题

2)布隆过滤器,将所有可能存在的数据存放到布隆过滤器中,使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值,根据哈希值在位数组中把对应的下标值置为1。判断一个元素是否存在于布隆过滤器中,也要对他进行相同的哈希计算,如果位数组中每个元素都为1,那么在布隆过滤器中。但是可能会出现哈希碰撞

缓存雪崩:缓存在同一时间大面积失效,请求直接落在数据库上,是更严重的缓存击穿

针对Redis服务不可用的情况:采用Redis集群或者限流避免同时出现大量请求

针对热点缓存失效的情况:设置不同的失效时间比如随机设置存储的失效时间,或者缓存永不失效

缓存击穿:Redis里原本有这个数据,但是过期了,请求落在数据库上(注意与缓存穿透的区别),解决方法是只设置热点事件永不过期

8.6 Redis底层实现

img

请问:Redis究竟有几种数据结构?分别有什么特点? - 知乎 (zhihu.com)

8.7 MVCC

锁是单版本控制,MVCC是多版本控制,为了实现高并发的数据访问,并通过事务的可见性来保证事务能看到自己应该看到的数据版本,只在读提交和可重复读隔离级别上产生,目的是读写冲突,不加锁,乐观锁或悲观锁可以解决写写冲突

MVCC(多版本并发控制)是如何实现“读已提交”和“可重复读”这两种隔离级别的。主要是版本链undo logRead View来实现的

MVCC其实主要包含三个概念:隐藏列,undo log,ReadView

隐藏列

涉及MVCC的有两个隐藏列:创建版本号(创建该行数据的事务id)和回滚指针

undo log

两个作用,一个是事务执行失败进行回滚,二是MVCC中对于历史数据版本的查看

当事务对数据行进行一次更新操作时,会把旧数据行记录在一个叫做undo log的记录中,在undo log中除了记录数据行,还会记录下该行数据的对应的创建版本号,然后将原来数据行中的回滚指针指向undo log记录的这行数据。然后再在原来数据表中进行一次更新操作,如果这次更新操作回滚了,那么就可以根据回滚指针去undo log中查找之前的数据进行复原

分为insert undo log:只在事务回滚时需要,提交后可以立即丢弃,因为只对事务本身可见

update undo log(主要):在快照读时也需要,不能随便删除,会对已经存在的记录产生影响

ReadView

查询操作呢?这才是实现不同隔离级别的关键地方

当进行查询操作时,事务会生成一个ReadView,ReadView是一个事务快照,准确来说是当前时间点系统内活跃的事务列表,也就是说系统内所有未提交的事务,都会记录在这个Readview内,事务就根据它来判断哪些数据是可见的,哪些是不可见

记录并维护系统当前活跃事务的ID(没有commit,当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大),是系统中当前不应该被本事务看到的其他事务id列表

Read View几个属性

  • trx_ids: 当前系统活跃(未提交)事务版本号集合。
  • low_limit_id: 创建当前read view 时“当前系统最大事务版本号+1”。
  • up_limit_id: 创建当前read view 时“系统正处于活跃事务最小版本号
  • creator_trx_id: 创建当前read view的事务版本号;

查询一条数据时,事务会拿到这个ReadView,去到undo log中进行判断。若查询到某一条数据,根据事务id:

  • db_trx_id < up_limit_id || db_trx_id == creator_trx_id(显示) 如果数据事务ID小于read view中的最小活跃事务ID,则可以肯定该数据是在当前事务启之前就已经存在了的,所以可以显示。 或者数据的事务ID等于creator_trx_id ,那么说明这个数据就是当前事务自己生成的,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示的。
  • db_trx_id >= low_limit_id(不显示) 如果数据事务ID大于read view 中的当前系统的最大事务ID,则说明该数据是在当前read view 创建之后才产生的,所以数据不显示。如果小于则进入下一个判断
  • db_trx_id是否在活跃事务(trx_ids)中
    • 不存在:则说明read view产生的时候事务已经commit了,这种情况数据则可以显示
    • 已存在:则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的。

读已提交和可重复读的实现

ReadView就是这样来判断数据可见性的。

那又是如何实现读已提交和可重复读呢?其实很简单,就是生成ReadView的时机不同。

对读已提交来说,事务中的每次读操作都会生成一个新的ReadView,也就是说,如果这期间某个事务提交了,那么它就会从ReadView中移除。这样确保事务每次读操作都能读到相对比较新的数据

而对可重复读来说,事务只有在第一次进行读操作时才会生成一个ReadView,后续的读操作都会重复使用这个ReadView。也就是说,如果在此期间有其他事务提交了,那么对于可重复读来说也是不可见的,因为对它来说,事务活跃状态在第一次进行读操作时就已经确定下来,后面不会修改了。

https://blog.csdn.net/SCUTJAY/article/details/104653599

8.8 如何保证Redis和数据库数据的一致性

Cache Aside Pattern(旁路缓存模式)是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。读直接在redis读,写的话采取先写后删除,认为这种情况下数据不一致概率比较小,因为缓存的写入速度是比数据库的写入速度快很多(数据库写要加锁),一般还是可以顺利删除脏数据缓存的,不要严格保证正确性可以使用这种方法,否则采取其他办法

读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存和数据库间的数据一致性问题。不管是先写数据库,再删除缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举个例子:

1.如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。

2.如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。或者删除数据速度太快,脏数据来之前删了个寂寞,概率小

在写库之后,删缓存之前,另一个读线程读了缓存的老数据,这种情况勉强能接受,只错一次,最终缓存和数据库的数据还是一致的,就怕更新完了还不一致

延迟双删,可以解决大多数极端情况,解决的是不同时期的脏数据问题,以上两种情况都能解决,延时解决的是先删后写脏数据的情况

但是预估时间外还可能不一致,最后删除redis缓存可能不成功(延迟双删的第二次删也是),因为不是原子操作

这个时候要重新删除,最好的方法是异步重试,把重试请求写道消息队列中,可以直接把一开始的删除缓存也写到消息队列中。写到消息队列也可能失败,最近比较流行的解决方案是:订阅数据库变更日志,再操作缓存。MySQL一条数据发生修改时,就会产生一条变更日志(Binlog),我们可以订阅这个日志,拿到具体操作的数据,然后再根据这条数据删除对应的缓存。

一:延迟双删策略

在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。具体步骤是:

1)先删除缓存

2)再写数据库

3)休眠500毫秒(根据具体的业务时间来定)

4)再次删除缓存。

二、设置缓存的过期时间

从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存

结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

但是我们思考延时双删策略,此策略只能保证最终一致性,保证了第二次删除缓存之后的数据均为新数据,那第二次删除缓存之前还是能够读到旧数据的,如果对于数据没有强一致性要求的话延时双删已经足够了,但是如果对于数据有强一致性要求延时双删显然就不满足条件了,这个时候我们进一步优化的话可以考虑加锁操作,在写更新时阻塞读操作,带来的影响就是可以保证强一致性,但是吞吐量会下降

如果必须要强一致性,最常见的方案是2PC、3PC、Raxos、Raft这一类的一致性协议,在更新操作完成之前不能有任何请求进来,可以通过加分布式锁实现,但是性能较差,不值得

缓存和数据库一致性问题,看这篇就够了 (qq.com)

8.9 Redis底层数据结构

Redis中hash、set、zset有多牛?从底层告诉你数据结构原理 - 知乎 (zhihu.com)

8.10 SQL优化
  1. 使用预编译语句进行数据库操作,相同语句可以一次解析多次使用,还可以解决动态SQL带来的SQL注入问题
  2. 避免数据类型的隐式转换,查询的字段是什么类型,等号右边的条件就写成对应的类型
    1. 当操作符左右两边的数据类型不一致时,会发生隐式转换
    2. 当 where 查询操作符左边为数值类型时发生了隐式转换,那么对效率影响不大,但还是不推荐这么做。
    3. 当 where 查询操作符左边为字符类型时发生了隐式转换,那么会导致索引失效,造成全表扫描效率极低。
    4. 字符串转换为数值类型时,非数字开头的字符串会转化为0,以数字开头的字符串会截取从第一个字符到第一个非数字内容为止的值为转化结果。
  3. 充分利用索引,最左前缀匹配
  4. 禁止使用SELECT *,禁止使用不含字段列表的INSERT
  5. 把子查询优化为join操作
  6. 避免使用JOIN关联太多的表
  7. 禁止使用order by rand()随机排序,会把所有符合条件的数据转载到内存中,消耗CPU和IO资源,推荐从程序中获取一个随机值

有很多种原因会导致索引不生效,很多情况是出现了计算与判断,如:or,like,±*/,!=,<>,is null,is not null

and 走所有,并不是所有的函数

8.11 三大范式

第一范式:属性的原子性,每个列不可再分

第二范式:以上基础上消除了部分依赖,如学分依赖课程号**,**姓名依赖与学号

第三范式:以上基础上消除了传递(函数)依赖,如(学号) → (学生)→(所在学院) → (学院电话)

8.12 数据库一些语句

内连接:显示内连接一般更好,使用JOIN + ON,隐式直接两个表 + WHERE

外连接:左外连接会显示左表所有内容,如果对应的右表列没有,则为null

自连接:也是内连接的一种,可以分为等值连接和自身连接

等值连接:查找两个表中连接字段相等的记录,比如A表有123,B表有123456,出来123

自身连接:和自己进行连接查询,给一张表取两个不同的别名然后附上连接条件,比如要查询与姓名某某同龄且籍贯也相同的学生信息

GROUP BY语句通常用于配合聚合函数(如 COUNT()、MAX() 等),根据一个或多个列对结果集进行分组,条件筛选一般选择HAVING,可以认为他是Set集合,每类只显示一个

8.13 为什么使用B+树作为索引

hash索引最快,找一个最好,如果是范围查询(where id > 3),需要把所有数据找出来加载到内存,然后再在内存里筛选筛选目标范围内的数据。但是这个范围查找的方法也太笨重了,没有一点效率而言。

二叉查找树,极端情况下会偏向一边

红黑树,依然会有“左右倾”趋势,而数据库中的基本主键自增操作,主键一般都是数百万数千万的,如果红黑树存在这种问题,对于查找性能而言也是巨大的消耗,我们数据库不可能忍受这种无意义的等待的

AVL树,会严格保持平衡,但是首先保持平衡比较耗时,最主要的问题是不能减少磁盘io,数据库查询数据的瓶颈在于磁盘 IO,如果使用的是 AVL 树,我们每一个树节点只存储了一个数据,我们一次磁盘 IO 只能取出来一个节点上的数据加载到内存里。所以我们设计数据库索引时需要首先考虑怎么尽可能减少磁盘 IO 的次数。磁盘 IO 有个有个特点,就是从磁盘读取 1B 数据和 1KB 数据所消耗的时间是基本一样的,我们就可以根据这个思路,我们可以在一个树节点上尽可能多地存储数据,一次磁盘 IO 就多加载点数据到内存,这就是 B 树,B+树的的设计原理了。

B树,时间复杂度O(h * logn) h是树高,n为节点关键词个数,尽可能少的io,支持范围查找

B+树格外的优点:

非叶子节点存储索引,一个节点可以存很多索引,使整个树高降低,叶子节点用单向链表串联起来,便于范围查找(各个页之间是通过双向链表连接的)

8.14 一些sql语句

inner join:求交集 union:求并集并去重

显式内连接和左右连接与on配合,where加更多限定,隐式使用where
对于左连接
1、 on条件是在生成临时表时使用的条件,它不管on中的条件是否为真,都会返回左边表中的记录。
2、where条件是在临时表生成好后,再对临时表进行过滤的条件。这时已经没有left join的含义(必须返回左边表的记录)了,条件不为真的就全部过滤掉。

having和where
where在分组前过滤,不可使用聚合函数,优先级高
having分组后过滤,可使用聚合函数,优先级低

最左前缀匹配,and走索引,like ‘1%’ 走,like ‘%班’ 不走

8.15 zset底层结构

zset的底层存储结构包括ziplist或skiplist,在有序集合保存的元素数量小于128个且长度小于64字节时,使用ziplist,其他时候使用skiplist

当ziplist作为zset的底层存储结构时候,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值

skiplist每个节点应该包含member和score两部分,插入的时候没有严格按照多少层设置高度,是随机的,新插入的时候也要先经历一个类似的查找过程,确认插入位置后在完成插入操作。根据score再比较
跳表的优势,范围查找很快,内存占用小,且插入和删除只需要修改相邻节点的指针

排行榜使用redis就可以使用zset

8.16 Redis的过期策略和内存淘汰策略

过期策略:

  1. 定时过期,费CPU
  2. 惰性过期,费内存
  3. 定期过期,随机扫描,折中

内存淘汰

在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请格外空间的数据 ,当内存不足以容纳新写入数据时

noeviction:新写入操作会报错

allkeys-lru:在键空间中,移除最近最少使用的key

allkeys-random:在键空间中,随机移除某个key

volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的key。

volatile-random。。。volatile-ttl

8.17 数据库信息查看

explain查看的是走没走索引,打开慢查询可以在数据库日志中找到慢查询语句

expain出来的信息有10列,分别是id、select_type、table、type、possible_keys、key、key_len、ref、rows、Extra

8.18 数据库字段

char长度不可变,定义一个char[10]和varchar[10],如果存进去的是‘tao’,那么char所占的长度依然为3,除了字符‘tao’外,后面跟7个空格,varchar就立马把长度变为3了

取数据的时候,char类型的要用trim()去掉多余的空格,而varchar是不需要的。

char速度快,长度固定,以空间换时间,varchar相反

char英文字符占一个字节,varchar占两个字节,中文都是两个字节

int(4),存储1的时候展示的就是0001,int后面代表的是宽度,实际占用空间永远为4字节

8.19 数据库语句优化

主要要做到三点

  • 最大化利用索引。
  • 尽可能避免全表扫描。
  • 减少无效数据的查询。
  1. 尽量避免在字段开头模糊查询,会导致数据库引擎放弃索引进行全表扫描 ——LIKE ‘%陈%’

  2. 尽量避免in和not in,会全表扫描,如果是相连的数据可以用between,不相连就没办法了

    如果in后面是子查询,可以使用exists代替in

  3. 尽量避免使用or,会全表扫描,可以拆分两条语句使用union代替or

  4. 尽量避免进行null值的判断,会全表扫描,可以给字段添加默认值0,对0进行判断

  5. 尽量避免在 where 条件中等号的左侧进行表达式、函数操作,会全表扫描,放右边

  6. 最佳左前缀匹配

  7. 避免隐式转换

  8. order by 条件要与 where 中条件一致,否则 order by 不会利用索引进行排序

    SELECT * FROM t where age > 0 order by age;  没有where不走索引
    
  9. 避免出现select *

  10. 多表关联查询时,小表在前,大表在后,第一张表会涉及到全表扫描

  11. 用where替换having,having只会在检索出所有纪律之后才对结果进行过滤,一般用于聚合函数的过滤,除此之外应该将条件写在where中,where后面不能使用聚合函数(AVG,COUNT,MAX,SUM)

  12. 调整where字句的连接顺序,应该把过滤数据多的条件放在前面

9.操作系统

9.1 进程通信的方式

1.共享内存

可以理解为甲和乙中间有一个大布袋,通过布袋交互。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。

2.消息传递(队列)

数据交互以格式化的消息为单位,分为直接通信和间接通信。甲告诉乙某些事情需要写信,通过邮差给乙,直接给乙就是直接,放在乙家门口的信箱就是间接

3.管道通信

是共享存储通信方式的升级版,共享存储只能允许单进程访问,而管道通信允许一边写入一边写出,不过也还是半双工通信,如果要实现父子进程双方互动通信,需要定义两个管道。匿名管道用于具有亲缘关系的父子进程间或者兄弟进程之间的通信,有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。

4.信号

用于通知接收进程某个事件已经发生

5.信号量

信号量是一个计数器,用于多进程对共享数据的访问,PV操作的就是信号量

6.套接字

此方法主要用于在客户端和服务器之间通过网络进行通信

线程间的通信方式:锁、信号量、信号

协程:又称为微线程,是一种用户态的轻量级线程,完全由用户控制,拥有自己的寄存器上下文和栈,切换的时候保存到线程的堆区,没有切换线程的开销,不需要锁,只需要判断状态就好了。

9.2 文件描述符

文件描述符,一般叫fd。linux万物皆文件,fd就是抽象之后的体现。linux中,我们很多操作都是靠着这个fd,具体来说,当我们想操作一个资源的时候,就会调用操作系统对应的接口,这个接口就会返回一个fd,后续我们就可以通过这个fd去操作这个资源

img

  1. 文件描述符就是一个整形数字
  2. 每个进程默认打开 0、1、2 (标准输入、标准输出、标准错误)三个文件描述符, 新的文件描述符都是从 3 开始分配
  3. 一个文件描述符被回收后可以再次被分配 (文件描述符并不是递增的)
  4. 每个进程单独维护了一个文件描述符的集合
9.3 三种常见I/O模型的区别

一个进程的地址空间划分为 用户空间(User space)内核空间(Kernel space )。像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。

当应用程序发起 I/O 调用后,会经历两个步骤(从这里说起):

  1. 内核等待 I/O 设备准备好数据
  2. 内核将数据从内核空间拷贝到用户空间。

BIO(阻塞同步)

同步阻塞 IO 模型中,应用程序用while循环不断向内核发起 read 调用,内核会立刻把是否读到了数据立即返回,直到内核把数据拷贝到用户空间。如果每个请求都需要新建一个线程来专门处理,那么在高并发的场景下,机器资源很快就会被耗尽

NIO(非阻塞同步)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ODgcw1k7-1663937727225)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb174e22dbe04bb79fe3fc126aed0c61~tplv-k3u1fbpfcp-watermark.image)]

是同步非阻塞的IO模型,相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。

AIO(非阻塞异步)

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

https://zhuanlan.zhihu.com/p/386745556

9.4 I/O多路复用

Java中的NIO也可以为加上IO多路复用技术的New IO,线程轮询地去查看一堆IO缓冲区中哪些就绪

IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间 -> 用户空间)还是阻塞的。

CPU单核在同一时刻只能做一件事情,所有需要对CPU进行时分复用,但是这样成本比较高。线程、进程创建成本,CPU切换不同线程、进程成本,多线程的资源竞争

IO多路复用可以在单线程/进程中处理多个事件流(主要优点),解决的本质问题是用更小的资源完成更多的事。多路复用是指使用一个线程轮询来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。

NIO主要有buffer、channel、selector三种技术的整合,通过零拷贝的buffer取得数据,每一个客户端通过channel在selector(多路复用器)上进行注册。服务端不断轮询channel来获取客户端的信息。channel上有connect,accept(阻塞)、read(可读)、write(可写)四种状态标识。根据标识来进行后续操作。所以一个服务端可接收无限多的channel。不需要新开一个线程。大大提升了性能。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8wJoXKno-1663937727225)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/88ff862764024c3b8567367df11df6ab~tplv-k3u1fbpfcp-watermark.image)]

有5种IO处理模型:阻塞IO、非阻塞IO、IO多路复用、信号驱动IO、异步IO

几种不同IO多路复用方案

select、poll、epoll

Netty是一个基于NIO的客户端服务器框架,编程很方便,使用它可以快速简单地开发网络应用程序。极大简化了TCP和UDP套接字服务器等网络编程,支持多种协议如FTP,SMTP,HTTP以及各种二进制和基于文本的传统协议

它可以作为RPC框架的网络通信工具、实现一个自己的HTTP服务器、即时通讯系统、消息推送系统

9.5 什么是select/poll/epoll

select/poll/epoll是一种机制。这种机制是来实现IO多路复用的。那么这种机制是怎么实现的,就是内核提供了一种方式来实现一个进程可以监听多个不同的描述符,一旦有描述符数据准备就绪,就通知进程过来读取数据。而select/poll/epoll就是来实现这个方式的三种不同的方法

**select方式的实现。**在一个或多个进程管理着不同的描述符,每个描述符都有唯一的标识。当一个或多个进程向内核发起了select的系统调用后,内核就开始准备数据,当数据准备好后,要返回给对应进程中对应的描述符时,select需要遍历所有进程中所有的描述符,找到对应的发起请求的那个描述符后通知进程来读取数据。由于每次都要把select管理的进程轮询一遍,时间复杂度就是我们所说的 O(N) 复杂度。在select方式中,系统规定了单个进程中能够打开的描述符最大上限是1024个。至于为啥是1024个,这里就不再说明了

举例来理解:还是上面提到的买奶茶的例子。当有不同的客户【进程】来买奶茶,同时每个客户还要帮各自的朋友们买【描述符】,不过每个客户最多只能帮1024个朋友买【上限1024个描述符】,多的就不行了。这时候这些客户把自己的身份证号,自己朋友的生份证号,还有排队编号写在了一张卡片上交给了店员。等到奶茶做好后,店员就在这一堆卡片中,根据排队编号找到对应的客户身份证号,再通知客户过来取奶茶,再把奶茶送到对应的朋友手中。在这个过程中,每次店员都要把每一张卡片看遍【轮询遍历】,这就是所谓的O(N)复杂度,很明显,这很耗时间

**poll方式的实现。**poll的实现过程其实和select是一样的,只不过poll方式没有select有1024个最大描述符的限制,用链表

**epoll方式的实现。**在epoll方式中,跟select和poll方式不同的是,当一个或多个进程向内核发起了epoll调用后,内核这个时候就给该进程的描述符注册一个回调函数,在内核准备好数据后,让对应的描述符的进程自己过来读取数据,每一次只需要通知一个进程就行了,这个时间复杂度就是 O(1) 复杂度,很明显,这比 O(N) 复杂度效率高的太多了

举例来理解:还是同样的买奶茶的例子。当这些不同的客户过来买奶茶时,不再是扔一堆卡片跟店员了,而是店员给每一个客户一个呼叫机【注册了一个回调函数】,当呼叫机对应的奶茶做好后,就通知客户【进程】过来拿奶茶,这时候店员的工作量就已经是最小的了,每一次只需要通知到一个客户就行,这样效率就高太多了

9.6 快表和页表

属于内存管理,页式管理是把主存分为大小相等的一页页的形式,页较小,调高了内存的利用率,减少的碎片,页式管理通过页表对应逻辑地址和物理地址。快表解决虚拟地址到物理地址的转换慢的问题,可以理解为Cache,相当于Redis和MySQL的关系

9.7 Linux查看系统资源的命令

top查看CPU使用情况,free查看内存。htop可以通过图形界面查看

df显示磁盘分区上可以使用的磁盘空间 -h以KB、MB、GB的单位来显示,可读性高

du显示每个文件和目录的磁盘使用空间 -h以KB、MB、GB的单位来显示,可读性高

其他常用命令

15 个常用高效率 Linux 命令汇总 - 知乎 (zhihu.com)

9.8 进程切换开销大在哪里

进程切换涉及到虚拟地址空间的切换(也就是页表)而线程切换则不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,无需开辟内存空间,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。

为什么切换页表很慢呢

关键就在快表(TLB联想寄存器)上。页表的切换实质上导致TLB的缓存全部失效,这些寄存器里的内容需要全部重写。而线程切换无需经历此步骤。

9.9 线程状态

有五种状态:新建,就绪,运行,阻塞,死亡

Running只能由Runnable进入

处于运行状态的线程在某些情况下,让出CPU并暂时停止自己的运行,进入 阻塞状态

当发生如下情况时,线程将会进入阻塞状态:

  1. 线程调用sleep()方法,主动放弃所占用的处理器资源,暂时进入中断状态(不会释放持有的对象锁),时间到后等待系统分配CPU继续执行;
  2. 线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;
  3. 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有;
  4. 程序调用了线程的suspend方法将线程挂起
  5. 线程调用wait,等待notify/notifyAll唤醒时(会释放持有的对象锁);

阻塞状态分类:

  1. 等待阻塞:运行状态中的 线程执行wait()方法,使本线程进入到等待阻塞状态;
  2. 同步阻塞:线程在 获取synchronized同步锁失败(因为锁被其它线程占用),它会进入到同步阻塞状态;
  3. 其他阻塞:通过调用线程的 sleep()或join()或发出I/O请求 时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕 时,线程重新转入就绪状态;

线程状态转换关系

线程有哪些状态?每个状态是什么意思?又是如何切换的? - 知乎 (zhihu.com)

java中的notify和notifyAll有什么区别? - 知乎 (zhihu.com)

锁池:

所有需要竞争同步锁(sycnchronized)的线程都会放在锁池中,某个对象的锁已经被其中一个线程得到,其他线程需要在这个锁池中进行等待,当前面的线程释放同步锁后,锁池中的线程去竞争同步锁,当某个线程获得同步锁已经其他所需资源(除cpu资源外)后会进入就绪队列,等待分配cpu资源运行.

等待池:

当我们调用了wait()方法后,线程会放在等待池中,等待池的线程是不会去竞争同步锁的,只有调用了notify()或者notifyAll()方法后等待池的线程才会开始竞争锁,notify()是随机从等待池中选出一个线程放到锁池,而notifyAll()是将等待池中的全部线程放到锁池当中.
sleep()和wait()的区别:

sleep是Thread类的静态本地方法,wait是object类的本地方法;

sleep方法是不会释放lock,但是wait会释放,而且wait会加入到等待队列中;

sleep就是把cpu的执行资格和执行权释放出去,一定时间内不再运行池线程,超时之后再取回cpu资源,参与cpu调度,获取到cpu资源后就可以继续运行,如果执行sleep()时,该线程有锁,sleep会带着这个锁进入冻结状态,即sleep该线程,与该线程竞争锁的其他线程也获取不到锁,从而不可能进入就绪状态.如果在sleep期间其他线程调用了interrupt()方法,那么这个线程会抛你出interruptexception异常,这点和wait是一样的.

sleep方法不依赖于同步锁,可以独立使用,而wait()需要依赖同步锁synchronized关键字;

sleep不需要被唤醒(超时后自动退出阻塞),但是wait需要被中断(需要其他线程notify唤醒);

sleep一般用于当前线程休眠,或者轮循暂停操作,wait则多用于多线程之间的通信;

sleep会让出cpu执行时间且强制上下文切换,而wait则不一定,wait后可能还有机会重新竞争到锁继续执行.

yield():

yield()执行后线程直接进入就绪状态,马上释放cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到cpu资源继续执行.

join():

join()执行后线程进入阻塞状态,例如在线程B中调用了A的join(),那么线程B会进入阻塞状态,直到线程A结束或者中断线程.

9.10 用户态与内核态

用户输入,会发出中断,CPU立即进入核心态,当前运行的进程暂停运行,并由操作系统内核对中断进行处理

“用户态→核心态”是通过中断实现的。并且中断是唯一途径。
“核心态→用户态”的切换是通过执行一个特权指令,将程序状态字(PSW)的标志位设置为“用户态”

中断分为内中断和外中断, 核心区别就是这个中断信号的来源, 内中断是本CPU运行这段代码段锁发出的, 外中断是其他CPU执行代码段发来的, 也就是与当前CPU执行的指令无关.

10. 分布式与中间件

10.1 SpringCloud,Dubbo,gRPC

gRPc是一个高性能开源RPC框架

Dubbo是rpc分布式框架,基于TCP协议

SpringCloud提供了一整套的解决方案,服务间的调用是通过http通信的,就相当于调用RESTFul接口

HTTP和RPC不是对等的概念,RPC包括接口规范+序列化反序列化+通信协议,HTTP+Restful规范+序列化和反序列化与之对等

10.2 CAP理论

img

P(分区容错性)是一定要满足的,其他两个只能满足其一。同时A和C是冲突的。若系统出现“分区”,系统中的某个节点在进行写操作。为了保证 C, 必须要禁止其他节点的读写操作,这就和 A 发生冲突了。如果为了保证 A,其他节点的读写操作正常的话,那就和 C 发生冲突了。如果网络分区正常的话(系统在绝大部分时候所处的状态),也就说不需要保证 P 的时候,C 和 A 能够同时保证。

常见的可以作为注册中心的组件有:ZooKeeper、Eureka、Nacos

ZooKeeper保证CP。Leader选举中或者半数以上机器不可用的时候服务就是不可用的

Eureka保证AP。不存在Leader节点,每个节点都是平等的,只有有一个节点能用就行了,但这个节点数据可能不是最新的

Nacos不仅支持CP也支持AP

10.3 JMS VS AMQP
对比方向JMSAMQP
定义Java API协议
跨语言
跨平台
支持消息类型提供两种消息模型:①Peer-2-Peer;②Pub/sub提供了五种消息模型:①direct exchange;②fanout exchange;③topic change;④headers exchange;⑤system exchange。本质来讲,后四种和 JMS 的 pub/sub 模型没有太大差别,仅是在路由机制上做了更详细的划分;
支持消息类型支持多种消息类型 ,我们在上面提到过byte[](二进制)

总结:

JMS是java的消息服务,JMS API是一个消息服务的标准或者说是规范,典型代表——ActiveMQ

AMQP是一个高级消息队列协议,兼容JMS,典型代表——RabbitMQ

如果并发量要求不是太高,RabbitMQ是首选,否则建议Kafka,并发能力很强

10.4 RabbitMQ

**组成:**由消息头和消息体组成,消息体是不透明的,消息头由一系列的可选属性组成,包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。RabbitMQ会根据消息头把消息发送给感兴趣的Consumer

**模式:*常用的有五种场景模式,前两个分别是简单模式和工作队列模式,不需要关注交换器(Exchange)

交换器类型

fanout:交换器把发送过来的消息路由到所有与它绑定的Queue中,速度最快

direct:把消息路由到那些Bindingkey与RoutingKey完全匹配的Queue中

topic:与上面相似,但是用的是通配符

优点:RabbitMQ 基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。其他三个都是 ms 级。

缺点:不能保证接受顺序

10.5 Kafka比较

其实并不算mq,只是可以当作mq用,是一个分布式流失处理平台

流平台具有三个关键功能:

  1. 消息队列:发布和订阅消息流,这个功能类似于消息队列,这也是 Kafka 也被归类为消息队列的原因。
  2. 容错的持久方式存储记录消息流: Kafka 会把消息持久化到磁盘,有效避免了消息丢失的风险。
  3. 流式处理平台: 在消息发布的时候进行处理,Kafka 提供了一个完整的流式处理类库。

RabbitMQ是基于主从架构实现高可用,kafka是分布式的

Kafka 仅仅提供较少的核心功能,但是提供超高的吞吐量,ms级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。唯一的一点劣势是可能消息重复消费,对数据准确性会造成及其轻微的影响

10.6 消息队列消息丢失怎么办

RabbitMQ

img

img

关闭RabbitMQ自动ACK,自己在程序里ack

Kafka

消费端丢失了数据:

唯一可能导致消费者弄丢数据的情况,就是你消费到了这个消息,然后消费者那边自动提交了 offset,让 Kafka 以为你已经消费好了这个消息,但其实你才刚准备处理这个消息,你还没处理,你自己就挂了,此时这条消息就丢咯。处理方法也是关闭自动提交offset,处理完之后自己手动提交

Kafka丢失了数据:

leader 所在 broker 发生故障,进行 leader 切换时,数据可能会丢失。可以设置参数,partition必须至少有两个副本

生产者会不会丢数据?

如果按照上述的思路设置了 acks=all,一定不会丢,要求是,你的 leader 接收到消息,所有的 follower 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试,重试无限次。

10.7 消息队列如何保证顺序

rebbitmq 消费者取队列的顺序不一,拆分多个 queue,每个 queue 一个 consumer

kafka单线程没问题,因为可以可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。

如果多线程从partition取可能乱了
写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性(另一种单线程方式)。

10.8 如何保证消息队列幂等性

什么是消息幂等?

当出现消费者对某条消息重复消费的情况时,重复消费的结果与消费一次的结果是相同的,并且多次消费并未对业务系统产生任何负面影响

常用方法

  1. 如果并发不高,可以先查再保存
  2. 业务表添加唯一约束条件(UNIQUE),不能分库分表
  3. 再添加一张消息消费记录表,表字段加上(UNIQUE),消费完之后就往表里插入一条数据,第二次保存的时候mysql会报错
  4. 如果系统是分布式,又做了分库分表,可以使用redis来做记录,把消息id存在redis里
  5. 如果并发量很高,可以使用redis或者zookeeper的分布式对消息id加锁,然后使用上面的几个方法进行幂等性控制
10.9 分库分表

分库:单个数据库拆分成多个数据库,将数据散落在多个数据库中

分表:单张表拆分为多张表,将数据散落在多张表内

如果数据库的查询QPS过高,就需要考虑拆库,通过分库来分担单个数据库的连接压力

如果单表数据量过大,当数据量超过一定量级后,无论是对于数据查询还是数据更新,在经过索引优化等纯数据库层面的传统优化手段之后,还是可能存在性能问题,既然数据量很大,那我们就来个分而治之,化整为零。这就产生了分表,把数据按照一定的规则拆分成多张表,来解决单表环境下无法解决的存取性能问题。

10.10 延迟双删

读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存和数据库间的数据一致性问题。不管是先写数据库,再删除缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举个例子:

1.如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。

2.如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。或者删除数据速度太快,脏数据来之前删了个寂寞,概率小

Cache Aside Pattern(旁路缓存模式)是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。采取先写后删除,认为这种情况下数据不一致概率比较小,因为缓存的写入速度是比数据库的写入速度快很多(数据库写要加锁),一般还是可以顺利删除脏数据缓存的,不要严格保证正确性可以使用这种方法,否则采取其他办法

一:延迟双删策略

在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。具体步骤是:

1)先删除缓存

2)再写数据库

3)休眠500毫秒(根据具体的业务时间来定)

4)再次删除缓存。

二、设置缓存的过期时间

从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存

结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

11. 开发问题

11.1 跨域

浏览器处于对安全方面的考虑,不允许跨域调用其他页面的对象,只有当协议、域名和端口都相同的时候才算是同一个域名,否则都认为需要做跨域处理

前端解决方案:

  1. 通过jsonp解决跨域(老方法,不要用)

    通常为了减轻web服务器的负载,我们把js、css、图片等静态资源分离到另一台独立域名的服务器上,在html页面中再通过相应的标签从不同域名下加载静态资源,而被浏览器允许。

  2. NodeJS服务器作为服务代理,前端发起请求到NodeJS服务器代理转发请求到后端服务器

后端解决方案

  1. nginx反向代理

    a->b访问不了,可以找个中间的服务器c,服务器之间不存在跨域问题

  2. CORS:跨域资源共享

    后端设置允许跨域:需要在过滤器里设置响应头,此时开发环境下如果前端是使用axios请求,并且没有自定义请求头则不需要设置任何代理就能正常请求了。

    如果前端设置跨域还要允许携带cookie,需要自己开启配置

11.2 开发数据规范

img

11.3 sql注入和xss注入

sql注入是把代码插入到与SQL命令串联在一起执行

var Shipcity;  
ShipCity = Request.form ("ShipCity");  
var sql = "select * from OrdersTable where ShipCity = '" + ShipCity + "'";  

假定用户输入以下内容

Redmond'; drop table OrdersTable--  

此时脚本将组成以下查询

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';drop table OrdersTable--'  

分号 (😉 表示一个查询的结束和另一个查询的开始。 双连字符 (–) 指示当前行余下的部分是一个注释,应该忽略

xss是注入恶意指令代码到网页,用户访问后会执行

11.4 加密方法

非对称加密

RSA:是现在最广泛的非对称加密方法

11.5 为什么用get set设置private

安全性:属性声明为private 利用get set封装,当只提供get或set的业务场景出现是才体现出安全性,避免出现不该修改的属性修改了,不可以访问的属性值get到了
规范性:所有的类都应该私有化属性变量,通过方法获取和赋值
可维护性:当某个业务有一天突然需要只读取男性的信息的时候,可以再get方法内添加条件判断,而不需要在众多的类对象.属性得到后进行判断,修改位置唯一也便于恢复业务场景

11.6 为什么单继承多实现

单继承是避免多个父类中的重复属性

多实现通过实现接口拓展了类的功能

若实现的多个接口中有重复的方法也没关系,因为实现类中必须重写接口中的方法,所以调用时还是调用的实现类中重写的方法。

若各个接口中有重复的变量,接口中,所有属性都是 static final修饰的,即常量由于JVM的底层机制,所有static final修饰的变量都在编译时期确定了其值,若在使用时,两个相同的常量值不同,在编译时期就不能通过。

11.7 cookei,session和token

http是一个无状态协议,这种无状态的的好处是快速。坏处是假如我们想要把www.zhihu.com/login.htmlwww.zhihu.com/index.html关联起来,必须使用某些手段和工具

客户端访问服务器的流程如下

  • 首先,客户端会发送一个http请求到服务器端。
  • 服务器端接受客户端请求后,建立一个session,并发送一个http响应到客户端,这个响应头,其中就包含Set-Cookie头部。该头部包含了sessionId。Set-Cookie格式如下,具体请看Cookie详解
    Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure]
  • 在客户端发起的第二次请求,假如服务器给了set-Cookie,浏览器会自动在请求头中添加cookie
  • 服务器接收请求,分解cookie,验证信息,核对成功后返回response给客户端

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hzdDHMAf-1663937727227)(https://segmentfault.com/img/bVbmYbQ?w=400&h=200)]

简而言之, session 有如用户信息档案表, 里面包含了用户的认证信息和登录状态等信息. 而 cookie 就是用户通行证

token在客户端一般存放于localStorage,cookie,或sessionStorage中。在服务器一般存于数据库中

token 的认证流程与cookie很相似

  • 用户登录,成功后服务器返回Token给客户端。
  • 客户端收到数据后保存在客户端
  • 客户端再次访问服务器,将token放入headers中
  • 服务器端采用filter过滤器校验。校验成功则返回请求数据,校验失败则返回错误码
11.8 部署项目

实习的后端部署,因为没有前端和数据库,比较简单,如果有前端用nginx

pom文件配置好jar包,打包jar文件,Dockfile文件编写,然后cmd命令制作后端镜像。

docker build . -t name:version

镜像制作好就可以跑了

docker run -p port:port name/id

push到镜像仓库,在服务器上也可以直接用

docker push name

docker pull name

11.9 通过日志定位线上问题

首先kibana从错误文件筛选错误信息
里面只有大致的问题,具体信息从logstash服务器查,记下时间和线程id(一个远程线程,一个本地线程)

zgrep 线程id 压缩文件名(日期)
warn或error是问题信息,上面的info是log打印出来的,包括方法,调用行数

11.10 什么情况下会导致OOM,如何排查

Java中什么情况会导致内存泄漏 - 编程语言 - 亿速云 (yisu.com)

JVM使用从GC root开始的可达性分析算法判断对象是否存活

  1. static字段,拥有与整个应用程序相匹配的生命周期
  2. 未关闭的资源
  3. 不正确的重写equals和hashCode
  4. 引用了外部类的内部类
  5. finalize方法
  6. 常量字符串
  7. ThreadLocal

如何排查OOM?

1、先查看应用进程号pid:ps -ef | grep 应用名

2、查看pid垃圾回收情况:jstat -gc pid 5000(时间间隔)

即会每5秒一次显示进程号为68842的java进成的GC情况

3、开启OOM快照:

-XX:+HeapDumpOnOutOfMemoryError(开启堆快照)

-XX:HeapDumpPath=C:/m.hprof(保存文件到哪个目录)

4、dump 查看方法栈信息:

jstack -l pid > /home/test/jstack.txt

5、dump 查看JVM内存分配以及使用情况

jmap -heap pid > /home/test/jmapHeap.txt

6、dump jvm二进制的内存详细使用情况 (效果同在Tomcat的catalina.sh中添加 set JAVA_OPTS=%JAVA_OPTS% -server -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/test//oom.hprof 此文件需要借用内存分析工具如:Memory Analyzer (MAT)来分析)

jmap -dump:format=b,file=/home/test/oom.hprof pid

11.11 如何排查死锁
  1. 通过jsp+jstack命令

    jps位于jdk的bin目录下,其作用是显示当前系统的java进程情况,及其id号

    使用jstack命令去查看该线程的dump日志信息

  2. jconsole,图形界面

11.12 如何排查CPU使用率过高
  1. 通过top命令查看内存、cpu及各进程的信息

  2. 使用ps命令查看对应Java进程服务

  3. 找出耗cpu的线程(进程中有多个线程)

  4. 获取到线程id后,通过JVM堆栈信息找到线程信息

    jstack - l 26999 >> 26999.txt
    
  5. 分析堆栈信息,通过https://fastthread.io分析日志数据。重点关注CPU consuming threads的日志情况

  6. 定位代码问题,如代码导致线程死循环

12. 场景题

12.1 如何在一亿个数中找到最大的一万个数

直接快排不建议

  1. 分治法

    将1亿数据分成100份,用多线程使用快排找到每一份中最大的10000个,再在剩下的数据里快速排序找到最大的10000个

  2. 最小堆算法

    首先读入前10000个数来创建大小为10000的最小堆,然后遍历后序的数字并于栈顶(最小)数字进行比较,如果比最小的数字还小就忽略,否则删除栈顶数组,把改数字加入重新排序。最后输出当前堆中的所有10000个数据

12.2 10G数据,数据包含信息和日期,只有1G内存,如何找到最近的数据

TopK

分别排序:根据内存1G,数据10G,我们将10G数据切分成10份,通过内存调用磁盘的方式,每1G进行排序,排序结束后,我们会得到10个有序的数据数组。
归并:多路归并过程可以使用败者树或最小堆。为方便起见我还是用最小堆吧,原理是一样的。
内存中开辟一个大小为10的最小堆,和一个缓冲区(小于1G,不要太小)。
取10份排序好的数据的首位进入最小堆。则最小的数位于堆顶,移除堆顶元素并写入缓冲区,然后从移除元素的元素所属数组中的下一位进入最小堆,在次移除堆顶进入缓冲区…直到缓冲区满,缓冲区回写磁盘,清空缓冲区,再次将数据置入最小堆…
直到10份数据全部写完,然后将最小堆的元素按顺序回写磁盘即可。

img

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值