0% found this document useful (0 votes)
2 views154 pages

Java

本文件为《代码随想录知识星球精华(最强八股文)第四版(Java篇)》的内容概述,涵盖Java基础知识、面向对象编程、数据结构(如ArrayList、LinkedList、HashMap等)及其特点和使用方法。文中详细介绍了Java的核心概念和常用集合类的实现原理及性能特点,适合Java学习者参考。

Uploaded by

588 9
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views154 pages

Java

本文件为《代码随想录知识星球精华(最强八股文)第四版(Java篇)》的内容概述,涵盖Java基础知识、面向对象编程、数据结构(如ArrayList、LinkedList、HashMap等)及其特点和使用方法。文中详细介绍了Java的核心概念和常用集合类的实现原理及性能特点,适合Java学习者参考。

Uploaded by

588 9
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

代码随想录知识星球精华(最强⼋股

⽂)第四版(Java篇))
代码随想录知识星球精华(最强⼋股⽂)第四版为九份PDF,分别是:

代码随想录知识星球⼋股⽂概述
C++篇
go篇
Java篇
前端篇
算法题篇
计算机基础篇
问答精华篇
⾯经篇

本篇为最强⼋股⽂之Java篇。

Java基础

Java概述
1、JVM、JRE、JDK之间的关系

我们知道Java⼀次编写到处运⾏,可移植性好,保证这⼀点的就是java虚拟机JVM

JRE是运⾏环境,不能创建新程序。他是包括JVM的
JDK是功能最齐全的,包括编译器和各种⼯具,我们写代码就需要这个了。

2、public、protected、default、private的区别

(1)default是默认的,什么都不写,在同⼀个包内是可见的,不使⽤任何修饰符。

(2)public能⽤来修饰类,在⼀个java源⽂件中只能有⼀个类被声明为public,⽽且⼀旦有⼀个类为public,那这
个java源⽂件的⽂件名就必须要和这个被public所修饰的类的类名相同,否则编译不能通过。public⽤来修饰类中成
员(变量和⽅法),被public所修饰的成员可以在任何类中都能被访问到。

(3)protected是受保护的,受到该类所在的包所保护,被protected所修饰的成员会被位于同⼀package中的所有
类访问到。同时,被protected所修饰的成员也能被该类的所有⼦类继承下来。

(4)private,private是私有的,即只能在当前类中被访问到,它的作⽤域最⼩。

3、final、finally、finalize的区别

(1)final就是不可变的意思,可以修饰变量、⽅法和类。修饰变量时,这个变量必须初始化,所以也称为常量。

(2)finally是异常处理的⼀部分,只能⽤在try/catch中,并且附带⼀个语句块表⽰这段语句⼀定会被执⾏,⽆论
是否抛出异常。

(3)finalize是[Link]中的⽅法,也就是每⼀个对象都有这个⽅法,⼀个对象的finalize⽅法只会调⽤⼀
次,调⽤了不⼀定被回收,因为只有对象被回收的时候才会被回收,就会导致前⾯调⽤,后⾯回收的时候出现问
题,不推荐使⽤。
4、static关键字啥作⽤?

这就要提到new对象,只有new对象之后,数据存储空间才会被分配,⽅法才能供外界调⽤。但是当没有创建对象
的时候也想要调⽤⽅法或者就是想为特定分配存储空间的时候,就需要⽤static。所以有了static,成员变量或者⽅
法就可以在没有所属类的时候被访问了。

5、⾯向对象、⾯向过程

⾯向过程的性能⽐较⾼,因为没有实例化等操作,开销⽐较⼩。

⾯向对象因为有了封装继承多态的特性,可以设计出低耦合的系统,使得系统更灵活、容易维护。

封装是指封装成抽象的类,并且对于可信的类或者对象,是可以操作的,对于不可信的进⾏隐藏。

继承是指可以使⽤现有类的所有功能,⽽且还可以在现有功能的基础上做拓展。

多态是基于继承的,他是指⽗类中定义的属性和⽅法被⼦类继承之后,可以具有不同的数据类型或者表现出不同的
⾏为,使得同⼀个属性在⽗类及其⼦类中具有不同的含义。

重载就是多态的⼀个例⼦,是编译时的多态。其实我们所说的多态是运⾏时多态,也就是说编译的时候不确定调⽤
哪个具体⽅法,⼀直延迟到运⾏时才可以确定,所以多态又叫延迟⽅法。

那,重载和重写都是实现多态的⽅式,区别是啥?

区别在于重载是编译时多态,重写是运⾏时多态。

ArrayList
ArrayList 底层就是⼀个 Object[] 数组

ArrayList 底层数组默认初始化容量为 10

1、jdk1.8 中 ArrayList 底层先创建⼀个长度为 0 的数组

2、当第⼀次添加元素(调⽤ add() ⽅法)时,会初始化为⼀个长度为 10 的数组


当 ArrayList 中的容量使⽤完之后,则需要对容量进⾏扩容:

1、ArrayList 容量使⽤完后,会“⾃动”创建容量更⼤的数组,并将原数组中所有元素拷贝过去,这会导致效率降低

2、优化:可以使⽤构造⽅法 ArrayList (int capacity) 或 ensureCapacity(int capacity) 提供⼀个初始化容量,避免刚


开始就⼀直扩容,造成效率较低

ArrayList 构造⽅法:

1. ArrayList():创建⼀个初始化容量为 10 的空列表
2. ArrayList(int initialCapacity):创建⼀个指定初始化容量为 initialCapacity 的空列表
3. ArrayList(Collection<? extends E> c):创建⼀个包含指定集合中所有元素的列表

ArrayList 特点:

优点:

1. 向 ArrayList 末尾添加元素(add() ⽅法)时,效率较⾼


2. 查询效率⾼

缺点:

1. 扩容会造成效率较低(可以通过指定初始化容量,在⼀定程度上对其进⾏改善)
2. 另外数组⽆法存储⼤数据量(因为很难找到⼀块很⼤的连续的内存空间)
3. 向 ArrayList 中间添加元素(add(int index)),需要移动元素,效率较低
1. 但是,向 ArrayList 中国位置增/删元素的情况较少时不影响;
2. 如果增/删操作较多,可考虑改⽤链表

Linklist
LinkedList 特点
数据结构: LinkedList 底层是⼀个双向链表

优点: 增/删效率⾼

缺点: 查询效率较低

LinkedList 也有下标,但是内存不⼀定是连续的(类似C++重载[]符号,将循位置访问模拟为循秩访问)

LinkedList 可以调⽤ get(int index) ⽅法,返回链表中第 index 个元素

但是,每次查找都要从头结点开始遍历

LinkedList 部分源码解读
add()⽅法
ListIterator 接⼜
1、[Link] ⽅法只能将数据添加到链表的末尾

2、如果要将对象添加到链表的中间位置

则需要使⽤ ListIterator 接⼜的 add ⽅法

1. ListIterator 是 Iterator 的⼀个⼦接⼜


3、Iterator 中 remove ⽅法

1. 调⽤ next 之后,remove ⽅法删除的是迭代器左侧的元素(类似键盘的 backspace)


2. 调⽤ previous 之后,remove 删除的是迭代器右侧的元素

4、ListIterator 中 add ⽅法

1. 调⽤ next 之后,在迭代器左侧添加⼀个元素
2. 调⽤ previous 之后,add 是在迭代器右侧添加元素

Vector
Vector 底层是数组

初始化容量为 10

扩容: 原容量使⽤完后,会进⾏扩容。新容量扩⼤为原始容量的 2 倍

Vector 是线程安全的(⾥⾯⽅法都带有 synchronized 关键字),效率较低,现在使⽤较少

如何将 ArrayList 变成线程安全的?

调⽤ Collections ⼯具类中的 static List synchronizedList(List list) ⽅法


Set
泛型:

1、jdk 1.5 引⼊,之前都是使⽤ Object[]

2、使⽤ Object[] 的缺点(2个)

1. 获取⼀个值时必须进⾏强制类型转换
2. 调⽤⼀个⽅法前必须使⽤ instanceof 判断对象类型

泛型的好处:

1、减少了强制类型转换的次数

获取数据值更⽅便

2、类型安全

调⽤⽅法时更安全

3、泛型只在编译时期起作⽤

运⾏阶段 JVM 看不见泛型类型(JVM 只能看见对应的原始类型,因为进⾏了类型擦除)

4、带泛型的类型

在使⽤时没有指定泛型类型时,默认使⽤ Object 类型

List list = new HashTestrrayList(); // 默认可以放任意 Object 类型

5、lambda 表达式

HashSet
特点:HashSet ⽆序(没有下标),不可重复

TreeSet
TreeSet ⽆序(没下标),不可重复,但是可以排序

HashSet 为 HashMap 的 key 部分;TreeSet 为 TreeMap 的 key 部分。

所以,这⾥没有重点将。重点掌握 HashMap 和 TreeMap。


Map
1. Map 和 Collection 没有继承关系
2. Map 以 (key ,value) 的形式存储数据:键值对
key 和 value 存储的都是对象的内存地址(引⽤)

Map 接⼜常见⽅法:

注意:

[Link]<K, V> 是 Map 的⼀个接⼜。接⼜中的内部接⼜默认是 public static 的。


Map的遍历⽅式

第⼀类⽅法

先获取map 的 keySet,然后取出 key 对应的 value

特点:

效率相对较低。(因为还要根据 key 从哈希表中查找对应的 value)

⽅法1:通过 foreach 遍历 [Link](),取出对应的 value

⽅法2:通过迭代器迭代 [Link](),来取出对应的 value


第⼆类⽅法

调⽤ [Link]()⽅法,获取 entrySet,然后直接从 entrySet 中获取 key 和 value。

特点:

1. 效率较⾼(直接从 node 中获取key,value)


2. 适⽤于⼤数据量 map 遍历

⽅法3:调⽤ [Link](),然后使⽤ foreach 遍历 entrySet

⽅法4:调⽤ [Link](),然后使⽤迭代器遍历 entrySet


HashMap
HashMap 概述
1. HashMap 底层是⼀个数组
2. 数组中每个元素是⼀个单向链表(即,采⽤拉链法解决哈希冲突)
单链表的节点每个节点是 Node<K, V> 类型(见下源码)
3. 同⼀个单链表中所有 Node 的 hash值不⼀定⼀样,但是他们对应的数组下标⼀定⼀样
数组下标利⽤哈希函数/哈希算法根据 hash值计算得到的
4. HashMap 是数组和单链表的结合体
1. 数组查询效率⾼,但是增删元素效率较低
2. 单链表在随机增删元素⽅⾯效率较⾼,但是查询效率较低
3. HashMap 将⼆者结合起来,充分它们各⾃的优点
5. HashMap 特点
1. ⽆序、不可重复
2. ⽆序:因为不⼀定挂在那个单链表上了
6. 为什么不可重复?
通过重写 equals ⽅法保证的

HashMap 部分源码解析

1. HashMap 默认初始化容量: 16
1. 必须是 2 的次幂,这也是 jdk 官⽅推荐的
2. 这是因为达到散列均匀,为了提⾼ HashMap 集合的存取效率,所必须的
2. HashMap 默认加载因⼦:0.75
数组容量达到 3/4 时,开始扩容
3. JDK 8 之后,对 HashMap 底层数据结构(单链表)进⾏了改进
1. 如果单链表元素超过8个,则将单链表转变为红⿊树;
2. 如果红⿊树节点数量⼩于6时,会将红⿊树重新变为单链表。

这种⽅式也是为了提⾼检索效率,⼆叉树的检索会再次缩⼩扫描范围。提⾼效率。

put() ⽅法原理
1. 先将 key, value 封装到 Node 对象中
2. 底层会调⽤ key 的 hashCode() ⽅法得出 hash 值
3. 通过哈希函数/哈希算法,将 hash 值转换为数组的下标
如果下标位置上没有任何元素,就把 Node 添加到这个位置上;
如果下标位置上有但链表,此时会将当前 Node 中的 key 与链表上每⼀个节点中的 key 进⾏ equals ⽐较
如果所有的 equals ⽅法返回都是 false,那么这个新节点 Node 将被添加到链表的末尾;
如果其中有⼀个 equals 返回了 true,那么链表中对应的这个节点的 value 将会被新节点 Node 的 value 覆
盖。(保证了不可重复)

注:

1. HashMap 中允许 key 和 value 为 null,但是只能有⼀个(不可重复)!


2. HashTable 中 key 和 value 都不允许为 null。

get() ⽅法原理
1. 先调⽤ key 的 hashCode() ⽅法得出 hash 值
2. 通过哈希函数/哈希算法,将 hash 值转换为数组的下标
3. 通过数组下标快速定位到数组中的某个位置:
如果这个位置上什么也没有(没有链表),则返回 null;
如果这个位置上有单链表,此时会将当前 Node 中的 key 与链表上每⼀个节点中的 key 进⾏ equals ⽐较。
如果所有的 equals ⽅法返回都是 false,那么 get ⽅法返回 null;
如果其中有⼀个 equals 返回了 true,那么这个节点的 value 便是我们要找的 value,此时 get ⽅法最终返
回这个要找的 value。

注:

1. 放在 HashMap 中 key 的元素(或者放在 HashSet 中的元素)需要同时重写 hashCode() 和 equals() ⽅法!!!

同时重写 hashCode() 和 equals() ⽅法


重写 hashCode() ⽅法时要达到散列分布均匀!!!
如果 hashCode() ⽅法返回⼀个固定的值,那么 HashMap 底层则变成了⼀个单链表;
如果 hashCode() ⽅法所有返回的值都不同,此时 HashMap 底层则变成了⼀个数组。
这两种情况称之为,散列分布不均匀

equals 和 hashCode⽅法⼀定要同时重写(直接⽤ eclipse ⽣成就⾏)

HashTable & Properties


1. HashTable
2. Properties

Properties 的 key 和 values 都是 String


常⽤⽅法
String getProperty(String key)
Object setProperty(String key, String value)

TreeMap
TreeMap 概述
1. TreeSet/TreeMap 是⾃平衡⼆叉树
2. TreeSet/TreeMap 迭代器采⽤的是中序遍历⽅式

TreeMap 特点

⽆序,不可重复,但是可排序

排序规则
TreeSet/TreeMap中key 可以⾃动对 String 类型或8⼤基本类型的包装类型进⾏排序

TreeSet ⽆法直接对⾃定义类型进⾏排序

直接将⾃定义类型添加到 TreeSet/TreeMap中key 会报错 [Link]

原因: 是因为⾃定义没有实现 [Link] 接⼜(此时,使⽤的是 TreeSet 的⽆参构造器)

对 TreeSet/TreeMap 中 key部分 元素,必须要指定排序规则。主要有两种解决⽅案:

⽅法⼀: 放在集合中的⾃定义类型实现 [Link] 接⼜,并重写 compareTo ⽅法

⽅法⼆: 选择 TreeSet/TreeMap 带⽐较器参数的构造器 ,并从写⽐较器中的 compare ⽅法

此时,在传递⽐较器参数给 TreeSet/TreeMap 构造器时,有 3 种⽅法:

1. 定义⼀个 Comparator 接⼜的实现类


2. 使⽤匿名内部类
3. lambda 表达式(Comparator 是函数式接⼜)
利⽤ -> 的 lambda表达式 重写 compare ⽅法
利⽤ [Link] ⽅法

两种解决⽅案如何选择呢?

1. 当⽐较规则不会发⽣改变的时候,或者说⽐较规则只有⼀个的时候,建议实现 Comparable 接⼜
2. 当⽐较规有多个,并且需要在多个⽐较规则之间频繁切换时,建议使⽤ Comparator 接⼜

⽅法⼀代码:
⽅法⼆代码:

/**
* ⽅法2:利⽤⽐较器 Comparator
* @date 2021-06-25 14:08
* @author preci
* @version 1.0
*
*/
class Cat { // 没有实现 Comparable 接⼝
int age;
public Cat(int age) {
[Link] = age;
}
}
public static void main(String[] args) {
// 1、使⽤接⼝实现类
// Set<Cat> set = new TreeSet<>(new MyCmp()); // 传递⼀个⽐较器对象给 TreeSet 构造器
// 2、使⽤匿名内部类
Set<Cat> set = new TreeSet<>(new Comparator<Cat>() {
@Override
public int compare(Cat o1, Cat o2) {
return [Link] - [Link];
}
});
// (3) 使⽤ lambda 表达式,传递⼀个⽐较器对象
// Set<Cat> set = new TreeSet<>((o1, o2) -> [Link] - [Link]);
[Link](new Cat(1));
[Link](new Cat(15));
[Link](new Cat(12)); // output:1,12,15
}
// 创建⼀个⽐较器类
class MyCmp implements Comparator<Cat> {
@Override
public int compare(Cat o1, Cat o2) {
return [Link] - [Link];
}
}

Collections ⼯具类
[Link](List list)
[Link](List list, Compataor cmp)
如果⽐较⾃定义类型,需要传⼊⽐较器或者⾃定义类型实现 Comparable 接⼜

HashSet 可以利⽤ ArrayList 构造器转换为 list

String存储原理
1、String 类型是不可变的

2、Java 中⽤双引号括起来的字符串,例如:"abc"、"def",都是直接存储在“⽅法区”的“字符串常量池”当中的。

3、为什么把字符串存储在⼀个“字符串常量池”当中呢?

1. 因为字符串在实际的开发中使⽤太频繁
2. 为了提⾼执⾏效率,所以把字符串放到了⽅法区的“字符串常量池”当中

以上代码在 JVM 中加载过程如下:

以上代码在 JVM 中加载过程如下:


String、StringBuilder、StringBuffer 部分源码解读

String 底层数组⽤ final 修饰,不可变。


StringBuilder 底层数组没有⽤ final 修饰,可变;线程不安全,效率⾼(⼀般⽤的多)
StringBuffer 底层数组没有⽤ final 修饰,可变;线程安全,效率低(⼀般⽤的少)

⽅法都采⽤ 了 synchronized 修饰

JDK1.8 中 String 部分源码解读

JDK1.8 中 StringBuilder 部分源码解读


JDK1.8 中 AbstractStringBuilder 部分源码解读

JDK1.8 中 StringBuffer 部分源码解读


⽅法参数
Java 中⽅法的参数传递只有按值调⽤,没有c++中的按引⽤调⽤。也就是说, ⽅法得到的是所有参数值的⼀个副
本。

⽅法参数共有两种类型:

1. 基本数据类型(8种)
2. 引⽤类型

Java 中⽅法参数的使⽤情况:

1. ⼀个⽅法不能修改⼀个基本数据类型的参数(即数值型或布尔型)。
2. ⼀个⽅法可以改变⼀个对象参数的状态。
3. ⼀个⽅法不能让对象参数引⽤⼀个新的对象。

Java⾯向对象

⾯向对象程序设计概述
⾯向过程:

先确定如何操作数据,再决定数据的结构。适⽤于⼩规模问题;

⾯向对象OOP:

先决定数据的结构,在考虑操作数据的算法。适⽤于⼤规模问题;

1. 类

1、类是构造对象的模板或蓝图

2、封装是处理对象的⼀个重要概念

就是将数据和⾏为组合在⼀个包中,并对对象的使⽤者隐藏具体的实现⽅式

1. 实例字段:对象中的数据数据;
2. ⽅法:操作数据的过程;
3. 对象的状态:特定对象有⼀组特定的实例字段值,这些值的集合就是这个对象的当前状态。

3、OOP原则:

1. 封装:绝对不能让类中的⽅法直接访问其他类的实例字段;
2. 扩展:可以通过扩展其他类扩展来构建新类。

4、类、超类和⼦:

1. 继承:基于已有的类创建新的类。继承⼀个类就是复⽤(继承)这些类的⽅法,⽽且可以增加⼀些新的⽅法和字

2. 继承:is-a 关系
5、定义⼦类

1. extends 关键字
2. ⽗类,又叫基类、超类
3. ⼦类,又叫派⽣类、孩⼦类
4. ⼦类⽐⽗类拥有的功能更多
1. 通过扩招⽗类定义⼦类时,只⽤指出⼦类与⽗类的不同之处
2. ⼀般的⽅法放在⽗类中,更特殊的⽅法放在⼦类中

6、覆盖⽅法

1. 超类中的有些⽅法对⼦类并不⼀定适⽤,此时需要在⼦类中覆盖(重写)⽗类⽅法
2. 使⽤ super 关键字可以调⽤⽗类⽅法,避免覆盖⽗类⽅法时调⽤⽗类⽅法造成不必要的递归
3. super 不是⼀个对象的引⽤,不能将super 赋给另⼀个对象变量

7、⼦类构造器

1. super(...) 调⽤⽗类构造器
2. 必须放在⼦类构造器的第⼀条
3. 若⼦类构造器没有显⽰调⽤⽗类构造器,将⾃动调⽤⽗类的⽆参构造器;
4. 若⽗类没有⽆参构造器,必须要在⼦类构造器中明确指明调⽤⽗类哪个构造器;否则,Java编译器就会报错

⼀个对象变量可以指⽰多种实际类型的现象成为多态。

在运⾏时能够⾃动选择适当的⽅法,成为动态绑定。

8、this 与 super 关键字

1. this 关键字
隐式参数的引⽤
调⽤该类的其他构造器
2. super 关键字
调⽤⽗类⽅法
调⽤⽗类构造器

注意:this 可以作为当前对象的引⽤,但是 super 却不可以作为⽗类对象的引⽤

2. 对象

1. 对象的状态改变必须通过调⽤⽅法实现;(如果不经过⽅法改变对象状态,说明破坏了封装性)
2. 对象的状态不能完全描述⼀个对象,每个对象有⼀个唯⼀标识;
3. 作为同⼀个类的实例,每个对象的标识总是不同的,状态往往也存在着差异。

3. 类之间的关系

依赖(uses-a):⼀个类的⽅法使⽤另⼀个类的对象;
------->
聚合(has-a) :类A的对象包含类B的对象;
◇———
继承(is-a) :⼀个更特殊的类和⼀个更⼀般的类之间的关系;
——
4. 继承与多态

1、继承层次

1. 由⼀个公共超类派⽣出来的所有类的集合成为继承层次。
2. 在继承层次中,从某个特定的类到其祖先的路径成为该类的继承链(⼦到⽗)

Java中不⽀持多继承,但是可以通过接⼜来实现

2、Java中,对象变量是多态的

1. ⽗类类型的变量既可以引⽤⾃⾝类型的变量,还可以引⽤⼦类类型的变量
2. 但是,⼦类类型的变量不可以引⽤⽗类类型的变量

3、⼦类可以调⽤⽗类public、protected、包权限的⽅法

但是⽗类不可以调⽤⼦类的特有⽅法(⼦类的特殊性)

1. 纯⽗类变量(左右都是⽗类)不可以调⽤⼦类的任何⽅法
2. 上转性变量(左边是⽗类,右边是⼦类)可以调⽤⼦类重写⽗类的⽅法(多态),但是仍然不能调⽤⼦类独有的
⽅法

4、理解⽅法调⽤

以调⽤ x.f(arg) 为例,隐式参数 x 为类 C 的⼀个对象:

(1)编译器查看对象的声明类型和⽅法名

编译器查找 C 类中所有名为 f 的⽅法和⽗类中名为 f 且可访问的⽅法(⽗类 private ⽅法不可访问)

此时,编译器知道所有可能被调⽤的候选⽅法

(2)编译器确定⽅法调⽤中提供的参数类型

重载解析: 在所有名为 f 的⽅法中,找到⼀个与所提供参数类型完全匹配的⽅法

此时,编译器已经知道需要调⽤的⽅法名字和参数类型

(3)静态绑定(编译阶段绑定)

如果是 private ⽅法、static ⽅法、final ⽅法或者构造器, 那么编译器将可以准确地知道应该调⽤哪个⽅法。

此时调⽤⽅法只⽤考虑 x 的类型(若在类 C 中找不到 f ⽅法,则向其⽗类中找),不需要考虑类 C 的⼦类(因为这


⼏种修饰的⽅法都不能被继承)
只有静态绑定成功,即编译通过了,才能进⼊运⾏阶段

(4)动态绑定(运⾏阶段绑定)

如果调⽤的⽅法依赖于隐式参数的实际类型,那么必须在运⾏时使⽤动态绑定。

虚拟机必须调⽤与 x 所引⽤对象的实际类型对应的那个⽅法

例如:x 的实际类型是 D,它是 C 类的⼦类。

如果:D 类定义了⽅法 f(String) ,就直接调⽤它;


否则:将在 D 类的超类中寻找 f(String) ,以此类推。

在覆盖⼀个⽅法的时候,⼦类⽅法不能低于⽗类⽅法的可见性。
⽐如,⽗类⽅法为 public,⼦类必须为 public。
如果⼦类遗漏了 public,编译器就会报错。

5、阻⽌继承:final 类和⽅法

(1)final 关键字

1. final 修饰字段

* 基本类型:不可更改
* 引⽤类型:不可指向新的引⽤,但是对象状态可能会改变

2. final 修饰类:该类不可以被继承

3. final 修饰⽅法:⼦类不能覆盖这个⽅法

(2)将⽅法或类声明为 final 主要原因

确保它们不会在⼦类中改变语义

6、强制类型转换

7、向上转型(upcasting)

⼦类型 -> ⽗类型


又被称为:⾃动类型转换

8、向下转型(downcasting)

⽗类型 -> ⼦类型


又被称为:强制类型转换

进⾏强制类型转换的唯⼀原因是:

在暂时忽视对象的实际类型之后, 使⽤对象的全部功能。

⽆论是向上转型还是向下转型,这两种类型之间必须要有继承关系;否则,编译器报错。
⼩结:

1. 只能在继承层次内进⾏类型转换
2. 在将超类转换成⼦类之前,应该使⽤ instanceof 进⾏检查。

通过类型转换调整对象的类型并不是⼀种好的做法。在⼀般情况下,应该尽量少⽤强制类型转换和 instanceof 运算
符。

⼤多情况下,因为多态性的动态绑定机制,我们不⽤将 Employee 转换为 Manager 也能正确调⽤⽅法;

只有在使⽤ Manager 特有⽅法才需要进⾏强制类型转换。例如,setBonus ⽅法。

但是,此时应该⾃问超类设计是否合理。是否需要重新设计超类,并添加 setBonus ⽅法,这才是更合适的选择。

抽象类
abstract 关键字:

1、abstract 修饰⽅法

1. 抽象⽅法,不⽤实现,在具体的⼦类中实现
2. 若⼦类未实现抽象⽅法,仍然要定义为抽象类

2、abstract 修饰类

1. 抽象类,包含⼀个或多个抽象⽅法(也可以⼀个也不包含)
2. 抽象类也可以包含字段和具体⽅法

3、提⾼程序清晰度

1. ⽗类中定义抽象⽅法,⼦类中具体实现
2. 变量定义为⽗类(抽象类)类型,具体实现(new)⼦类类型(上转型)
3. ⽅法调⽤,通过⽗类变量(多态,动态绑定)

思维导图⼩结:
原图链接:「代码随想录」知识星球

使⽤预定义的类

1. 对象与对象变量

1. 对象变量并没有时间包含⼀个对象,它只是引⽤⼀个对象
2. 在Java中,任何对象变量的值都是对存储在另⼀个地⽅某个对象的引⽤
3. new操作符的返回值也是⼀个引⽤
4. Java对象都存储在堆中,当⼀个对象包含另⼀个对象变量时,它只是包含着另⼀个堆对象的指针
5. Java中如果使⽤⼀个没有初始化的指针,运⾏时系统将会产⽣⼀个运⾏时错误,同时不必担⼼内存管理问题,垃
圾回收器会处理相关的事宜
6. Java中必须使⽤clone⽅法获得对象的完整副本

2. Java中的LocalDate类

1、Java类库包含两种保存时间的类

1. Date类:表⽰时间点
2. LocalDate类:⽇历表⽰法表⽰⽇期

2、⼀般通过这三种静态⼯⼚⽅法构造⼀个LocalDate

[Link](2020,4,20);
[Link]();
[Link]("2021-04-20");

3、使⽤⽅法

isLeapYear(); //此LocalDate是否闰年
minusWeeks(); minusDays(); //减去星期/天之后的表示
plusWeeks(); plusDays(); //加上星期/天之后的表示
getMonthValue(); //获得⽉的值
getDayOfMonth(); //获得该LocalDate的day
getDayOfWeek(); getMonth(); //返回枚举值,获得该天的枚举值
//(1-7),获得该⽉的枚举值(1~12)
1. 访问器⽅法:只访问对象⽽不修改对象的⽅法;(如plusDays返回⼀个新的对象,原对象还在)
2. 更改器⽅法:调⽤⽅法之后修改对象的内容。

在C++中,带有const后缀的⽅法是访问器⽅法,没有的是更改器⽅法。⽽在Java中,这两种⽅法没有明⾯上的语法
区别

⽤户⾃定义类
1. 主⼒类:通常没有main⽅法,却有⾃⼰的实例字段和实例⽅法;(例如:⾃定义User类)
2. ⼀个源⽂件中只能有⼀个公共类,但是可以有任意数量的⾮公共类。

1. 多个源⽂件的使⽤

将两个类存放在⼀个单独的类⽂件中

例如:Employee类存放在⽂件[Link]中,然后键⼊以下指令javac [Link] 此时并没有显⽰


的编译[Link],不过编译器会发发现需要使⽤这两个类时,会⾃动搜索[Link],并编译。

如果编译器发现[Link]较已有的[Link]已有更新,那么Java编译器会⾃动的编译这个⽂件。

2. 构造器⽅法

构造器与类名同名,构造器总是结合new运算符来调⽤,不能对⼀个已存在的对象调⽤构造器来达到重新设置实例
字段的⽬的。

3. ⽤var声明局部变量(在Java10中)

在Java10中,如果可以从变量的初始值推导出他们的类型,那么可以使⽤var关键字声明局部变量,⽽⽆需指定类型

var关键字只能⽤于⽅法中的局部变量,参数和字段的类型必须声明

4. 使⽤null引⽤

定义⼀个类时,最好清楚哪些字段可能为null,对于初始化时字段可能null,⼀般有两种解决⽅案:

1、宽容型

将null参数转换成⼀个⾮null值

if(n == null) name = "unknown"; else name = n;


/* Java9中有⼀个⽅法
name = [Link](n,"unknown");
达到同样的效果 */

2、严格型

直接拒绝null参数

[Link](n, "the name cannot be null");


5. 隐式参数和显式参数

1、隐式参数:

出现在⽅法名前的对象的参数;

2、显⽰参数:

位于⽅法名后⾯括号⾥的数值;

每个⽅法中,⽤this关键字指⽰隐式参数,这样可以将 实例字段 和 局部变量 明显的区分开来。

6. 封装的优点

注意不要编写返回可变对象引⽤的访问器⽅法(getter⽅法)

例如返回⼀个Date类:

因为Date类中有setDate()⽅法,所以Date对象可变,也就意味着破坏了封装性,如果需要返回⼀个可变对象的引
⽤,⾸先应该对他进⾏clone,然后再返回克隆的对象副本。

7. 私有⽅法

只要⽅法是私有的,类设计者就可以确信它不会在别处使⽤,所以可以删去,⽽如果是公共的,那么可能会因为其
他其他代码依赖这个⽅法。

8. final字段

1、final修饰符对于 基本类型 或者不可变类

类中所有⽅法都不会改变其对象,如String类的字段尤其有⽤

2、对于可变的类

使⽤final会造成混乱,final关键字只表⽰存储在该 变量 中的 对象引⽤ 不会再指向另⼀个不同的对象,⽽这个对象


还是可以更改

静态字段与静态⽅法
static的含义

1. 静态字段

1、静态字段:

静态字段属于类(即使没有创建该类的对象,静态字段也存在),每个类只有⼀个这样的字段。创建的多个对象共享
这同⼀个静态字段;

2、⾮静态字段:

每个对象都有⾃⼰的⼀个副本。
2. 静态常量

静态常量:设置为公共没问题,因为不允许再将它赋值为另⼀个值(System类中的setOut()⽅法可以将[Link]
设置为不同流,这是因为setOut⽅法是⼀个原⽣⽅法,不是在Java语⾔中实现的,因此可以绕开Java的访问控制机
制。)

3. 静态⽅法

静态⽅法是⼀个没有this参数的⽅法;(可直接通过类名调⽤)

可以使⽤对象来调⽤静态⽅法,不过很容易造成混乱,因为⽅法计算的结果与该对象⽆关。建议使⽤类名直接调⽤
静态⽅法;

两种情况下使⽤静态⽅法:

1. ⽅法不需要访问对象状态,如[Link]
2. ⽅法只需要访问类的静态字段

4. ⼯⼚⽅法

静态⽅法还有⼀种,类似LocalDate和NumberFormat 类使⽤静态⼯⼚⽅法来构造对象。

使⽤这种⽅式构造对象的原因有两个:

1、⽆法命名构造器:

构造器名字必须与类名相同,但是有事希望有两个不同的名字,分别得到货币实例和百分⽐实例

2、使⽤构造器时:

⽆法改变所构造对象的类型。静态⼯⼚⽅法可以,如⼯⼚⽅法实际将返回DecimalFormat类对象,这是
NumberFormat的⼀个⼦类

5. ⽅法参数

Java中,⽅法参数:

1. ⽅法不能修改基本数据类型的参数;(数值型或布尔型)
2. ⽅法可以改变对象参数的状态;
3. ⽅法不能让⼀个对象参数引⽤⼀个新的对象。

对象构造

1. 重载

多个⽅法有相同的名字,不同的参数,便出现了重载

重载解析: 编译器必须挑选出具体调⽤那⼀个⽅法,它⽤各个⽅法⾸部中的 参数类型 和 值类型来匹配

⽅法签名:⽅法名 + 参数类型 注意返回类型不是⽅法签名的⼀部分


2. 默认字段初始化

如果在构造器中没有显式的为字段设置初始值,会⾃动赋为初始值(0,false,null)

⽅法中的局部变量必须要明确的初始化,否则会报错

类中的字段,没有初始化会默认初始化

3. ⽆参数的构造器

⽆参构造默认将所有字段设置为默认值

当且仅当类没有任何其他构造器的时候,才会得到⼀个默认的⽆参构造器

4. 显⽰字段初始化

可以在类定义时,为每个实例字段设置⼀个有意义的初始值(如果⼀个类的所有构造器希望把某个特定的实例字段
设置为同⼀个值,这个就有⽤)

初始值也可以不是常量值,利⽤⽅法调⽤初始化⼀个字段,使⽤类中的静态变量,每次调⽤改变这个静态变量的值
即可。

5. 参数名

参数变量会遮蔽同名的实例字段,但是给实例字段加上this就可以避免这种情况。

6. 调⽤另⼀个构造器

如果构造器的第⼀个语句形如this(...),这个构造器将会调⽤另⼀个构造器,采⽤这种⽅式使⽤this关键字⾮常
有⽤,对公共的构造器代码只需要编写⼀次即可。

C++中⼀个构造器不能调⽤另⼀个构造器。

7. 初始化块

⼀个类的⽣命中,可以包含任意多个代码块,只要构造这个类的对象,这些块就会被执⾏

(这种机制不常见,通常使⽤构造器初始化对象。同时,为了避免混淆,初始化块放在字段定义之后,避免重复定
义带来的⿇烦)

⽽静态初始化在类的第⼀次加载的时候,会进⾏初始化。


Java 允许使⽤包( package) 将类组织起来

使⽤包的主要原因是确保类名的唯⼀性

包名通常是域名逆序来写,为了保证包名的绝对唯⼀性,将公司的因特⽹域名(这显然是独⼀⽆⼆的) 以逆序的形
式作为包名
1. 类的导⼊

1、⼀个类可以使⽤所属包中的所有类,以及其他包中的公共类

2、两种⽅法访问另⼀个包中的公共类

(1)完全限定名(⽐较繁琐)

包名之后跟着类名[Link] today = [Link]();

(2)import导⼊(简单常⽤)

import应该位于源⽂件的顶部,package语句的后⾯

只能使⽤星号(*) 导⼊⼀个包, ⽽不能使⽤ import java.* 导⼊以java 为前缀的所有包

3、如果 import 两个包中包含了同名的类,则会发⽣命名冲突。

主要有以下两种解决⽅案:

1. 如果只是使⽤⽤⼀个包中的同名类时,可以考虑增加⼀个特定的 import 语句来解决这个问题


2. 如果两个个包中的同名类都被使⽤时,则需要在每个类名前加上完整的包名

其中,⽅法2通⽤,但是相对可能繁琐⼀些

2. 静态导⼊

使⽤import语句导⼊静态⽅法和静态字段,⽽不只是类

1. import static [Link].*;


2. 就可以使⽤System 类的静态⽅法和静态域,⽽不必加类名前缀:
3. [Link]("Goodbye, World!"); // i.e., [Link]

但是,这种编写形式不利于代码的清晰度。最好少⽤或者不⽤

3. 在包中增加类

1. 要想将⼀个类放⼊包中,就必须将包的名字放在源⽂件的开头,包中定义类的代码之前
2. 如果没有在源⽂件中放置 package 语句,这个源⽂件中的类就属于⽆名包
3. 编译器在编译源⽂件的时候不检查⽬录结构
4. 如果包与⽬录不匹配,或许可以成功编译(如果不依赖于其他包时),但是⽆法成功运⾏(虚拟机就找不到类)

1、包作⽤域:

1. 标记为 public 的部分可以被任意的类使⽤;


2. 标记为 private 的部分只能被定义它们的类使⽤
3. 如果没有指定public 或 private, 这个部分(类、⽅法或变量)可以被同⼀个包中的所有⽅法访问

注意:变量必须显式地标记为 private, 不然的话将默认为包可见。显然,这样做会破坏封装性。

2、类路径:

(1)类路径是所有包含类⽂件的路径的集合。

(2)javac 编译器总是在当前的⽬录中查找⽂件,
但 Java 虚拟机仅在类路径中有『.』⽬录的时候才查看当前⽬录

1. 如果没有设置类路径,默认的类路径包含『.』⽬录
2. 然⽽如果设置了类路径却忘记了包含 『.』 ⽬录,则程序仍然可以通过编译, 但却不能运⾏(因为此时JVM找不
到当前⽬录的⽂件)

4. Jar⽂件

在将应⽤程序打包时,希望只向⽤户提供⼀个单独的⽂件,⽽不是⼀个包含⼤量⽂件的⽬录结构。

JAR⽂件便是为此⽽设计的,⼀个JAR ⽂件可以包含⽂件、图像、声⾳等其他类型的⽂件。

JAR⽂件是压缩的,它使⽤了我们熟悉的zip压缩格式。

5. 清单⽂件

除了类⽂件,图像的其他资源外,每个JAR⽂件还包含⼀个清单⽂件(manifest) ,⽤于描述⽂档的特殊特性

清单⽂件位于JAR⽂件的 META-INF ⼦⽬录中

6. 可执⾏JAR⽂件

jar命令中的e指定程序的⼊⼜点,指定执⾏jar⽂件时启动的类。

执⾏命令:java -jar [Link]

⽂档注释
JDK包含⼀个很有⽤的⼯具,javadoc,它可以由源⽂件⽣成⼀个HTML⽂档

ch3中联机API ⽂档就是通过对标准Java 类库的源代码运⾏javadoc ⽣成的

1. 注释的插⼊

1、javadoc从以下项中抽取信息

1. 模块
2. package
3. public 的 class 和 interface
4. public 和 protected 的字段,构造器,⽅法

2、应该为以上各个部分添加注释/** */

注释中的内容是 ⾃由格式⽂本,标记以@开始,如@param

第⼀句为概要性句⼦,javadoc将抽取第⼀句⽣成概要页

⾃由格式⽂本中可以使⽤HTML标签,如<strong></strong>表强调,<img>插⼊图⽚

其他⽂件图⽚的⽂件,应该放到包含源⽂件⽬录的⼀个⼦⽬录doc-files中
2. 类注释

1. @param 可以占据多⾏,可以使⽤HTML,⼀个⽅法的param必须放在⼀起
2. @return 可以多⾏,使⽤HTML
3. @throws 添加异常抛出注释

3. 字段注释

只需要对公共字段(静态常量static final)建⽴⽂档。

4. 通⽤注释

1. @since 引⼊该条⽬ 始于版本;


2. @author 可⽤多个;
3. @version 当亲版本。

类设计技巧
1、保证数据私有

1. 绝对不要破坏封装性
2. 当数据保持私有时, 它们的表⽰形式的变化不会对类的使⽤者产⽣影响, 即使出现bug 也易于检测。

2、对数据初始化(避免错误)

1. Java 不对局部变量进⾏初始化, 但是会对对象的实例域进⾏初始化


2. 最好不要依赖于系统的默认值, ⽽是应该显式地初始化所有的数据

3、不要再类中使⽤过多基本类型(不易重复利⽤)

1. 就是说,⽤其他的类代替多个相关的基本类型的使⽤
2. 这样会使类更加易于理解且易于修改

4、不是所有的字段都需要单独的getter和setter

字段访问器和字段更改器(有⼀些实例字段不希望其他⼈修改)

在对象中,常常包含⼀些不希望别⼈获得或设置的实例域

5、分解职责过多的类

6、类名和⽅法名要充分体现职责(见名知意)

命名类名的良好习惯是采⽤⼀个名词( Order )、前⾯有形容词修饰的名词( RushOrder)或动名词(有“ -ing” 后


缀)修饰名词(例如, BillingAddress )。

对于⽅法来说,习惯是访问器⽅法⽤⼩写get 开头, 更改器⽅法⽤⼩写的set 开头

7、优先使⽤不可变的类(避免多线程并发同时更新⼀个对象带来的⿇烦)

更改对象的问题在于,如果多个线程试图同时更新⼀个对象,就会发⽣并发更改。其结果是不可预料的。如果类是
不可变的,就可以安全地在多个线程间共享其对象。

因此,要尽可能让类是不可变的
当然,并不是所有类都应当是不可变的。

内存中对象

前⾔

“对象在内存中是怎么布局的?”

“对象头具体包括什么?”

“锁在对象的哪⾥?”

“对象怎么定位?”

⾯试过程中,你是否被问到过这些问题,如果不能完全回答出来,那么这篇⽂章⼀定要看下去。

对象的内存布局

对象实例中主要包含三部分的内容,分别为对象头、⽰例数据和对齐填充padding。下⾯分别进⾏介绍。

1、对象头

对象头中的内容主要是运⾏时元数据和类型指针。

其中运⾏时元数据主要存储的是以下六个内容:

1. 哈希值
2. GC分代年龄
3. 锁状态标志
4. 线程持有的锁
5. 偏向线程ID
6. 偏向时间戳

所以我们知道,锁是在对象头中的。

那么类型指针又是什么呢?

实际上,类型指针是指向元数据类型的InstanceKlass,⽤来确定该对象所属的类型。⽐如通过getInstance()得到对
象实例时就需要⽤到该指针。这⾥需要注意的是,不是所有对象都会保留该指针的。

以上我们说的对象指的是⾃定义对象,如果是数组对象,那么还需要记录数据的长度。

2、⽰例数据

⽰例数据中是对象真正存储的有效信息,包括程序中定义的各种字段,这⾥的字段也包括从⽗类、Object类中定义
的各种字段。

那么这么多字段,是怎么放在⼀起的呢?所以就不得不提存放规则了。

⽰例数据的存放规则遵循以下三个原则

1. ⽗类中定义的变量在⼦类之前。
2. 同⼀个类中,相同宽度的字段被分配在⼀起。如四个字节的数据⼀起。
3. 若 CompareFileds参数为true(默认为true),⼦类的窄变量可能插⼊到⽗类变量的空隙。
对齐填充padding

由于HotSpot虚拟机的⾃动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的⼤⼩都
必须是8字节的整数倍。对象头部分已经被精⼼设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数
据部分没有对齐的话,就需要通过对齐填充来补全。

总的⼀句话来说,“数据项仅仅能存储在地址是数据项⼤⼩的整数倍的内存位置上(分别为偶地址、被4整除的地
址、被8整除的地址)”⽐如int类型占⽤4个字节,地址仅仅能在0,4,8等位置上。

想讲好对齐填充,其实还是得去了解c++实现,这⾥不过多介绍。

对象的访问定位

堆中的对象是通过元数据指针找到⽅法区中的元数据信息的,那么栈帧中的对象引⽤又是如何访问找到对应的对象
呢?

对象的访问定位⽅式主要有两种:

1. 句柄访问
2. 直接访问

句柄访问⽅式:
堆区中开辟了⼀块空间⽤于存储句柄,该空间称为句柄池。句柄池分别存储着两个句柄,分别是到对象实例数据的
指针和到对象类型数据的指针。前者指向对象实例数据,后者指向对象类型数据。

栈帧的局部变量表中存储着到对象实例数据指针的位置,即指向句柄池中的第⼀个句柄。

直接访问⽅式:

直接访问⽅式相⽐句柄访问就简单多了,局部变量表中直接存储着对象实例数据的地址,⽽对象实例数据存储着⽅
法区中对象类型数据的地址。这也是Hotspot虚拟机中的访问⽅式。
总结

关于对象的内部结构、内存布局和访问定位,值得深⼊的点还有很多,这⾥只是做⼀个初步的介绍,待深⼊学习
后,会及时更新相应的内容。

Java 异常体系
Java的异常都是派⽣于Throwable类的⼀个实例,所有的异常都是由Throwable继承⽽来的。

Throwable有分为了Error类和Exception类。

Error(错误)
Error 类层次结构描述了 Java 运⾏时系统的内部错误和资源耗尽错误。

Error表⽰⽐较严重的问题,⼀般是JVM运⾏时出现了错误,如没有内存可分配抛出OOM错误、栈资源耗尽抛出
StackOverflowError错误、Java虚拟机运⾏错误Virtual MachineError、类定义错误NoClassDefFoundError。

如果出现了这样的内部错误,除了通告给⽤户,并尽⼒使程序安全地终⽌之外,再也⽆能为⼒了。

Exception(异常)
异常又分为RuntimeException和其他异常:

由程序错误导致的异常属于RuntimeException,⽽程序本⾝没有问题,但由于像 I/O 错误这类问题导致的异常属于


其他异常。

运⾏时异常RuntimeException:

顾名思义,运⾏时才可能抛出的异常,编译器不会处理此类异常。⽐如数组索引越界、使⽤的对象为空、强制类型
转换错误、除0等等。出现了运⾏时异常,⼀般是程序的逻辑有问题,是程序⾃⾝的问题⽽⾮外部因素。

其他异常:

Exception中除了运⾏时异常之外的,都属于其他异常。也可以称之为编译时异常,这部分异常编译器要求必须处
置。这部分异常常常是因为外部运⾏环境导致,因为程序可能运⾏在各种环境中,如打开⼀个不存在的⽂件,此时
抛出FileNotFoundException。编译器要求Java程序必须捕获或声明所有的编译时异常,强制要求程序为可能出现
的异常做准备⼯作。

不要被运⾏时异常的名称所迷惑,理论上所有的错误都是运⾏时发⽣的。包括Error、RuntimeException、编译时
异常等等。所有的这些都只能在程序运⾏的过程中才能碰到。编译时异常指的是编译器要求必须处理的异常,并不
是代码编译间发⽣的错误。
受检异常和⾮受检异常
字⾯理解,接受检查的异常和不接受检查的异常。

根据上⾯的信息,Error和RuntimeException运⾏时异常都不能被检查。他们都是程序运⾏过程中所产⽣的,只是
Error的错误⽐较严重,⼀般是JVM产⽣,⽽RuntimeException⼀般是程序逻辑⾃⾝的问题。

受检异常就是上⾯的编译时异常,这些异常在编译时被强制要求捕获或者声明。编译器将会为所有的受检异常提供
异常处理器。

PS:其实异常发⽣了,除了更改程序或者配置等,没有其他的⽅法。只是对引起程序不正常⼯作的原因进⾏分类,就
出现了Error和Exception(运⾏时异常和编译时异常)。

这种分类能让程序员更好的定位因此错误的原因,更⽅便更⾼效的进⾏开发,写出健壮性更好的代码。但是异常并
不能改变当前运⾏的结果,因为从程序开始运⾏的那⼀刻,整个逻辑和数据都已经固定了。

异常处理机制
主要是try-catch-finally和throw、throws关键字。

异常都是派⽣于Throwable类的⼀个实例,这个实例可以由JVM产⽣,也可以在程序中⼿动创建,⽤throw⼿动抛
出。throw的对象必须是派⽣于Throwable类的实例,其他类型⽆法通过编译。

受检异常只有两种选择。要么被捕获处理,要么被抛出,让调⽤者处理。

⽽⾮受检异常没有此强制要求。

throws是⽤来声明异常,只要是派⽣于Throwable类的都可以被声明。

⽅法应该在其⾸部声明所有可能抛出的受检异常,这是强制要求的。⾮受检异常不要求必须通过throws声明,因为
Error发⽣后对其⽆能为⼒,⽽如果有运⾏时异常,那么就是程序⾃⾝的问题,应该把时间花在修正程序的错误上,
⽽不是说明程序发⽣的可能性上。
所以编写程序的时候throws关注点是受检异常,但是⾮受检异常也可以通过throws声明,只是不强制要求⽽已。若
有多个受检异常,必须在throws中全部声明,如果⽅法没有声明所有可能的受检异常,编译器就会发出错误提⽰。

try-catch-finally⽤来捕获异常。

所有派⽣于Throwable类都可以通过 catch 捕获,try 中放可能存在异常的⽅法,如果在 try语句块中的任何代码抛


出了⼀个在 catch ⼦句中说明的异常类,那么程序将跳过 try 语句块的其余代码,并且执⾏ catch ⼦句中的处理器代
码。如果在 try 语句块中的代码没有拋出任何异常,那么程序将跳过 catch ⼦句,如果⽅法中的任何代码拋出了⼀
个在 catch ⼦句中没有声明的异常类型,那么这个⽅法就会⽴刻退出。

在⼀个 try 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。可以为每个异常类型使⽤⼀个


单独的 catch ⼦句。需要注意,如果多个catch中的异常⾮继承关系,那么catch顺序不影响结果,如果catch异常存
在类继承关系,那么⼦类的catch应该放在前⾯,⽗类的在后⾯。

如下图,catch的判断是从上到下,如果⽗类在前,那么派⽣于⽗类的都会被捕获,执⾏⽗类的处理代码,这样导致
后⾯⼦类的处理代码永远不会被执⾏。

finally⼀般⽤来关闭所占⽤的资源。如果代码抛出异常,就会终⽌剩余代码的处理,并且退出这个⽅法。

这样可能会导致⼀些程序占⽤的系统并不能被正确的释放。⽽不管是否有异常被捕获,finally中⼦句的代码都会被
执⾏,可以在这⾥正确的释放资源。

抓抛模型
所有的异常,⼀定是先有⼀个实例然后抛出。这个可以JVM完成,也可以⼿动执⾏。

这个实例是整个异常处理的源头。然后调⽤类可以利⽤try-catch-finally对异常进⾏捕获,也可以在⽅法⾸部通过
throws继续向上层抛出。
try-catch-finally是抓,throw/throws是抛。⼀个类遇到异常,要么捕获后处理,要么继续向上抛出,让调⽤者进⾏
处理。

catch中可以为空,这样程序就会忽略掉哪些异常。不进⾏处理本⾝就是⼀种处理⽅式。在catch中也可能抛出异
常,也可以⼿动抛出。这样可以改变异常的类型,使⽤异常的包装技术,原先的异常时新异常产⽣的原因,可以让
⽤户抛出⼦系统中的⾼级异常,⽽不会丢失原始异常的细节。

try-catch-finally可以灵活组合。

可以try-catch、try-finally或者try-catch-finally。可以分为以下⼏种情况

1、try中执⾏正常,忽略catch,最后执⾏finally。

2、try中出现异常,且异常在catch中声明,执⾏catch,最后执⾏finally.

3、try中出现异常,但catch中未声明,不执⾏catch,最后执⾏finally.

4、try中出现异常,且异常在catch中声明,执⾏catch时出现异常,停⽌catch中代码,执⾏finally并且抛出catch中
新出现的异常。

try {
[Link](1);
IOException e = new IOException();
throw e;
} catch (IOException e){
[Link](2);
RuntimeException e1 = new ArrayIndexOutOfBoundsException();
throw e1;
} finally {
[Link](3);
}

执⾏结果:

1
2
3
Exception in thread "main" [Link]

finally
finally不管是try或者catch中没有由异常,最后⼤概率都会执⾏。

这是因为编译器会讲 finally 块中的代码复制两份并分别添加在 try 和 catch 的后⾯。

但是如果try或者catch中出现了 [Link]()语句,则会直接退出,并不会执⾏finally模块。

finally中没有return语句 最后返回值为2
int i = 1;
try {
i = 2;
return i;
} catch (Exception e) {

} finally {
i = 3;
}

finally中有return语句 最后返回值为3

int i=1;
try {
i = 2;
return i;
} catch (Exception e) {
} finally {
i = 3;
return i;
}

上⾯两个代码只是在finally中是否存在return语句的区别,但直接结果却并不相同。

可以看到如果finally中没有return语句,程序就会把finally中的操纵数据忽略掉。

其实finally中的数据操作也是执⾏了的,但是并没有返回。这是因为在return语句返回之前,虚拟机会将待返回的
值压⼊操作数栈,等待返回。即使 finally 语句块对 i 进⾏了修改,但是待返回的值已经确实的存在于操作数栈中
了,所以不会影响程序返回结果。

在try中的数据处理完,检测到有return语句时,会先将数据压⼊操作数栈等待返回,然后去执⾏finally语句,如果
finally语句没有将新的结果压⼊操作数栈,那么只可能返回原先的结果。

从这个⾓度理解,即使finally中对数据处理,但是返回的依旧时try中的“脏数据”。

finally并不是没有执⾏,⽽是执⾏了却没有没返回。添加return语句则会将finally处理过的数据压⼊操作数栈返回,
原先的“脏数据”失效。

注意这⾥指的是基本变量,如果是引⽤类型不受此影响。因为不管在哪⾥进⾏运算,处理的都是引⽤背后的“实
体”。

Throwable
Throwable是顶层的异常类,下⾯是 Throwable 类的主要⽅法:

1、public String getMessage()

返回关于发⽣的异常的详细信息。这个消息在Throwable 类的构造函数中初始化了
2、public Throwable getCause()

返回⼀个Throwable 对象代表异常原因

3、public String toString()

使⽤getMessage()的结果返回类的串级名字

4、public void printStackTrace()

打印toString()结果和栈层次到[Link],即错误输出流

5、public StackTraceElement [] getStackTrace()

返回⼀个包含堆栈层次的数组。下标为0的元素代表栈顶,最后⼀个元素代表⽅法调⽤堆栈的栈底

6、public Throwable fillInStackTrace()

⽤当前的调⽤栈层次填充Throwable 对象栈层次,添加到栈层次任何先前信息中

⾃定义异常
Java 的异常机制中所定义的所有异常不可能预见所有可能出现的错误,某些特定的情境下,则需要我们⾃定义异常
类型来向上报告某些错误信息。

⼀般地,⽤户⾃定义异常类都是RuntimeException的⼦类

⾃定义异常类通常需要编写⼏个重载的构造器

⾃定义异常需要提供 serialVersionUID 序列化唯⼀ID,⽅便调试

⾃定义异常最重要的是异常类的名字,当异常出现时,可以根据 名字判断异常类型

注意事项
当⼦类重写⽗类带有throws声明的函数时,其声明的异常范围必须要在⽗类的⽀持范围内,即范围不能⽐⽗类⼤。
只能保持范围不变或者更精确,不能变⼤。如果⽗类没有throws,那么⼦类也不能有throws,受检异常必须在⼦类
内部捕获处理。

Java程序可以是多线程的。每⼀个线程都是⼀个独⽴的执⾏流,独⽴的函数调⽤栈。如果程序只有⼀个线程,那么
没有被任何代码处理的异常 会导致程序终⽌。如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所
在的线程结束。也就是说,Java中的异常是线程独⽴的,线程的问题应该由线程⾃⼰来解决,⽽不要委托到外部,
也不会直接影响到其它线程的执⾏。

异常处理不能代替简单的测试。捕获异常所花费的时间要远远的超过了测试的时间,所以在可能的情况下,先对执
⾏条件进⾏判断,⽽不要等执⾏出错之后捕获异常。
接⼜

1. 接⼜的概念
1、接⼜中所有⽅法⾃动为 public ⽅法,定义接⼜时,不必提供关键字 public

2、接⼜可以定义常量

3、接⼜没有实例字段

4、JAVA8之前,接⼜中不能实现⽅法,之后可以

5、实现接⼜时,必须把⽅法默认声明为 public,否则编译器报错

6、Comparable接⼜⽂档建议 compareTo ⽅法应当与 equals ⽅法兼容。

当 [Link](y) 时 [Link](y) 就应当返回 0

Java API ⼤多实现 Comparable 接⼜的类都遵从了,除了BigDecimal,跟精度有关系 1.00 和 1.0

7、[Link]()

要求数组中的元素必须属于实现了 Comparable 接⼜的类,且元素间必须可⽐较

2. 接⼜的属性
1、可以像使⽤ instanceof 检查⼀个对象是否属于某个特定类⼀样,也可以使⽤ instanceof 检查⼀个对象是否实现
了某个特定的接⼜

if(object1 instanceof Comparable) {...}

2、与接⼜中的⽅法⾃动被设置成 public ⼀样,接⼜中的字段总是

public static final

3、每个类只能有⼀个超类,但是可以实现多个接⼜,使⽤逗号将要实现的各个接⼜分隔开

3. 静态与私有⽅法
在 Java 9 中,接⼜中的⽅法可以使 private,这个⽅法可以是 静态⽅法 或者 实例⽅法

由于私有⽅法只能在接⼜本⾝的⽅法中使⽤,所以他只能⽤于接⼜中其他⽅法的辅助⽅法
4. 默认⽅法
可以为接⼜⽅法提供⼀个默认实现,必须⽤ default 修饰符标记这个⽅法

default void remove() {throw new


UnsupportedOperationException("remove")}

默认⽅法可以调⽤其他⽅法

作⽤1:

迭代器中的remove⽅法,实现迭代器需要提供 hasNext 和 next ⽅法,这些⽅法没有默认实现,它们依赖于遍历的


数据结构

作⽤2:

“接⼜演化”,保证源代码兼容,默认⽅法可以在为以前的接⼜增加⽅法时候使⽤,这样以前继承了这个接⼜的类,
就不需要修改,因为新加⼊的⽅法是 默认⽅法,⾃动有默认实现

如果在⼀个接⼜中将⼀个⽅法定义为默认⽅法,然后又在超类或另⼀个接⼜中定义同样的⽅法

在Java中:

超类优先 (如果超类提供了⼀个具体⽅法,同名⽽且有相同参数类型的默认⽅法会被忽略)

接⼜冲突 (如果⼀个接⼜提供了⼀个默认⽅法,另⼀个接⼜也提供了⼀个同名参数相同的⽅法,必须覆盖这个⽅法
来解决冲突)

如果⼀个类扩展了⼀个超类,同时实现了⼀个接⼜,并从超类和接⼜继承了相同的⽅法

遵循,类优先,即只会考虑类⽅法

5. 对象克隆
1、浅拷贝:

默认的克隆操作,只拷贝基本字段,不拷贝克隆对象中引⽤的其他对象

如果克隆对象的⼦对象是不可变的,或者⼦对象没有更改器⽅法,那么就是安全的

2、深拷贝:

重新定义 clone ⽅法,克隆所有⼦对象

Cloneable 接⼜出现和接⼜的使⽤没有关系,因为 clone ⽅法是Object类继承⽽来的

Cloneable接⼜是 标记接⼜,不含任何⽅法,唯⼀的作⽤就是允许在类型查询中视同 instanceof

3、所有数组类型都有⼀个公共的 clone ⽅法,不是受保护的

可以⽤这个⽅法建⽴⼀个新数组,包含原数组所有元素的副本
int[] arr = {1,2,3,4,5};
int[] arr2 = [Link]();

Java集合

前⾔
何集合框架都包含三⼤块内容:对外的接⼜、接⼜的实现和对集合运算的算法。

接⼜:

是代表集合的抽象数据类型。例如 Collection、List、Set、Map 等。之所以定义多个接⼜,是为了以不同的⽅式操


作集合对象

实现(类):

是集合接⼜的具体实现。从本质上讲,它们是可重复使⽤的数据结构,例如:ArrayList、LinkedList、HashSet、
HashMap。

算法:

是实现集合接⼜的对象⾥的⽅法执⾏的⼀些有⽤的计算,例如:搜索和排序。这些算法被称为多态,那是因为相同
的⽅法可以在相似的接⼜上有着不同的实现。
总述
理解Java的集合体系可以从三个层次,分别是最上层的接⼜、中间的抽象类、最后的实现类。

最上层接⼜:

表⽰不同的类型集合,Collection、Map、List、Set、Queue、Deque等,以及辅助性质的接⼜Iterator、
LinkIterator、Comparator、Comparable这些接⼜是为迭代和⽐较元素⽽准备。

中层抽象类:

在这⾥实现⼤多数的接⼜⽅法,继承类只需要根据⾃⾝特性重写部分⽅法或者实现接⼜⽅法即可。

最底层实现类:
对接⼜的具体实现,主要常⽤的有:ArrayList、LinkedList、HashMap、HashSet、TreeMap等等。

除此之外,还有Collections和Arrays类⽤来提供各种⽅法,⽅便开发。

接⼜和实现类都⽐较熟悉,在敲代码时经常碰到,但是中间的抽象类⼀般看不到,但是各种各样的抽象类却有着承
上启下的作⽤。
所有的集合都位于[Link]包下,Java集合的基本接⼜是Collection接⼜和Map接⼜,所有的实现类都是这两个接⼜
派⽣出来的。

Collection⼀次存⼀个元素,是单列集合,Map⼀次存⼀对元素,是双列集合。Map存储的⼀对元素:键–值,键
(key)与值(value)间有对应(映射)关系。

单列集合继承关系图:
双列集合继承图:

Collection接⼜
思维导图:
上⾯的这些⽅法在实现类中都必须提供,⽽每⼀个类都要提供如此多的例⾏⽅法将是⼀件很烦⼈的事情,为了能够
让实现者更容易地实现这个接⼜,Java 类库提供了⼀个类 AbstractCollection,它将基础⽅法 size 和 iterator 抽象
化,但是实现了其他的⽅法。参照上⾯单列集合继承关系图,Collection下的所有实现类都是继承⾃
AbstractCollection。AbstractCollection下又派⽣出AbstractList、AbstractSet、AbstractQueue、ArrayQueue。

List下的全部继承⾃AbstractList,⽽Set下的继承⾃AbstractSet。List 是⼀个有序集合,元素会加到容器的特定位
置,并且允许重复元素。⽽Set是⽆序集合,并且不允许元素重复。Set要保证没有重复,就需要equals()和
hashCode的正确重写。

Java中经常使⽤的语法糖"for each"循环可以与任何实现了Iterable接⼜的对象⼀起⼯作。Iterable包含了Iterator对
象,可以调⽤iterator()⽅法,返回⼀个Iterator对象。这就是常⽤的迭代器,其中包含hasNext()、next()、
remove()、forEachRemaining(Consumer)4个⽅法

Iterator()的功能简单,只能单向移动:

1、调⽤iterator()⽅法返回⼀个Iterator对象。
2、第⼀次调⽤Iterator的next()⽅法,返回序列的第⼀个元素。此后每调⽤⼀个next(),会返回序列的下⼀个元素。调
⽤next()⽅法前最好先调⽤hasNext()⽅法,判断序列后⾯是否还有元素。

3、remove()删除上次next()返回的对象,remove()只能在next()之后使⽤,并且⼀个remove()匹配⼀个next(),不能
重复调⽤.

4、使⽤hasNext()判断序列中是否还有元素

List接⼜
思维导图:

List接⼜常⽤的实现类有ArrayList、LinkedList

List接⼜继承⾃Collection。List中的⽅法和Collection中的⼤致相同,多出sort()、get()、listIterator()等⽅法,
listIterator返回⼀个链表迭代器。
List有着特殊的迭代器接⼜ListIterator,继承Iterator接⼜。Iterator是单向的,但是ListIterator是双向的,增加了
previous()和hasPreviousO⽅法⽤来反向遍历,ListIterator是⼀个功能更加强⼤的迭代器。注意只能List使⽤,其他
接⼜⽆法使⽤。

Set接⼜
Set继承Collection,其⽅法的⾏为有更严谨的定义。Set不允许添加重复元素,为了实现这⼀点就要适当的定义equals
⽅法和hashcode()⽅法。

Set常⽤的实现类有:HashSet、TreeSet、LinkedHashSet
Queue接⼜
Queue接⼜也继承⾃Collenction。很多实现类既实现类List接⼜,又实现类Queue接⼜,如LinkedList,也有只实现
了⼀个多,⽐如ArrayList就没有实现Queue接⼜,⽽PriorityQueue也没有实现List接⼜。这些交叉实现的类最容易
混淆。
Deque接⼜继承⾃Queue,在Deque的基础上更加细化,可以选择从头部或者尾部操作。另外如果Deque中使⽤
Queue的⽅法,⼀定要清楚实现类是从头部还是尾部操作的。

Map接⼜
思维导图:
Map是⼀组键值对,它是独⽴于Collection接⼜的。常⽤的实现类有:HashMap、TreeMap、HashTable、
LinkedHashMap、ConcurrentHashMap等

由双列集合继承关系图可知,Map是顶层接⼜,sortedMap接⼜提供排序选项。所有实现类都派⽣于AbstractMap
抽象类。

集合⼯具类Collections

类中⽅法都是静态⽅法,不需要创建对象就可直接使⽤。

排序

// 对指定 List 集合元素进⾏逆向排序。


void reverse(List list);

// 对 List 集合元素进⾏随机排序(shuffle ⽅法模拟了“洗牌”动作)。


void shuffle(List list);

// 根据元素的⾃然顺序对指定 List 集合的元素按升序进⾏排序。


void sort(List list);

// 根据指定 Comparator 产⽣的顺序对 List 集合元素进⾏排序。


void sort(List list, Comparator c);

// 将指定 List 集合中的 i 处元素和 j 处元素进⾏交换。


void swap(List list, int i, int j);

// 当 distance 为正数时,将 list 集合的后 distance 个元素“整体”移到前⾯;


// 当 distance 为负数时,将 list 集合的前 distance 个元素“整体”移到后⾯。
// 该⽅法不会改变集合的⻓度。
void rotate(List list, int distance):
查找、替换

// 使⽤⼆分搜索法搜索指定的 List 集合,以获得指定对象在 List 集合中的索引。


// 如果要使该⽅法可以正常⼯作,则必须保证 List 中的元素已经处于有序状态。
int binarySearch(List list, Object key);

// 根据元素的⾃然顺序,返回给定集合中的最⼤元素。
Object max(Collection coll);

// 根据 Comparator 指定的顺序,返回给定集合中的最⼤元素。
Object max(Collection coll, Comparator comp);

// 根据元素的⾃然顺序,返回给定集合中的最⼩元素。
Object min(Collection coll);

// 根据 Comparator 指定的顺序,返回给定集合中的最⼩元素。
Object min(Collection coll, Comparator comp);

// 使⽤指定元素 obj 替换指定 List 集合中的所有元素。


void fill(List list, Object obj);

// 返回指定集合中指定元素的出现次数。
int frequency(Collection c, Object o);

// 返回⼦ List 对象在⽗ List 对象中第⼀次出现的位置索引;


// 如果⽗ List 中没有出现这样的⼦ List,则返回 -1。
int indexOfSubList(List source, List target);

// 返回⼦ List 对象在⽗ List 对象中最后⼀次出现的位置索引;


// 如果⽗ List 中没有岀现这样的⼦ List,则返回 -1。
int lastIndexOfSubList(List source, List target);

// 使⽤⼀个新值 newVal 替换 List 对象的oldVal


boolean replaceAll(List list, Object oldVal, Object newVal);

复制

void copy(List dest,List src)


数组⼯具类Arrays
类中⽅法都是静态⽅法,不需要创建对象就可直接使⽤。

// 对数组array的元素进⾏升序排序
void sort(array);

// 查询元素值val在数组array中的下标
int binarySearch(array,val);

// 该⽅法将会把⼀个数组array转换成字符串
String toString(array);

// ⽐较两个数组是否相等
boolean equals(array1,array2);

// 把数组array所有元素都赋值为val
void fill(array,val);

// 把数组array复制成⻓度为length的新数组
copyof(array,length);

// 查看数组array中是否有特定的值val1
[Link](array).contains(val1);

// 基于指定数组的内容返回哈希码
[Link](array);

Java 8 新特性

Lambda 表达式
将代码块传递到某个对象,这个对象将会在某个时间调⽤(延迟执⾏)

1. 语法
1、即使lambda表达式没有参数,仍然要提供空括号,类似于⽆参⽅法

() -> {....}

2、如果可以推导出⼀个lambda表的参数类型,则可以忽略其类型

(s1, s2) -> {...}

3、如果⽅法只有⼀个参数,⽽且这个参数类型可以推导出,那么可以省略⼩括号

a -> {....}

4、如果⼀个lambda表达式只在⼀些分⽀返回值,这是不合法的
如(int x) -> {if (x > 0) return 1;}

2. 函数式接⼜
1、只有⼀个抽象⽅法的接⼜,可以提供⼀个lambda表达式

2、在Java中,对lambda表达式所能做的也就是转换为 函数式接⼜

3、ArrayList类有⼀个 removeIf ⽅法,它的参数就是⼀个 Predicate,这个接⼜⽤来传递 lambda 表达式

如,下⾯的语句将从⼀个数组列表删除所有null值

[Link](e -> e == null)

4、供应者(supplier)没有参数,调⽤时会⽣成⼀个T类型的值,⽤于实现懒计算

LocalDate hire = [Link](day,


new LocalDate(1970,1,1));

这种情况下,每次都会构建默认的LocalDate,⽽day 为 null 的情况很少,因此,可⽤通过supplier实现延迟这个计


LocalDate hire = [Link](day, ()->new LocalDate(1970,1,1));

此时只有在需要值时,才会调⽤供应者

3. ⽅法引⽤

[Link]::println;

1、指⽰编译器⽣成⼀个函数式接⼜的实例

覆盖这个接⼜的抽象⽅法来调⽤给定的⽅法

2、类似于lambda表达式,⽅法引⽤也不是⼀个对象

3、要⽤ ::运算符分隔⽅法名 与 对象 或 类名

主要有三种情况:

(1)对象::实例⽅法 lambda参数作为⽅法的显⽰参数传⼊

(2)类::实例⽅法 String::trim ,lambda表达式会成为隐式对象

(4)类::静态⽅法 Integer::valueOf,lambda表达式会传递到这个静态⽅法

只有当那个lambda表达式的体只调⽤⼀个⽅法⽽不做其他操作时,才能把lambda表达式重写成⽅法引⽤

4、⽅法引⽤不能独⽴存在,总是会转换为函数式接⼜的实例

5、包含对象的⽅法引⽤ 与 等价的lambda表达式还有⼀个细微差别

如果对象为空,⽅法引⽤会直接抛出异常,⽽lambda表达式只有在调⽤时才会抛出异常
6、可以在⽅法引⽤中使⽤ this 、super参数

4. 构造器引⽤
1、与⽅法引⽤类似,不过⽅法名为 new

Person::new这就是Person构造器的⼀个引⽤,具体哪⼀个构造器呢,取决于上下⽂

2、数组的构造器引⽤Integer[]::new

5. 变量作⽤域
1、lambda表达式可以“捕获”外围作⽤域中变量的值,只要确保所捕获的值时明确定义的

(1)lambda表达式中,只能引⽤值不会改变的外部变量
(2)lambda表达式中捕获的变量必须是 final 变量

2、lambda表达式的与 嵌套块 有相同的作⽤域,在lambda表达式中声明⼀个与局部变量同名的参数或局部变量是


不合法的

3、lambda表达式中的 this 含义与外⾯⼀致

Java 源码学习

Linklist

1. 概述
LinkedList底层数据结构是链表,是实现了List接⼜和Deque接⼜的双端链表;
其能够⾼效的实现插⼊删除操作, ⽽且也拥有了队列(简单的队列、双端队列)所拥有的特性。

和ArrayList相⽐,因为其没有实现RandomAccess,是以下标进⾏访问元素,
所以对于元素访问不及ArrayList,随机访问元素慢。

2. 细节
1. 对于普通的单向链表⽽⾔,按照下标查找查找的时间复杂度为O(n),但是对于双向链表按照下标查找,可以利⽤
其特性,选择从头部查找还是从尾部查找,所以更精确来讲,双向链表按照下标查找是O(n/2),这应该也是
LinkedList使⽤双向链表实现的⼀个原因吧,在源码⾥⾯也能够发现绝⼤多数的CRUD的API都是先按照下标找
到对节点,再进⾏后续操作。
2. 默认的插⼊⽅式是尾部插⼊,不过也可以明确指定插⼊⽅式(头部插⼊、中间插⼊、尾部插⼊),remove⽅法
同理,有这么多插⼊、删除的形式,为了就是能够在其基础上,实现栈、队列等数据结构。
3. 具有fail-fast机制。
CopyOnWriteArrayList
1. 概述

CopyOnWriteArrayList可以理解成是ArrayList的线程安全的版本,内部也是使⽤数组实现;

每次对数组的修改都完全拷贝⼀份新的数组来修改,修改后再替换原来的⽼数组,这样⼦只阻塞的了写操作,不阻
塞读操作,实现读写分离。

有⼀问题是,其⽆法保证实时的⼀致性,只能保证最终的⼀致性,所以适⽤于对实时性要求不⾼,读多写少的场
景,譬如⿊⽩名单。
2. 部分细节
1. 每次对原数组的修改操作,都先加锁,然后copy⼀份新的数组,在新数组上做修改
2. 锁使⽤的是ReentrantLock,独占的不公平锁
3. 整个数组使⽤volatile修饰保证了可见性,结合锁之后,也就确保了单个api操作的线程安全
4. 内部⽆size属性,直接通过获取当前数组⼤⼩得到对应的元素的个数
5. 需要注意的是,因为写时会复制⼀个近乎等⼤⼩的数组,所以需要考虑内存空间和集合使⽤的业务场景
6. 看完源码之后,加深了[Link]、[Link]的理解

3. 相似的思想
使⽤fork创建⼦进程,避免了内核直接将整个⽗进程的虚拟内存的数据(堆、栈、数据段、执⾏代码段)内容整个
复制并分配给⼦进程。

刚开始是让⽗⼦进程共享同⼀个副本,只有在需要写⼊的时候,数据才会被复制,从⽽⽗⼦进程才拥有各⾃的副
本,在此之前都属于共享的读。

这样之后,对⽗进程实际物理内存中页的复制被推迟到发⽣写⼊的时候。

并且有的时候,共享的副本可能都不会被写(fork之后,调⽤exec)。

通过写时复制之后,fork的实际开销就变成了复制⽗进程的页表和给⼦进程创建PCB。

treemap

概述:

TreeMap底层基于红⿊树实现,可以保证在log(n)的时间复杂度内完成containsKey、get、put、remove操作。

同时因为其由红⿊树构成,也就是说明了能够维持内部元素的有序性,关于⽀持内部元素有序性的集合还有
LinkedHashMap。

并没有对红⿊树做很深的了解,只是跟着整体的思路,使⽤AVL树实现了⼀遍。

部分细节:

1. 红⿊树相⽐于AVL树,牺牲了部分平衡性,以换取删除/插⼊操作时少量的旋转次数,整体来说,性能优于AVL
树,但是做了性能测试,发现优化了的AVL树和红⿊树相⽐差不了太多。
2. AVL树为了维护严苛的平衡条件,在破坏了平衡之后(插⼊、删除),需要执⾏旋转操作。共分为四种:左单
旋、先左后右旋、右单旋、先右后左旋。
3. TreeMap内部⽆扩容的概念,因为使⽤的是树的链式存储结构
4. ⽀持范围查找,查找最近的元素
5. 以为内部是按照key进⾏排序的,所以不⽀持key为null
6. 排序依据,根据存放的对象是否实现Comprable接⼜。若实现了,则依据其⾃定义的compareTo⽅法,否者需要
⾃定义外部⽐较器(Comparator),若是都为实现,则报错。

相关练习题:(Leetcode)

285. ⼆叉搜索树中的中序后继节点(plus会员)
108. 将有序数组转换为⼆叉搜索树
110. 平衡⼆叉树
450. 删除⼆叉搜索树中的节点
270. 最接近的⼆叉搜索树值(plus会员)

PriorityQueue

概述:

优先队列,是0个或者多个元素构成的集合,集合中按照某种排序⽅式(元素⾃⾝的权重)进⾏排序,不保证内部
元素整体有序,但是每次弹出的元素的优先级最⾼/低。

内部的数据结构是堆,因为堆底层的结构是完全⼆叉树,对于树的存储包含有链式存储或者顺序存储,
PriorityQueue使⽤的是顺序存储,所以使⽤的是Object[]数组,然后利⽤完全⼆叉树的性质,解决⽗⼦节点关系问
题。

默认实现是⼩根堆

部分细节:

1. 未指定初始化容量⼤⼩,默认为11,感觉对于底层使⽤数组实现的集合,默认⼤⼩的规定好要没有啥规律可循
2. 扩容⽐其他集合多了⼀步,在数组长度 < 64 时,扩容为原先的两倍+2,超过64时,扩容为原先的1.5倍,同时做
了放溢出处理,⽀持最⼤元素个数Integer.MAX_VALUE;
3. 删除和插⼊操作均会破坏当前的堆结果,所以每次都需要调⽤siftUp、siftDown动态调整
4. 插⼊操作是插⼊当前堆的末尾,调⽤siftUp,⾃底向上调整
5. 删除操作弹出堆顶元素,然后将堆最后⼀个元素置于堆顶,调⽤siftDown,⾃顶向下调整
6. 同样具有fast-fail机制

相关题⽬:

题⽬的话,感觉还是根据具体情况使⽤,对于⼦序列DP问题也可以引⼊优先队列来优化暴⼒搜索的过程

(1) 频率相关问题,结合map使⽤
(2) TopK问题(海量数据处理)

HashMap
概述:

HashMap从宏观来看是基于Hash表实现的,hash冲突的解决⽅式是拉链法。

具体实现上,在JDK1.8时有做过优化。引⼊了红⿊树,避免因为⼤量的冲突导致某个位置链表长度过长,使得某些
元素的查询效率变低。

同时将链表插⼊操作的头插法改为了尾插法,保证了插⼊顺序的同时,也避免了并发操作下的⼀些异常(并发操
作,肯定要想办法解决线程安全嘛,不然出现异常才是正常现象吧)。同时扩容后的rehash的过程也做了优化。

部分细节:
1. 内部采⽤Node数组来表⽰Hash表,数组长度为2的幂次,主要是为了能够通过位运算获取key的索引位置,提升
计算的效率
2. 链表长度>8时会进⾏树化,也就是转变为红⿊树。⼤前提是整个Node数组容量>64。同时当树节点数量⼩于6时
又会变为链表
3. 既然是⼀个集合类,肯定涉及到动态扩容,HashMap内部的扩容机制,默认是扩容为原数组长度的2倍,其次在
初始化是若是没有指定容量⼤⼩,直接从0扩容为16。如果初始化时指定了容量⼤⼩,则整个Hash表的容量⼤⼩
为最接近指定容量且⼤于指定容量的2的幂次
4. 扩容过程也伴随着rehash的过程,1.8之前是扩容后,对所有元素rehash,之后映射到对应的位置。1.8时做了优
化,巧⽤的运⽤了2次幂的扩展(指长度扩为原来2倍)之后,元素的位置要么是在原位置,要么是在原位置再移动2
次幂的位置这个特点,避免了rehash的过程,⽽是直接将key的hash值和oldCap相与,如果为0,则保持原位,
如果为1,则放⼊到原位+oldCap的位置。
5. 内部没有capacity变量,Node数组的容量⼤⼩是在扩容时确定的
6. 默认的负载因⼦为0.75,和Redis中的⼀样,ThreadLocal的为黄⾦分割点
7. 对key取hash值,采⽤的是⾼地位异或求值,从hash分布的情况来看,离散型更好。
8. Redis中的rehash过程和HashMap1.8之前的实现很像,不过是渐进式的,同时其内部是有新旧两个hash表的,在
删除、查找、更新操作时,会操作两个hash表,不过在新增操作时,操作的是新的hash表。

相关题⽬:

1. ⽤于dfs、递归中的缓存操作,防⽌超时
2. ⽤于查找、频率统计和查重,需要明确key和value的分别代表什么
3. 映射关系,譬如判断给定的字符是否属于ABA这种格式

Java 编译过程

前⾔
“说下Java代码编译和执⾏过程?”

“为什么需要执⾏引擎,它的⼯作过程是怎样的?”

“什么是解释器,什么是JIT编译器,它们有什么区别?”

“为什么有了JIT编译器还需要解释器?”

⾯试过程中,你是否被问到过这些问题,如果不能完全回答出来,那么这篇⽂章⼀定要看下去。

执⾏引擎
从上图可以看出,JVM包含两个⼦系统和两个组件。

两个⼦系统:

1. 类加载⼦系统:负责将字节码⽂件装载内存成为⼀个对象。
2. 执⾏引擎:包含解释器、JIT编译器、垃圾收集器(GC)

两个组件:

1. 运⾏时数据区
2. 本地接⼜

执⾏引擎概述
执⾏引擎是JVM核⼼组成部分之⼀。这⾥的执⾏引擎特制虚拟机(VM)中的执⾏引擎,其实物理机中也存在执⾏引
擎。物理机中执⾏引擎是直接建⽴在处理器、缓存、指令集和操作系统层⾯上的,⽽虚拟机中的执⾏引擎是由软件
⾃⾏实现的。因此,可以不受物理条件制约地定制指令集与执⾏引擎的结构体系,能够执⾏那些不被硬件直接⽀持
的指令集格式。那么,什么是不被硬件直接⽀持的指令集格式呢,别急,我们把这个问题放⼀放,后⾯⾃有解答。

⼯作流程
为什么需要执⾏引擎呢?我们知道,jvm主要任务是装载字节码到其内部,但字节码不能直接运⾏在操作系统上,
因为它内部仅仅包含⼀些能被jvm所识别的字节码指令、符号表以及其他辅助信息。所以,想让java程序执⾏起
来,需要执⾏引擎将字节码指令解释/编译为对应平台上的本机机器指令。

执⾏引擎的执⾏过程:

如下图所⽰,执⾏引擎执⾏过程中执⾏什么指令依赖于程序计数器(PC寄存器)中的地址,它会根据该地址找到对
应的指令。执⾏完成⼀条指令操作后,程序计数器会更新下⼀条需要被执⾏的指令地址。

在⽅法执⾏过程中,执⾏引擎也可能根据虚拟机栈中局部变量表中存储的对象引⽤找到堆中对应的对象实例信息,
还可能通过对象头中的元数据指针定位到⽬标对象的类型信息。
指令集
包括:机器码&指令&汇编&字节码

了解代码编译和执⾏过程之前,先回顾下⼀些基础知识。

机器码:

各种⽤⼆进制编码⽅式表⽰的指令

任何⽤机器码编写的程序,cpu可以直接读取并运⾏,速度最快,不过可读性差

指令和指令集:

⽅便⼈们记忆,将机器码中的0、1序列简化表⽰为对应的指令。

由于硬件平台不同,执⾏同⼀个操作,对应的机器码可能不同,因此不同的硬件平台的同⼀个指令,对应 的机器码
可能不同(如mov)

机器指令与CPU 紧密相关,不同的机器⽀持不同的指令。

每个平台所⽀持的指令称为指令集,如x86架构平台的机器⽀持x86指令集,ARM架构的机器⽀持ARM指令集

汇编:

由于指令的可读性还是太差,于是⼈们又发明了汇编语⾔。

在汇编语⾔中,⽤助记符(Mnemonics)代替机器指令的操作码,⽤地址符号(Symbol)或标号(Label)代替指
令或操作数的地址。

在不同的硬件平台,汇编语⾔对应着不同的机器语⾔指令集,通过汇编过程转换成机器指令。
由于计算机只认识指令码,所以⽤汇编语⾔编写的程序还必须翻译成机器指令码,计算机才能识别和执⾏。

字节码:

⼀种中间状态的⼆进制代码(⽂件),它⽐机器码更加抽象,需要直译器转译后才能转为机器码。

主要是为了实现跨平台的特点,使得特定软件运⾏和软件环境、与硬件环境⽆关。

实现⽅式是通过编译器和虚拟机器。编译器将源码编译为字节码,特定平台上的虚拟机器将字节码转译为⾃⼰能识
别并执⾏的指令。

Java代码执⾏与编译
⾸先我们需要明确的是,⼤部分程序代码在转换为物理机器能理解的指令集之前,会经过以下步骤:

其中,橙⾊部分由javac编译器完成,⽣成线性的字节码指令。通过 javac 编译器,我们可以很⽅便地将 java 源⽂件


编译成字节码⽂件。相对于JIT编译器,因为其处于编译的前期,因此又被称为前端编译器。

这个过程和JVM⽆关,因为对于 Java 虚拟机来说,其实际输⼊的是字节码⽂件,⽽不是 Java ⽂件。

javac编译过程如下所⽰:

执⾏步骤中的绿⾊部分由解释器完成,这也说明了java语⾔的半解释性。

蓝⾊对应的是如下的编译过程:
综上,当JVM启动时会根据预定义的规范对字节码进⾏逐⾏解释的⽅式执⾏,将字节码⽂件中的内容“翻译”为操作
系统能理解的指令。

那么问题来了,为什么说Java是半解释型半编译型语⾔呢?

实际上,在jdk1.0时,java是解释执⾏的,后来做了优化。执⾏引擎在执⾏字节码指令时既可以选择解释器也可以选
择编译器。这样JVM在执⾏java代码时,可以将解释执⾏和编译执⾏结合起来进⾏。

解释器
问题引出
为什么需要字节码作为中介,不直接将源代码编译为机器能识别的机器指令呢?

这是因为Java设计者的初衷是为了实现跨平台,因此避免采⽤类似C、C++那种静态编译的⽅式直接⽣成机器指
令,从⽽诞⽣了通过编译器在运⾏时逐⾏解释字节码指令从⽽执⾏程序的想法。所以,解释器真正意义上所承担的
⾓⾊就是⼀个运⾏时“翻译者”,将字节码⽂件中的内容“翻译”为对应平台的本地机器指令执⾏。

解释器的分类
字节码解释器

需要在执⾏代码时通过纯软件代码模拟字节码的执⾏,效率⼗分低

模板解释器

将每⼀条字节码和⼀个模板函数相关联,模板函数中直接产⽣这条字节码执⾏时的机器码,能提⾼性能
Hotspot中,解释器主要有Interpreter模块和Code模块组成。

Interpreter模块:

实现了解释器的核⼼功能

Code模块:

⽤于管理Hotspot在运⾏时⽣成的本地机器指令
解释器的现状
虽然解释器在设计和实现上很简单,但基于它执⾏已经沦为低效的代名词。针对该问题,jvm平台⽀持⼀种称为及
时编译的技术。它的⽬的是避免函数被解释执⾏,⽽是将整个函数体编译为机器码,每次函数执⾏时,只执⾏编译
后的机器码,实现提⾼程序的执⾏效率。

JIT编译器
JIT编译器,英⽂为Just In Time Compiler。根据它,JVM可将源代码直接编译为和本地机器平台相关的机器语⾔指
令。既然根据及时编译的技术,可以提⾼程序的执⾏效率,那么为什么不把解释器抛弃掉呢?为什么要将JIT编译器
和解释器并存呢?

JVM执⾏代码主要有两种⽅式:

解释执⾏:

⼀段代码,解释⼀⾏执⾏⼀⾏。即源程序被编译为字节码⽂件后,解释器逐⾏将字节码解释成机器指令并执⾏。

编译执⾏:

事先已经被编译成机器码,直接执⾏,不⽤解释。源程序被编译为字节码⽂件后,JIT编译器将字节码编译为机器指
令,再由cpu执⾏。

虽然JIT编译器能提⾼执⾏效率,但是在执⾏机器指令前,它需要对字节码进⾏编译,因此造成响应时间较长,⽽解
释器可以直接对字节码进⾏解释执⾏。因此,Hotspot虚拟机采⽤⼆者并存的⽅式,在jvm执⾏过程中,⼆者相互协
作,各⾃取长补短,尽全⼒去权衡编译本地代码的时间和直接解释执⾏代码的时间,保证执⾏效率。

在jvm启动时,解释器可以先发挥作⽤,⽽不必等待JIT编译器全部编译完成后再去执⾏,省去不必要的编译的时
间。⽽随着程序运⾏时间的推移,即时编译器逐渐发挥作⽤,根据热点探测功能,将有价值的字节码编译为本地机
器指令,以换取更⾼的执⾏效率。

以下通过⼩例⼦查看JIT编译器的存在:
package jvm;
/**
* 运⾏后,打开jconsole,查看JIT编译器
*/

import [Link];

public class JITTest {


public static void main(String[] args) {
ArrayList list = new ArrayList();
for (int i=0;i<100;i++){
[Link]("Hello!");
try {
[Link](1000);
} catch (InterruptedException e) {
[Link]();
}
}
}
}

运⾏以上程序过程中,打开jconsole⼯具,查看指定的Java进程:
在jconsole的VM概要⼀栏中,可以看到JIT编译器是确实存在的:

总结
关于执⾏引擎、解释器、编译器、JIT编译器,值得深⼊的点还有很多,⽐如热点探测、如何判断热点代码、热点检
测⽅式,这篇⽂章只是做⼀个初步的介绍,待深⼊学习后,会及时更新相应的内容。

Java 后端编译优化
虚拟机最开始是通过解释器进⾏解释执⾏的,解释器是最基础的,但是没有优化,效率和速度都有待提⾼。但是只
依靠解释器不进⾏优化也是可以的。即时编译器和提前编译器都是采取的优化措施,是可选择的。

即时编译器
当虚拟机发现某个⽅法或代码块的运⾏特别频繁,就会把这些代码认定为“热点代码”(Hot Spot Code),为了提
⾼热点代码的执⾏效率,在运⾏时,虚拟机将会把这些代码编译成本地机器码,并以各种⼿段尽可能地进⾏代码优
化,运⾏时完成这个任务的后端编译器被称为即时编译器。

解释器与编译器
主流的虚拟机内部都同时包含解释器与编译器,⼆者各有优势:当程序需要迅速启动和执⾏的时候,解释器可以⾸
先发挥作⽤,省去编译的时间,⽴即运⾏。当程序 启动后,随着时间的推移,编译器逐渐发挥作⽤,把越来越多的
代码编译成本地代码,这样可以减少 解释器的中间损耗,获得更⾼的执⾏效率。

编译器是将代码转换为本机机器码保存下来,需要占⽤内存。所以内存资源不多的时候⽤解释器执⾏,反之可以⽤
编译器来提⾼程序执⾏速度和效率。同时,编译器的优化不能保证完全成功,⼀些激进优化措施是有失败的风险,
此时就不能再⽤编译器,要退回到解释器状态继续执⾏。
虚拟机有三种编译模式:

混合模式:解释器与编译器搭配使⽤

解释模式:解释器⼯作,编译器完全不⼯作

编译模式:优先采⽤编译器,但编译器⽆法进⾏时采⽤解释器

HotSpo内置的编译器有:“客户端编译器(C1编译器)”和“服务端编译器(C2编译器)”

分层编译:

第0层:程序纯解释执⾏,并且解释器不开启性能监控功能(Profiling)。

第1层:使⽤客户端编译器将字节码编译为本地代码来运⾏,进⾏简单可靠的稳定优化,不开启性能监控功能。

第2层:仍然使⽤客户端编译器执⾏,仅开启⽅法及回边次数统计等有限的性能监控功能。

第3层:仍然使⽤客户端编译器执⾏,开启全部性能监控,除了第2层的统计信息外,还会收集如分⽀跳转、虚⽅法
调⽤版本等全部的统计信息。

第4层:使⽤服务端编译器将字节码编译为本地代码,相⽐起客户端编译器,服务端编译器会启⽤更多编译耗时更
长的优化,还会根据性能监控信息进⾏⼀些不可靠的激进优化。

解释器执⾏的启动速度是⽐编译器快的,所以程序先再第0层⽤解释器执⾏。

⽽编译器的启动速度也有区别,C1编译器启动要⽐C2编译器快,这是因为C2会进⾏更多的优化,所以C2编译出的
代码要⽐C1的代码执⾏效率好很多。

从启动速度上看 解释器>C1编译器>C2编译器,从程序执⾏效率上看 4>1>2>3>0, 随着C1性能监控搜集的信息越来


越多,性能开销也越⼤。第⼆层和第三层基本是为第4层服务的,减少第四层编译执⾏的时间。

程序最后会在1或者4编译,C2编译出的效果最好,所以最后要进⼊4,但是如果⼀段程序本⾝就简单,可以优化的
地⽅不多,那么就不值得花费时间去搜集程序执⾏信息,直接去第1层⽤C1编译就可以,这样编译的效果也很好。

通常情况下,热点代码会被第三层C1编译,然后交给第4层的C2编译。

在字节码较少的情况下,此时性能监控收集的数据很少,就交给第1层的C1进⾏编译。

在C1⽐较繁忙时,会在第0层解释执⾏收集程序执⾏状态数据,然后直接交给第4层的C2编译。

在C2繁忙的时候,先交给第2层的C1,再交给低3层的C1,最后交给第4层的C2。
热点代码
1. 被多次调⽤的⽅法,触发标准编译请求
2. 被多次执⾏的循环体,触发栈上编译请求

热点代码编译的都是整个⽅法体。即使是⽅法中的循环体被认定是热点代码,热点只是整个⽅法的⼀部分,但编译
器依然必须以整个⽅法作为编译对象,只是执⾏⼊⼜(从⽅法第⼏条指令开始执⾏)有不同。

编译时会传⼊⼊⼜点字节码序号,这种编译⽅式因为 编译发⽣在⽅法执⾏的过程中因此称为”栈上替换(OSR)“,
即⽅法还在⽅法栈上但是具体执⾏的指令已经被替换了,注意⽅法栈本⾝并没有改变也没有移动,只是执⾏到热点
代码时去内存中读取编译后的⽅法。因为循环体是从某条指令开始执⾏,⼀轮完成之后会再次跳转到开始位置,所
以只传⼊⼀个⼊⼜点序号就可以(⼊⼜点也是出⼜)。

热点探测:

判断⼀个⽅法是不是热点代码,需不需要出发即时编译。主流有两种⽅法

1、基于采样的热点探测:

虚拟机会周期性地检查各个线程的调⽤栈顶,如果发现某个(或某些)⽅法经常出现在栈顶,那这个⽅法就是“热
点⽅法”。好处是实现简单,只需要采样当前的栈顶即可。缺点是精度不⾏,容易因为受到线程阻塞或别的外界因
素的影响⽽扰乱热点探测。

2、基于计数器的热点探测:

虚拟机为每个⽅法建⽴计数器,统计执⾏次数,超过⼀定阈值则认为是热点代码。好处是结果精确,缺点是⽐较⿇
烦,为每个⽅法建⽴计数器。

HotSpot采⽤此种⽅法,给每个⽅法准备两类计数器:⽅法调⽤计数器和回边计数器。

⽅法调⽤计数器:

统计⽅法被调⽤的次数。⽅法调⽤时判断是否存在编译过的版本,如果存在则执⾏编译后的本地代码。如果不存
在,⽅法计数器+1,和阈值⽐较判断是否触发即时编译。⼀旦超过,申请即时编译。

统计绝对次数:

统计⽅法所有时间段内执⾏次数,计数器值不会减少。只要程序执⾏⾜够多,⼀定会触发即时编译。

统计相对次数:

统计⼀段时间执⾏次数,如果超出时间还未能出发即时编译,统计次数减少⼀半。这个称为⽅法调⽤计数器热度的
衰减,这段时间半衰周期。默认统计相对次数。

注意上⾯这两个⽅法只有在⽅法调⽤器种可以选择,回边调⽤器只能记录绝对次数。

回边计数器:

统计⼀个⽅法中循环体代码执⾏的次数,在字节码中遇到控制流向后跳转的指令就称为“回边“。当计数达到阈值后
会触发栈上的替换操作(OSR),每个循环体都应该有⼀个回边计数器,所以⼀个⽅法内可能存在多个回边计数
器。回边调⽤只记录绝对次数,触发的阈值可以间接设置。
热点代码经过编译后是放在虚拟机的内存中,这样以后调⽤时可以直接从内存中读取。

但是如果代码缓存空间⽤完了,虚拟机遇到热点代码也不会进⾏即时编译,⽽是启⽤解释器执⾏。

代码缓存占满后并不会抛出OOM异常,只是等待下⼀次GC清理出空间。当程序较⼤想要保存更多的编译后本地代
码,应该扩⼤代码缓存空间。

提前编译器
1、在程序运⾏之前把程序代码编译成机器码的静态翻译⼯作

2、把原本即时编译器在 运⾏时要做的编译⼯作提前做好并保存下来,下次运⾏到这些代码(譬如公共库代码在被
同⼀台机器 其他Java进程使⽤)时直接把它加载进来使⽤。⽬前主流虚拟机均⽀持此种⽅法。

即时编译是在程序运⾏时发⽣的,优化需要时间和运算资源,⽽提前编译是在程序运⾏之前就编译好,可以减少系
统运⾏时优化消耗。
提前编译和即时编译优劣
提前编译的好处是省资源,避免了在运⾏时进⾏优化,节约了运算资源。⽽即时编译相对提前编译,有以下优点:

1. 性能分析制导优化
2. 激进预测性优化
3. 链接时优化

编译优化技术
⽅法内联:

把⽬标⽅法的代码原封不动地“复制”到发起调⽤的⽅法之中,避免发⽣真实的⽅法调⽤。这是编译器最重要的优化
⼿段,被称为优化之母。

除了消除⽅法调⽤的成本之外,它更重要的意义是为其他优化⼿段建⽴良好的基础,没有内联,多数其他优化都⽆
法有效进。

⽅法内联需要确定⽬标⽅法的具体版本,但是除了在编译期间进⾏解析的⾮虚⽅法和final修饰的⽅法,其他⽅法都
是虚⽅法。

虚⽅法可能会进⾏重载或者重写,为了确定⽅法的版本Java虚拟机采⽤类型继承关系分析技术(CHA),这个可以
确定在⽬前已加载的类中,某个接⼜是否有多于⼀种的实现、某个类是否存在⼦类、某个⼦类是否覆盖了⽗类的某
个虚⽅法等信息。

守护内联:

如果内联的是⾮虚⽅法,那么直接进⾏内联就⾏,这样⼀定时安全的,要内联的⽅法版本不会发⽣改变。

但是如果时虚⽅法,CHA会查询此⽅法在当前程序状态下是否真的有多个⽬标版本可供选择,如果查询到只有⼀个
版本,那就可以假设“应⽤程序 的全貌就是现在运⾏的这个样⼦”来进⾏内联,这种内联被称为守护内联。

如果后期类的关系没有变化,就⼀直按照内联优化的程序运⾏,如果加载了导致继承关系发⽣变化的新类,那么就
必须抛弃已经编译的代码,退回到解释状态进⾏执⾏,或者重新进⾏编译。

内联缓存:

如果向CHA查询出来的结果是该⽅法确实有多个版本的⽬标⽅法可供选择,编译器将采⽤内联缓存的⽅式来减少⽅
法调⽤开销。

在未发⽣⽅法调⽤之前,内联缓存为空。第⼀次调⽤⽅法时,记录下⽅法接受者的版本信息,随后的每次调⽤都先
⽐较接收者版本信息,如果⼀致则认为调⽤的是同⼀种⽅法,就通过缓存来调⽤,称为单态内联缓存。

如果接收者信息不⼀样,就会退化为超多态内联缓存。单态内联缓存只是⽐普通内联调⽤多⼀次⽐较,速度还是很
快,但是如果是超多态内联缓存,其速度相当于真正查找虚⽅法表来进⾏⽅法分派。

逃逸分析
分析对象动态作⽤域,当⼀个对象在⽅法⾥⾯被定义后,它可能被外部⽅法所引⽤,例如作为调⽤参数传递到其他
⽅法中,这种称为⽅法逃逸;

甚⾄还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;

从不逃逸、⽅法逃逸到线程逃逸,称为对象由低到⾼的不同逃逸程度。
逃逸分析与类型继承关系分析⼀ 样,并不是直接优化代码的⼿段,⽽是为其他优化措施提供依据的分析技术。

如果⼀个对象不会逃到⽅法和线程之外,即其他⽅法和线程⽆法通过任何⼿段访问到这个对象,或者逃逸程度低
(只是⽅法逃逸)则可以为这个对象实现不同程度的优化:

栈上分配:

如果确定⼀个对象不会逃逸出线程之外,让这个对象在栈上分配内存,对象所占⽤的内存 空间就可以随栈帧出栈⽽
销毁。因为不会逃逸出线程,所以其他线程是⽆法访问到的,那么在堆上创建会浪费GC资源,直接在此线程的栈
上创建,随着栈帧出栈⽽销毁,减少GC压⼒。栈上分配可以⽀持⽅法逃逸,但不能⽀持线程逃逸

标量替换:

若⼀个数据已经⽆法再分解成更⼩的数据(int char long)来表⽰就称为标量,如果⼀个数据可以继续分解(Java对


象),那它就被称为聚合量。

如果把⼀个Java对象拆散,根据程序访问的情况,将其⽤到的成员变量恢复为原始类型来访问,这个过程就称为标
量替换。

假如逃逸分析能够证明⼀个对象不会被⽅法外部访问,并且这个对象可以被拆散,那么程序真正执⾏的时候将可能
不去创建这个对象,⽽改为直接创建它的若⼲个被这个⽅法使⽤的成员变量来代替。标量替换不允许对象逃逸出⽅
法。

同步消除:

如果对象不会逃逸出线程,就不必做线程间的同步,对这个变量实施的同步措施也就可以安全地消除掉。

公共⼦表达式消除:

如果⼀个表达式E之前已经被计算过了,并且从先前的计算到现在E中所有变量的值都没有发⽣变化,那么E 的这次
出现就称为公共⼦表达式。没有必要花时间再对它重新进⾏计算,直接⽤之前的计算结果代表E即可。

数组边界检查消除:

在Java语⾔中访问数组元素系统将会⾃动进⾏上下界的范围检查,对于拥有⼤量数组访问的程序代码,这必定是⼀
种性能负担。

如果编译器只 要通过数据流分析就可以判定循环变量的取值范围永远在区间[0,length)之内,那么在循环中就可以
把整个数组的上下界检查消除掉,这可以节省很多次的条件判断操作。

直⽩的说就是⽤过编译器更聪明的判断是否需要进⾏边界检查,消除不必要的检查节省时间。

Java 内存模型
先强调,Java内存模型(JMM)和Java运⾏时数据区的别搞混了。

Java内存模型不是介绍内存是怎么分配的,Java的多线程并发依赖Java内存模型。Java运⾏时数据区才是将内存分
成了哪些部分,各部分分别放的是社么。JMM⼤概点可以分为基本的模型、3个同步原语的内存语句、happens-
before等⼏块,理解了他们也就理解就JMM。

内存模型:为在特定的操作协议下,对特定的内存或⾼速缓存进⾏读写访问的过程抽象。
不同的硬件之间的内存模型有些许差异,Java内存模型屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程
序在各种平台下都能达到⼀致的内存访问效果。

通信和同步:

在并发编程中,线程间的通信有两种模型:共享内存和消息传递。

共享内存是指线程间共享,通过读写内存中的共功状态来实现隐式通信,消息传递线程间没有共享状态,线程间必
须通过发送接受消息来进⾏显⽰通信。共享内存的同步是显⽰进⾏的,必须显⽰的指定某个⽅法或者某段代码在程
序之间互斥执⾏,

⽽消息传递的同步是隐式执⾏的,消息的发送⼀定在消息的接受之前嘛。

Java采⽤的是共享内存模型,隐式通信,显⽰同步。

共享模型:

简单理解就是有⼀个主内存,每个线程有⾃⼰的⼯作内存。

线程⼯作时先从主内存中把需要的数据拷贝到⼯作内存中,然后线程从⼯作内存中读取相关数据进⾏处理。随后将
处理完成的数据从⼯作内存写回到主内存中。这样当另⼀个线程从内存中读取数据时,得到的就是之前线程处理过
的,两个线程之间完成了通信(隐式通信)。但如何确保线程从主内存中读取的先后顺序,⽐如后⾯的线程⼀定是
等之前的线程将数据处理完之后再从主内存读取,这需要在程序中显⽰的指定互斥执⾏(显⽰同步)。

其实⼯作内存并不是⼀块真正的内存,它是缓存、写缓冲区、寄存器以及其他的硬件和编译器优化等等⼀系列的抽
象。在硬件层⾯,真正流程是下图:

主内存中存放的数据应该是线程间共享的。

这⾥的主内存对应于Java堆中的对象实例数据部分,在Java中,所有实例域、静态域和数组元素都存放堆内存中,
堆内存在线程间共享。⽽局部变量表和⽅法参数等是线程私有的,并不会被共享,局部变量表和⽅法参数是分配在
虚拟机栈上的,并不是堆。
⾼速缓存:

处理器⾸先需要先读取数据,然后才能进⾏各种运算。但是处理器的速度要⽐内存的速度⾼出⼏个数量级,要是从
内存读⼀个数然后处理器运算⼀个指令,处理器的时间都被浪费在等待内存上⾯了。

所以现代计算机系统都不得不加⼊⼀层或多层读写速度尽可能接近处理器运算速度的⾼速缓存(Cache)来作为内
存与处理器之间的缓冲:将运算需要使⽤的数据复制到缓存中,让运算能快速进⾏,当运算结束后再从缓存同步回
内存之中,这样处理器就⽆须等待缓慢的内存读写了。

⼯作内存概念不仅有⾼速缓存,还包括寄存器、硬件和编译器优化等待。

可见性在并发编程中,会出现各种各样的可见性问题。简单的说就是程序员认为的逻辑,和代码真正执⾏的逻辑并
不⼀致,就会造成可见性问题。程序员不断的检查⾃⼰程序逻辑,程序逻辑正确但是运⾏结果却错误,造成各种认
为的“⽞学”问题。⽽只有真正了解计算机是如何处理代码的,才能破解“⽞学”。

可见性其实就是⼀个线程执⾏的结果,其他的线程是否可以正确的访问到。假设线程A处理完后,将结果放⼊⾃⼰
的⼯作内存,但并没有从⼯作内存刷回到主内存,其他线程是看不到这个结果的。或者在线程 A 执⾏之前,其他线
程就去主内存中查看,这种肯定也看不到。JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 Java 程
序员提供内存可见性保证。

重排序:

在执⾏程序时,为了提⾼性能,编译器和处理器会对指令做重排序。分为三类:

1、编译器优化的重排序:

编译器在不改变单线程语义的前提下,可以重新安排语句执⾏顺序。

2、指令级别重排序:

如果不存在数据依赖,处理器可以改变语句对机器执⾏指令的顺序。

3、内存系统的重排序:

由于处理器使⽤缓存和读写缓冲区,这使的加载和存储操作看上去是乱序执⾏。

从源代码到最后执⾏的指令需要分别经历三次排序

数据依赖性:

如果两个操作访问同⼀个变量,且这两个操作中有⼀个为写操作,此时这两个操作之间就存在数据依赖性。

数据依赖分为 写后读、写后写、读后写三种情况(只要前后任意操作涉及到写,就存在数据依赖性),只要重排序
两个操作的执⾏顺序,程序的执⾏结果就会被改变。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处
理器不会改变存在数据依赖关系的两个操作的执⾏顺序。

但此处的数据依赖性只是单个处理器和单个线程中的操作,不同处理器和不同线程之间的数据依赖不被编译器和处
理器考虑。

as-if-serial 语义:
不管怎么重排序,单线程程序的执⾏结果不能被改变。各种重排序必须遵守as-if-serial 语义。⽐如上⾯的数据依赖
性,并不会重排存在数据依赖性的操作,因为这样会改变结果。单线程程序⽆论怎样重拍,最后的结果⼀定是正确
的。as-if-serial很好理解,单线程如果都不能保证,那整个程序就乱套啦。它使单线程程序员⽆需担⼼重排序会⼲
扰程序,也⽆需担⼼内存可见性问题。

但as-if-serial 隐含的意思时,只要重排序不影响最后的结果,那么你怎样重排都是可以的。单线程不会有影响,但
多线程就会出现可见性问题。此时就需要正确的同步。

顺序⼀致性模型:

顺序⼀致性模型是⼀种理想化理论参考模型

1、⼀个线程中的所有操作必须按照程序的顺序来执⾏。

2、(不管程序是否同步)所有线程都只能看到⼀个单⼀的操作执⾏顺序。在顺序⼀致性内存模型中,每个操作都
必须原⼦执⾏且⽴刻对所有线程可见。

JMM并没有实现顺序⼀致模型,因为那样的成本太⼤了,各种优化措施都不能⽤。

JMM只保证:如果程序是正确同步的,程序的执⾏将具有顺序⼀致性-----即程序的执⾏结果与该程序在顺序⼀致性
内存模型中的执⾏结果相同。

注意是执⾏结果相同,并不是按照⼀致性执⾏,JMM允许重排序且只有在正确同步下才能保证可见性。对于未同步
或未正确同步的多线程程序,JMM 只提供最⼩安全性。

顺序⼀致性模型中,所有操作完全按程序的顺序串⾏执⾏。

JMM中,临界区内的代码可以重排序(但是不允许临界区内的代码“逃逸”,那样会破坏互斥性,并不是正确的同
步)。

JMM 会在退出临界区和进⼊临界区这两个关键时间点做⼀些特别处理,使得线程在这两个时间点具有与顺序⼀致
性模型相同的内存视图。

因为同步的互斥性,使得其他线程根本看不都临界区内重排序,线程只能在公共节点出看到相同的内存视图。
JMM的基本⽅针:在不改变正确同步的程序执⾏结果前提下,尽可能地为编译器和处理器的优化打开⽅便之门。

未正确同步的程序,JMM提供最⼩安全性:线程读取到的值,要么是默认值(0,null等),要么是其他线程写⼊
的值。

注意,保证其他是其他线程写⼊的值并不是说这个值就是正确的,最⼩安全性只是确保读到的值不是胡乱产⽣的⽽
已。假设64位数据,先写⼊低32位后,此时其他线程来读取此值,此种情况虽然读取到的64位数不正确(⼀半写⼊
⼀半没写),但是依然符合最⼩安全性。因为即使这个64位数是拼凑起来的,但它也是之前的值(以前线程写⼊)
和后32位(现在线程写⼊),都是由线程写⼊的,并不是胡乱产⽣的。

JMM 不保证未同步程序的执⾏结果与该程序在顺序⼀致性模型中的执⾏结果⼀致。不同步JMM认为这两个线程间
没有关系,各⾃执⾏优化。

volatile 的内存语义

volatile 变量⾃⾝具有下列特性:

1、可见性:

对⼀个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写⼊。

2、原⼦性:

对任意单个 volatile 变量的读/写具有原⼦性,但类似于 volatile++这种复合操作不具有原⼦性。

3、volatile 写的内存语义:

当写⼀个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。

4、volatile 读的内存语义:

当读⼀个 volatile 变量时,JMM 会把该线程对应的本地内存置为⽆效。线程接下来将从主内存中读取共享变量。

从内存语义的⾓度讲,volatile写和释放锁具有相同的内存效果,把本线程处理的共享变量的刷回到主内存,其他线
程便可看见。volatile读和获取锁具有相同的内存效果,从主内存中读取最新的共享变量。

线程 A 写⼀个 volatile 变量,实质上是线程 A 向接下来将要读这个 volatile 变量的某个线程发出了(其对共享变量


所做修改的)消息。

线程 B 读⼀个 volatile 变量,实质上是线程 B 接收了之前某个线程发出的(在写这个 volatile 变量之前对共享变量


所做修改的)消息。

线程 A 写⼀个 volatile 变量,随后线程 B 读这个 volatile 变量,这个过程实质上是线程 A 通过主内存向线程 B 发送


消息。

volatile 内存语义的实现

对于主内存和⼯作内存之间的交互,JMM规定了8种操作:

lock(锁定):作⽤于主内存的变量,它把⼀个变量标识为⼀条线程独占的状态。

unlock(解锁):作⽤于主内存的变量,它把⼀个处于锁定状态的变量释放出来,释放后的变量 才可以被其他线程
锁定。

read(读取):作⽤于主内存的变量,它把⼀个变量的值从主内存传输到线程的⼯作内存中,以 便随后的load动作
使⽤。

load(载⼊):作⽤于⼯作内存的变量,它把read操作从主内存中得到的变量值放⼊⼯作内存的 变量副本中。

use(使⽤):作⽤于⼯作内存的变量,它把⼯作内存中⼀个变量的值传递给执⾏引擎,每当虚 拟机遇到⼀个需要
使⽤变量的值的字节码指令时将会执⾏这个操作。

assign(赋值):作⽤于⼯作内存的变量,它把⼀个从执⾏引擎接收的值赋给⼯作内存的变量, 每当虚拟机遇到⼀
个给变量赋值的字节码指令时执⾏这个操作。

store(存储):作⽤于⼯作内存的变量,它把⼯作内存中⼀个变量的值传送到主内存中,以便随 后的write操作使
⽤。

write(写⼊):作⽤于主内存的变量,它把store操作从⼯作内存中得到的变量的值放⼊主内存的 变量中。

同时还有许多规则,⽐如unlock必须在lock之后,read和load、store和write两两成对,不允许单独出现等等。和
volatile相关的只需要记住两个,load和store。标注volatile变量会限制⼀部分重排序:

1. 第⼆个操作是volatile写,不管第⼀个操作是啥都不允许重排序。确保 volatile 写之前的操作不会被编译器重排序


到 volatile 写之后
2. 第⼀个操作时volatile读,不管第⼆个操作是啥都不允许重排序。确保 volatile 读之后的操作不会被编译器重排序
到 volatile 读之前。
3. 第⼀个是volatile写第⼆个是volatile读不允许重排序。
为了实现volatile限制重排序的功能,编译器在⽣成字节码时会插⼊内存屏障来静⽌特定类型的处理器重排序。⽽因
为每个处理器重排序规则都不⼀样,JMM采取了保守策略插⼊内存屏障,保证JMM在不同处理器上最后重拍限制都
相同。

在每个 volatile 写操作的前⾯插⼊⼀个 StoreStore 屏障


在每个 volatile 写操作的后⾯插⼊⼀个 StoreLoad 屏障

在每个 volatile 读操作的后⾯插⼊⼀个 LoadLoad 屏障


在每个 volatile 读操作的后⾯插⼊⼀个 LoadStore 屏障

并不是严格的要求必须插⼊这些内存屏障,上述只是最保守的策略。在实际执⾏时,只要不该改变volatile写-读的
内存语义,编译器可以优化掉不必要的内存屏障。

上⾯提到,store和load分别是存储到主内存和从主内存读取。在内存屏障的两侧,各有⼀些数据,内存屏障会保证
在执⾏后⾯的执⾏之前,之前的相关指令已经全部完成。⽐如storestore屏障,在后⾯数据刷回到主内存之前,前⾯
的数据已经全部刷回主内存。LodaStore屏障保证在后⾯数据刷回到主内存之前,已经从主内存中载⼊数据
锁的内存语义

当线程释放锁时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存中。当线程获取锁时,JMM 会把该


线程对应的本地内存置为⽆效。从⽽使得被监视器保护的临界区代码必须从主内存中读取共享变量。锁的实现也采
⽤了 volatile内存语义。

在ReentrantLock 中,依赖于AQS框架实现锁,分为公平锁和⾮公平锁。

公平锁语义的实现:加锁⽅法⾸先读 volatile 变量 state(AQS的state),在释放锁的最后写 volatile 变量 state。

⾮公平锁的实现:在⾮公平锁时,会采⽤CAS设置[Link]具有 volatile 读和写的内存语义。

final内存语义

对于 final 域,编译器和处理器要遵守两个重排序规则。

1. 在构造函数内对⼀个 final 域的写⼊,与随后把这个被构造对象的引⽤赋值给⼀个引⽤变量,这两个操作之间不


能重排序。
2. 初次读⼀个包含 final 域的对象的引⽤,与随后初次读这个 final 域,这两个操作之间不能重排序。

final内存语义实现:

写final:

JMM 禁⽌编译器把 final 域的写重排序到构造函数之外

编译器会在 final 域的写之后,构造函数 return 之前,插⼊⼀个 StoreStore 屏障。这个屏障禁⽌处理器把 final 域的


写重排序到构造函数之外。

读final:

在⼀个线程中,初次读对象引⽤与初次读该对象包含的final 域,JMM 禁⽌处理器重排序这两个操作。编译

器会在读 final 域操作的前⾯插⼊⼀个 LoadLoad 屏障

final为引⽤类型(前提为final引⽤不能从构造函数溢出):

在构造函数内对⼀个 final 引⽤的对象的成员域的写⼊,与随后在构造函数外把这个被构造对象的引⽤赋值给⼀个


引⽤变量,这两个操作之间不能重排序。
happens-before

happens-before 是 JMM 最核⼼的概念

对于程序员:JMM 向程序员提供的 happens-before 规则能满⾜程序员的需求,也向程序员提供了⾜够强的内存可


见性 保证。

对于处理器和编译器:

只要不改变程序的执⾏结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都⾏。例如
锁消除和volatile消除。

定义: 此处的定义分别呼应上⾯的两点

1. 如果⼀个操作 happens-before 另⼀个操作,那么第⼀个操作的执⾏结果将对第⼆个操作可见,⽽且第⼀个操作


的执⾏顺序排在第⼆个操作之前。
2. 两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要按照 happens-before 关系指定
的顺序来执⾏。如果重排序之后的执⾏结果,与按happens-before 关系来执⾏的结果⼀致,那么这种重排序并不
⾮法。

happens-before 规则

1. 程序顺序规则:⼀个线程中的每个操作,happens-before 于该线程中的任意后续操作。
2. 监视器锁规则:对⼀个锁的解锁,happens-before 于随后对这个锁的加锁。
3. volatile 变量规则:对⼀个 volatile 域的写,happens-before 于任意后续对这个volatile 域的读。
4. 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
5. start()规则:如果线程 A 执⾏操作 [Link]()(启动线程 B),那么 A 线程的[Link]()操作 happens-
before 于线程 B 中的任意操作。
6. join()规则:如果线程 A 执⾏操作 [Link]()并成功返回,那么线程 B 中的任意操作 happens-before 于线程
A 从 [Link]()操作成功返回。

Java 线程安全的实现
Java中线程安全的实现⽅式---战略性观望

1、阻塞同步:

说⽩了也就是使⽤锁实现,具体采⽤什么锁,有两种选择:内置锁也就是synchronized关键字,JUC下具体锁的实

2、⾮阻塞同步:

使⽤锁带来的主要问题,是频繁的线程阻塞、唤醒操作以及⽤户态内核态的切换带来的性能问题。

可能这些额外的操作带来的时间消耗远⼤于线程⾃⾝的业务执⾏执⾏时间。所以引⼊⾮阻塞同步,也就是基于CAS
操作。

最直接的实现就是JUC下的各种原⼦类的实现。虽然CAS避免了锁带来的性能开销,不过其仅适⽤于少部分同步场
景,没有阻塞同步更加具有普适性。

缺点:
1. 未获取同步资源的线程陷⼊⾃旋状态,所以对于CPU的消耗很⾼
2. 仅能操作单个共享资源,对于组合类型还是需要加锁处理,或者重新组合为⼀个共享资源
3. ABA问题

3、⽆同步⽅案:

线程的本地存储,主要是⽤于对于⼀个共享资源都尽可能的在同⼀个线程中执⾏,使⽤场景:

1. 譬如⽣产者-消费者模型中,消费者消费消费⼀个内容
2. web服务中,⼀个请求对⽤于⼀个服务线程(也属于⽣产者-消费者模型)
3. 链路跟踪中动态采集⽅法的执⾏信息

Java 线程的状态转换
Java线程的状态转换和操作系统中进程的状态转换关系

Java中线程共有6种状态,也可以理解成其⽣命周期,同理操作系统的进程也有⾃⼰的⽣命周期,然后看Java并发编
程的艺术关于线程⽣命周期的图,有些凌乱,然后结合操作系统,再结合优质的博客内容,重新理解了下。
Java中将操作系统中的

1. 就绪状态/运⾏状态转化为⼀个状态Runnable
2. 阻塞状态 细分为了三种
1. BlOCKED
2. WAITING
3. TIMED_WAITING

Java 并发

Synchronized

1、作⽤
Java中的锁分为显⽰锁和隐式锁。隐式锁由synchronized关键字实现,⽽显⽰锁是由实现了Lock接⼜和AQS框架等
等类来实现。

Java中的synchronized,通过使⽤内置锁,来实现对共享变量的同步操作,进⽽解决了对共享变量操作的原⼦性、
保证了其他线程对共享变量的可见性、有序性,从⽽确保了并发情况下的线程安全。

同时synchronized是可重⼊的锁,避免了同⼀个线程重复请求⾃⾝已经获取的锁时出现死锁问题(请求于保持、不
可剥夺感觉都有体现)

2、基础⽤法
普通同步⽅法、静态同步⽅法、同步代码块⼉

3、什么是内置锁
在Java中,每个对象都有⼀把锁,放置于对象头中,⽤于记录当前对象被哪个线程所持有。

相对于实例数据,对象头属于额外开销,所以被设计的极⼩来提⾼效率(⼀个对象在堆中的分布:对象头、实例数
据、对齐填充)。

对象头中的markword更加体现了这⼀点。markword⾮结构化的,这样在不同的锁状态下,能够复⽤相同的bit
位。markword中就有存储锁的信息的部分。
4、内置锁又是通过什么实现
synchronized被编译之后,使⽤的是monitorEnter和monitorExit两个字节码指令(同步代码块⼉),⽽这两个字节码
指令实质上是依赖于操作系统中的mutex lock实现。

因为使⽤的是互斥锁,需要CPU的参与,涉及到线程的唤醒、同时切换需要记录线程的私有数据、寄存器等不共享
的数据,可能这些操作带来的时间开销远⼤于线程⾃⾝执⾏的时间消耗,所以JDK1.6开始,引⼊了⽆锁、偏向锁、
轻量级锁、重量级锁的锁优化过程,进⽽优化synchronized的性能

5、锁的分类
从宏观上看,锁的分类有多种不同划分。可以分为乐观锁和悲观锁,可以分为共享锁和排他锁,还可以分为可重⼊
锁和不可重⼊等等。

乐观锁:

乐观锁就是对数据冲突保持乐观点态度,认为不会有其他线程同时修改数据。因此乐观锁不会上锁,只是在更新数
据都时候判断是否有其他线程更新,如果没有其他线程修改则跟新数据,有其他线程修改则放弃数据,重新读取数
据处理。

悲观锁:

悲观锁被数据冲突持悲观的态度,认为总是发⽣数据冲突。因此它以⼀种预防的态度,先⾏把数据锁住,知道操作
完成才释放锁,在此期间其他线程⽆法操作数据。

6、synchronized关键字
Java 中的每⼀个对象都可以作为锁,有三种加锁的⽅式:

(1)对于普通同步⽅法,锁是当前实例对象。

(2)对于静态同步⽅法,锁是当前类的 Class 对象。

(3)对于同步⽅法块,锁是 Synchonized 括号⾥配置的对象。


7、对象头
synchronized关键字的实现,依赖于Java的对象头。

⼀个对象由三部分组成:对象头、实体数据、对齐填充。对象头的长度不是固定的,如果是数据类型则对象头占12
个字节,⾮数组类型对象头占8个字。

⾮数组类型的对象头分两部分,Mark Word 和对象类型指针,数组类型对象会多⼀部分来存储数组的长度。⽽


synchronized关键字的实现,就和对象头中的Mark Word密切相关。

Java 对象头⾥的 Mark Word ⾥默认存储对象的 HashCode、分代年龄、是否偏向锁以及锁标记位。32位 JVM 的


Mark Word 的默认存储结构如下

为了提⾼虚拟机空间的使⽤效率,Mark Word被设计成⼀个⾮固定的动态数据结构,以便存储更多的信息。不同状
态下对象头Mark Word存储的信息如下图
由图可知分为未锁定、偏向所、轻量级锁、重量级锁、GC,但是在轻量级锁升级为重量级锁时,还可能进⾏⾃
旋。

synchronized锁的状态被分为4种,级别从低到⾼依次是:⽆锁、偏向锁、轻量级锁、重量级锁。

注意在锁标志中并没有出现⾃旋锁,它仅仅是锁可能存在的⼀种状态,是暂时性的,并没有官⽅的标志。32位虚拟
机下,Mark Word的最后3位就可以判断锁的状态。

⽆锁和偏向锁标志位都是01,只是偏向锁时偏向模式会被置为1。锁可以升级但不能降级,意味着偏向锁升级成轻
量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,⽬的是为了提⾼获得锁和释放锁的效率。

难道锁的不同仅仅是标志不同吗?

肯定不是,不同的锁各⾃实现的途径不同,适合的场景也各不相同。理解各种锁是怎样实现的,也就理解了
synchronized关键字,理解了锁优化过程。

8、⽆锁
按最后3位标识位来判断,⽆锁应该是001状态,偏向标志为0,锁标志为01。

更准确的讲001状态是⽆锁不可偏,还有⼀种是101状态,⽆锁可偏(又叫匿名偏向)。⽆锁不可偏状态下遇到同
步,会直接升级为轻量级锁,⽽不会变为偏向锁(看名字也知道,不可偏嘛)。只有在⽆锁可偏的状态下,才可能
变成偏向锁。匿名偏向状态下虽然标识码是101,但是线程ID部分全部0,意味着没有线程实际获得偏向锁。

为啥要分⽆锁可偏和⽆锁不可偏呢?

因为所有偏向锁的起点就是匿名偏向状态,⽆锁不可偏状态会直接变为轻量级锁。

⾸先如果JVM设置取消偏向锁,那么⽆锁状态只可能是⽆锁不可偏。JDK8默认启动了偏向锁,但是偏向锁时在JVM
启动⼏秒(默认4秒,但可以设置更改)之后才会启动,此时能设置为匿名偏向的会全部设置为匿名偏向,因此匿
名偏向是偏向锁的起点。

⽽JVM为啥会在⼏秒之后才会启⽤偏向锁,这是因为JVM内部的代码会使⽤synchronized,这些类⾥有很多激烈线
程竞争,如果采⽤锁升级策略这些锁会浪费很多时间。所以JVM索性先不开启偏向锁,先执⾏这些库类,等过⼏秒
差不多都执⾏完了再开启偏向锁。⾄于JDK8为啥默认4秒,这是个经验值,4秒⼤多数类都启动完了,此数值可以修
改。
9、偏向锁
⼤多数情况下,锁不仅不存在多线程竞争,⽽且总是由同⼀线程多次获得,为了让线程获得锁的代价更低⽽引⼊了
偏向锁。

这个锁永远会偏向于获得它的线程,如果在获得锁之后并没有其他线程获取,则获得偏向锁的线程永远不需要同
步,减少锁带来的时间消耗。

9.1、偏向锁的获取

偏向锁对象头将不在存放Hash值,⽽在此位置上存放线程ID(23bit)+Epoch(2bit),⼀共25bit,其他部分保持不变。
Epoch是⼀个时间戳,⽤来判断线程ID是否过时。具体获得锁流程如下:

匿名偏向是偏向锁的初始状态,所以先判断锁标志,再判断偏向锁标志位,只有最后三位是101才开始,否则直接
⾛其他的锁。如果是匿名状态,线程ID为0,采⽤CAS去将当前线程写⼊,如果成功则获得锁,不成功表⽰存在竞
争。线程ID不为0,此前已经有偏向,判断此值是否和当前线程相同,若⼀致则表⽰线程之前就获得了锁,不⼀致
就尝试CAS替换。同意,替换成功获得锁,替换失败存在竞争。未获得锁时将会等待安全点(STW),安全点会进
⾏偏向锁的撤销。

安全点是JVM在进⾏垃圾GC时为了保证引⽤关系不会发⽣变化⽽设置的安全状态(GC Roots的确定就在此时),
STW(STOP THE WORLD)此时将暂停所有线程的⼯作。

在STW会检测持有偏向锁的线程是否还存活,如果存活则升级轻量级锁,如果线程未存活或者已经退出来同步代码
块,将会判断是否可重偏向,否则直接升级为轻量级锁。允许重偏向时会先设置为匿名重偏向,在使⽤CAS偏向线
程。

判断是否可重偏向需要⽤到Epoch,偏向锁中有⼀个Epoch,对应的Class类中也有⼀个Epoch。在进⼊全局安全点之
后,⾸先会对Class类中的Epoch进⾏增加,得到新的Epoch_new,然后扫描所有持有Class类实例的线程,根据线
程信息判断是否锁住了该对象。如果锁住了说明此对象还在使⽤,将Epoch_new更新给它,如果未锁住则说明不需
要加锁,不进⾏更新。如果对象的Epoch和类的Epoch相同,则表⽰它是被更新过的,需要锁,不能重偏向。⽽如
果不相同,则表⽰已经不需要加锁了,此对象可以重偏向到其他线程。
9.2、偏向锁的释放

从偏向锁的获取过程可以看到,等到竞争出现的时候才会释放。如果没有出现竞争,它不会去改变Mark Word的相
关字段。就算是线程已经执⾏完同步代码块,不需要加锁了,也不会去修改对象头,那个锁依旧存在,依旧保持偏
向。

只是在其他线程需要偏向,出现了竞争的时候会进⾏判断,如果以前偏向的线程不需要了,那么对象⾸先会被设置
为匿名偏向,然后CAS替换尝试加锁。如果以前偏向的线程还需要加锁,升级为轻量级锁。

所以线程不会主动的将偏向锁设置为匿名偏向状态,不会主动的去释放锁。

9.3、批量偏向与批量撤销

偏向锁有三个参数:
BiasedLockingBulkRebiasThreshold:偏向锁批量重偏向阈值,默认为20次

BiasedLockingBulkRevokeThreshold:偏向锁批量撤销阈值,默认为40次

BiasedLockingDecayTime:重置计数的延迟时间,默认值为25000毫秒(即25秒)

批量重偏向是以class⽽不是对象为单位的,每个class会维护⼀个偏向锁的撤销计数器,每当该class的对象发⽣偏向
锁的撤销时,该计数器会加⼀,当这个值达到默认阈值20时,jvm就会认为这个锁对象不再适合原线程,因此进⾏
批量重偏向。⽽距离上次批量重偏向的25秒内,如果撤销计数达到40,就会发⽣批量撤销,如果超过25秒,那么就
会重置在[20, 40)内的计数。

当⼀个线程建⽴了⼤量的对象,并对他们都加了偏向锁。⽽此时若另⼀个线程也来获取这些对象,此时发⽣了竞争
理论上都会升级轻量级锁。但是因为批量偏向的存在,并不会全部升级。

假设线程A建⽴了10个对象,全部加偏向锁,随后线程B同样也对这40个对象加锁。线程B对每⼀个对象进⾏加锁
是,都会导致撤销⼀次偏向锁,升级为轻量级锁。当这个数值变为20后,JVM会认为其余的对象也不适合线程A,
当后⾯的对象遇到需要同步的时候,会先被重置为可偏向状态,以便快速冲偏向。这样线程B对后⾯的对象加锁就
不会升级为轻量级锁,⽽是偏向了线程B。

当线程C再来加锁的时候,前20个对象竞争轻量级锁,直接竞争,后20个锁是偏向线程B的,是偏向锁。此时不会触
发批量重偏向,所以后20个也升级为轻量级锁。升级为轻量级锁就需要撤销偏向锁,加上之前的20次,⼀共40次。
达到40次撤销偏向锁,会触发批量撤销机制,将偏向锁升级为轻量级锁,并且此类新建的对象都不是⽆锁不可篇状
态,不会出现偏向锁。

9.4、偏向锁的优缺点

优点:

在只有单⼀线程访问对象的时候,偏向锁⼏乎没有影响。只有第⼀次需要CAS操作替换,随后的只要⽐较线程ID即
可,⽐较⽅便快速。

缺点:

如果有多个线程访问,就会出现竞争,竞争需要等到安全点时,并且进⾏⼀系列分析⽐较耗费时间。另外,偏向锁
存放线程ID和Epoch后,对象头中不存在Hash值,如果程序需要Hash值需要调⽤HashCode,这会导致偏向锁退
出。

如果对象需要调⽤Object⽅法,会启⽤对象的minoter内置锁.。此时会直接由偏向锁退出进⼊重量级锁。

10、轻量级锁
轻量级锁是相对于使⽤操作系统互斥量来实 现的传统锁⽽⾔的,因此传统的锁机制就被称为“重量级”锁。轻量级锁
并不是⽤来代替重量级锁的,它设计的初衷是在没有多线程竞争的前提下,减少传统的重量级锁使⽤操作系统互斥
量产⽣的性能消耗。

10.1、轻量级锁的获取

虚拟机⾸先将在当前线程的栈帧中建⽴⼀个名为锁记录(Lock Record)的空间,此空间包含两部分:

displaced mark word:⽤于存储锁对象⽬前的Mark Word的拷贝

owner:指向当前的锁对象的指针

虚拟即⾸先会将对象的Mark World拷贝到栈帧中的Lock Record,此时owner为空。


然后虚拟机使⽤CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针(此空间占30bit, 除了最后两位锁
标志,Mark Word前30bit均⽤来存放指针),如果操作成功则代表线程拥有了这个对象锁,对象锁标志位变为00.
如失败了则会会判断对象Mark Word存储的指针是否指向⾃⼰,如果是则表⽰拥有锁,执⾏同步代码,如果不是则
意味着其他线程抢占,出现激烈竞争需要升级为重量级锁。

10.2、轻量级锁的释放

使⽤CAS尝试将Lock Record中的displaced mark word替换回去,需要检查对象头中的指针是否指向当前线程。

如果替换成功,表⽰没有竞争,锁成功释放

如果替换失败会进⾏⾃旋,如果⾃旋之后仍未获得锁表⽰存在竞争并升级为重量级锁。

当其他线程竞争轻量级锁时,并不会对已经持有轻量级锁的线程发送什么,⽽是对象头的锁标志被修改,同时竞争
线程⾃⾝被挂起。因此如果CAS替换失败,原本持有锁的线程除了释放锁之外,还需要唤醒被挂起的线程。
10.3、轻量级锁的重⼊

轻量锁的每⼀次重⼊,都会在栈中⽣成⼀个Lock Record。只是只有第⼀次会拷贝Mark Word,随后的加锁


Displaced Mark Word区域为NULL,owner区域统⼀指向对象头。

线程第⼀次获得锁时,将对象的Mark Word拷贝到本线程Lock Record,同时将对象的指针指向⾃⼰,Lock Record


中的owner也指向对象。随后的加锁对象中存储的是指向本线程的指针,并没有Mark Word,也就不需要拷贝。只
是将owner指向对象即可。

每加⼀次锁帧栈中多⼀个Lock Record,Lock Record的个数也就是加锁的次数。释放锁时也要⼀个⼀个释放,只有


解锁次数等于加锁次数,才会真正释放锁。释放锁时,如果是重⼊锁则直接删掉⼀个Lock Record,如果不是重⼊
则采⽤CAS替换对象的Mark Word。
10.4、轻量级锁⾃旋

当轻量级出现竞争以后,会尝试进⾏⾃旋。⾃旋就是CPU空转,线程没有挂起依然在执⾏,等过⼀段时间后再去加
锁。

这是因为如果升级为重量级锁,是通过操作系统来实现,涉及到内核态和⽤户态之间的切换,这个操作的⽐较耗
时。

如果竞争没有那么激烈,锁住的同步代码块执⾏的时间还没有切换上下⽂花的时间多,反⽽得不偿失。因此采⽤⾃
旋锁,出现竞争之后等⼀等再去尝试,可能前⾯获得锁的线程已经执⾏完了,再次加锁。这样就免去了升级重量锁
带来的消耗。

⾃旋不能⼀直进⾏。⾃旋时CPU是空转,这就浪费了处理器资源。上⾯的情况是竞争不激烈,但假设竞争激烈那么
⾃旋完全是浪费时间,还不如直接升级到重量级锁省资源。以前的⾃旋次数默认是10,如果10次之后依然不⾏说明
竞争很激烈,需要升级到重量级锁。

JDK1.6以后加⼊了⾃适应⾃旋:

对于某个锁对象,如果⾃旋等待刚刚成功获得过锁,并且持有锁的线程正在运⾏中,那么虚拟机就会认为这次⾃旋
也是很有可能再次成功,进⽽允许⾃旋等待持续相对更长时间

对于某个锁对象,如果⾃旋很少成功获得过锁,那在以后尝试获取这个锁时将可能省略掉⾃旋过程,直接阻塞线
程,避免浪费处理器资源。

除此之外,JVM还会根据CPU的负载进⾏优化:

如果平均负载⼩于CPUs则⼀直⾃旋

如果有超过(CPUs/2)个线程正在⾃旋,则后来线程直接阻塞

如果正在⾃旋的线程发现Owner发⽣了变化则延迟⾃旋时间(⾃旋计数)或进⼊阻塞

如果CPU处于节电模式则停⽌⾃旋

⾃旋时间的最坏情况是CPU的存储延迟(CPU A存储了⼀个数据,到CPU B得知这个数据直接的时间差)

⾃旋时会适当放弃线程优先级之间的差异

10.5、轻量级锁⾃旋
优点:

适合多个线程不同时访问同步对象场景。偏向锁的撤销必须在安全点才可以进⾏,假设多个线程交替访问某个对
象,处理完成后又释放锁,这种情况下偏向锁也可以完成但是效率会很低,每次撤销再加锁必须等到安全点,同时
还要进⾏Epoch的分析,因此偏向锁才会有批量撤销机制,撤销偏向锁次数过多则意味偏向锁不适⽤,不再新增偏
向锁⽽直接变成轻量级锁。轻量锁则很⽅便,直接CAS替换就可以,对这种多线程竞争不激烈的场景很适⽤。锁的
⾃旋也是为此设计。

缺点:

竞争激烈的场景下不适⽤,此时进⾏⾃旋就是再浪费CPU资源。竞争激烈时可能进⾏很多次的⾃旋都不回获得锁,
这种浪费的代价⽐上下⽂切换的代价要⼤,所以引⼊⾃适应⾃旋来裁决。

11、重量级锁
重量级锁的实现依赖于ObjectMonitor,⽽ObjectMonitor又依赖于操作系统底层的Mutex Lock(互斥锁)实现。

Monitor可以理解为⼀个同步⼯具或⼀种同步机制,通常被描述为⼀个对象。每⼀个Java对象就有⼀把看不见的
锁,称为内部锁或者Monitor锁。主要包含以下⼏部分:

Contention List:竞争队列,所有请求锁的线程⾸先被放在这个竞争队列中;

Entry List:Contention List中那些有资格成为候选资源的线程被移动到Entry List中;

Wait Set:哪些调⽤wait⽅法被阻塞的线程被放置在这⾥;

OnDeck:任意时刻,最多只有⼀个线程正在竞争锁资源,该线程被成为OnDeck;

Owner:当前已经获取到所资源的线程被称为Owner;

!Owner:当前释放锁的线程。

count:monitor的计数器,数值加1表⽰当前对象的锁被⼀个线程获取,线程释放monitor对象时减1

11.1、重量级锁的获取

锁升级为重量级之后,Mark Word中存储的指针不再指向线程,⽽是指向ObjectMonitor。当线程访问同步代码块
时,每个线程都会被封装成⼀个ObjectWaiter对象进⼊monitor。
JVM每次从队列的尾部取出⼀个数据⽤于锁竞争候选者(OnDeck),但是并发情况下,ContentionList会被⼤量的
并发线程进⾏CAS访问,为了降低对尾部元素的竞争,JVM会将⼀部分线程移动到EntryList中作为候选竞争线程。

Owner线程会在unlock时,将ContentionList中的部分线程迁移到EntryList中,并指定EntryList中的某个线程为
OnDeck线程(⼀般是最先进去的那个线程)。

Owner线程并不直接把锁传递给OnDeck线程,⽽是把锁竞争的权利交给OnDeck,OnDeck需要重新竞争锁。这样
虽然牺牲了⼀些公平性,但是能极⼤的提升系统的吞吐量,在JVM中,也把这种选择⾏为称之为“竞争切换”。

OnDeck线程获取到锁资源后会变为Owner线程,⽽没有得到锁资源的仍然停留在EntryList中。如果Owner线程被
wait⽅法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify或者notifyAll唤醒,会重新进去EntryList中。

处于ContentionList、EntryList、WaitSet中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux内核下
采⽤pthread_mutex_lock内核函数实现的)。

Synchronized是⾮公平锁。 Synchronized在线程进⼊ContentionList时,等待的线程会先尝试⾃旋获取锁,如果获
取不到就进⼊ContentionList,这明显对于已经进⼊队列的线程是不公平的,还有⼀个不公平的事情就是⾃旋获取
锁的线程还可能直接抢占OnDeck线程的锁资源。

为啥采取⾮公平锁,主要还是考虑到性能。采取⾮公平锁下⼀次获得锁线程可能还是原来持有锁的线程,这样就避
免了内核和⽤户态的切换,节省时间。

11.2、重量级锁的重⼊

线程如果获取到锁,会判断是否重⼊,如果是重⼊锁,count计数+1,释放锁时count数值减1。

11.3、重量级锁的优缺点

重量级锁需要内核态和⽤户态的切换,这个代价很⼤。所以把它放在最后,经过偏向和轻量级之后才是它。

但是只有它能应对竞争激烈的场景,也算是JVM最后的杀⼿锏了。

Synchronized 反向理解
在总结前⽂吃透synchronized关键字中,锁升级介绍是由低到⾼,⼀步⼀步的出发。其实也可以反其道⽽⾏之,想
想怎么锁降级,这样能更好的理解锁升级的思路。

原先synchronized关键字就是现在的重量级锁,不论遇到何种情况都是重量级锁,需要内核态和⽤户态之间的相互
切换。在JDK1.6之后引⼊了锁升级,分为⽆锁-偏向锁-轻量级锁-重量级锁。从开发⼈员⾓度想,为啥要这么做呢?

⽆锁状态和重量级锁肯定好理解,⼀个最轻,⼀个最重。记住⽆论何时何地,只要出现两个线程⼀直争夺同⼀个
锁,必然是⽤重量级锁。不管原先是⽆锁、偏向还是轻量级,出现两个线程竞争同⼀个锁,最后⼀定是启动重量级
锁。但不是任何时候都会有多个线程来竞争同⼀个锁,像这种竞争激烈场景在全部程序中并不太多。
假设同⼀个对象,⼀会线程A得到,执⾏完成之后释放,之后线程B得到,执⾏完成之后释放,甚⾄可能还有线程
CDEFG。根本没有竞争,只是单纯的加个锁然后释放,这种情况下⽤重量级锁就浪费了,杀鸡⽤⽜⼑,不划算。那
就锁简化⼀下,不⽤重量级的等待阻塞队列,也不⽤内核态和⽤户态切换了。也不需要对象头⾥指向mionter的指
针了,直接把这个存放线程的指针。在线程的帧栈中加个Lock Record,⽅便轻量级锁的判断。设么?⽤轻量级锁
有些时候还会出现竞争,还只是短暂地?切换线程多不划算,轻量级锁⾃旋吧,多等⼀会说不定就没竞争了。万⼀
这不是短暂地竞争呢?最初是限制⾃旋次数,又来⽤适应性⾃旋,更灵活的判断。

绝⼤多数连竞争也没有?单个线程访问某个对象,这个对象到死都没见过其他线程。线程每次加锁又释放,费那么
⼤⼒⽓建Lock Reocrd,还采⽤CAS的⽅式。杀鸡⽤⽜⼑,这可太亏了。把锁再简化⼀点,轻量级⾥还复制Mark
Word,⼲脆啥都不要了。直接在对象头也写线程ID,线程访问的时候对⽐⼀下和⾃⼰的⼀样不⼀样就⾏,多简
单。锁⼲脆不释放了,就算不⽤了也懒得改,等下次那个线程要偏向⾃⼰去对象头⾥把线程ID写成它的就⾏。

不对啊,单线程访问的省⼼了,但还是有其他情况啊。万⼀锁多个对象,其中⼀部分已经可以释放了,另⼀部分还
必须拿着,偏向锁咋区分这种?简单,给个Epoch值,进⼊STW的时候看看那些对象还需要,给他们新的Epoch,
不需要的就不管。Epoch是不能重偏向,旧的就可以重偏向,哪个线程需要了就可以改过去。万⼀线程A和线程B交
替访问⼀个对象,或者现场A锁住⼤量对象后线程B也来锁这些对象,这压根不是偏向锁设计的初衷,引出批量重偏
向和批量撤销。

另外,出现竞争系统是怎么判断的?是某个线程想要加锁却被⼈占有,但是原先持有锁的线程并不知情。原先持有
轻量锁的想要释放锁的时候⼀看,变重量级锁了,已经有⼀堆线程排着等了。锁释放掉,还得把队列⾥睡眠都线程
唤醒,让他们竞争锁。偏向锁升级轻量级锁,需要到STW才能偏向撤销,还需要回复对象的Mark Word。

AQS
1、概念:

AQS,⼀个⽤来构建锁和同步器的框架,许多的经典的同步器都是基于AQS构建出来的,譬如CountDownLatch、
Semaphored。

其内部使⽤同步队列帮我们解决了实现同步器时的⼀些细节问题,譬如何时阻塞线程,如何按照顺序唤醒线程。
2、核⼼思想:

AQS通过⼀个被volatile修饰的int类型的成员变量表⽰同步状态(共享资源),然后使⽤cas对这个同步状态进⾏修
改,从⽽保证线程安全。

如果被请求的共享资源空闲,则将当前的请求线程设置为有效的⼯作线程,然后将对应的共享资源设置为锁定状
态。

如果共享资源被占⽤,则通过CLH同步队列将暂时⽤不到的线程封装成⼀个节点加⼊到队列中,同时在适当的时候
对其进⾏阻塞和唤醒。

基于AQS实现的应⽤(⽬前接触到的)

(1)lock

区别于synchronozed的锁,同时还⽀持了更加的灵活丰富操作,譬如可定时的尝试获取锁(避免了死锁的发⽣)、
可中断的获取锁。

(2)CountDownLatch

⼀个或⼀组操作等其他操作执⾏结束之后,继续执⾏。

(3)Semaphore

⽤于多个共享资源的互斥,或者⽤于控制并发数量

3、内部细节:

1. ⽬前理解是通过waitStatus来标志线程的阻塞、唤醒等状态,但是每个节点的waitStatus状态不是通过⾃⾝设置
的,⽽是通过其后继节点设置的。
2. 内部的同步队列采⽤的数据结构是双向链表,同时Head节点属于dummy节点,不存储信息,仅表⽰当前持有锁
的线程,同时还会负责后续阻塞线程的唤醒
3. exclusiveOwnerThread变量,标志当前持有锁的线程,防⽌错误的释放锁,这种思想在Redis分布式锁中也有体

4. state是⽤volatile修饰的int型变量,这样其除了能够实现独占锁之外,还能实现Semaphore、CountDownLatch
这样的同步⼯具
5. 对外提供了使⽤protected修饰的⽅法tryAcquire、tryRelease等,这样在实现的⼦类中,能够通过这些⽅法,实
现⼀些我们⾃定义的内容,譬如锁的可重⼊,公平与⾮公平锁。

基于ReentrantLock的独占式共享锁的整个流程:

原图链接:「代码随想录」知识星球

JDK

java 并发 中的 ThreadLocal
有多个线程(孩⼦),争抢同⼀个共享变量(玩具),就会产⽣冲突,⽽程序的解决办法是加锁(⽗母说服,讲道理,轮
流玩),但加锁就意味着性能的消耗(⽗母⽐较累)。

所以有⼀种解决办法就是避免共享(让每个孩⼦都各⾃拥有⼀个变形⾦刚),这样线程之间就不需要竞争共享变量(孩
⼦之间就不会争抢)。
所以 ThreadLocal 的存在就是为了通过本地化资源来避免共享,避免了多线程竞争导致的锁等消耗。

在每个线程内部都有⼀个名为threadLocals的成员变量,该变量的类型为HashMap,其中key为我们定义的
ThreadLocal变量的this 引⽤,value 则为我们使⽤set⽅法设置的值。每个线程的本地变量存放在线程⾃⼰的内存变
量threadLocals中,如果当前线程⼀直不消亡,那么这些本地变量会⼀直存在,所以可能会造成内存溢出,因此使
⽤完毕后要记得调⽤ThreadLocal的remove⽅法删除对应线程的threadLocals中的本地变量。

ThreadLocal
概述:

ThreadLocal提供了⼀种⽅式,让在多线程环境下,每个线程都可以拥有⾃⼰私有的数据结构,进⽽减少同⼀个线
程内多个函数或者组件之间⼀些公共变量的传递的复杂度。但因为其⾃⾝的实现机制,使⽤之后记得及时remove,
避免内存泄漏。

每个线程都有⼀个threadLocals变量,该变量的数据结构是threadLocalMap,⽽map中的每个entry中key是
threadLocal,value是我们需要存储的本地变量

部分细节:

1. get操作,因为threadLocal是⼀个key,所以每次获取其映射的value时,⾸先先获取当前线程的
threadLocalMap,然后再根据该map和key获取对⽤的value
2. set操作,同理也需要先根据threadLocal这个key获取map,然后通过map才能真正的将threadLocal变量和value
进⾏映射
3. threadLocalMap和HashMap差不多,使⽤的数据结构都是hash表,不过对于hash冲突的解决⽅式,
threadLocalMap使⽤的是线型探测,装载因⼦采⽤的是黄⾦分割点,然后hash算法使⽤的是斐波那契散列发,
扩容⽅式均为原Entry数组的2倍
4. 因为threadLocalMap的中的key也就是threadLocal是弱引⽤,所以只要触发GC,key就会被回收,这样
ThreadMap中的key就变成了null,但是value被Entry引⽤,Entry被ThreadLocalMap引⽤,ThreadLocalMap被
Thread引⽤,这也就说明了只要,线程不终⽌,value的值⼀直⽆法被回收,所以可能会出现内存泄漏的现象,
所以使⽤完之后,要及时调⽤remove⽅法。
5. 如果想⼦线程继承⽗线程的本地变量,可以采⽤InheritableThreadLocal,感觉这个在中间件⾥⾯⽤的会很多,
没验证过。
JVM

JVM内存管理
Java 运⾏时数据区组成部分?哪些是共享的,哪些是私有的?

有以下部分:

Java堆,⽅法区,Java 虚拟机栈,本地⽅法栈,程序计数器。

公有私有:

私有数据区
1、程序计数器

程序计数器,记录的是正的是正在执⾏的虚拟机字节码指令的地址。字节码解释器⼯作时,就是通过改变这个计数
器的值来选取下⼀条需要执⾏的字节码指令,完成程序的流程控制。

在多线程情况下,每个线程都需要有⼀个独⽴的程序计数器,以便于切换回来后可以恢复到正确的执⾏位置。

2、Java 虚拟机栈

Java 虚拟机栈由⼀个个栈帧组成,每个⽅法被执⾏时,Java 虚拟机都会同步创建⼀个栈帧。

⼀个⽅法从开始被调⽤到执⾏完毕,对应的就是⼀个栈帧在虚拟机中⼊栈到出栈的过程。

栈帧 Stack Frame,⽤于存储 局部变量表,操作数栈,动态连接,⽅法出⼜等信息。

3、局部变量表

存放编译期间可知的 各种 Java 虚拟机基本数据类型,对象引⽤和 returnAddress 类型(指向了⼀条字节码指令的


地址)。

这些数据类型在局部变量表中以局部变量槽来表⽰,局部变量表所需的内存空间在编译期间完成分配,⽅法运⾏期
间,不会改变槽的数量,具体的内存空间还是由虚拟机来决定。
4、本地⽅法栈

为虚拟机使⽤到的本地⽅法 native ⽅法服务,其余的同 Java 虚拟机栈。

共享数据区
1、Java 堆

Java 堆 在虚拟机启动时创建,是垃圾收集器管理的内存区域。这⾥暂时就这么多,再往下细分永久代啥的,以后再

2、⽅法区

⽅法区⽤于存储已经被虚拟机加载的类型信息,常量,静态变量,即时编译器编译后的代码缓存等数据。

⽅法区与永久代,可以说 ⽅法区是接⼜,是规范,永久代是 HotSpot 虚拟机给出的实现,

3、运⾏时常量池

运⾏时常量池是⽅法区的⼀部分,包括类的描述信息和常量池表,

其中 类的描述信息包括类的版本,字段,⽅法,接⼜。

常量池表⽤于存储 编译期⽣成的各种字⾯量与符号引⽤。在类加载后,常量池表存放到⽅法区的运⾏时常量池中。

运⾏时常量池具有动态性,运⾏期间也可以将新的常量放⼊。

对象的创建
五步

1、类加载检查

当 Java 虚拟机遇到⼀条字节码 new 指令时,⾸先将去检查这个指令的参数是否能够在常量池中定位到⼀个类的符


号引⽤,并且检查这个符号引⽤代表的类是否已经被加载,解析和初始化过,如果没有,那么就要先执⾏相应的类
加载过程。

2、分配内存

在类加载完成后,对象所需要的内存⼤⼩完全确定,可以对新⽣对象分配内存。

分配内存有两种⽅式:

(1)内存规整

如果内存规整,就是指针碰撞,将指针向空闲空间⽅向移动与对象⼤⼩相等的距离

(2)内存不规整

如果内存不规整,那就是空闲列表,维护⼀个列表,记录哪些内存块可⽤,在分配时,从列表中找到⼀块⾜够⼤的
空间划分给对象实例,并更新列表上的记录。

划分空间时的并发问题的解决⽅法:

CAS + 失败重试,另⼀种是 TLAB :先预先分配⼀块内存,称为本地内存缓冲 Thread Local Allocation Buffer,线程


先在⾃⼰的 TLAB 中分配,⽤完了之后,分配新的缓冲区时,进⾏同步锁定。
3、初始化内存空间

分配到的内存空间(对象头除外)全部分配为 0 值,这步保证了对象的实例字段在 Java 代码中可以不赋值就直接使


⽤,使得程序可以访问到这些字段的数据类型所对应的零值。

4、进⾏对象头的设置

在对象的对象头中保存⼀些必要的信息:这个对象是哪个类的实例,如何才能找到类的元数据信息, 对象的哈希
码,对象的GC 分代年龄等信息。

5、执⾏构造函数

按照程序员的意愿初始化对象

对象的结构
⼀个对象分为以下⼏个部分: 对象头,实例数据,对齐填充

1、对象头

对象头包括两类数据:对象⾃⾝的运⾏时数据和类型指针。

(1)对象⾃⾝的运⾏时数据

对象⾃⾝的运⾏时数据叫做 Mark Word, 包括哈希码, GC 分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏


向时间戳等。

Mark Word 是⼀个动态的数据结构,为的就是在极⼩的空间内存储尽量多的数据。

(2)类型指针

类型指针,对象指向元数据的指针,Java 通过这个来确定对象是哪个类的实例。

如果对象是数组类型,那么在对象头中还必须有⼀块⽤于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象
的元数据信息确定 Java 对象的⼤⼩,但是如果数据的长度不确定,将⽆法通过元数据中的信息判断出数组的⼤⼩。

2、实例数据

实例数据是对象真正存储的有效信息,也就是我们在程序代码中所定义的各种类型的字段内容,包括从⽗类继承
的。 这部分的存储顺序会受到 HotSpot 虚拟机默认的分配策略参数 和 字段在 Java 源码中定义顺序的影响。

3、对齐填充

对齐填充其实不是必然的,作⽤就是在占位符,保证对象的起始地址是 8 字节的整数倍。

也就是说,任何对象的⼤⼩都必须是 8 字节的整数倍,这个是 HotSpot 虚拟机的⾃动内存管理系统的要求。

对象访问定位
对象的访问的两种⽅式?区别和优缺点

Java 通过栈上的 reference 数据来操作堆上的具体对象。

对象访问的两种⽅式为句柄和直接指针

1、句柄⽅式
Java 堆中将可能会划分除⼀块内存来作为 句柄池,然后 reference 储存句柄池的地址,句柄中包含了指向对象实例
数据的指针,和对象类型数据的指针。

2、直接指针

reference 中放的是对象地址

3、优缺点

句柄⽅式⽐较稳定:对象被移动时,只会改变句柄中的实例数据指针,不需要修改 reference 本⾝。

直接指针的话,少了⼀次指针定位的开销,所以好处就是访问速度更快。

JVM之经典收集器---G1
全称Garbage-First,之前的收集器因为是直接将堆内存物理划分为新⽣代⽼年代

所以需要来两种收集器配合使⽤才能实现全代的回收,G1将整个堆内存划分为多个等⼤的region(内部连续)

然后每个region不同时间代表⾓⾊不固定,不过整体分为四种:Eden、Survioor、Old、Humongous(存储⼤对
象)

对于G1来说整体采⽤的是标记-整理算法,然后从region的⾓度来看,采⽤的是复制算法
G1 垃圾收集过程主要分为4个阶段:

这⾥不同的地⽅,总结不同,最后还是选择以深⼊理解Java虚拟机为标准。

1、初始标记

1. 联GC Roots直接关联的对象
2. 触发STW

2、并发标记

和⽤户程序并发执⾏,标记出所有回收对象

3、最终标记

1. 处理并发标记后,新产⽣的对象
2. 触发STW

4、筛选回收

1. 先对Region的回收价值进⾏排序,然后根据期望暂停时间,选择性回收Region
2. 回收时采⽤标记复制,多条收集器线程并发执⾏
3. 不追求⼀次全部清理完
4. 触发STW
类加载机制
类加载机制:

类的数据从Class⽂件加载到内存,并对数据进⾏校验、转换解析和初始化,最 终形成可以被虚拟机直接使⽤的
Java类型。

类的⽣命周期:

⼀个类型从被加载到虚拟机内存中开始,到卸载出内存为⽌,它的整个⽣命周期将会经历加载 (Loading)、验证
(Verification)、准备(Preparation)、解析(Resolution)、初始化 (Initialization)、使⽤(Using)和卸载
(Unloading)七个阶段,其中验证、准备、解析三个部分统称 为连接(Linking)。

其中解析也可以在初始化之后再开始,位置并不是唯⼀的。

加载
在加载阶段,虚拟机要完成三件事情

1、通过类的全限定名来获取定义此类的⼆进制字节流

2、将这个字节流所代表的静态存储结构转化为⽅法区的运⾏时数据结构

3、在内存中⽣成⼀个代表这个类的[Link]对象,作为⽅法区这个类的各种数据访问的⼊⼜

这个阶段需要⽤到加载器,“双亲委派模型”与此有关。

数组类对象是虚拟机直接在动态内存中构造出来,但这个数组的组件类型由加载器加载。⽐如int[],char[]组件
类型不是引⽤类型,由引导类加载器加载,⽽类似Integer[]这种组件为引⽤类型的,就按照本⽂说的步骤加载。

⽽关于第⼀点,获取类的⼆进制字节流。并没有规定必须从哪⾥获取,从ZIP压缩包读取,从⽹络中获取(例如
Web Applet),运算时计算⽣成(如动态代理),其他⽂件⽣成(如JSP),从加密⽂件中获取等等。

连接

验证

Class⽂件可以由Java源代码编译⽽来,但它也可以由其他途径产⽣。毕竟Class⽂件只是⼀种格式,最底层的还是
0/1,它可以使⽤包括靠键盘0和1直接在⼆进制编辑器中敲出 Class ⽂件在内的任何途径产。

Java虚拟机如果不检查输⼊的字节流,对其完全信任的话,很可能会因为载⼊了有错误或有恶意企图的字节码流⽽
导致整个系统受攻击甚⾄崩溃。

1、⽂件格式验证:
检查是否符合Class⽂件的格式,如Class⽂件开头的魔数,主次版本号是否可以被虚拟机接受、常量池是否正确等

2、元数据验证:

对类的元数据信息进⾏语义校验。⽐如是否有⽗类,是否继承了不被允许的类、是否实现了接⼜或者抽象类中的⽅
法等等

3、字节码验证:

通过数据流分析和控制流分析,确定 程序语义是合法的、符合逻辑的。就是对将要执⾏的程序字节码验证(即
Class中的Code属性),是否出现了逻辑性的错误。字节码验证有问题,则程序⼀定不对,但字节码验证没问题,
程序不⼀定能正确执⾏。通过程序去校验程序逻辑是⽆法做到绝对准确的,不可能⽤程序来准确判定⼀段程序是否
存在Bug。这⼀阶段很费时间,所以在编译器⽣成Class⽂件时就会⽣成StackMapTable,这样虚拟机直接检查
StackMapTable记录是否合法就⾏。

4、符号引⽤验证:

验证将符号引⽤转化为直接引⽤时候合法,判断该类是否缺少或者被禁⽌访问它依赖的某些外部类、⽅法、字段等
资源。这个动作实在解析阶段进⾏的,所以这7个步骤并不是严格的顺序,很多时候是交替进⾏的。

验证阶段有点类似查杀病毒,确保虚拟机执⾏的 Class ⽂件都是正确的。它很重要但并不是必须的,如果使⽤的


Class 类都是经过反复使⽤和验证的,那就可以关闭类验证措施,缩⼩类加载的时间。就像不开启防⽕墙,不下载
⼀些恶意软件也能正常使⽤计算机。

准备

准备阶段是为类中静态变量分配内存并设置类变量初始值的阶段。

此处仅包括静态变量(static修饰),⽽不包括实例变量,实例变量实在对象实例化的时候随着对象⼀起分配在堆
中。⽽且此处的初始值指的数据类型默认的零指。

⽐如public static int a = 123, 准备阶段会给 a 赋值为 0,⽽不是 123。a 变成 123 是类构造器完成的,所以
是在初始化阶段才会执⾏。

也有例外,如果此处类字段属性存在 ConstantValue 属性,那么在准备阶段就会赋值。

例如public static final int value = 123,添加 final 关键字后类会有 ConstantValue 属性,此时在准备阶
段 a 就会被赋值为 123。

解析

解析阶段是Java虚拟机将常量池内的符号引⽤替换为直接引⽤的过程。

符号引⽤是以⼀组符号来描述所引⽤的⽬标,它与虚拟机实现的内存布局⽆关,引⽤的⽬标并不⼀定是已经加载到
虚拟机内存当中的内容。⽽直接引⽤是可以直接指向⽬标的指针、相对偏移量或者是⼀个能间接定位到⽬标的句
柄。如果有了直接引⽤,那引⽤的⽬标必定已经在虚拟机的内存中存在。

初始化
类加载过程最后⼀步。进⾏准备阶段时,静态变量已经赋过⼀次系统要求的初始零值,⽽在初始化阶段,则会根据
程序员通过程序编码制定的主观计划去初始化类变量和其他资源。
通俗的说初始化就是执⾏类构造器()⽅法的过程。()⽅法是由编译器⾃动收集类中的所有类变量的赋值动作和静态语
句块(static{}块)中的语句合并产⽣的,编译器收集的顺序是由语句在源⽂件中出现的顺序决定的,静态语句块中
只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前⾯的静态语句块可以赋值,但是不能访问。

public class Test {


static i = 0; // 给变量复制可以正常编译通过 可以给后⾯定义都赋值
[Link](i); // 这句编译器会提示“⾮法向前引⽤” static int i=1在此之后才定义 ⽆法访
问}
static int i = 1; //此处才定义
}

⽗类中的()⽅法在⼦类之前执⾏,保证在⼦类执⾏前⽗类的()已经执⾏完毕。()⽅法对于类或接⼜来说并不是必需
的,如果⼀个类中没有静态语句块,也没有对变量的 赋值操作,那么编译器可以不为这个类⽣成()⽅法。

类加载器及加载机制

类加载器

在类加载流程第⼀步“加载”阶段,通过⼀个类的全限定名来获取描述该类的⼆进制字节流,实现这⼀功能的代码叫
做类加载器。

Java中的任意⼀个类,都必须由加载它的类加载器和这个类本⾝⼀起共同确⽴其在Java虚拟机中的唯⼀性,每⼀个
类加载器,都拥有⼀个独⽴的类名称空间。

判断两个类是否相等,必须在这两个类是被同⼀个加载器加载的前提下才有意义。否则即使Class⽂件相同,被同⼀
个虚拟机加载,但是使⽤不同的类加载器,那么两个类也是不同的。

双亲委派模型

如果⼀个类被不同的加载器加载,那虚拟机会认定是不同的类,这样Java体系最基础的⾏为也⽆从保证,应⽤程序
会变得混乱。

所以有了双亲委派模型,双亲委派⼀共分了四层的加载器。求除了顶层的启动类加载器外,其余的类加载器都应有
⾃⼰的⽗类加载器,⼦类使⽤“组合”关系来复⽤⽗类加载器的代码,⽽不是继承。

双亲委派模型⼀共分四层:
1、启动类加载器:

负责加载存放在 <JAVA_HOME>\lib⽬录,或者被-Xbootclasspath参数所指定的路径中存放的类

2、扩展类加载器:

它负责加载<JAVA_HOME>\lib\ext⽬录中,或者被[Link]系统变量所 指定的路径中所有的类库

3、应⽤类加载器:

负责加载⽤户类路径 (ClassPath)上所有的类库,开发者同样可以直接在代码中使⽤这个类加载器。如果应⽤程
序中没有 ⾃定义过⾃⼰的类加载器,⼀般情况下这个就是程序中默认的类加载器。

4、⾃定义加载器:

⽤户⾃⾏扩展,典型的如增加除了磁盘位置之外的Class⽂件来源,或者通过类 加载器实现类的隔离、重载等功
能。

如果⼀个类加载器收到了类加载的请求,它⾸先不会⾃⼰去尝试加载这个类,⽽是把这个请求委派给⽗类加载器去
完成,每⼀个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当
⽗加载器反馈⾃⼰⽆法完成这个加载请 求(它的搜索范围中没有找到所需的类)时,⼦加载器才会尝试⾃⼰去完成
加载。

破坏双亲委派

亲委派很好地解决了各个类加载器协作时基础类型的⼀致性问题(越基础的类由越上层的加载器进⾏加载),但如
果有基础类型又要调⽤回⽤户的代码就不得不破坏双亲委派模型,也就是⽗类加载器需要去请求⼦类加载器去完成
需要的类。

典型例⼦如JNDI,JNDI的代码由启动类加载器完成加载,但是这个类需要应⽤程序ClassPath下的JNDI服务提供者
接⼜,启动类加载器⽆法加载这些代码。为解决这个问题使⽤线程上下⽂类加载器,JNDI服务使⽤这个线程上下⽂
类加载器去加载所需的SPI服务代码,这是⼀种⽗类加载器去请求⼦类加载器完成类加载的⾏为,这种⾏为实际上
是打通了双亲委派模型的层次结构来逆向使⽤类加载器。

OSGi实现模块化热部署:在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,⽽是进⼀步发展为更加复
杂的⽹状结构。

内部类
使⽤原因:

1. 内部类可以对同⼀个包中的其他类隐藏
2. 内部类⽅法可以访问定义这个类的作⽤域中的数据,包括私有数据

1. 使⽤内部类访问对象状态
⼀个内部类⽅法可以访问⾃⾝的数据字段,也可以访问创建它的外围对象的数据字段,所以,内部类的对象总有⼀
个 隐式引⽤,指向创建它的外部类对象

外围类的引⽤在构造器中设置,编译器会修改所有内部类的构造器,添加提个对应的外围类引⽤的参数
只有内部类是可以是 private,⽽常规类只可以有 包可见性(default、protected) 或 公共可见性(public)

2. 内部类的特殊语法规则
1、外围类引⽤[Link]

如:[Link]

2、内部类对象的构造器

[Link] InnerClass(construction param)

3、外围类的作⽤域之外引⽤内部类

[Link]

4、内部类中声明的所有静态字段都必须是 final,并初始化为⼀个编译时常量

5、内部类不能有static⽅法,除了访问外围类的静态字段和⽅法的静态⽅法

3. 内部类与编译器
内部类是⼀个编译器现象,与虚拟机⽆关,编译器会讲内部类转换为常规的类⽂件,⽤ $ 分隔外部类名和内部类名

4. 局部内部类
当这个类的对象只被⼀个⽅法创建⼀次时,在⼀个⽅法中的局部地定义这个类

1、声明局部类时

不能有访问说明符(private,public) 局部类的作⽤域被限定在声明这个局部类的块中

2、局部类的优势

对外部世界完全隐藏,连外围类中的其他代码都不能访问它

5. 由外部⽅法访问变量
与其他内部类相⽐较,局部类可以访问 ⽅法中的局部变量
不过,这些变量必须是 final 变量

6. 匿名内部类
只创建这个类的⼀个对象,不需要为类指定名字

1、语法:new superType(construcrion param) {...}

superType可以是接⼜,内部类要实现这个接⼜,也可以是⼀个类,内部类就要扩展这个类

2、构造器名字必须与类名相同

匿名内部类没有类名,所以匿名内部类不能有构造器,实际上,构造参数要传递给超类的构造器。但,只要内部类
实现⼀个接⼜,就不能有任何构造参数,不过仍然要⼩括号

尽管匿名内部类不能有构造器,但可以提供⼀个对象初始化块
new Person("asd") { {init} ....}

3、如果构造参数列表的结束⼩括号后⾯跟⼀个开始⼤括号,就是在定义匿名内部类

4、静态⽅法中要得到当前类名,不能使⽤ this,静态⽅法没有this,可以使⽤如下技巧

new Object(){}.getClass().getEnclosingClass()
上式,创建了⼀个匿名内部类,getEnclosingClass得到他的外围类

7. 双括号初始化
利⽤了内部类语法,使⽤匿名列表

ArrayList<Integer> array = new ArrayList<>(){{


add(1);
add(2);
add(3);
}};
[Link]([Link]());

外层括号建⽴了ArrayList的⼀个匿名⼦类,内层括号则是⼀个对象初始化块

适⽤于这些数组列表不需要再使⽤的情况

Map,Deque,Set等集合也有类似技巧

8. 静态内部类
static声明的类

1. 静态内部类类似于其他内部类,不过静态内部类的独享没有⽣成它外围类对象的引⽤
2. 只要内部类不需要访问外围类对象,就应该使⽤静态内部类
3. 与常规内部类不同,静态内部类可以由静态字段和⽅法
4. 在接⼜中声明的内部类⾃动是 public 和 static

代理类
1. 创建代理对象
使⽤Proxy类的newProxyInstance⽅法,有三个参数

1. ⼀个类加载器
2. ⼀个Class对象数组
3. ⼀个调⽤处理器

Student s1 = new Student();


//动态代理增强s1对象
/*
三个参数:
类加载器:真实对象.getClass().getClassloader()
接⼝数组:真实对象.getClass().getInterfaces()
处理器:new InvocationHandler()
*/
Student proxy_s1 = (Student)[Link]([Link]().getClassLoader(),
[Link]().getInterfaces(), new InvocationHandler() {
//代理逻辑编写的⽅法,代理对象调⽤的所有⽅法都会触发该⽅法的执⾏
/*
参数:proxy:代理对象
method:代理对象调⽤的⽅法,被封装为对象
args:代理对象调⽤实际⽅法时,传递的实际参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable {
[Link]("每次都会调⽤我");

if([Link]().equals("setName")){//如果调⽤setName()⽅法就增强,否则不增强
String name = args[0];
name = "李四";
String obj = (String) [Link](s1,name);//使⽤真实对象调⽤该⽅法,改变参数调⽤,同
时改变返回值为String
return obj+"和王五";
}else{
Object obj = [Link](s1,args);//使⽤真实对象调⽤该⽅法,原样调⽤
return obj;
}
}
});
[Link](proxy_s1.setName("张三");)//输出“每次都会调⽤我”,“李四和王五”,
//证明每次调⽤代理对象的真实对象的⽅法都会执⾏invoke()
//并且method对象是代理对象调⽤的⽅法的被封装后的对象,args是传递的参数

2. 代理类的特性
代理类是在程序运⾏过程中动态创建的,⼀旦被创建,就成为了常规类

所有代理类都扩展Proxy类,⼀个代理类只有⼀个实例字段,调⽤处理器,在Proxy超类中定义,完成代理对象任务
所需要的任何额外数据都必须存储在调⽤处理器中

代理类总是 public 或 final,如果代理类实现的所有接⼜都是 public 这个代理类就不输⼊任何特定的包,⽃则所有


⾮公共的接⼜都必须属于同⼀个包,代理类也属于这个包

Spring
IoC
IoC: 控制反转,意思就是将创建对象的控制权从⾃⼰硬编码new的⼀个对象反转到了第三⽅⾝上。

IoC的主要实现⽅式是依赖注⼊,Spring中的依赖注⼊⽅式有:构造⽅法注⼊、settter注⼊、接⼜注⼊。

⽬的:

帮助我们接耦各种有依赖关系的业务对象之间的绑定关系

IoC-Provider
虽然不需要我们⾃⼰来做绑定关系,但是这部分的⼯作还是需要有⼈来实现的,所以IoC Provider就担任了这个⾓
⾊,同时IoC Provider的职责也不仅仅这些,其基础职责如下:

1、业务对象的构建管理:

IoC中,业务对象不需要关⼼所依赖的对象如何构建如何获取,这部分任务交由IoC Provider

2、业务对象之间的依赖绑定:

通过结合之前构建和管理的所有业务对象,以及各个业务对象之间可识别的依赖关系,将这些对象所依赖的对象注
⼊绑定。从⽽保证每个业务对象在使⽤的时候,可以出狱就绪状态。

Spring的IoC容器
担任了IoC Provider的职责,同时在此基础上,还增加了对Bean⽣命周期的管理、AOP⽀持内容。

从整体来看Spring的IoC容器的作⽤,共分为两部分:

1、容器启动阶段:

以某种⽅式将配置的Bean信息(XML、注解、Java编码)加载如整个Spring应⽤

2、Bean实例化阶段:

将加载的Bean配置信息组装成应⽤需要的业务对象

在此基础上,还充分运⽤了这两个阶段不同的特点,都预留了拓展钩⼦,供我们根据业务场景进⾏⾃定义拓展。
⼀些核⼼的接⼜、类介绍:

1. Resource: ⽤于解决 IoC 容器中的内容从哪⾥来的问题,也就是 配置⽂件从哪⾥读取、配置⽂件如何读取 的问题


2. BeanDefinition: ⽤于解决 Bean 的具体定义问题,包括 Bean 的名字是什么、它的类型是什么,它的属性赋予了
哪些值或者引⽤,也就是 如何在 IoC 容器中定义⼀个 Bean,使得 IoC 容器可以根据这个定义来⽣成实例 的问
题。
3. BeanFactoy: ⽤于解决 IoC 容器在 已经获取 Bean 的定义的情况下,如何装配、获取 Bean 实例 的问题。
4. ApplcationContext: 对上述的内容进⾏了功能的封装,解决 根据地址获取 IoC 容器并使⽤ 的问题。

从Bean的⾓度来看,其整个⽣命周期如下:
Spring只帮我们管理单例Bean的⽣命周期,对于prototype类型的bean,Spring在创建好交给使⽤者使⽤之后,就
不在管理其后续的⽣命周期了。

粗略涉及具体源码的整体流程:
1. BeanDefinitionReader读取Bean的配置信息(XML等),将读取到的每个Bean的配置信息使⽤BeanDefinition表
⽰,同时注册到相应的BeanDefinitionRegistry(⼀个map)中 。
2. 通过实现了BeanFactoryPostProcessor的类,⾃定义修改BeanDefinition中的信息(如果有的话)
3. Bean的实例化:
(1) 采⽤策略化bean的实例, 两种⽅式:cglib、反射
(2) 获取Bean的实例之后,根据BeanDefinition中信息,填充Bean的属性、依赖
4. 检测各种Aware接⼜,BeanFactory的、ApplicationContext的等
5. 调⽤BeanPostProcessor接⼜的前置处理⽅法,处理符合要求的Bean实例
6. 如果实现了InitializingBean接⼜,执⾏对应的afterPropertiesSet()⽅法
7. 如果定义了init-method,执⾏对应的⾃定义初始化⽅法
8. 调⽤BeanPostProcessor接⼜的前置处理⽅法,处理符合要求的Bean实例
9. 使⽤
10. 判断Bean的Scope,如果是prototype类型,不再管理
11. 如果是单例类型,如果实现了DisposableBean接⼜,执⾏对应的destoy⽅法
12. 如果定义了destory-method,执⾏对应的⾃定义销毁⽅法

两个拓展点:

1、BeanFactoryPostProcess

Spring提供的容器扩展机制,允许我们在bean实例化之前修改bean的定义信息即BeanDefinition的信息

2、BeanPostProcessor

也是Spring提供的容器扩展机制,不同于BeanFactoryPostProcessor的是,BeanPostProcessor在bean实例化后修改
bean或替换bean。BeanPostProcessor是后⾯实现AOP的关键。

Java知识导图

Java程序设计基本结构
原图链接:「代码随想录」知识星球
Java 基础总结
原图链接:「代码随想录」知识星球
Java基础总结
原图链接:「代码随想录」知识星球
原图链接:「代码随想录」知识星球

Java基础总结
原图链接:「代码随想录」知识星球
Java基础总结
原图链接:「代码随想录」知识星球

volatile关键字
原图链接:「代码随想录」知识星球
Java类和对象
原图链接:「代码随想录」知识星球
Java容器
原图链接:「代码随想录」知识星球
String Table
原图链接:「代码随想录」知识星球

List
原图链接:「代码随想录」知识星球
Java异常
Servlet
原图链接:「代码随想录」知识星球
JVM内存
原图链接:「代码随想录」知识星球
JVM内存管理
原图链接:「代码随想录」知识星球
JavaWeb基础
原图链接:「代码随想录」知识星球
Java类加载⼦系统(1)
原图链接:「代码随想录」知识星球
Java类加载⼦系统(2)
原图链接:「代码随想录」知识星球

Java类加载⼦系统(3)
原图链接:「代码随想录」知识星球
运⾏时数据区(1)
原图链接:「代码随想录」知识星球
原图链接:「代码随想录」知识星球
运⾏时数据区(2)
原图链接:「代码随想录」知识星球
运⾏时数据区(3)
原图链接:「代码随想录」知识星球
线程安全
原图链接:「代码随想录」知识星球
原图链接:「代码随想录」知识星球
锁优化
原图链接:「代码随想录」知识星球
Synchronized关键字
原图链接:「代码随想录」知识星球
AQS
原图链接:「代码随想录」知识星球

ThreadLocal
原图链接:「代码随想录」知识星球
Java⼯具包
原图链接:「代码随想录」知识星球

(1) [Link]

(2) [Link]
(3) [Link]
(4) [Link]
Spring
原图链接:「代码随想录」知识星球

You might also like