标题## JVM内存模型/JAVA内存空间和垃圾回收机制
Java内存空间主要分为堆内存(Heap)和非堆内存(Non-Heap)。
- 堆内存:用于存放对象实例,几乎所有的对象实例都在这里分配内存。堆内存还可以细分为新生代(Young Generation)和老年代(Old Generation)。
- 非堆内存:包括方法区(Method Area)和程序计数器(Program Counter Register)、Java虚拟机栈(Java Virtual Machine Stacks)以及本地方法栈(Native Method Stacks)
- 新生代(Young Generation):存放新创建的对象。它又分为Eden区和两个Survivor区(S0和S1)。大多数对象在Eden区创建,当Eden区满时,会触发Minor GC(年轻代垃圾收集),将存活的对象复制到Survivor区。
- 老年代(Old Generation):存放存活时间较长的对象。当新生代中的对象晋升到老年代后,如果老年代空间不足,会触发Major GC(全局垃圾收集)或Full GC(完全垃圾收集),回收不再使用的对象空间。
- 方法区是存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据的地方。在Java 8及之后的版本中,方法区被实现为元空间(Metaspace),它是直接使用本地内存,而不是像方法区那样使用JVM的内存。
Jvm内存模型就是我们常说的jvm的内存,也成为Runtime data area(运行时数据区域)。
那整个JVM的作用是,首先通过编译器把Java代码转换成字节码,也就是.class文件,类加载器再把字节码加载到内存中,将其放在运行时数据区也就是JVM内存的方法区内,而字节码文件只是JVM的一套指令集规范而已,并不能直接交给底层操作系统去执行,因此那,需要特定的命令解析器执行引擎(excution engine),将字节码翻译成底层系统指令,再交给CPU去执行,而这个过程需要调用其他语言的本地库接口(native interface)来实现整个程序的功能。
那jvm的内存中,比较重要的就是堆栈,一般来讲,堆是存放对象实例和数组,栈存放的是局部变量喔,一般堆空间远大于栈,从线程可见的角度来比较,堆是线程共享的区域,栈是线程私有的,他的生命周期和线程相同。
jvm的垃圾回收机制
Java使用垃圾回收器(Garbage Collector)自动管理内存,回收不再使用的对象占用的内存空间。垃圾回收器通过追踪对象的引用关系,找出不再被引用的对象(即垃圾对象),然后释放这些对象的内存空间。垃圾回收器的具体实现取决于JVM的实现和配置。
一般情况下,新创建的对象都会分配到Eden区,一些特殊的大的对象分配到Old区。
上面说了创建对象时jvm的操作流程,那么在进行垃圾回收的时候,首先要先找到是垃圾的对象,常用的算法有引用计数法和可达性分析。
引用计数法的意思是说对于某个对象而言,只要应用程序中有该对象的引用,就说明该对象不是垃圾,如果一个对象没有任何指针对其引用,他就是垃圾。但这种算法有一个问题就是相互引用问题,导致永远不能被回收。
可达性分析这种算法是从根上引用出一条单项的引用链,而在这个单向的引用链之上的对象,我们就称之为GC的可达对象,不在引用链上的对象,我们称之为垃圾。
当确定了一个对象为垃圾之后,接下来要考虑的就是回收,那也有相应的算法,也可以理解为垃圾回收的方法论,常见的垃圾收集算法有标记-清除,标记-复制,标记-清楚-整理。
标记-清楚,就是找到内存中所有存活对象,并且把他们都标记出来,然后清除掉没有被标记,也就是需要回收的对象,释放出相应的内存空间。这个算法,首先是标记和清除这两部的效率都不高,都比较耗时,其次是会产生大量的内存碎片,造成不连续空间,会导致在分配大对象时因为找不到足够空间而触发另一次垃圾回收动作。
标记-复制算法,是将内存划分为两块相等的区域,每次只使用其中一块,当其中一块内存使用完了,就将还存活的对象复制到另外一块上面,然后把已经使用过的内存空间一次性清除掉。那标记复制算法的缺点是空间利用率低。
标记-清除-整理。标记过程和标记-清楚算法一样,不同点在于,标记之后不是直接清楚,而是让所有存活对象都像一端移动,然后清理掉其他的内存。
上面这三种算法,在堆内存中,Young区使用的是复制算法,Old区标记清除或者标记整理。
如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
serial 单线程收集器,拥有很高的单线程收集效率
serial-old serial收集器的老年代版本,也是一个单线程收集器
parNew 可以理解为serial的多线程版本
parallel Scavenge 也是多线程收集器,但是比parNew更注重系统吞吐量,吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间),比如虚拟机总共运行了100分钟,垃圾收集时间用了1分钟,吞吐量=(100-1)/100=99%。
若吞吐量越大,意味着垃圾收集的时间越短,则用户代码可以充分利用CPU资源,尽快完成程序的运算任务
parallel Old parallel Scavenge 的老年代版本
CMS 是最短回收停顿时间垃圾收集器
常量池的分类
可以分为三类:静态常量池,运行时常量池,字符串常量池。
静态常量池是相对于运行时常量池来说的,静态常量池一般存放的是,类、接口、方法、字段相关的描述信息,在类被加载到jvm内村时,会将静态常量池加载到内存,也就是运行时常量池。
那运行时常量池就是静态常量池被加载到内存后的内存区域,也就是真正的把文件的内容落实到jvm内存了。
还有就是字符串常量池,字符串常量池的设计理念呢,是因为字符串是最常用的数据类型,为了减小内存的开销,专门为其开辟了一块内存区域,用于存放,JDK1.7以后,字符串常量池是位于heap堆中,
类加载
- 什么是类加载器?
类加载器是一个负责加载类的 Java 运行时系统的子系统。它负责查找字节码文件,加载类文件到方法区,并创建一个 java.lang.Class 类的实例。
2.类加载器有哪些?
类加载器主要有以下几种:
- 启动类加载器(Bootstrap ClassLoader):负责加载 Java 的核心类库。
- 扩展类加载器(Extension ClassLoader):负责加载 Java 的扩展类库。
- 系统类加载器(System ClassLoader)或应用类加载器:负责加载用户类路径上的类库。
- 用户自定义类加载器:用户可以通过继承 java.lang.ClassLoader 类来创建自定义类加载器。
3.类加载过程是什么?
类加载过程主要分为三个阶段:
- 加载(Loading):查找和导入类文件。
- 链接(Linking):可以细分为验证、准备和解析三个阶段。
- 验证(Verification):确保被加载的类文件符合 Java 语言的要求。
- 准备(Preparation):为类变量分配内存并设置默认初始值。
- 解析(Resolution):将符号引用转换为直接引用。
- 初始化(Initialization):对类变量进行初始化,执行静态代码块。
4.如何自定义类加载器?
自定义类加载器需要继承 java.lang.ClassLoader 类,并覆盖 findClass
方法
5.JVM类加载机制的三种特性
全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入。
父类委托,“双亲委派”是指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。
双亲委派”机制加载Class的具体过程是:
- ClassLoader先判断该Class是否已加载,如果已加载,则返回Class对象;如果没有则委托给父类加载器。
- 父类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则委托给祖父类加载器。
- 依此类推,直到始祖类加载器(引用类加载器)。
- 始祖类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的子类加载器。
- 始祖类加载器的子类加载器尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的孙类加载器。
- 依此类推,直到源ClassLoader。
- 源ClassLoader尝试从其对应的类路径下寻找class字节码文件并载入
双亲委派”机制只是Java推荐的机制,并不是强制的机制。
我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就应
该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法。
缓存机制,缓存机制将会保证所有加载过的Class都将在内存中缓存,当程序中需要使用某个Class时,类加载器先从内存的缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效.对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用
Java进程、线程、多线程
进程:一个独立的正在运行的程序
线程:一个进程最基本的执行单位,执行路径
多进程:在操作系统中,同时运行多个程序
多线程:在一个进程中,或者说同一个应用程序中,同时运行多个线程
一个进程如果有多个执行路径,则成为多线程程序,一个线程可以理解为一个进程的子任务。
如何使用线程
创建线程的四种方式
1.继承thread类,重写run方法
2.实现runnable接口,重写run方法
3.实现callable接口,重写run方法(可以生命抛出异常,且有返回值)
Callable callable = new MyCallable();
FutureTask futureTask = new FutureTask<>(callable); // 获取一个线程 肯定是要先创建一个Thread对象 futureTask本质上是Runable接口的实现 Thread t1 = new Thread(futureTask);
System.out.println(“main方法start…”);
t1.start(); // 本质还是执行的 Runable中的run方法,只是 run方法调用了call方法罢了
4.线程池方式创建
对多线程编程的了解,以及线程池的了解
就回答上面两个问题,再加
对于多线程编程的了解
使用CompletableFuture:Java 8引入了CompletableFuture类,它提供了异步编程的简化方式。你可以通过CompletableFuture的supplyAsync()或runAsync()方法来异步执行任务,并通过thenApply()、thenAccept()等方法处理异步任务的结果
线程池的核心参数
corePoolSize:核心线程数 (线程池内部运行起来之后,最少有多少个线程等活。 核心线程是懒加载 )
maximumPoolSize:最大线程数 (当工作队列堆满了,再来任务就创建创建非核心线程处理。)
keepAliveTime:最大空闲时间 (默认非核心线程,没活之后,只能空闲这么久,时间到了,干掉)
unit:空闲时间单位(上面时间的单位)
workQueue:工作队列 (当核心线程数足够后,投递的任务会扔到这个工作队列存储。LinkedBlockingQueue)
threadFactory:线程工厂(构建线程的,根据阿里的规范,线程一定要给予一个有意义的名字,方便后期排查错误)
handler:拒绝策略 (核心数到了,队列满了,非核心数到了,再来任务,走拒绝策略……)
线程池的执行原理/流程
任务扔到线程池之后,先查看核心线程数到了没,没到就构建核心线程去处理任务。
2、如果核心线程数到了,那就将任务扔到工作队列排队。
3、任务扔到工作队列时,工作队列满了,满了就尝试创建非核心线程去处理任务。
4、如果非核心线程创建失败(到最大线程数了)了,那就执行拒绝策略
常见的拒绝策略有哪些
AbortPolicy:扔异常。
DiscardPolicy:任务直接丢弃。
CallerRunsPolicy:谁投递的任务,谁自己处理。
DiscardOldestPolicy:将队列中排在最前面的任务干掉,尝试将自己再次投递到线程池。
面向对象和面向过程的区别
面向过程:是分析解决问题的步骤,然后用函数把这些步骤一步步地实现,然后在使用的时候调用即可。
面向对象:是把构成问题的事务分解成各个对象,而建立对象的目的也不是为了完成一个个步骤,而是为了描述某个事物在解决整个问题的过程中所发生的行为。面向对象有封装、继承、多态的特性,所以易维护、易复用、易扩展。可以设计出低耦合的系统。但从性能上来讲,比面向过程要低。
重载和重写的区别
重写:重写就是子类把父类原先有的方法再重新写一遍,子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以在方法名,参数列表,返回类型都相同的情况下,对方法体进行重写或修改,这就是重写。但要注意子类函数的方法修饰权限不能少于父类的,且重写方法一定不能抛出新的异常或者比被重写方法声明更加宽泛的检查异常。
重载:在同一个类中,同名的方法如果有不同的参数列表,即参数类型不同,参数个数不同,甚至参数顺序不同,则视为重载,重载和返回类型无关,重载是一个类中多态性的表现,也称为编译时的多态性的体现
Java的三大特性
JAVA有三大特性,分别是:封装、继承和多态。
1、封装:面向对象的封装就是把描述一个对象的属性和行为的代码封装在一个类中,有些属性是不希望公开的,或者说被其他对象访问的,所以我们使用private修饰该属性,使其隐藏起来;类中提供了方法(用public修饰),常用的是get、set方法,可以操作这些被隐藏的属性,其他类可以通过调用这些方法,改变隐藏属性的值!封装是保证软件部件具有优良的模块性的基础,封装的目标就是要实现软件部件的“高内聚、低耦合”,防止程序相互依赖性而带来的变动影响。在面向对象的编程语言中,对象是封装的最基本单位,面向对象的封装比传统语言的封装更为清晰、更为有力。
2、继承:在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,使用extends关键字实现继承;子类中可以加入若干新的内容,或修改原来的方法使之更适合特殊的需要,这就是继承。继承是子类自动共享父类数据和方法的机制,这是类之间的一种关系,提高了软件的可重用性和可扩展性。
3、多态:多态就是在声明时使用父类,在实现或调用时使用具体的子类;即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性,多态增强了软件的灵活性和扩展性。这里可以举个例子,比如声明时使用的是动物类,调用时传递的是一个猫类(动物类的子类)的对象,具体执行父类里动物——吃的方法时,实际执行的是猫——吃的方法
抽象类和接口的基本概念
抽象类:如果多个类中包含相同的行为,但行为发出的动作不一样,这时可以进行上层的抽象,抽象出一层功能定义,即抽象方法,但没有对应的功能实现。抽象方法包含在被 abstract
修饰的类中即抽象类。它具有如下特点
- 被
abstract
修饰的方法称为抽象方法,抽象方法只有方法声明没有方法体- 注意:抽象类是为了继承重写抽象方法的,所以抽象类不能用
final
修饰。同时外部抽象类不允许使用 static 声明,而内部的抽象类可以使用 static 声明(继承的时候使用“外部类.内部类”的形式表示类名称)
- 注意:抽象类是为了继承重写抽象方法的,所以抽象类不能用
- 抽象类可以包含属性、方法、构造方法,但构造方法不能用来实例化对象,只能被子类调用
- 抽象类中的构造方法,其存在目的是为了抽象类的属性初始化
- 注意:子类对象实例化时,先执行父类构造器,再执行子类构造器
- 抽象类不能被实例化,只能被继承
- 包含抽象方法的类一定是抽象类,但抽象类不一定包含抽象方法,抽象类还可以包含普通方法
- 抽象方法的权限修饰符只能为
public、protected、default
,默认情况下为 public - 一个类继承于一个抽象类,则子类必须实现抽象类的抽象方法,如果子类没有实现父类的抽象方法,那子类必须定义为抽象类
接口:Java接口可以看成是一种特殊的类,即用 interface
关键字修饰的类。它具有如下特点
- 接口中可以包含 变量 和 方法,变量被隐式指定为
public static final
,方法被隐式指定为public abstract
- 接口支持多继承,解决了 Java 中类不能多继承的问题
- 一个类可以同时实现多个接口,一个类实现某个接口则必须实现该接口中的抽象方法,否则该类必须被定义为抽象类
总结:从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范
接口 和 抽象类 区别
- 接口的方法默认是
public
,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现和静态方法),而抽象类可以有非抽象的方法 - 在 Java8 中,接口也可以定义静态方法,可以直接用接口名调用,实现类是不可以调用的。如果实现类同时实现两个接口,接口中都定义了一样的默认方法,则必须重写,不然会报错
- 接口中除了
static
、final
变量,不能有其他变量(在接口中,属性都是默认public static final修饰的,这三个关键字可以省略),而抽象类中则不一定 - 一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过
extends
关键字扩展多个接口 - 接口方法默认修饰符是
public
,抽象方法可以有public
、protected
和default
这些修饰符(抽象方法就是为了被重写所以不能使用private
关键字修饰)
String类型
创建字符串的方式:
直接赋值:使用双引号将字符串内容括起来,直接赋值给一个[字符串变量]
**使用new
关键字:**使用new
关键字创建一个新的字符串对象
**使用字符数组:**使用字符数组创建一个新的字符串对象
**使用StringBuilder
或StringBuffer
:**使用StringBuilder
或StringBuffer
类来动态构建字符串。
需要注意的是,字符串是不可变的,即一旦创建,其内容就不能被修改。因此,对字符串进行修改操作时,实际上是创建了一个新的字符串对象。
另外,Java中的字符串是引用类型,因此可以使用==
运算符来比较字符串的引用是否相等。但是,如果要比较字符串的内容是否相等,应该使用equals()
方法。
Char类型是否可存储中文汉字
在Java中,char
类型是一个16位的二进制数,可以用来表示一个字符。在基本的ASCII字符集中,char
可以存储英文字符,但是由于中文汉字是由多个字节组成的,一个char
不足以存储一个中文汉字。
对于中文汉字,通常使用Unicode字符集,其中每个汉字由一个或多个字符组成,通常是两个字节。在Java中,String
类型是用来存储字符串的,它是一个字符序列,可以包含Unicode字符,包括中文汉字。
因此,char
类型不能直接存储中文汉子,但是可以通过String
类型来存储和操作中文汉字。
Java当中的锁
首先,Java当中锁的分类大概有:
可重入锁、不可重入锁;
乐观锁、悲观锁;
公平锁、非公平锁;
互斥锁、共享锁。
首先Java当中提供的Synchronized、ReentrantLock、ReentrantReadWriteLock都是可重入锁。重入的意思是,当前线程获取到A锁,在获取之后再次获取A锁是可以直接拿到的。不可重入的意思是:当前线程获取到A锁,之后再次尝试获取A锁,无法拿到,因为当前锁被当前线程持有,只有等他释放以后才能获取到。
乐观锁和悲观锁,java中提供的synchronized、reentranLock、reentrantReadWriteLock都是悲观锁;java中提供的CAS操作就是乐观锁的实现。悲观锁,获取不到所资源,就会挂起当前线程,进入Blocking、Wating状态,线程挂起会涉及到用户态和内核态的切换,而这种切换是比较消耗资源的(用户态jvm可自己执行的命令,内核态,需要操作系统执行的命令)。乐观锁,获取不到所资源,可以让CPU再次调度,重新尝试获取锁资源。Atomic原子类中,就是基于CAS乐观锁实现的。
公平锁和非公平锁。java中提供的synchronized只能是非公平锁。java中提供的reentrantLock、reentrantReadWrite可以实现公平锁和非公平锁。
互斥锁和共享锁:synchronized、reentrantLock都是互斥锁,reentrantReadWritelOCK有互斥锁也有共享锁。互斥锁,同一时间点,只会有一个线程持有者。
那一般最常用的就是ReentrantLock和synchronized。
核心区别:
- ReentrantLock是个类,synchronized是关键字,当然都是在JVM层面实现互斥锁的方式
效率区别:
- 如果竞争比较激烈,推荐ReentrantLock去实现,不存在锁升级概念。而synchronized是存在锁升级概念的,如果升级到重量级锁,是不存在锁降级的。
底层实现区别:
- 实现原理是不一样,ReentrantLock基于AQS实现的,synchronized是基于ObjectMonitor
功能向的区别:
- ReentrantLock的功能比synchronized更全面。
- ReentrantLock支持公平锁和非公平锁
- ReentrantLock可以指定等待锁资源的时间。
sycchronized一般用法就是同步方法和同步代码块,lock的话就是手动的枷锁和释放锁。
在JDK1.5的时候,Doug Lee推出了ReentrantLock,lock的性能远高于synchronized,所以JDK团队就在JDK1.6中,对synchronized做了大量的优化。
锁消除:在synchronized修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,你即便写了synchronized,他也不会触发。
锁膨胀:如果在一个循环中,频繁的获取和释放做资源,这样带来的消耗很大,锁膨胀就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来不必要的消耗。
锁升级:ReentrantLock的实现,是先基于乐观锁的CAS尝试获取锁资源,如果拿不到锁资源,才会挂起线程。synchronized在JDK1.6之前,完全就是获取不到锁,立即挂起当前线程,所以synchronized性能比较差。
synchronized就在JDK1.6做了锁升级的优化
- 无锁、匿名偏向:当前对象没有作为锁存在。
- 偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断,当前指向的线程是否是当前线程 。
- 如果是,直接拿着锁资源走。
- 如果当前线程不是我,基于CAS的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级为轻量级锁。(偏向锁状态出现了锁竞争的情况)
- 轻量级锁:会采用自旋锁的方式去频繁的以CAS的形式获取锁资源(采用的是自适应自旋锁)
- 如果成功获取到,拿着锁资源走
- 如果自旋了一定次数,没拿到锁资源,锁升级。
- 重量级锁:就是最传统的synchronized方式,拿不到锁资源,就挂起当前线程。(用户态&内核态)、
synchronized是基于对象实现的。
先要对Java中对象在堆内存的存储有一个了解。MarkWord中标记着四种锁的信息:无锁、偏向锁、轻量级锁、
ReentrantLock是基于AQS实现的锁。
AQS就是AbstractQueuedSynchronizer抽象类,AQS其实就是JUC包下的一个基类,JUC下的很多内容都是基于AQS实现了部分功能,比如ReentrantLock,ThreadPoolExecutor,阻塞队列,CountDownLatch,Semaphore,CyclicBarrier等等都是基于AQS实现。
首先AQS中提供了一个由volatile修饰,并且采用CAS方式修改的int类型的state变量。
其次AQS中维护了一个双向链表,有head,有tail,并且每个节点都是Node对象
java 自旋锁
Java中的自旋锁是一种非阻塞锁,它会尝试获取锁,如果当前锁被其他线程持有,那么获取锁的操作会处于忙等状态,一直尝试获取锁,直到获取到为止。自旋锁适用于锁的持有时间非常短的场景,因为在锁的持有时间较长时,自旋等待会浪费大量CPU资源。
Java中可以使用java.util.concurrent.atomic.AtomicReference
配合CAS操作来实现自旋锁
import java.util.concurrent.atomic.AtomicReference;
public class Spinlock {
private AtomicReference<Thread> owner = new AtomicReference<>();
public void lock() {
Thread current = Thread.currentThread();
while (!owner.compareAndSet(null, current)) {
// 自旋等待,直到获取锁
}
}
public void unlock() {
Thread current = Thread.currentThread();
owner.compareAndSet(current, null);
}
public static void main(String[] args) {
Spinlock spinlock = new Spinlock();
Runnable task = () -> {
spinlock.lock();
try {
// 临界区代码
System.out.println(Thread.currentThread().getName() + " is running");
} finally {
spinlock.unlock();
}
};
for (int i = 0; i < 5; i++) {
Thread t = new Thread(task);
t.start();
}
}
}
集合和数组的区别
数组是固定长度,集合是动态长度。
数组可以存储基本数据类型,也可以存储引用数据类型,集合只能存储引用数据类型。
数组存储的元素必须是同一数据类型,集合存储的对象可以是不同数据类型
List,Set,Map三者的区别
Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、 List、Queue三种子接口。
我们比较常用的是Set、List,Map接口不是 collection的子接口。Collection集合主要有List和Set两大接口
List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重 复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素, 只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、 LinkedHashSet 以及TreeSet。
Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不 要求有序,允许重复。
Map没有继承于Collection接口,从Map集合中检索元 素时,只要给出键对象,就会返回对应的值对象。
Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、 ConcurrentHashMap
对HashMap的了解
HashMap实现了Map接口,是一个键值对集合,key 无序,唯一,只允许存储一个null值,value不要求有序,允许重复。
HashMap在JDK1.8以前是数组+链表的方式实现,在JDK1.8以后是数组+链表+红黑树的方式实现,在链表的节点数超过8个,链表就会转为红黑树来提高查询效率。
如果说实例化一个hashmap的时候没有指定长度,那么在第一次添加元素的时候就会调用resize()进行扩容,默认的初始化长度是16,之后在集合元素个数大于等于长度*加载因子(0.75)就会自动扩容,扩容为原先的两倍。那编写优质java代码一书中建议,最好是在创建hashmap的时候,根据大概的长度去给一个合理的长度,这样可以避免因为数据扩大,频繁扩容带来性能上的消耗。
hashmap如何解决hash冲突
hash冲突是因为两个key值在经过同意hash函数的时候,计算出相同的散列值的情况。那一方面是通过两次hash,也就是再哈希来降低碰撞的概率,一方面是通过链表的方式,当发生hash碰撞的时候,在比较key值确实不同的情况下,把这个数据放到链表中。
hashMap的长度为什么是2的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大
致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。
这个算法应该如何设计呢?我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操
作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说
hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对
于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方
List 和 Set 的区别
List , Set 都是继承自Collection 接口
List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null
元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。
Set 特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null
元素,必须保证元素唯一性。Set 接口常用实现类是
HashSet、LinkedHashSet 以及 TreeSet。
另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,
无法用下标来取得想要的值。
Set和List对比
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位
置改变
说一下 HashSet 的实现原理
HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为
PRESENT,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层
HashMap 的相关方法来完成,HashSet 不允许重复的值
向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles 方法
比较。
HashSet 中的add ()方法会使用HashMap 的put()方法。
HashMap 的 key 是唯一的,由源码可以看出 HashSet 添加进去的值就是作为 HashMap 的key,并且
在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap
比较key是否相等是先比较 hashcode 再比较equals )。
ArrayList 的优缺点
ArrayList的优点如下:
ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查
找的时候非常快。
ArrayList 在顺序添加一个元素的时候非常方便。
ArrayList 的缺点如下:
删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
插入元素的时候,也需要做一次元素复制操作,缺点同上。
ArrayList 比较适合顺序添加、随机访问的场景。
java 面试 常用数据结构
一、线性结构:数组、链表、哈希表;队列、栈
1.数组:
数组是有序元素的序列,在内存中的分配是连续的,数组会为存储的元素都分配一个下标(索引),此下标是一个自增连续的,访问数组中的元素通过下标进行访问;数组下标从0开始访问;
2.链表:
链表是由一系列节点Node(也可称元素)组成,数据元素的逻辑顺序是通过链表的指针地址实现,通常情况下,每个节点包含两个部分,一个用于存储元素的内存地址,名叫数据域,另一个则指向下一个相邻节点地址的指针,名叫指针域;根据链表的指向不同可分为单向链表、双向链表、循环链表等;我们本章介绍的是单向链表,也是所有链表中最常见、最简单的链表;
3.散列表(哈希表):
也叫哈希表,是根据键和值 (key和value) 直接进行访问的数据结构,通过key和value来映射到集合中的一个位置,这样就可以很快找到集合中的对应元素。它利用数组支持按照下标访问的特性,所以散列表其实是数组的一种扩展,由数组演化而来。
4.队列 队列与栈一样,也是一种线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。队列的特点是先进先出,从一端放入元素的操作称为入队,取出元素为出队
5.栈 是一种特殊的线性表,仅能在线性表的一端操作,栈顶允许操作,栈底不允许操作。 栈的特点是:先进后出从栈顶放入元素的操作叫入栈(压栈),取出元素叫出栈(弹栈)
二、非线性结构有:堆、树(二叉树、B树、B+树)
堆可以看做是一颗用数组实现的二叉树,所以它没有使用父指针或者子指针。堆根据“堆属性”来排序,“堆属性”决定了树中节点的位置。
树 它是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做 “树” 是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
1)每个节点有0个或多个子节点;
2)没有父节点的节点称为根节点;
3)每一个非根节点有且只有一个父节点;
4)除了根节点外,每个子节点可以分为多个不相交的子树;
5)右子树永远比左子树大,读取顺序从左到右;
树的分类有非常多种,平衡二叉树(AVL)、红黑树RBL(R-B Tree)、B树(B-Tree)、B+树(B+Tree)等,但最早都是由二叉树演变过去的;
https://blog.csdn.net/weixin_51326478/article/details/134130795
排序的方法
在Java中,可以通过多种方式对数组或集合进行排序
- 使用
Arrays.sort()
方法对数组进行排序:
import java.util.Arrays;
public class SortExample {
public static void main(String[] args) {
int[] numbers = {9, 5, 2, 7, 3};
Arrays.sort(numbers);
System.out.println(Arrays.toString(numbers)); // 输出排序后的数组
}
}
- 使用
Collections.sort()
方法对列表进行排序:
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class SortExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(9, 5, 2, 7, 3);
Collections.sort(numbers);
System.out.println(numbers); // 输出排序后的列表
}
}
- 使用
java.util.Comparator
自定义排序规则:
import java.util.Arrays;
import java.util.Comparator;
public class SortExample {
public static void main(String[] args) {
String[] names = {"Bob", "Alice", "Charlie"};
Arrays.sort(names, Comparator.comparing(String::toString));
System.out.println(Arrays.toString(names)); // 输出排序后的数组
}
}
- 使用
java.util.stream.Stream
进行排序:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class SortExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(9, 5, 2, 7, 3);
List<Integer> sortedNumbers = numbers.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sortedNumbers); // 输出排序后的列表
}
}
十大经典排序
二分查找
public static int binarySearch(int[] arr, int key) {
int low = 0;
int high = arr.length - 1;
while (low <= high) {
int middle = (low + high) / 2;
if (key < arr[middle]) {
high = middle - 1;
} else if (key > arr[middle]) {
low = middle + 1;
} else {
return middle;
}
}
return -1;
}
// 测试下:从一组数中找3,输出数组下标
public static void main(String[] args) {
int[] arr = {2, 3, 5, 7, 9, 78, 90, 167};
System.out.println("数组下标:"+binarySearch(arr, 3));
}
https://blog.csdn.net/m0_50330815/article/details/133801071
机考算法题
第一题:
import java.util.*;
/**
题目大意:给定一个数组,问出现次数大于等于t的数有哪些,按照出现次数降序,次数相同按照数值升序输出
思路:用map统计每个数出现的次数,然后遍历map,将出现次数大于等于t的数加入到list中,然后对list进
行排序,排序规则是次数降序,次数相同按照数值升序
*/
class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int [] arr = new int[n];
for(int i=0; i<n; i++) {
arr[i] = sc.nextInt();
}
int t = sc.nextInt();
// key是数值,value是出现的次数
HashMap<Integer, Integer> map = new HashMap<>();
for(int i=0; i<n; i++) {
map.put(arr[i], map.getOrDefault(arr[i], 0)+1);
}
// list中存放的是出现次数大于等于t的数
List<Integer[]> list = new ArrayList<>();
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
if (entry.getValue() >= t)
list.add(new Integer[]{entry.getKey(), entry.getValue()});
}
// 次数降序,次数相同按照数值升序
Collections.sort(list, new Comparator<Integer[]>() {
@Override
public int compare(Integer[] o1, Integer[] o2) {
// 出现次数相等,按照数值升序
if (o1[1].compareTo(o2[1]) == 0) {
return o1[0] - o2[0];
}
// 次数降序
return o2[1] - o1[1];
}
});
// 输出结果
int sz = list.size();
System.out.println(sz);
for(int i=0; i<sz; i++) {
System.out.println(list.get(i)[0]);
}
}
}
第二题:动态数组 模拟
求幸存数之和
正整数列 nums
跳数 jump
幸存数量 left
// 将字符串转化为数组
public ArrayList<Integer> get (int[] nums){
return (ArrayList<Integer>)
Arrays.stream(nums).boxed().collect(Collectors.toList());
}
// 计算数组的和
public int getRes (ArrayList<Integer> list, int jump, int left){
return list.stream().reduce(Integer::sum).orElse(0);
}
// 计算最后的结果
public long sumOfleft(int[] nums, int jump, int left) {
ArrayList<Integer> list = get(nums); // 将数组转化为list
int st = 1; // 从1开始
while (list.size() > left) { // 当list的长度大于left时
int n = list.size(); // list的长度
st += jump; // st加上jump
st %= n; // st对n取余,防止越界
list.remove(st); // 移除st位置的元素
}
return getRes(list, jump, left); // 返回list的和
}
第三题:
本题要求实现一个简易的内存池,支持两种操作:
- REQUEST :请求分配指定大小的内存,如果分配成功,返回分配到的内存首地址;如果内存不足或指定
的大小为0,则输出error。
- RELEASE :释放之前分配的内存,释放成功无需输出,如果释放不存在的首地址则输出error。
内存池的总大小为 100 字节,内存分配必须是连续的,并优先从低地址分配。已释放的内存可以被再次分
配,但在空闲时不能被二次释放。不会释放已申请的内存块的中间地址。
算法思路:
- 用一个哈希表 memory 来存储已分配的内存块, key 为内存块的首地址, value 为内存块的末尾地
址。
- 用一个列表 free 来存储空闲内存块,每个元素为一个长度为 2 的数组,表示空闲内存块的首尾地
址。初始时 free 中只有一个元素 [0, 99] ,表示整个内存池都是空闲的。
- 对于REQUEST操作,遍历 free 列表,找到第一个满足条件的空闲内存块进行分配。如果找到了,就
在 memory 中记录下这个内存块,并更新 free 中的空闲区间;如果没找到,就输出 error 。
- 对于 RELEASE 操作,先判断要释放的地址是否存在于 memory 中:
如果存在,就从 memory 中删除这个内存块,并将其加入 free 列表中,然后对 free 列表按照
首地址从小到大排序。
如果不存在,就输出 error 。
- 注意,在分配和释放内存时,要时刻保证free 列表中的空闲区间是合并的,不能有相邻的空闲区间,由
于第一次的代码没有考虑到,导致有一些用例没通过。
下面是代码的详细注释:
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 用哈希表存储已分配的内存块,key为内存块的首地址,value为内存块的末尾地址
Map<Integer, Integer> memory = new HashMap<>();
// 用列表存储空闲内存块,每个元素为一个长度为2的数组,表示空闲内存块的首尾地址
List<int[]> free = new ArrayList<>();
// 初始时整个内存池都是空闲的
free.add(new int[]{0, 99});
// 读取操作命令的个数
int n = scanner.nextInt();
scanner.nextLine();
// 处理每个操作命令
for (int i = 0; i < n; i++) {
String op = scanner.nextLine();
String[] tokens = op.split("=");
String cmd = tokens[0].trim();
int sz = Integer.parseInt(tokens[1].trim());
if (cmd.equals("REQUEST")) { // 处理REQUEST操作
if (sz == 0) { // 如果请求的大小为0,输出error
System.out.println("error");
continue;
}
int m = free.size();
boolean flg = false;
// 遍历free列表,找到第一个满足条件的空闲内存块进行分配
for (int j = 0; j < m; j++) {
if (sz + free.get(j)[0] - 1 <= free.get(j)[1]) {
// 在memory中记录下这个内存块
memory.put(free.get(j)[0], free.get(j)[0] + sz - 1);
// 输出分配到的内存首地址
System.out.println(free.get(j)[0]);
// 更新free中的空闲区间
free.get(j)[0] += sz;
flg = true;
break;
}
}
if (!flg) { // 如果没找到合适的空闲内存块,输出error
System.out.println("error");
}
} else { // 处理RELEASE操作
if (memory.containsKey(sz)) { // 如果要释放的内存块存在
int r = memory.get(sz); // 获取内存块的末尾地址
// 将释放的内存块加入free列表中
free.add(new int[]{sz, r});
// 对free列表按照首地址从小到大排序
free.sort(Comparator.comparingInt(a -> a[0]));
// 从memory中删除这个内存块
memory.remove(sz);
// 合并相邻的空闲区间
for (int j = 1; j < free.size(); j++) {
if (free.get(j)[0] == free.get(j - 1)[1] + 1) {
free.get(j - 1)[1] = free.get(j)[1];
free.remove(j);
j--;
}
}
} else { // 如果要释放的内存块不存在,输出error
System.out.println("error");
}
}
}
}
}
OSI 七层参考模型/計算機网络分层
为了更好地促进互联网的研究和发展,国际标准化组织ISO在1985 年指定了网络互联模型。OSI 参考模型(Open System Interconnect Reference Model),具有 7 层结构
应用层:各种应用程序协议,比如HTTP、HTTPS、FTP、SOCKS安全套接字协议、DNS域名系统、GDP网关发现协议等等。
表示层:加密解密、转换翻译、压缩解压缩,比如LPP轻量级表示协议。
会话层:不同机器上的用户建立和管理会话,比如SSL安全套接字层协议、TLS传输层安全协议、RPC远程过程调用协议等等。
传输层:接受上一层的数据,在必要的时候对数据进行分割,并将这些数据交给网络层,保证这些数据段有效到达对端,比如TCP传输控制协议、UDP数据报协议。
网络层:控制子网的运行:逻辑编址、分组传输、路由选择,比如IP、IPV6、SLIP等等。
数据链路层:物理寻址,同时将原始比特流转变为逻辑传输路线,比如XTP压缩传输协议、PPTP点对点隧道协议等等。
物理层:机械、电子、定时接口通信信道上的原始比特流传输,比如IEEE802.2等等
什么是 HTTP
HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写。HTTP协议工作于客户端-服务端架构为上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。Web服务器根据接收到的请求后,向客户端发送响应信息。
说通俗点就是:客户端(浏览器)和服务器(例如linux服务器) 他们两个交流或沟通的一种规则,即浏览器找服务器的时候需要带上哪些东西,服务器回礼的时候,也会按照一定的规则和格式返回给浏览器,而浏览器和服务器之间的这种沟通时的规则就是所谓的http协议
说的再通俗点,你要找别人办事,得按照你们两个都认同的方式来
HTTP协议特点
无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
HTTP之状态码
1xx:指示信息–表示请求已接收,继续处理
2xx:成功–表示请求已被成功接收、理解、接受
3xx:重定向–要完成请求必须进行更进一步的操作
4xx:客户端错误–请求有语法错误或请求无法实现
5xx:服务器端错误–服务器未能实现合法的请求
常见的状态码:
200 OK //客户端请求成功
400 Bad Request //客户端请求有语法错误,不能被服务器所理解
401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
403 Forbidden //服务器收到请求,但是拒绝提供服务
404 Not Found //请求资源不存在,eg:输入了错误的URL
500 Internal Server Error //服务器发生不可预期的错误
503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常
HTTP工作原理
HTTP协议定义Web客户端如何从Web服务器请求Web页面,以及服务器如何把Web页面传送给客户端。
HTTP协议采用了请求/响应模型。客户端向服务器发送一个请求报文,请求报文包含请求的方法、URL、协议版本、请求头部和请求数据。服务器以一个状态行作为响应,响应的内容包括协议的版本、成功或者错误代码、服务器信息、响应头部和响应数据。
以下是 HTTP 请求/响应的步骤:
1、客户端连接到Web服务器
一个HTTP客户端,通常是浏览器,与Web服务器的HTTP端口(默认为80)建立一个TCP套接字连接。例如,http://www.baidu.con。
2、发送HTTP请求
通过TCP套接字,客户端向Web服务器发送一个文本的请求报文,一个请求报文由请求行、请求头部、空行和请求数据4部分组成。
3、服务器接受请求并返回HTTP响应
Web服务器解析请求,定位请求资源。服务器将资源复本写到TCP套接字,由客户端读取。一个响应由状态行、响应头部、空行和响应数据4部分组成。
4、释放连接TCP连接
若connection 模式为close,则服务器主动关闭TCP连接,客户端被动关闭连接,释放TCP连接;若connection 模式为keepalive,则该连接会保持一段时间,在该时间内可以继续接收请求;
5、客户端浏览器解析HTML内容
客户端浏览器首先解析状态行,查看表明请求是否成功的状态代码。然后解析每一个响应头,响应头告知以下为若干字节的HTML文档和文档的字符集。客户端浏览器读取响应数据HTML,根据HTML的语法对其进行格式化,并在浏览器窗口中显示。
例如:在浏览器地址栏键入URL,按下回车之后会经历以下流程:
1、浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
2、解析出 IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立TCP连接;
3、浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
4、服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
5、释放 TCP连接;
6、浏览器将该 html 文本并显示内容;
HTTP和HTTPS有什么区别
HTTPS:是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。
HTTPS协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性。
HTTPS和HTTP的区别主要如下:
1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
HTTPS的安全性是怎么实现的
(1)客户使用https的URL访问Web服务器,要求与Web服务器建立SSL连接。
(2)Web服务器收到客户端请求后,会将网站的证书信息(证书中包含公钥)传送一份给客户端。
(3)客户端的浏览器与Web服务器开始协商SSL连接的安全等级,也就是信息加密的等级。
(4)客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站。
(5)Web服务器利用自己的私钥解密出会话密钥。
(6)Web服务器利用会话密钥加密与客户端之间的通信。
说说TCP和UDP的区别
1、TCP面向连接(如打电话要先拨号建立连接):UDP是无连接的,即发送数据之前不需要建立连接。
2、TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。tcp通过校验和,重传控制,序号标识,滑动窗口、确认应答实现可靠传输。如丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。
3、UDP具有较好的实时性,工作效率比TCP高,适用于对高速传输和实时性有较高的通信或广播通信。
4.每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
5、TCP对系统资源要求较多,UDP对系统资源要求较少。
说下HTTP、TCP、Socket的关系是什么
- TCP/IP代表传输控制协议/网际协议,指的是一系列协议族。
- HTTP本身就是一个协议,是从Web服务器传输超文本到本地浏览器的传送协议。
- Socket是TCP/IP网络的API,其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面。对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
综上所述:
- 需要IP协议来连接网络
- TCP是一种允许我们安全传输数据的机制,使用TCP协议来传输数据的HTTP是Web服务器和客户端使用的特殊协议。
- HTTP基于TCP协议,所以可以使用Socket去建立一个TCP连接。
说下HTTP的长链接和短连接的区别
HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。
短连接
在HTTP/1.0中默认使用短链接,也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。如果客户端访问的某个HTML或其他类型的Web资源,如JavaScript文件、图像文件、CSS文件等。当浏览器每遇到这样一个Web资源,就会建立一个HTTP会话.
长连接
从HTTP/1.1起,默认使用长连接,用以保持连接特性。在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭。如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间
TCP原理
三次握手:
1.第一次握手:客户端将标志位syn重置为1,随机产生seq=a,并将数据包发送给服务端
2.第二次握手:服务端收到syn=1知道客户端请求连接,服务端将syn和ACK都重置为1,ack=a+1,随机产一个值seq=b,并将数据包发送给客户端,服务端进入syn_RCVD状态。
3.第三次握手:客户端收到确认后,检查ack是否为a+1,ACK是否为1,若正确将ACK重置为1,将ack改为b+1,然后将数据包发送给服务端服务端检查ack与ACK,若都正确,就建立连接,进入ESTABLISHEN.
四次挥手:
1.开始双方都处于连接状态
2.客户端进程发出FIN报文,并停止发送数据,在报文中FIN结束标志为1,seq为a连接状态下发送给服务器的最后一个字节的序号+1,报文发送结束后,客户端进入FIN-WIT1状态。
3.服务端收到报文,向客户端发送确认报文,ACK=1,seq为b服务端给客户端发送的最后字节的序号+1,ack=a+1,发送后客户端进入close-wait状态,不再发送数据,但服务端发送数据客户端一九可以收到(城为半关闭状态)。
4.客户端收到服务器的确认报文后,客户端进入fin-wait2状态进行等待服务器发送第三次的挥手报文。
5.服务端向fin报文FIN=1ACK=1,seq=c(服务器向客户端发送最后一个字节序号+1),ack=b+1,发送结束后服务器进入last-ack状态等待最后的确认。
6.客户端收到是释放报文后,向服务器发送确认报文进入time-wait状态,后进入close
7.服务端收到确认报文进入close状态。
Cookie和Session的区别
cookie是由Web服务器保存在用户浏览器上的文件(key-value格式),可以包含用户相关的信息。客户端向服务器发起请求,就提取浏览器中的用户信息由http发送给服务器
session是浏览器和服务器会话过程中,服务器会分配的一块储存空间给session。服务器默认为客户浏览器的cookie中设置sessionid,这个sessionid就和cookie对应,浏览器在向服务器请求过程中传输的cookie包含sessionid,服务器根据传输cookie中的sessionid获取出会话中存储的信息,然后确定会话的身份信息。
1、Cookie数据存放在客户端上,安全性较差,Session数据放在服务器上,安全性相对更高
2、单个cookie保存的数据不能超过4K,session无此限制
3、session一定时间内保存在服务器上,当访问增多,占用服务器性能,考虑到服务器性能方面,应当
使用cookie。
Tomcat是什么
Tomcat服务器Apache软件基金会项目中的一个核心项目,是一个免费的开放源代码的Web应用服
务器(Servlet容器),属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被
普遍使用,是开发和调试JSP程序的首选。
你了解的设计模式说一些
Java 中一般认为有 23 种设计模式,总体来说设计模式分为三大类:
创建型模式,共 5 种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共 7 种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共 11 种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
单例模式定义
单例模式确保某个类在整个应用程序中只有一个实例,而且自行实例化并向整个系统提供这个实例。比如数据库连接对象就是单例的。总之,选择单例模式就是为了避免不一致状态
单例模式的特点
单例类只能有一个实例。
● 单例类必须自己创建自己的唯一实例。
● 单例类必须给所有其他对象提供这一实例
单例模式保证了全局对象的唯一性,比如系统启动读取配置文件就需要单例保证配置的一致性。
单例的四大原则
构造私有
● 以静态方法或者枚举返回实例
● 确保实例只有一个,尤其是多线程环境
● 确保反序列换时不会重新构建对象
实现单例模式的方式
饿汉式(立即加载):
饿汉式单例在类加载初始化时就创建好一个静态的对象供外部使用,除非系统重启,这个对象不会改变,所以本身就是线程安全的。Singleton 通过将构造方法限定为 private 避免了类在外部被实例化,在同一个虚拟机范围内,Singleton 的唯一实例只能通过 getInstance()方法访问。(事实上,通过Java 反射机制是能够实例化构造方法为 private 的类的,会使 Java 单例实现失效)
package com.atguigu.interview.chapter02;
/**
* @author atguigu
*
* 饿汉式(立即加载)
*/
public class Singleton {
/**
* 私有构造
*/
private Singleton() {
System.out.println( "构造函数 Singleton1" );
}
/**
* 初始值为实例对象
*/
private static Singleton single = new Singleton();
/**
* 静态工厂方法
* @return 单例对象
*/
public static Singleton getInstance() {
System.out.println( "getInstance" );
return single;
}
public static void main(String[] args){
System.out.println( "初始化" );
Singleton instance = Singleton.getInstance();
}
}
懒汉式(延迟加载):
该示例虽然用延迟加载方式实现了懒汉式单例,但在多线程环境下会产生多个Singleton 对象
package com.atguigu.interview.chapter02;
/**
* @author atguigu
*
*懒汉式(延迟加载)
*/
public class Singleton2 {
/**
* 私有构造
*/
private Singleton2() {
System.out.println( "构造函数 Singleton2" );
}
/**
* 初始值为 null
*/
private static Singleton2 single = null;
/**
* 静态工厂方法
* @return 单例对象
*/
public static Singleton2 getInstance() {
if (single == null){
System.out.println( "getInstance" );
single = new Singleton2();
}
return single;
}
public static void main(String[] args){
System.out.println( "初始化" );
Singleton2 instance = Singleton2.getInstance();
}
}
同步锁(解决线程安全问题)
在方法上加 synchronized 同步锁或是用同步代码块对类加同步锁,此种方式虽然解决了多个实例对象问题,但是该方式运行效率却很低下,下一个线程想要获取对象,就必须等待上一个线程释放锁之后,才可以继续运行。
package com.atguigu.interview.chapter02;
/**
* @author atguigu
*
* 同步锁(解决线程安全问题)
*/
public class Singleton3 {
/**
* 私有构造
*/
private Singleton3() {}
/**
* 初始值为 null
*/
private static Singleton3 single = null;
public synchronized static Singleton3 getInstance() {
if (single == null){
single = new Singleton3();
}
return single;
}
}
}
双重检查锁(提高同步锁的效率)
使用双重检查锁进一步做了优化,可以避免整个方法被锁,只对需要锁的代码部分加锁,可以提高执行效率。
package com.atguigu.interview.chapter02;
/**
* @author atguigu
* 双重检查锁(提高同步锁的效率)
*/
public class Singleton4 {
/**
* 私有构造
*/
private Singleton4() {}
/**
* 初始值为 null
* 加 volatile 关键字是为了防止 创建对象时的指令重排问题,导致其他线程使用对象时造成空指针问题。
*/
private volatile static Singleton4 single = null;
/**
* 双重检查锁
* @return 单例对象
*/
public static Singleton4 getInstance() {
if (single == null) {
synchronized (Singleton4. class ) {
if (single == null) {
single = new Singleton4();
}
}
}
return single;
}
}
工厂设计模式(Factory)
工厂设计模式,顾名思义,就是用来生产对象的,在 java 中,万物皆对象,这些对象都需要创建,如果创建的时候直接 new 该对象,就会对该对象耦合严重,假如我们要更换对象,所有 new 对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则,如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就是:解耦
1、对于简单工厂和工厂方法来说,两者的使用方式实际上是一样的,如果对于产品的分类和名称是确定的,数量是相对固定的,推荐使用简单工厂模式;
2、抽象工厂用来解决相对复杂的问题,适用于一系列、大批量的对象生产。
简单工厂(Simple Factory)
一个工厂方法,依据传入的参数,生成对应的产品对象;
先将产品类抽象出来,比如,苹果和梨都属于水果,抽象出来一个水果类 Fruit,苹果和梨就是具体的产品类,然后创建一个水果工厂,分别用来创建苹果和梨。代码如下:
以上的这种方式,每当添加一种水果,就必然要修改工厂类,违反了开闭原则;所以简单工厂只适合于产品对象较少,且产品固定的需求,对于产品变化无常的需求来说显然不合适。
水果接口:
public interface Fruit {
void whatIm();
}
苹果类:
public class Apple implements Fruit {
@Override
public void whatIm() {
System.out.println( "苹果" );
}
}
梨类:
public class Pear implements Fruit {
@Override
public void whatIm() {
System.out.println( "梨" );
}
}
水果工厂:
public class FruitFactory {
public Fruit createFruit(String type) {
if (type.equals( "apple" )) { //生产苹果
return new Apple();
} else if (type.equals( "pear" )) { //生产梨
return new Pear();
}
抽象工厂(Abstract Factory)
为创建一组相关或者是相互依赖的对象提供的一个接口,而不需要指定它们的具体类。
抽象工厂和工厂方法的模式基本一样,区别在于,工厂方法是生产一个具体的产品,而抽象工厂可以用来生产一组相同,有相对关系的产品;重点在于一组,一批,一系列;举个例子,假如生产小米手机,小米手机有很多系列,小米 note、红米 note等;假如小米 note 生产需要的配件有 825 的处理器,6 英寸屏幕,而红米只需要650 的处理器和 5 寸的屏幕就可以了。用抽象工厂来实现:
public interface Cpu {
void run();
class Cpu650 implements Cpu {
@Override
public void run() {
System.out.println( "650 也厉害" );
}
}
class Cpu825 implements Cpu {
@Override
public void run() {
System.out.println( "825 更强劲" );
}
}
}
屏幕接口和实现类:
public interface Screen {
void size();
class Screen5 implements Screen {
@Override
public void size() {
System.out.println( "" + "5 寸" );
}
}
class Screen6 implements Screen {
@Override
public void size() {
System.out.println( "6 寸" );
}
}
}
抽象工厂接口:
public interface PhoneFactory {
Cpu getCpu(); //使用的 cpu
Screen getScreen(); //使用的屏幕
}
小米手机工厂:
public class XiaoMiFactory implements PhoneFactory {
@Override
public Cpu.Cpu825 getCpu() {
return new Cpu.Cpu825(); //高性能处理器
}
@Override
public Screen.Screen6 getScreen() {
return new Screen.Screen6(); //6 寸大屏
}
}
红米手机工厂:
public class HongMiFactory implements PhoneFactory {
@Override
public Cpu.Cpu650 getCpu() {
return new Cpu.Cpu650(); //高效处理器
}
@Override
public Screen.Screen5 getScreen() {
return new Screen.Screen5(); //小屏手机
}
}
使用工厂生产产品:
public class PhoneApp {
Javapublic static void main(String[] args){
HongMiFactory hongMiFactory = new HongMiFactory();
XiaoMiFactory xiaoMiFactory = new XiaoMiFactory();
Cpu.Cpu650 cpu650 = hongMiFactory.getCpu();
Cpu.Cpu825 cpu825 = xiaoMiFactory.getCpu();
cpu650.run();
cpu825.run();
Screen.Screen5 screen5 = hongMiFactory.getScreen();
JavaScreen.Screen6 screen6 = xiaoMiFactory.getScreen();
screen5.size();
screen6.size();
}
}
以上例子可以看出,抽象工厂可以解决一系列的产品生产的需求,对于大批量,多系列的产品,用抽象工厂可以更好的管理和扩展。
代理模式(Proxy)
代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。
举个例子来说明:假如说我现在想买一辆二手车,虽然我可以自己去找车源,做质量检测等一系列的车辆过户流程,但是这确实太浪费我得时间和精力了。我只是想买一辆车而已为什么我还要额外做这么多事呢?于是我就通过中介公司来买车,他们来给我找车源,帮我办理车辆过户流程,我只是负责选择自己喜欢的车,然后付钱就可以了。用图表示如下:
动态代理的原理: 使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。
代理对象决定是否以及何时将方法调用转到原始对象上
动态代理的方式
基于接口实现动态代理: JDK 动态代理
基于继承实现动态代理: Cglib、Javassist 动态代理
在某些情况下,一个客户类不想或者不能直接引用一个委托对象,而代理类对象可以在客户类和委托对象之间起到中介的作用,其特征是代理类和委托类实现相同的接口。
可以分为两种:静态代理、动态代理。
● 静态代理是由程序员创建或特定工具自动生成源代码,再对其编译。在程序员运行之前,代理类.class 文件就已经被创建了。
● 动态代理是在程序运行时通过反射机制动态创建的。
静态代理(Static Proxy)
优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展。
缺点:我们得为每一个服务创建代理类,工作量太大,不易管理。同时接口一旦发生改变,代理类也得相应修改。
第一步:创建服务类接口
public interface BuyHouse {
void buyHouse();
}
第二步:实现服务接口
public class BuyHouseImpl implements BuyHouse {
@Override
public void buyHouse() {
System.out.println( “我要买房” );
}
}
第三步:创建代理类
public class BuyHouseProxy implements BuyHouse {
private BuyHouse buyHouse;
public BuyHouseProxy(final BuyHouse buyHouse) {
this .buyHouse = buyHouse;
}
@Override
public void buyHouse() {
System.out.println( “买房前准备” );
buyHouse.buyHouse();
System.out.println( “买房后装修” );
}
}
JDK 动态代理(Dynamic Proxy)
在动态代理中我们不再需要再手动的创建代理类,我们只需要编写一个动态处理器就可以了。真正的代理对象由 JDK 在运行时为我们动态的来创建。
第一步:创建服务类接口
代码和上例一样
第二步:实现服务接口
代码和上例一样
第三步:编写动态处理器
public class DynamicProxyHandler implements InvocationHandler {
private Object object;
public DynamicProxyHandler(final Object object) {
this .object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Th
rowable {
System.out.println( "买房前准备" );
Object result = method.invoke(object, args);
System.out.println( "买房后装修" );
Javareturn result;
}
}
第四步:编写测试类
public class HouseApp {
public static void main(String[] args) {
BuyHouse buyHouse = new BuyHouseImpl();
BuyHouse proxyBuyHouse = (BuyHouse) Proxy.newProxyInstance(
BuyHouse. class .getClassLoader(),
new Class[]{BuyHouse. class },
new DynamicProxyHandler(buyHouse));
proxyBuyHouse.buyHouse();
}
}
Proxy 是所有动态生成的代理的共同的父类,这个类有一个静态方法Proxy.newProxyInstance(),接收三个参数:
● ClassLoader loader:指定当前目标对象使用的类加载器,获取加载器的方法是固定的
● Class<?>[] interfaces:指定目标对象实现的接口的类型,使用泛型方式确认类型
● InvocationHandler:指定动态处理器,执行目标对象的方法时,会触发事件处理器的方法
JDK动态代理总结:
优点:相对于静态代理,动态代理大大减少了开发任务,同时减少了对业务接口的依赖,降低了耦合度。
缺点:Proxy 是所有动态生成的代理的共同的父类,因此服务类必须是接口的形式,不能是普通类的形式,因为 Java 无法实现多继承。
CGLib 动态代理(CGLib Proxy)
DK 实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要 CGLib 了。CGLib 采用了底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。但因为采用的是继承,所以不能对 final 修饰的类进行代理。
JDK 动态代理与 CGLib 动态代理均是实现 Spring AOP 的基础。
Cglib子类代理实现方法:
(1)引入 cglib 的 jar 文件,asm 的 jar 文件
(2)代理的类不能为 final
(3)目标业务对象的方法如果为 final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法
第一步:创建服务类
public class BuyHouse2 {
public void buyHouse() {
System.out.println( "我要买房" );Java
}
}
第二步:创建 CGLIB代理类
public class CglibProxy implements MethodInterceptor {
private Object target;
public CglibProxy(Object target) {
this .target = target;
}
/**
* 给目标对象创建一个代理对象
* @return 代理对象
*/
public Object getProxyInstance() {
//1.工具类
Enhancer enhancer = new Enhancer();
//2.设置父类Java
enhancer.setSuperclass(target.getClass());
//3.设置回调函数
enhancer.setCallback( this );
//4.创建子类(代理对象)
return enhancer.create();
}
Javapublic Object intercept(Object object, Method method, Object[] args, Metho
dProxy methodProxy) throws Throwable {
System.out.println( "买房前准备" );
//执行目标对象的方法
Object result = method.invoke(target, args);
System.out.println( "买房后装修" );
return result;
}
}
第三步:创建测试类
public class HouseApp {
public static void main(String[] args) {
BuyHouse2 target = new BuyHouse2();
CglibProxy cglibProxy = new CglibProxy(target);
BuyHouse2 buyHouseCglibProxy = (BuyHouse2) cglibProxy.getProxyInstan
ce();
buyHouseCglibProxy.buyHouse();
}
}
CGLib代理总结:
CGLib 创建的动态代理对象比 JDK 创建的动态代理对象的性能更高,但是 CGLIB 创建代理对象时所花费的时间却比 JDK 多得多。所以对于单例的对象,因为无需频繁创建对象,用 CGLIB 合适,反之使用 JDK 方式要更为合适一些。同时由于 CGLib 由于是采用动态创建子类的方法,对于 final 修饰的方法无法进行代理。
计算机组成原理 5 个部分
5 个部分组成:运算器、控制器、存储器、输入设备、输出设备
中央处理器
32 位和 64 位 CPU 最主要区别在于一次能计算多少字节数据:
32 位 CPU 一次可以计算 4 个字节;
64 位 CPU 一次可以计算 8 个字节;
不代表 64 位 CPU 性能比 32 位 CPU 高很多,很少应用需要算超过 32 位的数字,所以如果计算的数额不超过 32 位数字的情况下,32 位和 64 位 CPU 之间没什么区别的,只有当计算超过 32 位数字的情况下,64 位的优势才能体现出来。
CPU 内部有寄存器、控制单元和逻辑运算单元等,控制单元负责控制 CPU 工作,逻辑运算单元负责计算,而寄存器可以分为多种类。
-
通用寄存器,用来存放需要进行运算的数据,比如需要进行加和运算的两个数据。
-
程序计 数器,用来存储 CPU 要执行下一条指令「所在的内存地址」,注意不是存储了下一条要执行的指令,此时指令还在内存中,程序计数器只是存储了下一条指令「的地址」。
-
指令寄存器,用来存放当前正在执行的指令,也就是指令本身,指令被执行完成之前,指令都存储在这里。
计算机操作系统的五大功能
Mysql SQL优化
mysql索引
按「数据结构」分类:B+tree索引、Hash索引、Full-text索引。
按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。
按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。
按「字段个数」分类:单列索引、联合索引。
首先InnoDB和MyISAM都是使用的B+树实现的,但是InnoDB使用的是聚簇索引而MyISAM使用的是非聚簇索引,聚簇索引根据主键创建一颗B+树,叶子节点则存放的是数据行记录,也可以把叶子结点称为数据页。通俗点来说就是把数据和索引存在同一个块,找到了索引也就找到了数据。
- 因为叶子结点将索引和数据放在一起,就决定了聚簇索引的唯一性,一张表里面只能有一个聚簇索引。
- InnoDB引擎默认将主键设置为聚簇索引,但如果没有设置主键,那么InnoDB将会选择非空的唯一索引作为代替,如果没有这样的索引,InnoDB将会定一个隐式主键作为聚簇索引。
- 因为聚簇索引特殊的物理结构所决定,叶子结点将索引和数据存放在一起,在获取数据的速度上是比非聚簇索引快的。
- 聚簇索引数据的存储是有序的,在进行排序查找和范围查找的速度也是非常快的。
- ⚠️ 也正因为有序性,在数据插入时按照主键的顺序插入是最快的,否则就会出现页分裂等问题,严重影响性能。对于InnoDB我们一般采用自增作为主键ID。
- 第二个问题主键最好不要进行更新,修改主键的代价非常大,为了保持有序性会导致更新的行移动,一般来说我们通常设置为主键不可更新。
而非聚簇索引是将索引和数据分开存储,那么在访问数据的时候就需要2次查找,但是和InnoDB的非聚簇部分还是有所区别。InnoDB是需要查找2次树,先查找辅助索引树,再查找聚簇索引树(这个过程也叫回表)。而MyISAM的主键索引叶子结点的存储的部分还是有所区别。InnoDB中存储的是索引和聚簇索引ID,但是MyISAM中存储的是索引和数据行的地址,只要定位就可以获取到。
查看执行计划
对于低性能的语句,一般先使用explain命令来查看语句的执行计划,看一下它是否使用了索引,使用什么索引,使用的索引的相关信息。
执行计划包含的信息id由一组数字组成,表示一个查询中各个子查询的执行顺序
- id相同,执行顺序由上至下。
- id不同,id值越大优先级越高,越先被执行。
- id为null时,表示一个结果集,不需要使用它查询,常出现在包含nuion等查询语句中。
select_type 每个子查询的查询类型,一些常见的查询类型
type非常重要,可以看到有没有走索引
- all 全表扫描
- index
- range 索引范围查找
- ref 使用非唯一索引查找数据
extra非常有用
- using index 使用覆盖索引
- using where 使用where子句来过滤结果集
- using filesort 使用非索引列进行排序时出现,非常消耗性能,尽量优化
- using temporary 使用临时表
【推荐】SQL性能优化的目标:至少要达到 range 级别,要求是ref级别,如果可以是consts 好。
说明:
1) consts 单表中 多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。
2) ref 指的是使用普通的索引(normal index)。
3) range 对索引进行范围检索。 反例:
explain表的结果,type=index,索引物理文件全扫描,速度非常慢,这个index级别比较range还低,与全表扫描是小巫见大巫。
慢查询日志
用于记录执行时间超过某个临界值的SQL日志,用于快速定位慢查询,为我们的优化做参考。开启慢查询日志
配置项:slow_query_log 可以使用show variables like ‘slov_query_log’查看是否开启,如果状态值为OFF,可以使用set GLOBAL slow_query_log = on来开启,它会在datadir下产生一个xxx-slow.log的文件。
设置临界时间配置项:long_query_time 查看:show VARIABLES like ‘long_query_time’,单位秒设置:set long_query_time=0.5
实操时应该从长时间设置到短的时间,即将 慢的SQL优化掉
查看日志,一旦SQL超过了我们设置的临界时间就会被记录到xxx-slow.log中
关心过业务系统里面的sql耗时吗?统计过慢查询吗?对慢查询都怎么优化过?
在业务系统中,除了使用主键进行的查询,其他的我都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。慢查询优化首先要搞明白慢的原因是什么? 是查询条件没有命中索引?是 load了不需要的数据列?还是数据量太大?所以优化也是针对这三个方向来的,首先分析语句,看看是否load了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。
数据库优化
创建索引的原则
索引虽好,但也不是无限制的使用,好符合一下几个原则
1)左前缀匹配原则,组合索引非常重要的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a=1andb=2andc>3andd=4如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
2)较频繁作为查询条件的字段才去创建索引
3)更新频繁字段不适合创建索引
4)若是不能有效区分数据的列不适合做索引列(如性别,男女未知,多也就三种,区分度实在太低)
5)尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
6)定义有外键的数据列一定要建立索引。
7)对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。8)对于定义为text、image和bit的数据类型的列不要建立索引
优化where子句
-
对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引
-
应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:1 select id from t where num is null – 可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:select id from t where num=0
-
应尽量避免在 where 子句中使用!=或<>操作符,否则引擎将放弃使用索引而进行全表扫描。
-
应尽量避免在 where 子句中使用or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如select id from t where num=10 or num=20 – 可以这询:select id from t where num=10 union all select id from t where num=20 in 和 not in 也要慎用,否则会导致全表扫描,如: select id from t wherenum in(1,2,3) – 对于连续的数值,能用 between 就不要用 in 了:select id from t where num between 1 and 3
-
下面的查询也将导致全表扫描:select id from t where name like ‘% 李%’若要提高效率,可以考虑全文检索。
-
化程序不能将访问计划的选择推迟到运行时;它必须在编译时进行选择。然 而,如果在编译时建立访问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如下面语句将进行全表扫描:1 select id from t where num=@num --可以改为强制查询使用索引:select id from t with(index(索引名)) where num=@num
-
应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:select id from t where num/2=100 – 应改为:select id from t where num=100*2
-
应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:
1 select id from t where substring(name,1,3)=’abc’ – name以abc开头的id应改为: select
id from t where name like ‘abc%’
-
不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。
优化UNION查询
UNION ALL的效率高于UNION
优化子查询
用关联查询替代
优化特定类型的查询语句
ount()会忽略所有的列,直接统计所有列数,不要使用count(列名) MyISAM中,没有任何where条件的count()非常快。
mysql 隔离级别和事务
MyISAM索引与InnoDB****索引的区别?
InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。
InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。
InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。
mysql数据类型
整数类型,包括TINYINT、SMALLINT、MEDIUMINT、INT、BIGINT,分别表示1字节、2字节、3字节、4字节、8字节整数。任何整数类型都可以加上UNSIGNED属性,表示数据是无符号的,即非负整数。整数类型可以被指定长度,例如:INT(11)表示长度为11的INT类型。长度在大多数场景是没有意义的,它不会限制值的合法范围,只会影响显示字符的个数,而且需要和UNSIGNEDZEROFILL属性配合使用才有意义。
例子,假定类型设定为INT(5),属性为UNSIGNEDZEROFILL,如果用户插入的数据为12的话,那么数据库实际存储数据为00012。
实数类型,包括FLOAT、DOUBLE、DECIMAL。DECIMAL可以用于存储比BIGINT还大的整型,能存储精确的小数。而FLOAT和DOUBLE是有取值范围的,并支持使用标准的浮点进行近似计算。计算时FLOAT和DOUBLE相比DECIMAL效率更高一些,DECIMAL你可以理解成是用字符串进行处理。
字符串类型,包括VARCHAR、CHAR、TEXT、BLOBVARCHAR用于存储可变长字符串,它比定长类型更节省空间。VARCHAR使用额外1或2个字节存储字符串长度。列长度小于255字节时,使用1字节表示,否则使用2字节表示。
VARCHAR存储的内容超出设置的长度时,内容会被截断。
CHAR是定长的,根据定义的字符串长度分配足够的空间。
CHAR会根据需要使用空格进行填充方便比较。
CHAR适合存储很短的字符串,或者所有值都接近同一个长度。
CHAR存储的内容超出设置的长度时,内容同样会被截断。
使用策略:
对于经常变更的数据来说,CHAR比VARCHAR更好,因为CHAR不容易产生碎片。
对于非常短的列,CHAR比VARCHAR在存储空间上更有效率。
使用时要注意只分配需要的空间,更长的列排序时会消耗更多内存。尽量避免使用TEXT/BLOB类型,查询时会使用临时表,导致严重的性能开销。
数据库三大范式
第一范式:每个列都不可以再拆分。
第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。
第三范式:在第二范式的基础上,非主键列只依赖于主键,不依赖于其他非主键。在设计数据库结构的时候,要尽量遵守三范式,如果不遵守,必须有足够的理由。比如性能。事实上我们经常会为了性能而妥协数据库的设计。
mysql用过的函数
MAX(expression)返回字段 expression 中的最大值
select Max(age) AS maxAge from t_student;
SUM(expression)返回指定字段的总和
select sum(age) as totalAge from t_student;
CONCAT(s1,s2…sn)字符串 s1,s2 等多个字符串合并为一个字符串
select concat(‘hell’,‘o’);
select current_time();
CURRENT_TIMESTAMP()返回当前日期和时间
select current_timestamp();
DATE_FORMAT按表达式 f的要求显示日期 d
select date_format(‘2020.12.12 12:24:23’,‘%Y-%m-%d %r’);
IF(expr,v1,v2)如果表达式 expr 成立,返回结果 v1;否则,返回结果 v2
select if(2>0,‘yes’,‘no’)
Redis
讲一讲Redis的数据类型,以及每种数据类型的使用场景
String(字符串)
- 简介:String 是 Redis 最基础的数据结构类型,它是二进制安全的,可以存储图片
- 或者序列化的对象,值最大存储为 512M
- 简单使用举例: set key value、get key等
- 应用场景:共享 session、分布式锁,计数器、限流。
Hash(哈希)
- 简介:在 Redis 中,哈希类型是指 v(值)本身又是一个键值对(k-v)结构
- 简单使用举例:hset key field value、hget key field
- 应用场景:缓存用户信息等
List(列表)
-
简介:列表(list)类型是用来存储多个有序✁字符串,一个列表最多可以存储2^32-1 个元素。
-
简单实用举例: lpush key value [value …] 、lrange key start end
-
lpush+lpop=Stack(栈)
lpush+rpop=Queue(队列)
lpsh+ltrim=Capped Collection(有限集合)
lpush+brpop=Message Queue(消息队列)
-
Set(集合)
- 简介:集合(set)类型也是用来保存多个✁字符串元素,但是不允许重复元素
- 简单使用举例:sadd key element [element …]、smembers key
- 应用场景: 用户标签,生成随机数抽奖、社交需求。
zset(有序集合)
- 简介:已排序的字符串集合,同时元素不能重复
- 简单格式举例:zadd key score member [score member …],zrank key
- 应用场景:排行榜,社交需求(如用户点赞)。
谈谈你对redis的了解
Redis是一个基于Key-Value存储结构的Nosql开源内存数据库。
它提供了5种常用的数据类型,String、Map、Set、ZSet、List。
针对不同的结构,可以解决不同场景的问题。
因此它可以覆盖应用开发中大部分的业务场景,比如top10问题、好友关注列表、热点话题等。
其次,由于Redis是基于内存存储,并且在数据结构上做了大量的优化所以IO性能比较好,在实际开发中,会把它作为应用与数据库之间的一个分布式缓存组件。
并且它又是一个非关系型数据的存储,不存在表之间的关联查询问题,所以它可以很好的提升应用程序的数据IO效率。
最后,作为企业级开发来说,它又提供了主从复制+哨兵、以及集群方式实现高可用在Redis集群里面,通过hash槽的方式实现了数据分片,进一步提升了性能。
Redis 为什么这么快?
我们都知道内存读写是比在磁盘快很多的,Redis 基于内存存储实现的数据库,相对于数据存在磁盘的 MySQL 数据库,省去磁盘 I/O的消耗。
我们知道,Mysql 索引为了提高效率,选择了 B+树的数据结构。其实合理的数据结构,就是可以让你的应用/程序更快。那Redis中string的实现就是一个SDS简单动态字符串。
IO多路复用机制,核心思想是让单个线程去监视多个连接,一旦某个连接就绪,也就是触发了读/写
事件。
就通知应用程序,去获取这个就绪的连接进行读写操作。
也就是在应用程序里面可以使用单个线程同时处理多个客户端连接,在对系统资源消耗较少的情况下
提升服务端的链接处理数量。
在IO多路复用机制的实现原理中,客户端请求到服务端后,此时客户端在传输数据过程中,为了避
免Server端在read客户端数据过程中阻塞,服务端会把该请求注册到Selector复路器上,服务端此时
不需要等待,只需要启动一个线程,通过selector.select()阻塞轮询复路器上就绪的channel即可,
也就是说,如果某个客户端连接数据传输完成,那么select()方法会返回就绪的channel,然后执行
相关的处理就可以了
缓存穿透,缓存雪崩,缓存击穿
缓存穿透
指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。
业务不合理的设计,比如大多数用户都没开守护,但是你的每个请求都去缓存,查
询某个 userid 查询有没有守护。
业务/运维/开发失误的操作,比如缓存和数据库的数据都被误删除了。
黑客非法请求攻击,比如黑客故意捏造大量非法请求,以读取不存在✁业务数据
如何避免缓存穿透呢? 一般有三种方法。
1.如果是非法请求,我们在 API 入口,对参数进行校验,过滤非法值。
2.如果查询数据库为空,我们可以给缓存设置个空值,或者默认值。但是如有有写
请求进来✁话,需要更新缓存哈,以保证缓存一致性,同时,最后给缓存设置适当
✁过期时间。(业务上比较常用,简单有效)
缓存雪崩问题
指缓存中数据大批量到过期时间,而查询数据量巨大,请求都直接访问数据库,引起数据库压力过大甚至 down 机
缓存雪奔一般是由于大量数据同时过期造成的,对于这个原因,可通过均匀设置过期时间解决,即让过期时间相对离散一点。如采用一个较大固定值+一个较小的随机值,5 小时+0 到 1800 秒酱紫。
Redis 故障宕机也可能引起缓存雪奔。这就需要构造 Redis 高可用集群啦
缓存击穿问题
指热点key 在某个时间点过期✁时候,而恰好在这个时间点对这个Key 有大量的并发请求过来,从而大量的请求打到 db。缓存击穿看着有点像,其实它两区别是,缓存雪奔是指数据库压力过大甚至down 机,缓存击穿只是大量并发请求到了 DB 数据库层面。可以认为击穿是缓存雪奔的一个子集吧。有些文章认为它俩区别,是区别在于击穿针对某一热点 key 缓存,雪奔则是很多 key
使用互斥锁方案。缓存失效时,不是立即去加载 db 数据,而是先使用某些带成
功返回的原子操作命令,如(Redis ✁ setnx)去操作,成功的时候,再去加载
db数据库数据和设置缓存。否则就去重试获取缓存。
2. “永不过期”,是指没有设置过期时间,但是热点数据快要过期时,异步线程去
更新和设置过期时间。
Redis过期策略
定时过期
每个设置过期时间的 key 都需要创一个定时器,到过期时间就会立即对 key进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量
✁ CPU 资源去处理过期的数据,从而影响缓存的响应时间和吞吐量
惰性过期
只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量✁过
期 key 没有再次被访问,从而不会被清除,占用大量内存
定期过期
每隔一定✁时间,会扫描一定数量✁数据库✁ expires 字典中一定数量✁key,并清除其中已过期✁ key。该策略是前两者✁一个折中方案。通过调整定时扫描✁时间间隔和每次扫描✁限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优✁平衡效果。
Redis的常用应用场景
缓存
排行榜
计数器应用
共享Session
分布式锁
社交网络
消息队列
位操作
Redis持久化方式
首先,Redis本身是一个基于Key-Value结构的内存数据库,为了避免Redis故障导致数据丢失的问题,
所以提供了RDB和AOF两种持久化机制。
RDB是通过快照的方式来实现持久化的,也就是说会根据快照的触发条件,把内存里面的数据快照写入到磁盘,
以二进制的压缩文件进行存储。RDB快照的触发方式有很多,比如执行bgsave命令触发异步快照,执行save命令触发同步快照,同步快照会阻塞客户端的执行指令。
根据redis.conf文件里面的配置,自动触发bgsave主从复制的时候触发AOF持久化,它是一种近乎实时的方式,把Redis Server执行的事务命令进行追加存储。
简单来说,就是客户端执行一个数据变更的操作,Redis Server就会把这个命令追加到aof缓冲区的末尾,然后再把缓冲区的数据写入到磁盘的AOF文件里面,至于最终什么时候真正持久化到磁盘,是根据刷盘的策略来决定的。
另外,因为AOF这种指令追加的方式,会造成AOF文件过大,带来明显的IO性能问题,所以Redis针对这种情况提供了AOF重写机制,也就是说当AOF文件的大小达到某个阈值的时候,就会把这个文件里面相同的指令进行压缩。
因此,基于对RDB和AOF的工作原理的理解,我认为RDB和AOF的优缺点有两个。
RDB是每隔一段时间触发持久化,因此数据安全性低,AOF可以做到实时持久化,数据安全性较高
RDB文件记录的是数据,AOF存储的是执行指令,所以RDB在数据恢复的时候性能比AOF要好
Linux技能
# 列出当前目录下的文件
ls
# 改变当前工作目录到/usr/local
cd /usr/local
# 打印当前工作目录的全路径
pwd
# 创建一个空文件 named.txt
touch named.txt
# 查看named.txt文件的内容
cat named.txt
# 复制文件named.txt到/home/user目录下
cp named.txt /home/user/
# 移动named.txt文件到/home/user目录下并重命名为named_new.txt
mv named.txt /home/user/named_new.txt
# 删除named_new.txt文件
rm named_new.txt
# 在所有.txt文件中查找单词"Java"
grep "Java" *.txt
# 在/home/user目录及其子目录下查找名为named.txt的文件
find /home/user -name named.txt
# 查看当前运行的所有进程
ps -aux
# 终止进程ID为1234的进程
kill 1234
# 查看实时运行的进程状态
top
# 查看内存和交换区的使用情况
free -m
# 查看磁盘空间的使用情况
df -h
# 查看/home目录的磁盘使用情况
du -sh /home
# 从网络下载一个文件
wget http://example.com/file.zip
# 打包文件为archive.tar
tar -cvf archive.tar file1 file2
# 解压archive.tar
tar -xvf archive.tar
# 安全地通过SSH登录到远程主机
ssh user@remotehost
微服务面试
早期我们一般认为的Spring Cloud五大组件是
- Eureka : 注册中心
- Ribbon : 负载均衡
- Feign : 远程调用
- Hystrix : 服务熔断
- Zuul/Gateway : 网关
随着SpringCloudAlibba在国内兴起 , 我们项目中使用了一些阿里巴巴的组件
- 注册中心/配置中心 Nacos
- 负载均衡 Ribbon
- 服务调用 Feign
- Hystrix : 服务熔断
- 服务网关 Gateway
只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量✁过
期 key 没有再次被访问,从而不会被清除,占用大量内存
定期过期
每隔一定✁时间,会扫描一定数量✁数据库✁ expires 字典中一定数量✁key,并清除其中已过期✁ key。该策略是前两者✁一个折中方案。通过调整定时扫描✁时间间隔和每次扫描✁限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优✁平衡效果。
Redis的常用应用场景
缓存
排行榜
计数器应用
共享Session
分布式锁
社交网络
消息队列
位操作
Redis持久化方式
[外链图片转存中…(img-MEp0OhjO-1713169048046)]
[外链图片转存中…(img-Do325n0Z-1713169048046)]
首先,Redis本身是一个基于Key-Value结构的内存数据库,为了避免Redis故障导致数据丢失的问题,
所以提供了RDB和AOF两种持久化机制。
RDB是通过快照的方式来实现持久化的,也就是说会根据快照的触发条件,把内存里面的数据快照写入到磁盘,
以二进制的压缩文件进行存储。RDB快照的触发方式有很多,比如执行bgsave命令触发异步快照,执行save命令触发同步快照,同步快照会阻塞客户端的执行指令。
根据redis.conf文件里面的配置,自动触发bgsave主从复制的时候触发AOF持久化,它是一种近乎实时的方式,把Redis Server执行的事务命令进行追加存储。
简单来说,就是客户端执行一个数据变更的操作,Redis Server就会把这个命令追加到aof缓冲区的末尾,然后再把缓冲区的数据写入到磁盘的AOF文件里面,至于最终什么时候真正持久化到磁盘,是根据刷盘的策略来决定的。
另外,因为AOF这种指令追加的方式,会造成AOF文件过大,带来明显的IO性能问题,所以Redis针对这种情况提供了AOF重写机制,也就是说当AOF文件的大小达到某个阈值的时候,就会把这个文件里面相同的指令进行压缩。
因此,基于对RDB和AOF的工作原理的理解,我认为RDB和AOF的优缺点有两个。
RDB是每隔一段时间触发持久化,因此数据安全性低,AOF可以做到实时持久化,数据安全性较高
RDB文件记录的是数据,AOF存储的是执行指令,所以RDB在数据恢复的时候性能比AOF要好
Linux技能
# 列出当前目录下的文件
ls
# 改变当前工作目录到/usr/local
cd /usr/local
# 打印当前工作目录的全路径
pwd
# 创建一个空文件 named.txt
touch named.txt
# 查看named.txt文件的内容
cat named.txt
# 复制文件named.txt到/home/user目录下
cp named.txt /home/user/
# 移动named.txt文件到/home/user目录下并重命名为named_new.txt
mv named.txt /home/user/named_new.txt
# 删除named_new.txt文件
rm named_new.txt
# 在所有.txt文件中查找单词"Java"
grep "Java" *.txt
# 在/home/user目录及其子目录下查找名为named.txt的文件
find /home/user -name named.txt
# 查看当前运行的所有进程
ps -aux
# 终止进程ID为1234的进程
kill 1234
# 查看实时运行的进程状态
top
# 查看内存和交换区的使用情况
free -m
# 查看磁盘空间的使用情况
df -h
# 查看/home目录的磁盘使用情况
du -sh /home
# 从网络下载一个文件
wget http://example.com/file.zip
# 打包文件为archive.tar
tar -cvf archive.tar file1 file2
# 解压archive.tar
tar -xvf archive.tar
# 安全地通过SSH登录到远程主机
ssh user@remotehost
微服务面试
[外链图片转存中…(img-4O8Gmqn1-1713169048046)]
早期我们一般认为的Spring Cloud五大组件是
- Eureka : 注册中心
- Ribbon : 负载均衡
- Feign : 远程调用
- Hystrix : 服务熔断
- Zuul/Gateway : 网关
随着SpringCloudAlibba在国内兴起 , 我们项目中使用了一些阿里巴巴的组件
- 注册中心/配置中心 Nacos
- 负载均衡 Ribbon
- 服务调用 Feign
- Hystrix : 服务熔断
- 服务网关 Gateway