面试题
java基础
值传递和引用传递
- 值传递:是对基本型变量而言的,传递的是 该变量的一个副本,改变副本不影响原变量
- 引用传递:一般是对于对象型变量而言的,传递的是该对象地址的一个副本,并不是原对象本身
- java内的传递都是值传递,java中实例对象的传递都是引用传递
string和stringbuffer和stringduilder的区别
- string:是不可变的字符序列,每次操作都会生成新的string对象,然后指向新的对象
- stringbuffer与stringbuilder:是可变的字符序列,是在原有的对象基础 上进行操作。
- stringbuffer是线程安全的,新能低,stringbuilder是线程不安全的,性能高。
底层:
- 这三个类都是由final修饰,都不能被继承
- 他们数据都是存在字符数组【jdk9变为字节数组】,string里面的字符数组由final修饰
a==b 与a.equals(b)有什么区别
- ==如果比较的是基本数据类型,比较的是值,如果比较的是引用数据类型 ,比较的是地址值
- equals默认比较的是地址,我们可以重写让它比较值
对equal()和hashcode的理解
- 为什么在重写equals方法的时候需要重写hashCode方法
- 因为有强制的规范指定需要同时重写hashcode与equals方法,许多容器类,如HashMap,HashSet都依赖于hashcode与equals的规定
- 有没有可能两个不相等的对象有相同的hashcode
- 有可能,两个不相等的对象可能会有相同的hashcode值,这就是为什么在hashmap中会有冲突。相等hashcode值得规定只是说如果两个对象相等,必须有相同得hashcode值,但是没有关于不相等对象的任何规定。
- 两个相同的对象会有不同的hashcode吗
- 不能,根据hashcode的规定,这是不允许的
this()& super()在构造方法中的区别
- 调用super()必须写在子类构造方法的第一行,否则编译不通过
- super从子类调用父类构造方法,this在同一类中调用其他构造均需要放在第一行
- 尽管可以用this调用一个构造器,却不能调用两个
- this和super不能出现在同一个构造器中,否则编译不通过
- this(),super()都指的对象,不可以在static环境中使用
- 本质this指向本对象的指针。super是一个关键字
八大基本数据类型
byte short int float lang double char boolean
&&与&的区别 &左边为false右边还要执行
||与|的区别 |左边为true右边还要判断
a++ ++a的区别 a++先赋值再自增 ++a先自增再赋值
String、StringBuffer与StringBuilder的区别
- 从可变和适用范围上看:
- String对象是不可变的
- StringBuffer和StringBuilder是可变字符序列
- 每次对String的操作相当于生成一个新的String对象,而对StringBuffer和StringBuilder的操作是对对象本身的操作,而不会生成新的对象,所以对于频繁改变内容的字符串避免使用String,因为频繁的生成对象将对系统性能产生影响。
- 从线程安全上看:
- String线程安全,String由于有final修饰,是immutable的,安全性是简单而纯粹的。
- StringBuilder和StringBuffer的区别在于StringBuilder不保证同步,也就是说如果需要线程安全需要使用Stringbuffer,不需要同步的Stringbuilder效率更高
Integer a= 127 与 Integer b = 127相等吗?
- 对于对象引用类型:比较的是对象的内存地址
- 对于基本数据类型:比较的是值
- 如果整型字面量的值在-128到127之间,那么自动装箱时不会new新的Integer对象,二十直接引用常量池中的Integer对象,超过范围a1==b1的结果是false
integer缓冲池
string常量池
java异常类层次结构
- **Throwable:**是 Java 语言中所有错误与异常的超类。
- Error 类及其子类:程序中无法处理的错误,表示运行应用程序中出现了严重的错误。
- Exception 程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。
-
**运行时异常:**都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
-
非运行时异常(编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
checkedException与RuntimeExcpetion区别?
- **checkedException:**必须显示处理,不处理编译不通过
- **RuntimeExcpetion:**可以处理也可以不处理
throw和throws的区别
- **throw:**我们可以把手动创建的一个异常抛给方法,跟的是异常对象
- **throws:**用在方法的声明后,跟的是异常的类名,用来申明方法有这个异常,可以同时申明多个异常的类名,用逗号隔开,由调用者处理
- throws表示异常的一种可能性 ,不一定会发生,调用这个方法可能会发生某种异常
final,finally,finalize的区别
- **final:**最终的,可以修饰类(修饰的类不能被继承),可以修饰成员变量(修饰变量就不能重新赋值),可以修饰成员方法(不能被重写)
- **finally:**是异常处理的一部分,放在里面的代码不管是否发生异常都会执行,一般用来存放一些关闭资源的代码
- **finalize:**object类中的一个方法,用于垃圾回收器回收对象的时候调用
字节流和字符流的区别
- 字节流的单位是字节,字符流的单位是字符、
- 字符流=字节流+编码 字符流只能处理字符
- 字符流是带缓冲的
序列化和反序列化
- 序列化:是使用序列化流把对象转换成字节
- 反序列化:是使用反序列化流把字节转换成对象
- 注意:用transient或者static修饰的属性不能进行序列化;需要序列化的对象所在的类实现serializable接口
object类有哪些常见方法?
- registerNatives():void
- getClass():Class<?>
- hashCode():int
- equals(Object):boolean
- clone():Object
- toString():String
- notify():void
- notifyAll():void
- wait(long):void
- wait(long,int):void
- wait():void
- finalize():void
short s1=1,s1=s1+1/short s1=1,s1+=1;有错吗?
- **short s1=1,s1=s1+1;**有错
- short s1=1,s1+=1;无错,执行s1+=1其实执行的是**s1=(short)(s1+1)**其中会有一个强制转换的过程
java的四种引用:强弱软虚
java创建对象有几种方式?
- new关键字
- Class.newInstance
- Constructor.newInstance
- Clone方法
- 反序列化
有没有可能两个不相等的对象有相同的hashcode?
- 有可能,在产生hash冲突时,两个不相等的对象就会有相同的hashcode值,当hash冲突时,一般有一下几种方式来处理
- 拉链法:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单项链表,被分配到同一个索引上的多个节点可以用这个单项链表进行存储
- 开放定址法:一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并讲记录存入
- List iniData = new ArrayList<>()
- 再哈希:又叫双哈希法,有多个不同的hash函数,当发生冲突时,使用第二个,第三个…等哈希函数计算地址,知道无冲突
深拷贝和浅拷贝的区别是什么?
- 浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象,换言之,浅拷贝仅仅复制所考虑的对象,而不复制它锁引用的对象
- 深拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象,换言之,深拷贝把要复制的对象所引用的对象都复制了一遍
UDP和TCP的区别
- TCP:是面向连接的,三次握手,两次挥手,数据传输时可靠的
- UDP:是面向无连接,不可靠的,传输的数据大小有限制(64kb)
说一下TCP三次握手机制及发送的数据报文?
tcp/ip模型
- OSI七层参考模型:物理层,数据链路层,网络层,传输层,会话层,表示层,应用层
- TCP/IP模型:
- 五层模型:物理层,数据链路层,网络层,传输层,应用层
- 四层模型:网络接口层,网络层,传输层,应用层
- tcp/udp是处于传输层;ip是处于网络层;thhp,ftp等是处于应用层
重写和重载的区别
- 重载:在同一个类中,方法名相同,参数列表不同(和参数的个数或者类型不同),与返回值无关
- 重写:在继承体系中,子类重写父类的方法,方法的申明要一致,如果父类方法被fianl修饰,那么就不能重写。子类不能抛出比父类更大的异常
注解
- 常见的元注解:
- @Target({ElementType.METHOD,
- ElementType.ANNOTATION_TYPE})@Retention(RetentionPolicy.RUNTIME)
- 自定义注解:
- @Perm:自定义权限注解
- @Log:自定义日志注解
IO和NIO的区别
IO(Input/Output)和NIO(New Input/Output)都是Java中处理输入输出的机制,它们之间的主要区别在于实现方式和特点:
- IO(传统的阻塞IO):
- 使用流(Stream)的方式进行数据的输入输出,数据按顺序从流中读取或写入。
- 每个IO操作都是阻塞的,即在执行IO操作时,线程会被阻塞直到数据准备好或操作完成。
- 对于每个连接都需要独立的线程进行处理,如果连接数很大,会导致线程资源消耗过多。
- NIO(非阻塞IO):
- 使用缓冲区(Buffer)和通道(Channel)的方式进行数据的输入输出。
- 支持非阻塞IO操作,即当没有数据可读取时,线程不会被阻塞,可以继续处理其他任务。
- NIO通过选择器(Selector)实现了单个线程管理多个通道,可以有效地处理大量的并发连接,提高了系统的扩展性和性能。
总的来说,NIO相对于传统的IO具有更高的并发性和性能,适用于需要处理大量并发连接的场景,如网络编程中的服务器端。但是,NIO编程相对复杂一些,需要更深入地理解缓冲区、通道和选择器等概念。而传统的IO编程则更简单直观,适用于一些简单的IO操作场景。
设计模式七大原则是什么
-
单一职责原则:告诉我们实现类要指责单一
- 里氏替换原则:告诉我们不要破环继承体系【继承父类而不是改变父类,最好不要重写父类上的方法,而是更多实现父类没有实现的方法】
- 依赖倒置原则:告诉我们要面向接口编程
- 接口隔离原则:告诉我们在设计接口的时候要精简单一
- 迪米特法则:告诉我们要降低耦合【无关的两个类之间交互一般引入中间者】
- 开闭原则:是总纲,它告诉我们要扩展开放,对修改关闭
- 合成复用原则:告诉多用组合少用继承
设计模式七大原则详解
1. 设计模式之单一职责原则
一个类只负责一项职责,不要存在 1 个以上导致类发生变更的原因。
优点:
a. 降低类的复杂度,一个类只负责一项职责,逻辑简单清晰;
b. 类的可读性,系统的可维护性更高;
c. 因需求变更引起的风险更低,降低对其它功能的影响。
总结:
只有逻辑足够简单,才可以在代码级别上违反单一职责原则;
只有类中方法数量足够少,才可以在方法级别上违反单一职责原则;
模块化的程序设计以及在员工工作安排上面,都适用单一职责原则。
2. 设计模式之里式替换原则
子类可以扩展父类的功能,不能改变父类原有的功能,子类可以替换父类,方法或者行为也没有改变
注意:
a. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法;
b. 子类中可以增加自己特有的方法;
c. 当子类的方法重载父类的方法时,方法的前置条件(形参)要比父类方法更宽松;
d. 当子类的方法实现父类的抽象方法时,方法的后置条件(返回值)要比父类更严格。
3.设计模式之依赖倒置原则
高层模块不应该依赖低层模块,二者都应该依赖其抽象
抽象不应该依赖细节,细节应该依赖抽象
理解:
a. 相对于细节的多变性,抽象的东西要稳定的多,以抽象为基础搭建起来的架构比以细节为基础搭
建起来的架构要稳定的多。这里,抽象指的是接口或者抽象类,细节就是具体的实现类,使用抽象
类或者接口的目的是,制定好规范和契约,不去涉及任何具体的操作,把展现细节的任务交给实现
类来完成;
b. 依赖倒置原则的核心思想是面向接口编程,达到解耦的过程。
注意:
a. 底层模块尽量都要有抽象类或者接口;
b. 变量的声明类型尽量是抽象类或接口;
c. 使用继承时遵循里式替换原则。
4.设计模式之接口隔离原则
客户端不应该依赖它不需要的接口
一个类对另一个类的依赖应该建立在最小的接口上面
理解:
a. 建立单一接口,尽量细化接口,接口中的方法尽量少;
b. 为单个类建立专用的接口,不要包含太多;
c. 依赖几个专用的接口要比依赖一个综合的接口更灵活,提高系统的灵活性和可维护性。
注意:
a. 接口尽量小,但是要有限度,过小则导致接口数量过多,设计复杂化;
b. 为依赖接口的类定制服务,只暴露给调用类需要的方法,建立最小的依赖关系;
c. 提高内聚,减少对外交互,用最少的方法去完成最多的事情。
和单一职责原则的对比:
a. 单一职责原则注重的是职责,而接口隔离原则注重对接口依赖的隔离;
b. 单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔
离原则主要约束接口,针对抽象和程序整体框架的构建。
5. 设计模式之迪米特法则
迪米特法则在于降低类之间的耦合,每个类尽量减少对其他类的依赖,尽量减少对外暴露的方法,使得
功能模块独立,低耦合
理解:
a. 只直接的朋友交流(成员变量、方法的输入输出参数中的类);
b. 减少对朋友的理解(减少一个类对外暴露的方法)。
注意:
a. 虽然可以避免和非直接的类通信,但是要通信,必然会通过一个”中介“来发生联系,过分的使用
迪米特原则,会产生大量的中介和传递类,导致系统复杂度变高。
6.设计模式之开闭原则
软件中的对象(类、模块、函数等)应该对于扩展是开放的,对于修改是封闭的
理解:
a. 当需求发生变化时,尽量扩展实体的行为来变化,而不是通过修改已有的代码来实现变化;
b. 低层模块的变化,必然有高层模块进行耦合,它并不意味着不做任何修改;
c. 这个原则比较虚,可以通过具体的设计模式的设计思维去加深理解。
7.合成复用原则
原则是尽量使用合成/聚合的方式,而不是使用继承
理解:
合成/聚合复用原则经常又叫做合成复用原则,就是在一个新的对象里面使用一些已有的对象,使之成为
新对象的一部分,新的对象通过这些对象的委派达到复用已有功能的目的。他的设计原则是:要尽量使
用合成/聚合,尽量不要使用继承。
接口和抽象类的区别?
- 接口有常量,抽象方法,(java8后:静态方法,default方法)
- 抽象类有构造方法,抽象方法,普通方法
- 接口和类是实现关系,抽象类和类是继承关系
集合有哪些类
- Set (无序,不重复)
- List o TreeSet 基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是 查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)
- HashSet 基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插 入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
- LinkedHashSet 具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。
- List (有序,可重复)
- ArrayList 基于动态数组实现,支持随机访问。
- Vector 和 ArrayList 类似,但它是线程安全的。
- LinkedList 基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除 元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
- Queue
- LinkedList 可以用它来实现双向队列。
- PriorityQueue 基于堆结构实现,可以用它来实现优先队列。
你用过HashMap吗?什么是HashMap?你为什么用到它?
- HashMap是 Java中最常用的集合类框架,使用它是基于它的几大特性: HashMap可 以接受null键值和值,而Hashtable则不能;
- HashMap是非synchronized;HashMap很快;
- 以及HashMap储存的是键值对
你知道HashMap的工作原理吗? get()方法的工作原理呢?
- HashMap是基于hashing的原理,我们使用put(key, value)存储对象到HashMap中, 使用 get(key)从 HashMap 中获取对象。
- 当我们给 put()方法传递键和值时,我们先对键调 用hashCode()方法,返回的hashCode用于找到bucket位置来储存Entry对象;
当两个对象的hashcode相同会发生什么?
- 因为 hashcode 相同,所以它们的 bucket 位置相同,‘碰撞’会发生。因为 HashMap 使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。
如果两个键的hashcode相同,你如何获取值对象?
- 当我们调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后 获取值对象;
- 如果有两个值对象储存在同一个 bucket, 将会遍历链表直到找到值对象;
- 找到 bucket 位置之后,会调用 keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象
如果 HashMap 的大小超过了负载因子(load factor)定义的 容量,怎么办?
- 默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候, 和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组, 来重新调整 map 的大小,并将原来的对象放入新的 bucket 数组中。这个过程叫作 rehashing,因为它调用hash方法找到新的bucket位置
重新调整HashMap大小存在什么问题吗?
- 当多线程的情况下,可能产生条件竞争(race condition); 所以多线程情况下更适合用 ConcurrentHashMap
为什么String, Interger 这样的 wrapper 类适合作为键?
- 因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法 了。其他的wrapper类也有这个特点
我们可以使用自定义的对象作为键吗?
- 可以使用任何对象作为键,只要它遵守了 equals()和 hashCode()方法的定义规则, 并且当对象插入到Map中之后将不会再改变了
我们可以使用 CocurrentHashMap 来代替 Hashtable 吗?
List和Set的区别
- List , Set 都是继承自 Collection 接口
- List 特点:元素有放入顺序,元素可重复
- Set 特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(元素虽然无放入顺序,但是元素在set 中的位 置是有该元素的 HashCode 决定的,其位置其实是固定的,加入Set 的 Object 必须定义 equals ()方法 ,另外list 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为 他无序,无法用下标来取得想 要的值。)
- Set 和List 对比
- Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变
- List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置 改变
ArrayList和LinkedList的区别
- ArrayList:底层是数组,查询快,增删慢
- LinkedList:底层是链表,查询慢,增删快
ArrayList自动扩容机制
- ArrayList底层是数组,默认初始化长度是10,如果长度不够会创建一个新数组扩容1.5倍,老数组长度+老数组长度右移一位=1.5倍(oldcapacity + oldcapacity >> 1)
ArrayList的底层理解
- ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入
null
元素,底层通过数组实现。除该类未实现同步外,其余跟Vector大致相同。每个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。前面已经提过,Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。
HashMap的数据结构
- 哈希表结构(链表散列:数组+链表)实现,结合数组和链表的优点。
- 当链表长度超过 8 时,链表转换为红黑树。
HashMap中put方法的过程
- 调用哈希函数获取Key对应的hash值,再计算其数组下标;
- 如果没有出现哈希冲突,则直接放入数组;如果出现哈希冲突,则以链表的方式放在链表后面;
- 如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表;
- 如果结点的key已经存在,则替换其value即可;
- 如果集合中的键值对大于12,调用resize方法进行数组扩容。
HashMap底层原理(HashSet底层原理)***
- HashMap的底层原理:HashMap在jdk1.8之前的实现方式是数组+链表,但是在jdk1.8之后对HashMap进行了底层优化,改为了由 数组+链表或者数值+红黑树 实现,主要的目的是提高查找效率
- jdk8数组+链表或者数组+红黑树实现,当链表中的 元素超过了8个以后,会将链表转换成红黑树,当红黑树节点小于等于6时又会退化为链表
- new HashMap():底层没有创建数组,首次调用put()方法时,底层创建长度为16的数组,jdk8底层的数组时:Node[]而非Entry[],用数组容量大小乘以加载因子得到一个值,一旦数组中存储的元素个数超过该值就会调用rehash方法将数组容量增加到原来的两倍,专业术语叫扩容,在做扩容的时候会生成一个新的数组,原来的所有数据需要重新计算哈希码值,重新分配到新的数组,所以扩容的操作非常消耗性能。默认的负载因子大小为0.75数组大小为16,也就是说,默认情况下,那么当HashMap中元素个数超过16_0.75=12的时候,就把数组的大小扩展为2_16=32,即扩大一倍。
- 在我们java中任何对象都有hashcode,hash算法就是通拓hashcode与自己进行向右位移16的异或运算。这样做时为了计算出来的hash值足够随机,足够分散,还有产生的数组下标足够随机
- map.put(k,v)实现原理 (1)首先将 k,v 封装到 Node 对象当中(节点)。 (2)先调用 k 的 hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。 (3)下标位置上如果没有任何元素,就把 Node 添加到这个位置上。如果说下标对应的位置上有 链表。此时,就会拿着 k 和链表上每个节点的 k 进行 equal。如果所有的 equals 方法返回都是 false,那么这个新的节点将被添加到链表的末尾。如其中有一个 equals 返回了true,那么这个节 点的 value 将会被覆盖。
- map.get(k)实现原理 (1)、先调用 k 的 hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标。 (2)、在通过数组下标快速定位到某个位置上。重点理解如果这个位置上什么都没有,则返回 null。 如果这个位置上有单向链表,那么它就会拿着参数 K 和单向链表上的每一个节点的 K 进行 equals,如果所有 equals 方法都返回 false,则 get 方法返回 null。如果其中一个节点的 K 和参数 K 进行 equals 返回 true,那么此时该节点的 value 就是我们要找的 value了,get 方法最终返回这 个要找的 value。
- Hash冲突:不同的对象算出来的数组下标是相同的这样就会产生hash冲突,当单线链表达到一定长度后效率会非常低。
- 在 链表长度大于8的时候,将链表变成红黑树,提高查询的效率
说说对红黑树的见解
- 每个节点非红即黑
- 根节点总是黑色的
- 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
- 每个叶子节点都是黑色的空节点(NIL节点)
- 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)
红黑树的特点
- 节点是RED或者BLACK
- 根节点是BLACK
- 叶子节点(外部节点,空节点都是BLACK
- 注意,这里的叶子节点和我们之前说的叶子节点不同,在红黑树中要保持任意的节点度都为2,所以就添加了null节点
- RED节点的子节点都是BLACK
- RED节点的parent都是BLACK,从根节点到叶子节点的所有路径上不能有2个连续的RED节点
- 从任一节点到叶子节点的所有路径都包含相同数目的BLACK节点
HashMap和Hashtable的区别
-
HashMap和Hashtable都实现了Map接口
-
HashMap是线程不安全的,效率高,Hashtable是线程安全的,效率低
-
HashMap的key和value都可以为空,Hashtable的key和value都不能为空
-
HashMap 默认初始化数组的大小为16,HashTable 为 11,前者扩容时,扩大两倍,后者扩大两倍 +1
-
HashMap 需要重新计算 hash 值,HashTable 直接使用对象的 hashCode
创建多线程的方式有哪些==***==
- 方式一:继承Thread
- 编写一个类继承thread
- 重写run方法
- 创建线程对象
- 调用start方法
- 方式二:实现Runnable接口
- 编写一个类实现Runnable接口
- 实现run方法
- 创建Runnable对象
- 创建Threa对象,吧Runnable对象作为构造函数的参数传递给了Thread对象
- 调用Thread对象的start方法开启线程
- 为什么有了第一种还会有第二种方式
- 可以避免java单继承带来的局限性
- 适合多个相同类型的线程处理同一个资源的情况,把线程运行的代码和线程本身进行了分离
- 面试题:开启一个线程是调用run方法还是start方法
- run方法是封装我们多线程要执行的代码,如果直接调用就是普通的调用方法
- start方法是通知jvm开启一个新的线程来执行run方法
线程生命周期
什么是线程池
- java.util.concurrent.Executors 提供了一个 java.util.concurrent.Executor 接口的 实现用于创建线程池
- 多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理 器单元的闲置时间,增加处理器单元的吞吐能力。
- 假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执 行任务的时间,T3 销毁线程时间。
- 如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
一个线程池包括以下四个基本组成部分:
- 线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池, 销毁线程池,添加新任务;
- 工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态, 可以循环的执行任务;
- 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的 执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态 等;
- 任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
- 线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序 性能的。它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些 空闲的时间段,这样在服务器程序处理客户请求时,不会有 T1,T3 的开销了。
- 线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目, 看一个例子:
- 假设一个服务器一天要处理50000个请求,并且每个请求需要一个单独的线程 完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池 中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为 50000。一般线程池大小是远小于50000。所以利用线程池的服务器程序不会 为了创建50000而在处理请求时浪费时间,从而提高效率。
常见线程池
- newSingleThreadExecutor 单个线程的线程池,即线程池中每次只有一个线程工作,单线程串行执行任务
- newFixedThreadExecutor(n) 固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数 量,然后后面进入等待队列直到前面的任务完成才继续执行
- newCacheThreadExecutor(推荐使用) 可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分 空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执 行。
- newScheduleThreadExecutor:大小无限制的线程池,支持定时和周期性的执行线程
创建线程池的正确方式
- 避免使用Executors创建线程池,主要是避免使用其中的默认实现,那么我们 可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建 的同时,给BlockQueue指定容量就可以了。
- 或者是使用开源类库:开源类库,如apache和guava等。
线程池常用参数
- corePoolSize:核心线程数量,会一直存在,除非 allowCoreThreadTimeOut 设置 为true
- maximumPoolSize:线程池允许的最大线程池数量
- keepAliveTime:线程数量超过corePoolSize,空闲线程的最大超时时间
- unit:超时时间的单位
- workQueue:工作队列,保存未执行的Runnable 任务
- threadFactory:创建线程的工厂类
- handler:当线程已满,工作队列也满了的时候,会被调用。被用来实现各种拒绝策略。
java获取 反射的三种方法
- 通过new对象实现反射机制
- 通过路径实现反射机制
- 通过类名实现反射机制
java反射机制原理
- java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
-
Class 类与 java.lang.reflect 类库一起对反射的概念进行了支持,该类库包含了 Field,Method,Constructor 类 (每 个类都实现了 Member 接口)。
这些类型的对象时由 JVM 在运行时创建的,用以表示未知类里对应的成员。
这样你就可以使用 Constructor 创建新的对象,用 get() 和 set() 方法读取和修改与 Field 对象关联的字段,用 invoke() 方法调用与 Method 对象关联的方法。
另外,还可以调用 getFields() getMethods() 和 getConstructors() 等很便利的方法,以返回表示字段,方法,以及构造器的对象的数组。
这样匿名对象的信息 就能在运行时被完全确定下来,而在编译时不需要知道任何事情。
什么是反射?反射的优缺点?
反射是指在运行时动态地获取类的信息并操作类或对象的属性、方法、构造函数等。反射的优点是可以在运行时动态地创建对象、调用方法,以及处理未知类型的对象;缺点是性能相对较低,因为需要在运行时进行类型检查和动态调用。
什么是事务?事务基本特性ACID?
-
什么是事务: 一系列的操作的要么同时成功,要么同时失败
-
事务基本特性ACID
- A原子性(atomicity) 指的是一个事务中的操作要么全部成功,要么全部失败
- **C一致性(consistency)**指的是数据库总是从一个一致性的状态转换到另外一个一致性的状态。比如A转账给B100块钱,假设中间sql执行过程中系统崩溃A也不会损失100块,因为事务没有提交,修改也就不会保存到数据库
- **I隔离性(isolation)**指的是一个事务的修改在最终提交前,对其他事务是不可见的
- D持久性(durability) 指的是一旦事务提交,所做的修改就会永久保存到数据库中
-
脏读: 一个事务 读到 另外一个事务未提交数据(脏数据)
-
不可重复读: 一个事务 读到了别人提交的数据
-
幻读: 强调是数据的插入操作。
-
事务的隔离级别:
- 未提交读(read-uncommitted):解决了丢失更新 还会存在脏读 和不可重复读 和幻读
- 提交读(read-committed):解决了丢失更新、脏读 ,还会存在不可重复读 和 幻读
- 可重复读(repeatable-read):解决了丢失更新、脏读、不可重复读 存在 幻读
- 串读(serializable):解决了所有问题(丢失更新、脏读、不可重复读、幻读)
- mysql默认的隔离级别是repeatable-read
-
怎么开启事务:
- start transaction;
- set autocommit=0;
-
jdbc怎么控制事务:
- connection.setAutoCommit(false);
- connection.commit()/rollback();
事务的隔离等级?
- 未提交读(READ UNCOMMITTED) 事务中的修改,即使没有提交,对其它事务也是可见的。
- 提交读(READ COMMITTED) 一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。
- 可重复读(REPEATABLE READ) 保证在同一个事务中多次读取同样数据的结果是一样的。
- 可串行化(SERIALIZABLE) 强制事务串行执行。
未完待续
javaWEB
Cookie和session的区别
Cookie和Session都是用来在Web应用中跟踪用户身份和状态的机制,但它们有一些重要的区别:
- 存储位置:
- Cookie: Cookie是存储在客户端(用户浏览器)中的小型文本文件,由服务器通过HTTP响应头发送给客户端,然后客户端在后续的HTTP请求中通过HTTP请求头将Cookie发送回服务器。
- Session: Session是存储在服务器端的数据结构,通常是一个全局的变量,用来存储用户的会话信息。服务器为每个用户创建一个唯一的会话ID,并将该ID存储在客户端的Cookie中,同时将对应的会话数据存储在服务器端。
- 内容:
- Cookie: Cookie通常包含一些标识符和值,用来唯一标识用户和存储少量的用户相关信息,如用户ID、用户名、登录状态等。
- Session: Session存储的内容可以更加丰富,可以存储用户的登录状态、购物车内容、用户偏好设置等任意数据。
- 安全性:
- Cookie: 由于存储在客户端,Cookie可能会受到一些安全威胁,如跨站脚本(XSS)攻击和跨站请求伪造(CSRF)攻击。为了增强安全性,可以通过设置Cookie的属性来限制Cookie的访问范围、过期时间等。
- Session: 由于存储在服务器端,Session相对于Cookie更加安全,客户端无法直接访问或修改Session数据。但是,如果服务器的会话管理实现不当,也可能存在会话劫持的安全风险。
- 生命周期:
- Cookie: 可以设置Cookie的过期时间,使得Cookie可以在客户端保存一段时间,即使用户关闭了浏览器后再次打开也能保持登录状态。可以通过设置持久性Cookie来实现长期存储,也可以设置会话Cookie来实现临时存储。
- Session: Session的生命周期通常与用户的访问会话(Session)相关联,当用户关闭浏览器或一段时间内没有活动时,服务器端的Session数据可能会被销毁。
综上所述,Cookie和Session在存储位置、内容、安全性和生命周期等方面有所不同,开发人员可以根据具体的需求和场景选择合适的机制来管理用户身份和状态。
servlet生命周期
- 加载servlet并实例化
- 只实例化一次
- 可以在第一次访问该servlet的时候实例化 ,也可以在web容器启动的时候实例化,如果容器启动时实例化,需要配置load-on-startup
- 初始化
- 初始化一次
- 实例化之后
- 第一次访问该servlet的时候 也可以在容器启动的时候,配置同上
- 提供服务
- 销毁
http协议
-
HTTP协议使用的是无状态的连接
- 不能存储用户的操作或数据信息
- 不能存储用户的身份信息
-
http://www.jd.com:80/index.html 背后发生的一些事情,越详细越好?
访··问"http://www.jd.com:80/index.html"这个URL时,会触发一系列的网络通信和操作,包括域名解析、建立TCP连接、发送HTTP请求、服务器响应、接收数据等。下面是详细的步骤:
- 域名解析(DNS解析): 当浏览器接收到用户输入的URL时,首先会进行域名解析,将域名"www.jd.com"解析为对应的IP地址。浏览器会先查找本地DNS缓存,如果没有找到对应的IP地址,则会向本地DNS服务器发起查询请求。
- 建立TCP连接: 一旦得到了目标服务器的IP地址,浏览器就会使用HTTP协议中的默认端口80(因为URL中指定了端口80)与目标服务器建立TCP连接。这个过程通常包括三次握手,确保客户端和服务器之间建立可靠的连接。
- 发送HTTP请求: 一旦TCP连接建立成功,浏览器会构造一个HTTP GET请求,请求的目标是指定的资源路径"/index.html"。这个请求消息包含了请求方法、资源路径、HTTP版本号、消息头等信息,并且发送给服务器。
- 服务器处理请求: 服务器收到HTTP请求后,会解析请求消息,根据请求的资源路径找到相应的资源,然后生成HTTP响应。在这个过程中,服务器可能会执行一些处理逻辑,如动态生成页面、查询数据库等。
- 服务器发送HTTP响应: 服务器生成HTTP响应后,会将响应消息发送回客户端。响应消息包含了状态行、消息头和消息体。状态行包含了响应状态码和状态描述,消息头包含了一些元数据信息,如Content-Type、Content-Length等,消息体包含了实际的响应内容。
- 接收响应并渲染页面: 浏览器接收到服务器的HTTP响应后,会根据响应的状态码和内容进行处理。如果响应状态码为200,表示请求成功,浏览器会解析响应内容,并根据内容类型进行相应的渲染。对于HTML页面,浏览器会解析HTML、CSS和JavaScript,并将页面呈现给用户。
- 关闭TCP连接: 当页面渲染完成后,浏览器会关闭与服务器之间的TCP连接,释放资源并结束通信。
以上就是访问"http://www.jd.com:80/index.html"背后发生的一些主要步骤,涉及了域名解析、TCP连接、HTTP请求和响应等过程。
-
常见响应状态码:
-
200:请求成功
-
301:Moved Permanently 永久性重定向。请求的资源已被永久的移动到新URI,返回信息会包括 新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替
-
302:Found 临时性重定向。与301类似。但资源只是临时被移动。客户端应继续使用原有URI
-
304: Not Modified 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。 客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资 源
-
401:Unauthorized 请求要求用户的身份认证
-
403:Forbidden拒绝执行此请求
-
404:Not Found 客户端所请求的资源找不到
-
405:方法禁用,禁用请求中指定的方法
-
500:Internal Server Error 服务器内部错误,无法完成请求
-
503:Service Unavailable 服务器暂时的无法处理客户端的请求
jsp其他
- jsp内置对象及作用
- request,session,application,page,pageContext,config,out,exception,response
- jsp中的三种指令:<%@ 指令 %> page include taglib
- jsp中的两种包含区别: <jsp:include> <%@include >
- 四种作用域:pageContext request session application
Mybatis
#{} 和 ${} 的区别是什么?
- #{}是预编译处理,${}是字符串替换。
- Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调用 PreparedStatement 的 set方法来赋值;
- Mybatis 在处理== = = 时,就是把 = = {}==时,就是把== ==时,就是把=={}==替换成变量的值。
- 使用#{}可以有效的防止 SQL 注入,提高系统安全性。
谈谈Mybatis的好处
-
简单易学,容易上手(相比于Hibernate) 基于SQL编程;
-
JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
-
很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库 MyBatis 都支持,而JDBC提供了可扩展性,所以只要这个数据库有针对Java的jar包就可以就可以与 MyBatis 兼容),开发人员不需要考虑数据库的差异性。
-
提供了很多第三方插件(分页插件 / 逆向工程);
-
能够与Spring很好的集成;
-
MyBatis 相当灵活,不会对应用程序或者数据库的现有设计强加任何影响,SQL写在XML里,从程序代码中彻底分离,解除sql与程序代码的耦合,便于统一管理和优化,并可重用。
-
提供XML标签,支持编写动态SQL语句。
-
提供映射标签,支持对象与数据库的ORM字段关系映射。
-
提供对象关系映射标签,支持对象关系组建维护。
MyBatis 是一个持久层框架,它通过简化数据库操作和提供灵活性来帮助开发者更轻松地管理数据库交互。以下是 MyBatis 带来的一些好处:
- 简化 SQL 操作: MyBatis 将 SQL 语句与 Java 代码解耦,通过 XML 文件或注解方式管理 SQL,使得 SQL 语句的编写和维护更加简单。开发者无需在代码中硬编码 SQL,而是可以将 SQL 语句集中管理,易于维护和修改。
- 灵活性: MyBatis 提供了灵活的映射配置机制,可以将查询结果映射到 POJO(Plain Old Java Object)、Map、基本数据类型等不同的数据结构中,满足不同场景下的需求。开发者可以根据需要灵活地配置对象映射关系。
- 高度可控性: MyBatis 提供了丰富的配置选项和扩展点,可以对 SQL 执行过程进行精细化控制。例如,可以配置缓存、参数映射、结果集处理等,以及自定义插件来扩展 MyBatis 的功能。
- 性能优化: MyBatis 支持多种缓存机制,可以减少数据库访问次数,提高系统性能。通过合理地配置缓存,可以有效地减少数据库查询压力,提升系统响应速度。
- 与现有系统集成简便: MyBatis 不会对现有的 Java 代码和数据库架构做出太多限制,可以与已有系统集成得相对简便。开发者可以根据需要选择性地使用 MyBatis,无需对现有系统做出太多改动。
- 广泛的社区支持和文档资源: MyBatis 是一个开源项目,拥有庞大的社区支持和丰富的文档资源。开发者可以通过阅读官方文档、参与社区讨论等方式获取帮助和支持。
总的来说,MyBatis 的好处包括简化 SQL 操作、灵活性、高度可控性、性能优化、与现有系统集成简便以及广泛的社区支持和文档资源等。这些特点使得 MyBatis 成为许多 Java 项目中常用的持久层框架之一。
请说说MyBatis的工作原理?
- 读取MyBatis的配置文件。mybatis-config.xml 为 MyBatis 的全局配置文件,用于配置数据库连接信 息。
- 加载映射文件。映射文件即SQL映射文件,该文件中配置了操作数据库的SQL语句,需要在MyBatis 配置文件mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数 据库中的一张表。
- 构造会话工厂。通过MyBatis的环境配置信息构建会话工厂SqlSessionFactory。
- 创建会话对象。由会话工厂创建SqlSession对象,该对象中包含了执行SQL语句的所有方法。
- Executor 执行器。MyBatis 底层定义了一个Executor 接口来操作数据库,它将根据SqlSession 传递 的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护。
- MappedStatement 对象。在Executor 接口的执行方法中有一个MappedStatement 类型的参数,该 参数是对映射信息的封装,用于存储要映射的SQL语句的id、参数等信息。
- 输入参数映射。输入参数类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。 输入参数映射过程类似于JDBC对preparedStatement对象设置参数的过程。
- 输出结果映射。输出结果类型可以是Map、List等集合类型,也可以是基本数据类型和POJO类型。 输出结果映射过程类似于JDBC对结果集的解析过程。
- 流程图:
- 源码上的体现:
- 根据配置文件(全局,sql映射)初始化出Configuration对象
- 创建一个DefaultSqlSession对象,他里面包含Configuration以及Executor(根据全局配置文件中的defaultExecutorType创建出对应的Executor)
- DefaultSqlSessioon.getMapper():拿到Mapper接口对应的MapperProxy;
- MapperProxy里面有(DefaultSqlSession);
- 执行增删改查方法:
- 调用DefaultSqlSession的增删改查(Exectuor)
- 会创建一个StatementHandler对象 。(同时也会创建出ParameterHander和ResultSetHandler)
- 调用StatementHandler预编译参数以及设置参数数值;使用ParameterHandler来给sql设置参数
- 调用StatementHandler的增删改查方法;
- ResultSetHandler封装结果
MyBatis 与 Hibernate 有哪些不同?
- mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句。
- mybatis直接编写原生态sql,可以严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求 不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是 mybatis 无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套sql 映射 文件,工作量大。
- Hibernate 对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用 hibernate 开发可以节省很多代码,提高效率。
Mybatis-Plus 跟 Mybatis 的区别
- Mybatis-Plus 是一个 Mybatis 的增强工具,它在Mybatis 的基础上做了增强,却不做改变。我们在使用 Mybatis-Plus 之后既可以使用Mybatis-Plus 的特有功能,又能够正常使用Mybatis的原生功能。
- Mybatis-Plus(以下简称 MP)是为简化开发、提高开发效率而生,但它也提供了一些很有意思的插件,比如 SQL 性能监控、乐观锁、执行分析等。
mybatis 传递参数的方式
- 第一种方式 匿名参数 顺序传递参数
- 第二种方式 使用@Param注解
- 使用Map传递参数
- 使用java bean 传递多个参数
- 直接使用json传递参数
- 传递集合类型传递List、Set、Array
- 参数类型为对象+集合
Mybatis 动态sql有什么用?执行原理?有哪些动态 sql?
- Mybatis 动态sq 可以在Xml映射文件内,以标签的形式编写动态sql,执行原理是根 据表达式的值完成逻 辑判断并动态拼接sql的功能。
- Mybatis 提供了9种动态sql标签 :trim | where | set | foreach | if | choose| when | otherwise
Mybatis怎么实现分页?(利用插件pagehelper)
- 分页插件的基本原理 是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句 和物理分页参数
插件原理?
- 再Mybatis中插件时通过拦截器来实现的,那么既然是通过拦截器来实现的,就会有一个问题,哪些对象才允许被拦截呢?
- 四大对象:Executor,Statement Handler,Parameter Handler,ResultSetHandler
- Mybatis只能针对上面的四大对象进行拦截
- 上面四个对象创建好之后并没有直接返回,二十都调用executor=interceptorChain.pluginAll(executor);将上面四个对象都放到pluginAll做了一个处理,又返回了一个该对象
- 里面用了对原对象进行的动态代理,代理的时候,加入了拦截器的执行,但是并不是这四大对象中的所有方法都能被拦截
Mybatis使用了哪些设计模式?
- 建造者模式:SqlSessionFactoryBuiler、XMLConfigBuiler
- 代理模式:MappedProxy使用的是jdk的动态代理
- 工厂 模式:SqlSessionFactory
- 模板方法:BaseExcutor和SimpleExecutor
Mybatis 的一级缓存,二级缓存有什么区别?
- 一级缓存:基于PerpetualCache的HashMap本地缓存,其存储作用域为Session,当 Session flush或 close 之后,该 Session 中的所有Cache就将清空,默认打开一级缓存。
- 二级缓存与一级缓存其机制相同,默认也是采用PerpetualCache,HashMap存储,不同在于其存储作 用域为Mapper(Namespace),并且可自定义存储源,如Ehcache。默认不打开二级缓存,要开启二级缓存, 使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ;
- 对于缓存数据更新机制,当某一个作用域(一级缓存Session/二级缓存Namespaces)的进行了C/U/D操 作后,默认该作用域下所有select中的缓存将被clear。
- 查询的顺序是:
- 先查询二级缓存,因为二级缓存中可能会有其他程序已经查出来 的数据,可以直接拿来使用。
- 如果二级缓存没有命中,再查询一级缓存
- 如果一级缓存也没有命中,则查询数据库
- SqlSession关闭之前,以及缓存中的数据会写入二级缓存
通常一个Xml映射文件,都会写一个Dao接口与之对应, 请问,这个Dao 接口的工作原理是什么?Dao接口里的方法, 参数不同时,方法能重载吗?
- Dao 接口,就是人们常说的Mapper接口,接口的全限名,就是映射文件中的namespace的值,接口的方 法名,就是映射文件中MappedStatement的id值,接口方法内的参数,就是传递给sql的参数。Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MappedStatement,
- 举例: com.mybatis3.mappers.StudentDao.findStudentById , 可 以 唯 一 找 到 namespace 为 com.mybatis3.mappers.StudentDao 下面 id = findStudentById 的 MappedStatement。在 Mybatis 中,每 一个标签,都会被解析为一个MappedStatement 对象。 Dao 接口里的方法,是不能重载的,因 为是全限名+方法名的保存和寻找策略。
- Dao 接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对 象,代理对象proxy会拦 截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返 回。
Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?
- Mybatis仅支持assoctation关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。再Mybatis配置文件中,可以配置是否启用延迟加载
- lazyLoadingEnabled=true|false,默认是关闭的,使用的时候再发送sql去进行查询
- 他的原理是使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
- 不光是Mybatis,几乎所有的包括Hibernate,支持 延迟加载的原理都是一样的
Spring
谈谈你对Spring的理解?
- spring从狭义上理解是一个轻量级的框架,主要是针对早期的重量级EJB而言,可以看作是一个大杂烩,整合各种框架,管理了很多的bean,底层由最基础的ioc和aop支撑
- spring从广义是一个生态,包括
- spring framework
- springboot
- springcloud
- spring data
- spring Integration
- spring batch
- spring security
- spring amqp和消息中间件的集成
- spring session
- spring cache等
你用过spring的哪些?spring框架由哪些好处?
- 用过:spring ioc,spring test,springjdbc,spring的声明性事务,springmvc,spring data,spring cloud,springboot等
- 好处主要谈ioc和aop
- ioc:主要解决问题就是解耦
- aop:主要是让与核心业务无关的业务独立出来,做成切面,让开发者更关注核心业务开发,在需要的时候将切面切入即可。
说一下spring的ioc和di
- IOC:inverse of control:控制反转,把对象的创建权交给spring进行管理,由spring利用反射来创建对象,然后把创建好的对象放入spring的map集合中
- 没有用spring之前:我们创建对象等操作需要我们自己主动去创建,控制权在我们手上
- 用了spring之后:对象的创建等工作都交给spring完成,我们只需要问spring要就行了,控制权发生了反转
- DI:依赖注入,在创建对象的过程中给属性赋值
- dependence injection,依赖注入,原来获取对象,需要自己去创建操作,现在需要对象,是spring直接注入给我们的(比如,set注入,构造器注入等)
BeanFactory和ApplicationContext有什么区别?
- ApplicationContext是BeanFactory的子接口,既然是子接口,那么肯定对BeanFactory做了增强,做了这些方面的增强:
- 国际化
- 事件发布机制
- 资源文件的访问方式等
spring中scope属性(bean作用范围)
- singleton单例
- prototype多例
- request【新版本中取消】
- session【新版本中取消】
- global session【新版本中取消】
- 默认是singleton,在scope上进行配置
spring创建对象的方式有哪些
- 构造器实例化
- 静态工厂实例化
- 实例化工厂实例化
- 实现FactoryBean接口
spring Bean是线程安全的吗
- bean默认是单例的bean,也就是线程不安全的
BeanFactory和FactoryBean的区别
- BeanFactory:
- 是管理所有spring容器的父接口,他就是一个工厂,它管理的bean的创建工作是要符合bean的生命周期的
- FactoryBean:
- 它是个bean,只不过这个bean不是一个简单的bean,是能创建和产生对象的一个工厂bean,我们可以自定义bean的创建流程,不需要按bean的生命周期来创建。此接口包含了三个方法:
- isSingleton:判断是否是单例对象
- getObjectType:获取对象的类型
- getObject:创建对象的方法,可以自己去new创建,可以使用代理创建都可以
- 它是个bean,只不过这个bean不是一个简单的bean,是能创建和产生对象的一个工厂bean,我们可以自定义bean的创建流程,不需要按bean的生命周期来创建。此接口包含了三个方法:
- FactoryBean使用场景:
- spring整合第三方技术,第三方技术对象需要交给spring管理,这个第三方对象的创建过程非常复杂,这个时候只需要 第三方技术编写一个FactoryBean对象来创建直接需要交给spring管理的对象,spring只需要配置这个工厂,然后调用这个工厂的getObject方法来交给spring管理
- 如果只有接口,然后需要用动态代理为接口创建代理对象,我们可以通过FactoryBean来编写创建代理对象的代码,然后把代理对象交给spring管理
说一下Resoutce和Autowired的区别
- Resource是jdk自带的注解,默认按照名称注入,如果找不到再按照类型注入
- Autowired是spring提供的注入,按照类型注入,如果注入的接口存在多个实现类,可以用@Primary注解指定默认注入的实现类
- @Autowired注解,是byType和byName的结合。@Autowired是先byType,如果找到多个则byName。
- @Autowired注解可以写在:
- 属性上:先根据属性类型去找Bean,如果找到多个再根据属性名确定一个
- 构造方法上:先根据方法参数类型去找Bean,如果找到多个再根据参数名确定一个
- set方法上:先根据方法参数类型去找Bean,如果找到多个再根据参数名确定一个
spring bean的生命周期?***
- 实例化所有实现而来BeanFactoryPostProcessor接口
- 执行实现了BeanFactoryPostProcessor的postProcessBeanFactory方法
- 实例化所有的实现BeanPostProcess的接口的实现
- bean的构造器
- 注入属性
- 执行BeanPostProcess的接口的实现对应的before方法
- 调用实现了InitialzingBean的after方法
- bean的init方法
- 执行BeanPostProcess的接口的实现对应的after方法
- 调用DisposibleBean的destroy方法
- 调用bean的destroy方法
spring的常见注入方式
- 通过setert方法注入
- 通过构造方法注入
- P空间注入【了解】
- spel注入【了解】
spring是如何解决循环依赖的?***
- 是依靠三级缓存解决的,怎么解决的:
- 一级缓存:singletonObjects,存放已经经历了完整生命周期的Bean对象
- 二级缓存:earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完)
- 三级缓存:singletonFactories,存放可以生成Bean的工厂
- 具体背以下16点:
- 获取A对象,首先检查singletonObjects(单例池)有不有,发现这里没有
- 开始创建A对象,创建A对象再检查singletonObjects(单例池)有不有 ,也就是双重检查,没有就将A对象的名字a加入到singletonsCurrentInCreation(正在创建的单例对象的名字)中
- 开始真正创建对象,于是创建了A对象
- 创建了A对象之后就将创建的结果保存在singletonFactories中,注意保存的并不是a和对象的对应关系,而是 a和一个lamda,原因是这个lamda还可以执行SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference来进行增强
- 现在要对A进行赋值,发现要装配B
- 这时就从容器中获取B,获取B对象的时候就发生了和获取A一样的流程
- 先检查singletonObjects(单例池)有不有B,发现这里没有
- 开始创建B对象,创建B对象再检查singletonObjects(单例池)有不有 ,也就是双重检查,没有就将B对象的名字b加入到singletonsCurrentInCreation(正在创建的单例对象的名字)中
- 开始真正创建对象,于是创建了B对象
- 创建了B对象之后就将创建的结果保存在singletonFactories中,注意保存的并不是b和对象的对应关系,而是 b和一个lamda,原因是这个lamda还可以执行SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference来进行增强
- 现在要对B进行赋值,发现要装配A
- 又去获取A,先检查singletonObjects(单例池)有不有,发现这里没有,然后发现A的名字在singletonsCurrentInCreation(正在创建中)就去earlySingletonObjects中有不有,这里没有就去singletonFactories中获取A,获取到之后将A从singletonFactories中移除,并添加到earlySingletonObjects中
- B赋值结束,B初始化,B初始化结束之后,要进行暴露B,这时将b从singletonsCurrentInCreation中移除,并将b从singletonFactories中移除,然后从将B的实例添加到singletonObjects中
- 这时回到A之前装配B的流程,这时已经将将B赋值给A
- 进入A的初始化完成之后会暴露单例对象
- 这时会先检查singletonObjects(单例池)有不有,没有就去earlySingletonObjects找到了A,此时A从singletonsCurrentInCreation中移除,并将A从singletonFactories中移除,从earlySingletonObjects中移除,然后从将A的实例添加到singletonObjects中
- 为什么要用三级,不用两级?
- 使用三级缓存而非二级缓存并不是因为只有三级缓存才能解决循环引用问题,其实二级缓存同样也能很好解决循环引用问题。使用三级缓存而非耳机缓存并非出于IOC的考虑,而是出于AOP的考虑,即若使用二级缓存,在AOP情形下,注入到其他bean的,不是最终的代理对象,而是原始对象
聊一下springAop
- Aop是一种面向切面的思想,纵向重复代码横向抽取。springAop底层是用动态代理实现的,如果实现了接口就采用jdk动态代理,如果没有实现接口采用cglib动态代理。
- 我们编写Advice,增强的代码,我们肯定要通过配置的方式告诉spring,spring把增强的代码和被代理对象结合起来去创建代理对象
- 使用AOP的目的在于:让与核心业务无关的功能独立出来做成切面,在使用的时候切入即可,让开发者更多的关注核心业务
- 应用场景:
- 阐述springaop管理事务( 自己构思)
- aop实现注解权限控制
- 记录日志
springAOP是什么?AOP的配置?AOP常见术语?
- AOP是面向切面编程,是让开发者更关注核心业务的开发,将与核心业务不太相关的功能剥离出来做成切面,需要的时候简单切入即可
- AOP配置:(自己构思)
- 常见术语:
- 切面:对某方面的关注,比如对日志的关注就叫日志切面,对权限的关注,就叫权限的切面
- 通知:将切面中的某个逻辑应用到目标对象上,切入的逻辑就是定义在通知中的
- 切入点:将切面里的通知应用到哪些地方去,这些地方是一套表达式构成的,我们称为是切入点
- 连接点:切到某个具体的方法,这个方法就是我们的连接点
Spring AOP有什么作用?你哪些地方使用了AOP?
- 权限检查
- 记录日志
- 某个方法的执行时长
- spring的声明性事务
spring aop和AspectJ aop有什么区别?
-
首先明白AspectJ是Eclipse旗下的一个项目,spring aop中集成了AspectJ
-
区别:
-
spring aop:基于代理(Proxying)
AspectJ:基于字节码操作(Bytecode Manipulation)
-
spring aop术语运行时增强,而AspectJ是编译时增强
spring aop基于动态代理(Peoxying),而AspectJ基于字节码操作(Bytecode Manipulation)
-
spring aop采用的动态织入,而AspectJ是静态织入
-
aop原理【源码】
- 第一步:@EnableAspectJAutoProxy
- 在invokeBeanFactoryPostProcessors(beanFactory);里面会有一个ConfigurationClassPostProcessor
的后置处理器来处理配置类,处理配置类的时候里面会专门处理@Import,会将所有的实现了ImportBeanDefinitionRegistrar 的加入到容器中,在ConfigurationClassBeanDefinitionReader里面的loadBeanDefinitionsFromRegistrars会拿到容器中的所有的ImportBeanDefinitionRegistrar进行执行,
会执行到AspectJAutoProxyRegistrar里面的registerBeanDefinitions方法
@EnableAspectJAutoProxy ->@Import(AspectJAutoProxyRegistrar.class)
AspectJAutoProxyRegistrar这个注册器向注册中心导入了一个BeanDefinition,这个BeanDefintion的类型是AnnotationAwareAspectJAutoProxyCreator,名字是org.springframework.aop.config.internalAutoProxyCreator
- 在invokeBeanFactoryPostProcessors(beanFactory);里面会有一个ConfigurationClassPostProcessor
- 第二步:AnnotationAwareAspectJAutoProxyCreator的创建
- 在registerBeanPostProcessors(beanFactory);这步中完成AnnotationAwareAspectJAutoProxyCreator
的创建,并放到BeanPostProcessor的容器中
- 在registerBeanPostProcessors(beanFactory);这步中完成AnnotationAwareAspectJAutoProxyCreator
- 第三步:AnnotationAwareAspectJAutoProxyCreator的方法什么时候被调用
- 这一步其实就是找到切面,并将切面中的带通知注解的方法封装为Advisor保存起来要注意执行时机,是随便一个后置处理器在创建的时候, AnnotationAwareAspectJAutoProxyCreator的回调方法会执行,任意一个后置处理器在创建的时候 AnnotationAwareAspectJAutoProxyCreator就干预了,找到容器中所有的Object类型的组件,挨个判断是否是切面
,并遍历每个切面,找到切面中的所有方法,每个方法判断是否是通知方法(有不有PointCut.class, Before.class,Around.class等)。并将通知方法封装为Advisor(增强器)
- 这一步其实就是找到切面,并将切面中的带通知注解的方法封装为Advisor保存起来要注意执行时机,是随便一个后置处理器在创建的时候, AnnotationAwareAspectJAutoProxyCreator的回调方法会执行,任意一个后置处理器在创建的时候 AnnotationAwareAspectJAutoProxyCreator就干预了,找到容器中所有的Object类型的组件,挨个判断是否是切面
- 第四步:get某个目标对象
- get某个目标对象的时候,在目标对象已经赋值结束之后,有个后置处理器postProcessorAfterInitionlization会执行,挨个判断所有的增强器的正则会不会切入到这个对象,如果可以,就为这个对象创建代理
- 第五步:为对象创建代理
- 第六步:执行拦截器方法
- 把所有的增强器(只是保存了信息)转换为拦截器(可以执行方法),并将拦截器保存在代理对象里面然后采用拦截器链的执行方法进行执行
aop中常见的通知类型?
- 前置通知Before:目标方法被调用之前调用该通知的功能
- 后置通知After:目标方法完成之后调用
- 环绕通知Around:在被通知的方法调用前后调用后执行,自定义的功能
- 返回通知After-returning:目标方法成功执行后调用该通知的功能
- 异常通知After-throwing:目标方法抛出异常后调用该通知功能
说说你在spring中用到的常见annotation?
- @Component,@Service,@Repository,@Bean,@Configuration,@Priamry,@Resource,@Autowired,@scope等
spring中事务的传播特性
-
PROPAGATION_REQUIRED
- 如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,这是最常见的选择,也是Spring默认的事务传播行为。
-
PROPAGATION_SUPPORTS
- 支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
-
PROPAGATION_MANDATORY
- 支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
-
PROPAGATION_REQUIRES_NEW
- 创建新事务,无论当前存不存在事务,都创建新事务。
-
PROPAGATION_NOT_SUPPORTED
- 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
-
PROPAGATION_NEVER
- 以非事务方式执行,如果当前存在事务,则抛出异常。
-
PROPAGATION_NESTED
- 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。
spring容器启动过程
spring中用到了哪些设计模式?
- 简单工厂
- BeanFactory就是简单工厂设计模式体现,根据传入一个唯一标识来获取Bean对象
- 工厂方法模式
- FActoryBean是个工厂bean,使用了工厂方法模式,是一个可以产生对象的工厂bean,由spring管理后,spring自动调用getObject方法获取具体需要创建的对象交给spring管理
- 单例模式
- 一个类仅有一个实例,spring创建实例默认是单例的
- 适配器模式
- 在springMVC中,DispatcherServlet作为前端控制器,HandlerAdapter作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller作为需要适配的类
- 代理模式
- spring的aop使用的动态代理,jdk和cglib
- 观察者模式
- spring中obsever模式常用的地方是listener的实现,如ApplicationListener
- 模板模式
- spring中的RedisTemplate,觉得不错Template,KafaTeplate使用了模板设计模式
- 解释器:spel
- 责任链模式
- HandlerExecutionChain,其中我们可以看到,在springMVC中,DispatcherServlet这个核心类中使用到了HandlerExecutionChain这个类,他就是责任链模式实行的具体类。在DispatcherServlet的doDispatch这个方法中,我们可以看到它贯穿了整个请求dispatch的流程
spring事务实现方式
- 编程式事务管理:这意味着你可以通过编程的方式管理事务,这种方式带来了很大的灵活性,但很难维护。
- 声明式事务管理:这种方式意味着你可以将事务管理和业务代码分离,你只需要通过注解或者xml配置管理事务
设计模式七大原则
- 单一职责原则(Single Responsibility Principle)
每一个类应该专注于做一件事情。
-
里氏替换原则(Liskov Substitution Principle)
超类存在的地方,子类是可以替换的。 -
依赖倒置原则(Dependence Inversion Principle)
实现尽量依赖抽象,不依赖具体实现。 -
接口隔离原则(Interface Segregation Principle)
应当为客户端提供尽可能小的单独的接口,而不是提供大的总的接口。 -
迪米特法则(Law Of Demeter)
又叫最少知识原则,一个软件实体应当尽可能少的与其他实体发生相互作用。 -
开闭原则(Open Close Principle)
面向扩展开放,面向修改关闭。 -
组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP)
尽量使用合成/聚合达到复用,尽量少用继承。原则: 一个类中有另一个类的对象。
springmvc
springmvc请求流程
- 客户端发起请求后被DispatcherServlet拦截,DispatcherServlet首先要去匹配对应的 handlerMapping,找到对应的handlerMapping 并从中找到对应的handler,并返回给DispatcherServlet
- 根据handler去适配对应的handlerAdapter,通过handlerAdapter去执行对应的handler,返回 modelandview 交给dispatcherServlet
- dispatcherServlet收到handler返回的modelandview 去请求对应的视图解析器,最终生成view对 象
- 最后将view进行渲染返回给客户端
springmvc中常见组件
- MultipartResolver:文件上传解析器
- LocaleResolver国际化解析器
- ThemeResolver:主题解析器
- HandlerMapping:Handler(处理器、能处理请求的人(Controller))的映射:【保存的就是所 有请求都由谁来处理的映射关系】
- HandlerAdapter:Handler(处理器、能处理请求的人(Controller))的适配器;【超级反射工 具】 ,使用反射调用到映射器里的方法
- HandlerExceptionResolver:Handler的异常解析器,异常处理功能
- RequestToViewNameTranslator:把请求转成视图名(我们要跳转的页面地址)的翻译器【没啥 用】 spring的官方文档都没有解释
- FlashMapManager:重定向和转发,由于重定向之后会丢失request中的信息,FlashMap 借助 session 重定向前通过 FlashMapManager将信息放入FlashMap,重定向后 再借助 FlashMapManager 从 session中找到重定向后需要的 FalshMap
springmvc异常是怎么处理的?
- 遍历所有的异常处理器找哪个异常处理器能处理我这个异常,找到之后就使用这个异常处理器来处理
- 系统默认的异常处理器:
- ExceptionHandlerExceptionResolver:在异常类上标注@ExceptionHandler的
- ResponseStatusExceptionResolver:在异常类上标注@ResponseStatus
- DefaultHandlerExceptionResolver:异常是否是我指定的一些异常,如果是我就能处理
拦截器与过滤器?
- 过滤器 需要web容器的支持,拦截器不需要
- 过滤器可以拦截一切资源,拦截器只能拦截到controller的请求
- 自定义拦截器:
- 实现HandlerInterceptor
- 重写里面的preHandle postHandle afterCompletion 方法,要知道三个方法的执行时机
- 配置哪些资源被拦截
- 拦截器作用: 1、权限拦截 2、token拦截 3、黑名单等。
spring的父子容器?
- spring+springmvc环境有两个spring容器
- spring容器 监听器启动进行加载 父容器
- springmvc容器 servlet加载 子容器
- 父容器不能注入子容器的bean,也不能使用子容器的properties
- 子容器可以注入父容器的bean ,也不能使用父容器的properties
- 两个容器的properties加载的内容是相互隔离的!!!
- 也就是子容器 可以查找到父容器的bean,父容器不能找到子容器的bean
springmvc中常见注解
@RequestMapping、@RequestBody 、@PathVariable 、@RequestParam 、@ResponseBody、@RestController、@RequestHeader
springmvc源码
- 数据收集工作 实现了InitializingBean 在afterPropertiesSet中完成的收集工作
- 九大组件的初始化工作 HttpServlet | HttpServletBean |__initServletBean(空方法) FrameworkServlet |initServletBean(实现) |initWebApplicationContext |__onRefresh(空方法) DispatcherServlet |onRefresh(实现) |initStrategies 完成九大组件的初始化工作 说明:除了文件上传的解析,其他的组件都是先到ioc容器中查找,没找到就用默 认,找到就用找到的
- 处理请求 HttpServlet | HttpServletBean | FrameworkServlet |servicve |doService(抽象的) DispatcherServlet |doService(实现) |doDispatch
jvm
栈的常见问题
-
栈溢出的情况
栈溢出:StackOverflowError
举个简单的例子:在main方法中调用main方法,就会不断压栈执行,直到栈溢出;栈的大小可以固定,也可以动态变化。如果固定可以用-Xss设置栈的大小;如果动态变化,当栈大小,到达,内存空间不足,就会抛出OutOfMemory异常。
-
调整栈大小,就能保证不出现溢出吗?
不能,因为调整栈大小,只会减少出现溢出的可能,栈大小不是无限扩大的,所以不能保证不出现溢出。
-
分配的栈内存越大越好吗?
不是,因为增加栈大小,会造成每个线程的栈都变得很大,使得一定的栈空间下,能创建的线程数量会变小。
-
垃圾回收是否会涉及虚拟机栈?
不会,垃圾回收只会涉及到方法区和堆中,方法区和堆也会存在溢出的可能;程序计数器只记录运行下一行的地址,不存在溢出和垃圾回收;虚拟机栈和本地方法栈,都是只涉及压栈和出栈,可能存在栈溢出,不存在垃圾回收。
程序计数器
- 为了保证程序连续执行下去,cpu必须采取某些手段来确定下一条指令的地址。程序计数器正是起到了这种作用,通常又称为指令计数器
- 在程序开始执行前,必须将它的起始地址,即程序的指令的内存单元地址送入PC,因此程序计数器内容是从内存提取的第一条指令的地址。当执行指令时,cpu将自动修改程序计数器的内容,即每执行一条指令程序计数器增加一个量,这个量等于指令所含的字节数以便保持将要执行的下一条指令的地址。
- 由于大多数指令都是按照顺序来执行的,所有修改的过程通常只是简单的对pc加1
- 当程序转移时,转移指令执行的最终结果就是要改变pc值,来实现转移。
本地方法栈
简单来说,一个本地方法就是一个java调用非java代码接口,一个本地方法是一个java方法:该方法的实现由非java语言实现,比如c.
- java虚拟机用于管理java方法的调用,而本地方法栈用于管理本地方法的调用。
- 本地方法栈,也是线程私有的
- 允许被实现固定或者是可动态扩展的内存大小
- 本地方法是使用c语言实现的
方法区
jdk7方法区称为永久代,jdk8,使用元空间取代永久代
元空间与永久代最大的区别在于:元空间不在虚拟机设置内存中,而是使用本地内存
方法区中放什么?
类型信息,字段信息、方法信息、常量池、静态常量等。
堆
分代?
java堆区分为年轻代和老年代
年轻代又可以分为eden空间、survivor0空间和survivor1空间
- 几乎所有的java对象都是在eden区被new出来的
- 绝大部分的java对象的销毁都在新生代进行。
如何设置新生代和老年代比例?
默认:新生代占1,老年代占2
可以使用选项-Xmn设置新生代最大内存大小
如何设置Eden、幸存者区比例?
eden空间和另外两个survivor空间缺省所占比例是8:1:1
可以用-XX:SurvivorRatio调整这个空间比例。
对象内存分配
对象栈上分配
我们通过jvm内存分配可以知道java中的对象都是在堆上进行分配,当对象没有被引用的时候,需要依靠GC进行回收内存,如果对象数量较多的时候,会给GC带来较大压力,也间接影响了应用的性能。为了减少临时对象在堆内分配的数量,jvm通过逃逸分析确定该对象不会被外部访问。如果不会被外部访问,在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧而销毁,就减轻了垃圾回收的压力。
对象逃逸分析:就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,jvm对于这种情况可以通过开启逃逸分析参数来优化对象内存分配位置,使其通过标量替换优先分配在栈上,jdk7之后默认开启逃逸分析,
结论:栈上分配依赖于逃逸分析和标量替换
对象在eden区分配
大多数情况下,对象在新生代中eden区分配。
大对象直接进入老年代
为什么呢?为了避免为大对象分配内存时的复制操作而降低效率
长期存活的对象将进入老年代
当它的年龄增加到一定程度(默认15岁,cms收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。
老年代空间分配担保机制
年轻代每次minor gc 之前jvm都会计算下老年代剩余可用空间,
如果这个可用空间小于年轻代里面所有对象大小之和
就会看一个-XX:-HandlePromotionFailure的参数是否设置了
如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一层minor gc后进入老年代的对象平均大小。
如果小于或者没有参数设置,那么就会触发一次Full gc ,老年代和年代代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生OOM
当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可以空间,那么也会触发full gc,如果还是没有空间,则会发生oom
对象内存回收
可达性分析算法
将GC Root对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象。
常用引用类型
java的引用类型一般分为四种:强引用、软引用、弱引用、虚引用
强引用:内存空间不够的时候,不会回收
软引用:正确情况不会被回收,内存空间不够的时候,会回收
弱引用:遇见gc就会回收
虚引用:回收机制和弱引用差不多,但是它被回收之前,会被放入ReferenceQueue中。
finalize()方法最终判定对象是否存活
即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候他们暂时处于缓刑阶段,要真正宣布一个对象死亡,至少要经历再次标记过程。
-
第一次标记并进行一次筛选
筛选条件此对象是否有必要执行finalize()方法,没有覆盖finalize()方法,将直接被回收。
-
第二次标记
如果覆盖了finalize()方法,对象还有一次存活的机会,如果对象要在finalize中拯救自己,只要重新与引用链上的对象关联。否则被回收
如何判断一个类是无用的类?
无用类需要满足3个条件:
- 该类所有的对象实例都已经被回收,也就是java堆中不存在该类的任何实例
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾收集算法及垃圾收集器
垃圾收集算法
分代收集理论
根据对象存活周期的不同将内存分为几块,一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
标记-复制算法
缺点:在对象存活率较高时就要进行较多的复制工作,效率变低
对空间浪费,因为复制至少需要2块相同内存,但是每次只用一块
标记-清除算法
缺点:效率问题(如果需要标记的对象太多,效率不高)
空间问题(标记清除后会产生大量不连续的碎片)
标记-整理算法
标记过程,然后让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现
serial收集器
最基本,历史最悠久的垃圾收集器。是一个单线程,只会使用一条垃圾收集线程去完成垃圾收集工作,重要但是他在垃圾收集工作的时候必须暂停其他所有的工作线程,直到它收集结束。
Parallel Scavenge 收集器
Parallel收集器其实就是Serial收集器的多线程版本。除了使用多线程进行垃圾收集外,其余行为和Serial收集器类似。
ParNew收集器
ParNew收集器其实跟Parallel收集器类似,区别主要在于它可以和CMS收集器配合使用。
CMS收集器
是一种以获取最短回收停顿时间为目标的收集器。它非常符合用户体验在应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
cms收集器是一种标记-清除算法实现的,整个过程分为四个步骤:
初始标记->并发标记->重新标记->并发清理->并发重置
并发
创建多线程的方式有哪些?
-
继承thread
编写一个类继承thread,重写run方法,创建线程对象,调用start方法
-
实现Runnable接口
编写一个类实现Runnable接口,实现run方法,创建Runnable对象,创建Thread对象,把Runnable对象作为构造函数的参数传递给了Thread对象,调用Thread对象的start方法开启线程
为什么有了Thread方法创建线程还会有第二种方法呢?
可以避免java单继承带来的局限性
适合多个相同类型的线程处理同一资源的情况,把线程运行的代码和线程本身进行了分离
开启一个线程是调用run方法还是start方法?(为什么不是直接调用run,而是调用start,最后采用调用run?)
开启一个线程调用的是start方法,在调用start方法的时候,会从用户态转变为核心态,通知jvm开启一个新的线程来执行run方法。如果调用run,不会转变为核心态,相对于调用的就是普通的方法。
线程生命周期
New
Runnable
Blocked
Waiting
Time Waiting
Terminated
Synchronized和Lock区别
-
synchronized是java内置关键字,是jvm层面实现的;
lock是java的一个接口,是通过代码实现的,为具体的java类,属于API层面的锁
-
synchronized jvm自动上锁和解锁,lock锁需要手动的加锁和释放锁
-
synchronized无法判读是否获取锁的状态,lock可以判断是否获取到锁
-
synchronized获取锁的时候没有超时,不能中断一个正在试图获取锁的线程
synchronized和lock都是可重入锁,同一线程可以多次获得同一个锁
ThreadLocal
ThreadLocal 是 Java 中的一个类,用于在多线程环境下存储线程局部变量。简单来说,它提供了一种将数据与当前线程关联起来的机制,使得每个线程都可以独立地访问自己的局部变量,而不会受到其他线程的影响。
ThreadLocal 主要有以下特点和用途:
- 线程隔离性: 每个线程都拥有自己独立的变量副本,彼此之间互不干扰。
- 数据共享: 允许在同一个线程的多个方法之间共享数据,而无需通过参数传递。
- 线程安全性: 每个线程都操作自己的数据,不需要考虑线程安全问题。
- 避免资源竞争: 在多线程环境下,避免使用共享变量而导致的竞态条件。
ThreadLocal 主要通过三个方法来实现:
set(T value)
: 将值设置到当前线程的 ThreadLocal 变量中。get()
: 获取当前线程的 ThreadLocal 变量的值。remove()
: 将当前线程的 ThreadLocal 变量移除,防止内存泄漏。
使用 ThreadLocal 时需要注意以下几点:
- 谨慎使用静态变量。因为静态变量会被所有线程共享,可能会导致内存泄漏或数据错乱。
- 及时清理。使用完 ThreadLocal 后,要及时调用
remove()
方法清理,避免内存泄漏问题。 - 注意线程池中的使用。在使用线程池时,ThreadLocal 变量的值可能会被多个线程共享,需要谨慎处理。
总的来说,ThreadLocal 提供了一种在多线程环境下管理线程局部变量的便捷机制,能够简化线程间数据传递和共享的操作。
工作原理:
1、提交任务的时候,先判断,如果线程池中的线程数小于corePoolSize,即使其他线程处于空闲,也会创建一个新的线程来运行新任务
2、如果线程数>=corePoolSize但是小于maxPoolSize,则将任务放在队列中
3、如果队列已满,并且线程数小于maxPoolSize,则创建新的线程来运行任务
4、如果队列也满了,并且线程数大于或者等于maxPoolSize,则拒绝该任务
如果线程池内存队列满了会怎么样?
如果线程池的内存队列满了,其行为取决于所使用的队列类型,是有界的阻塞队列还是无界的阻塞队列。我可以详细解释一下:
- 有界队列:
- 当线程池的有界队列满了之后,如果还有空闲线程,新的任务将会被阻塞直到有空闲线程可用,或者直到队列中有空间来接受任务。
- 如果有界队列已满且线程池中的线程数已达到最大线程数,这时候线程池会触发拒绝策略来处理新提交的任务。
- 无界队列:
- 对于无界队列,如果线程池的任务队列是无界的,那么新的任务会一直被添加到队列中,直到内存耗尽。
- 当无界队列满了时,如果线程池中的线程数没有达到最大线程数,新的任务会触发线程池动态创建额外线程来处理。
- 如果线程池中的线程数已达到最大线程数,那么线程池会触发拒绝策略来处理新提交的任务。
自定义拒绝策略是一个很好的选择,可以根据具体需求来处理无法提交的任务。例如,将任务持久化到硬盘或数据库中,然后在有空闲线程时重新提交这些任务。这种方法可以保证任务不会丢失,并且可以在系统重启后继续执行未完成的任务。通过在数据库中记录任务的状态,可以确保任务的一致性和可靠性。
总之,对于线程池中的任务管理,合适的队列类型和拒绝策略是非常重要的,需要根据具体的业务场景和性能需求来进行选择和调整。
中断
- 中断标识:
- 通过调用
interrupt()
方法来设置线程的中断状态为true
,通知线程应该中断。 - 中断标识是线程的一个内部标记,线程可以通过检查这个标识来判断是否被中断,然后采取相应的措施。
- 通过调用
- 常见的中断相关方法:
interrupted()
: 这是一个静态方法,用于返回当前线程的中断状态,并在调用时清除中断状态,将其重新设置为false
。通常用于检查中断状态并清除状态。isInterrupted()
: 是实例方法,用于检查线程的中断状态,但不清除中断状态。
- 异常处理:
- 当线程在执行
wait()
,sleep()
,join()
等阻塞方法时,如果收到中断信号,会抛出InterruptedException
异常,并清除中断信号。这表示线程收到了中断请求,但仍然可以选择如何处理它。 - 在捕获
InterruptedException
异常后,通常的做法是重新中断线程,以便在异常处理后恢复中断状态,让上层代码知道当前线程已经收到了中断请求。
- 当线程在执行
- 合理使用:
- 中断应该被视为一种协作机制,而不是强制终止线程的手段。线程应该根据自身逻辑来决定如何响应中断请求,可能是优雅地结束线程的执行,清理资源,或者继续执行直到任务完成。
总的来说,中断是多线程编程中一个重要的机制,能够有效地进行线程间的通信和协作,但需要谨慎使用,以避免引入不必要的复杂性和错误。
停止线程
方式一、volatile的boolean变量
方式二、AtomicBoolean
方式三、中断(这种)
在多线程编程中,停止线程是一个重要的问题。你提到了三种常见的停止线程的方式,让我进一步说明它们:
- 使用 volatile 的 boolean 变量:
- 这种方式通常涉及一个控制标志,例如
volatile boolean isRunning
,线程在执行任务时会周期性地检查这个标志,并在标志变为false
时自行退出执行。 - 这种方式的优点是简单易懂,但需要线程自行检查标志位,不够灵活。
- 这种方式通常涉及一个控制标志,例如
- 使用 AtomicBoolean:
- 与第一种方式类似,使用
AtomicBoolean
来控制线程的执行状态。AtomicBoolean
提供了原子操作,保证了线程安全性。 - 这种方式相对于简单的 volatile 变量更加安全,因为它提供了更强的原子性和线程安全性。
- 与第一种方式类似,使用
- 中断:
- 使用中断是 Java 多线程编程中一种常见的线程停止机制。通过调用
interrupt()
方法来通知线程应该中断,线程可以通过检查中断状态来响应中断请求并安全地停止执行。 - 这种方式可以与阻塞方法(如
sleep()
、wait()
、join()
等)配合使用,当线程被阻塞时,调用interrupt()
方法会立即唤醒线程并抛出InterruptedException
异常,线程可以在异常处理中安全地退出执行。
- 使用中断是 Java 多线程编程中一种常见的线程停止机制。通过调用
这三种方式各有优缺点,具体选择取决于应用场景和需求。通常来说,使用中断机制是比较灵活和安全的选择,尤其适用于需要与阻塞方法配合使用的场景。
synchronized
使用层面:理解 对象锁 和类锁
同一个对象上面 有一把锁
同一个类上有一把锁
synchronized
1、同步代码块 使用的是monitorenter 和moniterexit指令,
一般情况都是一个monitorentor和2个monitorexit(保证异常的时候也释放monitor),但是在同步
方法中加入抛出异常,就只有一个enter和一个exit
2、同步方法是在方法上设置ACC_SYNCHRONIZED的标识,如果方法设置了,
执行线程会将先持有monitor然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放 monitor
对象的结构
对象在内存中分为三个部分:
对象头
mark word(对象标记): 存储hashcode,分代年龄,锁标志位等信息 ,占8个字节大小
类型指针(类元信息):说明是哪个类型,对象都要指到该类型,来说明对象是哪种类型,通常占8
个字节,但是开启压缩,只占4个字节
实例数据
存放类的属性(Field)数据信息,包括父类的属性信息,
(如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。)
对齐填充
按8的整数倍补齐
new Object() 占多少字节?16字节
synchronized 锁升级过程
1、无锁 偏向锁 轻量级锁 重量级锁 都是改变对象的对象头中的mark word的锁标志位和偏向锁位来完成的
2、偏向锁
为了解决只有在一个线程执行同步时提高性能
3、轻量级锁
轻量级锁本质就是自旋锁(使用CAS自旋)。
假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。
而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。
此时线程B操作中有两种情况:
如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A → B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位;
4、重量级锁
是依靠操作系统的mutex lock(互斥锁)来实现的,需要切换到内核态
JMM
jmm:Java内存模型(java memory model) 不是Jvm内存结构【堆、栈、方法区、程序计数器、本地方法栈】
Java内存模型实际上是围绕着三个特征建立起来的。分别是:原子性,可见性,有序性。Java 内存模型(Java Memory Model,简称 JMM)定义了 Java 程序中多线程并发访问共享变量时的行为规范。它规定了线程如何与主内存和工作内存交互,以及在多线程环境下保证可见性、原子性和有序性的机制。Java 内存模型的设计就是为了解决并发编程中的原子性、可见性和有序性这些关键问题,从而确保多线程环境下的程序能够正确、高效地运行。
CAS
CAS(比较并交换,Compare and Swap) 是一种用于实现多线程同步的机制,常用于并发编程中,特别是在无锁算法中。
CAS 操作涉及三个值:
- 内存地址:指向要操作的共享变量在内存中的地址。
- 旧的预期值:线程在进行 CAS 操作时,期望共享变量的值为多少。
- 要修改的新值:线程希望将共享变量修改为的值。
CAS 的基本原理是,它先比较共享变量的当前值是否等于旧的预期值,如果相等,则更新为新值;如果不相等,则不做任何操作。这个比较和更新操作是原子性的,因此可以保证在多线程环境下对共享变量的正确操作。
如果 CAS 操作失败,即共享变量的当前值与旧的预期值不相等,通常情况下线程会重试 CAS 操作,这种重试的方式称为自旋。自旋的次数可以由程序员指定,或者根据具体情况动态调整。
CAS 的优点是可以避免使用锁所带来的性能开销,因为它不会造成线程的阻塞。然而,它也存在一些限制,比如 ABA 问题和循环时间长的问题。ABA 问题指的是共享变量的值从 A 变成了 B,然后又变回了 A,这种情况下 CAS 操作可能会误以为没有其他线程修改过共享变量。为了解决这个问题,通常需要使用版本号或者标记来确保 CAS 操作的正确性。
cas怎么保证原子性?
依靠硬件保证,生成CPU的原子指令(cmpxchg指令),执行的时候先判断是否为多核,是多核就对总线加锁,加锁成功执行cas操作。
unsafe是什么?
jdk提供的操作内存的后门,但是操作起来比较麻烦,故在java层面提供了atomic相关的原子操作工具类。
unsafe为什么叫不安全?
1、因为太强大,不好控制
2、直接写操作内存的代码,可能会出现一些问题。
CAS问题?
1、CAS长时间不成功,造成开销大
2、ABA问题
解决:AtomicStampedReference
常见并发容器或工具类
- ConcurrentHashMap:它是线程安全的哈希表实现,用于替代同步的 HashMap。内部采用分段锁(Segment)来实现,并发访问时只锁定特定的段,可以提高并发性能。在节点的操作上,通常使用 CAS 操作来保证线程安全。
- CopyOnWriteArrayList 和 CopyOnWriteArraySet:这两个容器都是写时复制的集合类,适用于读多写少的场景。在写操作时,会复制一份新的数组,并在新数组上进行操作,这样可以避免写操作对读操作的影响。常用于读多写少的情况下。
- 并发队列和阻塞队列:这些队列用于在多线程环境中安全地传递数据。其中包括有界队列和无界队列,以及特殊类型的队列如优先级队列和同步队列。
- ArrayBlockingQueue:有界队列,基于数组实现。
- LinkedBlockingQueue:有界或无界队列,基于链表实现。
- PriorityBlockingQueue:优先级排序的无界阻塞队列。
- SynchronousQueue:不存储元素的阻塞队列,用于直接传输元素。
- CountDownLatch、CyclicBarrier 和 Semaphore:这些工具类用于在多线程环境中进行协调和同步。
- CountDownLatch:通过一个计数器来实现线程等待,直到计数器为零时,所有等待的线程才会继续执行。
- CyclicBarrier:用于多个线程互相等待,直到所有线程都达到某个公共的屏障点,然后所有线程继续执行。
- Semaphore:用于控制同时访问特定资源的线程数量,常用于资源池的实现。
这些并发容器和工具类提供了丰富的功能,可以帮助开发者在多线程编程中实现高效的并发控制和数据共享。
常见的并发包
java.util.concurrent
java.util.concurrent.atomic
java.util.concurrent.locks
AQS
AbstractQueuedSynchronizer:
state + 队列
1、AQS使用一个volatile的int类型的成员变量来表示同步状态【0就是没人,自由状态可以办理,大于
等于1,有人占用窗口,等着去,放到队列中】,通过内置的FIFO队列来完成资源获取的排队工作将每
条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。
AQS 使用state控制状态 + 双端队列(尾部入队,头部出队) ,队列中放的就是封装的Node。
ReentrantLock、Semaphore、countdownlatch这些都是使用AQS实现。
没错,AbstractQueuedSynchronizer (AQS) 是 Java 并发包中用于实现同步器的一个重要框架。它提供了一个简单而强大的机制来构建同步器,用于构建锁和其他同步工具。AQS 基于两个重要概念:
- 同步状态(State):AQS 使用一个
volatile
的int
类型成员变量来表示同步状态。通常情况下,0 表示无锁状态,大于 0 表示有线程持有锁,小于 0 通常表示某种特殊的状态。通过对这个状态的修改和判断,来实现对临界资源的控制。 - 等待队列(Wait Queue):AQS 使用一个内置的 FIFO 队列来管理等待获取同步器的线程。每个等待线程都会被封装成一个
Node
节点,这个节点包含了线程的引用以及一些其他的状态信息。通过双端队列的结构,新的等待线程总是加入到队列的尾部,而等待线程被唤醒时总是从队列的头部开始执行。
使用 AQS 可以方便地实现各种同步器,例如:
- ReentrantLock:可重入锁就是通过 AQS 实现的,它使用
int
类型的 state 来表示持有锁的次数。 - Semaphore:信号量也是基于 AQS 实现的,它的 state 表示可用的许可证数量。
- CountDownLatch:倒计时门栓同样是通过 AQS 实现的,它的 state 表示需要等待的倒计时数量。
AQS 的设计使得 Java 并发包中的各种同步工具能够高效、灵活地实现各种同步策略,同时也提供了一种标准的、可扩展的框架,使得用户可以自定义自己的同步器。
volatile
volatile****可以保证可见性和有序性,是依靠内存屏障 指令来实现
volatile在jvm层面是通过内存屏障来实现的,在硬件层面是通过lock前缀指令来实现的。
ConcurrentHashMap
在JDK 7 中,ConcurrentHashMap 的实现确实是采用了分段锁的方式来保证线程安全。这个分段锁的结构就是 Segment,每个 Segment 都拥有一个 ReentrantLock,并且 ConcurrentHashMap 的数据被分成了一段一段存储。这样的设计可以让多个线程同时访问这个 ConcurrentHashMap 的不同段,从而提高了并发性能。
内部结构大致可以描述为:
- ConcurrentHashMap 内部维护一个 Segment 数组,每个 Segment 是一个独立的散列表,拥有自己的锁。
- 每个 Segment 中存储键值对的数组,这些数组加起来组成了整个 ConcurrentHashMap 的存储结构。
- 在插入或者获取元素时,会先根据 Key 的 Hash 值定位到对应的 Segment,然后再在该 Segment 中进行操作。
在 JDK 8 中,ConcurrentHashMap 进行了改进,采用了 CAS(比较并交换)操作来保证线程安全,而不再使用分段锁。这种改进使得 ConcurrentHashMap 的性能得到了提升,特别是在高并发情况下,CAS 操作比传统的锁机制更高效。
wait与sleep
1、wait必须要在synchronized的前提下才能使用wait,sleep不需要
2、wait在wait的过程中会释放锁,但是sleep在睡觉的过程不会释放锁
3、wait是Object上的方法,sleep是Thread上方法。
Hashmap与Hashtable
1、hashmap是线程不安全的,性能高,hashtable线程安全的,性能低
2、hashmap的key和value可以为null,hashtable的key和value都不能为null
3、hashmap的父亲是AbstractMap,hashtable的父亲是Ditionary
mysql
mysql中有哪些函数?哪些聚合函数?
1、类型转换函数 cast
2、字符串处理函数(自己去查,随便记一些比如LOWER、UPPER、concat、substr等)
3、日期函数:str_to_date(字符串转日期)、DATE_FORMAT(日期格式化)
4、聚合函数:COUNT、avg、max、min、sum
MySQL 中有许多函数可用于执行各种操作,包括类型转换、字符串处理、日期处理和聚合计算等。下面列举了一些常见的函数类型及其示例:
-
类型转换函数:
-
CAST(value AS type)
: 将值转换为指定的数据类型。
sqlCopy CodeSELECT CAST('123' AS UNSIGNED);
-
-
字符串处理函数:
-
LOWER(str)
: 将字符串转换为小写。 -
UPPER(str)
: 将字符串转换为大写。 -
CONCAT(str1, str2, ...)
: 将多个字符串连接起来。 -
SUBSTR(str, start, length)
: 返回字符串的子串。
sqlCopy CodeSELECT LOWER('Hello'), UPPER('world'), CONCAT('Hello', ' ', 'world'), SUBSTR('Hello world', 7, 5);
-
-
日期函数:
-
STR_TO_DATE(str, format)
: 将字符串转换为日期。 -
DATE_FORMAT(date, format)
: 格式化日期。
sqlCopy CodeSELECT STR_TO_DATE('2022-01-01', '%Y-%m-%d'), DATE_FORMAT(NOW(), '%Y-%m-%d');
-
-
聚合函数:
-
COUNT(expr)
: 返回表中符合条件的行数。 -
AVG(expr)
: 返回表中符合条件的列的平均值。 -
MAX(expr)
: 返回表中符合条件的列的最大值。 -
MIN(expr)
: 返回表中符合条件的列的最小值。 -
SUM(expr)
: 返回表中符合条件的列的总和。
sqlCopy CodeSELECT COUNT(*), AVG(salary), MAX(salary), MIN(salary), SUM(salary) FROM employees;
-
这些函数只是 MySQL 中可用函数的一小部分,您可以根据具体的需求去查找和使用其他函数。
char与varchar区别?
CHAR
和 VARCHAR
是用于存储字符数据的两种数据类型,它们之间的主要区别在于存储方式和使用场景:
- CHAR:
CHAR
类型用于存储固定长度的字符数据。- 每个
CHAR
列都需要占用固定的存储空间,无论实际存储的字符数量是多少。 - 如果存储的字符串长度小于定义的长度,则会在末尾用空格进行填充。
- 适合存储长度固定的数据,比如存储固定长度的代码、状态等。
- VARCHAR:
VARCHAR
类型用于存储可变长度的字符数据。- 每个
VARCHAR
列只会占用实际存储数据所需的空间,不会额外填充空格。 - 适合存储长度可变的数据,比如姓名、地址、描述等。
总的来说,CHAR
适合存储固定长度的数据,而 VARCHAR
则适合存储长度可变的数据。使用 VARCHAR
可以节省存储空间,因为它只占用实际数据所需的空间,而不会浪费额外的空间用于填充。
limit 的优化
在 MySQL 中,LIMIT
用于限制 SELECT
查询返回的行数。对于大型数据集,使用 LIMIT
可以提高查询性能和减少资源消耗。以下是一些 LIMIT
的优化技巧:
- 使用合适的索引:
- 确保查询中涉及的列都有合适的索引,特别是在用于
ORDER BY
或WHERE
子句中的列上建立索引。这有助于数据库引擎快速定位并返回需要的行。
- 确保查询中涉及的列都有合适的索引,特别是在用于
- 配合索引使用
ORDER BY
:- 如果查询包含
ORDER BY
子句,尽可能使用索引覆盖排序的列,这样数据库引擎可以直接使用索引返回有序的结果,而不需要额外的排序操作。
- 如果查询包含
- 使用
EXPLAIN
分析查询计划:- 使用
EXPLAIN
命令来查看查询的执行计划,确保 MySQL 正确地使用了索引,并且没有不必要的排序和扫描操作。通过分析执行计划,可以发现优化的潜在问题并进行改进。
- 使用
- 避免不必要的列:
- 在
SELECT
查询中,只选择需要的列,避免选择不必要的列。这可以减少数据库引擎在查询执行过程中传输的数据量,从而提高性能。
- 在
- 合理设置缓冲区大小:
- 对于频繁使用
LIMIT
的查询,可以考虑适当增加sort_buffer_size
和read_rnd_buffer_size
参数的大小,以提高排序和随机读取的效率。
- 对于频繁使用
- 分页优化:
- 对于分页查询,尽可能使用基于游标的分页技术,而不是简单的
OFFSET
。基于游标的分页技术可以避免在大型数据集上执行 OFFSET 操作时的性能问题。
- 对于分页查询,尽可能使用基于游标的分页技术,而不是简单的
- 定期优化表:
- 对于频繁进行
LIMIT
查询的表,定期进行表的优化操作,例如重新组织表以减少碎片,可以提高查询性能。
- 对于频繁进行
通过综合使用以上技巧,可以有效地优化使用 LIMIT
的查询,提高查询性能并减少资源消耗。
行列转换
在数据库中,有时候需要进行行列转换,即将原始数据中的行转换为列,或者将列转换为行。这在数据分析和报表生成等场景中经常会遇到。以下是一些常见的行列转换方法:
- 使用聚合函数和条件语句:
- 使用聚合函数如
SUM
、MAX
、MIN
、COUNT
等以及条件语句(如CASE WHEN
)来将多行数据合并为单行,实现行列转换。
- 使用聚合函数如
- 使用透视表:
- 透视表是一种通过将行数据转换为列数据来重塑数据的方法。在 SQL 中,可以使用
PIVOT
或CROSSTAB
等功能实现透视表操作。
- 透视表是一种通过将行数据转换为列数据来重塑数据的方法。在 SQL 中,可以使用
- 使用自连接:
- 可以通过自连接同一张表多次,每次查询不同的列,然后将结果组合成单行,实现将多行数据转换为单行的效果。
- 使用动态 SQL:
- 对于不确定列数的情况,可以使用动态 SQL 来动态构建查询语句,将查询结果动态地转换为所需的格式。
- 使用编程语言进行转换:
- 在应用程序中,可以通过编程语言(如 Python、Java 等)读取原始数据,进行适当的处理和转换,最后将数据保存为所需的行列格式。
- 使用数据库特定的功能:
- 不同的数据库系统可能提供了特定的功能来实现行列转换,如 Oracle 中的
PIVOT
和UNPIVOT
、PostgreSQL 中的crosstab
函数等。
- 不同的数据库系统可能提供了特定的功能来实现行列转换,如 Oracle 中的
选择哪种方法取决于数据的特点、所使用的数据库系统以及具体的需求。常见的行列转换场景包括将交叉表转换为规范化的表、将多行数据合并为单行、将单行数据拆分为多行等。
什么是事务?
一系列的操作的要么同时成功,要么同时失败
事务特点?
ACID atomic consistency isolation durable
A :原子性
C:一致性
I:隔离性:如果开启了一个事务,这个事务 和其他的事务是隔离的
D:持久性:操作完成之后,数据会持久化下来
能解释什么是脏读,什么是不可重复读,什么是幻读?
1、脏读:一个事务 读到 另外一个事务未提交数据(脏数据)
2、不可重复读: 一个事务 读到了别人提交的数据
3、幻读:强调是数据的插入操作。
- 脏读(Dirty Read):脏读指的是一个事务读取了另一个事务未提交的数据,也就是读取到了未经提交的“脏数据”。如果另一个事务在稍后回滚,则读取的数据就会变得无效或不一致。脏读通常发生在一个事务修改数据但尚未提交时,另一个事务读取了这些未提交的数据。
- 不可重复读(Non-Repeatable Read):不可重复读指的是在一个事务中,多次读取同一行数据,但得到的结果不一致。这可能是因为另一个事务在两次读取之间修改了该行数据,导致了读取的结果不一致。不可重复读强调的是数据的修改操作,而不仅仅是提交。
- 幻读(Phantom Read):幻读与不可重复读类似,但强调的是数据的插入或删除操作。幻读指的是在一个事务中,多次查询时发现不同数量的记录。例如,一个事务在查询某个范围内的数据时,发现了在两次查询之间被插入的新记录,就称为幻读。幻读通常发生在并发事务中,其中一个事务在另一个事务正在进行插入或删除操作时查询了相同的范围。
这些问题都是由并发事务引起的,为了避免这些问题,数据库系统通常采用不同的隔离级别来控制事务之间的相互影响。常见的隔离级别包括读未提交(Read Uncommitted)、读提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。不同的隔离级别对于解决脏读、不可重复读和幻读问题有不同的影响和限制。
事务的隔离级别
1、未提交读(read-uncommitted):解决了丢失更新 还会存在脏读 和不可重复读 和幻读
2、提交读(read-committed):解决了丢失更新、脏读 ,还会存在不可重复读 和 幻读
3、可重复读(repeatable-read):解决了丢失更新、脏读、不可重复读 存在 幻读
4、串读(serializable):解决了所有问题(丢失更新、脏读、不可重复读、幻读)
mysql默认的隔离级别是repeatable-read
说一下内连接和外连接的区别
内连接是指两张表的交集,用inner join来实现。
左外连接是指左边这部分和中间的交集,left join,
右外连接是指右边这部分和交集,用 right join
Statement和PreparedStatement的区别
Statement:
1.Statement有sql注入问题
2.Statement需要拼接sql
PreparedStatement:
1.PreparedStatement提前编译sql 提高性能
2.避免sql注入
3.设置值非常方便
数据库设计的三范式
第一范式:属性不可再分
第二范式:在满足第一范式前提下,不存在部分依赖,非主键列完全依赖于主键
第三范式:在满足第二范式的前提下,不存在依赖范式
B树和B+树的区别?
B树的节点上都会存储数据,B+树只在叶子节点存储数据
B树的叶子节点不构成链表,B+树叶子节点构成链表
B树:对于范围查询,需要在树的内部进行多次跳转,B+树:由于叶子节点形成了有序链表,范围查询在B+树上更为高效,只需遍历叶子节点即可。
B树:用于实现数据库索引,如MySQL中的InnoDB引擎,B+树:在大多数关系型数据库中广泛应用,因为其有序叶子节点链表结构适合范围查询以及顺序遍历。
- 节点结构:
- B树:节点不仅包含关键字,还包含对应的数据(或者指向数据的指针)。
- B+树:非叶子节点仅包含关键字(或者说键值),而所有数据都仅存储在叶子节点中。
- 叶子节点:
- B树:叶子节点存储数据,并且在一定程度上形成了链表,但并不严格有序。
- B+树:所有的数据都仅存储在叶子节点中,叶子节点之间形成了有序链表,方便范围查询。
- 范围查询:
- B树:对于范围查询,需要在树的内部进行多次跳转。
- B+树:由于叶子节点形成了有序链表,范围查询在B+树上更为高效,只需遍历叶子节点即可。
- 节点指针:
- B树:非叶子节点中的指针直接指向子节点。
- B+树:非叶子节点中的指针也仅指向下一层节点,而不包含数据。
- 常用于数据库索引:
- B树:用于实现数据库索引,如MySQL中的InnoDB引擎。
- B+树:在大多数关系型数据库中广泛应用,因为其有序叶子节点链表结构适合范围查询以及顺序遍历。
总的来说,B+树相较于B树在范围查询和顺序遍历等方面具有更好的性能,并且更适合作为数据库索引的实现方式。
为什么B+树比B树更适合做索引?
- 范围查询效率高: B+树的叶子节点构成了一个有序链表,这使得范围查询更为高效。因为在B+树上进行范围查询时,只需遍历叶子节点链表即可,而B树可能需要在树的内部进行多次跳转,效率较低。
- 顺序遍历性能优越: 由于B+树的叶子节点形成了有序链表,因此对整个树进行顺序遍历时,只需遍历叶子节点的链表即可,而不需要额外的跳转操作,这在某些场景下(如数据库表的全表扫描)可以带来较好的性能表现。
- 非叶子节点只存储索引: 在B+树中,非叶子节点只存储索引,不存储实际数据,这样可以使得树的高度更低,减少了磁盘I/O操作的次数,提高了检索效率。
- 更适合磁盘存储: B+树的特性使得它更适合于磁盘存储,因为在磁盘上连续存储的数据更容易进行顺序读取,而B+树的叶子节点形成的有序链表更符合这种连续存储的要求。
综上所述,B+树相较于B树在数据库索引等场景下具有更优越的性能和更好的存储特性,因此更常用于实际应用中。
Mysql存储引擎
InnoDB、MyISAM、Memory是MySQL中常见的存储引擎,它们在支持各种索引类型方面有所不同:
MyISAM与InnoDB的区别
-
InnoDB支持事务,MyISAM不支持
-
InnoDB支持行锁和表锁,myisam只支持表锁。
-
InnoDB支持外键,而MyISAM不支持
-
InnoDB是聚集索引,数据和索引存到同一个文件里;MyISAM是非聚集索引,数据和索引不在同一个文件里;都是使用B+Tree作为索引结构
-
InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快(注意不能加有任何WHERE条件)
Mysql优化
MySQL的性能优化是一个复杂而细致的过程,涉及到数据库设计、索引优化、查询调优、硬件配置以及系统参数调整等多个方面。下面是一些常见的MySQL性能优化技巧:
- 合适的数据类型:选择合适的数据类型可以减少存储空间的占用,并提高查询性能。尽量避免使用过大的数据类型,比如使用
INT
代替BIGINT
。 - 优化数据库设计:良好的数据库设计可以减少不必要的数据冗余和关联查询,降低数据库的复杂度,提高查询效率。
- 创建合适的索引:根据查询需求创建合适的索引可以加速查询速度。但过多的索引会增加写操作的成本,需要权衡。
- 避免全表扫描:尽量避免在查询中使用全表扫描,确保查询能够充分利用索引。
- 优化查询语句:编写高效的SQL查询语句,避免不必要的子查询和联合查询,尽量减少查询返回的数据量。
- 使用连接池:使用连接池可以减少连接数据库的开销,提高系统的并发性能。
- 分区表:对于大型数据表,可以考虑使用分区表来减少单个数据表的数据量,提高查询效率。
- 定期优化表:定期对表进行优化操作,如
OPTIMIZE TABLE
、ANALYZE TABLE
等,可以提高表的性能。 - 使用缓存:利用缓存技术,如Redis、Memcached等,可以减轻数据库的压力,提高系统的响应速度。
- 监控和调优:定期监控数据库性能指标,如查询响应时间、数据库连接数、缓冲区命中率等,根据监控结果进行针对性调优。
- 升级和优化硬件:根据业务需求和数据库负载情况,考虑升级数据库服务器的硬件配置,如CPU、内存、磁盘等。
综上所述,MySQL性能优化是一个持续不断的过程,需要结合数据库设计、索引优化、查询调优等多个方面的工作,才能最大程度地提升数据库的性能和稳定性。
索引失效
1、违背最左匹配规则
2、对索引列使用了函数、表达式或运算符:当查询条件中使用了函数、表达式或运算符时,MySQL就无法使用该列的索引,因为它需要对每行数据进行计算,而不是直接查找索引。
3、查询条件中使用了不等于操作符(<>、!=)、NOT NULL, NOT IN 等
4、模糊查询:当查询条件中使用了LIKE、%或_等模糊匹配符号时,MySQL无法使用索引进行快速定位。
5、OR条件:当查询条件中包含多个OR条件时,MySQL无法使用索引进行快速定位。
6、范围查询:当查询条件中使用了BETWEEN、<、>、<=、>=等操作符时,MySQL只能使用索引中的一部分数据,需要读取更多的数据进行过滤,降低了查询效率。
7、数据类型不匹配,确保查询条件的数据类型与索引列的数据类型匹配,避免隐式类型转换,可以提高索引的利用率。
读写分离
要知道mysql的读写分离的目的就是为了提升mysql的并发能力
在我们的项目中其实我们面对的更多的是我们的读多写少这种操作,这个时候为了增加我们的并发我们就可以去采用这种读写分离这种策略,所谓的读写分离的话实际上就是我们这种读的请求就去访问我们的读库,写的话进去访问我们的写库,这个时候既然要做我们的读写分离我们的读库和写库数据要同步,在同步数据的时候会去用到MySQL主从复制,我们主库这边会去开启一个二进制的日志,我们从库这边会去读取我们这个二进制的日志,来达到我们同步数据的作用,然后我们这个读写分离的原理我们去用到了我们的spring的动态数据源,在这个数据源里面首先在我们的项目里面我们会 配置两个数据源,一个是我们主库的数据源一个是我们从库读库的数据源,紧接着我们会去,编写动态数据源的这个类这个类就会去当前线程里面会获取标记看是读库的标记还是写库的标记,现在有个问题就是我们怎么去设置这个标记并放到我们的当前这个线程里面,我们可以去用一个前置通知,在执行这个目标方法之前,我们就去获取他的这个前缀,如果说他是查询的方法我们就去标记他为读库的标记走我们的读库,如果是写库的的话就去标记走写库,标记了之后在我们的动态数据源里面,就可以在当前线程里面获取到这个标记了,然后就可以按照这个标记去执行了,那如果说我们现在是一主多从,多个我们的读库的话,我们判断要走读库的时候,因为现在有很多读库可以选择,我们就可以去用我们的负载均衡策略(轮询,随机)去选择对应的读库就可以了。
主从复制
主从复制(Master-Slave Replication)是数据库中常用的一种数据复制方式,用于将一个数据库服务器(主服务器)上的数据同步到其他一个或多个数据库服务器(从服务器)上。主从复制通常用于提高数据的可用性、可扩展性和备份。
基本的主从复制过程如下:
- 主服务器记录更新操作: 当主服务器接收到来自客户端的更新请求(如插入、更新、删除操作)时,它会在自己的数据库上执行该操作,并将操作记录到其二进制日志(binary log)中。
- 从服务器请求更新: 从服务器定期向主服务器发送请求,询问是否有新的更新操作。主服务器会将从上次同步以来的所有更新操作发送给从服务器。
- 从服务器应用更新: 从服务器接收到更新操作后,会将这些更新操作应用到自己的数据库中,以使其与主服务器的数据库保持一致。
主从复制的优点包括:
- 提高数据可用性: 当主服务器出现故障时,可以快速切换到一个从服务器,从而保证系统的持续运行。
- 分担读负载: 读操作可以分发到多个从服务器上,从而减轻主服务器的负载,提高系统的性能。
- 备份和灾难恢复: 可以使用从服务器进行实时备份,并且在主服务器出现灾难性故障时,可以快速将从服务器提升为主服务器,以实现业务的连续性。
但是,主从复制也存在一些缺点和注意事项,例如:
- 数据一致性: 主从复制是异步的,从服务器的数据可能会滞后于主服务器。在特定情况下(如网络延迟、主从服务器负载过重等),可能会出现数据不一致的情况。
- 单点故障: 主服务器是系统的单点故障,如果主服务器宕机,整个系统可能会受到影响。
- 复制延迟: 由于网络延迟、从服务器负载等原因,从服务器上的数据可能会滞后于主服务器,需要合理设置复制延迟的阈值以避免数据不一致。
因此,在使用主从复制时,需要根据业务需求和系统特点来合理设计和部署主从复制架构,并采取相应的措施来保证数据的一致性和系统的可用性。
分库分表
分库分表是一种常见的数据库水平扩展方案,用于解决单一数据库存储容量和性能瓶颈的问题。在应对大规模数据和高并发访问的场景下,分库分表可以有效地提高系统的扩展性和性能。
分库分表的基本概念:
- 分库: 将原本存储在单一数据库中的数据按照某种规则划分到多个数据库中。每个数据库可以部署在不同的物理服务器上,从而分担存储和处理压力。
- 分表: 将原本存储在单一数据表中的数据按照某种规则划分到多个数据表中。通常使用的划分规则包括按照数据范围、哈希值、业务类型等。
分库分表的优点:
- 提高并发能力: 分库分表可以将数据存储和查询的压力分散到多个数据库和数据表中,从而提高系统的并发处理能力。
- 扩展存储容量: 可以通过增加数据库实例和数据表数量来扩展系统的存储容量,应对不断增长的数据量。
- 降低单库单表的锁竞争: 将数据分散到多个数据库和数据表中可以减少单一数据库或数据表上的锁竞争,提高系统的并发性能。
- 提高查询性能: 在一定情况下,合理设计分库分表的策略可以提高查询的性能,例如将数据按照地理位置或时间范围进行分表,可以减少跨表查询的开销。
分库分表的挑战和注意事项:
- 数据一致性: 分库分表会增加数据一致性的管理难度,特别是在跨库、跨表事务和查询中需要谨慎处理。
- 跨节点查询: 分库分表后,涉及跨数据库、跨表的查询会变得复杂,需要通过合适的数据路由和查询优化来提高效率。
- 分片策略选择: 需要根据业务特点和需求选择合适的分片策略,避免分片不均匀或导致热点数据集中在某个分片上。
- 数据迁移和维护: 数据库的扩容、迁移和维护会变得复杂,需要考虑到分片间的数据迁移和同步机制。
- 分库分表的成本: 分库分表需要投入更多的人力、物力和财力进行设计、开发和运维,成本相对较高。
在实际应用中,需要根据业务需求、系统规模和技术栈等因素来综合考虑是否采用分库分表方案,以及如何合理地设计和实施分库分表策略。
能说说什么是索引吗?索引的种类有哪些?
索引用来快速地寻找那些具有特定值的记录,所有MySQL索引都以B+树的形式保存。如果没有索引,执行查询时MySQL必须从第一个记录开始扫描整个表的所有记录,直至找到符合要求的记录。表里面的记录数量越多,这个操作的代价就越高。如果作为搜索条件的列上已经创建了索引,MySQL无需扫描任何记录即可迅速得到目标记录所在的位置。如果表有1000个记录,通过索引查找记录至少要比顺序扫描记录快100倍。
主键索引 哈希索引 唯一索引 普通索引 全文索引
什么情况下适合建索引?
建立索引可以提高数据库查询的性能,但并不是在所有情况下都适合建立索引。一般来说,以下情况适合建立索引:
- 经常用于检索的列: 如果某列经常用于查询条件或者连接条件,建立索引可以加快查询速度。例如,经常以用户ID进行查询的表可以在用户ID列上建立索引。
- 唯一性约束列: 主键和唯一性约束的列自动具有索引,因为它们需要保证唯一性,建立索引可以提高查找速度。
- 经常用于排序的列: 如果某列经常用于排序操作,例如ORDER BY子句中的列,建立索引可以提高排序性能。
- 经常用于分组的列: 如果某列经常用于分组操作,例如GROUP BY子句中的列,建立索引可以提高分组性能。
- 经常用于连接的列: 如果某列经常用于连接操作,例如JOIN操作中的列,建立索引可以提高连接性能。
- 大数据表中的查询性能优化: 对于大数据表,建立索引可以加快查询速度,特别是在涉及大量数据的情况下。
需要注意的是,虽然建立索引可以提高查询性能,但也会增加写操作的成本,并且会占用额外的存储空间。因此,在选择建立索引时需要权衡查询性能和写操作成本,避免过度索引导致性能下降。同时,对于一些稀疏的列或者很少使用的列,建立索引可能并不会带来明显的性能提升,甚至会降低性能,因此需要谨慎考虑是否建立索引。
你知道B+树的叶子结点都可以存放哪些东西吗?
在 InnoDB 存储引擎中,索引是通过 B+ 树结构来实现的。B+ 树的叶子节点存储着实际的数据行,这些数据行可以是整行数据,也可以是主键的值。
- 聚簇索引(Clustered Index):当索引的叶子节点存储的是整行数据时,这种索引被称为聚簇索引。在 InnoDB 中,主键索引就是一种聚簇索引。因为 InnoDB 表数据存储在主键索引的叶子节点上,所以主键索引也被称为聚簇索引。
- 非聚簇索引(Non-Clustered Index):当索引的叶子节点只存储了主键的值时,这种索引被称为非聚簇索引。在 InnoDB 中,除了主键索引外的其他索引都是非聚簇索引。非聚簇索引的叶子节点存储了主键的值,通过主键值再去查找对应的数据行。
使用聚簇索引的优点是可以减少磁盘 I/O,因为数据行和索引位于同一块页上,可以减少额外的查找操作。而非聚簇索引则需要先通过索引查找到主键值,再通过主键值查找对应的数据行,因此可能会增加额外的 I/O 操作。
聚簇索引和非聚簇索引,在查询的时候有区别吗?
- 聚簇索引查询:
- 当使用主键进行查询时,由于主键索引就是聚簇索引,数据库可以直接在主键索引上进行搜索,并且在找到匹配的主键后,可以直接获取对应的整行数据,因为数据行就存储在主键索引的叶子节点上。这样的查询效率通常很高,因为只需要一次索引搜索就可以获取所需数据。
- 如果查询涉及到主键之外的其他列,数据库可能需要根据主键值再次查找对应的数据行,这可能会导致额外的 I/O 操作。
- 非聚簇索引查询:
- 当使用非主键索引进行查询时,数据库首先会在非聚簇索引上进行搜索,找到匹配的索引条目(通常是主键的值),然后再根据主键值去主键索引中查找对应的数据行。这就意味着对于非聚簇索引的查询,通常需要进行两次索引搜索,一次是在非聚簇索引上,另一次是在主键索引上。
- 因为需要额外的主键查找步骤,非聚簇索引的查询效率可能会略低于聚簇索引。
总的来说,聚簇索引在查询时通常具有更高的效率,因为它直接将数据行与索引绑定在一起,减少了额外的查找步骤。而非聚簇索引则可能需要更多的 I/O 操作来获取完整的数据行。
mysql日志:redo log、bin log、undo log 区别与作用
一、redo log
重做日志
作用:确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。
内容:物理格式的日志,记录的是物理数据页面的修改的信息,其redo log是顺序写入redo log file的物理文件中去的。
二、bin log
归档日志(二进制日志)
作用:用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步。
用于数据库的基于时间点的还原。
内容:逻辑格式的日志,可以简单认为就是执行过的事务中的sql语句。
但又不完全是sql语句这么简单,而是包括了执行的sql语句(增删改)反向的信息,也就意味着delete对应着delete本身和其反向的insert;update对应着update执行前后的版本的信息;insert对应着delete和insert本身的信息。
bin log 有三种模式:Statement(基于 SQL 语句的复制)、Row(基于行的复制) 以及 Mixed(混合模式)
三、undo log
回滚日志
作用:保存了事务发生之前的数据的一个版本,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读
内容:逻辑格式的日志,在执行undo的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,这一点是不同于redo log的。
mysql锁
一、共享锁(Shared Lock)
共享锁(Shared Lock),又称S锁、读锁。针对行锁。
当有事务对数据加读锁后,其他事务只能对锁定的数据加读锁,不能加写锁(排他锁),所有其他事务只能读,不能写。
主要为了支持并发读的场景,读时不允许写操作。
加锁方式:
select * from T where id=1 lock in share mode;
释放方式:
commit、rollback;
二、排他锁(EXclusive Lock)
排他锁(EXclusive Lock),又称X锁、独占锁、写锁。针对行锁。
当有事务对数据加写锁后,其他事务不能再对锁定的数据加任何锁,又因为InnoDB对select语句默认不加锁,所以其他事务除了不能写操作外,照样是允许读的(尽管不允许加读锁)。主要为了在事务进行写操作时,不允许其他事务修改。
加锁方式:
自动:DML语句默认加写锁
手动:select * from T where id=1 for update;
释放方式:commit、rollback;
InnoDB行锁实现方式
MySQL InnoDB 行锁是通过给索引上的索引项加锁来实现的。
只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
实现原理:
InnoDB行锁是通过给索引项加锁来实现的,这一点MysqL和oracle不同,后者是通过在数据库中对相应的数据行加锁来实现的,InnoDB这种行级锁决定,只有通过索引条件来检索数据,才能使用行级锁,否则,直接使用表级锁。
特别注意: 使用行级锁一定要使用索引
什么是MVCC?
- MVCC(多版本并发控制):
- MVCC 是一种并发控制方法,旨在提高数据库的并发性能。
- 在 MySQL InnoDB 中,MVCC 的实现主要是为了处理读写冲突,实现非阻塞并发读。
- MVCC 通过维护数据的多个版本来实现读写操作的并发,从而避免了传统的加锁方式。
- 当前读:
- 在 RR(可重复读)隔离级别下,第一次 select 操作是当前读。
- 在 RC(提交读)隔离级别下,每个 select 操作都是当前读。
- 当前读操作读取的是记录的最新版本,并且会对读取的记录进行加锁,以防止其他并发事务修改。
- 快照读:
- 快照读是一种非阻塞读,不会对记录进行加锁。
- 在隔离级别不是串行级别下,select 操作通常是快照读,基于 MVCC 实现。
- 快照读可能读取的是数据的历史版本,而不一定是最新版本。
- MVCC、当前读和快照读的关系:
- MVCC 是维护多个数据版本来实现并发控制的方法。
- 当前读和快照读是 MySQL 中实现 MVCC 的两种方式之一,快照读是为了实现非阻塞的并发读取而设计的。
- MVCC 的具体实现包括三个组件:隐式字段、undo 日志和 read view。
MVCC解决的问题是什么?
MVCC(多版本并发控制)解决了数据库系统中的并发控制问题,主要包括以下几个方面:
- 读-写冲突:
- 在传统的加锁并发控制机制下,读操作和写操作之间存在冲突。读操作需要对数据进行共享锁(S 锁),而写操作需要对数据进行排它锁(X 锁)。当一个事务正在写入数据时,其他事务无法读取数据,导致阻塞和性能下降。
- MVCC 允许读操作与写操作并发进行,读操作不会被写操作所阻塞,从而提高了并发性能。
- 写-写冲突:
- 在传统的加锁并发控制机制下,写操作之间也存在冲突。两个事务同时尝试修改同一条数据时,会发生死锁或者一个事务被回滚。
- MVCC 通过维护数据的多个版本来解决写-写冲突,每个事务可以同时修改同一条数据的不同版本,从而避免了写操作之间的冲突。
- 并发度提高:
- MVCC 允许读操作不加锁,从而提高了数据库系统的并发度。多个事务可以同时读取同一份数据的不同版本,而不会互相阻塞。
- 快照读:
- MVCC 支持快照读,即事务可以读取数据在事务开始时的一个一致性快照,而不受其他事务对数据的修改影响。这种方式避免了读取过程中数据被修改的问题,保证了读操作的一致性。
总的来说,MVCC 解决了传统加锁并发控制机制下的阻塞和性能瓶颈问题,提高了数据库系统的并发性能和吞吐量,同时保证了数据的一致性和事务的隔离性。
同步锁、死锁、乐观锁、悲观锁
同步锁:
当多个线程同时访问同一个数据时,很容易出现问题。为了避免这种情况出现,我们要保证线程同步互斥,就是指并发执行的多个线程,在同一时间内只允许一个线程访问共享数据。Java 中可以使用synchronized 关键字来取得一个对象的同步锁。
死锁:
何为死锁,死锁是指两个或多个事务相互持有对方所需要的资源,导致它们永远无法继续执行下去的情况。
乐观锁:
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS 算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition 机制,其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式
CAS 实现的。
悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java 中 synchronized和 ReentrantLock 等独占锁就是悲观锁思想的实现。
mysql执行流程
客户端发起SQL查询,首先通过连接器,它会检查用户的身份,包括校验账户密码,权限
然后会查询缓存,如果缓存命中直接返回,如果没有命中再执行后续操作,但是MySQL8.0之后已经删除了缓存功能
接下来到达分析器,主要检查语法词法,比如SQL有没有写错,总共有多少关键字,要查询哪些东西
然后到达优化器,他会以自己的方式优化我们的SQL
最后到达执行器,调用存储引擎执行SQL并返回结果
优化sql从哪些方面着手?
不需要的字段就不要查询出来
小结果集驱动大结果集,将能过滤更多数据的条件写到前面
in和not in尽量不要用,会导致索引失效
避免在where中使用or链接条件,这会导致索引失效
给经常要查询的字段建立索引
在可能的情况下可以通过使用中间表等措施对SQL语句做一定的“分解”,使每 一步查询都能在较短时间完成,从而减少锁冲突。如果复杂查询不可避免,应尽量安排在数据库空闲时段执行,比如一些定期统计可以安排在夜间执行。
如果表数据量实在太庞大了,考虑数据库的分表优化。
如何定位慢sql
通过druid连接池的内置监控来定位慢SQL
通过MySQL的慢查询日志查看慢SQL
通过show processlist,查看当前数据库SQL执行情况来定位慢SQL
页面上发起的一个查询很慢,你怎么去优化
首先看一下硬件和网络层面,有没有什么异常
然后分析代码有没有什么问题,算法有没有什么缺陷,比如多层嵌套循环
最后我们再定位到慢SQL,比如
通过druid连接池的内置监控来定位慢SQL
通过MySQL的慢查询日志查看慢SQL
通过show processlist,查看当前数据库SQL执行情况来定位慢SQL
定位到慢SQL再考虑优化该SQL,比如说
不需要的字段就不要查询出来
小结果集驱动大结果集,将能过率更多数据的条件写到前面in和not in尽量不要用,会导致索引失效
避免在where中使用or链接条件,这会导致索引失效
考虑如果不需要事务,并且主要查询的化,可以考虑使用MyISAM存储引擎
如果优化SQL后还是很慢,可以考虑给查询字段建索引来提升效率
如果建立索引了还是慢,看一下是不是数据量太庞大了,应该考虑分表了
Linux
linux中常见命令
- 文件和目录操作:
ls
:列出目录中的文件和子目录。cd
:更改当前工作目录。pwd
:显示当前工作目录的路径。mkdir
:创建新目录。rm
:删除文件或目录。cp
:复制文件或目录。mv
:移动文件或目录。
- 文件内容查看和编辑:
cat
:查看文件内容。less
:逐页查看文件内容。head
:显示文件的开头几行。tail
:显示文件的末尾几行。nano
:基本文本编辑器。vim
或vi
:强大的文本编辑器。
- 系统信息查看:
uname
:显示系统信息。uptime
:显示系统的运行时间和平均负载。hostname
:显示计算机的主机名。df
:显示文件系统的磁盘空间使用情况。free
:显示内存使用情况。
- 进程管理:
ps
:显示当前运行的进程。top
:实时显示系统中各个进程的资源占用情况。kill
:终止指定进程。killall
:终止指定名称的所有进程。
- 用户和权限管理:
whoami
:显示当前登录的用户名。who
:显示当前登录系统的用户信息。su
:切换用户。sudo
:以超级用户权限执行命令。chmod
:修改文件权限。chown
:修改文件所有者。
- 网络相关:
ping
:测试与另一台计算机的连接。ifconfig
或ip
:显示和配置网络接口信息。netstat
:显示网络状态和连接。 netstat -nltpssh
:远程登录到另一台计算机。scp
:安全地复制文件到另一台计算机。
这些是 Linux 中的一些常见命令,可以帮助你进行文件和目录操作、查看系统信息、管理进程、用户和权限,以及进行网络相关的操作。
部署过系统没有?
我们公司里面的系统部署是运维再做,但是 我是能部署的,我说一下部署的流程。
比如说我们公司里面的测试,他们部署不来,都是我们这边来部署到测试环境,我们的生产环境是运维做的。
说一下,怎么部署的。
系统架构
手工:自己打包,安装好环境,将我们的应用放在上面。
自动:CI/CD流程化
在系统中如何查看进程,杀进程
-
查看进程:
-
使用
ps
命令可以查看当前系统中正在运行的进程。常用的选项包括:
ps aux
:显示所有用户的所有进程。ps -ef
:显示所有进程的详细信息。
-
-
杀死进程:
- 使用kill命令来终止指定进程。你可以提供进程的 PID(进程 ID)来杀死进程。
- 例如:
kill PID
,其中 PID 是要终止的进程的 ID。
- 例如:
- 你也可以使用killall命令来杀死指定名称的所有进程。
- 例如:
killall firefox
将终止所有名为 “firefox” 的进程。
- 例如:
- 使用kill命令来终止指定进程。你可以提供进程的 PID(进程 ID)来杀死进程。
有时候,如果进程无法正常终止,你可以尝试使用 kill -9 PID
命令,其中 -9
选项表示强制终止进程。但请注意,强制终止可能导致数据丢失或其他问题,应该谨慎使用。
如何查看端口占用情况
netstat -nltp :启动过程发现某些端口被别人占用,我就想知道是谁占用这个端口
如何将一个程序放到后台运行
-
第一个命令:
nohup java -jar springboot-water.jar 1>water.log 2>error.log &
这个命令使用了
nohup
命令,它可以让你在退出终端或关闭会话后继续运行程序。1>water.log
和2>error.log
分别将标准输出和标准错误重定向到文件water.log
和error.log
中。最后的&
符号表示将命令放入后台运行。 -
第二个命令:
nohup java -jar springboot-water.jar 1>water.log 2>&1 &
这个命令与第一个命令的区别在于标准错误的重定向方式。
2>&1
表示将标准错误重定向到与标准输出相同的地方,即都输出到water.log
文件中。
无论你选择哪个命令,都会将 Java 程序以后台进程的方式运行,并将输出和错误信息记录到指定的日志文件中。
如何查看系统的负载
-
使用
uptime
命令:uptime
这个命令会显示系统的当前时间、系统已运行时间以及系统的平均负载。平均负载通常会分别显示 1 分钟、5 分钟和 15 分钟的负载情况。
-
使用
top
命令:top
这个命令会显示系统中当前运行的进程列表,同时也会显示系统的负载情况、CPU 使用情况和内存使用情况等。你可以按下
q
键退出top
命令。 -
使用
w
命令:w
这个命令会显示当前登录系统的用户信息,包括登录时间、运行时间以及系统的平均负载。
-
使用
sar
命令:sar -q
这个命令会显示系统的平均负载情况,以及与 CPU 相关的统计信息。你可能需要安装
sysstat
包来使用sar
命令。
通过这些命令,你可以了解系统的当前负载情况,以便及时调整资源或排查问题。
系统出问题了如何排错
排查错误 就是查看日志,所以系统里面一定记录日志
如何查看:tail -200f xx.log
你们用的linux发行版是什么?
centos7
你写过shell脚本吗?
自己有时间去简单的看一下。
或者你说写过,基本上是拿别人的过来改。
df
:显示文件系统的磁盘空间使用情况。free
:显示内存使用情况。
- 进程管理:
ps
:显示当前运行的进程。top
:实时显示系统中各个进程的资源占用情况。kill
:终止指定进程。killall
:终止指定名称的所有进程。
- 用户和权限管理:
whoami
:显示当前登录的用户名。who
:显示当前登录系统的用户信息。su
:切换用户。sudo
:以超级用户权限执行命令。chmod
:修改文件权限。chown
:修改文件所有者。
- 网络相关:
ping
:测试与另一台计算机的连接。ifconfig
或ip
:显示和配置网络接口信息。netstat
:显示网络状态和连接。 netstat -nltpssh
:远程登录到另一台计算机。scp
:安全地复制文件到另一台计算机。
这些是 Linux 中的一些常见命令,可以帮助你进行文件和目录操作、查看系统信息、管理进程、用户和权限,以及进行网络相关的操作。
部署过系统没有?
我们公司里面的系统部署是运维再做,但是 我是能部署的,我说一下部署的流程。
比如说我们公司里面的测试,他们部署不来,都是我们这边来部署到测试环境,我们的生产环境是运维做的。
说一下,怎么部署的。
系统架构
手工:自己打包,安装好环境,将我们的应用放在上面。
自动:CI/CD流程化
在系统中如何查看进程,杀进程
-
查看进程:
-
使用
ps
命令可以查看当前系统中正在运行的进程。常用的选项包括:
ps aux
:显示所有用户的所有进程。ps -ef
:显示所有进程的详细信息。
-
-
杀死进程:
- 使用kill命令来终止指定进程。你可以提供进程的 PID(进程 ID)来杀死进程。
- 例如:
kill PID
,其中 PID 是要终止的进程的 ID。
- 例如:
- 你也可以使用killall命令来杀死指定名称的所有进程。
- 例如:
killall firefox
将终止所有名为 “firefox” 的进程。
- 例如:
- 使用kill命令来终止指定进程。你可以提供进程的 PID(进程 ID)来杀死进程。
有时候,如果进程无法正常终止,你可以尝试使用 kill -9 PID
命令,其中 -9
选项表示强制终止进程。但请注意,强制终止可能导致数据丢失或其他问题,应该谨慎使用。
如何查看端口占用情况
netstat -nltp :启动过程发现某些端口被别人占用,我就想知道是谁占用这个端口
如何将一个程序放到后台运行
-
第一个命令:
nohup java -jar springboot-water.jar 1>water.log 2>error.log &
这个命令使用了
nohup
命令,它可以让你在退出终端或关闭会话后继续运行程序。1>water.log
和2>error.log
分别将标准输出和标准错误重定向到文件water.log
和error.log
中。最后的&
符号表示将命令放入后台运行。 -
第二个命令:
nohup java -jar springboot-water.jar 1>water.log 2>&1 &
这个命令与第一个命令的区别在于标准错误的重定向方式。
2>&1
表示将标准错误重定向到与标准输出相同的地方,即都输出到water.log
文件中。
无论你选择哪个命令,都会将 Java 程序以后台进程的方式运行,并将输出和错误信息记录到指定的日志文件中。
如何查看系统的负载
-
使用
uptime
命令:uptime
这个命令会显示系统的当前时间、系统已运行时间以及系统的平均负载。平均负载通常会分别显示 1 分钟、5 分钟和 15 分钟的负载情况。
-
使用
top
命令:top
这个命令会显示系统中当前运行的进程列表,同时也会显示系统的负载情况、CPU 使用情况和内存使用情况等。你可以按下
q
键退出top
命令。 -
使用
w
命令:w
这个命令会显示当前登录系统的用户信息,包括登录时间、运行时间以及系统的平均负载。
-
使用
sar
命令:sar -q
这个命令会显示系统的平均负载情况,以及与 CPU 相关的统计信息。你可能需要安装
sysstat
包来使用sar
命令。
通过这些命令,你可以了解系统的当前负载情况,以便及时调整资源或排查问题。
系统出问题了如何排错
排查错误 就是查看日志,所以系统里面一定记录日志
如何查看:tail -200f xx.log
你们用的linux发行版是什么?
centos7
你写过shell脚本吗?
自己有时间去简单的看一下。
或者你说写过,基本上是拿别人的过来改。