Java核心
2022-07-30 21:01:17 0 举报
AI智能生成
Java 核心基础技术总结
作者其他创作
大纲/内容
工具
lombok
Quartz
guava
Joda-Money使用
面向对象
封装
封装内在的实现,对外提供公共的访问方法
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。
继承
java类 单继承,接口可以多继承
抽象公有实现的抽象(父类),子类扩展父类的功能
多态
继承,重写,父类引用指向子类对象
动态绑定
多态分为编译时多态和运行时多态:
编译时多态主要指方法的重载
运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定
编译时多态主要指方法的重载
运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定
面向对象的理解
面向对象将重点放在数据( 即对象 )和对象的接口上。程序= 算法+ 数据结构,而面向对象将数据(对象)先考虑,然后考虑算法实现
对象具有状态(属性,域),行为(方法,函数),标识(每一个对象在内存中都有唯一的标识)。
用木匠打一个比方 ,一个 “ 面向对象的 ” 木匠始终关注的是所制作的椅子 , 第二位才是所使用的工具 ; 一个 “ 非面向对象的 ” 木匠首先考虑的是所用的工具。
抽象
面向对象程序设计的挑战之一,就是在问题空间和元素和解空间的对象之间创建一对一的映射。
面向对象六大原则
单一职责
它的定义是:就一个类而言,应该仅有一个引起它变化的原因。
一个类中是一组相关性和高的函数,一个类尽量只实现一个功能。
开闭原则
它的定义是:程序中的对象应该对应扩展是开放的,对于修改是封闭的。
里氏代换原则
它的定义是:所有引用基类的地方必须能透明的使用其子类对象。
依赖倒换原则
高级模块不应依赖于低级模块,两者都应依赖于抽象。抽象不应依赖于细节。细节应依赖于抽象。
理解为: 面向接口编程,或者说面向抽象编程。程序要依赖于抽象接口,不要依赖于具体实现。
接口隔离原则
它的定义是:客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。
迪米特法则或最少知识原则
它的定义是:一个对象应该对其他对象有最少的了解
扩展
细思极恐-你真的会写java吗?
如何优雅的设计java异常
java匠人手法-优雅的处理空值
java编程最佳实践
正确的打日志姿势
对象的创建与内存分配
集合
Collection接口
Set接口
HashSet
LinkedHashSet
SortedSet接口
TreeSet
List接口
ArrayList
LinkedList
Vector
Stack
Queue
LinkedList:可以用它来实现双向队列
PriorityQueue:基于堆结构实现,可以用它来实现优先队列
Map接口
HashMap
Hashtable
SortedMap接口
TreeMap
WeakHashMap
LinkedHashMap
Collections工具类
各种集合处理的工具类
同步集合
Collections.synchronizedList(list);
Arrays工具类
java.util.Arrays#asList() 可以把数组类型转换为 List 类型。
asList的缺陷
等等
泛型
?通配符
泛型编程
泛型类,泛型接口
泛型方法
Arrays.asList
泛型变量
<? extends Parent >指定了泛型类型的上限
<? super Child> 指定了泛型类型的下限
<?> 指定了没有限制的泛型类型
泛型擦除
Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛
型中的类型信息的。
型中的类型信息的。
参考博客
https://www.jianshu.com/p/986f732ed2f1
https://cloud.tencent.com/developer/article/1033693
源码分析
ArrayList
扩容
删除元素
添加元素
Fail-Fast
序列化
ArrayList是线程不安全的解决方案
CopyOnWriteArrayList
适用场景
CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。
但是 CopyOnWriteArrayList 有其缺陷:
内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。
CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。
但是 CopyOnWriteArrayList 有其缺陷:
内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。
HashMap
基于1.7
存储结构
结构
put 操作
put
putForNullKey
其余操作
确定桶下标
步骤总结
get 方法
计算数组容量
拉链法的工作原理
注意到链表的插入是以头插法方式进行的
计算hash值
确定桶下标(取模)
确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的 n 次方, 那么就可以将这个操作转换为位运算。
X % 2^n = X & (2^n - 1)
扩容-基本原理
参数 含义
capacity table 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方。
size 键值对数量。
threshold size 的临界值,当 size 大于等于 threshold 就必须进行扩容操作。
loadFactor 装载因子,table 能够使用的比例,threshold = capacity * loadFactor。
capacity table 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方。
size 键值对数量。
threshold size 的临界值,当 size 大于等于 threshold 就必须进行扩容操作。
loadFactor 装载因子,table 能够使用的比例,threshold = capacity * loadFactor。
加载因子0.75,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷
加载因子是表示Hsah表中元素的填满的程度。
加载因子越大,填满的元素越多,空间利用率越高,但冲突的机会加大了。
反之,加载因子越小,填满的元素越少,冲突的机会减小,但空间浪费多了
加载因子越大,填满的元素越多,空间利用率越高,但冲突的机会加大了。
反之,加载因子越小,填满的元素越少,冲突的机会减小,但空间浪费多了
扩容-重新计算桶下标
扩容为2倍
多线程环境下可能扩容形成环,形成死锁(而环形链表产生后,会在后续遍历时出现死循环,导致CPU过高的情况。)
https://www.cnblogs.com/wen-he/p/11496050.html
\
基于1.8
hash算法
https://www.cnblogs.com/eycuii/p/12015283.html
链表转红黑树
红黑树
子主题
一个桶存储的链表长度大于等于 8 时会将链表转换为红黑树。但是当容量<64,优先扩容
计算数组容量
https://www.cnblogs.com/loading4/p/6239441.html
扩容避免环
高低位指针,当扩容是数组的两倍,而且旧容量都是2的指数次方,在上面1.7扩容-重新计算桶下标,知道数据分为两部分,要不在之前的位置不动,要不当前下标加上旧容量大小为新下标。1.8将扩容的链表数据分成两部分,分别加到数组中。不会出现指针互相指向的环
put 过程
get 方法
扩展
HashMap如果我想要让自己的Object作为K应该怎么办
ConcurrentHashMap
存储结构
jdk 1.7结构
size 操作
每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。
JDK 1.8 的改动
put 方法
get 方法
基于1.7
get 方法
put 方法
LinkedList详解
新增方法
查询方法
HashSet
add
LinkedHashMap
Java SE
数据类型
8种基本类型
数值型
整型
byte
short
int
Integer包装类型缓存池
new Integer(123) 与 Integer.valueOf(123) 的区别在于:
new Integer(123) 每次都会新建一个对象;
Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
new Integer(123) 每次都会新建一个对象;
Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
在 Java 8 中,Integer 缓存池的大小默认为 -128~127。
long
浮点型
float
double
字符型(char)
布尔型(boolean)
引用类型
类
对同一类型对象的抽象表达
接口
对行为的抽象表达,可以看做一种协议
数组
String
String 被声明为 final,因此它不可被继承。
在 Java 8 中,String 内部使用 char 数组存储数据。
在 Java 8 中,String 内部使用 char 数组存储数据。
String Pool
new String("abc")
使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对象)。
"abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面
量;
而使用 new 的方式会在堆中创建一个字符串对象。
"abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面
量;
而使用 new 的方式会在堆中创建一个字符串对象。
String s1 = "1";
String s2 = "1";
System.out.println(s1.intern()== s2.intern());// true
String s2 = "1";
System.out.println(s1.intern()== s2.intern());// true
枚举类型(enum)
类型转换
包装类型
基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。
BigDecimal
总结
工具类
BigDecimal格式化
BigDecimal大小比较
运算
参数传递
Java 的参数是以值传递的形式传入方法中,而不是引用传递。
方法参数传递是基本类型复制一个份值传入。也就是在方法中修改了变量也没有影响方法外的变量。
float 与 double
隐式类型转换
== 和equals
break、continue、return区别
继承
访问权限
抽象类与接口
重写与重载
1. 重写(Override)
存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
2. 重载(Overload)
存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。
应该注意的是,返回值不同,其它都相同不算是重载。
存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。
应该注意的是,返回值不同,其它都相同不算是重载。
Object 通用方法
equals
检查是否为同一个对象的引用,如果是直接返回 true;
检查是否是同一个类型,如果不是,直接返回 false;
将 Object 对象进行转型;
判断每个关键域是否相等。
检查是否是同一个类型,如果不是,直接返回 false;
将 Object 对象进行转型;
判断每个关键域是否相等。
hashCode()
理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所
有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是
一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。
一个数与 31 相乘可以转换成移位和减法: 31*x == (x<<5)-x ,编译器会自动进行这个优化。
有域的值都考虑进来。可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是
一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。
一个数与 31 相乘可以转换成移位和减法: 31*x == (x<<5)-x ,编译器会自动进行这个优化。
toString()
默认返回 ToStringExample@4554617c 这种形式
clone()
1. cloneable
clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类
实例的 clone() 方法。
clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类
实例的 clone() 方法。
2. 浅拷贝
拷贝对象和原始对象的引用类型引用同一个对象。
拷贝对象和原始对象的引用类型引用同一个对象。
3. 深拷贝
拷贝对象和原始对象的引用类型引用不同对象。
拷贝对象和原始对象的引用类型引用不同对象。
4. clone() 的替代方案
使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,
最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,
最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
关键字
final
1. 数据
声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。
对于基本类型,final 使数值不变;
对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。
对于基本类型,final 使数值不变;
对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
2. 方法
声明方法不能被子类重写。
private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方
法不是重写基类方法,而是在子类中定义了一个新的方法。
声明方法不能被子类重写。
private 方法隐式地被指定为 final,如果在子类中定义的方法和基类中的一个 private 方法签名相同,此时子类的方
法不是重写基类方法,而是在子类中定义了一个新的方法。
3. 类
声明类不允许被继承
声明类不允许被继承
static
1. 静态变量
静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来
访问它。静态变量在内存中只存在一份。
实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来
访问它。静态变量在内存中只存在一份。
实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死。
2. 静态方法
静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。
静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。
3. 静态语句块
静态语句块在类初始化时运行一次。
静态语句块在类初始化时运行一次。
4. 静态内部类
非静态内部类依赖于外部类的实例,而静态内部类不需要。
非静态内部类依赖于外部类的实例,而静态内部类不需要。
5. 静态导包
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
在使用静态变量和方法时不用再指明 ClassName,从而简化代码,但可读性大大降低。
6. 初始化顺序
静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺
序
静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺
序
内部类
根据定义的方式不同,内部类分为静态内部类,成员内部类,局部内部类,匿名内部类四种。
局部内部类(定义在方法中的类)
匿名内部类(要继承一个父类或者实现一个接口、直接使用 new 来生成一个对象的引用)
论SimpleDateFormat线程安全问题及解决方案
compareTo和compare方法之比较
JDK8下载
Java8函数式编程
一言以蔽之,函数式接口就是只定义一个抽象方法的接口。
`Lambda`——匿名函数
Lambda 表达式应用在函数式接口上。
方法引用
函数式接口(PSCF基本)
Predicate<T>
T->boolean
Supplier<T>
()->T
Consumer<T>
T->void
Function<T,R>
T ->R
UnaryOperator<T>
T->T
BinaryOperator<T>
(T,T)->T
BiFunction<T,U,R>
(T,U)->R
BiSupplier<T,U>
(T,U)->void
Predicate<L,R>
(L,R)->boolean
函数式数据处理 -->流
**请注意,和迭代器类似,流只能遍历一次。**
流操作
流和集合
流操作
子主题
多字段分组,去重,list转map,流的扁平化,group mapping
数值流
构建流
用流收集数据
归约和汇总
分组
分区
Collectors类的静态方法使用
jvm
类加载机制
类的生命周期
包括以下 7 个阶段:
加载(Loading)
验证(Verification)
准备(Preparation)
解析(Resolution)
初始化(Initialization)
使用(Using)
卸载(Unloading)
加载(Loading)
验证(Verification)
准备(Preparation)
解析(Resolution)
初始化(Initialization)
使用(Using)
卸载(Unloading)
类加载过程
1. 加载
加载是类加载的一个阶段,注意不要混淆。
2. 验证
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
3. 准备
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存
1.为类变量分配内存并且设置该类变量的默认初始值,即零值;
2.这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
3.类不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到java堆中。
4. 解析
将常量池的符号引用替换为直接引用的过程。
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
将常量池的符号引用替换为直接引用的过程。
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
5. 初始化
初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器() 方法的过程。在
准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化
类变量和其它资源。
初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段是虚拟机执行类构造器
准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化
类变量和其它资源。
初始化模块,初始化阶段就是执行类构造器方法clinit()的过程
类初始化时机
1. 主动引用
2. 被动引用
类与类加载器
两个类相等,需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名
称空间。
这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为
true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
称空间。
这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为
true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
类加载器的引用
类加载器分类
双亲委派模型
图示
1. 工作过程
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。
2. 好处
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一
3. 实现
部分源码实现
自定义类加载器实现
双亲委派模型的"破坏"
沙箱安全机制
例子
扩展
什么情况下需要开始类加载过程的第一个阶段加载
i++操作的字节码指令
类加载机制-双亲委派,破坏双亲委派
https://blog.csdn.net/w372426096/article/details/81901482
图示
运行时数据区域
图示
(图源阿里)JDK8的元数据区+JIT编译产物 就是JDK8以前的方法区
堆、栈、方法区的交互关系
JavaAPI中的Runtime
运行时数据区线程和进程
一般来说,jvm优化95%是优化堆区,5%优化的是方法区,至于栈区无非出入栈操作优化较少
线程角度看运行时数据区
JVM方面的理解线程
JVM系统线程分类
程序计数器
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
程序计数器是一块内存区域,用来记录线程当前要执行的指令地址。那么为何要将程序计数器设计为线程私有的呢?前面说了线程是占用CPU执行的基本单位,而CPU一般是使用时间片轮转方式让线程轮询占用的,所以当前线程CPU时间片用完后,要让出CPU,等下次轮到自己的时候再执行。那么如何知道之前程序执行到哪里了呢?其实程序计数器就是为了记录该线程让出CPU时的执行地址的,待再次分配到时间片时线程就可以从自己私有的计数器指定地址继续执行。另外需要注意的是,如果执行的是native方法,那么pc计数器记录的是undefined地址,只有执行的是Java代码时pc计数器记录的才是下一条指令的地址。
PC寄存器面试常问
上面就整理了
程序计数寄存器作用
图示
Java 虚拟机栈
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至
执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至
执行完成的过程,对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
栈帧
局部变量表
变量槽slot的理解
静态方法中this使用的例子
slot的重复利用
操作数栈(Operand Stack)
栈顶缓存技术ToS(Top-of-Stack Cashing)
动态链接(Dynamic Linking)
图示
指向常量池的箭头(橙色部分)
方法的调用
虚方法
非虚方法
虚拟机中提供了以下几条方法调用指令
关于invokedynamic指令
动态类型语言和静态类型语言
方法重写的本质
虚方法表
方法返回地址(Return Address)
方法返回的字节码指令
栈操作
方法的执行对应的栈帧压入,执行完弹出该栈帧,主方法执行完毕后最后完成。
JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。
栈是运行时的单位,而堆是存储的单位
Java虚拟机栈的存储结构和运行原理
如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
虚拟机栈的相关面试题
方法中定义的局部变量是否线程安全?具体情况具体分析
线程安全--栈封闭
静态变量与局部变量的对比及小结
在栈帧中,与性能调优关系最为密切的部分就是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
本地方法栈
本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待
这些方法需要特别处理。
本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待
这些方法需要特别处理。
堆
所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。
所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。
概述
配置及查看
堆的细分内存结构
JDK 7以前: 逻辑上分为新生区+养老区+永久区(即Xms/Xmx分配的内存物理上没有涉及永久区)
JDK 8以后: 逻辑上分为新生区+养老区+元空间(即Xms/Xmx分配的内存物理上没有涉及元空间)
图示
设置堆内存大小与OOM
年轻代与老年代
图解对象分配的一般过程
Minor GC、Major GC、Full GC
不同GC的触发机制
年轻代GC(Minor GC)触发机制
老年代GC(Major GC/Full GC)触发机制
Full GC触发机制
HandlePromotionFailure参数说明
注意
常用调优工具
堆空间分代思想
内存分配策略总结
新概念:TLAB(堆当中的线程私有缓存区域)
为什么有TLAB(Thread Local Allocation Buffer)
什么是TLAB
TLAB说明
TLAB对象分配过程
堆空间的参数设置
JVM--堆是分配对象的唯一选择么?
逃逸分析
参数设置
结论:开发中能使用局部变量的,就不要使用在方法外定义
代码优化理论
栈上分配
同步省略
分离对象或标量替换
例子
例子2
配置参数:-XX:+EliminateAllocations,开启标量替换。
逃逸分析小结
方法区(jdk1.7)
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
可利用参数 -XX:PermSize -XX:MaxPermSize 控制初始化方法区和最大方法区大小。
元数据区(JDK1.8)
方法区的理解
HotSpot虚拟机中方法区的演进
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不再虚拟机设置的内存中,而是使用本地内存(PC内存)。
永久代、元空间并不只是名字变了。内部结构也调整了。
根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常.。
永久代、元空间并不只是名字变了。内部结构也调整了。
根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常.。
图示
设置方法区大小的参数
jdk7及以前(永久代)
jdk8及以后(元空间)
解决报错OOM:(内存泄漏、内存溢出)
方法区的内部结构
类型信息
域信息(成员变量)
方法信息(method)
non-final的类变量(非声明为final的static静态变量)
全局常量(static final)
常量池
图示
区别
jdk7
官网解释
http://openjdk.java.net/jeps/122
StringTable 为什么要调整
变量存放位置
方法区的垃圾回收
常量池中废奔的常量
常量池中不再使用的类型
Class对象是存放在堆区的,不是方法区,这点很多人容易犯错。
运行时常量池
运行时常量池是方法区的一部分。
Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
运行时常量池是方法区的一部分。
Class 文件中的常量池(编译器生成的字面量和符号引用)会在类加载后被放入这个区域。
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
为什么需要常量池呢?
常量池的作用,就是为了提供一些符号和常量,便于指令的识别。
常量池的作用,就是为了提供一些符号和常量,便于指令的识别。
永久代为什么要被元空间替换
直接内存
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的
DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆
外内存来回拷贝数据。
在 JDK 1.4 中新引入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过 Java 堆里的
DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在堆内存和堆
外内存来回拷贝数据。
实例代码
直接内存来源于NIO
对比图示
直接内存异常OOM
直接内存参数设置
对象的实例化内存布局与访问定位
图示
创建对象的方式
创建对象的步骤
1.判断对象对应的类是否加载、链接、初始化
2.为对象分配内存
3.处理并发安全问题
4.初始化分配到的空间
5.设置对象的对象头
6.执行init方法进行初始化
对象的内存布局
图示
图示
对象头(Header)
实例数据(Instance Data)
对齐填充(Padding)
图示小结
对象的访问定位
图示
访问对象实例的主要方式有两种:句柄访问、直接指针(HotSpot采用)
句柄访问方式
图示
子主题
优缺点
直接指针方式
图示
直接指针(HotSpot采用)
总结
图示
面试题补充
执行引擎
Java架构图
执行引擎的工作过程
HotSpot VM 可以设置程序执行方式
宕机案例,热机状态切换
Java代码编译和执行过程
为什么说Java是半解释半编译型语言?
基于目前JVM执行引擎当中解释器与即时编译器共存
解释器(interpreter)
JIT (Just In Time Compiler):即时编译器
图示
热点代码及探测方式
概念
一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为OSR (On StackReplacement)编译。
目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。
采用基于计数器的热点探测,HotSpot VM将会为每一个 方法都建立2个不同类型的计数器,分别为方法调用计数器(Invocation Counter) 和回边计数器(BackEdge Counter) 。
方法调用计数器用于统计方法的调用次数。
回边计数器则用于统计循环体执行的循环次数。
方法调用计数器用于统计方法的调用次数。
回边计数器则用于统计循环体执行的循环次数。
方法调用计数器
图示
注意:热频代码片段经过即时编译后的产物--机器指令,需要缓存起来Code Cache,存放在方法区(元空间/本地内存)
热度衰减(现实当中的残酷概念,好理解,但不好受)
回边计数器
OSR
机器码、指令、汇编语言
最后补充几种编译器
HotSpot VM 中的JIT分类
C1和C2编译器不同的优化策略
Graal编译器
AOT编译器
JVM的参数
X参数
子主题
XX参数(重点)
Boolean类型
公式
-XX:+或者- 某个属性值
+表示开启
-表示关闭
-表示关闭
Case
是否打印GC收集细节
-XX:+PrintGCDetails
-XX:-PrintGCDetails
是否使用串行垃圾收集器
-XX:-UseSerialGC
-XX:+UseSerialGC
KV设值类型
公式
-XX:属性key=属性值value
case
-XX:MetaspaceSize=128m
-XX:MaxTenuringThreshold=15
jinfo举例,如何查看当前运行程序的配置
公式
jinfo -flag 配置项 进程编号
case
两个经典参数:-Xms和-Xmx
-Xms 等价于 -XX:InitialHeapSize
-Xmx 等价于-XX:MaxHeapSize
查看JVM默认值
-XX:+PrintFlagsInitial
查看初始默认值
公式
java -XX:+PrintFlagsInitial -version
java -XX:+PrintFlagsInitial
case
-XX:+PrintFlagsFinal
主要查看修改更新
公式
java -XX:+PirntFlagsFinal
java -XX:+PirntFlagsFinal -version
case
-XX:+PrintCommandLineFlags
JVM基本常用参数
基础
常用参数
-Xms 初始大小内存,默认为物理内存1/64
-Xmx 最大分配内存,默认为物理内存1/4
-Xss 设置单个线程的大小,一般默认为512K~1024K
等价于-XX:ThreadStackSize
-Xmn 设置年轻代的大小
-XX:MetaspaceSize
设置元空间大小
-Xms10m -Xmx10m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFinal
典型案例配置
-XX:+PrintGCDetails
输出详细GC收集日志信息
-XX:SurvivoRatio
子主题
-XX:NewRatio
-XX:MaxTenuringThreshold
设置垃圾最大年龄
常用参数阐述
解释
新生代和老年代配置比率
垃圾收集
什么是垃圾
为什么需要GC
什么是内存泄漏?
判断一个对象是否可被回收
1. 引用计数算法
为对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被
回收。
回收。
2. 可达性分析算法
以 GC Roots 为起始点进行搜索,可达的对象都是存活的,不可达的对象可被回收。
图示
可作为GC Roots 的对象
3. 方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,所以在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。
为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能
主要是对常量池的回收和对类的卸载。
为了避免内存溢出,在大量使用反射和动态代理的场景都需要虚拟机具备类卸载功能
4. finalize()
对象的finalization机制
对象是否"死亡"
例子
使用(MAT与JProfiler)工具分析GCRoots
引用类型
1. 强引用
被强引用关联的对象不会被回收。
使用 new 一个新对象的方式来创建强引用。
Object obj = new Object();
使用 new 一个新对象的方式来创建强引用。
Object obj = new Object();
2. 软引用
细节
3. 弱引用
不同和软引用
面试题:你开发中使用过WeakHashMap吗?
4. 虚引用
例子
GCRoots和四大引用的小总结
终结器引用
垃圾收集算法
引用计数
1. 标记 - 清除
2. 标记 - 整理
指针碰撞(Bump the Pointer )
3. 复制
4. 分代收集
细节
5. 增量收集算法
增量收集算法基本思想
分区算法G1回收器
子主题
垃圾收集器
相关概念
按线程数分(垃圾回收线程数)
按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器
按碎片处理方式分,可分为压缩式垃圾回收器和非压缩式垃圾回收器
按工作的内存区间分
评估GC的性能指标
吞吐量
暂停时间
高吞吐与低暂定对比
吞吐量和暂停时间
收集器配合
例子
新生代
1. Serial 收集器
Serial 翻译为串行,也就是说它以串行的方式执行。
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。
2. ParNew 收集器
它是 Serial 收集器的多线程版本。
它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集
器配合使用。
它是 Server 场景下默认的新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集
器配合使用。
ParNew收集器效率
3. Parallel Scavenge 收集器(吞吐量优先)
Parallel回收器:吞吐量优先
与 ParNew 一样是多线程收集器。
老年代
4. Serial Old 收集器
是 Serial 收集器的老年代版本,也是给 Client 场景下的虚拟机使用。如果用在 Server 场景下,它有两大用途:
在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
5. Parallel Old 收集器
是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
6. CMS 收集器(低延迟--低的暂停时间)
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
CMS执行过程
初始标记(Initial一Mark) 阶段
并发标记(Concurrent一Mark)阶段
重新标记(Remark) 阶段
并发清除( Concurrent一Sweep)阶段
优点
CMS弊端
有人会觉得既然重新标记阶段就包含垃圾修正了,那么为什么还会出现浮动垃圾呢?
CMS参数设置
JDK 后续版本中CMS的变化
垃圾回收器小结
G1 收集器(区域化分代式)
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。
HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年
代一起回收。
HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年
代一起回收。
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
H
Region 的概念
G1回收器垃圾回收过程
过程1年轻代GC
回收过程
过程2老年代GC+并发标记过程
过程3混合回收
过程4Full GC
记忆集与写屏障
G1垃圾回收器优势
常用配置参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=n : 设置G1区域的大小。值是2的幂,范围是1M到32M。目标是根据最小的Java堆大小划分出约2048个区域
-XX:MaxGCPauseMillis=n : 最大停顿时间,这是个软目标,JVM将尽可能(但不保证)停顿时间小于这个时间
-XX:InitiatingHeapOccupancyPercent=n 堆占用了多少的时候就触发GC,默认是45
-XX:ConcGCThreads=n 并发GC使用的线程数
-XX:G1ReservePercent=n 设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险,默认值是10%
G1回收器的常见操作步骤
和CMS比较
7种经典的垃圾回收器总结
怎么选择垃圾回收器
使用
查看自己默认垃圾收集器
java -XX:+PrintCommandLineFlags -version
垃圾收集器的类型
Server/Client模式分别是什么意思
如何选择垃圾回收器
Parallel垃圾回收器参数配置
对于JDK自带的JVM监控和性能分析工具用过哪些?一般你是怎么用的?
是什么
子主题
性能监控工具
G1和CMS的比较
GC中Stop the world(STW)
概念
停顿的原因
System.gc()无法保证GC一定执行
例子
内存溢出与内存泄漏
内存溢出
内存泄漏(Memory Leak)
垃圾回收的并发与并行
程序的并行和并发
图示
子主题
子主题
JVM垃圾回收安全点Safe Point
GC安全点(Safepoint)
如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
安全区域(Safe Region)
程序实际执行时:
JVM关于GC的日志分析
+PrintGC
PrintGCDetails
PrintGCTimeStamps
日志补充说明
MinorGC
fullGC
子主题
日志分析工具
谈谈你对ooM的认识
Java.lang.StackOverflowError
Java.lang.OutOfMemoryError:Java heap space
Java.lang.OutOfMemeoryError:GC overhead limit exceeded
子主题
Java.lang.OutOfMemeoryError:Direct buffer memory
子主题
Java.lang.OutOfMemeoryError:unable to create new native thread
服务器级别参数调优
Java.lang.OutOfMemeoryError:Metaspace
内存分配与回收策略
Minor GC 和 Full GC
Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比
较快。
Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC
慢很多。
Minor GC:回收新生代,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比
较快。
Full GC:回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC
慢很多。
内存分配策略
1. 对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
大多数情况下,对象在新生代 Eden 上分配,当 Eden 空间不够时,发起 Minor GC。
2. 大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
3. 长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,
增加到一定年龄则移动到老年代中。
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,
增加到一定年龄则移动到老年代中。
4. 动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄
所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到
MaxTenuringThreshold 中要求的年龄。
所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到
MaxTenuringThreshold 中要求的年龄。
5. 空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的
话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代
最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小
于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代
最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小
于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。
图示
Minor GC 的触发条件
Full GC 的触发条件
1. 调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
2. 老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数
调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对
象进入老年代的年龄,让对象在新生代多存活一段时间。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数
调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对
象进入老年代的年龄,让对象在新生代多存活一段时间。
3. 空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
4. JDK 1.7 及以前的永久代空间不足
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静
态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也
会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也
会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
5. Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时
性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
图示
分代回收
调优
减少 full gc,其实减少STW
案例1
图示
问题出在对象年龄动态判定,发生了fullgc
前提
解决在对象年龄动态 判定 对象不会直接进入老年代
JVM 发生 OOM 的 8 种原因、及解决办法
1. Java 堆空间
2. GC 开销超过限制
3. 请求的数组大小超过虚拟机限制
4. Perm gen 空间
5. Metaspace
6. 无法新建本机线程
7. 杀死进程或子进程
8. 发生 stack_trace_with_native_method
如何合理的规划一次jvm性能调优
外部命令导致系统缓慢
由Windows虚拟内存导致的长时间停顿
JVM性能监控
JVM字符串常量池StringTable
String的基本特性
String不可变的字符序列
String底层是不会扩容的HashTable
String的内存分配
显示的调用toString()方法在字符串常量池当中生成调用者对应的字符串,并且返回常量池当中的地址
测试代码
字符串拼接操作
常量拼接:编译期优化
例子
变量拼接:StringBuilder原理
final常量拼接
拼接操作与append的效率对比
子主题
new String("ab")会创建几个对象?
intern()的使用
总结String的intern()的使用
关于String.intern()的面试题
子主题
子主题
拓展
intern练习题1
intern练习题2
intern练习题3
intern()效率测试
StrtingTable的垃圾回收
G1中的String去重操作
去重实现步骤
命令行选项
IO
磁盘操作
File 类可以用于表示文件和目录的信息,但是它不表示文件的内容
字节操作
实现文件复制
装饰者模式
字符操作
编码与解码
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
如果编码和解码过程使用不同的编码方式那么就出现了乱码。
GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
UTF-16be 编码中,中文字符和英文字符都占 2 个字节。
UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
Java 的内存编码使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用
UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英
文都能使用一个 char 来存储。
如果编码和解码过程使用不同的编码方式那么就出现了乱码。
GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
UTF-16be 编码中,中文字符和英文字符都占 2 个字节。
UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
Java 的内存编码使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用
UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英
文都能使用一个 char 来存储。
String 的编码方式
Reader 与 Writer
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。但是在程序中操作的通常是字符形式的数据,因
此需要提供对字符进行操作的方法。
InputStreamReader 实现从字节流解码成字符流;
OutputStreamWriter 实现字符流编码成为字节流。
此需要提供对字符进行操作的方法。
InputStreamReader 实现从字节流解码成字符流;
OutputStreamWriter 实现字符流编码成为字节流。
实现逐行输出文本文件的内容
对象操作
序列化
序列化就是将一个对象转换成字节序列,方便存储和传输。
序列化:ObjectOutputStream.writeObject()
反序列化:ObjectInputStream.readObject()
不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。
序列化:ObjectOutputStream.writeObject()
反序列化:ObjectInputStream.readObject()
不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。
Serializable
序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行
序列化,会抛出异常。
序列化,会抛出异常。
序列化 ID
虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个
类的序列化 ID 是否一致(就是 private static final long serialVersionUID
类的序列化 ID 是否一致(就是 private static final long serialVersionUID
transient
transient 关键字可以使一些属性不会被序列化。
网络操作
InetAddress
URL
可以直接从 URL 中读取字节流数据。
Sockets
ServerSocket:服务器端类
Socket:客户端类
服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
Socket:客户端类
服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
Datagram
DatagramSocket:通信类
DatagramPacket:数据包类
DatagramPacket:数据包类
NIO
流与块
核心概念理解
1. 通道
通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通
道是双向的,可以用于读、写或者同时用于读写。
通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通
道是双向的,可以用于读、写或者同时用于读写。
通道包括以下类型:
FileChannel:从文件中读写数据;
DatagramChannel:通过 UDP 读写网络中数据;
SocketChannel:通过 TCP 读写网络中数据;
ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
FileChannel:从文件中读写数据;
DatagramChannel:通过 UDP 读写网络中数据;
SocketChannel:通过 TCP 读写网络中数据;
ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
2. 缓冲区
发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就
是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/
写进程。
是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/
写进程。
缓冲区包括以下类型:
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
多路复用
缓冲区状态变量
capacity:最大容量;
position:当前已经读写的字节数;
limit:还可以读写的字节数。
position:当前已经读写的字节数;
limit:还可以读写的字节数。
选择器
NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。
NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个
通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
1. 创建选择器
Selector selector = Selector.open();
Selector selector = Selector.open();
2. 将通道注册到选择器上
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
3. 监听事件
int num = selector.select();
使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。
int num = selector.select();
使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。
4. 获取到达的事件
5. 事件循环
因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代
码一般会放在一个死循环内。
因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代
码一般会放在一个死循环内。
文件 NIO 实例
套接字NIO实例
服务端
客户端
内存映射文件
内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。
向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修
改数据与将数据保存到磁盘是没有分开的。
下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的
子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修
改数据与将数据保存到磁盘是没有分开的。
下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的
子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
NIO 与普通 I/O 的区别主要有以下两点:
NIO 是非阻塞的;
NIO 面向块,I/O 面向流。
NIO 是非阻塞的;
NIO 面向块,I/O 面向流。
并发
线程相关概念
上下文切换
所以在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。线程上下文切换时机有:当前线程的CPU时间片使用完处于就绪状态时,当前线程被其他线程中断时。
在该线程被 CPU 剥夺时间片后又再次运行恢复上次所保存的信息的过程就称为上下文切换。
解决方案:
采用无锁编程,比如将数据按照 Hash(id) 进行取模分段,每个线程处理各自分段的数据,从而避免使用锁。
采用 CAS(compare and swap) 算法,如 Atomic 包就是采用 CAS 算法
合理的创建线程,避免创建了一些线程但其中大部分都是处于 waiting 状态,因为每当从 waiting 状态切换到 running 状态都是一次上下文切换。
进程和线程
进程是资源分配的基本单位。
进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对
PCB 的操作。
进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对
PCB 的操作。
进程的组成
线程是独立调度的基本单位。
一个进程中可以有多个线程,它们共享进程资源。
一个进程中可以有多个线程,它们共享进程资源。
区别
Ⅰ 拥有资源
进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。
Ⅱ 调度
线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程
中的线程时,会引起进程切换。
线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程
中的线程时,会引起进程切换。
Ⅲ 系统开销
由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销
线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而
线程切换时只需保存和设置少量寄存器内容,开销很小。
由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销
线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而
线程切换时只需保存和设置少量寄存器内容,开销很小。
Ⅳ 通信方面
线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。
线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。
进程间通信(IPC InterProcess Communication)
1.管道(使用最简单)
2.信号(开销最小)
3.共享映射区MMAP(无血缘关系的IPC)(共享内存)
4.本地套接字(最稳定)
5.消息队列
1.管道(使用最简单)
2.信号(开销最小)
3.共享映射区MMAP(无血缘关系的IPC)(共享内存)
4.本地套接字(最稳定)
5.消息队列
扩展
进程间五种通信方式的比较
并发和并行
并发是指宏观上在一段时间内能同时运行多个程序,而并行则指同一时刻能运行多个指令。
并行需要硬件支持,如多流水线、多核处理器或者分布式计算系统。
操作系统通过引入进程和线程,使得程序能够并发运行。
并行需要硬件支持,如多流水线、多核处理器或者分布式计算系统。
操作系统通过引入进程和线程,使得程序能够并发运行。
Erlang 之父 Joe Armstrong 用一张5岁小孩都能看懂的图解释了并发与并行的区别
程序的并行和并发
程序的并发(Concurrent)
程序的并行(Parallel)
二者对比
资源限制
线程状态转换
线程状态转换图示
Java Thread 类内部 state枚举 状态解析
可运行状态(就绪状态)
新建(New)
创建后尚未启动。
创建后尚未启动。
可运行(Runnable)
可能正在运行,也可能正在等待 CPU 时间片。
包含了操作系统线程状态中的 Running 和 Ready。
可能正在运行,也可能正在等待 CPU 时间片。
包含了操作系统线程状态中的 Running 和 Ready。
阻塞(Blocked)
等待获取一个排它锁,如果其线程释放了锁就会结束此状态。
等待获取一个排它锁,如果其线程释放了锁就会结束此状态。
无限期等待(Waiting)
等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
进入方法 退出方法
没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll()
没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕
LockSupport.park() 方法 LockSupport.unpark(Thread)
没有设置 Timeout 参数的 Object.wait() 方法 Object.notify() / Object.notifyAll()
没有设置 Timeout 参数的 Thread.join() 方法 被调用的线程执行完毕
LockSupport.park() 方法 LockSupport.unpark(Thread)
限期等待(Timed Waiting)
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
死亡(Terminated)
可以是线程结束任务之后自己结束,或者产生了异常而结束。
可以是线程结束任务之后自己结束,或者产生了异常而结束。
使用线程
使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递,而如果使用Runnable方式,则只能使用主线程里面被声明为final的变量。不好的地方是Java不支持多继承,如果继承了Thread类,那么子类不能再继承其他类,而Runable则没有这个限制。前两种方式都没办法拿到任务的返回结果,但是Futuretask方式可以
基础线程机制
Executor
Daemon
那么守护线程和用户线程有什么区别呢?区别之一是当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响JVM的退出。言外之意,只要有一个用户线程还没结束,正常情况下JVM就不会退出。
sleep()
如果在睡眠期间其他线程调用了该线程的interrupt()方法中断了该线程,则该线程会在调用sleep方法的地方抛出InterruptedException异常而返回。
yield()
调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。
join()
等待线程执行终止的join方法,当线程A调用Join方法会等待线程A执行完毕。
如何指定多个线程的执行顺序(join 方法)
中断
InterruptedException
interrupted()
该检测当前线程是否被中断,如果是返回true,否则返回false。与isInterrupted不同的是,该方法如果发现当前线程被中断,则会清除中断标志。但是不影响方法中的while(!Thread.currentThread().interrupted()) 的判断。即当线程被中断返回true,并中断标志清除为false 。
isInterrupted检测当前线程是否被中断,不清除中断标志
图示
子主题
Executor 的中断操作
互斥同步
对象头图示
volatile原理(主要补充知识点)
图示
实现原理
该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。
首先需要了解的是,Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。
所以,如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。
所以,如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。
https://blog.csdn.net/xdzhouxin/article/details/81236356
1.volatile是Java虚拟机提供的轻量级的同步机制
volatile 的应用
分析DCL单例模式
控制停止线程的标记
详细解释了volatile关键字的原理
https://juejin.im/post/6876681462712631303
synchronized
1. 同步一个代码块
2. 同步一个方法
3. 同步一个类
4. 同步一个静态方法
原理分析
流程图
ReentrantLock
ReentrantLock 实现原理
锁类型
公平锁获取锁
写入队列
挂起等待线程
非公平锁获取锁
释放锁
比较
1. 锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
2. 性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
3. 等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,而 synchronized 不行。
4. 公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
5. 锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
2. 性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
3. 等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,而 synchronized 不行。
4. 公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
5. 锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。
锁
锁优化
自旋锁
自旋锁其实就是在拿锁时发现已经有线程拿了锁,自己如果去拿会阻塞自己,这个时候会选择进行一次忙循环尝试。也就是不停循环看是否能等到上个线程自己释放锁。
解释
总结
锁消除
锁粗化
虚拟机通过适当扩大加锁的范围以避免频繁的拿锁释放锁的过程。
偏向锁
总结
轻量级锁
原理
当存在超过一个线程在竞争同一个同步代码块时,会发生偏向锁的撤销。当前线程会尝试使用CAS来获取锁,当自旋超过指定次数(可以自定义)时仍然无法获得锁,此时锁会膨胀升级为重量级锁。
重量级锁
乐观锁与悲观锁(数据库中引入的思想)
悲观锁
乐观锁
增加重试机制,
公平锁与非公平锁
独占锁与共享锁
什么是可重入锁
可重入锁最大的作用就是避免死锁
读写锁
不同进程
分布式锁
基于数据库
基于 Redis
基于 ZK
线程之间的协作(通信)
join()
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
wait() notify() notifyAll()
wait() 和 sleep() 的区别
wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
wait() 会释放锁,sleep() 不会。
wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
wait() 会释放锁,sleep() 不会。
为什么wait会释放锁,如果调用wait()方法的线程没有事先获取该对象的监视器锁,则调用wait()方法时调用线程会抛出IllegalMonitorStateException异常。
对象监视器锁在对象上面,wait是Object对象的方法,而sleep是Thread 的方法。
wait(long timeout)方法,线程挂起等待指定时间被notify 和notyfyAll 唤醒,超时释放锁。
await() signal() signalAll()
死锁
产生条件
互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
环路等待条件:指在发生死锁时,必然存在一个线程—资源的环形链,即线程集合{T0,T1, T2, …, Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,……Tn正在等待已被T0占用的资源。
如何避免
目前只有请求并持有和环路等待条件是可以被破坏的
破坏“请求和保持”条件
破坏“循环等待”条件
死锁定位
jps命令定位进程编号
jstack找到死锁查看
子主题
volatile 共享内存
CountDownLatch 并发工具
CyclicBarrier 并发工具
线程响应中断
线程池 awaitTermination() 方法
管道通信
Lock锁和Condition条件
Condition的特性
J.U.C - AQS
CountDownLatch
用来控制一个线程等待多个线程。
维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方
法而在等待的线程就会被唤醒。
维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方
法而在等待的线程就会被唤醒。
CyclicBarrier
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,
直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。
和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,
直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。
CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以
它才叫做循环屏障。
CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会
执行一次。
它才叫做循环屏障。
CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会
执行一次。
Semaphore
Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
信号量的主要用两个目的,一个是用于多个共享资源的相互排斥使用,另一个用于并发资源数的控制.
AQS
AbstractQueuedSynchronizer维护了一个volatile int类型的变量,用户表示当前同步状态。volatile虽然不能保证操作的原子性,但是保证了当前变量state的可见性。
getState()
setState()
compareAndSetState()
这三种叫做均是原子操作,其中compareAndSetState的实现依赖于Unsafe的compareAndSwapInt()方法。
setState()
compareAndSetState()
这三种叫做均是原子操作,其中compareAndSetState的实现依赖于Unsafe的compareAndSwapInt()方法。
AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
同步器的实现是 ABS 核心
以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程
lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失
败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放
锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,
获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。
lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失
败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放
锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,
获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。
以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与
线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state
会 CAS 减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程
就会从 await()函数返回,继续后余动作。
线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state
会 CAS 减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程
就会从 await()函数返回,继续后余动作。
核心三板斧,自旋,LockSupport,CAS
手写
J.U.C - 其它组件
FutureTask
阻塞队列
BlockingQueue
FIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定长度)
优先级队列 :PriorityBlockingQueue
使用 BlockingQueue 实现生产者消费者问题
执行
图示核心方法
分类
ArrayBlockingQueue: 由数组结构组成的有界阻塞队列.
LinkedBlockingDeque: 由链表结构组成的有界(但大小默认值Integer>MAX_VALUE)阻塞队列.
PriorityBlockingQueue:支持优先级排序的无界阻塞队列.
DelayQueue: 使用优先级队列实现的延迟无界阻塞队列.
SynchronousQueue:不存储元素的阻塞队列,也即是单个元素的队列.
LinkedTransferQueue:由链表结构组成的无界阻塞队列.
LinkedBlockingDeque:由链表结构组成的双向阻塞队列.
图示
ArrayBlockingQueue源码实现
写入队列
消费队列
实际案例
ForkJoin
demo
执行
线程池
线程池原理
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后
启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,
再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。
启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,
再从队列中取出任务来执行。他的主要特点为:线程复用;控制最大并发数;管理线程。
线程池的组成
一般的线程池主要分为以下 4 个组成部分:
1. 线程池管理器:用于创建并管理线程池
2. 工作线程:线程池中的线程
3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
4. 任务队列:用于存放待处理的任务,提供一种缓冲机制
1. 线程池管理器:用于创建并管理线程池
2. 工作线程:线程池中的线程
3. 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
4. 任务队列:用于存放待处理的任务,提供一种缓冲机制
ThreadPoolExecutor 的构造方法如下
1. corePoolSize:指定了线程池中的线程数量。
2. maximumPoolSize:指定了线程池中的最大线程数量。
3. keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多
少时间内会被销毁。
4. unit:keepAliveTime 的单位。
5. workQueue:任务队列,被提交但尚未被执行的任务。
6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。
7. handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。
2. maximumPoolSize:指定了线程池中的最大线程数量。
3. keepAliveTime:当前线程池数量超过 corePoolSize 时,多余的空闲线程的存活时间,即多
少时间内会被销毁。
4. unit:keepAliveTime 的单位。
5. workQueue:任务队列,被提交但尚未被执行的任务。
6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。
7. handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。
拒绝策略
JDK 内置的拒绝策略如下:
1. AbortPolicy : 直接抛出异常,阻止系统正常运行。(默认)
2. CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的
任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
3. DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再
次提交当前任务。
4. DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢
失,这是最好的一种方案。
1. AbortPolicy : 直接抛出异常,阻止系统正常运行。(默认)
2. CallerRunsPolicy : 只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的
任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
3. DiscardOldestPolicy : 丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再
次提交当前任务。
4. DiscardPolicy : 该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢
失,这是最好的一种方案。
以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际
需要,完全可以自己扩展 RejectedExecutionHandler 接口。
需要,完全可以自己扩展 RejectedExecutionHandler 接口。
Java 线程池工作过程
原理图示
任务提交图示
https://www.cnblogs.com/superfj/p/7544971.html
线程池中线程状态
线程池的关闭
ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:
shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
优雅的关闭线程池
优势
Executors框架
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
Executors.newWorkStealingPool(int)
生产上使用
我们生产上只能使用自定义的线程池
阿里巴巴Java开发手册
你在工作中是如何创建线程池的,是否自定义过线程池使用
合理配置线程池你是如何考虑的?
CPU密集型
IO密集型
由于IO密集型任务线程并不是一直在执行任务,应配置尽可能多的线程,如CPU核数*2
SpringBoot 使用线程池
监控线程池
线程池隔离
hystrix 隔离
运行
利用一个 Map 来存放不同业务对应的线程池。
ThreadLocalRandom
使用
和Random对比
原理分析
线程不安全示例
如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。
以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值有可能小于 1000。
以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值有可能小于 1000。
解决: 1.AtomicInteger 原子类操作。 2.为方法加上Synchronize 关键字
安全实现
将数据抽象成一个类 ,并将数据的操作作为这个类的方法
1. 将数据抽象成一个类,并将对这个数据的操作作为这个类的方法,这么设计可以和容易做到
同步,只要在方法上加”synchronized“
同步,只要在方法上加”synchronized“
Runnable 对象作为 一个类的内部类
2. 将 Runnable 对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数
据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各
个 Runnable 对象调用外部类的这些方法。
据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各
个 Runnable 对象调用外部类的这些方法。
Java 内存模型
图示
当一个线程操作共享变量时,它首先从主内存复制共享变量到自己的工作内存,然后对工作内存里的变量进行处理,处理完后将变量值更新到主内存。
CPU多核硬件架构
子主题
什么是伪共享
解释
主内存与工作内存
所有的变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线
程使用的变量的主内存副本拷贝。
线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。
程使用的变量的主内存副本拷贝。
线程只能直接操作工作内存中的变量,不同线程之间的变量值传递需要通过主内存来完成。
内存间交互操作
Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互操作。
read:把一个变量的值从主内存传输到工作内存中
load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
use:把工作内存中一个变量的值传递给执行引擎
assign:把一个从执行引擎接收到的值赋给工作内存的变量
store:把工作内存的一个变量的值传送到主内存中
write:在 store 之后执行,把 store 得到的值放入主内存的变量中
lock:作用于主内存的变量
unlock
load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
use:把工作内存中一个变量的值传递给执行引擎
assign:把一个从执行引擎接收到的值赋给工作内存的变量
store:把工作内存的一个变量的值传送到主内存中
write:在 store 之后执行,把 store 得到的值放入主内存的变量中
lock:作用于主内存的变量
unlock
图示
案例
内存模型三大特性
1. 原子性
AtomicInteger 能保证多个线程修改的原子性。
除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的原子性。它对应的内存间交互操作为:lock 和
unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。
unlock,在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。
注意
2. 可见性
3. 有序性
Java内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序。在单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题。
先行发生原则
1. 单一线程原则
Single Thread rule
在一个线程内,在程序前面的操作先行发生于后面的操作。
Single Thread rule
在一个线程内,在程序前面的操作先行发生于后面的操作。
2. 管程锁定规则
Monitor Lock Rule
一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
Monitor Lock Rule
一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
3. volatile 变量规则
Volatile Variable Rule
对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
Volatile Variable Rule
对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
4. 线程启动规则
Thread Start Rule
Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
Thread Start Rule
Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
5. 线程加入规则
Thread Join Rule
Thread 对象的结束先行发生于 join() 方法返回。
Thread Join Rule
Thread 对象的结束先行发生于 join() 方法返回。
6. 线程中断规则
Thread Interruption Rule
对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检
测到是否有中断发生。
Thread Interruption Rule
对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检
测到是否有中断发生。
7. 对象终结规则
Finalizer Rule
一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
Finalizer Rule
一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
8. 传递性
Transitivity
如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
Transitivity
如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
JMM的理解
线程安全
不可变
互斥同步
synchronized
内存含义
是加锁和释放锁的语义,当获取锁后会清空锁块内本地内存中将会被用到的共享变量,在使用这些共享变量时从主内存进行加载,在释放锁时将本地内存中修改的共享变量刷新到主内存。
缺点带来线程上下文切换。
ReentrantLock。
非阻塞同步
CAS
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件
支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是
内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
支持的原子性操作最典型的是:比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是
内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
问题
ABA
循环时间长开销很大
只能保证一个共享变量的原子性
JDK里面的Unsafe类提供了一系列的compareAndSwap*方法
boolean compareAndSwapLong(Object obj, long valueOffset, long expect, longupdate)方法:其中compareAndSwap的意思是比较并交换。CAS有四个操作数,分别为:对象内存位置、对象中的变量的偏移量、变量预期值和新的值。其操作含义是,如果对象obj中内存偏移量为valueOffset的变量值为expect,则使用新的值update替换旧的值expect。这是处理器提供的一个原子性指令。
Unsafe类中的重要方法(使用)
Unsafe类不能通过Unsafe.getUnsafe方法(具体可参考源码)
long objectFieldOffset(Field field)方法:返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该Unsafe函数中访问指定字段时使用
deme
AtomicInteger
J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。
无同步方案
1. 栈封闭
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有
的。
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有
的。
2. 线程本地存储(Thread Local Storage)
ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。
在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存
泄漏的情况,应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏
甚至是造成自身业务混乱的风险
在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存
泄漏的情况,应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏
甚至是造成自身业务混乱的风险
ThreadLocal 在每个线程内部都有一个名为threadLocals的成员变量,该变量的类型为HashMap,其中key为我们定义的ThreadLocal变量的this引用,value则为我们使用set方法设置的值。每个线程的本地变量存放在线程自己的内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢出,因此使用完毕后要记得调用ThreadLocal的remove方法删除对应线程的threadLocals中的本地变量。key是WeakReference ,key在下次垃圾回收会被清除但是本地变量不清除会一直堆积
InheritableThreadLocal
InheritableThreadLocal类通过重写getMap和createMap让本地变量保存到了具体线程的inheritableThreadLocals变量里面,那么线程在通过InheritableThreadLocal类实例的set或者get方法设置变量时,就会创建当前线程的inheritableThreadLocals变量。当父线程创建子线程时,构造函数会把父线程中inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals变量里面。
用途
比如子线程需要使用存放在threadlocal变量中的用户登录信息,再比如一些中间件需要把统一的id追踪的整个调用链路记录下来。
扩展
ThreadLocal为什么要使用弱引用和内存泄露问题
3. 可重入代码(Reentrant Code)
所谓共享资源,就是说该资源被多个线程所持有或者说多个线程都可以去访问该资源。
Java用static 修饰的变量的是方法区是线程共享资源。
如果多个线程都只是读取共享资源,而不去修改,那么就不会存在线程安全问题,只有当至少一个线程修改共享资源时才会存在线程安全问题
ThreadLocal应用和那些“坑”
多线程开发良好的实践
多线程变量问题
静态变量:线程非安全。静态变量即类变量,位于方法区,为所有对象共享,共享一份内存,一旦静态变量被修改,其他对象均对修改可见,故线程非安全。
实例变量:单例模式(只有一个对象实例存在)线程非安全,非单例线程安全。实例变量为对象实例私有,在虚拟机的堆中分配,若在系统中只存在一个此对象的实例,在多线程环境下,“犹如”静态变量那样,被某个线程修改后,其他线程对修改均可见,故线程非安全;如果每个线程执行都是在不同的对象中,那对象与对象之间的实例变量的修改将互不影响,故线程安全。
局部变量:线程安全。每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。
实例变量:单例模式(只有一个对象实例存在)线程非安全,非单例线程安全。实例变量为对象实例私有,在虚拟机的堆中分配,若在系统中只存在一个此对象的实例,在多线程环境下,“犹如”静态变量那样,被某个线程修改后,其他线程对修改均可见,故线程非安全;如果每个线程执行都是在不同的对象中,那对象与对象之间的实例变量的修改将互不影响,故线程安全。
局部变量:线程安全。每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。
给线程起个有意义的名字,这样可以方便找 Bug。
缩小同步范围,从而减少锁争用。例如对于 synchronized,应该尽量使用同步块而不是同步方法。
多用同步工具少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些
同步类简化了编码操作,而用 wait() 和 notify() 很难实现复杂控制流;其次,这些同步类是由最好的企业编写和
维护,在后续的 JDK 中还会不断优化和完善。
使用 BlockingQueue 实现生产者消费者问题。
多用并发集合少用同步集合,例如应该使用 ConcurrentHashMap 而不是 Hashtable。
使用本地变量和不可变类来保证线程安全。
使用线程池而不是直接创建线程,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任
务。
缩小同步范围,从而减少锁争用。例如对于 synchronized,应该尽量使用同步块而不是同步方法。
多用同步工具少用 wait() 和 notify()。首先,CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些
同步类简化了编码操作,而用 wait() 和 notify() 很难实现复杂控制流;其次,这些同步类是由最好的企业编写和
维护,在后续的 JDK 中还会不断优化和完善。
使用 BlockingQueue 实现生产者消费者问题。
多用并发集合少用同步集合,例如应该使用 ConcurrentHashMap 而不是 Hashtable。
使用本地变量和不可变类来保证线程安全。
使用线程池而不是直接创建线程,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任
务。
Disruptor框架
高级特性
注解
关键字@interface : @interface是Java中表示声明一个注解类的关键字。使用@interface 表示我们已经继承了java.lang.annotation.Annotation类,这是一个注解的基类接口,
注解再次被注解 : 注解的注解叫做元注解包括 @Retention: 定义注解的保留策略;@Target:定义注解的作用目标; @Document:说明该注解将被包含在javadoc中; @Inherited:说明子类可以继承父类中的该注解四种。
注解和反射的使用
https://www.cnblogs.com/tiko/p/12369003.html
注解使用
https://juejin.im/post/6876773156560994311#heading-1
反射
反射可以提供运行时的类信息
使用
类和接口
Class(标识一个类或者接口)Constructor(构造器),Methed,Filed,Modefier(修饰符)
Field :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;
Method :可以使用 invoke() 方法调用与 Method 对象关联的方法;
Constructor :可以用 Constructor 创建新的对象
Method :可以使用 invoke() 方法调用与 Method 对象关联的方法;
Constructor :可以用 Constructor 创建新的对象
在java的反射中,通过字段获取对象,是通过
public Object get(Object obj)
字段不是静态字段的话,要传入反射类的对象.如果传null是会报
java.lang.NullPointerException
但是如果字段是静态字段的话,传入任何对象都是可以的,包括null
public Object get(Object obj)
字段不是静态字段的话,要传入反射类的对象.如果传null是会报
java.lang.NullPointerException
但是如果字段是静态字段的话,传入任何对象都是可以的,包括null
https://www.sczyh30.com/posts/Java/java-reflection-1/
JavaBean内省机制
内省是什么?: 通过反射的方式访问javabean的技术
PropertyDescriptor类操作Bean的属性
https://www.cnblogs.com/yejiurui/archive/2012/10/06/2712693.html
作用
异常
Java 语 言 规 范 将 派 生 于 Error 类 或 RuntimeExceptio类的所有异常称为非受查( unchecked ) 异常, 所有其他的异常称为受查 ( checked )异常。
受查异常就是指,编译器在编译期间要求必须得到处理的那些异常,你必须在编译期处理了
当捕获到异常`se- Throwable` 时, 就可以使用下面这条语句重新得到原始异常 :`Throwable e = se.getCause()` ;强烈建议使用这种包装技术。 这样可以让用户抛出子系统中的高级异常 , 而不会丢失原始异常的细节。
https://www.jianshu.com/p/49d2c3975c56
https://www.tianmaying.com/tutorial/Java-Exception
JDK8
Lambda表达式
函数式接口
接口可以使用默认方法和静态方法
方法引用与构造器引用
Stream的API
日期时间
函数式编程思想
JDK7
1. 自动资源管理
2. 钻石表达式
3. 数字字面量下划线支持
4. switch探测String
5.异常捕获 允许多个异常 并列捕获 SQLException | IOException e
设计模式
创建型(5)
抽象工厂模式
提供一个创建一系列相关或相互依赖对象的接口,而无需指定它 们具体的类。
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
原型模式
用 原 型 实 例 指 定 创 建 对 象 的 种 类 , 并 且 通 过 拷 贝 这 个 原 型 来 创 建 新 的 对 象。
创建者模式
将 一 个 复 杂 对 象 的 构 建 与 它 的 表 示 分 离 , 使 得 同 样 的 构 建 过 程 可 以 创 建 不 同的表示。
工厂模式
定义一个用于创建对象的接口,让子类决定将哪一个类实例化。 Factory Method使一个类的实例化延迟到其子类。
结构型(7)
代理模式
为其他对象提供一个代理以控制对这个对象的访问。
适配器模式
将一个类的接口转换成客户希望的另外一个接口。 Adapter 模式使得原本 由于接口不兼容而不能一起工作的那些类可以一起工作。
桥梁模式
将抽象部分与它的实现部分分离,使它们都可以独立地变化。
装饰模式
动态地给一个对象添加一些额外的职责。就扩展功能而言, D e c o r a t o r 模 式比生成子类方式更为灵活。
门面模式
为子系统中的一组接口提供一个一致的界面, F a c a d e 模式定义了一个高层 接口,这个接口使得这一子系统更加容易使用。
组合模式
将 对 象 组 合 成 树 形 结 构 以 表 示 “ 部 分 -整 体 ” 的 层 次 结 构 。 Composite 使 得客户对单个对象和复合对象的使用具有一致性。
享元模式
运用共享技术有效地支持大量细粒度的对象。
行为型(11)
策略模式
定 义 一 系 列 的 算 法 , 把 它 们 一 个 个 封 装 起 来 , 并 且 使 它 们 可 相 互 替 换 。 本 模
式使得算法的变化可独立于使用它的客户。
式使得算法的变化可独立于使用它的客户。
观察者模式
定义对象间的一种一对多的依赖关系 , 以便当一个对象的状态发生改变时 , 所有依赖于它的对象都得到通知并自动刷新。
责任链模式
为 解 除 请 求 的 发 送 者 和 接 收 者 之 间 耦 合 , 而 使 多 个 对 象 都 有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象 处理它。
模板方法模式
定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。
Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
Template Method使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
状态模式
允 许 一 个 对 象 在 其 内 部 状 态 改 变 时 改 变 它 的 行 为 。 对 象 看 起 来 似 乎 修 改 了 它
所属的类。
所属的类。
备忘录模式
在 不 破 坏 封 装 性 的 前 提 下 , 捕 获 一 个 对 象 的 内 部 状 态 , 并 在 该 对 象 之 外 保存这个状态。这样以后就可将该对象恢复到保存的状态。
命令模式
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数 化;对请求排队或记录请求日志,以及支持可取消的操作
迭代器模式
提供一种方法顺序访问一个聚合对象中各个元素 , 而 又 不 需 暴 露 该 对 象 的 内部表示。
访问者模式
表 示 一 个 作 用 于 某 对 象 结 构 中 的 各 元 素 的 操 作 。 它 使 你 可 以 在 不 改 变 各 元
素的类的前提下定义作用于这些元素的新操作。
素的类的前提下定义作用于这些元素的新操作。
解释器模式(Interperter)
给定一个语言 , 定义它的文法的一种表示,并定义一个解释器 , 该解释 器使用该表示来解释语言中的句子。
中介者模式(Mediator)
用 一 个 中 介 对 象 来 封 装 一 系 列 的 对 象 交 互 。 中 介 者 使 各 对 象 不 需 要 显 式 地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
扩展
避免过多if - else的新姿势:策略模式、工厂 + 策略
0 条评论
下一页