类是对象的抽象,对象时类的具体,类是对象的模板,对象是类的实例
Super表示当前类的父类对象
This表示当前类的对象
java.utilCollection是一个集合接口(集合类的一个顶级接口),它提供了对集合对象进行基本操作的通用接口方法,Collection接口在java类型中有很多具体的实现,Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接集成接口有List和Set
Collctions则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序,搜索以及线程安全等各种操作
相同点:都是返回第一个元素,并在队列中删除返回的对象
不同点:如果没有元素poll会返回null,而remove会直接抛出NoSuchElementException异常
显示转换就是类型强转,把一个大类型的数据强制赋值给小类型的数据,隐式转换就是大范围的变量能够接收小范围的数据,隐式转换和显式转换其实就是自动类型转换和强制类型转换
有指针,但是隐藏了,开发人员无法直接操作指针,由jvm来操作指针
理论上来说,java都是引用传递,对于基本数据类型,传递是值的副本,而不是值本身,对于对象类型,传递是对象的引用,当在一个方法操作参数的时候,其实操作的是引用所指向的对象
改变了,因为传递是对象的引用,操作的是引用所指向的对象
Class文件是一组以8位字节为基础单位的二进制流,各个数据项严格按顺序排列
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这样的伪结构仅仅有两种数据类型:无符号数和表
无符号数:是基本数据类型,以U1,U2,U4,U8分别代表一个字节,两个字节,四个字节,八个字节的无符号数,能够用来描写叙述数字,索引引用,数量值或者依照UTF-8编码构成的字符串值
表:由多个无符号数或者其他表作为数据项构成的符合数据类型,全部表习惯性的以_info结尾
形参:全称为:“形式参数”,是在定义方法名和方法体的时候使用的参数,用于接收调用该方法是传入的实际值
实参:全称为"实际参数",是调用该方法时传递给该方法的实际值
静态代理:
由程序员创建或工具生成代理类的源码,再编译代理类,所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托的关系在运行前就确定了
缺点:每个需要代理的对象都需要自己重复编写代理,很不舒服
优点:但是可以面相实际对象或者是接口的方式实现代理
动态代理:
也叫做JDK代理,接口代理,动态代理的对象,是利用JDK的API,动态的在内存中构建代理对象(是根据被代理的接口来动态生成代理类的class文件,并加载运行的过程),这就是动态代理
优点:不用关心代理类,只需要在运行阶段才指定代理哪一个对象
当前主流VM垃圾收集都采用分代收集(Fenerational Collection)算法,这种算法会根据对象存活周期的不同将内存划分为几块,如JVM中的新生代,老年代,永久代,这样就可以根据个年代特点分别采用最适合的GC算法
公共静态不可变(public static final)变量也即是我们所说的编译器常量,这里的public是可选的,实际上这些变量在编译时会被替换掉,因为编译器知道这些变量的值,并且知道这些变量在运行时不能改变,这种存在的一个问题是你使用了一个内部的或第三方库中的共有编译时常量,当时这个值后面被其他人改变了,当时你的客户端仍然在使用老的值,甚至你已经部署了一个洗呢jar,为了避免这种情况,当你在更新依赖jar文件时,确保重新编译你的程序
控制反转(IOC)是Spring框架的核心思想,用我自己的话说,就是你要做一件事,别自己可劲new了,你就说你要干啥,然后外包出去就好
依赖注入(DI)在我浅薄的想法中,就是通过接口的引用和构造方法的表达,将一些事情整好了反过来传给需要用到的地方
⾯向对象易维护、易复⽤、易扩展。 因为⾯向对象有封装、继承、多态性的特
性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,⾯向对象性能
⽐⾯向过程低。
不一样,因为内存的分配方式不一样,String str =“i” 的方式,Java虚拟机会将其分配到常量池中,而String str = new String (“i”)则会被分到堆内存中
不会,在下一个垃圾回收周期中,这个对象将是可被回收的
数据库连接是非常消耗资源的,影响到程序的性能指标,连接池是用来分配,管理释放数据库连接的,可以使应用重复使用同一个数据库连接,而不是每次都创建一个新的数据库连接连接,通过释放空闲时间较长的数据库连接避免使用数据库因为创建太多的连接而造成的连接遗漏问题,提高了程序性能
Dbcp,c3p0den,用的最多的还是c3p0,因为更加稳定,安全,通过配置文件的形式来维护数据库信息,而不是通过硬编码,当连接的数据库信息发生改变时,不需要再更改程序代码就实现了数据库信息的更新
不能,定义抽象类就是让其他类继承的,如果定义为final该类就不能被基础,这样彼此就会产生矛盾,所以final不能修饰抽象类
Java中数据类型分两种:
1.基本类型:long,int,byte,float,double,char
2.对象类型:Long,Integer,Byte,Float,Double其它一切java提供的,或者你自己创建的类。其中Long叫 long的包装类。Integer、Byte和Float也类似,一般包装类的名字首写是数值名的大写开头。
ID用long还是Long?
hibernate、el表达式等都是包装类型,用Long类型可以减少装箱/拆箱;
在hibernate中的自增的hid在实体中的类型要用Long 来定义而不是long。否则在DWR的匹配过程中会出现Marshallingerror:null的错误提示。
到底是选择Long 还是long这个还得看具体环境,如果你认为这个属性不能为null,那么就用long,因为它默认初值为0,如果这个字段可以为null,那么就应该选择Long。
JVM:java虚拟机,运用硬件或软件手段实现的虚拟的计算机
Java虚拟机包括:寄存器,堆栈,处理器
Java 虚拟机(JVM)是运⾏ Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),⽬的是使⽤相同的字节码,它们都会给出相同的结果。
什么是字节码?采⽤字节码的好处是什么?
JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的⽂件),它不⾯向任何特定的处理器,只⾯向虚拟机。由于字节码并不针对⼀种特定的机器,因此,Java 程序⽆须重新编译便可在多种不同操作系统的计算机上运⾏。
Java程序从源代码到运行一般步骤:
.java文件(源代码)经过JDK中的javac编译,生成.class文件(JVM中可理解的Java字节),JVM生成机器可执行的二进制机器码
为什么java是编译与解释共存的语言?
.class->机器码 这⼀步。有些⽅法和代码块是经常需要被调⽤的(也就是所谓的热点代码),所以后⾯引进了 JIT 编译器,⽽ JIT 属于运行时编译。当 JIT 编译器完成第⼀次编译后,其会将字节码对应的机器码保存下来,下次可以直接使⽤。机器码的运⾏效率肯定是⾼于 Java 解释器的。这也解释了我们为什么经常会说 Java 是编译与解释共存的语⾔。
Java 既有解释执行,也有编译执行,为了解决解释器的性能瓶颈问题,优化 Java 的性能,引入了即时编译器,大幅度的提高运行效率。
JDK 是 Java Development Kit,它是功能⻬全的 Java SDK。
JRE 是 Java 运⾏时环境。它是运⾏已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟(JVM),Java 类库,java 命令和其他的⼀些基础构件。
B/S(Browser/Server),浏览器/服务器程序
C/S(Clent/Server),客户端/服务端,桌面应用程序
JAVA SE:主要用在客户端开发
JAVA EE:主要用在web应用程序开发
JAVA ME:主要用在嵌入式应用程序开发
面向对象思想OOP
抽象
关键词abstract声明的类叫作抽象类,abstract声明的⽅法叫抽象⽅法
⼀个类⾥包含了⼀个或多个抽象⽅法,类就必须指定成抽象类
抽象⽅法属于⼀种特殊⽅法,只含有⼀个声明,没有⽅法体
抽象支付:pay(金额,订单号),默认实现是本地支付,微信支付,支付宝支付,银行卡支付
封装
封装是把过程和数据包围起来,对数据的访问只能通过已定义的接⼝即⽅法
在java中通过关键字private,protected和public实现封装。
封装把对象的所有组成部分组合在⼀起,封装定义程序如何引⽤对象的数据,
封装实际上使⽤⽅法将类的数据隐藏起来,控制⽤户对类的修改和访问数据的程度。 适当的
封装可以让代码更容易理解和维护,也加强了代码的安全性
类封装
⽅法封装
继承
⼦类继承⽗类的特征和行为,使得⼦类对象具有⽗类的方法和属性(包括私有属性和私有⽅法),但是⽗类中的私有属性和⽅法⼦类是⽆法访问,只是拥有。⽗类也叫基类,具有公共的⽅法和属性
动物<-猫
动物<-狗
abstract class AbsPay{
}
WeixinPay extends AbsPay{
}
AliPay extends AbsPay{
}
多态
所谓多态就是指程序中定义的引⽤变量所指向的具体类型和通过该引⽤变量发出的⽅法调⽤在编程时并不确定,⽽是在程序运⾏期间才确定,即⼀个引⽤变量到底会指向哪个类的实例对象,该引⽤变量发出的⽅法调⽤到底是哪个类中实现的⽅法,必须在由程序运⾏期间才能决定。
多态性分为编译时的多态性和运行时的多态性。方法重载实现的是编译时的多态性,而方法重写实现的是运行时的多态性。
优点:减少耦合、灵活可拓展
⼀般是继承类或者重写⽅法实现
Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到⼀个类中有多个构造函数的情况。
重载Overload:表示同一个类中可以有多个名称相同的方法,但这些方法的参数列表各不相同,参数个数或类型不同
重写Override:重写就是当⼦类继承⾃⽗类的相同⽅法,输⼊数据⼀样,但要做出有别于⽗类的响应时,你就要覆盖⽗类⽅法
重写发⽣在运⾏期,是⼦类对⽗类的允许访问的⽅法的实现过程进⾏重新编写。
内存泄漏的原因很简单:
常见的内存泄漏的例子:
解决这个内存泄漏问题也很简单,将set设置为null,那就可以避免上述内存泄漏问题了,其他内存内存泄漏得一步一步分析了
内存溢出的原因:
解决:
查看程序是否存在内存泄漏的问题
设置参数加大空间
代码中是否存在死循环或者循环产生过多重复的对象实
查看是否使用nio直接操作内存
String为什么是不可变的?
可变性
简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串, private final char value[] (在 Java 9 之后,String 类的实现改⽤ byte 数组存储字符串 private final byte[] value),所以 String 对象是不可变的。
⽽ StringBuilder 与 StringBuffer 都继承⾃ AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使⽤字符数组保存字符串 char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是StringBuilder 与 StringBuffer 的公共⽗类,定义了⼀些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共⽅法。StringBuffer 对⽅法加了同步锁或者对调⽤的⽅法加了同步锁,所以是线程安全的。StringBuilder 并没有对⽅法进⾏加同步锁,所以是⾮线程安全的。
以StringBuffer的apend举例:
性能
每次对 String 类型进⾏改变的时候,都会⽣成⼀个新的 String 对象,然后将指针指向新的 String对象。StringBuffer 每次都会对 StringBuffer 对象本身进⾏操作,⽽不是⽣成新的对象并改变对象引⽤。
相同情况下使⽤ StringBuilder 相⽐使⽤ StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的⻛险
总结:
装箱:将基本类型⽤它们对应的引⽤类型包装起来;
拆箱:将包装类型转换为基本数据类型;
由于静态⽅法可以不通过对象进⾏调⽤,因此在静态⽅法⾥,不能调用其他非静态变量,也不可以访问⾮静态变量成员。
Non-static field ‘a’ cannot be referenced from a static context
Java 程序在执⾏⼦类的构造⽅法之前,如果没有用 super() 来调用父类特定的构造方法,则会调用父类中“没有参数的构造⽅法”。因此,如果⽗类中只定义了有参数的构造⽅法,⽽在⼦类的构造⽅法中⼜没有⽤ super() 来调⽤⽗类中特定的构造⽅法,则编译时将发⽣错误,因为 Java 程序在⽗类中找不到没有参数的构造⽅法可供执⾏。解决办法是在⽗类⾥加上⼀个不做事且没有参数的构造⽅法。
接口是否可以继承接口?接口是否支持多继承?类是否支持多继承?接口里面是否可以有方法实现?
接⼝⾥可以有静态⽅法和⽅法体
接⼝中所有的⽅法必须是抽象⽅法(JDK8之后就不是)
接⼝不是被类继承了,而是要被类实现
接⼝⽀持多继承, 类不⽀持多个类继承
⼀个类只能继承⼀个类,但是能实现多个接⼝,接⼝能继承另⼀个接⼝,接⼝的继承使⽤extends关键字,和类继承⼀样
JDK8接口新特性
interface中可以有static方法,但必须有⽅法实现体,该⽅法只属于该接⼝,接⼝名直接调⽤该⽅法
接⼝中新增default关键字修饰的方法,default⽅法只能定义在接⼝中,可以在⼦类或⼦接⼝ 中被重写default定义的⽅法必须有⽅法体
⽗接⼝的default⽅法如果在⼦接⼝或⼦类被重写,那么⼦接⼝实现对象、⼦类对象,调⽤该方法,以重写为准
本类、接⼝如果没有重写⽗类(即接⼝)的default⽅法,则在调⽤default⽅法时,使⽤⽗类(接口) 定义的default⽅法逻辑
⼀个类的构造⽅法的作⽤是什么? 若⼀个类没有声明构造⽅法,该程序能正确执⾏吗? 为什么?
主要作用是完成对类对象的初始化⼯作。可以执⾏。因为⼀个类即使没有声明构造⽅法也会有默认的不带参数的构造⽅法。
构造⽅法有哪些特性?
两个等号,如果是基本数据类型判断的是值,引用数据类型判断的是内存地址
equals() : 它的作⽤也是判断两个对象是否相等。但它⼀般有两种使⽤情况:
情况 1:类没有覆盖 equals() ⽅法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
情况 2:类覆盖了 equals() ⽅法。⼀般,我们都覆盖 equals() ⽅法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
==hashCode() 的作⽤是获取哈希码,也称为散列码;它实际上是返回⼀个 int 整数。==这个哈希码的作⽤是确定该对象在哈希表中的索引位置。 hashCode() 定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashcode ⽅法是本地⽅法,也就是⽤ c 语⾔或 c++ 实现的,该⽅法通常⽤来将对象的 内存地址 转换为整数之后返回。
public native int hashCode();
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就
利⽤到了散列码!(可以快速找到所需要的对象)
如果两个对象相等,则 hashcode ⼀定也是相同的。两个对象相等,对两个对象分别调⽤ equals⽅法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不⼀定是相等的 。因此,equals ⽅法被覆盖过,则 hashCode ⽅法也必须被覆盖。
主要⽤在三个地⽅:变量、⽅法、类。
变量:如果是基本数据类型,那么加上final字段后,其值就不能进行更改,如果是引用数据类型,那么就不能让其指向另一个对象
方法:1.锁定方法,防止继承类修改它的含义;2.是效率。在早期的 Java 实现版本中,会将 final ⽅法转为内嵌调⽤。但是如果⽅法过于庞大,可能看不到内嵌调⽤带来的任何性能提升(现在的 Java 版本已经不需要使⽤final ⽅法进⾏这些优化了)。类中所有的 private ⽅法都隐式地指定为 final。
类:加了final字段的类不允许被继承,其中所有成员方法被隐式地在指定为final方法
在 Java 中,所有的异常都有⼀个共同的祖先 java.lang 包中的 Throwable 类。 Throwable 类有两个重要的子类** Exception (异常)**和 Error (错误)。Exception 能被程序本身处理( try catch ),Error 是⽆法处理的(只能尽量避免)。
在以下 3 种特殊情况下, finally 块不会被执⾏:
Java 序列化中如果有些字段不想进⾏序列化,怎么办?
使用transient或者transient注解
transient 关键字的作⽤是:阻⽌实例中那些⽤此关键字修饰的的变量序列化;当对象被反序列化时,被transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和⽅法。
⽅法 1:通过 Scanner
Scanner sc = new Scanner(System.in);
方法2:通过BufferedReader
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
Annotation(注解)是 Java 提供的一种对元程序中元素关联信息和元数据(metadata)的途径和方法。Annatation(注解)是一个接口,程序可以通过反射来获取指定程序中元素的 Annotation对象,然后通过该 Annotation 对象来获取注解中的元数据信息。
@Target说明了Annotation所修饰的对象范围: Annotation可被用于packages、types(类、接口、枚举、Annotation 类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch 参数。在 Annotation 类型的声明中使用了 target 可更加明晰其修饰的目标
Retention 定义了该 Annotation 被保留的时间长短:表示需要在什么级别保存注解信息,用于描
述注解的生命周期(即:被描述的注解在什么范围内有效),取值(RetentionPoicy)由:
SOURCE:在源文件中有效(即源文件保留)
CLASS:在 class 文件中有效(即 class 保留)
RUNTIME:在运行时有效(即运行时保留)
@ Documented 用于描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API,因此可以被例如 javadoc 此类的工具文档化。
@Inherited 元注解是一个标记注解,@Inherited 阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited 修饰的 annotation 类型被用于一个 class,则这个annotation 将被用于该class 的子类。
/1:*** 定义注解*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
/**供应商编号*/
public int id() default -1;
/*** 供应商名称*/
public String name() default "";
/** * 供应商地址*/
public String address() default "";
}
//2:注解使用
public class Apple {
@FruitProvider(id = 1, name = "陕西红富士集团", address = "陕西省西安市延安路")
private String appleProvider;
public void setAppleProvider(String appleProvider) {
this.appleProvider = appleProvider;
}
public String getAppleProvider() {
return appleProvider;
} }/3:*********** 注解处理器 ***************/
public class FruitInfoUtil {
public static void getFruitInfo(Class<?> clazz) {
String strFruitProvicer = "供应商信息:";
Field[] fields = clazz.getDeclaredFields();//通过反射获取处理注解
for (Field field : fields) {
if (field.isAnnotationPresent(FruitProvider.class)) {
FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);
//注解信息的处理地方
strFruitProvicer = " 供应商编号:" + fruitProvider.id() + " 供应商名称:"
+ fruitProvider.name() + " 供应商地址:"+ fruitProvider.address();
System.out.println(strFruitProvicer);
}
}
} }
public class FruitRun {
public static void main(String[] args) {
FruitInfoUtil.getFruitInfo(Apple.class);
/***********输出结果***************/
// 供应商编号:1 供应商名称:陕西红富士集团 供应商地址:陕西省西安市延
} }
反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
Person p=new Student();
其中编译时类型为 Person,运行时类型为 Student。
不管是⽂件读写还是⽹络发送接收,信息的最⼩存储单元都是字节,那为什么I/O 流操作要分为字节流操作和字符流操作呢?
字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就⼲脆提供了⼀个直接操作字符的接⼝,⽅便我们平时对字符进⾏流操作。如果⾳频⽂件、图⽚等媒体⽂件⽤字节流⽐较好,如果涉及到字符的话使⽤字符流⽐好。
BIO (Blocking I/O): 同步阻塞 I/O 模式
NIO (Non-blocking/New I/O): NIO 是⼀种同步⾮阻塞的 I/O 模型
AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引⼊了 NIO 的改进版 NIO 2,它是异步⾮阻塞的 IO 模型。
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容
如果某个方法不能按照正常的途径完成任务,就可以通过另一种路径退出方法。在这种情况下会抛出一个封装了错误信息的对象。此时,这个方法会立刻退出同时不返回任何值。另外,调用这个方法的其他代码也无法继续执行,异常处理机制会将代码执行交给异常处理器。
**Throwable **是 Java 语言中所有错误或异常的超类。下一层分为 **Error 和 Exception **
Error
Exception(RuntimeException、CheckedException)
2. Exception 又有两个分支,一个是运行时异常 RuntimeException ,一个是CheckedException。
RuntimeException 如 : NullPointerException 、 ClassCastException ;一个是检查异常CheckedException,如 I/O 错误导致的 IOException、SQLException。 RuntimeException 是那些可能在 Java 虚拟机正常运行期间抛出的异常的超类。
Java 类中不仅可以定义变量和方法,还可以定义类,这样定义在类内部的类就被称为内部类。根据定义的方式不同,内部类分为静态内部类,成员内部类,局部内部类,匿名内部类四种。
静态内部类:public static class Inner
成员内部类:public class Inner
局部内部类:定义在方法中的类,就是局部类。如果一个类只在某个方法中使用,则可以考虑使用局部类。
public void test(final int c) {
final int d = 1;
class Inner {
public void print() {
System.out.println(c);
}
}
}
匿名内部类:
test.test(new Bird() {
public int fly() {
return 10000;
}
public String getName() {
return "大雁";
}
});
使用CallableStatement
使用PreparedStatement类,而不是使用Statement类
保存(持久化)对象及其状态到内存或者磁盘
Java 平台允许我们在内存中创建可复用的 Java 对象,但一般情况下,只有当 JVM 处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比 JVM 的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。
Java 对象序列化就能够帮助我们实现该功能。
序列化对象以字节数组保持-静态成员不保存
使用 Java 对象序列化,==在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。==必须注意地是,==对象序列化保存的是对象的”状态”,即它的成员变量。==由此可知,对象序列化不会关注类中的静态变量。
序列化用户远程对象传输
==除了在持久化对象时会用到对象序列化之外,当使用 RMI(远程方法调用),或在网络中传递对象时,==都会用到对象序列化。Java序列化API为处理对象序列化提供了一个标准机制,该API简单易用。
Serializable 实现序列化
在 Java 中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化。
ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化
通过 ObjectOutputStream 和 ObjectInputStream 对对象进行序列化及反序列化。writeObject 和 readObject 自定义序列化策略
在类中增加 writeObject 和 readObject 方法可以实现自定义序列化策略。
序列化 ID
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)
Transient 关键字阻止该变量被序列化到文件中
Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。
Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的子类,比如 List、Set 等。
Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序方法: Collections. sort(list)。
地带起是一种设计模式,它是一个兑现,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层机构,迭代器通常被称为"轻量级"对象,因为创建它的代价小
可以使用Collections.unmodifiableCollection(Collection c)方法来创建一个只读集合,这样改变集合的任何操作都会抛出Java.langUnsupportedoperationException异常
Java 的 List 是非常常用的数据类型。List 是有序的 Collection。Java List 一共三个实现类:分别是 ArrayList、Vector 和 LinkedList。
ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
Vector 与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList 慢。
LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。
Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法。
HashSet底层由HashMap实现
HashSet的值存放于HashMap的key上
HashMap的value统一为PRESENT
哈希表边存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素。哈希值相同 equals 为 false 的元素是怎么存储呢,就是在同样的哈希值下顺延(可以认为哈希值相同的元素放在一个哈希桶中)。也就是哈希一样的存一列。
List (对付顺序的好帮⼿): 存储的元素是有序的、可重复的。
Set (注重独⼀⽆⼆的性质): 存储的元素是⽆序的、不可重复的。
**Map **(⽤ Key 来搜索的专家): 使⽤键值对(kye-value)存储,类似于数学上的函数y=f(x),“x”代表 key,"y"代表 value,Key 是⽆序的、不可重复的,value 是⽆序的、可重复的,每个键最多映射到⼀个值。
public interface RandomAccess {
}
RandomAccess 接⼝中什么都没有定义。所以,RandomAccess 接⼝不过是⼀个标识罢了。标识什么? 标识实现这个接⼝的类具有随机访问功能。在 binarySearch ⽅法中,它要判断传⼊的 list 是否RamdomAccess 的实例,如果是,调用 indexedBinarySearch() ⽅法,如果不是,那么调⽤ iteratorBinarySearch() ⽅法
HashSet 是 Set 接⼝的主要实现类 , HashSet 的底层是 HashMap ,线程不安全的,可以存储 null 值;
LinkedHashSet 是 HashSet 的⼦类,能够按照添加的顺序遍历;
TreeSet 底层使⽤红⿊树,能够按照添加元素的顺序进⾏遍历,排序的⽅式有⾃然排序和定制排
序。
Fail-Fast:一旦发现遍历的同时其他人来修改,则立刻抛出异常
Fail-Safe:发现遍历的同事其他人来修改,应当能有应对策略,例如牺牲一致性来让整个遍历运行完成
从线程安全角度:
ArrayList:底层是数组实现,线程不安全,查询和修改非常快根,根据下标就可以进行操作时间复杂度1,但是增加和删除慢,需要移动大量的元素,时间复杂度n
LinkedList: 底层是双向链表,线程不安全,查询和修改速度慢,需要进行遍历操作,时间复杂度为n,但是增加和删除速度快,时间复杂度1
Vector: 底层是数组(Object[] )实现,线程安全的,操作的时候使用synchronized进行加锁
使用场景:
Vector已经很少用了
增加和删除场景多则用LinkedList
查询和修改多则用ArrayList
方式一:自己写个包装类,根据业务一般是add/update/remove加锁
方式二:Collections.synchronizedList(new ArrayList<>()); 使用synchronized加锁
//本质还是加锁
List list2 = Collections.synchronizedList(list1);
方式三:CopyOnWriteArrayList<>() 使用ReentrantLock加锁
CopyOnWriteArrayList:执行修改操作时,会拷贝一份新的数组进行操作(add、set、remove等),代价十分昂贵,在执行完修改后将原来集合指向新的集合来完成修改操作,源码里面用ReentrantLock可重入锁来保证不会有多个线程同时拷贝一份数组
以添加元素源码举例:
public boolean add(E e) {
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
场景:读高性能,适用读操作远远大于写操作的场景中使用(读的时候是不需要加锁的,直接获取,删除和增加是需要加锁的, 读多写少)
Collections.synchronizedList:线程安全的原因是因为它几乎在每个方法中都使用了synchronized同步锁
场景:CopyOnWriteArrayList适合读多的场景,synchronizedList适合写多的场景
场景:写操作性能比CopyOnWriteArrayList好,读操作性能并不如CopyOnWriteArrayList
设计思想:读写分离+最终一致
缺点:内存占用问题,写时复制机制,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象,如果对象过大(大对象会直接保存在老生代)则容易发生Yong GC和Full GC
注意:JDK1.7之前ArrayList默认大小是10,JDk1.8开始是未指定集合容量,默认是0,若已经指定的大小,(小于集合大小,小于10),当集合第一次添加元素的时候,集合大小扩容为10
ArrayList的元素个数大于其容量,扩容的大小=原始大小+原始大小/2
tips:关于ArraysList.addAll扩容机制详解
addAll(Collection c) 没有元素时,扩容为 Math.max(10, 实际元素个数),有元素时为 Math.max(原容量 1.5 倍, 实际元素个数)
if(list.size=0&&addAll.size>10) then list.size = addAll.size else if ((addAll+list.size)> 下次扩容容量) then list.size扩容 = addAll+list.size else if((addAll+list.size)< 下次扩容容量) then list.size扩容 = 下次扩容的容量
HashMap、Hashtable、LinkedHashMap、TreeMap、ConcurrentHashMap
HashMap:底层是基于数组+链表,JDK8以后引入了红黑树,当链表大于8的时候,则会转成红黑树,非线程安全的,默认容量是16、允许有空的健和值
Hashtable:基于哈希表实现,线程安全的(加了synchronized),默认容量是11,不允许有null的健和值
hashcode
顶级类Object里面的方法,所有的类都是继承Object,返回是一个int类型的数
根据一定的hash规则(存储地址,字段,长度等),映射成一个数组,即散列值
equals
顶级类Object里面的方法,所有的类都是继承Object,返回是一个boolean类型
根据自定义的匹配规则,用于匹配两个对象是否一样,一般逻辑如下
//判断地址是否一样
//非空判断和Class类型判断
//强转
//对象里面的字段一一匹配
使用场景:对象比较、或者集合容器里面排重、比较、排序
hashCode() 与 equals() 的相关规定:
public class User {
private int age;
private String name;
private Date time;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getTime() {
return time;
}
public void setTime(Date time) {
this.time = time;
}
@Override
public int hashCode() {
//int code = age/name.length()+time.hashCode();
//return code
return Objects.hash(age,name,time);
}
@Override
public boolean equals(Object obj) {
if(this == obj) return true;
if(obj == null || getClass() != obj.getClass()) return false;
User user = (User) obj;
return age == user.age && Objects.equals(name, user.name) && Objects.equals(time, user.time);
}
}
主要原因在于并发下的Rehash 会造成元素之间会形成⼀个循环链表。不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使⽤ HashMap,因为多线程下使⽤ HashMap 还是会存在其他问题⽐如数据丢失。并发环境下推荐使⽤ ConcurrentHashMap 。
hashMap: 散列桶(数组+链表),可以实现快速的存储和检索,但是确实包含无序的元素,适用于在map中插入删除和定位元素
treeMap:使用存储结构是一个平衡二叉树->红黑树,可以自定义排序规则,要实现Comparator接口,能便捷的实现内部元素的各种排序,但是一般性能比HashMap差,适用于安装自然排序或者自定义排序规则(写过微信支付签名工具类就用这个类)
核心就是不保存重复的元素,存储一组唯一的对象
set的每一种实现都是对应Map里面的一种封装,
HashSet对应的就是HashMap,treeSet对应的就是treeMap
使用CopyOnWriteSet解决
按照添加顺序使用LinkedHashMap,按照自然排序使用TreeMap,自定义排序 TreeMap(Comparetor c)
多线程环境下可以用concurrent包下的ConcurrentHashMap, 或者使用Collections.synchronizedMap(),
ConcurrentHashMap虽然是线程安全,但是他的效率比Hashtable要高很多
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的⽅式上不同。
底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采⽤分段的数组+链表 实现,JDK1.8采⽤的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑⼆叉树。 Hashtable 和JDK1.8 之前的 HashMap 的底层数据结构类似都是采⽤ 数组+链表 的形式,数组是HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的;
实现线程安全的⽅式(重要):
① 在 JDK1.7 的时候, ConcurrentHashMap (分段锁)对整个桶数组进⾏了分割分段( Segment ),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,并发控制使⽤ synchronized 和 CAS来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap ,虽然在 JDK1.8 中还能看到Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
② Hashtable (同⼀把锁) :使⽤** synchronized** 来保证线程安全,效率⾮常低下。当⼀个线程访问同步⽅法时,其他线程也访问同步⽅法,可能会进⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不
能使⽤ put 添加元素,也不能使⽤ get,竞争会越来越激烈效率越低。
使用Collections.synchronizedMap包装后返回的map是加锁的
索引计算方法
数组容量为何是 2 的 n 次幂
注意
树化意义
退化规则
HashMap底层(数组+链表+红黑树 jdk8才有红黑树)
数组中每一项是一个链表,即数组和链表的结合体
Node
在JDK1.8中,链表的长度大于8,链表会转换成红黑树
hash碰撞的意思是不同key计算得到的Hash值相同,需要放到同个bucket中
常见的解决办法:链表法、开放地址法、再哈希法,二次寻址法等
HashMap采用的是链表法
数组 Node
链表的作用是解决hash冲突,将hash值一样的对象存在一个链表放在hash值对应的槽位,红黑树 JDK8使用红黑树来替代超过8个节点的链表,主要是查询性能的提升,从原来的O(n)到O(logn),通过hash碰撞,让HashMap不断产生碰撞,那么相同的key的位置的链表就会不断增长,当对这个Hashmap的相应位置进行查询的时候,就会循环遍历这个超级大的链表,性能就会下降,所以改用红黑树
为啥选择红黑树而不用其他树,比如二叉查找树,为啥不一直开始就用红黑树,而是到8的长度后才变换?
二叉查找树在特殊情况下也会变成一条线性结构,和原先的链表存在一样的深度遍历问题,查找性能就会慢,使用红黑树主要是提升查找数据的速度,红黑树是平衡二叉树的一种,插入新数据后会通过左旋,右旋、变色等操作来保持平衡,解决单链表查询深度的问题
数据量少的时候操作数据,遍历线性表比红黑树所消耗的资源少,且前期数据少平衡二叉树保持平衡是需要消耗资源的,所以前期采用线性表,等到一定数之后变换到红黑树
put:
ConcurrentHashMap线程安全的Map, hashtable类基本上所有的方法都是采用synchronized进行线程安全控制高并发情况下效率就降低ConcurrentHashMap是采用了分段锁的思想提高性能,锁粒度更细化
JDK8之前,ConcurrentHashMap使用锁分段技术,将数据分成一段段存储,每个数据段配置一把锁,即segment类,这个类继承ReentrantLock来保证线程安全
技术点:Segment+HashEntry
JKD8的版本:取消Segment这个分段锁数据结构,底层也是使用Node数组+链表+红黑树,从而实现对每一段数据就行加锁,也减少了并发冲突的概率,CAS(读)+Synchronized(写)
技术点:Node+Cas+Synchronized
不允许key或者value为空
spread(key.hashCode()) 二次哈希,减少碰撞概率
tabAt(i) 获取table中索引为i的Node元素
casTabAt(i) 利用CAS操作获取table中索引为i的Node元素
put的核心流程
1、key进行重哈希spread(key.hashCode())
2、对当前table进行无条件循环
3、如果没有初始化table,则用initTable进行初始化
4、如果没有hash冲突,则直接用cas插入新节点,成功后则直接判断是否需要扩容,然后结束
5、(fh = f.hash) == MOVED 如果是这个状态则是扩容操作,先进行扩容
6、存在hash冲突,利用synchronized (f) 加锁保证线程安全
7、如果是链表,则直接遍历插入,如果数量大于8,则需要转换成红黑树
8、如果是红黑树则按照红黑树规则插入
9、最后是检查是否需要扩容addCount()
进程: 本质上是一个独立执行的程序,进程是操作系统进行资源分配和调度的基本概念,操作系统进行资源分配和调度的一个独立单位
在 Java 中,当我们启动 main 函数时其实就是启动了⼀个 JVM 的进程,⽽ main 函数所在的线程就是这个进程中的⼀个线程,也称主线程。
线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每条线程执行不同的任务,切换受系统控制。
协程: 又称为微线程,是一种用户态的轻量级线程,协程不像线程和进程需要进行系统内核上的上下文切换,协程的上下文切换是由用户自己决定的,有自己的上下文,所以说是轻量级的线程,也称之为用户级别的线程就叫协程,一个线程可以多个协程,线程进程都是同步机制,而协程则是异步
Java的原生语法中并没有实现协程,目前python、Lua和GO等语言支持
关系:一个进程可以有多个线程,它允许计算机同时运行两个或多个程序。线程是进程的最小执行单位,CPU的调度切换的是进程和线程,进程和线程多了之后调度会消耗大量的CPU,CPU上真正运行的是线程,线程可以对应多个协程
程序:是含有指令和数据的⽂件,被存储在磁盘或其他的数据存储设备中,也就是说程序是静态的代码。
Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法,它将启动一个新线程,并执行 run()方法。
如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个Runnable 接口。
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}
有返回值的任务必须实现 Callable 接口,类似的,无返回值的任务必须 Runnable 接口。执行Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的 Object 了,再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程
了
//创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 执行任务并获取 Future 对象
Future f = pool.submit(c);
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从 Future 对象上获取任务的返回值,并输出到控制台
System.out.println("res:" + f.get().toString());
}
线程和数据库连接这些资源都是非常宝贵的资源。那么每次需要的时候创建,不需要的时候销毁,是非常浪费资源的。那么我们就可以使用缓存的策略,也就是使用线程池。
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
while(true) {
threadPool.execute(new Runnable() { // 提交多个线程任务,并执行
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running ..");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
} }
Java 里面线程池的顶级接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而
只是一个执行线程的工具。真正的线程池接口是 ExecutorService。
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。==调用 execute 将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。==因此,长时间保持空闲的线程池不会使用任何资源
==创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。==在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3);
scheduledThreadPool.schedule(newRunnable(){
@Override
public void run() {
System.out.println("延迟三秒");
}
}, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){
@Override
public void run() {
System.out.println("延迟 1 秒后每三秒执行一次");
}
},1,3,TimeUnit.SECONDS);
Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去!
两种可能:
优点:
非常快速的上下文切换,不用系统内核的上下文切换,减小开销
单线程即可实现高并发,单核CPU可以支持上万的协程
由于只有一个线程,也不存在同时写变量的冲突,在协程中控制共享资源不需要加锁
缺点:
协程无法利用多核资源,本质也是个单线程
协程需要和进程配合才能运行在多CPU上
目前java没成熟的第三方库,存在风险
调试debug存在难度,不利于发现问题
从计算机底层来说: 线程可以⽐作是轻量级的进程,是程序执⾏的最⼩单位,线程间的切换和调度的成本远远⼩于进程。另外,多核 CPU 时代意味着多个线程可以同时运⾏,这减少了线程上下⽂切换的开销。
从当代互联⽹发展趋势来说: 现在的系统动不动就要求百万级甚⾄千万级的并发量,⽽多线程并发编程正是开发⾼并发系统的基础,利⽤好多线程机制可以⼤⼤提⾼系统整体的并发能⼒以及性能。
再深⼊到计算机底层来探讨:
单核时代: 在单核时代多线程主要是为了提⾼ CPU 和 IO 设备的综合利⽤率。举个例⼦:当只有⼀个线程的时候会导致 CPU 计算时,IO 设备空闲;进⾏ IO 操作时,CPU 空闲。我们可以简单地说这两者的利⽤率⽬前都是 50%左右。但是当有两个线程的时候就不⼀样了,当⼀个线程执⾏ CPU 计算时,另外⼀个线程可以进⾏ IO 操作,这样两个的利⽤率就可以在理想情况下达到 100%了。
多核时代: 多核时代多线程主要是为了提高CPU利用率。举个例⼦:假如我们要计算⼀个复杂的任务,我们只⽤⼀个线程的话,CPU 只会⼀个 CPU 核⼼被利⽤到,⽽创建多个线程就可以让多个 CPU 核⼼被利⽤到,这样就提⾼了 CPU 的利⽤率。
并发编程的⽬的就是为了能提高程序的执行效率和提高程序运行速度,但是并发编程并不总是能提⾼程序运⾏速度的,⽽且并发编程可能会遇到很多问题,⽐如:内存泄漏、上下文切换、死锁
多线程编程中⼀般线程的个数都⼤于 CPU 核心的个数,⽽⼀个 CPU 核⼼在任意时刻只能被⼀个线程使用,为了让这些线程都能得到有效执⾏,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当⼀个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使⽤,这个过程就属于⼀次上下⽂切换。
概括来说就是:当前任务在执⾏完CPU时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下⽂切换。
上下⽂切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒⼏⼗上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下⽂切换对系统来说意味着消耗⼤量的CPU 时间,事实上,可能是操作系统中时间消耗最⼤的操作。
串行:串行表示所有任务都一一按先后顺序进行。串行意味着必须先装完一车柴才能运送这车柴,只有运送到了,才能卸下这车柴,并且只有完成了这整个三个步骤,才能进行下一个步骤。相当于一条流水线执行一组任务一样
并发 concurrency:一台处理器上同时处理任务, 这个同时实际上是交替处理多个任务,程序中可以同时拥有两个或者多个线程,当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,并发指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。要解决大并发问题,通常是将大任务分解成多个小任务
并行 parallellism:多个CPU上同时处理多个任务,一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行
并发指在一段时间内宏观上去处理多个任务。 并行指同一个时刻,多个任务确实真的同时运行。
例子:
并发是一心多用,听课和看电影,但是CPU大脑只有一个,所以轮着来
并行:火影忍者中的影分身,有多个你出现,可以分别做不同的事情
一个项目经理A和3个程序BCD的故事
单线程:A给B讲完,等B做完,给C讲,等C完成,给D讲,等D完成
并发:A给B讲完需求,B自己去实现,期间A继续给C和D讲,不用等待某个程序员去完成,期间项目经理没空闲下来
并行:直接找3个项目经理分别分配给3个程序员
public class ThreadDemo1 extends Thread {
@Override
public void run() {
System.out.println("继承Thread实现多线程,名称:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
ThreadDemo1 threadDemo1 = new ThreadDemo1();
threadDemo1.setName("demo1");
threadDemo1.start();
System.out.println("主线程名称:"+Thread.currentThread().getName());
}
public class ThreadDemo2 implements Runnable {
@Override
public void run() {
System.out.println("通过Runnable实现多线程,名称:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
ThreadDemo2 threadDemo2 = new ThreadDemo2();
Thread thread = new Thread(threadDemo2);
thread.setName("demo2");
thread.start();
System.out.println("主线程名称:"+Thread.currentThread().getName());
}
JDK8之后采用lambda表达式
public static void main(String[] args) {
Thread thread = new Thread(()->{
System.out.println("通过Runnable实现多线程,称:"+Thread.currentThread().getName());
});
thread.setName("demo2");
thread.start();
System.out.println("主线程名称:"+Thread.currentThread().getName());
}
public class MyTask implements Callable<Object> {
@Override
public Object call() throws Exception {
System.out.println("通过Callable实现多线程,名称:"+Thread.currentThread().getName());
return "这是返回值";
}
}
public static void main(String[] args) {
FutureTask<Object> futureTask = new FutureTask<>(()->{
System.out.println("通过Callable实现多线程,名称:"+Thread.currentThread().getName());
return "这是返回值";
});
// MyTask myTask = new MyTask();
// FutureTask
//FutureTask继承了Runnable,可以放在Thread中启动执行
Thread thread = new Thread(futureTask);
thread.setName("demo3");
thread.start();
System.out.println("主线程名称:"+Thread.currentThread().getName());
try {
System.out.println(futureTask.get());
} catch (InterruptedException e) {
//阻塞等待中被中断,则抛出
e.printStackTrace();
} catch (ExecutionException e) {
//执行过程发送异常被抛出
e.printStackTrace();
}
}
public class ThreadDemo4 implements Runnable {
@Override
public void run() {
System.out.println("通过线程池+runnable实现多线程,名称:"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
//指定线程池的大小为3
ExecutorService executorService = Executors.newFixedThreadPool(3);
for(int i=0;i<10;i++){
executorService.execute(new ThreadDemo4());
}
System.out.println("主线程名称:"+Thread.currentThread().getName());
//关闭线程池
executorService.shutdown();
}
线程池中,有四个重要的参数,决定影响了拒绝策略:
corePoolSize - 核心线程数,也即最小的线程数。
workQueue - 阻塞队列 。
maximumPoolSize -最大线程数
拒绝策略
当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直到达到maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池的拒绝策略了。总结起来,也就是一句话,当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略。
池化技术相⽐⼤家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应⽤。池化技术的思想主要是为了减少每次获取资源的消耗,提⾼对资源的利⽤率。
线程池提供了⼀种限制和管理资源(包括执⾏⼀个任务)。 每个线程池还维护⼀些基本统计信息,例如已完成任务的数量。
CallerRunsPolicy: 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
AbortPolicy: 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
DiscardPolicy: 直接丢弃,其他啥都没有
DiscardOldestPolicy: 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列workQueue 中最老的一个任务,并将新任务加入
自定义线程池:常驻线程数量,最大线程数量,过期时间,单位,阻塞队列,线程工厂,拒绝策略
ThreadPoolExecutor(2, 5, 2L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
当调用 execute()方法添加一个请求任务时,线程池会做出如下判断:
2.1 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
2.2 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
2.4 如果队列满了且正在运行的线程数量大于或等与maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
(1)是否有返回值
(2)是否抛出异常
(3)实现方法名称不同,一个是run方法,一个是call方法
JDK的线程状态分6种,JVM里面9种,我们一般说JDK的线程状态
常见的5种状态
创建(NEW): 生成线程对象,但是并没有调用该对象start(), new Thread()
就绪(Runnable):当调用线程对象的start()方法,线程就进入就绪状态,但是此刻线程调度还没把该线程设置为当前线程,就是没获得CPU使用权。 如果线程运行后,从等待或者睡眠中回来之后,也会进入就绪状态
运行(Running):程序将处于就绪状态的线程设置为当前线程,即获得CPU使用权,这个时候线程进入运行状态,开始运行run里面的逻辑
注意:有些文档把就绪和运行两种状态统一称为 “运行中”
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回
阻塞(Blocked)
死亡(TERMINATED):一个线程run方法执行结束,该线程就死亡了,不能进入就绪状态
三个层面
不同点
公平锁
条件变量
两者最主要的区别在于: sleep() ⽅法没有释放锁,⽽ wait() ⽅法释放了锁 。
两者都可以暂停线程的执⾏。
wait() 通常被⽤于线程间交互/通信, sleep() 通常被⽤于暂停执⾏。
线程是否会自动苏醒:wait() ⽅法被调⽤后,线程不会⾃动苏醒,需要别的线程调⽤同⼀个对象上的 notify() 或 者 notifyAll() ⽅法。 sleep() ⽅法执⾏完成后,线程会⾃动苏醒。或者可以使⽤ wait(longtimeout) 超时后线程会⾃动苏醒。
new ⼀个 Thread,线程进⼊了新建状态。调⽤ start() ⽅法,会启动⼀个线程并使线程进⼊了就绪状态,当分配到时间片后就可以开始运⾏了。 start() 会执⾏线程的相应准备⼯作,然后⾃动执⾏ run() ⽅法的内容,这是真正的多线程⼯作。 但是,直接执⾏ run() ⽅法,会把 run()⽅法当成⼀个 main 线程下的普通⽅法去执⾏,并不会在某个线程中执⾏它,所以这并不是多线程⼯作。
总结: 调用 start() 方法方可启动线程并使线程进⼊就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。
execute():只能执行 Runnable 类型的任务。
submit():可以执行 Runnable 和 Callable 类型的任务。
Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
异步任务:用户注册、记录日志
定时任务:定期备份日志、备份数据库
分布式计算:Hadoop处理任务mapreduce,master-wark(单机单进程)
服务器编程:Socket网络编程,一个连接一个线程
atomic 主要利用 CAS (Compare And Wwap) 和 volatile 和 native 方法来保证原子操作,从而避免synchronized 的高开销,执行效率大为提升。
HashMap、ArrayList、LinkedList
类的不同:sleep() 来自 Thread,wait() 来自 Object。
释放锁:sleep() 不释放锁;wait() 释放锁。
用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。
CAS(Compare And Swap/Set)比较并交换,CAS 算法的过程是这样:它包含 3 个参数CAS(V,E,N)。V 表示要更新的变量(内存值),E 表示预期值(旧的),N 表示新值。当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS 返回当前 V 的真实值。
CAS 操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时
使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂
起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,
CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有⾃⼰的专属本地变量该如何解决呢? JDK 中提供的 ThreadLocal 类正是为了解决这样的问题。ThreadLocal 类主要解决的就是让每个线程绑定⾃⼰的值,可以将 ThreadLocal 类形象的⽐喻成存放数据的盒⼦,盒⼦中可以存储每个线程的私有数据。
如果你创建了⼀个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是 ThreadLocal 变量名的由来。他们可以使⽤ get()和 set()⽅法来获取默认值或将其值更改为当前线程所存的副本的值,从⽽避免了线程安全问题。
再举个简单的例⼦:
⽐如有两个⼈去宝屋收集宝物,这两个共⽤⼀个袋⼦的话肯定会产⽣争执,但是给他们两个⼈每个⼈分配⼀个袋⼦的话就不会出现这样的问题。如果把这两个⼈⽐作线程的话,那么ThreadLocal 就是⽤来避免这两个线程竞争的。
ThreadLocal 内部维护的是⼀个类似 Map 的 ThreadLocalMap 数据结构, key 为当前对象的 Thread 对象,值为 Object 对象。
==ThreadLocalMap 中使⽤的 key 为 ThreadLocal 的弱引⽤,⽽ value 是强引⽤。==所以,如果ThreadLocal 没有被外部强引⽤的情况下,==在垃圾回收的时候,key 会被清理掉,⽽ value 不会被清理掉。==这样⼀来, ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远⽆法被 GC 回收,这个时候就可能会产⽣内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调⽤ set() 、 get() 、 remove() ⽅法的时候,会清理掉 key 为 null的记录。使⽤完 ThreadLocal ⽅法后 最好⼿动调⽤ remove() ⽅法
第一:无锁状态,多线程抢夺资源,乱
第二:使用synchronized和ReentrantLock,都是独占的,每次只能来一个操作,读读1,读写1,写写1
第三:读写锁 reentrantReadWriteLock,读读,可共享,提升性能,同时多人进行读操作,写写1
reentrantReadWriteLock缺点(1):造成锁饥饿,一直读,没有写操作(2)读时候,不能进行写操作,只有完成之后才能进行写操作,写操作可以读
原子性
可见性
有序性
volatile是轻量级的synchronized,保证了共享变量的可见性,被volatile关键字修饰的变量,如果值发生了变化,其他线程立刻可见,避免出现脏读现象
volatile:保证可见性,但是不能保证原子性
synchronized:保证可见性,也保证原子性
使用场景
1、不能修饰写入操作依赖当前值的变量,比如num++、num=num+1,不是原子操作,肉眼看起来是,但是JVM字节码层面不止一步
2、由于禁止了指令重排,所以JVM相关的优化没了,效率会偏弱
JAVA内存模型简称 JMM
JMM规定所有的变量存在在主内存,每个线程有自己的工作内存,线程对变量的操作都在工作内存中进行,不能直接对主内存就行操作,使用volatile修饰变量,每次读取前必须从主内存属性获取最新的值,每次写入需要立刻写到主内存中
volatile关键字修修饰的变量随时看到的自己的最新值,假如线程1对变量v进行修改,那么线程2是可以马上看见
JVM在编译java代码或者CPU执行JVM字节码时,对现有的指令进行重新排序,主要目的是优化运行效率(不改变程序结果的前提)
int a = 3 //1
int b = 4 //2
int c =5 //3
int h = abc //4
定义顺序 1,2,3,4
计算顺序 1,3,2,4 和 2,1,3,4 结果都是一样
虽然指令重排序可以提高执行效率,但是多线程上可能会影响结果,有什么解决办法?
解决办法:内存屏障
解释:内存屏障是屏障指令,使CPU对屏障指令之前和之后的内存操作执行结果的一种约束
先行发生原则,volatile的内存可见性就体现了该原则之一
例子:
//线程A操作
int k = 1;
//线程B操作
int j = k;
//线程C操作
int k = 2
分析:
假设线程A中的操作“k=1”先行发生于线程B的操作“j=k”,那确定在线程B的操作执行后,变量j的值一定等于1,依据有两个:一是先行发生原则,“k=1”的结果可以被观察到;二是第三者线程C还没出现,线程A操作结束之后没有其他线程会修改变量k的值。
但是考虑线程C出现了,保持线程A和线程B之间的先行发生关系,线程C出现在线程A和线程B的操作之间,但是线程C与线程B没有先行发生关系,那j的值会是多少?答案是1和2都有可能,因为线程C对变量k的影响可能会被线程B观察到,也可能不会,所以线程B就存在读取到不符合预期数据的风险,不具备多线程安全性
八大原则
1、程序次序规则
2、管程锁定规则
3、volatile变量规则
4、线程启动规则
5、线程中断规则
6、线程终止规则
7、对象终结规则
8、传递性
原子性:一个不可再被分割的颗粒,原子性指的是一个或多个操作要么全部执行成功要么全部执行失败,期间不能被中断,也不存在上下文切换,线程切换会带来原子性的问题
int num = 1; // 原子操作
num++; // 非原子操作,从主内存读取num到线程工作内存,进行 +1,再把num写到主内存, 除非用原子类,即java.util.concurrent.atomic里的原子变量类
解决办法是可以用synchronized 或 Lock(比如ReentrantLock) 来把这个多步操作“变成”原子操作,但是volatile,前面有说到不能修饰有依赖值的情况
public class XdTest {
private int num = 0;
//使用lock,每个对象都是有锁,只有获得这个锁才可以进行对应的操作
Lock lock = new ReentrantLock();
public void add1(){
lock.lock();
try {
num++;
}finally {
lock.unlock();
}
}
//使用synchronized,和上述是一个操作,这个是保证方法被锁住而已,上述的是代码块被锁住
public synchronized void add2(){
num++;
}
}
解决核心思想:把一个方法或者代码块看做一个整体,保证是一个不可分割的整体
有序性: 程序执行的顺序按照代码的先后顺序执行,因为处理器可能会对指令进行重排序,JVM在编译java代码或者CPU执行JVM字节码时,对现有的指令进行重新排序,主要目的是优化运行效率(不改变程序结果的前提)(volatile禁止了指令重排)
int a = 3 //1
int b = 4 //2
int c =5 //3
int h = abc //4
上面的例子 执行顺序1,2,3,4 和 2,1,3,4 结果都是一样,指令重排序可以提高执行效率,但是多线程上可能会影响结果
假如下面的场景,正常是顺序处理
//线程1
before();//处理初始化工作,处理完成后才可以正式运行下面的run方法
flag = true; //标记资源处理好了,如果资源没处理好,此时程序就可能出现问题
//线程2
while(flag){
run(); //核心业务代码
}
指令重排序后,导致顺序换了,程序出现问题,且难排查
//线程1
flag = true; //标记资源处理好了,如果资源没处理好,此时程序就可能出现问题
//线程2
while(flag){
run(); //核心业务代码
}
before();//处理初始化工作,处理完成后才可以正式运行下面的run方法
可见性: 一个线程A对共享变量的修改,另一个线程B能够立刻看到
// 线程 A 执行
int num = 0;
// 线程 A 执行
num++;
// 线程 B 执行
System.out.print("num的值:" + num);
线程A执行 i++ 后再执行线程 B,线程 B可能有2个结果,可能是0和1。
因为 i++ 在线程A中执行运算,并没有立刻更新到主内存当中,而线程B就去主内存当中读取并打印,此时打印的就是0;也可能线程A执行完成更新到主内存了,线程B的值是1。
所以需要保证线程的可见性
synchronized、lock和volatile能够保证线程可见性
先来先服务调度算法:
按照作业/进程到达的先后顺序进行调度 ,即:优先考虑在系统中等待时间最长的作业
排在长进程后的短进程的等待时间长,不利于短作业/进程
短作业优先调度算法:
短进程/作业(要求服务时间最短)在实际情况中占有很大比例,为了使得它们优先执行
对长作业不友好
高响应比优先调度算法:
在每次调度时,先计算各个作业的优先权:优先权=响应比=(等待时间+要求服务时间)/要求服务时间,
因为等待时间与服务时间之和就是系统对该作业的响应时间,所以 优先权=响应比=响应时间/要求服务时间,选择优先权高的进行服务需要计算优先权信息,增加了系统的开销
时间片轮转调度算法:
轮流的为各个进程服务,让每个进程在一定时间间隔内都可以得到响应
由于高频率的进程切换,会增加了开销,且不区分任务的紧急程度
优先级调度算法:
根据任务的紧急程度进行调度,高优先级的先处理,低优先级的慢处理
如果高优先级任务很多且持续产生,那低优先级的就可能很慢才被处理
线程调度器选择优先级最高的线程运行,但是,如果发生一下情况就回终止线程的运行:
线程调度是指系统为线程分配CPU使用权的过程,主要分两种
协同式线程调度(分时调度模式):线程执行时间由线程本身来控制,==线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。==最大好处是实现简单,且切换操作对线程自己是可知的,没啥线程同步问题。坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里
抢占式线程调度:每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞
Java线程调度就是抢占式调度,优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那就随机选择一个线程
所以我们如果希望某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。
JAVA的线程的优先级,以1到10的整数指定。当多个线程可以运行时,VM一般会运行最高优先级的线程(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY)
在两线程同时处于就绪runnable状态时,优先级越高的线程越容易被系统选择执行。但是优先级并不是100%可以获得,只不过是机会更大而已。
有人会说,wait,notify不就是线程本身控制吗?
其实不是,wait是可以让出执行时间,notify后无法获取执行时间,随机等待队列里面获取而已
悲观锁:当线程去操作数据的时候,总认为别的线程会去修改数据,所以它每次拿数据的时候都会上锁,别的线程去拿数据的时候就会阻塞,比如synchronized
乐观锁:每次去拿数据的时候都认为别人不会修改,更新的时候会判断是别人是否会去更新数据,通过版本号来判断,如果数据被修改了就拒绝更新,在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作,比如CAS是乐观锁,但严格来说并不是锁,通过原子性来保证数据的同步,比如说数据库的乐观锁,通过版本控制来实现,CAS不会保证线程同步,乐观的认为在数据更新期间没有其他线程影响
java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败
小结:悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,乐观锁的吞吐量会比悲观锁多
公平锁:指多个线程按照申请锁的顺序来获取锁,简单来说 如果一个线程组里,能保证每个线程都能拿到锁 比如ReentrantLock(底层是同步队列FIFO:First Input First Output来实现)
非公平锁:获取锁的方式是随机获取的,保证不了每个线程都能拿到锁,也就是存在有线程饿死,一直拿不到锁,比如synchronized、ReentrantLock :在构造函数中提供了是否公平锁的初始化方式,默认为非公平锁。非公平锁实际执行的效率要远远超出公平锁,除非程序有特殊需要,否则最常用非公平锁的分配机制。
小结:
非公平锁:效率高因为能重复利用CPU的时间,不过可能存在线程饿死
公平锁:效率相对较低
可重入锁:也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,synchronized(隐式)和lock(显式)
synchronized:
Object o = new Object();
new Thread(()->{
synchronized (o){
System.out.println(Thread.currentThread().getName()+"外层");
synchronized (o){
System.out.println(Thread.currentThread().getName()+"中层");
synchronized (o){
System.out.println(Thread.currentThread().getName()+"内层");
}
}
}
},"T1").start();
lock:
Lock lock = new ReentrantLock();
new Thread(()->{
lock.lock();
System.out.println("aaaa");
lock.unlock();
},"aa").start();
new Thread(()->{
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"外层");
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"中层");
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+"内层");
}finally {
lock.unlock();
}
}finally {
lock.unlock();
}
}finally {
lock.unlock();
}
},"T1").start();
不可重入锁:若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞
小结:可重入锁能一定程度的避免死锁 synchronized、ReentrantLock 重入锁
private void meathA(){
//获取锁 TODO
meathB();
}
private void meathB(){
//获取锁 TODO
//其他操作
}
分段锁、行锁、表锁
自旋锁:一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有一个执行单元获得锁.
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
小结:不会发生线程状态的切换,一直处于用户态,减少了线程上下文切换的消耗,缺点是循环会消耗CPU
常见的自旋锁:TicketLock,CLHLock,MSCLock
共享锁:也叫S锁/读锁,能查看但无法修改和删除的一种数据锁,加锁后其它用户可以并发读取、查询数据,但不能修改,增加,删除数据,该锁可被多个线程所持有,用于资源数据共享
互斥锁:也叫X锁/排它锁/写锁/独占锁/独享锁/ 该锁每一次只能被一个线程所持有,加锁后任何线程试图再次加锁的线程会被阻塞,直到当前线程解锁。例子:如果 线程A 对 data1 加上排他锁后,则其他线程不能再对 data1 加任何类型的锁,获得互斥锁的线程即能读数据又能修改数据
Java 中读写锁有个接口 java.util.concurrent.locks.ReadWriteLock ,也有具体的实现
ReentrantReadWriteLock。
分段锁:分段锁也并非一种实际的锁,而是一种思想 ConcurrentHashMap 是学习分段锁的最好实践
下面三种是Jvm为了提高锁的获取与释放效率而做的优化针对Synchronized的锁升级,锁的状态是通过对象监视器在对象头中的字段来表明,是不可逆的过程,
偏向锁:一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,获取锁的代价更低
轻量级锁:当锁是偏向锁的时候,被其他线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,但不会阻塞,且性能会高点
重量级锁:当锁为轻量级锁的时候,其他线程虽然是自旋,但自旋不会一直循环下去,当自旋一定次数的时候且还没有获取到锁,就会进入阻塞,该锁升级为重量级锁,重量级锁会让其他申请的线程进入阻塞,性能也会降低
两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法让程序进行下去
死锁代码:
new Thread(()->{
synchronized (a){
System.out.println(Thread.currentThread().getName()+"持有a,试图获取b");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println(Thread.currentThread().getName()+"获取b");
}
}
},"a").start();
new Thread(()->{
synchronized (b){
System.out.println(Thread.currentThread().getName()+"持有b,试图获取a");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (a){
System.out.println(Thread.currentThread().getName()+"获a");
}
}
},"b").start();
jps
jstack 30288
互斥条件:资源不能共享,只能由一个线程使用
请求与保持条件:线程已经获得一些资源,但因请求其他资源发生阻塞,对已经获得的资源保持不释放
不可抢占:有些资源是不可强占的,当某个线程获得这个资源后,系统不能强行回收,只能由线程使用完自己释放
循环等待条件:多个线程形成环形链,每个都占用对方申请的下个资源
只要发生死锁,上面的条件都成立;只要一个不满足,就不会发生死锁
synchronized 它可以把任意一个非 NULL 的对象当作锁。他属于独占式的悲观锁,同时属于可重入锁。
synchronized是解决线程安全的问题,常用在 同步普通方法、静态方法、代码块中
是非公平锁和可重入锁
每个对象有一个锁和一个等待队列,锁只能被一个线程持有,其他需要锁的线程需要阻塞等待。锁被释放后,对象会从队列中取出一个并唤醒,唤醒哪个线程是不确定的,不保证公平性
两种形式:
方法:生成的字节码文件中会多一个 ACC_SYNCHRONIZED 标志位,当一个线程访问方法时,会去检查是否存在ACC_SYNCHRONIZED标识,如果存在,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象,也叫隐式同步
代码块:加了 synchronized 关键字的代码段,生成的字节码文件会多出 monitorenter 和 monitorexit 两条指令,每个monitor维护着一个记录着拥有次数的计数器, 未被拥有的monitor的该计数器为0,当一个线程获执行monitorenter后,该计数器自增1;当同一个线程执行monitorexit指令的时候,计数器再自减1。当计数器为0的时候,monitor将被释放.也叫显式同步
两种本质上没有区别,底层都是通过monitor来实现同步, 只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成
jdk1.6后进行了优化,你知道哪些大的变化
有得到锁的资源进入Block状态,涉及到操作系统用户模式和内核模式的切换,代价比较高
jdk6进行了优化,增加了从偏向锁到轻量级锁再到重量级锁的过渡,但是在最终转变为重量级锁之后,性能仍然较低
synchronized 关键字最主要的三种使⽤⽅式:
1.修饰实例⽅法: 作⽤于当前对象实例加锁,进⼊同步代码前要获得 当前对象实例的锁
synchronized void method() {
//业务代码
}
作用于方法时,锁住的是对象的实例(this);
2.修饰静态⽅法: 也就是给当前类加锁,会作⽤于类的所有对象实例 ,进⼊同步代码前要获得 当前 class 的锁。因为静态成员不属于任何⼀个实例对象,是类成员( static 表明这是该类的⼀个静态资源,不管 new 了多少个对象,只有⼀份)。所以,如果⼀个线程 A 调⽤⼀个实例对象的⾮静态 synchronized ⽅法,⽽线程 B 需要调⽤这个实例对象所属类的静态 synchronized ⽅法,是允许的,不会发⽣互斥现象,因为访问静态 synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁。
synchronized void staic method() {
//业务代码
}
当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8 则是 metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
3.修饰代码块 :指定加锁对象,对给定对象/类加锁。 synchronized(this|object) 表示进⼊同步代码库前要获得给定对象的锁。 synchronized( .class) 表示进⼊同步代码前要获得 当前 class 的锁
synchronized(this) {
//业务代码
}
总结:
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进⼊加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
另外,需要注意 uniqueInstance 采⽤ volatile 关键字修饰也是很有必要。uniqueInstance 采⽤ volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执⾏:
构造⽅法本身就属于线程安全的,不存在同步的构造⽅法⼀说。
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。
当执⾏ monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。
在执⾏ monitorenter 时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
在执⾏ monitorexit 指令后,将锁计数器设为 0,表明锁被释放。如果获取对象锁失败,那当前
线程就要阻塞等待,直到锁被另外⼀个线程释放为⽌。
synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法。JVM 通过该ACC_SYNCHRONIZED 访问标志来辨别⼀个⽅法是否声明为同步⽅法,从⽽执⾏相应的同步调
⽤。
总结:
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中monitorenter 指令指向同步代码块的开始位置, monitorexit 指令则指明同步代码块的结束位置。
synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法。
不过两者的本质都是对对象监视器 monitor 的获取。
ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等
避免多线程死锁的方法。
Semaphore 是一种基于计数的信号量。==它可以设定一个阈值,多个线程竞争获取许可信号,做完自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞。===Semaphore 可以用来构建一些对象池,资源池之类的,比如数据库连接池
实现互斥锁(计数器为 1)
我们也可以创建计数为 1 的 Semaphore,将其作为一种类似互斥锁的机制,这也叫二元信号量,
表示两种互斥状态。
问题:有两个线程,A 线程向一个集合里面依次添加元素“abc”字符串,一共添加十次,当添加到第五次的时候,希望 B 线程能够收到 A 线程的通知,然后 B 线程执行相关的业务操作。线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。
public class TestSync {
//定义共享变量来实现通信,它需要volatile修饰,否则线程不能及时感知
static volatile boolean notice = false;
public static void main(String[] args) {
List<String> list = new ArrayList<>();
//线程A
Thread threadA = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A添加元素,此时list的size为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
notice = true;
}
});
//线程B
Thread threadB = new Thread(() -> {
while (true) {
if (notice) {
System.out.println("线程B收到通知,开始执行自己的业务...");
break;
}
}
});
//需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再启动线程A
threadA.start();
}
}
public class TestSync {
public static void main(String[] args) {
//定义一个锁对象
Object lock = new Object();
List<String> list = new ArrayList<>();
// 线程A
Thread threadA = new Thread(() -> {
synchronized (lock) {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A添加元素,此时list的size为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
lock.notify();//唤醒B线程
}
}
});
//线程B
Thread threadB = new Thread(() -> {
while (true) {
synchronized (lock) {
if (list.size() != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程B收到通知,开始执行自己的业务...");
}
}
});
//需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//再启动线程A
threadA.start();
}
}
由输出结果,在线程 A 发出 notify() 唤醒通知之后,依然是走完了自己线程的业务之后,线程 B 才开始执行,正好说明 notify() 不释放锁,而 wait() 释放锁。
public class TestSync {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(1);
List<String> list = new ArrayList<>();
//线程A
Thread threadA = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A添加元素,此时list的size为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
countDownLatch.countDown();
}
});
//线程B
Thread threadB = new Thread(() -> {
while (true) {
if (list.size() != 5) {
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程B收到通知,开始执行自己的业务...");
break;
}
});
//需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//再启动线程A
threadA.start();
}
}
public class TestSync {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
List<String> list = new ArrayList<>();
//线程A
Thread threadA = new Thread(() -> {
lock.lock();
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A添加元素,此时list的size为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
condition.signal();
}
lock.unlock();
});
//线程B
Thread threadB = new Thread(() -> {
lock.lock();
if (list.size() != 5) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程B收到通知,开始执行自己的业务...");
lock.unlock();
});
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadA.start();
}
}
这种方式使用起来并不是很好,代码编写复杂,而且线程 B 在被 A 唤醒之后由于没有获取锁还是不能立即执行,也就是说,A 在唤醒操作之后,并不释放锁。这种方法跟 Object 的 wait()/notify() 一样。
public class TestSync {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
//线程B
final Thread threadB = new Thread(() -> {
if (list.size() != 5) {
LockSupport.park();
}
System.out.println("线程B收到通知,开始执行自己的业务...");
});
//线程A
Thread threadA = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A添加元素,此时list的size为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
LockSupport.unpark(threadB);
}
});
threadA.start();
threadB.start();
}
}
组建的作用:首先通过类加载器(ClassLoader)会把Java代码转换成字节码,运行时数据区(Runtime Data Area)再把字节码加载到内存,而字节码文件只是JVM的一套指令集规范,不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由CPU去执行,而这个过程中需要调用其他语言的本地库接口(Natic Interface)来实现这个程序的功能
不同虚拟机的运行时数据区可能略有不同,但都会遵循java虚拟机规范,java虚拟机规范规定的区域分为以下5个部分:
JDK自带了很多监控工具,都位于JDK的bin目录下,其中最常用的是jconsole和jvisualvm这两款视图监控工具
实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器
主要有以下四种类加载器:
当一个类收到了类加载请求,不会自己先去加载这个类,而是将其委派给父类,有父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载
编译 》加载 》验证 》准备 》解析 》初始化
编译:将java代码编译为字节码文件
加载:查找并通过io读入字节码文件,在内存中生出一个代表类的class对象,作为访问方法区的输入入口,使用到类的时候才会加载
验证:字节码的校验,是否正确
准备:给类的静态变量分配内存,并赋予默认值
解析:将符号引用替换为直接引用,静态链接过程
初始化:对类的静态遍历,初始化指定值,执行静态代码块
GC是垃圾收集的意思(GabageCollection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至奔溃,java提供的gc功能可以自动检测对象是否超过作用域从而达到自动回收内存的目的,java语言没有提供释放已分配内存的显示操作方法
在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自动执行,在jvm中,有一个垃圾回收线程,它是低优先级的在正常情况下不会指定的,只有一个虚拟机空闲或者当前堆内存不足才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到回收的集合中,进行回收
对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址,大小以及使用情况,通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象,通过这种方式确定那些对象是可达的那些对象是不可达的,当gc确定一些对象为不可达的时候,gc就有责任回收这些内存空间,程序员可以手动执行System,gc(),通知gc运行,但是java语言规范并不保证gc一定会执行
这两个方法用来提示JVM要进行垃圾回收,但是立即开始还是延迟进行垃圾回收是取决于JVM的
不会,在下一个垃圾回收周期中,这个对象是可被回收的
DGC叫做分布式垃圾回收,rmi使用dgc来做自动垃圾回收,因为rmi包含了跨虚拟机的远程对象的引用,垃圾回收是很困难的,DGC使用引用技术算法来给远程对象提供自动内存管理
吞吐量版本使用并行版本的新生代收集器,它用于中等规模和大规模数据的应用程序,而串行收集器对大对数的小应用(在现在处理器上需要大概100M左右的内存)就足够了
当对象对当前使用这个兑现的应用程序变得不可触及的时候,这个对象就可以被回收了
对象优先在堆的Eden区分配
大对象直接进入老年代
长期存活的对象直接进入老年代
当Eden区没有足够的空间进行分配时,虚拟机会执行一次MinorGC,MinorGC通常发生在新生代的Eden区,在这个区的对象生存周期短,往往GC的频率较高,回收速度比较快,FullGC/MajorGC发生在老年代,一般情况下,触发老年代GC的时候不会触发MinorGC,但是可以通过配置,可以在FullGC之前进行一次MinorGC这样可以加快老年代的回收速度
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(FullGC)
注:java8中已经移除了永久代,新佳乐一个叫做元数据区的native内存区
功能方面:堆是用来存放对象的,栈是用来执行程序的
共享性:堆是线程共享的,栈是线程私有的
空间大小:堆大小远远大于栈
队列和栈都是用来预存储数据的
队列雨荨先进先出检索元素,但也有例外的情况,Deque接口云讯从两端检索元素
栈和队列很相似,但是它运行对元素进行先进后出的检索
分代回收期有两个分区:
所谓内存泄漏就是指一个不再被程序使用的对象或变量一直被占据在内存中,java中有垃圾回收机制,它可以保证对象不再被引用的时候,即对象变成孤儿的时候,对象将自动被垃圾回收器从内存中清楚点
由于java使用有向图的方式进行垃圾回收,可以消除引用循环依赖的问题,例如有两个对象,相互引用,只要他们和进程不可达的,那么gc也是可以回收它们的
简单来说就是复制,克隆
Person p = new Person(“张三”)
浅拷贝就是对对象中的数据成员进行简单复制,如果存在动态成员或者指针就会报错,深拷贝就是对对象中存在的动态成员或指针重新开辟内存空间
浅克隆:当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
深克隆:除了对象本身被复制外,对象所包含的所有成员变量也将复制。
-Xms 最小堆内存(包括新生代和老年代)
-Xmx 最大对内存(包括新生代和老年代)
通常建议将 -Xms 与 -Xmx 设置为大小相等,即不需要保留内存,不需要从小到大增长,这样性能较好
-XX:NewSize 与 -XX:MaxNewSize 设置新生代的最小与最大值,但一般不建议设置,由 JVM 自己控制
-Xmn 设置新生代大小,相当于同时设置了 -XX:NewSize 与 -XX:MaxNewSize 并且取值相等
保留是指,一开始不会占用那么多内存,随着使用内存越来越多,会逐步使用这部分保留内存。下同
-XX:NewRatio=2:1 表示老年代占两份,新生代占一份
-XX:SurvivorRatio=4:1 表示新生代分成六份,伊甸园占四份,from 和 to 各占一份
class space 存储类的基本信息,最大值受 -XX:CompressedClassSpaceSize 控制
non-class space 存储除类的基本信息以外的其它信息(如方法字节码、注解等)
class space 和 non-class space 总大小受 -XX:MaxMetaspaceSize 控制
注意:
这里 -XX:CompressedClassSpaceSize 这段空间还与是否开启了指针压缩有关,这里暂不深入展开,可以简单认为指针压缩默认开启
如果 -XX:ReservedCodeCacheSize < 240m,所有优化机器代码不加区分存在一起
否则,分成三个区域
解释:
要点:
特点:
特点:
GC 的目的在于实现无用对象内存自动释放,减少内存碎片、加快分配速度
GC 要点:
伊甸园 eden,最初对象都分配到这里,与幸存区 survivor(分成 from 和 to)合称新生代,
当伊甸园内存不足,标记伊甸园与 from(现阶段没有)的存活对象
将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放
将 from 和 to 交换位置
经过一段时间后伊甸园的内存又出现不足
标记伊甸园与 from(现阶段没有)的存活对象
将存活对象采用复制算法复制到 to 中
复制完毕后,伊甸园和 from 内存都得到释放
将 from 和 to 交换位置
老年代 old,当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会导致提前晋升)
垃圾回收器 - Parallel GC
垃圾回收器 - ConcurrentMarkSweep GC
垃圾回收器 - G1 GC
Minor GC 发生在新生代的垃圾回收,暂停时间短
Mixed GC 新生代 + 老年代部分区域的垃圾回收,G1 收集器特有
Full GC 新生代 + 老年代完整垃圾回收,暂停时间长,应尽力避免
即用三种颜色记录对象的标记状态
该对象的引用已经处理完成,用黑色表示,黑色引用的对象变为灰色
依次类推
沿着引用链都标记了一遍
最后为标记的白色对象,即为垃圾
比较先进的垃圾回收器都支持并发标记,即在标记过程中,用户线程仍然能工作。但这样带来一个新的问题,如果用户线程修改了对象引用,那么就存在漏标问题。例如:
如图所示标记工作尚未完成
用户线程同时在工作,断开了第一层 3、4 两个对象之间的引用,这时对于正在处理 3 号对象的垃圾回收线程来讲,它会将 4 号对象当做是白色垃圾
但如果其他用户线程又建立了 2、4 两个对象的引用,这时因为 2 号对象是黑色已处理对象了,因此垃圾回收线程不会察觉到这个引用关系的变化,从而产生了漏标
如果用户线程让黑色对象引用了一个新增对象,一样会存在漏标问题
因此对于并发标记而言,必须解决漏标问题,也就是要记录标记过程中的变化。有两种解决方法:
误用线程池导致的内存溢出
查询数据量太大导致的内存溢出
动态生成类导致的内存溢出
类加载过程的三个阶段
方法,在初始化时被调用JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收,堆 和 一个存储方法域。JVM 是运行在操作系统之上的,它与硬件没有直接的交互。
说明
会发生内存溢出的区域
方法区、永久代、元空间
JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA 堆、方法区】、直接内存。
一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。字节码解释器⼯作时通过改变这个计数器的值来选取下⼀条需要执⾏的字节码指令,分⽀、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
正在执行 java 方法的话,计数器记录的是虚拟机字节码指令的地址(当前指令的地址)。如果还是 Native 方法,则为空。
这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。
是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。
由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。
永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区。
⽅法区是 Java 虚拟机规范中的定义,是⼀种规范,而永久代是⼀种实现,⼀个是标准⼀个是实现
Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代。
由于频繁创建对象,所以新生代会频繁触发MinorGC 进行垃圾回收。新生代又分为 Eden 区(伊甸园区)、ServivorFrom、ServivorTo 三个区。
Java 新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden 区内存不够的时候就会触发MinorGC,对新生代区进行一次垃圾回收。
上一次 GC 的幸存者,作为这一次 GC 的被扫描者。
保留了一次 MinorGC 过程中的幸存者。
MinorGC 采用复制算法。
谁空谁是TO
主要存放应用程序中生命周期长的内存对象。
老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。
MajorGC 采用标记清除算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。
MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。
大多数情况下是不需要的,Java提供了一个系统级的线程来跟踪内存分配,不再使用的内存区将会自动回收
方法区用于存储被虚拟机加载的类型信息,常量,静态变量,集市编译器编译后的代码缓存等数据
JDK8之前使用永生代实现方法区,容易内存溢出,因为永生代有上线,集市不设置也有默认大小,JDK7把永生代的字符串常量池,静态变量等移除,JDK8中永生代完全废弃,改用在本地内存中实现的元空间代替,把JDK7中永久代剩余内容(主要是类型信息)全部移到元空间
虚拟机贵方对方法区的约束宽松,除和堆一样不需要连续内存和可选择固定大小/可扩展外,还可以不实现垃圾回收,垃圾回收在方法区出现较少,主要目标针对常量池和类型卸载,如果方法区无法满足新的内存分配需求,将抛出OOM
指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被放入永久区域,它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。
在 Java8 中,永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间的本质和永久代类似,元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。
通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。
要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
在 Java 中最常见的就是强引用,把==一个对象赋给一个引用变量,这个引用变量就是一个强引用。==当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
相当于必不可少对的生活物品
软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
相当于可有可无的生活物品
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
相当于可有可无的生活物品
虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化。
启动类加载器(Bootstrap ClassLoader)
当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。
301:永久重定向。
302:暂时重定向。
它们的区别是,301 对搜索引擎优化(SEO)更加有利;302 有被提示为网络拦截的风险。
forward 是转发 和 redirect 是重定向:
地址栏 url 显示:foward url 不会发生改变,redirect url 会发生改变;
数据共享:forward 可以共享 request 里的数据,redirect 不能共享;
效率:forward 比 redirect 效率高。
tcp 和 udp 是 OSI 模型中的运输层中的协议。tcp 提供可靠的通信传输,而 udp 则常被用于让广播和细节控制交给应用的通信传输。
两者的区别大致如下:
tcp 面向连接,udp 面向非连接即发送数据前不需要建立链接;
tcp 提供可靠的服务(数据传输),udp 无法保证;
tcp 面向字节流,udp 面向报文;
tcp 数据传输慢,udp 数据传输快;
如果采用两次握手,那么只要服务器发出确认数据包就会建立连接,但由于客户端此时并未响应服务器端的请求,那此时服务器端就会一直在等待客户端,这样服务器端就白白浪费了一定的资源。若采用三次握手,服务器端没有收到来自客户端的再此确认,则就会知道客户端并没有要求建立请求,就不会浪费服务器的资源。
tcp 粘包可能发生在发送端或者接收端,分别来看两端各种产生粘包的原因:
发送端粘包:发送端需要等缓冲区满才发送出去,造成粘包;
接收方粘包:接收方不及时接收缓冲区的包,造成多个包接收。
物理层:传输比特流,比特,网卡工作层
数据链路层:如何格式化数据以进行传输,差错检测,保证数据传输的可靠性,帧,交换机
网络层:路由器,数据包
传输层:数据间传输,TCP和UDP,分段
会话层:
表示层:
应用层:
http1.0定义了三种:
GET: 向服务器获取资源,比如常见的查询请求
POST: 向服务器提交数据而发送的请求
Head: 和get类似,返回的响应中没有具体的内容,用于获取报头
http1.1定义了六种
PUT:一般是用于更新请求,比如更新个人信息、商品信息全量更新
PATCH:PUT 方法的补充,更新指定资源的部分数据
DELETE:用于删除指定的资源
OPTIONS: 获取服务器支持的HTTP请求方法,服务器性能、跨域检查等
CONNECT: 方法的作用就是把服务器作为跳板,让服务器代替用户去访问其它网页,之后把数据原原本本的返回给用户,网页开发基本不用这个方法,如果是http代理就会使用这个,让服务器代理用户去访问其他网页,类似中介
TRACE:回显服务器收到的请求,主要用于测试或诊断
浏览器向服务器请求时,服务端响应的消息头里面有状态码,表示请求结果的状态
分类
1XX: 收到请求,需要请求者继续执行操作,比较少用
2XX: 请求成功,常用的 200
3XX: 重定向,浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址,这个地址可以从响应的Location首部中获取;
好处:网站改版、域名迁移等,多个域名指向同个主站导流
必须记住: 301:永久性跳转,比如域名过期,换个域名 302:临时性跳转
4XX: 客服端出错,请求包含语法错误或者无法完成请求
必须记住:
400: 请求出错,比如语法协议
403: 没权限访问
404: 找不到这个路径对应的接口或者文件
405: 不允许此方法进行提交,Method not allowed,比如接口一定要POST方式,而你是用了GET
5XX: 服务端出错,服务器在处理请求的过程中发生了错误
必须记住:
500: 服务器内部报错了,完成不了这次请求
503: 服务器宕机
说下Cookie和Session的区别和联系
cookie数据保存在客户端,session数据保存在服务端
cookie不是很安全,容易泄露,不能直接明文存储信息
Cookie大小和数量存储有限制
你们公司C端业务登录的是怎样做的(业务量大,集群部署)
部分业务是采用redis替代本身的tomcat单机session (业务需要高度可控)
还有其他业务是使用JSON Web token (C端普通业务)
1、浏览器输入url, 解析url地址是否合法
2、浏览器检查是否有缓存, 如果有直接显示。如果没有跳到第三步。
3、在发送http请求前,需要域名解析(DNS解析),解析获取对应过的ip地址。
4、浏览器向服务器发起tcp链接,完成tcp三次握手
5、握手成功后,浏览器向服务器发送http请求
6、服务器收到处理的请求,将数据返回至浏览器
7、浏览器收到http响应。
8、浏览器解析响应。如果响应可以缓存,则存入缓存
9、浏览器进行页面渲染
输入url,解析url是否合法,查找浏览器是否有缓存,有的话直接进行显示,没有的话,进行dns域名解析,拿到ip地址,然后发起tcp连接,三次握手,服务器对请求作出响应,发回数据到浏览器,浏览器收集癖到jttp响应,进行解析和进行数据渲染。
同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。
由Netscape提出的一个著名的安全策略。
当一个浏览器的两个tab页中分别打开来 百度和谷歌的页面
当浏览器的百度tab页执行一个脚本的时候会检查这个脚本是属于哪个页面的,
即检查是否同源,只有和百度同源的脚本才会被执行。
如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。
同源策略是浏览器的行为,是为了保护本地数据不被JavaScript代码获取回来的数据污染,因此拦截的是客户端发
出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被浏览器接收
为什么会出现跨域,有什么常见的解决方案
跨域:浏览器同源策略 1995年,同源政策由 Netscape 公司引入浏览器。目前,所有浏览器都实行这个政策。 最初,它的含义是指,A网页设置的 Cookie,B网页不能打开,除非这两个网页"同源"。所谓"同源"指的是"三个相同"
协议相同 http https
域名相同 www.baidu.com
端口相同 80 81
一句话:浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域
浏览器控制台跨域提示:
No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘null’ is therefore not allowed access.
解决方法
程序代码中处理 SpringBoot 通过拦截器配置
//表示接受任意域名的请求,也可以指定域名
response.setHeader(“Access-Control-Allow-Origin”, request.getHeader(“origin”));
//该字段可选,是个布尔值,表示是否可以携带cookie
response.setHeader(“Access-Control-Allow-Credentials”, “true”);
response.setHeader(“Access-Control-Allow-Methods”, “GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS”);
response.setHeader(“Access-Control-Allow-Headers”, “*”);
第一范式:列都是不可再分的
第二范式:每个表只描述一件事情
第三范式:不存在对非主键列的传递依赖
原子性Atomicity: 一个事务必须被事务不可分割的最小工作单元,整个操作要么全部成功,要么全部失败,一般就是通过commit和rollback来控制
一致性Consistency: 数据库总能从一个一致性的状态转换到另一个一致性的状态,只要有任何一方发生异常就不会成功提交事务,比如下单支付成功后,开通视频播放权限
隔离性Isolation: 一个事务相对于另一个事务是隔离的,一个事务所做的修改是在最终提交以前,对其他事务是不可见的
持久性Durability:==一旦事务提交,则其所做的修改就会永久保存到数据库中。==此时即使系统崩溃,修改的数据也不会丢失
脏读: 事务中的修改即使没有提交,其他事务也能看见,事务可以读到未提交的数据称为脏读
不可重复读: 同个事务前后多次读取,不能读到相同的数据内容,中间另一个事务也操作了该同一数据
幻读:当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,发现两次不一样,产生幻读
幻读和不可重复读的区别是:幻读是一个范围,不可重复读是本身,从总的结果来看, 两者都表现为两次读取的结果不一致
事务的隔离级别越高,事务越安全,但是并发能力越差。
Read Uncommitted(未提交读,读取未提交内容):事务中的修改即使没有提交,其他事务也能看见,事务可以读到为提交的数据称为脏读,也存在不可重复读、幻读问题
例子:一个活动,原价500元的课程,配置成50元,但是事务没提交。你刚好看到那么便宜准备购买,但是马上回滚了事务,重新配置并提交了事务,你准备下单的时候发现价格变回了500元
Read Committed(提交读,读取提交内容):一个事务开始后只能看见已经提交的事务所做的修改,在事务中执行两次同样的查询可能得到不一样的结果,也叫做不可重复读(前后多次读取,不能读到相同的数据内容),也存幻读问题
例子:你有1000积分,准备去兑换《面试专题课程》,查询数据库确实有1000积分,但是女友同时也在别的地方登录,把1000积分兑换了《SpringCloud微服务专题课程》,且在你之前提交事务;当系统帮你兑换《面试专题课程》是发现积分预计没了,兑换失败。
事务A事先读取了数据,事务B紧接了更新了数据且提交了事务,事务A再次读取该数据时,数据已经发生了改变
Repeatable Read(可重复读,mysql默认的事务隔离级别):解决脏读、不可重复读的问题,存在幻读的问题,使用 MMVC机制 实现可重复读
例子:有1000积分,准备去兑换《面试专题课程》,查询数据库确实有1000积分,女友同时也在别的地方登录先兑换了这个《面试专题课程》,事务提交的时候发现存在了,之前读取的没用了,像是幻觉
幻读问题:MySQL的InnoDB引擎通过MVCC自动帮我们解决,即多版本并发控制
Serializable(可串行化):解决脏读、不可重复读、幻读,可保证事务安全,但强制所有事务串行执行,所以并发效率低
常见的有:InnoDB、MyISAM、MEMORY、MERGE、ARCHIVE、CSV等
一般比较常用的有InnoDB、MyISAM
MySQL 5.5以上的版本默认是InnoDB,5.5之前默认存储引擎是MyISAM
区别项 Innodb myisam
事务 支持 不支持
锁粒度 行锁,适合高并发 表锁,不适合高并发
是否默认 默认 非默认
支持外键(物理) 支持外键 不支持
适合场景 读写均衡,写大于读场景,需要事务 读多写少场景,不需要事务
全文索引 不支持,可以通过插件实现, 更多使用ElasticSearch 支持全文索引
是否⽀持事务和崩溃后的安全恢复:
MyISAM 强调的是性能,每次查询具有原⼦性,其执⾏速度⽐InnoDB类型更快,但是不提供事务⽀持。
InnoDB 提供事务⽀持事务,外部键等⾼级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能⼒(crash recoverycapabilities)的事务安全(transaction-safe (ACID compliant))型表。
是否⽀持MVCC :
仅 InnoDB ⽀持。应对⾼并发事务, MVCC⽐单纯的加锁更⾼效;MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下⼯作;MVCC可以使⽤ 乐 观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统⼀。
MyISAM采⽤表级锁(table-level locking)。
InnoDB⽀持**⾏级锁(row-level locking)**和表级锁,默认为⾏级锁
表级锁和⾏级锁对⽐:
表级锁: MySQL中锁定 粒度最⼤ 的⼀种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最⼤,触发锁冲突的概率最⾼,并发度最低,MyISAM和 InnoDB引擎都⽀持表级锁。
⾏级锁: MySQL中锁定 粒度最⼩ 的⼀种锁,只针对当前操作的⾏进⾏加锁。 ⾏级锁能⼤⼤减少数据库操作的冲突。其加锁粒度最⼩,并发度⾼,但加锁的开销也最⼤,加锁慢,会出现死锁。
索引的出现就是为了提高查询的效率,就像一本书的目录。对于一张表来说,索引其实就是它的目录。
MySQL索引使⽤的数据结构主要有BTree索引 和 哈希索引 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝⼤多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余⼤部分场景,建议选择BTree索引。
MySQL的BTree索引使⽤的是B树中的B+Tree,但对于主要的两种存储引擎的实现⽅式是不同的。
MyISAM: B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。
叶节点存的是地址
InnoDB: 其数据⽂件本身就是索引⽂件。相⽐MyISAM,索引⽂件和数据⽂件是分离的,其表数据⽂件本身就是按B+Tree组织的⼀个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据⽂件本身就是主索引。这被称为“聚簇索引(或聚集索引)”。⽽其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值⽽不是地址,这也是和MyISAM不同的地⽅。在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再⾛⼀遍主索引。 因此,在设计表的时候,不建议使⽤过⻓的字段作为主键,也不建议使⽤⾮单调的字段
作为主键,这样会造成主索引频繁分裂。
树节点存的是完整的数据记录
索引名称 特点 创建语句
普通索引 最基本的索引,仅加速查询 CREATE INDEX idx_name ON table_name(filed_name)
唯一索引 加速查询,列值唯一,允许为空;
组合索引则列值的组合必须唯一 CREATE UNIQUE INDEX idx_name ON table_name(filed_name_1,filed_name_2)
主键索引 加速查询,列值唯一,
一个表只有1个,不允许有空值 ALTER TABLE table_name ADD PRIMARY KEY ( filed_name )
组合索引 加速查询,多条件组合查询 CREATE INDEX idx_name ON table_name(filed_name_1,filed_name_2);
覆盖索引 索引包含所需要的值,不需要“回表”查询,比如查询 两个字段,刚好是 组合索引 的两个字段
全文索引 对内容进行分词搜索,仅可用于Myisam, 更多用ElasticSearch做搜索 ALTER TABLE table_name ADD FULLTEXT ( filed_name )
中型公司或者业务发展好的公司,一天新增几百万数据量
业务核心数据存储在Mysql里面,针对业务创建合适的索引
打点数据、日志等存储在ElasticSearch或者MongoDB里面
考虑点:结合实际的业务场景,在哪些字段上创建索引,创建什么类型的索引
说下执行顺序 select、where、from、group by、having、order by
from 从哪个表查询
where 初步过滤条件
group by 过滤后进行分组[重点]
having 对分组后的数据进行二次过滤[重点]
select 查看哪些结果字段
order by 按照怎样的顺序进行排序返回[重点]
select video_id,count(id) num from chapter group by video_id having num >10 order by video_id desc
当MySQL单表记录数过⼤时,数据库的CRUD性能会明显下降,⼀些常见的优化措施如下:
限定数据的范围:务必禁止不带任何限制数据范围条件的查询语句。比如:我们当⽤户在查询订单历史的时候,我们可以控制在⼀个⽉的范围内;
读/写分离:经典的数据库拆分⽅案,主库负责写,从库负责读;
垂直分区:根据数据库⾥⾯数据表的相关性进⾏拆分。 例如,⽤户表中既有⽤户的登录信息⼜有⽤户的基本信息,可以将⽤户表拆分成两个单独的表,甚⾄放到单独的库做分库。简单来说垂直拆分是指数据表列的拆分,把⼀张列⽐较多的表拆分为多张表。
池化设计应该不是⼀个新名词。我们常⻅的如java线程池、jdbc连接池、redis连接池等就是这类设计的代表实现。
这种设计会初始预设资源,解决的问题就是抵消每次获取资源的消耗,如创建线程的开销,获取远程连接的开销等。就好⽐你去⻝堂打饭,打饭的⼤妈会先把饭盛好⼏份放那⾥,你来了就直接拿着饭盒加菜即可,不⽤再临时⼜盛饭⼜打菜,效率就⾼了。
除了初始化资源,池化设计还包括如下这些特征:池⼦的初始值、池⼦的活跃值、池⼦的最⼤值等,这些特征可以直接映射到java线程池和数据库连接池的成员属性中。
数据库连接本质就是⼀个socket的连接。数据库服务端还要维护⼀些缓存和⽤户权限信息之类的,所以占⽤了⼀些内存。我们可以把数据库连接池是看做是维护的数据库连接的缓存,以便将来需要对数据库的请求时可以重⽤这些连接。为每个⽤户打开和维护数据库连接,尤其是对动态数据库驱动的⽹站应⽤程序的请求,既昂贵⼜浪费资源。在连接池中,创建连接后,将其放置在池中,并再次使⽤它,因此不必建⽴新的连接。如果使⽤了所有连接,则会建⽴⼀个新连接并将其添加到池中。 连接池还减少了⽤户必须等待建⽴与数据库的连接的时间。
因为要是分成多个表之后,每个表都是从 1 开始累加,这样是不对的,我们需要⼀个全局唯⼀的id 来⽀持。
⽣成全局 id 有下⾯这⼏种⽅式:
UUID:不适合作为主键,因为太⻓了,并且⽆序不可读,查询效率低。比较适合⽤于⽣成唯⼀的名字的标示⽐如⽂件的名字。
数据库自增 id : 两台数据库分别设置不同步⻓,⽣成不重复ID的策略来实现⾼可⽤。这种⽅式⽣成的 id 有序,但是需要独⽴部署数据库实例,成本⾼,还会有性能瓶颈。
利⽤ redis ⽣成 id : 性能比较好,灵活⽅便,不依赖于数据库。但是,引⼊了新的组件造成系统更加复杂,可⽤性降低,编码更加复杂,增加了系统成本。
Twitter的snowflake算法 :Github 地址:https://github.com/twitter-archive/snowflake。
美团的Leaf分布式ID生成系统 :Leaf 是美团开源的分布式ID⽣成器,能保证全局唯⼀性、趋势递增、单调递增、信息安全,⾥⾯也提到了⼏种分布式⽅案的对⽐,但也需要依赖关系数据库、Zookeeper等中间件。
varchar(len) char(len) len存储的是字符
线上数据库的一个商品表数据量过千万,做深度分页的时候性能很慢,有什么优化思路
现象:千万级别数据很正常,比如数据流水、日志记录等,数据库正常的深度分页会很慢
慢的原因:select * from product limit N,M
MySQL执行此类SQL时需要先扫描到N行,然后再去取M行,N越大,MySQL扫描的记录数越多,SQL的性能就会越差
1、后端、前端缓存
2、使用ElasticSearch分页搜索
3、合理使用 mysql 查询缓存,覆盖索引进行查询分页
select title,cateory from product limit 1000000,100
4、如果id是自增且不存在中间删除数据,使用子查询优化,定位偏移位置的 id
select * from oper_log where type=‘BUY’ limit 1000000,100; //5.秒
select id from oper_log where type=‘BUY’ limit 1000000,1; // 0.4秒
select * from oper_log where type=‘BUY’ and id>=(select id from oper_log where type=‘BUY’ limit 1000000,1) limit 100; //0.8秒
针对线上的数据库,你会做哪些监控,业务性能 + 数据安全 角度分析
大厂一般都有数据库监控后台,里面指标很多,但是开发人员也必须知道
业务性能
1、应用上线前会审查业务新增的sql,和分析sql执行计划
比如是否存在 select * ,索引建立是否合理
2、开启慢查询日志,定期分析慢查询日志
3、监控CPU/内存利用率,读写、网关IO、流量带宽 随着时间的变化统计图
4、吞吐量QPS/TPS,一天内读写随着时间的变化统计图
数据安全
1、短期增量备份,比如一周一次。 定期全量备份,比如一月一次
2、检查是否有非授权用户,是否存在弱口令,网络防火墙检查
3、导出数据是否进行脱敏,防止数据泄露或者黑产利用
4、数据库 全量操作日志审计,防止数据泄露
5、数据库账号密码 业务独立,权限独立控制,防止多库共用同个账号密码
6、高可用 主从架构,多机房部署
redo 重做日志
作用:确保事务的持久性,防止在发生故障,脏页未写入磁盘。重启数据库会进行redo log执行重做,到达事务一致性
undo 回滚日志
作用:保证数据的原子性,记录事务发生之前的数据的一个版本,用于回滚。
innodb事务的可重复读和读取已提交 隔离级别就是通过mvcc+undo实现
errorlog 错误日志
作用:Mysql本身启动、停止、运行期间发生的错误信息
slow query log 慢查询日志
作用:记录执行时间过长的sql,时间阈值可以配置,只记录执行成功
binlog 二进制日志
作用:用于主从复制,实现主从同步
relay log 中继日志
作用:用于数据库主从同步,将主库发送来的binlog先保存在本地,然后从库进行回放
general log 普通日志
作用:记录数据库操作明细,默认关闭,开启会降低数据库性能
容灾使用,用于故障切换
业务需要,进行读写分离减少主库压力
既然你们搭建了主从同步,且你们日增量数据量也不少,有没遇到同步延迟问题
为什么会有同步延迟问题,怎么解决?
保证性能第一情况下,不能百分百解决主从同步延迟问题,只能增加缓解措施。
现象:主从同步,大数据量场景下,会发现写入主库的数据,在从库没找到。
原因:
1、主从复制是单线程操作,当主库TPS高,产生的超过从库sql线程执行能力
2、从库执行了大的sql操作,阻塞等待
3、服务器硬件问题,如磁盘,CPU,还有网络延迟等
解决办法:
1、业务需要有一定的容忍度,程序和数据库直接增加缓存,降低读压力
2、业务适合的话,写入主库后,再写缓存,读的时候可以读缓存,没命中再读从库
3、读写分离,一主多从,分散主库和从库压力
4、提高硬件配置,比如使用SSD固态硬盘、更好的CPU和网络
5、进行分库分表,减少单机压力
什么场景下会出现主从数据不一致
1、本身复制延迟导致
2、主库宕机或者从库宕机都会导致复制中断
3、把一个从库提升为主库,可能导致从库和主库的数据不一致性
是否有做过主从一致性校验,你是怎么做的,如果没做过,你计划怎么做
如果不一致你会怎么修复
Mysql主从复制是基于binlog复制,难免出现复制数据不一致的风险,引起用户数据访问前后不一致的风险
所以要定期开展主从复制数据一致性的校验并修复,避免这些问题
解决方案之一,使用Percona公司下的工具
pt-table-checksum工具进行一致性校验
原理:
主库利用表中的索引,将表的数据切割成一个个chunk(块),然后进行计算得到checksum值。
从库也执相应的操作,并在从库上计算相同数据块的checksum,然后对比主从中各个表的checksum是否一致并存储到数据库,最后通过存储校验结果的表就可以判断出哪些表的数据不一致
pt-table-sync(在从库执行)工具进行修复不一致数据,可以修复主从结构数据的不一致,也可以修复非主从结构数据表的数据不一致
原理:在主库上执行数据的更改,再同步到从库上,不会直接更改成从的数据。在主库上执行更改是基于主库现在的数据,也不会更改主库上的数据,可以同步某些表或整个库的数据,但它不同步表结构、索引,只同步不一致的数据
注意:
默认主库要检查的表在从库都存在,并且同主库表有相同的表结构
如果表中没有索引,pt-table-checksum将没法处理,一般要求最基本都要有主键索引
pt-table-sync工具会修改数据,使用前最好备份下数据,防止误操作
pt-table-checksum怎么保证某个chunk的时候checksum数据一致性?
当pt工具在计算主库上某chunk的checksum时,主库可能在更新且从库可能复制延迟,那该怎么保证主库与从库计算的是”同一份”数据,答案把要checksum的行加上for update锁并计算,这保证了主库的某个chunk内部数据的一致性
使用C语言开发,基于内存,效率特别高,单线程的,运用了IO多路复用技术,主要用来做缓存,和分布式锁,甚至可以用来做消息队列,Redis还支持事务和持久化,lua脚本的方案.此外,还可以做到高性能和高可用,高性能的话主要是由于使用了单线程,可以通过搭建主从架构还有哨兵模式实现redis的高可用方案
简单来说 Redis 就是⼀个使⽤ C 语⾔开发的数据库,不过与传统数据库不同的是 Redis 的数据是存在内存中的,也就是它是内存数据库,所以读写速度⾮常快,因此 Redis 被⼴泛应⽤于缓存⽅向。另外,Redis 除了做缓存之外,Redis 也经常⽤来做分布式锁,甚⾄是消息队列。Redis 提供了多种数据类型来⽀持不同的业务场景。Redis 还⽀持事务 、持久化、Lua 脚本、多种集群⽅案。
String
hash
4. 介绍 :hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,
Redis 的 hash 做了更多优化。另外,hash 是⼀个 string 类型的 field 和 value 的映射表,
特别适合⽤于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的
值。 ⽐如我们可以 hash 数据结构来存储⽤户信息,商品信息等等。
5. 常⽤命令: hset,hmset,hexists,hget,hgetall,hkeys,hvals 等。
6. 应⽤场景: 系统中对象数据的存储,购物车。
list
7. 介绍 :==list 即是链表。==链表是⼀种常见的数据结构,特点是易于数据元素的插⼊和删除并且且可以灵活调整链表⻓度,但是链表的随机访问困难。许多⾼级编程语⾔都内置了链表的实现⽐如 Java 中的LinkedList,但是 C 语⾔并没有实现链表,所以 Redis 实现了⾃⼰的链表数据结构。Redis 的 list 的实现为⼀个双向链表,即可以⽀持反向查找和遍历,更⽅便操作,不过带来了部分额外的内存开销。
8. 常⽤命令: rpush,lpop,lpush,rpop,lrange、llen 等。
9. 应⽤场景: 发布与订阅或者说消息队列、慢查询。
set
10. 介绍 : set 类似于 Java 中的 HashSet。Redis 中的 set 类型是⼀种无序集合,集合中的元素没有先后顺序。当你需要存储⼀个列表数据,⼜不希望出现重复数据时,set 是⼀个很好的选择,并且 set 提供了判断某个成员是否在⼀个 set 集合内的重要接⼝,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。⽐如:你可以将⼀个⽤户所有的关注⼈存在⼀个集合中,将其所有粉丝存在⼀个集合。Redis 可以⾮常⽅便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
11. 常⽤命令: sadd,spop,smembers,sismember,scard,sinterstore,sunion 等。
12. 应⽤场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景
sroted set
13. 介绍: 和 set 相⽐,sorted set 增加了⼀个权重参数 score,使得集合中的元素能够按 score进⾏有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap和 TreeSet 的结合体。
14. 常⽤命令: zadd,zcard,zscore,zrange,zrevrange,zrem 等。
15. 应⽤场景: 需要对数据根据某个权重进⾏排序的场景。⽐如在直播系统中,实时排⾏信息包含直播间在线⽤户列表,各种礼物排⾏榜,弹幕消息(可以理解为按消息维度的消息排⾏榜)等信息。
假如⽤户第⼀次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,⽤户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心i地将该⽤户访问的数据存在缓存中。这样有什么好处呢? 那就是保证⽤户下⼀次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。不过,要保持数据库和缓存中的数据的⼀致性。 如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
⾼并发:
⼀般像 MySQL 这类的数据库的 QPS ⼤概都在 1w 左右(4 核 8g) ,但是使⽤ Redis 缓存之后很容易达到 10w+,甚⾄最⾼能达到 30w+(就单机 redis 的情况,redis 集群的话会更⾼)。
QPS(Query Per Second):服务器每秒可以执⾏的查询次数;
所以,直接操作缓存能够承受的数据库请求数量是远远⼤于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样⽤户的⼀部分请求会直接到缓存这⾥⽽不⽤经过数据库。进而,我们也就提⾼的系统整体的并发。
Redis 基于 Reactor 模式来设计开发了⾃⼰的⼀套⾼效的事件处理模型 (Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是⾼性能 IO 的基⽯),这套事件处理模型对应的是 Redis中的⽂件事件处理器(file event handler)。由于⽂件事件处理器(file event handler)是单线程⽅式运⾏的,所以我们⼀般都说 Redis 是单线程模型。
既然是单线程,那怎么监听⼤量的客户端连接呢?
Redis 通过IO 多路复用程序来监听来⾃客户端的⼤量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发⽣。这样的好处⾮常明显: I/O 多路复⽤技术的使⽤让 Redis 不需要额外创建多余的线程来监听客户端的⼤量连接,降低了资源的消耗(和 NIO 中的 Selector 组件很像)。另外, Redis 服务器是⼀个事件驱动程序,服务器需要处理两类事件: 1. ⽂件事件; 2. 时间事件。
为了处理只在一段时间内有效的数据,比如用户验证码.还有分布式锁的时候也需要指定过期时间,防止持有锁的线程down掉之后锁未释放
因为内存是有限的,如果缓存中的所有数据都是⼀直保存的话,分分钟直接Out of memory。
过期时间除了有助于缓解内存的消耗,还有什么其他⽤么?
很多时候,我们的业务场景就是需要某个数据只在某⼀时间段内存在,⽐如我们的短信验证码可能只在1分钟内有效,⽤户登录的 token 可能只在 1 天内有效。如果使⽤传统的数据库来处理的话,⼀般都是⾃⼰判断过期,这样更麻烦并且性能要差很多。
如果假设你设置了⼀批 key 只能存活 1 分钟,那么 1 分钟后,Redis 是怎么对这批 key 进⾏删除的呢?
定期删除对内存更加友好,惰性删除对CPU更加友好。两者各有千秋,所以Redis 采⽤的是 定期
删除+惰性/懒汉式删除 。
但是,仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉
了很多过期 key 的情况。这样就导致⼤量过期 key 堆积在内存⾥,然后就Out of memory了。
怎么解决这个问题呢?答案就是: Redis 内存淘汰机制
一般会使用淘汰策略
常见的淘汰策略有 FIFO、LRU、LFU
能分别说下FIFO、LRU、LFU这些策略不
先进先出First In,First Out
新访问的数据插入FIFO队列尾部,数据在FIFO队列中顺序移动,淘汰FIFO队列头部的数据
最近最少使用 Least recently used
根据数据的历史访问记录来进行数据淘汰,如果数据最近被访问过,那么将来被访问的几率也更高
新数据插入到链表头部,每当缓存数据被访问,则将数据移到链表头部,当链表满的时候,将链表尾部的数据丢弃。
最近不经常使用 Least Frequently Used
根据数据的历史访问频率来淘汰数据,如果数据过去被访问多次,那么将来被访问的频率也更高
把数据加入到链表中,按频次排序,一个数据被访问过,把它的频次+1,发生淘汰的时候,把频次低的淘汰掉
支持AOF和RDB持久化
AOF
以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录
支持秒级持久化、兼容性好,对于相同数量的数据集而言,AOF文件通常要大于RDB文件,所以恢复比RDB慢
RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘,可以指定时间归档数据(形成冷数据),但不能做到实时持久化
文件紧凑,体积小,对于灾难恢复而言,RDB是非常不错的选择,相比于AOF机制,如果数据集很大,RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快
有save和bgsave两种方式,bgsave会在后台fork一个子进程进行持久化操作
Redis 可以通过 MULTI,EXEC,DISCARD 和 WATCH 等命令来实现事务(transaction)功能。
使⽤ MULTI命令后可以输⼊多个命令。Redis不会⽴即执⾏这些命令,⽽是将它们放到队列,当调⽤了EXEC命令将执⾏所有命令。
我们知道事务具有四⼤特性:1. 原⼦性,2. 隔离性,3. 持久性,4. ⼀致性。
Redis 是不⽀持 roll back 的,因⽽不满⾜原⼦性的(⽽且不满⾜持久性)
缓存击穿 (某个热点key缓存失效了)
缓存中没有但数据库中有的数据,假如是热点数据,那key在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力增大。
和缓存雪崩的区别在于这里针对某一key缓存,后者则是很多key。
预防:设置热点数据不过期,定时任务定时更新缓存,或者设置互斥锁
缓存雪崩 (多个热点key都过期)
大量的key设置了相同的过期时间,导致在缓存在同一时刻全部失效,造成瞬时DB请求量大、压力骤增,引起雪崩
预防:存数据的过期时间设置随机,防止同一时间大量数据过期现象发生,设置热点数据永远不过期,定时任务定时更新
缓存穿透(查询不存在数据)
查询一个不存在的数据,由于缓存是不命中的,并且出于容错考虑,如发起为id为“-1”不存在的数据
如果从存储层查不到数据则不写入缓存这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。存在大量查询不存在的数据,可能DB就挂掉了,这也是黑客利用不存在的key频繁攻击应用的一种方式。
预防:接口层增加校验,数据合理性校验,缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,设置短点的过期时间,防止同个key被一直攻击
Spring是一款开源的轻量级的Java开发框架,一般说的Spring框架都是SpringFramework,它是很多模块的集合,通过这些模块可以很快速的完成我们的开发工作,比如AOP和IOC,还有集成测试,同时也可以很方便的集成第三方组件,比如邮件啊缓存啊各种。
Spring 是一款开源的轻量级 Java 开发框架,旨在提高开发人员的开发效率以及系统的可维护性。
我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发,比如说 Spring 支持 IoC(Inverse of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程)、可以很方便地对数据库进行访问、可以很方便地集成第三方组件(电子邮件,任务,调度,缓存等等)、对单元测试支持比较好、支持 RESTful Java 应用程序的开发。
Spring 最核心的思想就是不重新造轮子,开箱即用,提高开发效率。
Spring 翻译过来就是春天的意思,可见其目标和使命就是为 Java 程序员带来春天啊!感动!
Spring 提供的核心功能主要是 IoC 和 AOP。学习 Spring ,一定要把 IoC 和 AOP 的核心思想搞懂!
Spring5.x 版本
Spring5.x 版本中 Web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。
Spring 各个模块的依赖关系如下:
Core Container
Spring 框架的核心模块,也可以说是基础模块,主要提供 IoC 依赖注入功能的支持。Spring 其他所有的功能基本都需要依赖于该模块,我们从上面那张 Spring 各个模块的依赖关系图就可以看出来。
Spring Test
Spring 团队提倡测试驱动开发(TDD)。有了控制反转 (IoC)的帮助,单元测试和集成测试变得更简单。
Spring 的测试模块对 JUnit(单元测试框架)、TestNG(类似 JUnit)、Mockito(主要用来 Mock 对象)、PowerMock(解决 Mockito 的问题比如无法模拟 final, static, private 方法)等等常用的测试框架支持的都比较好。
很多人对 Spring,Spring MVC,Spring Boot 这三者傻傻分不清楚!这里简单介绍一下这三者,其实很简单,没有什么高深的东西。
Spring 包含了多个功能模块(上面刚刚提高过),其中最重要的是 Spring-Core(主要提供 IoC 依赖注入功能的支持) 模块, Spring 中的其他模块(比如 Spring MVC)的功能实现基本都需要依赖于该模块。
下图对应的是 Spring4.x 版本。目前最新的 5.x 版本中 Web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。
Spring MVC 是 Spring 中的一个很重要的模块,主要赋予 Spring 快速构建 MVC 架构的 Web 程序的能力。MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。
使用 Spring 进行开发各种配置过于麻烦比如开启某些 Spring 特性时,需要用 XML 或 Java 进行显式配置。于是,Spring Boot 诞生了!
Spring 旨在简化 J2EE 企业应用程序开发。Spring Boot 旨在简化 Spring 开发(减少配置文件,开箱即用!)。
Spring Boot 只是简化了配置,如果你需要构建 MVC 架构的 Web 程序,你还是需要使用 Spring MVC 作为 MVC 框架,只是说 Spring Boot 帮你简化了 Spring MVC 的很多配置,真正做到开箱即用!
轻量:Spring是轻量的,基本的版本大约2MB
控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象
面向切面编程(AOP):Spring支持面向切面编程,并且把应用业务逻辑和系统服务分开
容器:Spring包含并管理应用中对象的生命周期和配置
MVC框架:Spring的WEB框架是个精心设计的框架,是web框架的一个很好的替代品
事务管理:Spring提供了一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)
异常处理:Spring提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转换为一致的unchecked异常
IOC或依赖注入把应用的代码量降到最低,它使应用容易测试,单元测试不再需要单例和JNDI查找机制,最小的代价和最小的侵入性使松散耦合得以实现,IOC容易支持加载服务时的饿汉式初始化和懒加载
**IoC(Inverse of Control:控制反转)**是一种设计思想,而不是一个具体的技术实现。==IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。==不过, IoC 并非 Spring 特有,在其他语言中也有应用。
为什么叫控制反转?
控制 :指的是对象创建(实例化、管理)的权力
**反转 **:控制权交给外部环境(Spring 框架、IoC 容器)
将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。
在实际项目中一个 Service 类可能依赖了很多其他的类,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。
在 Spring 中, IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 Map(key,value),Map 中存放的是各种对象。
Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是SpringBoot 注解配置就慢慢开始流行起来。
IOC 控制反转,指将对象的创建权,反转到Spring容器
DI 依赖注入,指Spring创建对象的过程中,将对象依赖属性通过配置进行注入,不能单独存在,需要在IOC的基础上完成操作
依赖注入(DI)和控制反转(IOC)是从不同的角度的描述的同一件事情,通过引入IOC容器,利用依赖关系注入的方式,实现对象之间的解耦。
简单来说,Bean 代指的就是那些被 IoC 容器所管理的对象。
我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。
<bean id="..." class="...">
<constructor-arg value="..."/>
bean>
下图简单地展示了 IoC 容器如何使用配置元数据来管理对象。
org.springframework.beans和 org.springframework.context 这两个包是 IoC 实现的基础,如果想要研究 IoC 相关的源码的话,可以去看看
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}
上面的代码相当于下面的 xml 配置
<beans>
<bean id="transferService" class="com.acme.TransferServiceImpl"/>
beans>
下面这个例子是通过 @Component 无法实现的。
@Bean
public OneService getService(status) {
case (status) {
when 1:
return new serviceImpl1();
when 2:
return new serviceImpl2();
when 3:
return new serviceImpl3();
}
}
Spring 内置的 @Autowired 以及 JDK 内置的 @Resource 和 @Inject 都可以用于注入 Bean。
@Autowired 和@Resource使用的比较多一些。
Autowired 属于 Spring 内置的注解,默认的注入方式为byType(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。
这会有什么问题呢? 当一个接口存在多个实现类的话,byType这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。
这种情况下,注入方式会变为 byName(根据名称进行匹配),这个名称通常就是类名(首字母小写)。
// smsService 就是我们上面所说的名称
@Autowired
private SmsService smsService;
举个例子,SmsService 接口有两个实现类: SmsServiceImpl1和 SmsServiceImpl2,且它们都已经被 Spring 容器所管理。
// 报错,byName 和 byType 都无法匹配到 bean
@Autowired
private SmsService smsService;
// 正确注入 SmsServiceImpl1 对象对应的 bean
@Autowired
private SmsService smsServiceImpl1;
// 正确注入 SmsServiceImpl1 对象对应的 bean
// smsServiceImpl1 就是我们上面所说的名称
@Autowired
@Qualifier(value = "smsServiceImpl1")
private SmsService smsService;
我们还是建议通过 @Qualifier 注解来显示指定名称而不是依赖变量的名称。
@Resource属于 JDK 提供的注解,默认注入方式为 byName。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为byType。
@Resource 有两个比较重要且日常开发常用的属性:name(名称)、type(类型)。
public @interface Resource {
String name() default "";
Class<?> type() default Object.class;
}
如果仅指定 name 属性则注入方式为byName,如果仅指定type属性则注入方式为byType,如果同时指定name 和type属性(不建议这么做)则注入方式为byType+byName。
// 报错,byName 和 byType 都无法匹配到 bean
@Resource
private SmsService smsService;
// 正确注入 SmsServiceImpl1 对象对应的 bean
@Resource
private SmsService smsServiceImpl1;
// 正确注入 SmsServiceImpl1 对象对应的 bean(比较推荐这种方式)
@Resource(name = "smsServiceImpl1")
private SmsService smsService;
简单总结一下:
Spring 中 Bean 的作用域通常有下面几种:
xml 方式:
<bean id="..." class="..." scope="singleton">bean>
注解方式:
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Person personPrototype() {
return new Person();
}
大部分时候我们并没有在项目中使用多线程,所以很少有人会关注这个问题。单例 Bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候是存在资源竞争的。
常见的有两种解决办法:
在 Bean 中尽量避免定义可变的成员变量。
在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。
不过,大部分 Bean 实际都是无状态(没有实例变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。
Bean 的生命周期了解么?
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
Aspect Oriented Program 面向切面编程, 在不改变原有逻辑上增加额外的功能
AOP思想把功能分两个部分,分离系统中的各种关注点
核心关注点
业务的主要功能
横切关注点
非核心、额外增加的功能
用户下单为例子
核心关注点:创建订单
横切关注点:记录日志、控制事务
好处
减少代码侵入,解耦
可以统一处理横切逻辑
方便添加和删除横切逻辑
Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:
当然你也可以使用 AspectJ !Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。
横切关注点
对哪些方法进行拦截,拦截后怎么处理,这些就叫横切关注点
比如 权限认证、日志、事物
通知 Advice
在特定的切入点上执行的增强处理
做啥? 比如你需要记录日志,控制事务 ,提前编写好通用的模块,需要的地方直接调用
连接点 JointPoint
要用通知的地方,业务流程在运行过程中需要插入切面的具体位置,一般是方法的调用前后,全部方法都可以是连接点
切入点 Pointcut
不能全部方法都是连接点,通过特定的规则来筛选连接点, 就是Pointcut,选中那几个你想要的方法
在程序中主要体现为书写切入点表达式(通过通配、正则表达式)过滤出特定的一组 JointPoint连接点
过滤出相应的 Advice 将要发生的joinpoint地方
切面 Aspect
通常是一个类,里面定义 切入点+通知 , 定义在什么地方; 什么时间点、做什么事情
通知 advice指明了时间和做的事情(前置、后置等)
切入点 pointcut 指定在什么地方干这个事情
web接口设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面,对象和对象,方法和方法之间都是一个个切面
目标 target
目标类,真正的业务逻辑,可以在目标类不知情的条件下,增加新的功能到目标类的链路上
织入 Weaving
把切面(某个类)应用到目标函数的过程称为织入
能否解释下什么是静态代理
什么是静态代理
优点
缺点
能否解释下什么是动态代理,spring aop是用什么代理
Count.java
/**
* 定义一个账户接口
*
* @author Administrator
*
*/
public interface Count {
// 查看账户方法
public void queryCount();
// 修改账户方法
public void updateCount();
}
CountImpl.java
/**
* 委托类(包含业务逻辑)
*
* @author Administrator
*
*/
public class CountImpl implements Count {
@Override
public void queryCount() {
System.out.println("查看账户方法...");
}
@Override
public void updateCount() {
System.out.println("修改账户方法...");
}
}
//CountProxy.java
package net.battier.dao.impl;
import net.battier.dao.Count;
/**
* 这是一个代理类(增强CountImpl实现类)
*
* @author Administrator
*
*/
public class CountProxy implements Count {
private CountImpl countImpl;
/**
* 覆盖默认构造器
*
* @param countImpl
*/
public CountProxy(CountImpl countImpl) {
this.countImpl = countImpl;
}
@Override
public void queryCount() {
System.out.println("事务处理之前");
// 调用委托类的方法;
countImpl.queryCount();
System.out.println("事务处理之后");
}
@Override
public void updateCount() {
System.out.println("事务处理之前");
// 调用委托类的方法;
countImpl.updateCount();
System.out.println("事务处理之后");
}
}
TestCount.java
/**
*测试Count类
*
* @author Administrator
*
*/
public class TestCount {
public static void main(String[] args) {
CountImpl countImpl = new CountImpl();
CountProxy countProxy = new CountProxy(countImpl);
countProxy.updateCount();
countProxy.queryCount();
}
}
观察代码可以发现每一个代理类只能为一个接口服务,这样一来程序开发中必然会产生过多的代理,而且,所有的代理操作除了调用的方法不一样之外,其他的操作都一样,则此时肯定是重复代码。解决这一问题最好的做法是可以通过一个代理类完成全部的代理功能,那么此时就必须使用动态代理完成。
1 JDK动态代理需要一个接口和一个类
1.1 InvocationHandler (调用处理程序)
InvocationHandler 是生成代理实例的类需要实现的接口,然后需要实现接口中的 invoke() 方法,在这个方法中进行对代理实例的处理
1.2 Proxy (代理)
Proxy 是所有代理实例的父类,它提供了创建动态代理实例的静态方法.
2 代码演示一
2.1 创建一个抽象对象
//租房public interface Rend { public void rend();}
2.2 创建一个 真实对象
public class Homeowner implements Rend {
@Override
public void rend(){
System.out.println("房东出租了房子");
}}
3.3 创建一个生成代理实例的类(核心)
//这个类是用来生成代理实例的类
public class ProxyInvocationHandle implements InvocationHandler {
//被代理的接口
private Rend rend;
public void setRend(Rend rend) {
this.rend = rend;
}
/**
* 参数说明:
* ClassLoader loader:类加载器
* Class>[] interfaces:得到全部的接口
* InvocationHandler h:得到InvocationHandler接口的子类实例
*/
//生成得到代理类
public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader(),rend.getClass().getInterfaces(),this);
}
/**
* 参数说明:
* Object proxy:指被代理的对象。
* Method method:要调用的方法
* Object[] args:方法调用时所需要的参数
*/
//处理代理实例,并返回结果
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//动态代理的本质,就是使用反射机制实现!
seeHouse();
seeHouse();
Object result = method.invoke(rend, args);
fare();
return result;
}
public void seeHouse(){
System.out.println("中介带你看房子");
}
public void fare(){
System.out.println("收中介费");
}
}
3.4 创建客户类
public class Client {
public static void main(String[] args) {
//真实角色
Homeowner homeowner = new Homeowner();
//代理角色:现在没有
ProxyInvocationHandle pih = new ProxyInvocationHandle();
//通过调用程序处理角色来处理我们要调用的接口对象!
pih.setRend(homeowner);
Rend proxy = (Rend) pih.getProxy();//这里的proxy就是动态生成的,我们并没有写
proxy.rend();
}
}
但是,JDK的动态代理依靠接口实现,如果有些类并没有实现接口,则不能使用JDK代理,这就要使用cglib动态代理了。
4 总结
JDK的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能实现JDK的动态代理,cglib是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对final修饰的类进行代理。
1、BookFacadeCglib.java
public interface BookFacade {
public void addBook();
}
2、BookCadeImpl1.java
/**
* 这个是没有实现接口的实现类
*
* @author student
*
*/
public class BookFacadeImpl1 {
public void addBook() {
System.out.println("增加图书的普通方法...");
}
}
BookFacadeProxy.java
/**
* 使用cglib动态代理
*
* @author student
*
*/
public class BookFacadeCglib implements MethodInterceptor {
private Object target;
/**
* 创建代理对象
*
* @param target
* @return
*/
public Object getInstance(Object target) {
this.target = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(this.target.getClass());
// 回调方法
enhancer.setCallback(this);
// 创建代理对象
return enhancer.create();
}
@Override
// 回调方法
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("事物开始");
proxy.invokeSuper(obj, args);
System.out.println("事物结束");
return null;
}
}
4、TestCglib.java
public class TestCglib {
public static void main(String[] args) {
BookFacadeCglib cglib=new BookFacadeCglib();
BookFacadeImpl1 bookCglib=(BookFacadeImpl1)cglib.getInstance(new BookFacadeImpl1());
bookCglib.addBook();
}
}
Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。
Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。
1、通常使用@Order 注解直接定义切面顺序
// 值越小优先级越高
@Order(3)
@Component
@Aspect
public class LoggingAspect implements Ordered {
2、实现Ordered 接口重写 getOrder 方法。
@Component
@Aspect
public class LoggingAspect implements Ordered {
// ....
@Override
public int getOrder() {
// 返回值越小优先级越高
return 1;
}
}
@Controller 返回⼀个⻚⾯
单独使⽤ @Controller 不加 @ResponseBody 的话⼀般使⽤在要返回⼀个视图的情况,这种情况属于比较传统的Spring MVC 的应⽤,对应于前后端不分离的情况。
@RestController 返回JSON 或 XML 形式数据
但 @RestController 只返回对象,对象数据直接以 JSON 或 XML 形式写⼊ HTTP 响应
(Response)中,这种情况属于 RESTful Web服务,这也是⽬前⽇常开发所接触的最常⽤的情况
(前后端分离)。
@Controller +@ResponseBody 返回JSON 或 XML 形式数据
@ResponseBody 注解的作⽤是将 Controller 的⽅法返回的对象通过适当的转换器转换为指定的格式之后,写⼊到HTTP 响应(Response)对象的 body 中,通常⽤来返回 JSON 或者XML 数据,返回 JSON 数据的情况比较多。
编程式事务 :在代码中硬编码(不推荐使用) : 通过 TransactionTemplate或者 TransactionManager 手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。
声明式事务 :在 XML 配置文件中配置或者直接基于注解(推荐使用) : 实际是通过 AOP 实现(基于@Transactional 的全注解方式使用最多)
事务传播行为是为了解决业务层方法之间互相调用的事务问题。
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
正确的事务传播行为可能的值如下:
1.TransactionDefinition.PROPAGATION_REQUIRED
使用的最多的一个事务传播行为,我们平时经常使用的@Transactional注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
2.TransactionDefinition.PROPAGATION_REQUIRES_NEW
创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
3.TransactionDefinition.PROPAGATION_NESTED
如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
4.TransactionDefinition.PROPAGATION_MANDATORY
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
这个使用的很少。
若是错误的配置以下 3 种事务传播行为,事务将不会发生回滚:
和事务传播行为这块一样,为了方便使用,Spring 也相应地定义了一个枚举类:Isolation
public enum Isolation {
DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),
READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),
READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),
REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),
SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);
private final int value;
Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
下面我依次对每一种事务隔离级别进行介绍:
Exception 分为运行时异常 RuntimeException 和非运行时异常。事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。
当 @Transactional 注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。
在 @Transactional 注解中如果不配置rollbackFor属性,那么事务只会在遇到RuntimeException的时候才会回滚,加上 rollbackFor=Exception.class,可以让事务在遇到非运行时异常时也回滚。
通过使用JDBC抽象和DAO模块,保证数据库代码的简介,并能避免数据库资源错误关闭导致的问题,它在各种不同的数据库的错误信息之上,提供了一个统一的异常访问层,它还利用Spring的AOP模块给Spring应用中的对象提供事务管理服务
setter属性注入
构造方法注入
注解方式注入
Spring配置文件是个XML文件,这个文件包含了类信息,描述了如何配置它们,已经如何相互调用
SpringBeans是那些形成Spring应用的主干的java对象,它们被SpringIOC容器初始化,装配和管理,这些beans通过容器中配置的元数据创建,比如,以xml文件中的形式定义
Spring’框架定义的beans都是单间beans,在bean tag中有个属性"singleton",如果它被赋为True,bean就是单件,否则就是一个prototype bean,默认是true,所以所有在spring框架中的beans缺省都是单件
一个SpringBean的定义包含容器必知的所有配置元数据,包括如何创建一个bean,它的生命周期详情及它的依赖
spring中的bean默认是单例模式,spring框架并没有对单例bean进行多线程的封装处理
实际上大部分时候springbean无状态的(比如果dao类),所以某种程度上来说bean也是安全的,但是如果bean有状态的话(比如view model对象),那就要开发者自己去保证线程安全了,最简单的就是改变bean的作用域,把"singleton"变更为"prototype",这样请求bean相当于new Bean()了,所以就可以保证线程安全了
基于java的配置,允许你在少量的java注解的帮助下,进行你的大部分Spring配置而非通过xml文件
以@Configuration注解为例,它用来标记类可以当做一个bean的定义,被SpringIOC容器使用,另一个例子是@Bean注解,它表示次方法将要返回一个对象,作为一个bean注册进Spring应用上下文
相对于xml文件,注解型的配置依赖于通过字节码元数据装配组件,而非尖括号的声明
开发者通过在相应的类,方法或属性上使用注解的方式,直接组件类中进行配置,而不使用xml表述bean的装配关系
这个注解表明bean的属性必须在配置的时候设置,通过一个bean定义的显示的属性值或通过自动装配,若@Required注解的bean属性未被设置,容器将抛出BeanInitializatioonException
@Autowired注解提供了一种更细粒度的控制,包括在何处以及如何完成自动装配,它的用法和@Required一样.修饰setter方法,构造器,属性或者具有任意名称和多个参数的方法
当有多个相同类型的bean却只有一个需要自动装配时候,将@Qualifier注解和@Autowired注解结合使用以消除这种混淆,指定需要装配的确切的bean
使用SpringJDBC框架,资源管理和错误处理的代价将会被减轻,所有开发者只需要写statements和querues从数据存取数据,jdbc也可以在spring框架提供的模板类的帮助下更有效的被使用,这个模板叫做jdbcTemplate
jdbcTemplate类提供了很多便利的方法解决诸如把数据库数据转变成基本数据类型或对象,执行写好的或可调用的数据库操作语句,提供自定义的数据错误处理
MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。
Model 1 时代
很多学 Java 后端比较晚的朋友可能并没有接触过 Model 1 时代下的 JavaWeb 应用开发。在 Model1 模式下,整个 Web 应用几乎全部用 JSP 页面组成,只用少量的 JavaBean 来处理数据库连接、访问等操作。
这个模式下 JSP 即是控制层(Controller)又是表现层(View)。显而易见,这种模式存在很多问题。比如控制逻辑和表现逻辑混杂在一起,导致代码重用率极低;再比如前端和后端相互依赖,难以进行测试维护并且开发效率极低。
Model 2 时代
学过 Servlet 并做过相关 Demo 的朋友应该了解“Java Bean(Model)+ JSP(View)+Servlet(Controller) ”这种开发模式,这就是早期的 JavaWeb MVC 开发模式。
Model:系统涉及的数据,也就是 dao 和 bean。
View:展示模型中的数据,只是用来展示。
Controller:处理用户请求都发送给 ,返回数据给 JSP 并展示给用户。
Model2 模式下还存在很多问题,Model2 的抽象和封装程度还远远不够,使用 Model2 进行开发时不可避免地会重复造轮子,这就大大降低了程序的可维护性和复用性。
于是,很多 JavaWeb 开发相关的 MVC 框架应运而生比如 Struts2,但是 Struts2 比较笨重。
Spring MVC 时代
随着 Spring 轻量级开发框架的流行,Spring 生态圈出现了 Spring MVC 框架, Spring MVC 是当前最优秀的 MVC 框架。相比于 Struts2 , Spring MVC 使用更加简单和方便,开发效率更高,并且 Spring MVC 运行速度更快。
MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring MVC 可以帮助我们进行更简洁的 Web 层的开发,并且它天生与 Spring 框架集成。Spring MVC 下我们一般把后端项目分为 Service 层(处理业务)、Dao 层(数据库操作)、Entity 层(实体类)、Controller 层(控制层,返回数据给前台页面)。
记住了下面这些组件,也就记住了 SpringMVC 的工作原理。
1.客户端(浏览器)发送请求, DispatcherServlet拦截请求。
2.DispatcherServlet 根据请求信息调用 HandlerMapping 。HandlerMapping 根据 uri 去匹配查找能处理的 Handler(也就是我们平常说的 Controller 控制器) ,并会将请求涉及到的拦截器和 Handler 一起封装。
3.DispatcherServlet 调用 HandlerAdapter适配执行 Handler 。
4.Handler 完成对用户请求的处理后,会返回一个 ModelAndView 对象给DispatcherServlet,ModelAndView 顾名思义,包含了数据模型以及相应的视图的信息。Model 是返回的数据对象,View 是个逻辑上的 View。
5.ViewResolver 会根据逻辑 View 查找实际的 View。
6.DispaterServlet 把返回的 Model 传给 View(视图渲染)。
7.把 View 返回给请求者(浏览器)
推荐使用注解的方式统一异常处理,具体会使用到 @ControllerAdvice + @ExceptionHandler 这两个注解 。
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(BaseException.class)
public ResponseEntity<?> handleAppException(BaseException ex, HttpServletRequest request) {
//......
}
@ExceptionHandler(value = ResourceNotFoundException.class)
public ResponseEntity<ErrorReponse> handleResourceNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) {
//......
}
}
这种异常处理方式下,会给所有或者指定的 Controller 织入异常处理的逻辑(AOP),当 Controller 中的方法抛出异常的时候,由被@ExceptionHandler 注解修饰的方法进行处理。
ExceptionHandlerMethodResolver 中 getMappedMethod 方法决定了异常具体被哪个被 @ExceptionHandler 注解修饰的方法处理异常。
@Nullable
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList<>();
//找到可以处理的所有异常信息。mappedMethods 中存放了异常和处理异常的方法的对应关系
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
if (mappedException.isAssignableFrom(exceptionType)) {
matches.add(mappedException);
}
}
// 不为空说明有方法处理异常
if (!matches.isEmpty()) {
// 按照匹配程度从小到大排序
matches.sort(new ExceptionDepthComparator(exceptionType));
// 返回处理异常的方法
return this.mappedMethods.get(matches.get(0));
}
else {
return null;
}
}
从源代码看出:getMappedMethod()会首先找到可以匹配处理异常的所有方法信息,然后对其进行从小到大的排序,最后取最小的那一个匹配的方法(即匹配度最高的那个)。
使用devtools启动热部署,添加devtools库,在配置文件中把spring.devtools.restart.enable设置为true
使用IDEA编辑器,勾上自动编译或者手动重新编译
SpringBootActuator是Spring启动框架中的重要功能之一,SpringBoot监视器可帮助你访问生产环境中正在运行的应用程序的当前状态,有几个指标必须在生产环境中进行检查和监控,集市一些外部应用程序可以正在使用这些服务来向相关人员触发警报消息,监视器模块公开了一组可直接作为HTTP url 访问的rest端点来检查状态
默认情况下,所有敏感的http端点都是安全的,只有具有Actuator角色的用户才能访问它们,安全性是使用标准的HttpServletRequest.isUserInRole方法实施的
我们可以使用management.security.enabled=false来禁用安全性
只有在执行机构端点在防火墙后访问时,才建议禁用安全性
Spring提供了一种使用ControllerAdvice处理异常的非常有用的方法,我们可以通过实现一个ControllerAdvice类,来处理控制器类抛出的所有异常
WebSocket是一种计算机通信协议,通过单个tcp连接提供全双工通信信道
WebSocket是双向的,使用WebSocket客户端或服务器可以发起消息发送
WebSocket是全双工的,客户端和服务器通信是相互独立的
单个tcp连接,初始连接使用http,然后将此连接升级到基于套接字的连接,然后这个单一连接用于所有未来的通信Light,与http相比,WebSocket消息i数据交换要轻得多
第一阶段:准备阶段;第二阶段:提交阶段。
事务协调者(事务管理器)给每个参与者(资源管理器)发送 Prepare 消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的 redo 和 undo 日志,但不提交,到达一种“万事俱备,只欠东风”的状态。
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
将提交分成两阶段进行的目的很明确,就是尽可能晚地提交事务,让事务在提交前尽可能地完成所有能完成的工作。
相同点:
不同点:
在路由中定义变量规则后,通常我们需要在处理方法(也就是@RequestMapping注解的方法)中获取这个URL的具体值,并根据这个值(例如用户名)做相应的操作,SpringMVC提供的@PathVariable可以帮助我们:
@RequestMapping(value=“/user/{username}”)
public String userProfile(@PathVariable(value=“username”) String username) {
return “user”+username;
}
在上面的例子中,当@Controller处理HTTP请求时,userProfile的参数username会自动设置为URL中对应变量username(同名赋值)的值。
在SpringMVC框架中,可以通过定义@RequestMapping来处理URL请求。和@PathVariable一样,需要在处理URL的函数中获取URL中的参数,也就是?key1=value1&key2=value2这样的参数列表。通过注解@RequestParam可以轻松地将URL中的参数绑定到处理函数方法的变量中:一旦我们在方法中定义了@RequestParam变量,如果访问的URL中不带有相应的参数,就会抛出异常——这是显然的,Spring尝试帮我们进行绑定,然而没有成功。但有的时候,参数确实不一定永远都存在,这时我们可以通过定义required属性:@RequestParam(value = “username”,required = false)
@RequestParam和@PathVariable都能够完成类似的功能——因为本质上,它们都是用户的输入,只不过输入的部分不同,一个在URL路径部分,另一个在参数部分。要访问一篇博客文章,这两种URL设计都是可以的:
通过@PathVariable,例如/blogs/1
通过@RequestParam,例如blogs?blogId=1
那么究竟应该选择哪一种呢?建议:
1、当URL指向的是某一具体业务资源(或资源列表),例如博客,用户时,使用@PathVariable
2、当URL需要对资源或者资源列表进行过滤,筛选时,用@RequestParam
@RequestPart 接收文件以及其他更为复杂的数据类型
比如 XXX(@RequestPart(“file”) MultipartFile file, @RequestPart(“userVO”) UserVO userVO) 复杂协议
@RequestBody
主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的);而最常用的使用请求体传参的无疑是POST请求了,
所以使用@RequestBody接收数据时,一般都用POST方式进行提交。在后端的同一个接收方法里,@RequestBody与@RequestParam()
可以同时使用,@RequestBody最多只能有一个,而@RequestParam()可以有多个。
RequestBody 接收的是请求体里面的数据;而RequestParam接收的是key-value里面的参数
注解主要用于实体类的属性上,作用可以简单的理解为在反序列化的时候给属性重命名(多一个名字来识别)
通常⼀个 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 执⾏结果返回。
select|insert|updae|delete 标签之外,还有哪些标签?
、 、 、 、 ,加上动态 sql 的 9个标签,trim|where|set|foreach|if|choose|when|otherwise|bind 等,其中为 sql ⽚段标签,通过 标签引⼊ sql ⽚段, 为不⽀持⾃增的主键⽣成策略标签。
Mybatis 动态 sql 可以让我们在 Xml 映射⽂件内,以标签的形式编写动态 sql,完成逻辑判断和动态拼接 sql 的功能,Mybatis 提供了 9 种动态 sql 标签trim|where|set|foreach|if|choose|when|otherwise|bind 。
有没用过Mybatis一级缓存,能否介绍下
一级缓存的作用域是SQLSession,同一个SqlSession中执行相同的SQL查询(相同的SQL和参数),第一次会去查询数据库并写在缓存中,第二次会直接从缓存中取
基于PerpetualCache 的 HashMap本地缓存,默认开启一级缓存
失效策略:当执行SQL时候两次查询中间发生了增删改的操作,即insert、update、delete等操作commit后会清空该SQLSession缓存; 比如sqlsession关闭,或者清空等
有没用过Mybatis二级缓存,能否介绍下
二级缓存是namespace级别的,多个SqlSession去操作同一个namespace下的Mapper的sql语句,多个SqlSession可以共用二级缓存,如果两个mapper的namespace相同,(即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中,但是最后是每个Mapper单独的名称空间)
基于PerpetualCache 的 HashMap本地缓存,可自定义存储源,如 Ehcache/Redis等
默认是没有开启二级缓存
操作流程:
第一次调用某个namespace下的SQL去查询信息,查询到的信息会存放该mapper对应的二级缓存区域。
第二次调用同个namespace下的mapper映射文件中,相同的sql去查询信息,会去对应的二级缓存内取结果
失效策略:执行同个namespace下的mapepr映射文件中增删改sql,并执行了commit操作,会清空该二级缓存
注意:实现二级缓存的时候,MyBatis建议返回的POJO是可序列化的, 也就是建议实现Serializable接口
缓存淘汰策略:会使用默认的 LRU 算法来收回(最近最少使用的)
一级缓存和二级缓存同时启用,查询顺序是怎样的?
优先查询二级缓存-》查询一级缓存-》数据库
什么是Mybatis3.X的懒加载?
哪些查询配置支持懒加载
<resultMap id="VideoOrderResultMapLazy" type="VideoOrder">
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="out_trade_no" property="outTradeNo"/>
<result column="create_time" property="createTime"/>
<result column="state" property="state"/>
<result column="total_fee" property="totalFee"/>
<result column="video_id" property="videoId"/>
<result column="video_title" property="videoTitle"/>
<result column="video_img" property="videoImg"/>
<association property="user" javaType="User" column="user_id" select="findUserByUserId"/>
resultMap>
<select id="queryVideoOrderListLazy" resultMap="VideoOrderResultMapLazy">
select
o.id id,
o.user_id ,
o.out_trade_no,
o.create_time,
o.state,
o.total_fee,
o.video_id,
o.video_title,
o.video_img
from video_order o
select>
<select id="findUserByUserId" resultType="User">
select * from user where id=#{id}
select>
parameterType为输入参数,在配置的时候,配置相应的输入参数类型即可。parameterType有基本数据类型和复杂的数据类型配置。
1.基本数据类型,如输入参数只有一个,其数据类型可以是基本的数据类型,也可以是自己定的类类型。包括int,String,Integer,Date,如下:
select from user where id = #{id}
说下Cookie和Session的区别和联系
cookie数据保存在客户端,session数据保存在服务端
cookie不是很安全,容易泄露,不能直接明文存储信息
Cookie大小和数量存储有限制
你们公司C端业务登录的是怎样做的(业务量大,集群部署)
部分业务是采用redis替代本身的tomcat单机session (业务需要高度可控)
还有其他业务是使用JSON Web token (C端普通业务)
JWT 是一个开放标准,它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名
JWT格式组成 头部、负载、签名
header+payload+signature
头部:主要是描述签名算法
负载:主要描述是加密对象的信息,如用户的id等,也可以加些规范里面的东西,如iss签发者,exp 过期时间,sub 面向的用户
签名:主要是把前面两部分进行加密,防止别人拿到token进行base解密后篡改token
简单来说: 就是通过一定规范来生成token,然后可以通过解密算法逆向解密token,这样就可以获取用户信息
为啥使用这个呢,有什么优缺点
优点
缺点
生成的token,在客户端或者浏览器是怎么存储的
可以存储在cookie,localstorage和sessionStorage里面
问题来源
JWT令牌保存在客户端,会存在过期时间,那么如果令牌一直没有变化,那么过期时间也不会发生变化。假设一个JWT令牌的过期时间是5天,
但是用户在这5天内一直在使用本系统,那么理论上当到了第五天的时候就应该是自动对这个令牌进行续期操作,而不是让用户重新登录。
解决办法
双令牌机制
设置长短日期的两个令牌,两个令牌都传给客户端,客户端每次携带两个令牌请求
当两个令牌都没有过期的时候,服务端正常验证逻辑
如果短令牌过期,长令牌没有过期,那么服务端重新生成两个新的令牌返回给客户端,客户端下次就带着新的令牌请求,完成了令牌的自动刷新。
缓存令牌机制
服务端不仅将令牌返回给客户端,同时将令牌缓存到Redis中,缓存时间是客户端令牌的过期时间的一倍
如果客户端令牌过期了,但是Redis中的没有过期,那么就生成一个新的令牌返回给客户端,完成自动的令牌续期
如果两者都过期了,那么就让用户重新登录。
SSO(Single Sign On)即单点登录说的是⽤户登陆多个⼦系统的其中⼀个就有权访问与其相关的其他系统。举个例⼦我们在登陆了京东⾦融之后,我们同时也成功登陆京东的京东超市、京东家电等⼦系统
OAuth 是⼀个⾏业的标准授权协议,主要⽤来授权第三⽅应⽤获取有限的权限。⽽ OAuth 2.0是 对 OAuth 1.0 的完全重新设计,OAuth 2.0更快,更容易实现,OAuth 1.0 已经被废弃。实际上它就是⼀种授权机制,它的最终⽬的是为第三⽅应⽤颁发⼀个有时效性的令牌 token,使得第三⽅应⽤能够通过该令牌获取相关的资源。
OAuth 2.0 比较常⽤的场景就是第三⽅登录,当你的⽹站接⼊了第三⽅登录的时候⼀般就是使⽤的 OAuth 2.0 协议。另外,现在OAuth 2.0也常⻅于⽀付场景(微信⽀付、⽀付宝⽀付)和开发平台(微信开放平
台、阿⾥开放平台等等)。
Kafka不能脱离zookeeper单独使用,因为kafka使用zookeeper管理和协调kafka的节点服务器(3.0可以脱离zookeeper)
kafka有两种数据保存策略:按照过期时间保留和按照存储的消息大小保留
这个时候kafka会执行数据清除工作,时间和大小不论那个满足条件,都会清空数据
kafka将消息以topic为单位进行归纳
将向kafka topic发布消息的程序称为producers
将预定topics并消费消息的程序称为consumer
kafka以集群的方式运行,可以由一个或多个服务组成,每个服务叫做一个broker
producers通过网络将消息发送到kafka集群,集群向消费者提供消息
数据传输的事务定义通常有以下三种级别:
producer直接将数据发送到broker的leader(主节点),不需要再多个节点进行分发,为了帮助prodecer做到这点,所有kafka节点都可以及时的告知,那些节点是活动的,目标topic目标分区的leader在哪,这样producer就可以直接将消息发送到目的地了
kafka consumer消费消息时,向broker发出"fetch"请求去消费特定分许的消息,consumer指定消息在日志中的偏移量(offset),就可以消费从这个位置开始的消息,consumer拥有了offset的控制权,可以向后回滚去重新消费之前的消息
kafka最初考虑的问题是,consumer应该从broker拉去消息还是broker将消息推送到consumer,也就是pull还是push,在这方面,kafka遵循了一种大部分消息系统共同的传统的设计:producer将消息推送到broker,consumer从broker拉取消息
一些消息系统比如scribe和 apache flume 采用了push模式,将消息推送到下游的consumer,这样做有好处也有坏处,由broker决定消息的速率,对于不同消费塑料的consumer就不太好处理了,消息系统都致力于让consumer以最大的速率最快速的消费消息,但不幸的是.push模式下,当broker推送的速率远大于comsumer消费的速率时,consumer恐怕就要崩溃了,最终kafka还是选取了传统的pull模式
pull模式的另外个好处是consumer可自主决定是否批量的从broker拉去数据,push模式必须在不知道下游consumer消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送,如果为了避免consumer奔溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费,pull模式下,consumer就可以根据自己的消费能力去决定这些策略
pull有个缺点是,如果broker没有可供消费的消息,将大道至consumer不断在循环中轮询,知道新消息到达,为了避免这一些,kafka有个参数可以让consumer阻塞直到新的消息到达(当然也可以阻塞直到消息的数量达到某个特定的量这样就可以批量发)
消息由一个固定长度的头部和可变长度的字节数组组成,头部包含了一个版本号和CRC32校验码
消息长度:4bytes
版本号:1byte
CRC校验码:4bytes
具体的消息:n bytes
topic中的多个partition以文件夹的形式保存到broker,每个分区序号从0递增,且消息有序
parition文件下有多个segment(xxx.index,xxx.log)
segment文件里的大小和配置文件大小一致可以根据要求修改,默认为1g
如果大小大于1g时,会滚动一个新的segemtn并且以上一个segmen最后一条消息的偏移量命名
request.required.acks有三个值0,1,-1
0:生产者不会等带broker的ack,这个延迟最低但是存储的保证最弱,当server挂掉的时候就会丢数据
1:服务端会等待ack值leader副本确认接收到消息会发送ack但是如果leader挂掉后它不确保是否复制完成新laader也会导致数据丢失
01:同样在1的基础上,服务端会等所有follower的副本收到数据后才会收到leader发出的ack,这样数据不会丢失
消费者每次消费数据的时候,消费者都会记录消费的物理偏移量(offset)的位置等到下次消费时,它会接着上次位置继续消费
一个消费者组中的一个分片对应一个消费者成员,它能保证消费者成员都能访问,如果组中成员太多会有空闲的成员
一个消费者组里它的内部是有序的,消费者组与消费者之间是无序的
生产者决定数据产生到集群的哪一个pariton中,每个消息都是以key value格式,key是由生产者发送数据传入,所以生产者(key)决定了数据产生到集群的那个parition
在启动kafka集群之前,我们需要配置好log.dirs参数,其值是kafka数据的存放目录,这个参数可以配置多个目录,目录之间使用逗号分割,通过这些目录是分布在不同的磁盘上用于提高读写性能
当然我们也可以配置log,dir参数,含义一样,只需要设置设其中一个即可
如果log.dirs参数只配置了一个目录,那么分配到各个broker上的分区肯定只能在这个目录下创建文件夹用于存放数据
但是如果log,dirs参数配置了多个目录,那么kafka会在那个文件夹中创建目录呢?
答案是:kafka会在含有分区目录最少的文件夹中创建新的分区目录,分区目录名为Topic名+分区ID,注意,是分区文件夹最少的目录,而不会磁盘使用量最少的目录,也就是说,如果你给log,dirs参数新增了一个新的磁盘,新的分区目录肯定实先在这个新的磁盘上创建直到这个新的磁盘目录拥有的分区目录不是最少的为之
主要有random(随机,这种是默认的负载均衡策略)、RoundRobin (轮询)、LeastActive (最少活跃数)、ConsistentHash(一致性hash)可以在暴露服务的时候使用loadbalance进行指定。
随机:在一个截面上碰撞的概率高,调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。
轮询:存在慢的提供者累积请求的问题,一台机器很慢,但没挂,当请求轮询到那台机子就卡在那,久而久之,所有请求都卡在那台服务器上。
最少活跃数:使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
一致性hash:当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。可以方便节点的增加及移除
dubbo在调用服务不成功时,默认是会重试两次的。这样在服务端的处理时间超过了设定的超时时间时,就会有重复请求,此时在接口设计的时候,需要考虑接口的幂等性,避免重复调用导致出现脏数据。
Dubbo基于生产者、消费者的模式,
首先服务容器负责启动,加载,运行服务提供者。
服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者在启动时,向注册中心订阅自己所需的服务。
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
优点:解耦系统、异步化、削峰
缺点: 系统可用性降低、复杂度增高、维护成本增高
主流消息队列Apache ActiveMQ、Kafka、RabbitMQ、RocketMQ
ActiveMQ:http://activemq.apache.org/
Apache出品,历史悠久,支持多种语言的客户端和协议,支持多种语言Java, .NET, C++ 等,基于JMS Provider的实现
缺点:吞吐量不高,多队列的时候性能下降,存在消息丢失的情况,比较少大规模使用
Kafka:http://kafka.apache.org/
是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理大规模的网站中的所有动作流数据(网页浏览,搜索和其他用户的行动),副本集机制,实现数据冗余,保障数据尽量不丢失;支持多个生产者和消费者
缺点:不支持批量和广播消息,运维难度大,文档比较少, 需要掌握Scala
RabbitMQ:http://www.rabbitmq.com/
是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不错
缺点:使用Erlang开发,阅读和修改源码难度大
RocketMQ:http://rocketmq.apache.org/
阿里开源的一款的消息中间件, 纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点, 性能强劲(零拷贝技术),支持海量堆积, 支持指定次数和时间间隔的失败消息重发,支持consumer端tag过滤、延迟消息等,在阿里内部进行大规模使用,适合在电商,互联网金融等领域使用
缺点:成熟的资料相对不多,社区处于新生状态但是热度高
A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E系统也要这个数据呢?那如果 C系统现在不需要了呢?A 系统负责崩溃在这个场景中,A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。A 系统要时时刻刻考虑 BCDE 四个系统如果挂了该咋办?要不要重发,要不要把消息存起来? 如果使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护 这个代码,也不需要考虑人家是否调用成功、失败超时等情况。 通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,A 系统就跟其它系统彻底解耦了。
再来看一个场景,A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200= 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求,等待个 1s,这几乎是不可接受的。如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,对于用户而言,其实感觉上就是点个按钮,8ms 以后就直接返回了,爽!网站做得真好,真快!
一般的 MySQL,扛到每秒 2k 个请求就差不多了,如果每秒请求到 5k 的话,可能就直接把MySQL 给打死了,导致系统崩溃,用户也就没法再使用系统了。但是高峰期一过,到了下午的时候,就成了低峰期,可能也就 1w 的用户同时在网站上操作,每秒中的请求数量可能也就 50 个请求,对整个系统几乎没有任何的压力。如果使用 MQ,每秒 5k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟
最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok,这样下来,哪怕是高峰期的时候,A 系统也绝对不会挂掉。而MQ 每秒钟 5k 个请求进来,就 2k 个请求出去,结果就导致在中午高峰期(1 个小时),可能有几十万甚至几百万的请求积压在 MQ 中。这个短暂的高峰期积压是 ok 的,因为高峰期过了之后,每秒钟就 50 个请求进 MQ,但是 A 系统依然会按照每秒 2k 个请求的速度在处理。所以说,只要高峰期一过,A 系统就会快速将积压的消息给解决掉。
发送方式一般分三种
Producer 将消息发送到消息队列broker服务端,但并不期望这条消息立马投递,而是推迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费
使用场景一:通过消息触发一些定时任务,比如在某一固定时间点向用户发送提醒消息
使用场景二:消息生产和消费有时间窗口要求,定时关闭订单。比如在天猫电商交易中超时未支付关闭订单的场景,在订单创建时会发送一条 延时消息。这条消息将会在 30 分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。 如支付未完成,则关闭订单。如已完成支付则忽略
什么是顺序消息:
消息的生产和消费顺序一致
全局顺序:topic下面全部消息都要有序(少用),性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景,并行度成为消息系统的瓶颈, 吞吐量不够
使用场景:在证券处理中,以人民币兑换美元为例子,在价格相同的情况下,先出价者优先处理,则可以通过全局顺序的方式按照 FIFO 的方式进行发布和消费
局部顺序:只要保证一组消息被顺序消费即可,性能要求高
使用场景:电商的订单创建,同一个订单相关的创建订单消息、订单支付消息、订单退款消息、订单物流消息、订单交易成功消息 都会按照先后顺序来发布和消费(阿里巴巴集团内部电商系统均使用局部顺序消息,既保证业务的顺序,同时又能保证业务的高性能)
下面是用RocketMQ举例(用kafka或rabbitmq类似)
一个topic下面有多个queue
顺序发布:对于指定的一个 Topic,客户端将按照一定的先后顺序发送消息
举例:订单的顺序流程是:创建、付款、物流、完成,订单号相同的消息会被先后发送到同一个队列中,
根据MessageQueueSelector里面自定义策略,根据同个业务id放置到同个queue里面,如订单号取模运算再放到selector中,同一个模的值都会投递到同一条queue
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
//如果是订单号是字符串,则进行hash,得到一个hash值
Long id = (Long) arg;
long index = id % mqs.size();
return mqs.get((int)index);
}
顺序消费:对于指定的一个 Topic,按照一定的先后顺序接收消息,即先发送的消息一定会先被客户端接收到。
举例:消费端要在保证消费同个topic里的同个队列,不应该用MessageListenerConcurrently,
应该使用MessageListenerOrderly,自带单线程消费消息,不能在Consumer端再使用多线程去消费,消费端分配到的queue数量是固定的,集群消费会锁住当前正在消费的队列集合的消息,所以会保证顺序消费。
注意:
顺序消息暂不支持广播模式
顺序消息不支持异步发送方式,否则将无法严格保证顺序
不能再Consumer端再使用多线程去消费
幂等性:一个请求,不管重复来多少次,结果是不会改变的。
RabbitMQ、RocketMQ、Kafka等任何队列不保证消息不重复,如果业务需要消息不重复消费,则需要消费端处理业务消息要保持幂等性
//Redis中操作,判断是否已经操作过 TODO
boolean flag = jedis.setNX(key);
if(flag){
//消费
}else{
//忽略,重复消费
}
int num = jedis.incr(key);
if(num == 1){
//消费
}else{
//忽略,重复消费
}
消息可靠性传输,是非常重要,消息如果丢失,可能带来严重后果,一般从三个角度去分析
修复consumer, 然后慢慢消费?也需要几小时才可以消费完成,新的消息怎么办?
核心思想:紧急临时扩容,更快的速度去消费数据
使用场景:单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象(比如数据源,session工厂),使用单例模式可以提高系统性能
单例设计模式八种方式:
饿汉式(静态常量):基于类加载机制,避免了多线程的同步问题,但是也导致了没有达到懒加载效果,从而造成性能浪费
饿汉式(静态代码块):同上
懒汉式(线程不安全):起到了懒加载的目的,但是不能保证多个线程同时进入if判断,所以线程不安全,不要使用这种方式
if(instance==null){
instance = new Singleton();
}
懒汉式(线程安全,同步方法):解决了线程安全问题,不过由于对静态方法加锁实际上是对整个类加锁,效率不高
懒汉式(同步代码块):不推荐使用
双重检查:延迟加载效率高,推荐使用
静态内部类:采用了类加载机制来保证实例时只有一个线程,JVM保证了线程的安全性
枚举:不仅避免多线程同步问题,而且还能防止反序列化重新创建新的对象