面向对象(封装,继承,多态);
平台无关性,平台无关性的具体表现在于,Java 是“一次编写,到处运行(Write Once,Run any Where)”的语言,因此采用 Java 语言编写的程序具有很好的可移植性,而保证这一点的正是 Java 的虚拟机机制。在引入虚拟机之后,Java 语言在不同的平台上运行不需要重新编译。
可靠性、安全性;
支持多线程。C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持;
支持网络编程并且很方便。Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便;
编译与解释并存;
都是面向对象的语言,都支持封装、继承和多态;
支持指针,而 Java 没有指针的概念;
C++ 支持多继承,而 Java 不支持多重继承,但允许一个类实现多个接口;
Java 自动进行无用内存回收操作,不再需要程序员进行手动删除,而 C++ 中必须由程序释放内存资源,这就增加了程序员的负担。
Java 不支持操作符重载,操作符重载则被认为是 C++ 的突出特征;
Java 是完全面向对象的语言,并且还取消了 C/C++ 中的结构和联合,使编译程序更加简洁;
C 和 C++ 不支持字符串变量,在 C 和 C++ 程序中使用“Null”终止符代表字符串的结束。在 Java 中字符串是用类对象(String 和 StringBuffer)来实现的;
goto 语句是 C 和 C++ 的“遗物”,Java 不提供 goto 语句,虽然 Java 指定 goto 作为关键字,但不支
持它的使用,这使程序更简洁易读;
Java之所以可以“一次编译,到处运行”,一是因为JVM针对各种操作系统、平台都进行了定制,二是因为无论在什么平台,都可以编译生成固定格式的字节码(.class文件)供JVM使用。因此,也可以看出字节码对于Java生态的重要性。
之所以被称之为字节码,是因为字节码文件由十六进制值组成,而JVM以两个十六进制值为一组,即以字节为单位进行读取。在Java中一般是用javac命令编译源代码为字节码文件。
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
Oracle JDK 版本将每三年发布一次,而 OpenJDK 版本每三个月发布一次;
OpenJDK 是一个参考模型并且是完全开源的,而 Oracle JDK 是OpenJDK 的一个实现,并不是完全开源的;
Oracle JDK 比 OpenJDK 更稳定。OpenJDK 和 Oracle JDK 的代码几乎相同,但 Oracle JDK 有更多的类和一些错误修复。因此,如果您想开发企业/商业软件,建议选择 Oracle JDK,因为它经过了彻底的测试和稳定。某些情况下,有些人提到在使用 OpenJDK 可能会遇到了许多应用程序崩溃的问题,但是,只需切换到 Oracle JDK 就可以解决问题;
在响应性和 JVM 性能方面,Oracle JDK 与 OpenJDK 相比提供了更好的性能;
Oracle JDK 不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本;
Oracle JDK 根据二进制代码许可协议获得许可,而 OpenJDK 根据 GPLv2 许可获得许可
Java5 以前 switch(expr)中,expr 只能是 byte、short、char、int。
从 Java 5 开始,Java 中引入了枚举类型, expr 也可以是 enum 类型。
从 Java 7 开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。
default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
private : 在同一类内可见。使用对象:变量、方法。注意:不能修饰类(外部类)
public : 对所有类可见。使用对象:类、接口、变量、方法
protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。注意:不能修饰类(外
部类)。
break 跳出总上一层循环,不再执行循环(结束当前的循环体)
continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)
return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)
final 用于修饰变量、方法和类。
finally 作为异常处理的一部分,它只能在 try/catch 语句中,并且附带一个语句块表示这段语句最终一定被执行(无论是否抛出异常),经常被用在需要释放资源的情况下, System.exit (0) 可以阻断finally 执行。
finalize 是在 java.lang.Object 里定义的方法,也就是说每一个对象都有这么个方法,这个方法在gc 启动,该对象被回收的时候被调用。
一个对象的 finalize 方法只会被调用一次,finalize 被调用不一定会立即回收该对象,所以有可能调用finalize 后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会再次调用 finalize 了,进而产生问题,因此不推荐使用 finalize 方法。
基本上代码块分为三种:Static静态代码块、构造代码块、普通代码块
代码块执行顺序静态代码块——> 构造代码块 ——>构造函数——> 普通代码块
继承中代码块执行顺序:父类静态块——>子类静态块——>父类代码块——>父类构造器——>子类代码块——>子类构造器
代码块先于构造器,父类先于子类
面向过程:
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
缺点:没有面向对象易维护、易复用、易扩展。
面向对象:
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
缺点:性能比面向过程低
封装。封装最好理解了。封装是面向对象的特征之一,是对象和类概念的主要特性。封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
继承。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。
多态性。它是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。
本质上多态分两种:
1、编译时多态(又称静态多态)
2、运行时多态(又称动态多态)
重载(overload)就是编译时多态的一个例子,编译时多态在编译时就已经确定,运行的时候调用的是确定的方法。
我们通常所说的多态指的都是运行时多态,也就是编译时不确定究竟调用哪个具体方法,一直延迟到运行时才能确定。这也是为什么有时候多态方法又被称为延迟方法的原因。
Java实现多态有 3 个必要条件:继承、重写和向上转型。只有满足这 3 个条件,开发人员才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而执行不同的行为。
继承:在多态中必须存在有继承关系的子类和父类。
重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才既能可以调用父类的方法,又能调用子类的方法。
时的多态性。
关,即重载的方法不能根据返回类型进行区分。即外壳不变,核心重写!
同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表最常用的地方就是
构造器的重载。
抽象类可以提供成员方法的实现细节,而接口中只能存在public abstract 方法;
抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的接口中不能含有静态代码块以及静态方法而抽象类可以有静态代码块和静态方法;
一个类只能继承一个抽象类,而一个类却可以实现多个接口。
抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽
象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是
一种辐射式设计。
所以 final 不能修饰抽象类
java中提供了以下四种创建对象的方式:
new创建新对象
通过反射机制
采用clone机制
通过序列化机制
前两者都需要显式地调用构造方法。对于clone机制,需要注意浅拷贝和深拷贝的区别,对于序列化机制
需要明确其实现原理,在java中序列化可以通过实现Externalizable或者Serializable来实现。
值传递:指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。
引用传递:指的是在方法调用时,传递的参数是按引用进行传递,其实传递的是引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。
基本类型作为参数被传递时肯定是值传递;引用类型作为参数被传递时也是值传递,只不过“值”为对应的引用。
== 常用于相同的基本数据类型之间的比较,也可用于相同类型的对象之间的比较;
如果 == 比较的是基本数据类型,那么比较的是两个基本数据类型的值是否相等;
如果 == 是比较的两个对象,那么比较的是两个对象的引用,也就是判断两个对象是否指向了同一块内存区域;
equals方法主要用于两个对象之间,检测一个对象是否等于另一个对象
情况1,类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这
两个对象。
情况2,类覆盖了equals()方法。一般,我们都覆盖equals()方法来两个对象的内容相等;若它们的
内容相等,则返回true(即,认为这两个对象相等)。
自反性对于任意不为null的引用值x,x.equals(x)一定是true。
对称性对于任意不为null的引用值x和y,当且仅当x.equals(y)是true时,y.equals(x)也是true。
传递性对于任意不为null的引用值x、y和z,如果x.equals(y)是true,同时y.equals(z)是true,那
么x.equals(z)一定是true。
一致性对于任意不为null的引用值x和y,如果用于equals比较的对象信息没有被修改的话,多次
调用时x.equals(y)要么一致地返回true要么一致地返回false。对于任意不为null的引用值x,x.equals(null)返回false。
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是
确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
以“HashSet如何检查重复”为例子来说明为什么要有 hashCode:
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。
Java 对于 eqauls() 方法和 hashCode() 方法是这样规定的:
同一对象上多次调用 hashCode() 方法,总是返回相同的整型值。
如果 a.equals(b),则一定有 a.hashCode() 一定等于 b.hashCode()。
如果 !a.equals(b),则 a.hashCode() 不一定等于 b.hashCode()。此时如果 a.hashCode() 总是不等
于 b.hashCode(),会提高 hashtables 的性能。
a.hashCode()==b.hashCode() 则 a.equals(b) 可真可假
a.hashCode()!= b.hashCode() 则 a.equals(b) 为假。
上面结论简记:
如果两个对象 equals,Java 运行时环境会认为他们的 hashCode 一定相等。
如果两个对象不 equals,他们的 hashCode 有可能相等。
如果两个对象 hashCode 相等,他们不一定 equals。
如果两个对象 hashCode 不相等,他们一定不 equals
补充:关于 equals() 和 hashCode() 的重要规范
规范1:若重写 equals() 方法,有必要重写 hashcode()方法,确保通过 equals()方法判断结果为
true 的两个对象具备相等的 hashcode() 方法返回值。说得简单点就是:“如果两个对象相同,那么
他们的 hashCode 应该相等”。不过请注意:这个只是规范,如果非要写一个类让 equals() 方法返
回 true 而 hashCode() 方法返回两个不相等的值,编译和运行都是不会报错的。不过这样违反了
Java 规范,程序也就埋下了 BUG。
规范2:如果 equals() 方法返回 false,即两个对象“不相同”,并不要求对这两个对象调用
hashCode() 方法得到两个不相同的数。说的简单点就是:“如果两个对象不相同,他们的
hashCode 可能相同”。
判断的时候**先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。**如果只重写了
equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果
为true。
在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这
时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存
储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到
不同的位置而造成对象的不能覆盖的问题。
1.可变与不可变。String类中使用字符数组保存字符串,因为有“final”修饰符,所以string对象是不可变
的。对于已经存在的String对象的修改都是重新创建一个新的对象,然后把新的值保存进去
符数组保存字符串,这两种对象都是可变的。
2.是否线程安全。
String中的对象是不可变的,也就可以理解为常量,显然线程安全。
StringBuilder是非线程安全的。
StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
3.如果你只是在单线程中使用字符串缓冲区,那么StringBuilder的效率会更高些。值得注意的是StringBuilder是在JDK1.5版本中增加的。以前版本的JDK不能使用该类。
1.便于实现字符串池(String pool)
在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造
成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初
始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已
经存在了的字符串的引用。
2.使多线程安全
在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全
的,不可变对象不能被写,所以保证了多线程的安全。
3.避免安全问题
在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制
所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字
符串指向对象的值,那么会引起很严重的安全问题。
4.加快字符串处理速度
由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存
了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以
HashMap中的键往往都使用String。
形式上: 字符常量是单引号引起的一个字符,字符串常量是双引号引起的若干个字符;
含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算;字符串常量代表一个地址值
(该字符串在内存中存放位置,相当于对象;
在Java中占两个字节)
池,当需要使用字符串时,先去字符串池中查看该字符串是否已经存在,如果存在,则可以直接使用,
如果不存在,初始化,并将该字符串放入字符串常量池中。
串放进常量池,再将其地址赋给a;若有,将找到的”aaa”字符串的地址赋给a。
将”aaa”字符串放入常量池,相当于创建了两个对象,无论常量池中有没有”aaa”字符串,程序都会
在堆内存中开辟一片新空间存放新对象。
不是。Java 中的基本数据类型只有 8 个 :byte、short、int、long、float、double、char、boolean;
除了基本类型(primitive type),剩下的都是引用类型(referencetype),Java 5 以后引入的枚举类
型也算是一种比较特殊的引用类型。
建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并
频繁访问时,可以保证数据的一致性;
会直接返回缓存的引用;
当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。
从 Java 5 开始引入了自动装箱/拆箱机制,把基本类型转换成包装类型的过程叫做装箱(boxing);反
之,把包装类型转换成基本类型的过程叫做拆箱(unboxing),使得二者可以相互转换。
Java 为每个原始类型提供了包装类型:
原始类型: boolean,char,byte,short,int,long,float,double
包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
包装类型可以为 null,而基本类型不可以。
包装类型可用于泛型,而基本类型不可以。泛型不能使用基本类型,因为使用基本类型时会编
译出错。
因为泛型在编译时会进行类型擦除,最后只保留原始类型,而原始类型只能是 Object 类及其子类
——基本类型是个特例。
基本类型比包装类型更高效。基本类型在栈中直接存储的具体数值,而包装类型则存储的是堆中
的引用。 很显然,相比较于基本类型而言,包装类型需要占用更多的内存空间。
public class Test {
public static void main(String[] args) {
// 声明一个Integer对象,用到了自动的装箱:解析为:
Integer num = Integer.valueOf(9);
Integer num = 9; } }
9是属于基本数据类型的,原则上它是不能直接赋值给一个对象Integer的。但jdk1.5 开始引入了自动装
箱/拆箱机制,就可以进行这样的声明,自动将基本数据类型转化为对应的封装类型,成为一个对象以后
就可以调用对象所声明的所有的方法。
public class Test {
public static void main(String[] args) {
/ /声明一个Integer对象
Integer num = 9;
// 进行计算时隐含的有自动拆箱
System.out.print(num--);
} }
因为对象时不能直接进行运算的,而是要转化为基本数据类型后才能进行加减乘除。
Integer是int的包装类;int是基本数据类型;
Integer变量必须实例化后才能使用;int变量不需要;
Integer实际是对象的引用,指向此new的Integer对象;int是直接存储数据值 ;
Integer的默认值是null;int的默认值是0。
由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相
等的(因为new生成的是两个对象,其内存地址不同)。
Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true
非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的
Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内
存中的地址不同)
Integer b = new Integer(10000);
Integer c=10000;
System.out.println(b == c); // false
对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结
果为true,如果两个变量的值不在此区间,则比较结果为false
当值在 -128 ~ 127之间时,java会进行自动装箱然后会对值进行缓存,如果下次再有相同的值,会直
接在缓存中取出使用。缓存是通过Integer的内部类IntegerCache来完成的。当值超出此范围,会在堆中
new出一个对象来存储。
反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都
能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言
的反射机制。
优点:能够运行时动态获取类的实例,提高灵活性;可与动态编译结合
Class.forName(‘com.mysql.jdbc.Driver.class’); ,加载MySQL的驱动类。
缺点:使用反射性能较低,需要解析字节码,将内存中的对象进行解析。
Class.forName(“类的路径”);当你知道该类的全路径名时,你可以使用该方法获取 Class 类对象。
类名.class这种方法只适合在编译前就知道操作的 Class。
对象名.getClass()。
反射 API 用来生成 JVM 中的类、接口或则对象的信息。
Class 类:反射的核心类,可以获取类的属性,方法等信息。
Field 类:Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。
Method 类:Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行
方法。
Constructor 类:Java.lang.reflec 包中的类,表示类的构造方法。
获取想要操作的类的Class对象这是反射的核心,通过Class对象我们可以任意调用类的方法。
调用 Class 类中的方法既就是反射的使用阶段。
使用反射 API 来操作这些信息。
反射让开发人员可以通过外部类的全路径名创建对象,并使用这些类,实现一些扩展的功能。
反射让开发人员可以枚举出类的全部成员,包括构造函数、属性、方法以帮助开发者写出正确的代码。
测试时可以利用反射 API 访问类的私有成员,以保证测试代码覆盖率。
也就是说,Oracle 希望开发者将反射作为一个工具,用来帮助程序员实现本不可能实现的功能。
在JDBC 的操作中,如果要想进行数据库的连接,则必须按照以上的几步完成
通过Class.forName()加载数据库的驱动程序 (通过反射加载,前提是引入相关了Jar包);
通过 DriverManager 类进行数据库的连接,连接的时候要输入数据库的连接地址、用户名、密码;
通过Connection 接口接收连接。
Spring 中的 IOC 的底层实现原理就是反射机制,Spring 的容器会帮我们创建实例,该容器中使用的方法就是反射,通过解析xml文件,获取到id属性和class属性里面的内容,利用反射原理创建配置文件里类的实例对象,存入到Spring的bean容器中。
Spring这样做的好处是:
不用每一次都要在代码里面去new或者做其他的事情;
以后要改的话直接改配置文件,代码维护起来就很方便了;
有时为了适应某些需求,Java类里面不一定能直接调用另外的方法,可以通过反射机制来实现。
泛型是 JDK1.5 的一个新特性,泛型就是将类型参数化,其在编译时才确定具体的参数。
就是将原来的具体的类型参数化,然后再使用 /调用时传入具体的类型
使用泛型的好处有以下几点:
类型安全
泛型的主要目标是提高 Java 程序的类型安全
编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常
符合越早出错代价越小原则
消除强制类型转换
泛型的一个附带好处是,使用时直接得到目标类型,消除许多强制类型转换
所得即所需,这使得代码更加可读,并且减少了出错机会
潜在的性能收益
由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改
所有工作都在编译器中完成
编译器生成的代码跟不使用泛型(和强制类型转换)时所写的代码几乎一致,只是更能确保类型安全而已
类来设定类型的上界,另一种是它通过确保类型必须是T的父类来设定类型的下界。泛型类
型必须用限定内的类型来进行初始化,否则会导致编译错误。
合,它可以是 List ,也可以是 List ,或者 List 等等
List可以接受任何T的父类构成的List。例如List可以接受List或List。
简要描述:对内存中的对象进行持久化或网络传输**,** 这个时候都需要序列化和反序列化
序列化可以将内存中的类写入文件或数据库中。
对象、文件、数据,有许多不同的格式,很难统一传输和保存。
序列化以后就都是字节流了,无论原来是什么东西,都能变成一样的东西,就可以进行通用的格式传输或保存,传输结束以后,要再次使用,就进行反序列化还原,这样对象还是对象,文件还是文件。
对象序列化可以实现分布式对象。
java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。
实现Serializable接口或者Externalizable接口
类通过实现 java.io.Serializable 接口以启用其序列化功能。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。
对于不想进行序列化的变量,使用 transient 关键字修饰。
transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后, transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。transient 只能修饰变量,不能修饰类和方法。
不会。因为序列化是针对对象而言的, 而静态变量优先于对象存在, 随着类的加载而加载, 所以不会被序列化
Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。 Throwable 类有两个重要的子类 Exception (异常)和 Error (错误)
Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。 Exception 又可以分为运行时异常(RuntimeException, 又叫非受检查异常)和非运行时异常(又叫受检查异常) 。
Error : Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获 。例如,系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复。
非受检查异常和受检查异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检查异常否则就选择非受检查异常
译器不会检查运行时异常。例如: NullPointException(空指针) 、 NumberFormatException(字符串
转换为数字) 、 IndexOutOfBoundsException(数组越界) 、 ClassCastException(类转换异常) 、
ArrayStoreException(数据存储异常,操作数组时类型不一致) 等。
异常。常见的受检查异常有**: IO 相关的异常、 ClassNotFoundException 、 SQLException 等。**
throw 关键字用在方法内部只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常
和非受查异常都可以被抛出。
throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个
方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否
则也要在方法签名中用 throws 关键字声明相应的异常。
java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象
类或者接口时抛出该异常.
java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象
时抛出该错误。
java.lang.NullPointerException:空指针异常
catch 可以省略。更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运行时异常
+普通异常。也就是说,如果你只用try去处理普通异常却不加以catch处理,编译是通不过的,因为编译
器硬性规定,普通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时
没有如此规定,所以catch可以省略,你加上catch编译器也觉得无可厚非。
会执行,在 return 前执行
线程安全的:
Hashtable:比HashMap多了个线程安全。
ConcurrentHashMap:是一种高效但是线程安全的集合。
Vector:比Arraylist多了个同步化机制。
Stack:栈,也是线程安全的,继承于Vector。
线性不安全的:
HashMap
Arraylist
LinkedList
HashSet
TreeSet
TreeMap
是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
底层数据结构: Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向循环链表数据结构;
插入和删除是否受元素位置的影响: ArrayList 采用数组存储,所以插入和删除元素的时间 复杂度受元素位置的影响。 比如:执行 add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 **LinkedList 采用链表 存储,所以插入,删除元素时间复杂度不受元素位置的影响,**都是近似 O(1)而数组为近似 O(n)。
是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而ArrayList 实现了RandmoAccess 接口,所以有随机访问功能。快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) 方法)。
内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
Vector是线程安全的,ArrayList不是线程安全的。其中,Vector在关键性的方法前面都加了synchronized关键字,来保证线程的安全性。如果有多个线程会访问到集合,那最好是使用Vector,因为不需要我们自己再去考虑和编写线程安全的代码。
ArrayList在底层数组不够用时在原来的基础上扩展0.5倍,Vector是扩展1倍,这样ArrayList就有利于节约内存空间。
ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。
默认情况下,新的容量会是原容量的1.5倍。
Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型。
Array 大小是固定的,ArrayList 的大小是动态变化的。
ArrayList 提供了更多的方法和特性,比如:addAll(),removeAll(),iterator() 等等。
在JDK1.7 和JDK1.8 中有所差别:
在JDK1.7 中,由“数组+链表”组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。
在JDK1.8 中,由“数组+链表+红黑树”组成。当链表过长,则会严重影响 HashMap 的性能,红黑树搜索时间复杂度是 O(logn),而链表是糟糕的 O(n)。因此,JDK1.8 对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:
当链表超过 8 且数据总量超过 64 才会转红黑树。
将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间
解决Hash冲突方法有:开放定址法、再哈希法、链地址法(拉链法)、建立公共溢出区。HashMap中采用的是 链地址法 。
开放定址法也称为 再散列法 ,基本思想就是,如果 p=H(key) 出现冲突时,则以 p 为基础,再次
hash, p1=H§ ,如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地
址 pi 。 因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素,而且因为存在再次
hash,所以 只能在删除的节点上做标记,而不能真正删除节点。
**再哈希法(**双重散列,多重散列),提供多个不同的hash函数,当 R1=H1(key1) 发生冲突时,再计
算 R2=H2(key1) ,直到没有冲突为止。 这样做虽然不易产生堆集,但增加了计算的时间。
链地址法(拉链法),将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希
表的第i个单元中,查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除
的情况。
建立公共溢出区,将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区。
因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡,而单链表不需要。当元素小于 8 个的时
候,此时做查询操作,链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度
是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。
因此,如果一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。
Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold是HashMap所能容纳键值对的最大值。threshold = length * Load factor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。
默认的loadFactor是0.75,0.75是对空间和时间效率的一个平衡选择
首先根据key的值计算出hashcode的值,然后根据hashcode计算出hash值,最后通过hash&(length-1)
计算出的hashcode的高16位与低16位异或运算,然后将结果与(length-1)进行与运算。
以JDK1.8为例,简要流程如下:
首先根据 key 的值计算 hash 值,找到该元素在数组中存储的下标;
如果数组是空的,则调用 resize 进行初始化;
如果没有哈希冲突直接放在对应的数组下标里;
如果冲突了,且 key 已经存在,就覆盖掉 value;
如果冲突后,发现该节点是红黑树,就将这个节点挂在树上;
如果冲突后是链表,判断该链表是否大于 8 ,如果大于 8 并且数组容量小于 64,就进行扩容;如
果链表节点大于 8 并且数组的容量大于 64,则将这个结构转换为红黑树;否则,链表插入键值
对,若 key 存在,就覆盖掉 value。
HashMap 在容量超过负载因子所定义的容量之后,就会扩容。Java 里的数组是无法自动扩容的,方法
是将 HashMap 的大小扩大为原来数组的两倍,并将原来的对象放入新的数组中。
jdk1.7扩容时会重新计算hash值,jdk1.8则不需要
jdk1.7旧链表迁移到新链表元素会倒置,是尾插法。jdk1.8不会倒置,使用尾插法。
一般用Integer、String 这种不可变类当 HashMap 当 key,而且 String 最为常用。
因为字符串是不可变的,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。这就是
HashMap 中的键往往都使用字符串的原因。
因为获取对象的时候要用到 equals() 和 hashCode() 方法,那么键对象正确的重写这两个方法是非
常重要的,这些类已经很规范的重写了 hashCode() 以及 equals() 方法。
候有可能导致环形链表的出现,形成死循环。因此,JDK1.8使用尾插法插入元素,在扩容时会保持
链表元素原本的顺序,不会出现环形链表的问题。
多线程的put可能导致元素的丢失。多线程同时执行 put 操作,如果计算出来的索引位置是相同
的,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在JDK 1.7和 JDK 1.8 中
都存在
put和get并发时,可能导致get为null。线程1执行put时,因为元素个数超出threshold而导致
rehash,线程2此时执行get,有可能导致这个问题。此问题在JDK 1.7和 JDK 1.8 中都存在。
首先,hashmap是线程不安全的,hashtable是线程安全的。Hashtable是线程安全的,它的每个方法中都加入了Synchronize方法HashMap是继承自AbstractMap类,而HashTable是继承自Dictionary类。hashtable现在基本被弃用。Hashtable既不支持Null key也不支持Null value。HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。
ConcurrentHashMap 在 JDK1.7 和 JDK1.8 的实现方式是不同的。
先来看下JDK1.7
JDK1.7中的ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即
ConcurrentHashMap 把哈希桶切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成。
其中,Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色;HashEntry
用于存储键值对数据。
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据
时,其他段的数据也能被其他线程访问,能够实现真正的并发访问。
再来看下JDK1.8
在数据结构上, JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的数组**+链表+**红黑树结
构;**在锁的实现上,抛弃了原有的 Segment 分段锁,采用 CAS + synchronized 实现更加低粒度的锁。将锁的级别控制在了更细粒度的哈希桶元素级别,也就是说只需要锁住这个链表头结点(红黑树的根节点就不会影响其他的哈希桶元素的读写,大大提高了并发度。
CAS机制:是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。
CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
从思想上来说,synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。
先来看JDK1.7
首先,会尝试获取锁,如果获取失败,利用自旋获取锁;如果自旋重试的次数超过 64 次,则改为阻塞
获取锁。
获取到锁后:
将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的
value。
为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
释放 Segment 的锁。
再来看JDK1.8
大致可以分为以下步骤:
根据 key 计算出 hash值。
判断是否需要进行初始化。
定位到 Node,拿到首节点 f,判断首节点 f:
如果为 null ,则通过cas的方式尝试添加。
如果为 f.hash = MOVED = -1 ,说明其他线程在扩容,参与一起扩容。
如果都不满足 ,synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入。
get 方法不需要加锁**。因为 Node 的元素 val 和指针 next 是用 volatile 修饰的**,在多线程环境下线程A
修改结点的val或者新增节点的时候是对线程B可见的。
没有关系。哈希桶 table 用volatile修饰主要是保证在数组扩容的时候保证可见性
我们先来说value 为什么不能为 null ,因为 ConcurrentHashMap 是用于多线程的 ,如果
map.get(key) 得到了 null ,无法判断,是映射的value是 null ,还是没有找到对应的key而为 null ,
这就有了二义性。
而用于单线程状态的 HashMap 却可以用 containsKey(key) 去判断到底是否包含了这个 null 。
ConcurrentHashMap 中的key为什么也不能为 null 的问题,源码就是这样写的。
在JDK1.7中,并发度默认是16,这个值可以在构造函数中设置。如果自己设置了并发度,
ConcurrentHashMap 会使用大于等于该值的最小的2的幂指数作为实际并发度,也就是比如你设置的值
是17,那么实际并发度是32。
HashMap迭代器是强一致性不同,ConcurrentHashMap 迭代器是弱一致性。
ConcurrentHashMap 的迭代器创建后,就会按照哈希表结构遍历每个元素,但在遍历过程中,内部元
素可能会发生变化,如果变化发生在已遍历过的部分,迭代器就不会反映出来,而如果变化发生在未遍
历过的部分,迭代器就会发现并反映出来,这就是弱一致性。
这样迭代器线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线
程并发执行的连续性和扩展性,是性能提升的关键。
数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
保证线程安全机制:JDK1.7采用Segment的分段锁机制实现线程安全,其中segment继承自
ReentrantLock。JDK1.8 采用CAS+Synchronized保证线程安全。
(Node)。
时,会将链表转化为红黑树进行存储。
ConcurrentHashMap 的效率要高于Hashtable,因为Hashtable给整个哈希表加了一把大锁从而实现线
程安全。而ConcurrentHashMap 的锁粒度更低,在JDK1.7中采用分段锁实现线程安全,在JDK1.8 中采
用 CAS+Synchronized 实现线程安全。
Hashtable是使用Synchronized来实现线程安全的,给整个哈希表加了一把大锁,多线程访问时候,只
要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景
中性能就会非常差!
jvm将虚拟机分为5大区域,程序计数器、虚拟机栈、本地方法栈、java堆、方法区;
程序计数器:线程私有的,是一块很小的内存空间,作为当前线程的行号指示器,用于记录当前虚拟机正在执行的线程指令地址;
虚拟机栈:线程私有的,每个方法执行的时候都会创建一个栈帧,用于存储局部变量表、操作数、动态链接和方法返回等信息,当线程请求的栈深度超过了虚拟机允许的最大深度时,就会抛出StackOverFlowError;
**本地方法栈:**线程私有的,保存的是native方法的信息,当一个jvm创建的线程调用native方法后,jvm不会在虚拟机栈中为该线程创建栈帧,而是简单的动态链接并直接调用该方法;
堆:java堆是所有线程共享的一块内存,几乎所有对象的实例和数组都要在堆上分配内存,因此该 区域经常发生垃圾回收的操作;
方法区:存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据。即永久代,在jdk1.8中不存在方法区了,被元数据区替代了,原方法区被分成两部分;1:加载的类信息,2:运行时常量池;加载的类信息被保存在元数据区中,运行时常量池保存在堆中;
Java 内存模型(下文简称 JMM)就是在底层处理器内存模型的基础上,定义自己的多线程语义。它明确指定了一组排序规则,来保证线程间的可见性。
这一组规则被称为 Happens-Before, JMM 规定,要想保证 B 操作能够看到 A 操作的结果(无论它们是
否在同一个线程),那么 A 和 B 之间必须满足 Happens-Before 关系:
单线程规则:一个线程中的每个动作都 happens-before 该线程中后续的每个动作
监视器锁定规则:监听器的解锁动作 happens-before 后续对这个监听器的锁定动作
volatile 变量规则:对 volatile 字段的写入动作 happens-before 后续对这个字段的每个读取动作
线程 start 规则:线程 start() 方法的执行 happens-before 一个启动线程内的任意动作
线程 join 规则:一个线程内的所有动作 happens-before 任意其他线程在该线程 join() 成功返回之前
传递性:如果 A happens-before B, 且 B happens-before C, 那么 A happens-before C
怎么理解 happens-before 呢?如果按字面意思,比如第二个规则,线程(不管是不是同一个)的解锁
动作发生在锁定之前?这明显不对。happens-before 也是为了保证可见性,比如那个解锁和加锁的动
作,可以这样理解,线程1释放锁退出同步块,线程2加锁进入同步块,那么线程2就能看见线程1对共享
对象修改的结果。
Java 提供了几种语言结构,包括 volatile, final 和 synchronized, 它们旨在帮助程序员向编译器描述程序
的并发要求,其中:
volatile - 保证可见性和有序性
synchronized - 保证可见性和有序性; 通过管程(Monitor)保证一组动作的原子性
final - 通过禁止在构造函数初始化和给 final 字段赋值这两个动作的重排序,保证可见性(如果
this 引用逃逸就不好说可见性了)
编译器在遇到这些关键字时,会插入相应的内存屏障,保证语义的正确性
有一点需要注意的是,synchronized 不保证同步块内的代码禁止重排序,因为它通过锁保证同一时刻
只有一个线程访问同步块(或临界区),也就是说同步块的代码只需满足 as-if-serial 语义 - 只要单线
程的执行结果不改变,可以进行重排序。
所以说,Java 内存模型描述的是多线程对共享内存修改后彼此之间的可见性,另外,还确保正确同步的
Java 代码可以在不同体系结构的处理器上正确运行。
(1)申请方式
stack:由系统自动分配。例如,声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空间
heap:需要程序员自己申请,并指明大小,在 c 中 malloc 函数,对于Java 需要手动 new Object()的形式开辟
(2)申请后系统的响应
stack:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
heap:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链
表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空
间分配给程序。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那
部分重新放入空闲链表中。
(3)申请大小的限制
stack:栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最
大容量是系统预先规定好的,在 WINDOWS 下,栈的大小是 2M(默认值也取决于虚拟内存的大小),
如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小。
heap:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存
地址的, 自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效
的虚拟内存。由此可见, 堆获得的空间比较灵活,也比较大。
(4)申请效率的比较
stack:由系统自动分配,速度较快。但程序员是无法控制的。
heap:由 new 分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
(5)heap和stack中的存储内容
stack:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)
的地址, 然后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的,然后是函数中的局
部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址也就是主
函数中的下一条指令,程序由该点继续运行。
heap:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
1、栈是线程私有的,栈的生命周期和线程一样,每个方法在执行的时候就会创建一个栈帧,它包含局部
变量表、操作数栈、动态链接、方法出口等信息,局部变量表又包括基本数据类型和对象的引用;
2、当线程请求的栈深度超过了虚拟机允许的最大深度时,会抛出StackOverFlowError异常,方法递归
调用肯可能会出现该问题;
3、调整参数-xss去调整jvm栈的大小
除了程序计数器,其他内存区域都有 OOM 的风险。
栈一般经常会发生 StackOverflowError,比如 32 位的 windows 系统单进程限制 2G 内存,无限创建线程就会发生栈的 OOM
Java 8 常量池移到堆中,溢出会出 java.lang.OutOfMemoryError: Java heap space,设置最大元 空间大小参数无效;
堆内存溢出,报错同上,这种比较好理解,GC 之后无法在堆中申请内存创建对象就会报错;
方法区 OOM,经常会遇到的是动态生成大量的类、jsp 等;
直接内存 OOM,涉及到 -XX:MaxDirectMemorySize 参数和 Unsafe 对象对内存的申请。
排查 OOM 的方法:
增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;
同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;
使用 MAT 工具载入到 dump 文件(是进程的内存镜像),分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用 。
JVM常量池主要分为Class文件常量池、运行时常量池,全局字符串常量池,以及基本类型包装类 对象常量池。
编写的java文件就被编译为.class文件格式的二进制数据存放在磁盘中,其中就包括class文件常量
池。
量只能在运行时才产生,也就是说运行时常量池的内容并不全部来自class常量池,在运行时可以通
过代码生成常量并将其放入运行时常量池中,这种特性被用的最多的就是String.intern()。
它是一个叫做StringTable的全局表。在字符串常量池中维护的是字符串实例的引用,底层C++实现
就是一个Hashtable。这些被维护的引用所指的字符串实例,被称作”被驻留的字符串”或”interned
string”或通常所说的”进入了字符串常量池的字符串”。
Byte,Short,Integer,Long,Character,Boolean,另外两种浮点数类型的包装类则没有实现。另外上面
这5种整型的包装类也只是在对应值小于等于127时才可使用对象池,也即对象不负责创建和管理大
于127的这些类的对象。
判断一个对象是否存活,分为两种算法1:引用计数法;2:可达性分析算法;
引用计数法:
给每一个对象设置一个引用计数器,当有一个地方引用该对象的时候,引用计数器就+1,引用失效时,
引用计数器就-1;当引用计数器为0的时候,就说明这个对象没有被引用,也就是垃圾对象,等待回收;
缺点:无法解决循环引用的问题,当A引用B,B也引用A的时候,此时AB对象的引用都不为0,此时也就
无法垃圾回收,所以一般主流虚拟机都不采用这个方法;
可达性分析法
从一个被称为GC Roots的对象向下搜索,如果一个对象到GC Roots没有任何引用链相连接时,说明此对
象不可用,在java中可以作为GC Roots的对象有以下几种:
虚拟机栈中引用的对象
方法区类静态属性引用的变量
方法区常量池引用的对象
本地方法栈JNI引用的对象
但一个对象满足上述条件的时候,不会马上被回收,还需要进行两次标记;第一次标记:判断当前对象
是否有finalize()方法并且该方法没有被执行过,若不存在则标记为垃圾对象,等待回收;若有的话,则
进行第二次标记;第二次标记将当前对象放入F-Queue队列,并生成一个finalize线程去执行该方法,虚
拟机不保证该方法一定会被执行,这是因为如果线程执行缓慢或进入了死锁,会导致回收系统的崩溃;
如果执行了finalize方法之后仍然没有与GC Roots有直接或者间接的引用,则该对象会被回收;
强引用,就是普通的对象引用关系,如 String s = new String(“ConstXiong”)
软引用,用于维护一些可有可无的对象。只有在内存不足时,系统则会回收软引用对象,如果回收
了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。SoftReference 实现
论内存是否充足,都会回收被弱引用关联的对象。WeakReference 实现
动。PhantomReference 实现
不一定,看 Reference 类型,弱引用在 GC 时会被回收,软引用在内存不足的时候,即 OOM 前
会被回收,但如果没有在 Reference Chain 中的对象就一定会被回收。
java中有四种垃圾回收算法,分别是标记清除法、标记整理法、复制算法、分代收集算法;
标记清除法:
第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;
第二步:在遍历一遍,将所有标记的对象回收掉;
特点:效率不行,标记和清除的效率都不高;标记和清除后会产生大量的不连续的空间分片,可能会导
致之后程序运行的时候需分配大对象而找不到连续分片而不得不触发一次GC;
标记整理法:
第一步:利用可达性去遍历内存,把存活对象和垃圾对象进行标记;
第二步:将所有的存活的对象向一段移动,将端边界以外的对象都回收掉;
特点:适用于存活对象多,垃圾少的情况;需要整理的过程,无空间碎片产生;
复制算法:
将内存按照容量大小分为大小相等的两块,每次只使用一块,当一块使用完了,就将还存活的对象移到
另一块上,然后在把使用过的内存空间移除;(就是from区和to区然后就是空一个区不使用)
特点:不会产生空间碎片;内存使用率极低;
分代收集算法:(其实这个有点像我听课的东西)
根据内存对象的存活周期不同,将内存划分成几块,java虚拟机一般将内存分成新生代和老生代,在新
生代中,有大量对象死去和少量对象存活,所以采用复制算法,只需要付出少量存活对象的复制成本就
可以完成收集;老年代中因为对象的存活率极高,没有额外的空间对他进行分配担保,所以采用标记清
理或者标记整理算法进行回收;
Serial(串行GC)-复制
它的最大特点是在进行垃圾回收时,需要对所有正在执行的线程暂停(stop the world),对于有些应用是难以接受的,但是如果应用的实时性要求不是那么高,只要停顿的时间控制在N毫秒之内,大多数应用还是可以接受的,
ParNew(并行GC)-复制
Serial收集器的多线程版本,也需要stop the world
Parallel Scavenge(并行回收GC)-复制
和ParNew的最大区别是GC自动调节策略;虚拟机会根据系统的运行状态收集性能监控信息,动态设置这些参数,以提供最优停顿时间和最高的吞吐量
Serial Old(MSC)(串行GC)-标记-整理
Serial收集器的老年代版本,单线程收集器,使用标记整理算法。
Parallel Old(并行GC)–标记-整理
是Parallel Scavenge收集器的老年代版本,使用多线程,标记-整理算法。
CMS(并发GC)-标记-清除
是一种以获得最短回收停顿时间为目标的收集器,标记清除算法,运作过程:初始标记,并发标记,重新标记,并发清除,收集结束会产生大量空间碎片;
G1(JDK1.7update14才可以正式商用)-标记-整理
运作流程主要包括以下:初始标记,并发标记,最终标记,筛选回收。不会产生空间碎片,可以精确地控制停顿;G1将整个堆分为大小相等的多个Region(区域),G1跟踪每个区域的垃圾大小,在后台维护一个优先级列表,每次根据允许的收集时间,优先回收价值最大的区域,已达到在有限时间内获取尽可能高的回收效率;
说明:
注意:并行与并发
各个垃圾回收器对比:
CMS(Concurrent Mark Sweep,并发标记清除) 收集器是以获取最短回收停顿时间为目标的收集器(追
求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感
到明显的卡顿。
从名字就可以知道,CMS是基于“标记-清除”算法实现的。CMS 回收过程分为以下四步:
程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。
**链上的最尽头。这个过程是多线程的,**虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,
没有 STW。
没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。
由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发进行的。
CMS 的问题:
1. 并发回收导致CPU资源紧张:
在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低
程序总吞吐量。CMS默认启动的回收线程数是:(CPU核数 + 3)/ 4,当CPU核数不足四个时,CMS对
用户程序的影响就可能变得很大。
2. 无法清理浮动垃圾:
在CMS的并发标记和并发清理阶段,用户线程还在继续运行,就还会伴随有新的垃圾对象不断产生,但
这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留到下一次垃
圾收集时再清理掉。这一部分垃圾称为“浮动垃圾”。
3. 并发失败(Concurrent Mode Failure):
由于在垃圾回收阶段用户线程还在并发运行,那就还需要预留足够的内存空间提供给用户线程使用,因
此CMS不能像其他回收器那样等到老年代几乎完全被填满了再进行回收,必须预留一部分空间供并发回
收时的程序运行使用。默认情况下,当老年代使用了 92% 的空间后就会触发 CMS 垃圾回收,这个值可
以通过 -XX: CMSInitiatingOccupancyFraction 参数来设置。
这里会有一个风险:要是CMS运行期间预留的内存无法满足程序分配新对象的需要,就会出现一次“并发
失败”(Concurrent Mode Failure),这时候虚拟机将不得不启动后备预案:Stop The World,临时启
用 Serial Old 来重新进行老年代的垃圾回收,这样一来停顿时间就很长了。
**4.**内存碎片问题:
**CMS是一款基于“标记-清除”算法实现的回收器,这意味着回收结束时会有内存碎片产生。**内存碎片过多
时,将会给大对象分配带来麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续
空间来分配当前对象,而不得不提前触发一次 Full GC 的情况。
为了解决这个问题,CMS收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认开
启),用于在 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,是无法
并发的,这样停顿时间就会变长。还有另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数的
作用是要求CMS在执行过若干次不整理空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整理
(默认值为0,表示每次进入 Full GC 时都进行碎片整理)。
G1(Garbage First)回收器采用面向局部收集的设计思路和基于Region的内存布局形式,是一款主要
面向服务端应用的垃圾回收器。G1设计初衷就是替换 CMS,成为一种全功能收集器。G1 在JDK9 之后
成为服务端模式下的默认垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默认组合,而 CMS
被声明为不推荐使用的垃圾回收器。G1从整体来看是基于 标记-整理 算法实现的回收器,但从局部(两
个Region之间)上看又是基于 标记-复制 算法实现的。
G1 回收过程,G1 回收器的运作过程大致可分为四个步骤:
初始标记(会STW(对正在执行的线程暂停)):仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理在并发时有引用变动的对象。
最终标记(会STW):对用户线程做短暂的暂停,处理并发阶段结束后仍有引用变动的对象。
清理阶段(会STW):更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,必须暂停用户线程,由多条回收器线程并行完成的
先描述一下Java堆内存划分。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old ),新生代默认占总空间的
1/3,老年代默认占 2/3。
新生代有 3 个分区:Eden、To Survivor、From Survivor,它们的默认占比是 8:1:1。
新生代的垃圾回收(又称Minor GC)后只有少量对象存活,所以选用复制算法,只需要少量的复制成本
就可以完成回收。
老年代的垃圾回收(又称Major GC)通常使用“标记-清理”或“标记-整理”算法。
再描述它们之间转化流程:
Minor GC:只收集新生代的GC。
Full GC: 收集整个堆,包括 新生代,老年代,永久代在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间)等所有部分的模式。
Minor GC触发条件:当Eden区满时,触发Minor GC。
Full GC触发条件:
GC的平均晋升大小比目前old gen剩余的空间大,则不会触发Minor GC而是转为触发full GC。
久代的原因,可以减少Full GC的频率,减少GC负担,提升其效率)
存到老年代,且老年代的可用内存小于该对象大小。
如果YougGC时新生代有大量对象存活下来,而 survivor 区放不下了,这时必须转移到老年代中,但这
时发现老年代也放不下这些对象了,那怎么处理呢?其实JVM有一个老年代空间分配担保机制来保证对
象能够进入老年代。
在执行每次 YoungGC 之前,JVM会先检查老年代最大可用连续空间是否大于新生代所有对象的总大小。
因为在极端情况下,可能新生代 YoungGC 后,所有对象都存活下来了,而 survivor 区又放不下,那可
能所有对象都要进入老年代了。这个时候如果老年代的可用连续空间是大于新生代所有对象的总大小
的,那就可以放心进行 YoungGC。但如果老年代的内存大小是小于新生代对象总大小的,那就有可能老
年代空间不够放入新生代所有存活对象,这个时候JVM就会先检查 -XX:HandlePromotionFailure 参数是
否允许担保失败,如果允许,就会判断老年代最大可用连续空间是否大于历次晋升到老年代对象的平均
大小,如果大于,将尝试进行一次YoungGC,尽快这次YoungGC是有风险的如果小于,或者 -
XX:HandlePromotionFailure 参数不允许担保失败,这时就会进行一次 Full GC。
在允许担保失败并尝试进行YoungGC后,可能会出现三种情况:
① YoungGC后,存活对象小于survivor大小,此时存活对象进入survivor区中
② YoungGC后,存活对象大于survivor大小,但是小于老年大可用空间大小,此时直接进入老年
代。
对象了,此时就会发生“Handle Promotion Failure”,就触发了 Full GC。如果 Full GC后,老年代
还是没有足够的空间,此时就会发生OOM内存溢出了。
虚拟机把描述类的数据加载到内存里面,并对数据进行校验、解析和初始化,最终变成可以被虚拟机直接使用的class对象;
类的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)。如图所示:
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)
类加载过程如下:
加载,加载分为三步:
1、通过类的全限定性类名获取该类的二进制流;
2、将该二进制流的静态存储结构转为方法区的运行时数据结构;
3、在堆中为该类生成一个class对象;
验证:验证该class文件中的字节流信息复合虚拟机的要求,不会威胁到jvm的安全;
准备:为class对象的静态变量分配内存,初始化其初始值;
解析:该阶段主要完成符号引用转化成直接引用;
初始化:到了初始化阶段,才开始执行类中定义的java代码;初始化阶段是调用类构造器的过程;
类加载器是指:通过一个类的全限定性类名获取该类的二进制字节流叫做类加载器;类加载器分为以下
四种:
启动类加载器(BootStrapClassLoader):用来加载java核心类库,无法被java程序直接引用;
扩展类加载器(Extension ClassLoader):用来加载java的扩展库,java的虚拟机实现会提供一个
扩展库目录,该类加载器在扩展库目录里面查找并加载java类;
系统类加载器(AppClassLoader):它根据java的类路径来加载类,一般来说,java应用的类都是通过它来加载的;
自定义类加载器:由java语言实现,继承自ClassLoader;
当一个类加载器收到一个类加载的请求,他首先不会尝试自己去加载,而是将这个请求委派给父类加载
器去加载,只有父类加载器在自己的搜索范围类查找不到给类时,子加载器才会尝试自己去加载该类;
为了防止内存中出现多个相同的字节码;因为如果没有双亲委派的话,用户就可以自己定义一个
java.lang.String类,那么就无法保证类的唯一性
补充:那怎么打破双亲委派模型?
自定义类加载器,继承ClassLoader类,重写loadClass方法和findClass方法
JNDI 通过引入线程上下文类加载器,可以在 Thread.setContextClassLoader 方法设置,默认是应
用程序类加载器,来加载 SPI 的代码。有了线程上下文类加载器,就可以完成父类加载器请求子类
加载器完成类加载的行为。打破的原因,是为了 JNDI 服务的类加载器是启动器类加载,为了完成
高级类加载器请求子类加载器(即上文中的线程上下文加载器)加载类。
Tomcat,应用的类加载器优先自行加载应用目录下的 class,并不是先委派给父加载器,加载不了
才委派给父加载器。
tomcat之所以造了一堆自己的classloader,大致是出于下面三类目的:
对于各个 webapp 中的 class 和 lib ,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况,而对于许多应用,需要有共享的lib以便不浪费资源。
与 jvm 一样的安全性问题。使用单独的 classloader 去装载 tomcat 自身的类库,以免其他恶意或无意的破坏;
热部署。
tomcat类加载器如下图:
OSGi,实现模块化热部署,为每个模块都自定义了类加载器,需要更换模块时,模块与类加载器一
起更换。其类加载的过程中,有平级的类加载器加载行为。打破的原因是为了实现模块热替换。
类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可
以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。打破的原因,是为了添
加模块化的特性。
jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
jstat:jstat(JVM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令,它可以显示出虚
拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
jmap:jmap(JVM Memory Map)命令用于生成heap dump文件,如果不使用这个命令,还阔以使
用-XX:+HeapDumpOnOutOfMemoryError参数来让虚拟机出现OOM的时候·自动生成dump文
件。
用率、当前使用的是哪种收集器等。
置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注
意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服
务器生成的dump文件复制到本地或其他机器上进行分析。
以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果java程序崩溃生成core文
件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道
java程序是如何崩溃和在程序何处发生问题。
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口. 顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
1)采用实现Runnable. Callable接口的方式创建多线程。
优势是:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU. 代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势是:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
package com.itheima.d1_create;
/**
目标:学会线程的创建方式二,理解它的优缺点。
*/
public class ThreadDemo2 {
public static void main(String[] args) {
// 3、创建一个任务对象
Runnable target = new MyRunnable();
// 4、把任务对象交给Thread处理
Thread t = new Thread(target);
// Thread t = new Thread(target, "1号");
// 5、启动线程
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
/**
1、定义一个线程任务类 实现Runnable接口
*/
class MyRunnable implements Runnable {
/**
2、重写run方法,定义线程的执行任务的
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
2)使用继承Thread类的方式创建多线程
优势是:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势是:
线程类已经继承了Thread类,所以不能再继承其他父类。
/**
目标:多线程的创建方式一:继承Thread类实现。
*/
public class ThreadDemo1 {
public static void main(String[] args) {
// 3、new一个新线程对象
Thread t = new MyThread();
// 4、调用start方法启动线程(执行的还是run方法)
t.start();
for (int i = 0; i < 5; i++) {
System.out.println("主线程执行输出:" + i);
}
}
}
/**
1、定义一个线程类继承Thread类
*/
class MyThread extends Thread{
/**
2、重写run方法,里面是定义线程以后要干啥
*/
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程执行输出:" + i);
}
}
}
3)Runnable和Callable的区别
Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
Call方法可以抛出异常,run方法不可以。
运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度 的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并 发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
线程的生命周期及五种基本状态:
Java线程具有五种基本状态
1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();
2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;
3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞 — 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞 — 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时. join()等待线程终止或者超时. 或者I/O处理完毕时,线程重新转入就绪状态。
5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
死锁
死锁必须具备以下四个条件:
互斥条件:该资源任意一个时刻只由一个线程占用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
如何避免线程死锁**?**
只要破坏产生死锁的四个条件中的其中一个就可以了
破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)
破坏请求与保持条件
一次性申请所有的资源。
破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件
靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
锁排序法:(必须回答出来的点)
指定获取锁的顺序,比如某个线程只有获得A锁和B锁,才能对某资源进行操作,在多线程条件下,如何避免死锁?
通过指定锁的获取顺序,比如规定,只有获得A锁的线程才有资格获取B锁,按顺序获取锁就可以避免死锁。这通常被认为是解决死锁很好的一种方法。
使用显式锁中的ReentrantLock.try(long,TimeUnit)来申请锁
Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。Callable 接口可以返回结果或抛出检查异常
Runnable 接口不会返回结果或抛出检查异常,
如果任务不需要返回结果或抛出异常推荐使用 Runnable接口,这样代码看起来会更加简洁
工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。
(Executors.callable(Runnable task)或 Executors.callable(Runnable task,Objectresule))
shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止
isShutDown 当调用 shutdown() 方法后返回为 true。
isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true
区别
sleep方法:是Thread类的静态方法,当前线程将睡眠n毫秒,线程进入阻塞状态。当睡眠时间到了,会解除阻塞,进入可运行状态,等待CPU的到来。睡眠不释放锁(如果有的话)。
wait方法:是Object的方法,必须与synchronized关键字一起使用,线程进入阻塞状态,当notify或者notifyall被调用后,会解除阻塞。但是,只有重新占用互斥锁之后才会进入可运行状态。睡眠时,会释放互斥锁。
sleep 方法没有释放锁,而 wait 方法释放了锁 。
sleep 通常被用于暂停执行,Wait 通常被用于线程间交互/通信
sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者notifyAll() 方法
相同
new 一个 Thread,线程进入了新建状态; 调用start() 会执行线程的相应准备工作,然后自动执行run() 方法的内容,(调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。)这是真正的多线程工作。
直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。、
Yield方法可以暂停当前正在执行的线程对象,让其它有相同优先级的线程执行。它是一个静态方法而且只保证当前线程放弃CPU占用而不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。
volatile的两层语义:
volatile保证变量对所有线程的可见性:当volatile变量被修改,新值对所有线程会立即更新。或者理解为多线程环境下使用volatile修饰的变量的值一定是最新的。
jdk1.5以后volatile完全避免了指令重排优化,实现了有序性。
Thread 类本质上是实现 Runnable 接口的一个实例,代表一个线程的实例。创建线程实例一般有两种方法:
public class MyThread extends Thread {
@Override
public void run(){
System.out.println("MyThread running");
}
}
run() 方在调用 start() 方法后被执行,而且一旦线程启动后 start() 方法后就会立即返回,而不是等到 run() 方法执行完毕后再返回。
MyThread myThread = new MyThread();
myThread.start();
public class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println("MyRunnable running");
}
}
在新建类时实现 Runnable 接口,然后在 Thread 类的构造函数中传入 MyRunnable 的实例对象,最后执行 start() 方法即可;
Thread thread = new Thread(new MyRunnable());
thread.start();
当线程因为某种原因放弃 CPU 使用权后,即让出了 CPU 时间片,暂时就会停止运行,知道线程进入可运行状态( Runnable ),才有机会再次获得 CPU 时间片转入 RUNNING 状态。一般来讲,阻塞的情况
可以分为如下三种:
RUNNING 状态的线程执行 Object.wait() 方法后,JVM 会将线程放入等待序列(waitting queue);
RUNNING 状态的线程在获取对象的同步锁时,若该 同步锁被其他线程占用,则 JVM 将该线程放入 锁池(lock pool)中;
RUNNING 状态的线程执行 Thread.sleep(long ms) 或 Thread.join() 方法,或发出 I/O 请求时,JVM 会将该线程置为阻塞状态。当 sleep() 状态超时, join() 等待线程终止或超时. 或者 I/O 处理完毕时,线程重新转入可运行状态( RUNNABLE );
run() 或者 call() 方法执行完成后,线程正常结束;
线程抛出一个未捕获的 Exception 或 Error ,导致线程异常结束;
直接调用线程的 stop() 方法来结束该线程,但是一般不推荐使用该种方式,因为该方法通常容易导 致死锁;
volatile 解决的是内存可见性问题,会使得所有对 volatile 变量的读写都直接写入主存,即 保证 了变量的可见性。
synchronized 解决的是执行控制的问题,它会阻止其他线程获取当前对象的监控锁,这样一来就让当前对象中被 synchronized 关键字保护的代码块无法被其他线程访问,也就是无法并发执行。而且,synchronized 还会创建一个 内存屏障,内存屏障指令保证了所有 CPU 操作结果都会直接刷到主存中,从而 保证操作的内存可见性,同时也使得这个锁的线程的所有操作都 happens-before 于随后获得这个锁的线程的操作。
两者的区别主要有如下:
volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile 仅能使用在变量级别;synchronized 则可以使用在 变量. 方法. 和类级别的
volatile 仅能实现变量的修改可见性,不能保证原子性(因为修改不是一个操作,是读取,改变,写回三个操作所以没有原子性);而synchronized 则可以 保证变量的修 改可见性和原子性
volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。
synchronized 可以给类. 方法. 代码块加锁;而 lock 只能给代码块加锁。
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
1.两者都是可重入锁
可重入锁:重入锁,也叫做递归锁,可重入锁指的是在一个线程中可以多次获取同一把锁,比如:
一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行
调用的方法,而无需重新获得锁,
两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的
ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally语句块来完成)
3.ReentrantLock 比 synchronized 增加了一些高级功能
相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如
已完成任务的数量。
使用线程池的好处:
降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功 与否;
submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit) 方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
corePoolSize : 核心线程大小。线程池一直运行,核心线程就不会停止。
maximumPoolSize :线程池最大线程数量。非核心线程数量=maximumPoolSize-corePoolSize
keepAliveTime :非核心线程的心跳时间。如果非核心线程在keepAliveTime内没有运行任务,非核心线程会消亡。
workQueue :阻塞队列。ArrayBlockingQueue,LinkedBlockingQueue等,用来存放线程任务。
defaultHandler :饱和策略。ThreadPoolExecutor类中一共有4种饱和策略。通过实现
RejectedExecutionHandler接口。
AbortPolicy : 线程任务丢弃报错。默认饱和策略。
DiscardPolicy : 线程任务直接丢弃不报错。
DiscardOldestPolicy : 将workQueue队首任务丢弃,将最新线程任务重新加入队列执行。
CallerRunsPolicy :线程池之外的线程直接调用run方法执行。
ThreadFactory :线程工厂。新建线程工厂。
线程池执行execute/submit方法向线程池添加任务,当任务小于核心线程数corePoolSize,线程池中可以创建新的线程。
当任务大于核心线程数corePoolSize,就向阻塞队列添加任务。
如果阻塞队列已满,需要通过比较参数maximumPoolSize,在线程池创建新的线程,当线程数量大于maximumPoolSize,说明当前设置线程池中线程已经处理不了了,就会执行饱和策略。
索引是一种特殊的文件(InnoDB数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。
索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新
数据库表中数据。索引的实现通常使用B树及其变种B+树。更通俗的说,索引就相当于目录。为了方便
查找书中的内容,通过对内容建立索引形成目录。而且索引是一个文件,它是要占据物理空间的。
MySQL索引的建立对于MySQL的高效运行是很重要的,索引可以大大提高MySQL的检索速度。比如我
们在查字典的时候,前面都有检索的拼音和偏旁、笔画等,然后找到对应字典页码,这样然后就打开字
典的页数就可以知道我们要搜索的某一个key的全部值的信息了。
索引的优点
可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
索引的缺点
时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率;
空间方面:索引需要占物理空间。
1、从存储结构上来划分:BTree索引(B-Tree或B+Tree索引),Hash索引,full-index全文索引,R-Tree索引。这里所描述的是索引存储时保存的形式,
2、从应用层次来分:普通索引,唯一索引,复合索引。
普通索引:即一个索引只包含单个列,一个表可以有多个单列索引
唯一索引:索引列的值必须唯一,但允许有空值
复合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并
聚簇索引(聚集索引):并不是一种单独的索引类型,而是一种数据存储方式。具体细节取决于不同的实现,InnoDB的聚簇索引其实就是在同一个结构中保存了B-Tree索引(技术上来说是B+Tree)和数据行。
非聚簇索引: 不是聚簇索引,就是非聚簇索引
3、根据中数据的物理顺序与键值的逻辑(索引)顺序关系: 聚集索引,非聚集索引。
Hash索引
基于哈希表实现,只有精确匹配索引所有列的查询才有效,对于每一行数据,存储引擎都会对所有的索
引列计算一个哈希码(hash code),并且Hash索引将所有的哈希码存储在索引中,同时在索引表中保
存指向每个数据行的指针。
B-Tree索引(MySQL使用B+Tree)
B-Tree能加快数据的访问速度,因为存储引擎不再需要进行全表扫描来获取数据,数据分布在各个节点之中。
B+Tree索引
是B-Tree的改进版本,同时也是数据库索引索引所采用的存储结构。数据都在叶子节点上,并且增加了
顺序访问指针,每个叶子节点都指向相邻的叶子节点的地址。相比B-Tree来说,进行范围查找时只需要
查找两个节点,进行遍历即可。而B-Tree需要获取所有节点,相比之下B+Tree效率更高。
B+tree性质:
n棵子tree的节点包含n个关键字,不用来保存数据而是保存数据的索引。
所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依 关键字的大小自小而大顺序链接。
所有的非终端结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。
B+ 树中,数据对象的插入和删除仅在叶节点上进行。
B+树有2个头指针,一个是树的根节点,一个是最小关键码的叶节点。
B-tree: 从两个方面来回答
B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B(B-)树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对 IO读写次数就降低 了。
由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在 区间查询 的情况,所以通常B+树用于数据库索引。
Hash:
虽然可以快速定位,但是没有顺序,IO复杂度高;
基于Hash表实现,只有Memory存储引擎显式支持哈希索引 ;
适合等值查询,如=、in()、<=>,不支持范围查询 ;
因为不是按照索引值顺序存储的,就不能像B+Tree索引一样利用索引完成排序 ;
Hash索引在查询等值时非常快 ;
因为Hash索引始终索引的所有列的全部内容,所以不支持部分索引列的匹配查找 ;
如果有大量重复键值得情况下,哈希索引的效率会很低,因为存在哈希碰撞问题 。
二叉树: 树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且IO代价高。
红黑树: 树的高度随着数据量增加而增加,IO代价高。
在 InnoDB 里,索引B+ Tree的叶子节点存储了整行数据的是主键索引,也被称之为聚簇索引,即将数据
存储与索引放到了一块,找到索引也就找到了数据。
而索引B+ Tree的叶子节点存储了主键的值的是非主键索引,也被称之为非聚簇索引、二级索引。
聚簇索引与非聚簇索引的区别:
非聚集索引与聚集索引的区别在于非聚集索引的叶子节点不存储表中的数据,而是存储该列对应的主键(行号)
对于InnoDB来说,想要查找数据我们还需要根据主键再去聚集索引中进行查找,这个再根据聚集索引查找数据的过程,我们称为回表。第一次索引一般是顺序IO,回表的操作属于随机IO。需要回表的次数越多,即随机IO次数越多,我们就越倾向于使用全表扫描。
通常情况下, 主键索引(聚簇索引)查询只会查一次,而非主键索引(非聚簇索引)需要回表查询多次。当然,如果是覆盖索引的话,查一次即可。
注意:MyISAM无论主键索引还是二级索引都是非聚簇索引,而InnoDB的主键索引是聚簇索引,二
级索引是非聚簇索引。我们自己建的索引基本都是非聚簇索引。
不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询。一个索引包含(覆盖)所有需要查询字段的值,被称之为"覆盖索引"。
举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行 select score from student where score > 90 的查询时,在索引的叶子节点上,已经包含了score 信息,不会再次进行回表查询。
MySQL可以使用多个字段同时建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。
具体原因为:
MySQL使用索引时需要索引有序假设现在建立了"name,age,school"的联合索引,那么索引的排序为: 先按照name排序,如果name相同,则按照age排序,如果age的值也相等,则按照school进行排序。
当进行查询时,此时索引仅仅按照name严格有序,因此必须首先使用name字段进行等值查询,之后对于匹配到的列而言,其按照age字段严格有序,此时可以使用age字段用做索引查找,以此类推。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。此外可以根据特例的查询或者表结构进行单独的调整。
第一范式:强调的是列的原子性,即数据库表的每一列都是不可分割的原子数据项。
第二范式:要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性。表中的每列都和主键相关.
第三范式:任何非主属性不依赖于其它非主属性。在第二范式的基础上更进一层,目标是确保每列都和主键列直接相关,而不是间接相关.
MySQL 支持多种存储引擎,比如 InnoDB,MyISAM,Memory,Archive 等等.在大多数的情况下,直接选择使用 InnoDB 引擎都是最合适的,InnoDB 也是 MySQL 的默认存储引擎。
MyISAM 和 InnoDB 的区别有哪些:
InnoDB 支持事务,MyISAM 不支持
InnoDB 支持外键,而 MyISAM 不支持
InnoDB 是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高;MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针,主键索引和辅助索引是独立的。
Innodb 不支持全文索引,而 MyISAM 支持全文索引,查询效率上 MyISAM 要高;
InnoDB 不保存表的具体行数,MyISAM 用一个变量保存了整个表的行数。
MyISAM 采用表级锁(table-level locking);InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。
超键:在关系中能唯一标识元组的属性集称为关系模式的超键(属性集)。一个属性可以为作为一个超键,多个属性组合在一起也可以作为一个超键。超键包含候选键和主键。
候选键:是最小超键,即没有冗余元素的超键。
主键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。关系模式中用户正在使用候选键是主键
外键:在一个表中存在的另一个表的主键称此表的外键。
NOT NULL: 用于控制字段的内容一定不能为空(NULL)。
UNIQUE: 控件字段内容不能重复,一个表允许有多个 Unique 约束。
PRIMARY KEY: 也是用于控件字段内容不能重复,但它在一个表只允许出现一个。
FOREIGN KEY: 用于预防破坏表之间连接的动作,也能防止非法数据插入外键列,因为它必须是它指向的那个表中的值之一。
CHECK: 用于控制字段的值范围。
char 是一个定长字段,假如申请了 char(10) 的空间,那么无论实际存储多少内容.该字段都占用 10 个字
符,而 varchar 是变长的,也就是说申请的只是最大长度,占用的空间为实际字符长度+1,最后一个字符存储
使用了多长的空间.
在检索效率上来讲,char > varchar,因此在使用中,如果确定某个字段的值的长度,可以使用 char,否则应该
尽量使用 varchar.例如存储用户 MD5 加密后的密码,则应该使用 char。
MySQL中的in语句是把外表和内表作hash 连接,而exists语句是对外表作loop循环,每次loop循环再对内表进行查询。一直大家都认为exists比in语句的效率要高,这种说法其实是不准确的。这个是要区分环境的。
如果查询的两个表大小相当,那么用in和exists差别不大。如果两个表中一个较小,一个是大表,则子查询表大的用exists,子查询表小的用in。not in 和not exists:如果查询语句使用了not in,那么内外表都进行全表扫描,没有用到索引;而notextsts的子查询依然能用到表上的索引。所以无论那个表大,用not exists都比not in要快。
三者都表示删除,但是三者有一些差别:
delete的表结构还在,只是把数据删除掉了,而drop把表也给干掉了。
存储过程是一些预编译的 SQL 语句。
1、更加直白的理解:存储过程可以说是一个记录集,它是由一些 T-SQL 语句组成的代码块,这些 T
SQL 语句代码像一个方法一样实现一些功能(对单表或多表的增删改查),然后再给这个代码块取一个
名字,在用到这个功能的时候调用他就行了。
2、存储过程是一个预编译的代码块,执行效率比较高,一个存储过程替代大量 T_SQL 语句 ,可以降低网
络通信量,提高通信速率,可以一定程度上确保数据安全
但是,在互联网项目中,其实是不太推荐存储过程的,比较出名的就是阿里的《Java 开发手册》中禁止使用
存储过程,我个人的理解是,在互联网项目中,迭代太快,项目的生命周期也比较短,人员流动相比于传统的项
目也更加频繁,在这样的情况下,存储过程的管理确实是没有那么方便,同时,复用性也没有写在服务层那么
好。
客户端通过 TCP 连接发送连接请求到 MySQL 连接器,连接器会对该请求进行权限验证及连接资源分配
查缓存。(当判断缓存是否命中时,MySQL 不会进行解析查询语句,而是直接使用 SQL 语句和客户端发送过来的其他原始信息。所以,任何字符上的不同,例如空格、注解等都会导致缓存的不命中。)
语法分析(SQL 语法是否写错了)。 如何把语句给到预处理器,检查数据表和数据列是否存在,解析别名看是否存在歧义。
优化。是否使用索引,生成执行计划。
交给执行器,将数据保存到结果集中,同时会逐步将数据缓存到查询缓存中,最终将结果集返回给客户端。
更新语句执行会复杂一点。需要检查表是否有排它锁,写 binlog,刷盘,是否执行 commit。
事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。
事务最经典也经常被拿出来说例子就是转账了。
假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
事务就是一组原子性的操作,这些操作要么全部发生,要么全部不发生。事务把数据库从一种一致性状态转换成另一种一致性状态。
原子性。事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做
一致性。事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。
隔离性。一个事务的执行不能其它事务干扰。即一个事务内部的//操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
持续性。也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓 的 不可重复读(NonrepeatableRead),因为同一事务的其他实例在该实例处理其间可能会有新的 commit,所以同一 select 可能返回不同结果。
这是 MySQL 的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另 一个棘手的问题:幻读 (Phantom Read)。
通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
[外链图片转存中…(img-kyfB1qlo-1653698680510)]
1、脏读:事务 A 读取了事务 B 更新的数据,然后 B 回滚操作,那么 A 读取到的数据是脏数据
2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果 不一致。
3、幻读:系统管理员 A 将数据库中所有学生的成绩从具体分数改为 ABCDE 等级,但是系统管理员 B 就在这个时候插入了一条具体分数的记录,当系统管理员 A 改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
不可重复读侧重于修改,幻读侧重于新增或删除(多了或少量行),脏读是一个事务回滚影响另外一个事务。
事务是基于重做日志文件(redo log)和回滚日志(undo log)实现的。
每提交一个事务必须先将该事务的所有日志写入到重做日志文件进行持久化,数据库就可以通过重做日志来保证事务的原子性和持久性。
每当有修改事务时,还会产生 undo log,如果需要回滚,则根据 undo log 的反向语句进行逻辑操作,比如 insert 一条记录就 delete 一条记录。undo log 主要实现数据库的一致性。
innodb 事务日志包括 redo log 和 undo log。
undo log 指事务开始之前,在操作任何数据之前,首先将需操作的数据备份到一个地方。redo log 指事务中操作的任何数据,将最新的数据备份到一个地方。
事务日志的目的:实例或者介质失败,事务日志文件就能派上用场
MySQL的 binlog 是记录所有数据库表结构变更(例如 CREATE、ALTER TABLE)以及表数据修改(INSERT、UPDATE、DELETE)的二进制日志。binlog 不会记录 SELECT 和 SHOW 这类操作,因为这类操作对数据本身并没有修改,但你可以通过查询通用日志来查看 MySQL 执行过的所有语句。
MySQL binlog 以事件形式记录,还包含语句所执行的消耗的时间,MySQL 的二进制日志是事务安全型的。binlog 的主要目的是复制和恢复。
binlog 有三种格式,各有优缺点:
statement: 基于 SQL 语句的模式,某些语句和函数如 UUID, LOAD DATA INFILE 等在复制过程可能导致数据不一致甚至出错。
row: 基于行的模式,记录的是行的变化,很安全。但是 binlog 会比其他两种模式大很多,在一些大表中清除大量数据时在 binlog 中会生成很多条语句,可能导致从库延迟变大。
mixed: 混合模式,根据语句来选用是 statement 还是 row 模式。
尽量不要在同一个事务中使用多种存储引擎,MySQL服务器层不管理事务,事务是由下层的存储引擎实现的。
如果在事务中混合使用了事务型和非事务型的表(例如InnoDB和MyISAM表),在正常提交的情况下不会有什么问题。
但如果该事务需要回滚,非事务型的表上的变更就无法撤销,这会导致数据库处于不一致的状态,这种情况很难修复,事务的最终结果将无法确定。所以,为每张表选择合适的存储引擎非常重要。
当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。
保证多用户环境下保证数据库完整性和一致性。
在关系型数据库中,可以按照锁的粒度把数据库锁分为行级锁(INNODB引擎)、表级锁(MYISAM引擎)和页级锁(BDB引擎 )。
行级锁:
行级锁是MySQL中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁 和 排他锁。
开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
表级锁:
表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。
开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。
页级锁
页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。BDB支持页级锁
开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般
MyISAM和InnoDB存储引擎使用的锁:
MyISAM采用表级锁(table-level locking)。
InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁
从锁的类别上来讲,有共享锁和排他锁。
共享锁: 又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。
排他锁: 又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。
用上面的例子来说就是用户的行为有两种,一种是来看房,多个用户一起看房是可以接受的。 一种是真正的入住一晚,在这期间,无论是想入住的还是想看房的都不可以。
锁的粒度取决于具体的存储引擎,InnoDB实现了行级锁,页级锁,表级锁。
他们的加锁开销从大到小,并发能力也是从大到小。
数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过version的方式来进行锁定。实现方式:一般会使用版本号机制或CAS算法实现
两种锁的使用场景:
从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。
但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
InnoDB是基于索引来完成行锁
例: select * from tab_with_index where id = 1 for update;
for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么InnoDB将完成表锁,并发将无从谈起
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
常见的解决死锁的方法
1、如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
2、在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
3、对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
如果业务处理不好可以用分布式事务锁或者使用乐观锁
在Read Uncommitted级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突
在Read Committed级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁;
在Repeatable Read级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。
SERIALIZABLE 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。
使用较低的隔离级别
设计索引,尽量使用索引去访问数据,加锁更加精确,从而减少锁冲突
选择合理的事务大小,给记录显示加锁时,最好一次性请求足够级别的锁。列如,修改数据的话,最好申请排他锁,而不是先申请共享锁,修改时在申请排他锁,这样会导致死锁
不同的程序访问一组表的时候,应尽量约定一个相同的顺序访问各表,对于一个表而言,尽可能的固定顺序的获取表中的行。这样大大的减少死锁的机会。
尽量使用相等条件访问数据,这样可以避免间隙锁对并发插入的影响
不要申请超过实际需要的锁级别
数据查询的时候不是必要,不要使用加锁。MySQL的MVCC可以实现事务中的查询不用加锁,优化事务性能:MVCC只在committed read(读提交)和 repeatable read (可重复读)两种隔离级别
对于特定的事务,可以使用表锁来提高处理速度活着减少死锁的可能。
分表
比如你单表都几千万数据了,你确定你能扛住么?绝对不行,单表数据量太大,会极大影响你的 sql执行的性能,到了后面你的 sql 可能就跑的很慢了。一般来说,就以我的经验来看,单表到几百万的时候,性能就会相对差一些了,你就得分表了。
分表就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户 id 来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在 200 万以内。
分库
分库就是你一个库一般我们经验而言,最多支撑到并发 2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。
第一:选择最合适的字段属性。
mysql在创建数据库的时候肯定是数据库中的表越小越好,这样才能提高查询的速度。但是现实往往不如人意,每天可能产生的数据量是以数万或者数十万为单位的,这时就要充分地考虑在建表时的字段长度了。比如说在存储电话号的时候,如果将其写成CHAR(255),这显然会给数据库带来很多不必要的空间浪费,明明CHAR(11)就可以解决的问题。
第二:使用连接查询(join)代替子查询。
子查询的优点是可以使用简单的SELECT语句就可以完成逻辑较为复杂的查询动作,而且还能避免死锁的情况产生。但是有时也可以考虑使用连接查询来完成,毕竟连接查询不需要像子查询一样在内存中创建临时表,再从临时表中过滤数据,从而加快查询速度。
第三:使用union联合查询。
union本来就有联合的含义,它可以将多个SELECT语句的查询结果联合到一个查询中,从而替代了手动创建临时表的过程。而且union还有一个好处就是使用完了以后自动就删除了,一点也不占用内存空间。这真是“事了拂衣去,片叶不沾身”呀。
要查询的结果来自于多个表,且多个表没有直接的连接关系,但查询的信息一致时
特点:★
1、要求多条查询语句的查询列数是一致的!
2、要求多条查询语句的查询的每一列的类型和顺序最好一致
3、union关键字默认去重,如果使用union all 可以包含重复项
第四:通过事务来管理。
事务是数据库调优时的一个老生常谈的概念,由于其4大特性,事务是不可避免的调优方式之一,它可以保证数据的完整性、一致性。可以用一句话来总结“去不了终点,那就回到原点。
第五:锁定表。
尽管事务能够保证数据库的完整性和一致性,但是它本身其实也存在一些弊端。因为它本身具有一定的独占性,当事务没有执行完毕以前,用户发出的其他操作就只能等待,一直等到所有的事务都结束了才能继续执行。如果用户访问量特别大的时候,这会造成系统的严重延迟问题。
所以可以通过锁定表的方法,保证在没有执行到UNLOCKTABLES指令之前所有被LOCKTABLE修饰地表的查询语句不会被插入、删除和修改。
第六:合理使用外键。
外键本身存在的作用就是保证表与表之间的参照完整性,使用外键可以有效地增加数据之间的关联性。
第七:正确使用索引。
索引是提高数据库性能的最常见的方法,能够正确地使用索引,能够大大地提高查询效率。
但是也不能够为所有的列都创建索引,因为它本身会占用内存,维护起来也很麻烦,它就是一把双刃剑,所以索引在使用的时候需要根据实际情况正确使用才行。
第八:优化查询语句。
一个好的查询语句能够很明显地提高查询效率,反之就会慢到怀疑是不是程序写错了。
那么什么样的语句查询语句算是好的呢?可以参考以下几点要求
1、在比较的时候尽量在相同的字段之间进行比较。例如不能将一个带有索引的INT字段和BIGINT字段进行比较。
2、已将创建了索引的字段上不要使用函数,那样会导致索引失效。
3、查询时不鼓励使用like语句查询,因为那会消耗掉一部分系统的性能
Redis本质上是一个Key-Value类型的内存数据库,很像Memcached,整个数据库加载在内存当中操作,定期通过异步操作把数据库中的数据flush到硬盘上进行保存。
因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value 数据库
优点:
读写性能极高, Redis能读的速度是110000次/s,写的速度是81000次/s。
支持数据持久化,支持AOF和RDB两种持久化方式。
支持事务, Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
数据结构丰富,除了支持string类型的value外,还支持hash、set、zset、list等数据结构。
支持主从复制,主机会自动将数据同步到从机,可以进行读写分离。
丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等特性。
缺点:
数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性
**内存存储:**Redis是使用内存(in-memeroy)存储,没有磁盘IO上的开销。数据存在内存中,类似于HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1)。
单线程实现( Redis 6.0以前):Redis使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销。注意:单线程是指的是在核心网络模型中,网络请求模块使用一个线程来处理,即一个线程处理所有网络请求。
非阻塞IO:Redis使用多路复用IO技术,将epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
优化的数据结构:Redis有诸多可以直接应用的优化数据结构的实现,应用层可以直接使用原生的数据结构提升性能
1、缓存
缓存现在几乎是所有中大型网站都在用的必杀技,合理的利用缓存不仅能够提升网站访问速度,还能大
大降低数据库的压力。Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓
存的场合非常多。
2、排行榜
很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。Redis提供的有序集
合数据类构能实现各种复杂的排行榜应用。
3、计数器
什么是计数器,如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景。
4、分布式会话
集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。
5、分布式锁
在很多互联网公司中都使用了分布式技术,分布式技术带来的技术挑战是对同一个资源的并发访问,如全局ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。
6、 社交网络
点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。如在微博中的共同好友,通过Redis的set能够很方便得出。
7、最新列表
Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。
8、消息系统
消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用
于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。
有五种常用数据类型:String、Hash、Set、List、SortedSet。以及三种特殊的数据类型:Bitmap、
HyperLogLog、Geospatial ,其中HyperLogLog、Bitmap的底层都是 String 数据类型,Geospatial 的
底层是 Sorted Set 数据类型。
五种常用的数据类型:
1、String:String是最常用的一种数据类型,普通的key- value 存储都可以归为此类。其中Value既可以是数字也可以是字符串。使用场景:常规key-value缓存应用。常规计数: 微博数, 粉丝数。
2、Hash:Hash 是一个键值(key => value)对集合。Redishash 是一个 string 类型的 field 和 value 的
映射表,hash 特别适合用于存储对象,并且可以像数据库中update一个属性一样只修改某一项属性
值。
3、Set:Set是一个无序的天然去重的集合,即Key-Set。此外还提供了交集、并集等一系列直接操作集合的方法,对于求共同好友、共同关注什么的功能实现特别方便。
4、List:List是一个有序可重复的集合,其遵循FIFO的原则,底层是依赖双向链表实现的,因此支持正向、反向双重查找。通过List,我们可以很方面的获得类似于最新回复这类的功能实现。
5、SortedSet:类似于java中的TreeSet,是Set的可排序版。此外还支持优先级排序,维护了一个score的参数来实现。适用于排行榜和带权重的消息队列等场景。
轻量:Spring 是轻量的,基本的版本大约2MB
控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们
面向切面的编程(AOP):Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开
容器:Spring 包含并管理应用中对象的生命周期和配置
MVC框架:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品
事务管理:Spring 提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)
异常处理:Spring 提供方便的API把具体技术相关的异常(比如由JDBC,Hibernate or JDO抛出的)转化为一致的unchecked 异常。
Spring 框架的核心是 Spring 容器。容器创建对象,将它们装配在一起,配置它们并管理它们的完整生
命周期。Spring 容器使用依赖注入来管理组成应用程序的组件。容器通过读取提供的配置元数据来接收
对象进行实例化,配置和组装的指令。该元数据可以通过 XML,Java 注解或 Java 代码提供。
在依赖注入中,您不必创建对象,但必须描述如何创建它们。您不是直接在代码中将组件和服务连接在
一起,而是描述配置文件中哪些组件需要哪些服务。由 IoC 容器将它们装配在一起。
通常,依赖注入可以通过三种方式完成,即:
构造函数注入
setter 注入
接口注入
在 Spring Framework 中,仅使用构造函数和 setter 注入。
IOC容器的顶层核心接口就是我们的BeanFactory
,它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。BeanFactory只是个接口,并不是IOC容器的具体实现,但是Spring容器给出了很多种实现,如 DefaultListableBeanFactory、XmlBeanFactory、ApplicationContext等
BeanFactory和ApplicationContext就是spring框架的两个IOC容器,现在一般使用ApplicationnContext,其不但包含了BeanFactory的作用,同时还进行更多的扩展。
BeanFacotry是spring中比较原始的Factory。如XMLBeanFactory就是一种典型的BeanFactory。
原始的BeanFactory无法支持spring的许多插件,如AOP功能、Web应用等。
ApplicationContext包含BeanFactory的所有功能,通常建议比BeanFactory优先
ApplicationContext以一种更向面向框架的方式工作以及对上下文进行分层和实现继承,ApplicationContext包还提供了以下的功能:
• MessageSource, 提供国际化的消息访问
• 资源访问,如URL和文件
• 事件传播
• 载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层;
BeanFactory的优缺点:
优点:应用启动的时候占用资源很少,对资源要求较高的应用,比较有优势;
缺点:运行速度会相对来说慢一些。而且有可能会出现空指针异常的错误,而且通过Bean工厂创
建的Bean生命周期会简单一些。
ApplicationContext的优缺点:
现系统中的配置问题。
singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。
prototype : 每次请求都会创建一个新的 bean 实例。
request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
session : :在一个HTTP Session中,一个Bean定义对应一个实例。该作用域仅在基于web的
Spring ApplicationContext情形下有效。
有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容
器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话
IOC就是控制反转,通俗的说就是我们不用自己创建实例对象,这些都交给Spring的bean工厂帮我们创
建管理。这也是Spring的核心思想,通过面向接口编程的方式来是实现对业务组件的动态依赖。这就意
味着IOC是Spring针对解决程序耦合而存在的。在实际应用中,Spring通过配置文件(xml或者
properties)指定需要实例化的java类(类名的完整字符串),包括这些java类的一组初始化值,通过加
载读取配置文件,用Spring提供的方法(getBean())就可以获取到我们想要的根据指定配置进行初始化
的实例对象。
中不再需要单例或JNDI查找机制。简单的实现以及较少的干扰机制使得松耦合得以实现。IOC容器
支持勤性单例及延迟加载服务。
DI:DI—Dependency Injection,即“依赖注入”:组件之间依赖关系由容器在运行期决定,形象的说,
即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是
为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通
过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的
资源来自何处,由谁实现。
我们一般使用 @Autowired 注解自动装配 bean,要想把类标识成可用于 @Autowired 注解自动装配的
bean 的类,采用以下注解可实现:
可以使用@Component 注解标注。
@Repository : 对应持久层即 Dao 层,主要用于数据库相关操作。
@Service : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。
@Controller : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。
Spring bean 支持 5 种 scope:
Singleton - 每个 Spring IoC 容器仅有一个单实例。
Prototype - 每次请求都会产生一个新的实例。
Request - 每一次 HTTP 请求都会产生一个新的实例,并且该 bean 仅在当前 HTTP 请求内有效。
Session - 每一次 HTTP 请求都会产生一个新的 bean,同时该 bean 仅在当前 HTTP session 内有效。
Global-session - 类似于标准的 HTTP Session 作用域,不过它仅仅在基于 portlet 的 web 应用中
才有意义。Portlet 规范定义了全局 Session 的概念,它被所有构成某个 portlet web 应用的各种
不同的 portlet 所共享。在 global session 作用域中定义的 bean 被限定于全局 portlet Session 的
生命周期范围内。如果你在 web 中使用 global session 作用域来标识 bean,那么 web 会自动当
成 session 类型来使用。
仅当用户使用支持 Web 的 ApplicationContext 时,最后三个才可用。
当 bean 在 Spring 容器中组合在一起时,它被称为装配或 bean 装配。 Spring 容器需要知道需要什么
bean 以及容器应该如何使用依赖注入来将 bean 绑定在一起,同时装配 bean。
Spring 容器能够自动装配 bean。也就是说,可以通过检查 BeanFactory 的内容让 Spring 自动解析
bean 的协作者。
自动装配的不同模式:
no - 这是默认设置,表示没有自动装配。应使用显式 bean 引用进行装配。
byName - 它根据 bean 的名称注入对象依赖项。它匹配并装配其属性与 XML 文件中由相同名称定义的 bean。
byType - 它根据类型注入对象依赖项。如果属性的类型与 XML 文件中的一个 bean 名称匹配,则匹配并装配属性。
构造函数 - 它通过调用类的构造函数来注入依赖项。它有大量的参数。
autodetect - 首先容器尝试通过构造函数使用 autowire 装配,如果不能,则尝试通过 byType 自动装配
同一个配置文件内同名的Bean,以最上面定义的为准
不同配置文件中存在同名Bean,后解析的配置文件会覆盖先解析的配置文件
同文件中ComponentScan和@Bean出现同名Bean。同文件下@Bean的会生效,
@ComponentScan扫描进来不会生效。通过@ComponentScan扫描进来的优先级是最低的,原因
就是它扫描进来的Bean定义是最先被注册的
AOP(Aspect-Oriented Programming), 即 面向切面编程, 它与 OOP( Object-Oriented Programming,
面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角.
在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面)
实现 AOP 的技术,主要分为两大类:
静态代理 - 指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;
动态代理 - 在运行时在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。
CGLIB 动态代理: 如果目标类没有实现接口,那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类 。 CGLIB ( Code Generation Library ),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意, CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为 final ,那么它是无法使用 CGLIB 做动态代理的。
Spring AOP 基于动态代理方式实现;AspectJ 基于静态代理方式实现。
Spring AOP 仅支持方法级别的 PointCut;提供了完全的 AOP 支持,它还支持属性级别的 PointCut。
工厂设计模式 : Spring使用工厂模式通过 BeanFactory 、 ApplicationContext 创建 bean 对象。
代理设计模式 : Spring AOP 功能的实现。
单例设计模式 : Spring 中的 Bean 默认都是单例的。
模板方法模式 : Spring 中 jdbcTemplate 、 hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配 Controller 。
编程式事务管理:这意味着你可以通过编程的方式管理事务,这种方式带来了很大的灵活性,但很难维护。
声明式事务管理:这种方式意味着你可以将事务管理和业务代码分离。你只需要通过注解或者XML配置管理事务。
它提供了跨不同事务api(如JTA、JDBC、Hibernate、JPA和JDO)的一致编程模型。
它为编程事务管理提供了比JTA等许多复杂事务API更简单的API。
它支持声明式事务管理。
它很好地集成了Spring的各种数据访问抽象
选择。
PROPAGATION_SUPPORTS: 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY: 支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW: 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED: 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER: 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
① 用户发送请求至前端控制器DispatcherServlet。
② DispatcherServlet收到请求调用HandlerMapping处理器映射器。
③ 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果
有则生成)一并返回给DispatcherServlet。
④ DispatcherServlet调用HandlerAdapter处理器适配器。
⑤ HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
⑥ Controller执行完成返回ModelAndView。
⑦ HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
⑧ DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
⑨ ViewReslover解析后返回具体View。
⑩ DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。DispatcherServlet响应用户。
@Controller 注解标记一个类为 Spring Web MVC 控制器 Controller。Spring MVC 会将扫描到该注
解的类,然后扫描这个类下面带有 @RequestMapping 注解的方法,根据注解信息,为这个方法生成一
个对应的处理器对象,在上面的 HandlerMapping 和 HandlerAdapter组件中讲到过。
当然,除了添加 @Controller 注解这种方式以外,你还可以实现 Spring MVC 提供的 Controller 或
者 HttpRequestHandler 接口,对应的实现类也会被作为一个处理器对象
@RequestMapping 注解,在上面已经讲过了,配置处理器的 HTTP 请求方法,URI等信息,这样才能
将请求和方法进行映射。这个注解可以作用于类上面,也可以作用于方法上面,在类上面一般是配置这
个控制器的 URI 前缀
@RestController 注解,在 @Controller 基础上,增加了 @ResponseBody 注解,更加适合目前前
后端分离的架构下,提供 Restful API ,返回例如 JSON 数据格式。当然,返回什么样的数据格式,根据
客户端的 ACCEPT 请求头来决定。
@RequestMapping :可注解在类和方法上; @GetMapping 仅可注册在方法上
@RequestMapping :可进行 GET、POST、PUT、DELETE 等请求方法; @GetMapping 是 @RequestMapping 的 GET 请求方法的特例,目的是为了提高清晰度。
两个注解都用于方法参数,获取参数值的方式不同, @RequestParam 注解的参数从请求携带的参数中获取,而 @PathVariable 注解从请求的 URI 中获取
可以使用 @ResponseBody 注解,或者使用包含 @ResponseBody 注解的 @RestController 注解。
当然,还是需要配合相应的支持 JSON 格式化的 HttpMessageConverter 实现类。例如,Spring MVC
默认使用 MappingJackson2HttpMessageConverter
Spring的处理程序映射机制包括处理程序拦截器,当你希望将特定功能应用于某些请求时,例如,检查用户主题时,这些拦截器非常有用。拦截器必须实现org.springframework.web.servlet包的HandlerInterceptor。此接口定义了三种方法:
preHandle:在执行实际处理程序之前调用。
postHandle:在执行完实际程序之后调用。
afterCompletion:在完成请求后调用。
在使用Spring框架进行开发的过程中,需要配置很多Spring框架包的依赖,如spring-core、springbean、spring-context等,而这些配置通常都是重复添加的,而且需要做很多框架使用及环境参数的重复配置,如开启注解、配置日志等。Spring Boot致力于弱化这些不必要的操作,提供默认配置,当然这些默认配置是可以按需修改的,快速搭建、开发和运行Spring应用。
以下是使用SpringBoot的一些好处:
自动配置,使用基于类路径和应用程序上下文的智能默认值,当然也可以根据需要重写它们以满足开发人员的需求。
创建Spring Boot Starter 项目时,可以选择选择需要的功能,Spring Boot将为你管理依赖关系。
SpringBoot项目可以打包成jar文件。可以使用Java-jar命令从命令行将应用程序作为独立的Java应用程序运行。
在开发web应用程序时,springboot会配置一个嵌入式Tomcat服务器,以便它可以作为独立的应用程序运行。(Tomcat是默认的,当然你也可以配置Jetty或Undertow)
SpringBoot包括许多有用的非功能特性(例如安全和健康检查)。
启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下
3 个注解:
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源
自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
@ComponentScan:Spring组件扫描。
Starters可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成 Spring及其他技术,而不需要到处找示例代码和依赖包。如你想使用 Spring JPA 访问数据库,只要加入spring-boot-starter-data-jpa 启动器依赖就能使用了。
Starters包含了许多项目中需要用到的依赖,它们能快速持续的运行,都是一系列得到支持的管理传递性依赖。
Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,加载驱动、创建连接、创建statement等繁杂的过程,开发者开发时只需要关注如何编写SQL语句,可以严格控制sql执行性能,灵活度高。
作为一个半ORM框架,MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。
由于MyBatis专注于SQL本身,灵活度高,所以比较适合对性能的要求很高,或者需求变化较多的项目,如互联网项目。
优点:
基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。
与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
能够与Spring很好的集成;
提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。
缺点:
SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象
关系模型直接获取,所以它是全自动的。
而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射
工具。
相同点:都是对jdbc的封装,都是持久层的框架,都用于dao层的开发。
不同点
1、映射关系
MyBatis 是一个半自动映射的框架,配置Java对象与sql语句执行结果的对应关系,多表关联关系配置简
单。
Hibernate 是一个全表映射的框架,配置Java对象与数据库表的对应关系,多表关联关系配置复杂。
2、 SQL优化和移植性
Hibernate 对SQL语句封装,提供了日志、缓存、级联(级联比 MyBatis 强大)等特性,此外还提供
HQL(Hibernate Query Language)操作数据库,数据库无关性支持好,但会多消耗性能。如果项目需
要支持多种数据库,代码开发量少,但SQL语句优化困难。
MyBatis 需要手动编写 SQL,支持动态 SQL、处理列表、动态生成表名、支持存储过程。开发工作量相
对大些。直接使用SQL语句操作数据库,不支持数据库无关性,但sql语句优化容易。
3、开发难易程度和学习成本
Hibernate 是重量级框架,学习使用门槛高,适合于需求相对稳定,中小型的项目,比如:办公自动化
系统
MyBatis 是轻量级框架,学习使用门槛低,适合于需求变化频繁,大型的项目,比如:互联网电子商务
系统
总结
MyBatis 是一个小巧、方便、高效、简单、直接、半自动化的持久层框架,
Hibernate 是一个强大、方便、高效、复杂、间接、全自动化的持久层框架。
1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接池可解决此问题。
解决:在SqlMapConfig.xml中配置数据链接池,使用连接池管理数据库链接。
2、Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。
解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。
3、 向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。
解决: Mybatis自动将java对象映射至sql语句。
4、 对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。
解决:Mybatis自动将sql执行结果映射至java对象。
1、创建SqlSessionFactory
2、通过SqlSessionFactory创建SqlSession
3、 通过sqlsession执行数据库操作
4、 调用session.commit()提交事务
5、 调用session.close()关闭会话
1、Mybatis 和 hibernate 不同,它不完全是一个 ORM 框架,因为 MyBatis 需要 程序员自己编写 Sql
语句。
2、Mybatis 直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高,非常 适合对关系数据模型要
求不高的软件开发,因为这类软件需求变化频繁,一但需 求变化要求迅速输出成果。但是灵活的前提是
mybatis 无法做到数据库无关性, 如果需要实现支持多种数据库的软件,则需要自定义多套 sql 映射文
件,工作量大。
3、Hibernate 对象/关系映射能力强,数据库无关性好,对于关系模型要求高的 软件,如果用
hibernate 开发可以节省很多代码,提高效率
1、基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任 何影响,SQL 写在
XML 里,解除 sql 与程序代码的耦合,便于统一管理;提供 XML 标签,支持编写动态 SQL 语句,并可重用。
2、与 JDBC 相比,减少了 50%以上的代码量,消除了 JDBC 大量冗余的代码不 需要手动开关连接;
3、很好的与各种数据库兼容(因为 MyBatis 使用 JDBC 来连接数据库,所以只要 JDBC 支持的数据库MyBatis 都支持)。
4、能够与 Spring 很好的集成;
5、提供映射标签,支持对象与数据库的 ORM 字段关系映射;提供对象关系映射 标签,支持对象关系组件维护
1、SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写 SQL 语句的功底有一定要求。
2、SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
#{}是占位符,预编译处理;${}是拼接符,字符串替换,没有预编译处理。
Mybatis在处理#{}时,#{}传入参数是以字符串传入,会将SQL中的#{}替换为?号,调用PreparedStatement的set方法来赋值。
Mybatis在处理时 , 是 原 值 传 入 , 就 是 把 {}时,是原值传入,就是把时,是原值传入,就是把{}替换成变量的值,相当于JDBC中的Statement编译
变量替换后,#{} 对应的变量自动加上单引号 ‘’;变量替换后,${} 对应的变量不会加上单引 号 ‘’
#{} 可以有效的防止SQL注入,提高系统安全性;${} 不能防止SQL 注入
#{} 的变量替换是在DBMS 中;${} 的变量替换是在 DBMS 外
Dao接口即Mapper接口。接口的全限名就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名的拼接字符串作为key值,可唯一定位一个MapperStatement。
Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。
Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。
1、若Dao层函数有多个参数,那么其对应的xml中,#{0}代表接收的是Dao层中的第一个参数,#{1}代 表Dao中的第二个参数,以此类推。
2、使用@Param注解:在Dao层的参数中前加@Param注解,注解内的参数名为传递到Mapper中的参数名。
3、多个参数封装成Map,以HashMap的形式传递到Mapper中。
Mybatis动态sql可以在xml映射文件内,以标签的形式编写动态sql,执行原理是根据表达式的值完成逻
辑判断,并动态拼接sql的功能。
Mybatis提供了9种动态sql标签:trim、where、set、foreach、if、choose、when、otherwise、bind
不同的xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;
原因是namespace+id是作为Map
有联合查询和嵌套查询两种方式。
联合查询是几个表联合查询,通过在resultMap里面配置association节点配置一对一的类就可以完成;
嵌套查询是先查一个表,根据这个表里面的结果的外键id,再去另外一个表里面查询数据,也是通过
association配置,但另外一个表的查询是通过select配置的。
MyBatis多表配置方式:
一对一配置:使用做配置
一对多配置:使用+做配置
多对多配置:使用+做配置