Java
2023-07-03 14:39:35 56 举报
AI智能生成
登录查看完整内容
Java基础笔记
作者其他创作
大纲/内容
图解
主要任务是检查Java源程序是否符合Java语法,符合则生成.class文件,不符合无法生成.class文件
字节码文件(.class)文件不是纯粹的二进制,这种文件无法再操作系统当中直接执行
创建一个.Java文件,该文为源文件,源文件需要符合Java语法规则
需要使用JDK当中自带的javac.exe命令运行.Java文件进行编译
javac java源文件的路径
在DOS窗口中使用
一个java源文件可以编译生成多个.class文件,.class文件是最终要执行的文件
(跨平台性)编译结束后也可以将.class文件复制到其他操作系统中运行
javac.exe的使用规则
编译阶段的过程
编译阶段
JDK安装后,除了自带一个javac.exe文件外,还会有一个专门负责运行阶段的工具:java.exe
在DOS窗口中:java 类名
java.exe的使用
打开DOS
输入java类名
java.exe命令会启动JVM,JVM会启动类加载器Classloader
Classloader会在硬盘上搜索类名.class文件,找到该文件则该字节码文件装载到JVM
JVM将字节码文件解释称二进制数据
操作系统执行二进制和底层硬件平台进行交互
运行阶段的过程
运行阶段
JDK java开发包(自带JRE)
JRE java运行时环境(自带JVM)
JVM 虚拟机
关系图解
JDK、JRE、JVM
一个java源文件当中可以定义多个class
一个java源文件中public不是必须的
一个class会定义生成一个xxx.class字节码文件
一个java源文件当中定义公开类的话,只能由一个,类名必须与java源文件保持一致
public class 和class的区别
1.Java的加载与执行
在java源程序中凡是程序员有权利自己命名的单词都是标识符
标识符可以标识的元素:类名、方法名、变量名、接口名、常量名等
只能由“数字、字母、下划线、美元符号”组成
不能以数字开头
严格区分大小写
关键字不能作为标识符
理论上无长度限制,最好别太长
命名规则
类名、接口名:首字母大写,后面每个单词首字母大写
变量名、方法名:首字母小写,后面每个单词首字母大写
常量名:全部大写
最好见名知意
遵守驼峰命名方式
命名规范
2.标识符
Sun在开发java语言的时候,提前制定好的一些具有特定含义的字符序列
在语言当中具有特殊含义的单词,这些单词构成java程序的骨架
关键字在java语言当中全部小写
public、class、static、void 、if、for、while、do、default、byte、short、int、protected、switch、true、false、throw、throws、try、catch、long、float、double、boolean、char、private......
常见的关键字
3.关键字
字面值就是数据
整形、浮点型、布尔型、字符串型、字符型
4.字面值
变量本质上来说是内存中的一块空间,这块空间有\"数据类型\"、\"变量名\"、\"字面值\"
变量是内存中存储数据的最基本的单元
不同的数据又不同的类型
不同的数据类型底层会分配不同大小的空间
数据类型是指程序在运行阶段应该分配多大的内存空间
有了变量的概念之后,内存空间得到了重复的使用
第一种:读取变量中保存的具体数据 get/获取
第二种:修改变量中保存的数据数据 set/设置
注:java中的变量必须先声明再复制,才能访问(如例2)
代码是自上而下执行的
int i = 20; // setSystem.out.println(i); // get
例1
int i; // 不进行赋值无法开辟出内存空间System.out.println(i); // 编译报错,变量i未初始化
例2
通常访问一个变量包括两种访问形式
数据类型的作用
在同一个作用域当中,变量名不能重复,但可以重新赋值
作用域
变量的作用域就是变量的有效范围
变量的作用域:出了大括号就不认识了
局部变量:在方法体当中声明的变量叫做局部变量
成员变量:在方法体外(类体内)声明的变量叫做成员变量
在不同的作用域当中,变量名是可以相同的(会自动赋值)
类体中的声明无上下顺序之分
类体中不能直接编写java语句(除声明变量之外)
java遵循\"就近原则\"
变量的分类(根据变量声明的位置来分类)
5.变量
数据类型的作用:不同数据类型的数据占用空间大小不同,指导JVM在运行程序的时候给该数据分配多大的空间
byte、short、int、long、float、double、boolean、char占用的空间大小(字节) 1 2 4 8 4 8 1 2
整数型当中的byte类型,占用1个字节,所以byte类型的数据占用8个比特
二进制最左边为符号位,0表示正数,1表示负数
byte类型最大值:01111111 (10000000-1=01111111)
byte类型最大值:2的7次方-1,127
byte类型的范围:-128~127 表示256个不同的二进制位
关于byte类型的取值范围
基本数据类型
如String
引用数据类型
java中是数据类型包括两种
计算机在任何情况下都只能识别二进制
1字节=8个比特位,1个比特位表示一个二进制位(0或1)
1Byte = 8 bit
1KB = 1024 Byte
1MB = 1024KB
1GB=1024MB
1TB=1024GB
1TB=1024*1024*1024*1024*8bit
字节(byte)
char类型表示的是现实世界的文字,文字和计算机二进制之间默认情况下是不存在转换关系的
为了让计算机可以表示现实世界当中的文字,我们需要人为的干涉,需要人负责提前制定好\"文字\"和\"二进制\"之间的对照关系,这种对照关系称为:字符编码
'a' =97(01100001) 'A'=65'0'=48
'a'解码,按照ASCII解码,得到01100001,也就是97
01100001编码,按照ASCII编码,得到'a'
解码和编码的时候采用同一套字典/对照表,就不会出现乱码
最先出现的字符编码是:ASCII码
支持简体中文的编码方式:GB2312<GBK<GB18030差异在支持汉字的多少
支持繁体中文的编码方式:big5(大五码)
统一了全球所有文字的编码:unicode编码、UTF-8、UTF-16、UTF-32....
java语言源代码采用的是unicode编码,所以标识符可以用文中 class 学生{}
计算机只认二进制,那么计算机是怎么表示显示世界中的文字
byte、short、int默认值为0
long默认值为0L
float、double 默认值为0.0
boolean默认值为false(true是1,false是0)
char默认值为\\u0000
八种基本数据类型的默认值,一切向0看齐
成员变量没有手动赋值,系统会默认赋值(局部变量不会)
八种基本数据类型的默认值
null
引用数据类型的默认值
转义字符\\,反斜杠在ajva语言中具转义功能
\换行符 \\t制表符 \\' 普通单引号 \\\"普通双引号 \\\\普通反斜杠.....
注:制表符和空格不同,它们的ASCII不一样,体现在键盘上的不同两个按键
转义字符出现在特殊字符之前,会将特殊字符转换成普通字符
JDK中自带的native2ascii.exe命令。可以将文字转为unicode编码形式
在命令行中输入native2ascii回车
输入文字
得到unicode编码
例如:char c = '\\u4e2d' 输出为'中'
native2ascii.exe命令的使用
char类型
java语言中,整数型字面值被默认当最int型来处理。要让这个整数型字面值当作long类型来处理的话,需要在整数型字面值后面添加l或L
十进制(缺省默认的方式)
八进制(以0开头)
十六进制(0x开头)
int a = 10; 结果为10int b = 010; 结果为8int c = 0x10; 结果为16int d = a+b+c;结果为34
例
整数型字面值的三种表示方式
不会报错
456整数型字面值被当作int类型,占4个字节,x变量在声明时是long类型,占8个字节,int类型的字面值456赋值给long类型的变量x,存在类型转换,int是小容量,转换成long类型的大容量,小容量可以自动转换成大容量,称为自动类型转换
long x = 456;
会报错
2147483648被当作int类型4个字节处理,但是这个字面值超出了int类型的处理范围
long z = 2147483648;
在2447483648后面添加L会使该字面值一上来就被当long类型来处理,当然也不会存在类型转换了
long z = 2147483648L
int y = x;// 编译报错(大容量转小容量)
小容量可以直接转换成大容量
大容量转小容量需要进行强制类型转换
强制类型转换需要加\"强制类型转换符\"
虽然加上强制类型转换符之后可以通过,但是进入到运行阶段可能会损失精度
int y =(int)x; // 结果为100
原始数据: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 01100100强转之后: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 01100100
存储在计算机内部的数据都是从采用补码的形式
将以上的补码转换到原码就是最终的结果
在java语言中,当一个整数型字面值没有超过byte类型取值范围的话可以直接赋值给byte类型的变量
byte b = 50; // 可以编译byte c = 127; // 可以编译byte d = 128;// 编译报错,128超出了byte的范围,不能直接赋值使用强制类型转换符byte d2 = (byte)128; 结果为:-128
0是正、1是负
正数:与补码相同
负数:减一,全部取反
补码转原码
强制转换原理
long x=100L;
类型转换
整数型
java中,所有的浮点型字面值默认都是double类型来处理,想要当作float来处理,字面值后面加F/f
double和float在计算机内部二进制存储的时候存储的都是近似值,在现实世界当中有一些数字是无限循环的,例如3.33333....
计算机的资源是有限的,用有限的资源存储无限的数据,只能存储近似值
double d = 3.0; // 不存在类型转换,默认为doublefloat f = 5.1; // 编译错误,5.1是double,f是float类型的解决方案: 1.float f = (float)5.1; // 强制类型转换 2.float f = 5.1f; // 没有类型转换
浮点型
在java语言中,boolean类型只有两个值:true、false
在底层存储的时候boolean类型占用了1个字节,因为实际存储的时候false底层是0,true底层是1
布尔类型在实际开发中非常重要,经常使用在逻辑运算和条件控制语句中的
布尔型
1.八种基本类型除了布尔型外,剩下的其中都是可以相互转换的
2.小容量向大容量转称为自动类型转换 (容量从小到大) byte<short=char(能取更大的正整数)<int<long<float<double任何浮点数类型无论占用多少字节,都比整数型容量大
3.大容量转小容量需要强制转换符
4.整数字面值没有超出byte、short、char取值范围,可以直接赋值
5.byte、short、char混合运算时,先各自转换为int类型再运算
6.多种数据类型混合运算,先转换成容量最大的那种类型做运算
double = 10/3; 结果为3.0,先算出3,再转换为3.0double = 10.0/3;结果为3.333... 先把3变成了3.0 10.0/3.0 = 3.33333
编译期只检查语法,不进行运算long g = 10;byte b = (byte)g/3;编译错误byte b = (byte)(g/3); 可以
注: byte b =3;可以编译 int i = 10; byte b = i/3; // 编译报错,编译器不运算i/3 short s = 10; byte j = 5; short s1 = s+j; // 编译错误 short s2 = (short)(s+j);可以
例3
char c = 'a';byte b = (byte)c; 输出:97
例4
数据类型总结(基本数据类型的相互转换规则)
6.数据类型
单目运算符:++、--、+、-、*、/、%
int a = 100; int b = a++; 结果b=100
int a = 100; int b =++a;结果b=101
算数运算符
>大于 >=大于等于 <小于 <=小于等于 ==等于 !=不等于
关系运算符的运算结果一定是布尔类型
比较的时候比较的是值之间的大小比较
关系运算符
&逻辑与 |逻辑或 !逻辑非 ^逻辑异或 &&短路与 ||短路或
&(并且):两边算子都是true,结果才是true
|(或者):两边的算子有一个true,结果就是true
!(取反)算子为真,结果就为假,这是一个单目运算符
^(异或):两边的算子只要不一样,结果就是true
1.逻辑运算符要求两边算子都是布尔型,最终结果也是布尔型
2.短路与和逻辑与运算结果相同,只有存不存在短路之分
3.短路或和逻辑或运算结果相同,只有存不存在短路之分
int x = 10;int y = 8;System.out.println(x<y & ++x <y);System.out.println(x);,结果为11
int x = 10;int y = 8;System.out.println(x<y && ++x <y);System.out.println(x); // 结果为10
解释:x<y为false,整个表达式为false,后面的表达式没有执行,这种现象称为短路现象
关于逻辑与与短路与的区别
发生短路或的情况:第一个表达式执行的结果为true,会发生短路或
发生短路与的情况:第一个表达式执行的结果为false,会发生短路与
原理:都是通过判断第一个表达式来决定是否短路第二个表达式
逻辑运算符
基本赋值运算符:=
扩展的赋值运算符:+=、-=、*=、/=、%=
byte b = 10;b = b+5;编译错误b += 5;编译成功所以 b+=5相当于b=(byte)(b+5);
重要结论:扩展的赋值运算符不改变运算结果。也可以理解为自动加了一个与左边类型相同的强制转换符
赋值类运算符
7.运算符
控制选择结构语句:if、if else、switch
控制循环语句:for while do...while
改变控制语句顺序:break continue
switch(int或string类型的字面值或变量){ case int或string类型的字面值或变量: java语句 ... break; ...... default: java语句 ....}
如果分支语句的后面有break,整个switch语句终止
如果分支语句的后面没有break,可以不进行匹配,直接进入下一个分支语句,称为case穿透,提供break语句可以避免穿透
所偶有分支都没有匹配成功的话,会执行default中的语句
byte short char 也可以卸载switch和case的后面,因为这三个类型可以进行自动类型转换,可以转换为int
int i = 10;int i = 1;int i =2;switch(i){ case1:case2:case10: java语句...........}
case也可以合并
switch语句
for(初始化表达式;布尔表达式;更新表达式){ 需要反复执行的代码片段}
1.初始化表达式(只执行一次)
2.布尔表达式
3.循环体
4.更新表达式
总结:布尔表达式为true的话,会逆时针循环
continue:结束本次循环,开始下一次循环
执行顺序
for循环语句
8.控制语句
某个功能代码只需要写一遍
要使用这个功能,只需要给这个功能传递具体的数据,这个功能完成之返回一个最终结果
这样代码就实现了重复利用,提高了代码的复用性
方法就是一段代码片段,并且这段代码片段可以完成某个特定的功能并且可以被重复的使用
方法的本质
方法定义再类体中,在一个类中可以定义多个方法,方法编写的位置没有先后顺序,可以随意
1.方法只定义,不调用,是不会执行的,并且再JVM中也不会给他分配\"运行所属\"的内存空间,只有在调用这个方法的时候,才会动态的给方法分配内存
2.在JVM内存划分上有三块主要的内存空间:方法去内存 、堆内存、栈内存
方法再执行过程当中,再JVM中的内存是如何分配的呢?内存是怎么变化的?
.class文件存放在方法去内存中,方法代码片段属于.class文件的一部分,所以方法代码片段存放在方法区内存中,所以JVM中的三块主要的内存空间中方法区是最先有数据的
方法代码片段虽然在方法去内存中只有一份,但是可以被重复调用。每一次调用这个方法的时候,需要给该方法分配独立的活动场所,在栈内存中分配
方法在调用的瞬间,会给该方法分配内存空间,会在栈中发生压栈动作,方法执行结束之后,给该方法分配的内存空间全部释放,此时发生弹栈动作
局部变量在方法体中声明,局部变量在运行阶段,内存在栈中分配
栈内存中主要保存局部变量
方法代码片段存放在哪?方法执行的时候执行过程的内存在哪分配?
基本概念
功能相似,尽量让方法名相同
什么时候考虑使用方法重载?
1.在同一个类中
2.方法名相同
3.参数列表不同:数量、顺序、类型
什么条件满足之后构成了重载?
方法重载和方法名+参数列表有关
方法重载和返回值无关
方法重载和修饰符列表无关
方法重载和什么有关,和什么无关
方法重载:又称为overload
9.方法
主要关注的是实现的具体过程,因果关系
优点:对于业务逻辑比较简单的程序,可以达到快速开发,前期成本低
缺点:采用面向过程的开发很难解决非常复杂的业务逻辑,另外面向过程的方式导致软件元素之间的耦合度非常高,只要其中一环出问题,整个系统受到影响,导致最终的软件扩展里差,另外由于没有独立体的概念,所以无法达到组件的复用
面向过程
主要关注的是对象[独立体]能完成哪些功能
优点:耦合度低,扩展里强,更容易解决现实中更为复杂的业务逻辑,组件复用性强
缺点:前期投入成本较高,需要进行独立体的抽离,大量的分析和设计
面向对象
面向对象和面向过程的区别
面向对象的三大特征:封装、继承、多态
1.OOA(面向对象的分析)
2.OOD(面向对象的设计)
3.OOP(面向对象的变成)
从软件开发的生命周期来看可以分为三个阶段
10.面向对象
类在是现实世界当中是不存在的,是一个模板,是一个概念,是人类大脑思考抽象的结果
类代表了一类事物
在现实世界当中,对象A与对象B之间具有共同特征,进行抽象中介得到一个模板,这个模板就是类
什么是类?
对象是实际存在的个体,现实世界当中实际存在的
new运算符在堆内存中开辟的内存空间称为对象
什么是对象?
1.程序员先观察现实世界,从现实世界中寻找对象
2.寻找了N个对象后,发现所有的对象都有共同特征
3.程序员在大脑中形成了一个模板(类)
4.Java程序员可以通过java代码来描述一个类
5.java程序员中有了类的定义,然后通过类就可以创建对象
6.有了对象之后,可以让对象直接协作起来形成一个系统
描述一下整个软件开发的过程
类--(实例化)-->对象 ,对象又称为实例/instance
对象--(抽象)-->类
类描述的是对象的共同特征
共同特征例如:身高特征
这身高特征在访问的时候,必须先创建对象,通过对象去访问这个特征
因为这个特征具体到某个对象上之后,值不同,有的对象身高180,有的对象身高2.80
重点
一个类主要描述的是:状态+动作
状态信息:名字、身高、性别、年龄
动作信息:吃、唱歌、跳舞、学习
状态:是一个类的属性
动作:是一个类的方法
类{属性// 描述对象的状态信息方法// 描述对象的动作信息}
一个类主要描述什么信息呢?
java中所有的.class文件都是属于引用数据类型
对象又被称为实例,实例变量又被称为对象变量,不创建对象,对象中属性变量的内存空间是不存在的,只有创建了对象,属性变量的空间才会被创建
语法new 类名();
new是java语言中的运算符,new运算符的作用是创建对象,在JVM堆内存中开辟新的内存空间
方法区内存:在类加载的时候,class字节码文件被加载到该内存空间当中
栈内存:方法代码片段执行的时候会给该方法分配内存空间,在栈内存中压栈
堆内存:new的对象在堆内存中存储
对象的创建和使用
引用是一个变量,只不过这个变量中保存了另外一个java对象的内存地址
java语言中,程序员不能直接操作堆内存,java中没有指针
java中,程序员只能通过引用区访问堆内存中对象内部的实例变量
什么是引用?
Student stu = new Student();在堆内存中开辟一块内存空间,栈内存空间中的局部变量stu指向它
举例
局部变量在栈内存中存储
成员变量中的实例变量在堆内存的java对象内部存储
实例变量是一个对象一份,100个对象100份
除了new之外,字符串String不用new也可以开辟内存空间
引用是一个变量,变包括局部变量和成员变量
总结
图解例1
图解例2
图解例3
11.类和对象
1.JVM主要包括三块内存空间,分别是,栈内存、堆内存、方法区内存
2.堆内存和方法区内存各一个,栈内存一个线程一个
3.方法调用的时候,方法所需的内存空间在栈中分配,执行结束后,内存空间释放
4.栈中主要存储的是方法体当中的局部变量
5.方法的代码片段以及整个类的代码片段都被存储到方法区内存中,在类加载的时候这些代码会载入
6.在执行过程中使用new运算符创建的java对象,存储在堆内存中,对象内部又实例变量,所以实例变量存储在堆内存中
局部变量(方法体中声明)
实例变量(没有static)
静态变量(修饰符中又static)
成员变量(方法体外声明)
7.变量的分类
8.静态变量存储在方法区内存中
9.三块内存当中变化最频繁的是栈内存,最先有数据的是方法区内存,垃圾回收器主要针对的是堆内存
当堆内存当中的java对象称为垃圾数据的时候,会被垃圾回收器回收
没有更多的引用指向它的时候
这个对象无法被访问,因为访问对象只能通过引用的方式访问
什么时候堆内存中的java对象会变成垃圾呢?
10.垃圾回收器(自动垃圾回收机制、GC机制)什么时候会考虑将某个对象的内存回收呢?
12.关于java内存管理
安全,可重用,对外提供一个简单的操作入口,程序员只能通过这个入口进行操作
封装之后,对于那个事物来说,看不到这个事物比较复杂的一面,只能看到该事物简单的一面。例如:照相机,照相机的原理非常复杂,但是对于使用照相机的人来说,操作起来是非常方便的,它把复杂的部分封装起来了
封装之后才会形成真正的对象,真正的独立体
封装就意味着以后的程序可以重复使用,并且适应性强,在任何场所都能用
封装之后,对于事物本身,提高了安全性
封装的好处
1.所有属性私有化,使用private关键字进行修饰,private表示私有的,修饰的所有数据只能在本类中访问
2.对外提供简单的操作入口,也就是说以后外部程序想要访问某个属性,必须通过这些简单的入口进行访问 对外提供两个公开的方法,分别是set和get
3.set方法的命名规范: public void set+属性名字母大写(形参){ }
4.get方法的命名规范: public void get+属性名字母大写(形参){ }
封装的步骤
封装最重要的就是保证安全性,外部只会给你说对还是错,具体的判断逻辑和依据被保护起来了用户看不到,安全了
13.封装
创建对象用的
1.构造方法又被称为构造函数、构造器、Constructor
2.构造方法的语法结构修饰符列表 构造方法名(形参){ 构造方法体}注:构造方法名必须与类名保持一致
每一个构造方法实际上执行结束之后都有返回值,只是这个\"return 值\"这样的语句不需要,构造方法结束的时候java程序自动返回值,并且返回值类型就是构造方法所在类的类型,由于构造方法的返回值类型就是类本身,所以返回值类型不需要编写
3.构造方法调用执行之后,有返回值吗?
4.只要构造方法被调用就会创建对象,并且一定是在\"堆内存\"中开辟内存空间
1.创建对象
2.给实例变量赋值
实例变量的内存空间是在构造方法执行过程当中完成开辟的。系统在赋默认值的时候,也是在构造方法过程完成的
构造方法的作用
14.构造方法
this是一个引用,this是一个变量
this变量中保存了内存地址指向自身
this存储在JVM堆内存java对象内部
15.this关键字
静态变量:静态变量在类加载的时候初始化,不需要创建对象,内存就开辟了,存放在方法区内存中
实例变量:所有对象都有这个属性,但属性的值会随对象的变化而变化,不同对象的这个属性具体的值不同
静态变量:所有对象都有这个属性,并且所有对象的这个属性的值一样,建议定义为静态变量,节省内存的开销 访问的时候不需要创建对象,直接使用类名.属性名的方式访问
成员变量的声明
1.语法格式: static{ java语句 }
2.静态代码块在类加载时执行,并且只执行一次
3.静态代码块在一个类中可以编写多个,并且遵循自上而下的顺序执行
4.在主方法之前执行
和具体的需求有关,例如项目中要求类加载的时候执行代码完成日志的记录。那么这段记录日志的代码就可以编写到静态代码块中
静态代码块是java为程序员准备的一个特殊时刻,这个特殊的时刻被称为类加载时刻,若虚妄在此刻执行一段特殊的程序,这段程序可以直接放到静态代码块中
5.静态代码块的作用
6.通常在静态代码块中完成预备工作,先完成数据的准备工具,如:初始化连接池,解析XML配置文件等
可以使用static关键字来定义静态代码块
1.可以编写多个,自上而下
2.在构造方法执行之前执行,构造方法执行一次,实例代码块执行一次
3.实例代码块是一个特殊时机,称为:对象初始化时机
实例代码块(了解)
方法描述的是动作,当所有的对象执行这个动作的时候,最终产生影响是一样的,那么这个动作已经不再属于某一个对象动作了,可以将这个动作提升为类级别的动作,模板级别的动作
一般来说:工具类中的方法都是静态的
总结:动作不属于某一个对象了,工具类中的方法一般都是静态的
方法什么时候定义为静态的?
16.成员变量
1.继承的基本作用是代码复用,重要最用是:有了继承才有了方法覆盖和多态
私有的不支持继承
构造方法不支持继承
其他数据都可以被继承
2.子类可以继承父类中的哪些数据?
3.假设一个类没有现实的继承任何类,该类默认继承Object类
17.继承
方法覆盖又称为方法重写
当父类中的方法已经无法满足当前子类的业务需求,子类有必要将父类中继承过来的方法进行重新编写,这个重新编写的过程称为方法重写/方法覆盖
什么时候使用方法重写?
方法重写发生在具体的继承关系的父子类之间
返回值类型相同,方法名相同,参数列表相同
访问权限不能更低,可以更高或者不变(public > protected > default(没加修饰符) > private)
抛出异常不能更多,可以更少
什么条件满足之后方法会发生重写呢?
私有方法不能继承,所以不能覆盖
构造方法不能继承,所以不能覆盖
静态方法不存在覆盖
覆盖/重写只针对方法
注意
18.方法覆盖
向上转型(upcasting):子类型-->父类型,又称为:自动类型转换
向下转型(downcasting):父类型-->子类型,又称为:强制类型转换,需要加强制类型转换符
无论是向上转型或者向下转型都需要有继承关系,否则报错
Animal和Cat之间存在继承关系,Animal是父类,Cat是子类
Cat is a Animal(合理)
new Cat()创建的对象的类型是Cat,a这个应用的数据类型是Animal,可见它们进行了类型转换:子类型转为父类型,称为向上转型,或自动类型转换
java中允许这种语法:父类型引用指向子类型对象
Animal a = new Cat();
1.java程序永远都分为编译阶段和运行阶段
2.先分析编译阶段,再分析运行阶段,编译无法通过,根本是无法运行的
3.编译阶段编译器检查a这个引用的数据类型是Animal,由于Animal.class字节码文件中有move()方法,所以编译通过了,这个过程我们称为静态绑定,编译阶段绑定,只有静态绑定成功后才可以有后续的运行
4.在程序运行阶段,JVM堆内存当中真实创建的对象是Cat对象,那么以下程序再运行阶段一定会调用Cat对象的move()方法,此时发生了程序的动态绑定,运行阶段绑定
5.无论是Cat类有没有重写move()方法,运行阶段一定调用的是Cat对象的move()方法,因为底层真实对象就是Cat对象
6.父类型引用指向子类型对象,这种机制导致程序在编译阶段绑定和运行阶段绑定两种不同的状态/形态,这种机制称为多态
a.move()// 编译通过a.catchMouse()// 编译错误,Animal类中没有这个方法,(静态绑定失败)想要运行a.catchMouse()这个方法,得把Animal类向下转型为Cat类,因为Cat类中有catchMouse()这个方法
对于Animal a = new Cat();深度解析
当调用的方法是子类型中特有的,父类型中不存在的,必须把父类向下转型
Cat c = (Cat) a;c.catchMouse();//编译通过
什么时候需要向下转型呢?
举例:向下转型编译成功,运行失败
Animal a = new Brid();Cat c = (Cat) a; // 编译成功,运行报错
编译没问题,符合语法,向下转型
但是运行时JVM堆内存中出现的对象是Bird类,因为Bird类和Cat类没有继承关系,所以就出现了ClassCastException异常
注:向下转型存在隐患,编译过了,运行不一定能过,但是向上转型只要编译过了,一定可以运行
使用instanceof运算符
引用 instanceof 数据类型名
运算结果是boolean型
语法
Animal a = new Bird();a instance of Animal返回值: true:a这个引用指向的对象是一个Animal类型 false:a这个引用指向的对象不是一个Animal类型
避免向下转型出现ClassCastException异常
降低程序的耦合度,提升程序的扩展力
能使用多态尽量使用多态
父类型引用指向子类型对象
面向抽象变成(类),不要面向具体变成
定义好类,然后将类实例化为对象,给一个环境驱驶一下,让各个对象之间协作起来形成一个系统
好处:耦合度低,扩展力强
核心
无多态:Master和Cat、Dog这两个数据类型的关联程度强,耦合度高,扩展力差
有多态:Master面向一个抽象的Pat,不再面向具体的宠物,降低程序的耦合度[解耦合]
软件开发过程的一个很重要的目标,降低耦合度,提升扩展力
多态的作用
19.多态
final是一个关键字,表示最终的,不可变的
final修饰的类无法被继承
final修饰的方法无法被覆盖
final修饰的变量一旦赋值之后就无法再二次赋值
实例变量使用final之后,必须手动赋值,不能用默认值
20.final
不是java.lang包下,并且不在同一个包下,需要引入
什么时候需要import
包是为了方便程序的管理,查找方便,管理方便,易维护
公司域名倒叙+项目名+模块名+功能名
包的命名规范
21.package
跨包用的多
访问控制权限修饰符用来控制元素的访问范围
public 表示公开的,在任何位置都可以访问
protected 同包,子类
缺省 同包
private 表示私有,只能在本类中访问
访问控制权限修饰符包括
可以修饰类、变量、方法
当某个类只希望子类使用的使用,使用protected进行修饰
修饰符的范围:private<缺省<protected<public
22.访问控制权限修饰符
this能出现在实例方法中和构造方法中
this的语法是:this. this()
this不能使用在静态方法中
this. 除了在区分局部变量和实例变量的时候不能省略,其他的时候都可以省略
this() 只能出现在构造方法第一行,通过当前构造方法去调用本类中其他的构造方法(目的是为了实现代码复用)
this
super能出现在实例方法和构造方法中
super的语法是:super. super()
super不能使用在静态方法中
super不能省的情况:父类和子类有相同的属性,如果子类中想访问父的属性,super.不能省略
super()只能出现在构造方法第一行,通过当前的构造方法去调用父类中的构造方法,目的是:创建子类对象的时候,先初始化父类特征
super
super和this需要比较着学习
模拟现实世界中的场景:想要儿子,得现有父亲
super()表示通过子类的构造方法调用父类的构造方法
当一个构造方法第一行既没有this(),也没有super()的话,会默认有一个super()
表示通过当前子类的构造方法调用父类的无参构造方法,所以必须保证父类的无参构造方法是存在的
重要结论
this()和super()不能共存,它们都是只能出现在构造方法第一行
无论怎么折腾,父类中的构造方法一定是会执行的!!!!百分之百
不管new的是什么对象,最后老祖宗的Object类的无参构造方法一定会执行(Object的无参构造方法一定是最先执行的,Object类中没有super())
在构造方法执行过程中一连串调用了父类的构造方法,父类的构造方法又继续调用它的父类的构造方法,但实际上对象只创建了一个
super(实参)的作用:初始化当前对象的父类特征,并不是创建新对象,实际上对象只创建了一个
在恰当的时间使用:super(实际参数列表)
super关键字代表的就是当前对象的那部分父类特征
和super访问属性一样,如果父类和子类中有相同的方法,想在子中访问父,需要用super去调
super.属性名 访问父类的属性
super.方法名(实参) 访问父类的方法
super(实参) 调用父类的构造方法
在子类中访问父类私有的数据,你使用super.是没有权限访问的,父类与子类有同名属性、方法,想在子类中访问父类的属性、方法,super是不能省略的
23.super
抽象类是半抽象的
只支持单继承
类和类之间的共有特征,将这些具有共同特征的类再进一步进行抽象形成了抽象类
由于类本身是不存在的,所以抽象类无法创建对象(无法实例化)
抽象类和抽象类实际上可能还会有共同特征,还可以进一步再抽象
抽象类也属于引用类型
[修饰符列表] abstract class 类名{ 类体;}
抽象类的定义
抽象类无法实例化,无法创建对象,所以抽象类是被用来继承的
final和abstract不能同时使用
抽象类的子类还可以是抽象类,抽象类虽然无法实例化,但是抽象类可以有构造方法,这个构造方法是提供给子类使用的
!!!!!!!!!抽象类可以有构造方法,供给子类用的!!!!!!!!!!!!!!!!
抽象方法表示没有实现的方法,没有方法体的方法
例:public abstract void doSome();
没有方法体,以分号结尾
前面修饰符列表中有abstract关键字
抽象方法的特点:
抽象方法
抽象类中不一定有抽象方法和非抽象方法,但是抽象方法必须得出现在抽象类中
重要结论:一个非抽象的类继承抽象类,必须将抽象类中的抽象方法实现
24.抽象类
接口也是一种引用数据类型,编译之后也是一个class字节码文件
接口是完全抽象的,也可以说接口是特殊的抽象类
[修饰符列表] interface 接口名{}
接口的定义和语法
接口支持多继承,一个接口可以继承多个接口
接口中只包含两部分内容,一部分是抽象,一部分是抽象方法
接口中所有的元素都是public修饰的,表示公开的
接口中的抽象方法定义时:public abstract 修饰符可以省略
接口中的方法不能有方法体(native除外)
接口中的常量的public static final可以省略
类和类之间叫继承,类和接口之间叫做实现,可将实现看作继承继承的使用:extends实现的使用:implements
当一个非抽象的类实现接口的话,必须将接口中所有的抽象方法全部实现(覆盖重写)
类和类之间:单继承类和接口之间:多继承implements接口和接口之间:多继承extends接口与接口之间的多继弥补了类和类之间只能单继承的缺陷
如果继承和实现都存在的话:extends在前,implements在后
接口的基础语法
接口在开发中的作用类似于多态在开发中的作用
接口的使用离不开多态(接口+多态才能达到解耦合)
接口是纯抽象的,在接口中:要面向接口变编程
接口在开发中的作用
中午去吃饭菜单是一个接口(菜单上有一个抽象的图片:炒饭)谁面向接口调用(顾客面向菜单点菜,调用接口)谁负责实现这个接口(后台厨师负责把炒饭做好,接口的实现者)这个接口的作用:菜单让顾客和后厨解耦合了顾客不用找后厨,后厨不用找顾客,它们之间依靠抽象的菜单沟通
customer has a foodMenu
凡是满足了has a的都是以属性的形式存在
cat is a animal
凡是满足了is a的表示都可以设置为继承
接口例子
任何一个接口都有调用者和实现者
接口可以将调用者和实现者解耦合
调用者面向接口调用
实现者面向接口编写实现
接口可以解耦合,解的是谁和谁的耦合?
凡是满足is a的表示继承关系
is a
i has a pen
凡是满足has a关系的表示关联关系
关联关系通常以属性的形式存在
has a
cooker like a foodmenu
凡是满足like a关系的表示实现关系
like a
类型合类型之间的关系
抽象类是半抽象的,接口是完全抽象的
抽象类有构造方法,接口没有构造方法
接口和接口之间支持多继承,类和类之间只能单继承,一个类可以实现多个接口
一个抽象类只能继承一个类(单继承)
接口中只允许出现常量和抽象方法
接口一般都是对行为的抽象
抽象类和接口的区别(语法上)
25.接口
finalize()方法是sun工资为java程序员准备的一个时机,垃圾销毁时机希望在对象销毁时机执行一段代码的话,这段代码需要写道finalize()中
class Person{ protected void finalize() throws Throwable{ .............最终要执行的东西 }}
解释:Person对象被回收的时候,垃圾回收器负责自动调用p.finalize()
26.Object类的finalize()方法
在类的内部又定义了一个新的类,被称为内部类
静态内部类:类似于静态变量
实例内部类:类似于实例变量
局部内部类:类似于局部变量
匿名内部类:属于局部内部类的一种(类没有名字)
内部类的分类
使用内部类编写的代码可读性差,最好别用
public class Test{ static class Inner{} // 静态内部类 class Inner2{} // 实例内部类 public void doSome(){ class Inner3{} // 局部内部类 }}
27.内部类
获取系统当前时间:Date nowTime = new Date()
对日期进行格式化
yyyy年 MM月 dd日 HH时 mm分 ss秒 sss毫秒
SimpleDateFormat sdf = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss sss\")
Date time = new Date()
SimpleDateFormat sdf = new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss sss\")
String nowTime = sdf.format(time)
System.out.println(nowTime) // 2020-04-13 13:39:42 673
SimpleDateFormat
String time1 = \"2008-08-08 08:08:08 888\";
Date dateTime = sdf.parse(time1)
System.out.pringln(dateTime) // Fri Aug 08 08:08:08 CAST 2008
假设现在有一个日期字符串String,怎么转换成Date类型
String---(format)--->Date
Date---(parse)--->String
28.java中对日期的处理
# 代表任意数字
. 代表小数点
0 代表不够时补0
格式
DecimalFormat df = new DecimalFormat(\
DecimalFormat df = new DecimalFormat(\"##.00\");
String s1 = df.format(12.3);
输出12.30
DecimalFormat专门负责数字格式化的
属于大数据,精度极高,引用数据类型
bigDecimal在求和的时候得调用方法
bd.add(BigDecimal b) // 加法
bd.divide(BigDecimal b ) // 除法
BigDecimal
29.java中对数字的处理
Random random = new Random();// 创建随机数对象
int num 1 = random.nextInt(); // 随机产生一个int类型取值范围的数字
int num2 = random.nextint(101); // 0到100的随机数 不包括101 [0~101)
new Random()
Math.random()的随机数范围0.0~0.9
ceil()向上取舍
floor()向下取舍
Math.round(随机数)四舍五入
由于Math.random()小数位很多所以需要取舍
Math.Random()
30.Random
在实际开发中,有可能遇到一个方法的执行结果可能包括三种情况,四种情况等,但是每一个都是可以数清楚的,一枚一枚的列举出来的,这时boolean就无法满足需求了
枚举是引用数据类型
枚举中的每个值都可以看作是常量
31.枚举
Java基础
什么是异常:程序在执行工程中的不正常情况
异常的作用:增强程序的健壮性
异常信息是由JVM负责打印的
Object下有Throwable(可抛出的)
Error(不可处理,直接退出JVM)
Exception的直接子类:编译时异常(要求必须预处理)
RuntimeException:运行时异常(在编写程序阶段可处理,可不处理)
Exception(可处理的)有两个分支
Throwable下有两个分支
异常的结构
都是发生在运行阶段,编译阶段异常是不会发生的
编译时异常:程序在编译阶段不处理就会报错,所以称为编译时异常
编译时异常一般发生的概率比较高
运行时异常一般发生的概率比较低(所以可以不用预处理)
编译时异常和运行时异常的区别
编译时异常和运行时异常
所有异常都是发生在运行阶段
异常是java中类和对象的形式存在的
在方法声明的位置上使用throws关键字,抛给上一级,谁调我,我就抛给谁注:如果抛给main方法,如果出异常,一定会抛给JVM,JVM强行结束程序
使用try...catch语句进行异常的捕捉
处理方式
异常的处理机制
catch (FileNotFoundException | ArithmeticException | ...){}
JDK8新特性:catch中支持多个异常类(或的关系)
多个catch应遵循,自上而下,自小到大(不同级的情况下)
可以
try不能单独使用
try finally可以联合使用
try和finally,没有catch可以吗?
finally只有JVM退出后,才不会执行,否则百分百执行System.exit(0)// 退出JVM
public static int m(){ int i = 100; try{ return i; }finally{ i++; }}
结果为101
反编译的代码public static int m(){ int i = 100; int j = i; i++; return j;}
面试题
关于try .. catch
第一步:编写一个类继承Exception或RuntimeException
第二部:提供两个构造方法,一个是无参的,一个是有参的
自定义异常类
在指定位置手动抛出异常
throw new Exception();
throw
异常机制
集合实际上就是一个容器,可以用来容纳其他类型的数据,数组其实是一个集合
集合不能直接存储基本数据类型,另外集合也不能直接存储java对象,集合当中存储的都是java对象的内存地址
集合在Java中本身就是一个容器,是一个对象
集合中任何时候存储的都是引用list.add(100)// 这个100不是int类型,是自动装箱成Integer了
在java中每个不同的集合,底层都会对应不同的数据结构。往不同的集合中存储元素,等于将数据放到了不同的数据结构中,不同的数据结构存储方式不同
所有的集合类和集合接口都在java.util包下
单个方法存储元素,这一类集合的超级父接口是java.util.Collection
集合继承图
单个方式存储元素
以键值对方式存储元素,这一类集合中超级父接口是java.util.Map
键值对方式存储元素
集合分为两大类
ArrayList底层是数组
LinkedList底层是双向链表
Vector底层是数组,线程安全,效率低,用的少
HashSet底层是HashMap,放到HashSet中的元素,等同于放到了HashMap的key位置了
TreeSet底层是TreeMap,放到TreeSet中的元素相当于放到了TreeMap集合的key位置了
HashMap底层是哈希表
Hashtable底层是哈希表,线程安全,效率低,用的少
Properties线程安全的,并且key和value只能存String
TreeMap底层是二叉树,TreeMap集合的key可以自动按大小顺序排序
所有实现类总结
有序可重复
有序,存进去的顺序和取出来的顺序相同,每个元素都有下标
可重复,存进去1,还可以再存进去1
List集合存储元素的特点
无序不可重复
无序,存进去的顺序和取出来的顺序不一定相同,无元素下标
不可重复,存进去1,不能再存1了
Set(Map)集合存储元素的特点
首先是无序不可重复的,但是SortedSet(SortedMap)集合中的元素是可排序的
无序,存进去和取出来的顺序不一定相同,无下标
可排序,可以按照大小顺序排序
SortedSet(SortedMap)集合存储元素特点
boolean add(Object e)
int size()
void clear()
boolean contais(Object o)
boolean remove(Object o)
boolean isEmpty()
Object[] toArray()
Collection中的常用方法
迭代是Collection以及子类中通用的一种方式,Map集合除外
第一步:创建对象Collection c = new ArrayList();
第二步:获得集合对象的迭代器对象IteratorIterator it = c.iterator()
第三步:通过获取的迭代器对象开始迭代/遍历集合 hasNext() 如果仍有元素可以迭代返回true next()返回迭代的下一个元素 while(it.hasNext()){ System.out.println(it.next()); }
迭代
Collection c2 = new ArrayList();Iterator it = c2.iterator();while(it.hasNext()){ Object o = it.next(); c2.remove(0); // 异常,删除元素后,集合的结构会发生变化,应该重新获取迭代器才行}
但是如果用迭代器的remove方法就没事it.remove()
在获取迭代器对象时,迭代器会相当于先拍个快照,就集合复制一份
用Collection中的remove(1)相当于把目标集合中的1元素删除了,而快照中的元素1还在,所以就出问题了(没有更新迭代器)
用iterator中的remove方法删除元素时,会把目标集合和快照中的元素都删除,所以不会有问题(最根本的原因是iterator的remove会自动更新迭代器)
为什么使用it.remove()没有异常,而使用c2.remove(参)会有异常?
初始化容量是10(如果没有元素就是0,有了第一个就是10)
扩容, 增长到原容量的1.5倍(底层中是oldCapacity >> 1)
查询快,增删慢
ArrayList
基本的单元节点是Node
每个节点中有两个元素:存储到 数据,下一节点的内存地址
单链表
任何一个节点都有三个属性
上一个节点的内存地址
存储的数据
下一个节点的内存地址
双向链表
增删快,查询慢
LinkedList
底层是数组
初始化容量是10
扩容后变为原来的二倍
线程安全的,效率低,使用的少
Vector
将一个线程不安全的集合转换成线程安全的
集合工具类:java.util.Collections
java.util.Collection是集合接口
java.util.Collections是集合工具类
List myList = new ArrayList();// 非线程安全的Collections.synchronizedList(myList) // 变成线程安全的了
Collections.sort(list) // 自动排序,前提(排序的元素必须实现Comparable或Comparator)
用法
集合工具类
void get(Object key)
void clear
boolean containsKey(Object key)
boolean containsValue(Object value)
Collection values()
void remove(Object key)
Set<K> keySet() 获取Map集合所有的key
遍历使用
Map中的常用方法
底层是哈希表
数组+链表
初始化容量16
扩容因子0.75
扩容为原来的二倍
链上元素超过8链变为红黑树,小于6变回链表
HashMap
Hashtable的key和value都不能为null
HashMap的key和value可以为null
Hashtable是线程安全的
初始化容量是11
扩容后是原容量的2倍+1
Hashtable
是一个Map集合,继承了Hashtable
key和value都只能是String
Properties是线程安全的
Properties被称为属性对象
存:pro.setProperty(\"1\
取:pro.getProperty(\"1\") // 底层调用get方法
Properties
TreeSet集合底层实际是一个TreeMap
TreeMap集合底层是一个二叉树
放到TreeSet集合中的元素,等同于放到了TreeMap集合的ket部分了
TreeSet集合中的元素是无序不可重复的,但是可以按照元素大小自动排序:称为可排序集合
TreeSet/TreeMap是自平衡二叉树,遵循左小右大存放,存放是要依靠左小右大的原则,所以进行排序,存放的过程就是排序的过程
前序遍历:根左右
中序遍历:左根右
后序遍历:左右根
二叉树的遍历有三种
TreeSet/TreeMap采用了中序遍历
迭代器采用了中序遍历
放在TreeSet集合中的元素必须实现Comparable接口,并实现compareTo方法
重写的compareTo方法要写比较的逻辑,规则,按照什么比
实现Comparable
public class Test{ public static void main(String[] args){ Customer c1 = new Customer(32); Customer c2 = new Customer(20); Customer c3 = new Customer(30); // 创建TreeSet集合 TreeSet cc = new TreeSet(); cc.add(c1); cc.add(c2); cc.add(c3); for(Customer c : cc){ syso...(c); // 20 30 32 } }}class Customer implements Comparable<Customer>{ int age; public Customer(int age){ this.age = age; } // 实现compareTo public int compareTo(Customer c){ return c.age - this.age; } public String toString() { return \"Customer{\" + \"age=\" + age + '}'; }}
第一种方法(实现Compareable接口,重写compareTo())
class Bird{int age;public Bird(int age){this.age = age;}@Overridepublic String toString() {return \"Bird{\" +\"age=\" + age +'}';}}
鸟类
写一个比较器
public class Test03 { public static void main(String[] args) { TreeSet<Object> birds = new TreeSet(new BirdComparator()); birds.add(new Bird(100)); birds.add(new Bird(80)); for (Object bird : birds) { System.out.println(bird); } }}
第二种方式(创建TreeSet的时候使用匿名内部类的方式,不用那个比较器了)
第二种方法(传比较器)
自定义实现类型排序问题
TreeSet
集合
I:Input O:Output
通过IO可以完成硬盘文件的读和写
读和写都是以内存为参照物的
往内存中去,叫做输入(Input)或读(Read)
从内存中出来,叫做输出(Output)或写(Write)
按照流的方向进行分类(以内存为参照物)
按照字节的方式读取数据,一次读取一个字节byte,等同于8个二进制位(bit)
字节流是万能的,什么类型的文件都能读,包括:文本、图片、音频、视频等
file文件内:a中
a占一个字节,中占两个字节
第一次读:一个字节,正好读到'a'
第二次读:一个字节,读到'中'字符的一半
第三次读:一个字节,读到'中'字符的另一半
假设文件file.txt,采用字节流这样读取
字节流
按照字符的方式读取数据,一次读取一个字符
这种流是为了方便读取普通文本文件而存在的
普通文本:只要能用基本是编辑的都是普通文本文件
这种流不能读取图片、音频、视频等文件,只能读纯文本文件,连word都读不了
file.txt中的内容:a中
第一次读:一个字符,读到'a'字符
第二次读:一个字符,读到'中'字符
假设文件file.txt,采用字符流这样读
字符流
按照读取数据方式不同进行分类
输入流、输出流
字节流、字符流
总结流的分类
注:char在java中是2个字节 'a'英文字母,在windows操作系统中是1个字节 'a'英文字母,在java中是两个字节
IO流的分类
java中所有的流都是在:java.io.*;包下的
java.io.InputStream 字节输入流
java.io.OutputStream 字节输出流
java.io.Reader 字符输入流
java.io.Writer 字符输出流
四大家族首领(都是抽象的)
在java中\"类名\"结尾是Stream的都是字节流
在java中\"类名\"皆谓是Reader/Writer的都是字符流
所有的输出流都实现了:java.io.Flushable接口,都是可刷新的,都有flush()方法。输出流在最终输出之后,一定要记得flush()刷新一下。这个刷新表示将管道/通道中剩余未输出的数据强行输出完(清空管道),刷新的作用就是清空管道。如果没有flush()可能会丢数据
流的四大家族
java.io.FileInputStream
java.io.FileOutputStream
java.io.FileReader
java.io.FileWriter
文件专属
将字节流转换为字符流
java.io.InputStreamReader
java.io.OutputStreamWriter
转换流
java.io.BufferedInputStream
java.io.BufferedOutputStream
java.io.BufferedReader
java.io.BufferedWriter
缓冲流专属
java.io.DataInputStream
java.io.OutputStream
数据流专属
java.io.PrintWriter
java.io.PrientStream
标准输出流
java.io.ObjectInputStream
java.io.ObjectOutputStream
对象专属流
java.io包下需要掌握的16个流
文本字节输入流,万能的,任何类型的文件都能采用这个流读
字节的方式,完成输入的操作,完成读的操作(硬盘--->内存)
.read()方阿飞可以一次读一个字节(像迭代器一样),掉一次往下走一次,指向的地方没有数据返回-1。.read()返回值未int读到的字节
.read(byte[] b)一次最多读取b.length个字节,减少了硬盘和内存的交互,提高了执行效率,往byte[]数组中读,返回值为读到的字节数量
文件内容:abcdef
byte[] bytes = new byte[4]; // 准备一个长度为4的数组,一次最多读4个字节
int readCount = fis.read(bytes); // 第一次调用,读取到4,剩2
readCount = fis.read(bytes); // 第二次调用,读取到2,剩0
readCount = fis.read(bytes); // 第三次调用,读取到0,返回-1
最终版简写: FileInputStream fis = new FileInputStream(\"file.txt\
.read(byte[] b)例
byte[] bytes = new byte[fis.available()]
不用循环了,一次读够
int readCount = fis.read(bytes);
syso....(new String(bytes))
缺点,不适合用大文件,因为byte[]数组不能太大
int available():返回流当中剩余的没有读到的字节数量
fis.skip(3) // 跳过三个字节不读
例,文件中 是 abcdef
跳过三个
sout(fis.read()) // 100 就是d
long skip(long n):跳过几个字节不读
其他常用方法
FileInputStream
文本字节输出流,只负责写,内存--->硬盘
FileOutputStream fos = new FileOutputStream(\"myFile\"); // 不存在时自动创建文件
fos.write(byte[] b)
FileOutputStream fos = new FileOutputStream(\"myFile\"); // 不存在时自动创建
注:这种初始化fos方式会先清空原文件,然后再写入,如果不想清空,用另外一个构造,在后面加上trueFileOutputStream fos = new FileOutputStream(\"myFile\
将字符串转成byte数组
String s = \"哈哈\";
byte[] b = s.getBytes();
fos.write(b)
使用FileInputStream+FileOutputStream完成文件的拷贝
拷贝的过程是一边读,一边写,使用以上字节流拷贝文件时,文件类型随意、万能的,什么文件都能拷贝
边写边读
fis = new FileInputStream(\"file.txt\"); fos = new FileOutputStream(\"myFile.txt\
原理图
文件复制
FileOutputStream
文件字符输入流
只能读普通文本,读取文本内容的时候,比较方便,快捷
字符流中用的数组是char,字节流中用的数组是byte
FileReader fr = new FileReader(\"111\
FileReader
文件字符输出流
只能输出(写)普通文件
FileWriter fw = new FileWriter(\".....\"); // 也可以通过true来追加
FileWriter fw = new FileWriter(\".....\
fr = new FileReader(\"file.txt\"); fw = new FileWriter(\"fileWrite.txt\
边读边写
FileWriter
带有缓冲区的字符输入流
使用这个流的时候不需要自定义char数组或byte数组了,自带缓冲
.readLine()方法,读取一行,返回值是字符串,什么都没读到返回null注:读不到文件中的换行符
构造方法new BufferedReader(Reader reader)
当一个流的构造方法中需要传一个流的时候,这个被传进来的流叫做:节点流,外部负责包装的这个流,叫做包装六
BufferedReader br = new BufferedReader(new FileReader(\"...\"));
在关闭流的时候只需要关闭最外层的包装流,节点流会自动关闭
FileReader fr = new FileReader(\"...\");BufferedReader br = new BufferedReader(fr);String s = null;while((s=br.readLine()) !=null ){ sout....(s);}br.close();
字节流:FileInputStream fis = new FileInputStream(\"file.txt\"); BufferedReader br = new BufferedReader(fis)// 会报错 new BufferedReader(字符流);只能传字符流,不能传字节流
InputStreamReader reader = new InputStreamReader(fis);BufferedReader br = new BufferReader(reader); // 通过
合并上述代码 BufferedReader br = new BufferReader(new InputStreamReader(new FileInputStream(\"file.txt\")));
可以用转换流进行转换
如果想将字节流放入到BufferedReader中,需要转换流
BufferedReader
带有缓冲区的字符输出流
利用转换流也可以将字节流转换为字符流放入
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(\"myFile.txt\")));
bw.write()
BufferedWriter
创建的文件只能用DataInputStream去读,还得知道加密规则
数据专属的流,这个流可以将数据连同数据的类型一并写入文件
注:这个文件不是普通的文本文档,用记事本打不开
DataOutputStream dos = new DataOutputStream(new FileOutputStream(\"output.txt\"));byte b = 100;short s = 200;int i = 300;dos.writeByte(b);dos.writeShort(s);dos.writeInt(i);dos.flush();
DataOutputStream
数据字节输入流
DataOutputStream只能用DataInputStream去读,并且读的时候你需要提前知道写入的顺序
如果读的顺序和写的顺序一致,才能读取到数据
dis = new DataInputStream(new FileInputStream(\"out.txt\")); System.out.println(dis.readByte());System.out.println(dis.readShort());System.out.println(dis.readInt());
DataInputStream
标准的字节输出流。默认输出到控制台
标准是楚留不需要手动close关闭
写日志用的
联合起来写:System.out.println(\"hello world\");
分开写:PrintStream ps = System.out; ps.println(\"hello world\");
标准输出流不再指向控制台,指向log文件PrintStream printStream = new PrintStream(new FileOutputStream(\"log\"));
修改输出方向,将输出的方向修改到log文件System.setOut(printStream);
再输出就到log文件了,控制台不显示了System.out.println(\"哈\"); // log文件中有\"哈\"
PrintStream
序列化和反序列化
(拆分对象)序列化:Seriablize,java对象存储到文件中,将java对象的状态保存下来的过程
(组装对象)反序列化:DeSerialize,将硬盘上的数据重新恢复到内存中,恢复java对象
参与序列化和反序列化的对象,都必须实现Serializable接口,否则会出异常
Serializable接口只是一个标志接口,这个接口中什么代码都没有,这个接口起到标识的作用,JVM看到这个类实现了这个接口,会给这个类进行一些特殊待遇
特殊待遇:JVM在这个类中看到这个接口,会为该类自动生成一个序列化版本号
写法和前面学过的流一样
transient关键字,表示游离的,不参与序列化
在序列化的时候,JVM会给序列化类写一个序列化版本号,如果被序列化的类中源代码发生了变动,那么在反序列化原先序列化的文件时,就会出异常,因为在更改源码的时候,序列化版本号发生了改变
一:通过类名,雷鸣不同,类肯定不同
二:如果类名相同,通过序列化版本号区分
我写了一个类Student implements Serializable小明写了一个类Student implements Serializable不同的人编写了同一个类Student,但这两个类确实不是同一个类,这时使用序列化版本号来区分,这时JVM可以通过序列化版本号区分出来
JVM自动生成的序列化版本号的缺点:自动生成的序列化版本号:一旦代码确定之后,不能进行后续的修改,只要修改就会重写编译,此时会生成全新的序列化版本号,这个时候java虚拟机会认为这时一个全新的类,(这也意味着之前的数据拿不到了,要么不改,改了数据就拿不到了)
Java语言中采用了两种机制来区分类
手写序列化版本号:在实现了Serializable接口的类中第一行,自己写序列化版本号private static final long SERIALVERSIONUID = 1L;
第一步:File
第二步:settng--->Inspections--->搜Serializable--->找到末尾为UID的,打勾--->Apply、OK--->在实现Serializable的类名上ALT+Entery
IDEA生成序列化版本号
序列化版本号
ObjectOutputStream、ObjectInputStream
Properties是一个Map集合,key和value都是String类型的,想将userinfo中的数据加载到Properties对象当中
userinfo.txt中 username=admin password=123
FileInputStream fis = new FileInputStream(\"userinfo\");
Properties pro = new Properties()
调用Properties对象的load方法,将文件中的数据加载到map集合中
pro.load(fis)
pro.getProperty(\"username\") // admin
pro.getProperty(\"password\") // 123
IO+Properties
File类和四大家族没关系,所以File类不能完成文件的读和写
File对象代表什么? 文件和目录路径名的抽象表示形式 E:\\java学习 这是一个File对象 E:\\java学习\\java.txt 这也是一个File对象
一个File对象可能是目录,也可能是文件,不能通过File类完成文件读写
创建File对象:File f1 = new File(\"D:\\\\file\");注:这时file文件还没创建
判断文件是否存在:f1.exists()
以文件形式创建:f1.createNewFile()
以目录形式创建:f1.mkdir()
创建多重目录:.mkdirs()File f2 = new File(\"D:/a/b/c/d/e\");if(!f2.exists()){ f2.mkdirs();}
获取文件的父路径:.getParent()File f3 = new File(\"D:\\\\java学习\\\\java.ext\");sout....(f3.getParent()); // D:\\java学习
获取文件的父文件 .getParentFile()File parentFile = f3.getParentFile();
获取绝对路径 .getAbsolutePath()parentFile.getAbsolutePath(); // D:\\java学习
获取文件名 getName()
判断是否是目录 isDirectory()
判断是否是文件 isFile()
获取文件最后一次修改的时间 lastModified() // 返回毫秒 Date,需要转换格式
获取文件大小 length() 返回字节
获取当前目录下的所有子文件 File[] listFiles()
常用方法
File类
IO
1
2
子主题
3
4
5
6
注解
Java DataBase Connectivity(Java语言连接数据库)
第一步:注册驱动(告诉JVM,即将要连接的是哪个厂家的数据库)
第二步:获取连接(表示JVM的进程和数据库进程之间的管道打开了)
第三步:获取数据库操作对象(专门执行SQL语句的)
第四步:执行SQL语句(DQL DML...)
第五步:处理查询结果集(只有当第四步是select语句时,才有第五步处理查询结果集)
第六步:释放资源
String url = \"jdbc:mysql://192.168.0.1:3306/chenjiahao\"; String user = \"root\"; String password = \"333\
JDBC编程六步
1.Create Model
2.Model types
3.Physical Data Model(物理模型,就是建表)
4.在下方DBMS中选择对应的数据库
5.Model name:一般都是项目名
6.每一个小格子中都可以建很多张表
7.建表,右小角:physical Diagram中有个table
8.双击已经建好的表进行设计
9.name知识名称,Code才是真正用在数据库的名称
11.点击保存,.sql
PowerDesigner
数据库中的数据: 用户名 密码 zhangsan 123 jack 123
在模拟登录时,输入用户名:fdsa密码:fasa 'or '1' = '1这就登录成功了
不安全的例子
用户输入的信息中含有SQL语句的关键字,并且这些关键字参与SQL语句的编译过程,导致SQL语句的原意思被扭曲,从而达到SQL注入
\"非法\"的关键字参与了SQL的编译,原SQL语句的含义被扭曲了
导致SQL注入的根本原因
只要用户提供的信息不参与SQL语句的编译过程,问题就解决了
即使用户提供的信息中含有SQL语句的关键字,但是只要没有参与编译就不会起到作用
想要用户信息不参与SQL语句的编译,那么必须使用java.sql.PreparedStatement
PreparedStatement接口继承了java.sql.Statement
PreparedStatement是属于预编译的数据库操作对象
PreparedStatement的原理是:预先对SQL语句的框架进行编译,然后再给SQL语句传值
在编程六步的第三步中,创建预编译对象:ps = conn.prepareStatement(sql语句)
select * from t_user where loginName = ? and loginPwd = ?
?称为占位符,问好只能填充值
问号不能带引号,否则会认为是字符串
一个问号代表一个占位符
SQL语句中的值换成?
ps.setString(占位符下标,传值),没有参与编译,只赋值
PreparedStatement
业务方面要求必须支持SQL注入时
例如在进行升序和降序时,SQL语句末尾需要order by xxx desc 或 order by xxx asc,这时如果order by xxx ? ,再给占位符赋值,就变成了order by xxx 'desc',就报错了,这时只能使用SQL注入来解决
什么情况下要使用Statement
解决SQL注入的问题
SQL注入现象:(安全隐患)
默认自动提交
开启事务:conn.setAutoCommit(false)
手动提交:conn.commit()
回滚:conn.rollback();
JDBC事务机制
JDBC
https://www.cnblogs.com/chiangchou/archive/2017/09/05/idea-debug.html
debug
在本规约中,POJO 专指只有 setter/getter/toString 的简单类,包括 DO/DTO/BO/VO 等。
专指数据库表一一对应的 POJO 类。此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
DO(Data Object)
数据传输对象,Service 或 Manager 向外传输的对象。
to
DTO(Data Transfer Object)
业务对象,可以由 Service 层输出的封装业务逻辑的对象。
BO(Business Object)
数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用Map 类来传输。
qo
Query
显示层对象,通常是 Web 向模板渲染引擎层传输的对象。
VO(View Object)
阿里巴巴专指 Application Object,即在 Service 层上,极为贴近业务的复用代码。
AO(Application Object)
POJO(Plain Ordinary Java Object)
解决多线程并行情况下使用锁造成性能损耗的一种机制,这是硬件实现的原子操作。CAS 操作包含三个操作数:内存位置、预期原值和新值。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。
CAS(Compare And Swap)
Maven 坐标,是用来唯一标识 jar 包。
GAV(GroupId、ArtifactId、Version)
本文泛指类、对象的编程处理方式。
OOP(Object Oriented Programming)
利用先进先出队列实现的底层同步工具类,它是很多上层同步实现类的基础,比如:ReentrantLock、CountDownLatch、Semaphore 等,它们通过继承 AQS 实现其模版方法,然后将 AQS 子类作为同步组件的内部类,通常命名为 Sync。
AQS(AbstractQueuedSynchronizer)
ORM(Object Relation Mapping)
空指针异常
NPE(java.lang.NullPointerException)
源于 java.lang.OutOfMemoryError,当 JVM 没有足够的内存来为对象分配空间并且垃圾回收器也无法回收空间时,系统出现的严重状况。
OOM(Out Of Memory)
本工程内部子项目模块依赖的库(jar 包)
一方库
公司内部发布到中央仓库,可供公司内部其它应用依赖的库(jar 包)
二方库
公司之外的开源库(jar 包)。
三方库
专用名词
Java开发手册
设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
只需要一个实例
应用场景
类加载到内存后,就实例化一个单例,JVM保证线程安全
简答实用
优点
不管用不用,类加载时就完成实例化
缺点
package pers.chenjiahao.review;/** * review饿汉式 * 类加载到内存后,就实例化一个单例,JVM保证线程安全 * 简单实用,推荐使用 * 唯一缺点:不管用不用,类加载时就完成实例化 * @Author ChenJiahao * @Date 2021/7/31 16:01 */public class ReviewMgr01 { private static final ReviewMgr01 INSTANCE = new ReviewMgr01(); private ReviewMgr01() { } public static ReviewMgr01 getInstance(){ return INSTANCE; } public static void main(String[] args) { ReviewMgr01 instance = ReviewMgr01.getInstance(); ReviewMgr01 instance1 = ReviewMgr01.getInstance(); System.out.println(instance == instance1); }}
代码
饿汉式
package pers.chenjiahao.review;/** * review * 跟ReviewMgr01是一个意思 * @Author ChenJiahao * @Date 2021/7/31 16:05 */public class ReviewMgr02 { private static final ReviewMgr02 INSTANCE; static { INSTANCE = new ReviewMgr02(); } public static ReviewMgr02 getInstance(){ return INSTANCE; } public static void main(String[] args) { ReviewMgr02 instance = ReviewMgr02.getInstance(); ReviewMgr02 instance1 = ReviewMgr02.getInstance(); System.out.println(instance == instance1); }}
饿汉式的第二种实现方法
按需进行初始化
线程不安全,两个线程同时访问的话可能会造成创建了两个实例对象的问题
会出现问题的代码
懒汉式(lazy loading)
通过给方法上加synchronized解决
懒汉式(解决线程安全)
试着通过减小同步代码块的方式提高效率
反而会造成无法确保只有一个实例对象
比如说两个线程都进入null了,一个先拿到锁,另外一个等。所以就创建了两个实例化对象
package pers.chenjiahao.review;/** * review * 懒汉式 * 试着通过减小同步代码块的方式提高效率,!!!!!!!!!!!不可行!!!!!!! * 反而会造成无法确保只有一个实例对象 * 比如说两个线程都进入null了,一个先拿到锁,另外一个等。所以就创建了两个实例化对象 * @Author ChenJiahao * @Date 2021/7/31 16:05 */public class ReviewMgr05 { private static ReviewMgr05 INSTANCE; private ReviewMgr05(){ } // 减小同步代码块的范围 public static ReviewMgr05 getInstance(){ if (INSTANCE == null){ synchronized (ReviewMgr05.class){ try{ Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new ReviewMgr05(); } } return INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ System.out.println(ReviewMgr05.getInstance().hashCode()); }).start(); } }}
懒汉式(解决效率低的问题,但是线程又不安全了)
问题解决了,线程安全了,效率比前面的也高了
但是代码不是很优雅
package pers.chenjiahao.review;/** * review * 懒汉式 * 双重检查 * @Author ChenJiahao * @Date 2021/7/31 16:05 */public class ReviewMgr06 { // 不能加final 需要加volatile,要保证有序性,跟synchronized意思差不多 private static volatile ReviewMgr06 INSTANCE; private ReviewMgr06(){ } public static ReviewMgr06 getInstance(){ // 双重检查 if (INSTANCE == null){ synchronized (ReviewMgr06.class){ // 双重检查 if (INSTANCE == null){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new ReviewMgr06(); } } } return INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ System.out.println(ReviewMgr06.getInstance().hashCode()); }).start(); } }}
懒汉式(双重检查)
JVM保证单例 JVM只加载一次外部类 调用内部类后只加载一次内部类
加载外部类时不会加载内部类,这样可以实现懒加载
package pers.chenjiahao.review;/** * review * 静态内部类方式 * JVM保证单例 JVM只加载一次外部类 调用内部类后只加载一次内部类 * 加载外部类时不会加载内部类,这样可以实现懒加载 * 完美的写法之一 * @Author ChenJiahao * @Date 2021/7/31 16:06 */public class ReviewMgr07 { private ReviewMgr07(){ } private static class ReviewMgr07Holder{ private static final ReviewMgr07 INSTANCE = new ReviewMgr07(); } public static ReviewMgr07 getInstance(){ return ReviewMgr07Holder.INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(()->{ System.out.println(ReviewMgr07.getInstance().hashCode()); }).start(); } }}
静态内部类(最佳实现之一)
不仅可以解决线程同步,还可以防止反序列化
package pers.chenjiahao.review;/** * 枚举单例 * 不仅可以解决线程同步,还可以防止反序列化 * @Author ChenJiahao * @Date 2021/7/31 16:06 */public enum ReviewMgr08 { INSTANCE; public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(()->{ System.out.println(ReviewMgr08.INSTANCE.hashCode()); }).start(); } }}
枚举(最佳实现之一)
单例模式(Singleton)
策略模式是一种比较的思想,就像日常生活中遇到什么样的事情,就选择相应的**解决办法(策略)**去应对它一样。
策略模式只是一种比较的思想,不单单是比较器这么一种,也可以利用策略模式+工厂模式替代代码中的if-else,详情见:https://blog.csdn.net/qq_42874315/article/details/119877790
以比较器为例(Comparator/Comparable)
在类中定义的比较规则,一般用于两个相同的类进行比较
Comparable(两个之间比较,像是自带的比较规则)
通常会将需要比较的对象数组和对应对象类型的比较器传入到一个排序比较的方法中,一般用于多个相同的类进行比较排序使用,较为方便
Comparator(多个比较)
比较器的两种实现思路
使用Comparable
使用Comparator
实现方法
比较接口
实现比较接口的具体实现类
测试类
Comparable
实体类(被比较的对象)
比较器接口
实现比较器接口的具体比较器
传入要排序的对象数组和比较器
排序类
Comparator
需要
/** * @Author ChenJiahao(程序员五条) * @Date 2021/9/12 16:54 */public interface Comparable<T> { int compareTo(T o);}
定义Comparable<T>接口
Cat
/** * @Author ChenJiahao(程序员五条) * @Date 2021/9/12 16:54 */public class Dog implements Comparable<Dog>{ int food; public Dog(int food) { this.food = food; } /** * 定义一下两只狗怎么比较大小 * @param d 比较的Dog对象 * @return 利用数字判断大小 */ @Override public int compareTo(Dog d) { if (this.food < d.food){ return -1; }else if (this.food > d.food){ return 1; }else { return 0; } } @Override public String toString() { return \"Dog{\" + \"food=\" + food + '}'; }}
Dog
Comparable接口的实现类
一般来说会保留实现Comparable,这里为了避免大家混淆,我就不实现Comparable了
/** * @Author ChenJiahao(程序员五条) * @Date 2021/9/12 16:54 */public class Dog { int food; public Dog(int food) { this.food = food; } @Override public String toString() { return \"Dog{\" + \"food=\" + food + '}'; }}
比较的实体类
Comparator<T>接口
CatComparator(Cat类的比较器)
DogComparator(Dog类的比较器)
Comparator接口实现类
Sorter<T>(排序类,传入对象数组以及对应对象的比较器)
实例
如果只需要两个相同对象比较的话,只需要实现Comparable就够了
如果只需要多个相同对象比较的话,实现Compator就可以了
如果既需要两个相同对象比较,也需要多个相同对象比较的话,只实现Compator其实也够了,但是进行两两比较的时候不是很方便(看自己抉择了),也可以用实体类去实现Comparable,进行两两比较
策略模式(Strategy)
灵活控制生产过程
控制权限、修饰、日志等等
任何可以产生对象的方法或类都可以称之为工厂
单例也是一种工厂(静态工厂)
一个方法返回对象都可以成为工厂
为什么有了new之后,还要又工厂?
定义
产品维度好扩展
不好扩族
我们将创建一个 Moveable接口和实现 Moveable 接口的实体类。下一步是定义工厂类 CarFactory和CarFactory。Test 类使用 CarFactory 来获取 Car 对象,使用 PlaneFactory 来获取 Plane对象
图
类的接口
类(实现类)
简单工厂(创造这个类的)
额外的一个测试方法(正常情况下不需要)
以创造交通工具为例
接口中定义了一个go方法
package pers.chenjiahao.factorymethod;public interface Moveable { void go();}
接口Moveable(可移动)
实现Moveable接口,实现go方法
package pers.chenjiahao.factorymethod;public class Car implements Moveable{ public void go() { System.out.println(\"Car go wuwuwuwuwu....\"); }}
实现类Car
package pers.chenjiahao.factorymethod;public class Plane implements Moveable{ public void go(){ System.out.println(\"plane flying shuashuashua........\"); }}
实现类Plane
实现类
定义一个create方法,返回值为Moveable
package pers.chenjiahao.factorymethod;/** * 简单工厂 */public class CarFactory { // 严格意义来讲这里的返回值应该是Moveable public Moveable create(){ System.out.println(\"日志操作 ===== a car created!! \"); return new Car(); }}
Car工厂(简单工厂)
package pers.chenjiahao.factorymethod;/** * @Author ChenJiahao * @Date 2021/7/31 17:44 */public class PlaneFactory { // 严格意义来讲这里的返回值应该是Moveable public Moveable create(){ System.out.println(\"日志操作 ===== a car created!! \"); return new Plane(); }}
Plane工厂(简单工厂)
简单工厂
package pers.chenjiahao.factorymethod;public class Main { public static void main(String[] args) { Moveable m = new CarFactory().create(); m.go(); m = new PlaneFactory().create(); m.go(); }}
日志操作 ===== a car created!! Car go wuwuwuwuwu....日志操作 ===== a car created!! plane flying shuashuashua........
测试结果
每个工厂没有规范,里面的方法都可以随便写
如果两个工厂的方法名称不一样,同一套代码就不可行了
假设给每个工厂增加一个抽象父类去统一一下每个工厂的生产名
对于工厂方法的设想
package pers.chenjiahao.retrofitfactorymethod;/** * @Author ChenJiahao * @Date 2021/7/31 17:54 */public abstract class TrafficFactory { abstract Moveable create();}
增加一个抽象类TrafficFactory
package pers.chenjiahao.retrofitfactorymethod;/** * 简单工厂 */public class CarFactory extends TrafficFactory{ public Moveable create(){ System.out.println(\"日志操作 ===== a car created!! \"); return new Car(); }}
CarFactory继承TrafficFactory
package pers.chenjiahao.retrofitfactorymethod;/** * @Author ChenJiahao * @Date 2021/7/31 17:44 */public class PlaneFactory extends TrafficFactory { public Moveable create(){ System.out.println(\"日志操作 ===== a car created!! \"); return new Plane(); }}
PlaneFactory继承TrafficFactory
这样就使两个简单工厂中的方法名统一了
但是没有使测试类中的两行代码统一
继续改造
给TrafficFactory中增加一个Map
后期设想,每个简单工厂在初始化的时候自动加载到TrafficFctory的Map中
在测试类中Moveable m = TrafficFactory.trafficFactoryMap.get(\"PlaneFactory\").create();
改造结果:一半一半(给简单工厂初始化的时候失败了)
package pers.chenjiahao.retrofitfactorymethod;public class Main { public static void main(String[] args) { { TrafficFactory.trafficFactoryMap.put(\"PlaneFactory\
修改测试类改造成功,有瑕疵,暂时先这样
改造工厂方法(像抽象工厂了)
工厂方法
产品一族号扩展
不好扩展单个产品(IOC可以解决)
创建抽象工厂,由子工厂继承,通过子工厂去创建具体的抽象实现,这样可以实现一族的扩展,例如,下图中,现代工厂和魔法工厂生产出的武器和移动工具是不同的
抽象工厂(定义了抽象方法,返回值都是返回抽象类)
抽象类
继承了抽象类的实现类
工厂方法(继承抽象工厂)
以创造食物、武器、交通工具为例
有三个抽象方法,分别对应食物、武器、交通工具
package pers.chenjiahao.abstractfactory;public abstract class AbstractFactory { abstract Food createFood(); abstract Vehicle createVehicle(); abstract Weapon createWeapon();}
抽象工厂AbstractFactory
有一个自己的抽象方法,给实体类去继承的
package pers.chenjiahao.abstractfactory;public abstract class Food { abstract void printName();}
Food
package pers.chenjiahao.abstractfactory;public abstract class Weapon { abstract void shoot();}
Weapon
package pers.chenjiahao.abstractfactory;public abstract class Vehicle { abstract void go();}
Vehicle
package pers.chenjiahao.abstractfactory;public class Bread extends Food { public void printName(){ System.out.println(\"wdm\"); }}
Bread
package pers.chenjiahao.abstractfactory;public class MushRoom extends Food{ public void printName(){ System.out.println(\"dmg\"); }}
MushRoom
继承Food的实体类
package pers.chenjiahao.abstractfactory;public class AK47 extends Weapon{ public void shoot(){ System.out.println(\"凸凸凸凸凸凸凸....\"); }}
AK47
package pers.chenjiahao.abstractfactory;public class MagicStick extends Weapon { public void shoot(){ System.out.println(\"diandian.....\"); }}
MigicStick
继承Weapon的实体类
package pers.chenjiahao.abstractfactory;public class Broom extends Vehicle{ public void go(){ System.out.println(\"Broom go shuashuashua.........\"); }}
Broom
package pers.chenjiahao.abstractfactory;public class Car extends Vehicle{ public void go() { System.out.println(\"Car go wuwuwuwuwu....\"); }}
Car
继承Vehicle的实体类
实体类
继承AbstractFactory,重写三个抽象方法
package pers.chenjiahao.abstractfactory;public class ModernFactory extends AbstractFactory { @Override Food createFood() { return new Bread(); } @Override Vehicle createVehicle() { return new Car(); } @Override Weapon createWeapon() { return new AK47(); }}
创建现代工厂ModernFactory
package pers.chenjiahao.abstractfactory;public class MagicFactory extends AbstractFactory { @Override Food createFood() { return new MushRoom(); } @Override Vehicle createVehicle() { return new Broom(); } @Override Weapon createWeapon() { return new MagicStick(); }}
创建魔法工厂MigicFactory
package pers.chenjiahao.abstractfactory;public class Main { public static void main(String[] args) { AbstractFactory factory = new ModernFactory(); factory.createFood().printName(); factory.createVehicle().go(); factory.createWeapon().shoot(); factory = new MagicFactory(); factory.createFood().printName(); factory.createVehicle().go(); factory.createWeapon().shoot(); }}
需要创建新的一族只需要修改new后面的工厂,加类,加方法......
和工厂方法一样,如果能灵活的控制new的工厂,就可以实现代码复用了
抽象工厂
工厂方法是抽象方法的一种情况
形容词用接口,名词用抽象类
任意定制交通工具(继承Moveable)
任意定义生产过程(XXXFactory.create())
任意定制产品一族(抽象工厂)
工厂方法优点:产品维度好扩展,缺点:不好扩族
抽象工厂优点:产品一族号扩展,缺点不好扩展单个产品(IOC可以解决)
工厂模式(Factory)
面向政府的各个部门之间,来回跑,很累
没有门面之前
有了门面之后,只用去面向门面,轻松了不好啊
有门面之后
在没有门面的时候,我和别的事物之间是一对多的关系有了门面之后,我和别的事物之间就是一对一的关系了
去医院看病,可能要去挂号、门诊、划价、取药,让患者或患者家属觉得很复杂,如果有提供接待人员,只让接待人员来处理,就很方便
这里的接待员就充当了门面
核心:一对多转换为了一对一
创建一个 Shape 接口和实现了 Shape 接口的实体类。下一步是定义一个外观类 ShapeFacade。
ShapeFacade 类使用实体类来代表用户对这些类的调用。Test 类使用 ShapeFacade 类来显示结果。
将被代理的事物们抽象成一个接口
被代理的具体事物(实现接口)
门面中需要聚合被代理的具体事物
在无参构造中,需要给聚合的事物赋值(new相应事物的无参构造)
分别提供对应具体事物的方法
门面(提供给用户访问)
测试类(调用门面去实际操作上述的事物)
package pers.chenjiahao.facade;/** * @Author ChenJiahao * @Date 2021/8/16 22:02 */public interface Shape { void draw();}
Shape接口
package pers.chenjiahao.facade;/** * @Author ChenJiahao * @Date 2021/8/16 22:02 */public class Circle implements Shape{ @Override public void draw() { System.out.println(\"画个圆:Circle.draw()\"); }}
Circle
package pers.chenjiahao.facade;/** * @Author ChenJiahao * @Date 2021/8/16 22:03 */public class Rectangle implements Shape { @Override public void draw() { System.out.println(\"画个长方形:Rectangle.draw()\"); }}
Rectangle
package pers.chenjiahao.facade;/** * @Author ChenJiahao * @Date 2021/8/16 22:03 */public class Square implements Shape { @Override public void draw() { System.out.println(\"画个正方形:Square.draw()\"); }}
Square
Shape接口的实现类
package pers.chenjiahao.facade;/** * 管理Shape的门面 * @Author ChenJiahao * @Date 2021/8/16 22:04 */public class ShapeFacade { private Shape circle; private Shape rectangle; private Shape square; public ShapeFacade() { circle = new Circle(); rectangle = new Rectangle(); square = new Square(); } public void drawCircle(){ circle.draw(); } public void drawRectangle(){ rectangle.draw(); } public void drawSquare(){ square.draw(); }}
Shape的门面类
package pers.chenjiahao.facade;/** * 测试类 * @Author ChenJiahao * @Date 2021/8/16 22:04 */public class Test { public static void main(String[] args) { ShapeFacade shapeFacade = new ShapeFacade(); shapeFacade.drawCircle(); shapeFacade.drawRectangle(); shapeFacade.drawSquare(); }}
责任链+门面
由于上述的实例中,draw方法的返回值为空,所以可以将门面中的方法的返回值设置为当前门面,就可以让门面方法实现链式访问了
package pers.chenjiahao.facadeplus;/** * 管理Shape的门面 * @Author ChenJiahao * @Date 2021/8/16 22:04 */public class ShapeFacade { private Shape circle; private Shape rectangle; private Shape square; public ShapeFacade() { circle = new Circle(); rectangle = new Rectangle(); square = new Square(); } public ShapeFacade drawCircle(){ circle.draw(); return this; } public ShapeFacade drawRectangle(){ rectangle.draw(); return this; } public ShapeFacade drawSquare(){ square.draw(); return this; }}
修改后的门面为
package pers.chenjiahao.facadeplus;/** * 测试类 * @Author ChenJiahao * @Date 2021/8/16 22:04 */public class Test { public static void main(String[] args) { ShapeFacade shapeFacade = new ShapeFacade(); shapeFacade.drawCircle().drawRectangle().drawSquare(); }}
修改后的测试类
设想1(责任链+门面)
让门面也去实现接口
门面中不再写出每个具体事物对应的方法
门面中增加一个Map,用来存储相应对象
*********设想失败*******
方案不可行,最多只能改成门面+模板方法
门面+工厂的目的就是为了干掉门面中具体事物对应的方法,如果去掉了,在外面调用的时候还是得判断,没啥意义了
设想2(门面+工厂)方案不可行,最多只能改成门面+模板方法
门面/外观(Facade)
目的还是解耦
本质就是消息中间件
调停者:让每个部门互相不打交道了,每个部门将消息给中间件,需要就自己拿着
使一对多变成了一对一
多个部门之间的消息需要互相协调,以往的消息很混乱,每个部门之间都有消息的传播和联系
引入调停者模式之后,所有部门都将消息面向调停者,这样各个部门之间不需要再互相发送消息了
将消息全部发送给调停者,哪个部门需要啥消息也不需要再问别的部门要了,只需要将消息给调停者就好
例如:专教体育的学校A,专教艺术的学校B、专教电脑的学校C以往想要学体育就去A学校,想学艺术就去B学校,想学电脑就去C学校现在出来了一个综合性的学校D(调停者),学校A、B、C将自己的教学资源交给D,现在:想学体育、艺术、电脑的同学只需要去学校D学习即可
MVC 框架:其中C(控制器)就是 M(模型)和 V(视图)的中介者。
系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象。
想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。
通过聊天室实例来演示中介者模式。
实例中,多个用户可以向聊天室发送消息,聊天室向所有的用户显示消息。我们将创建两个类 ChatRoom 和 User。
User 对象使用 ChatRoom 方法来分享他们的消息。
MediatorPatternDemo,我们的演示类使用 User 对象来显示他们之间的通信。
在发送消息的方法中,嵌入中介类
发送消息的实体(上述例子中的部门)
中介类
User(发送消息的实体)
ChatRoom(中介类)
package pers.chenjiahao.mediator;/** * @Author ChenJiahao * @Date 2021/8/18 21:39 */public class Main { public static void main(String[] args) { User chen = new User(\"Jiahao Chen\"); User yang = new User(\"Qinhang Yang\"); chen.sendMessage(\"hi yang\"); yang.sendMessage(\"hi chen\"); }}
在原有的基础上增加管理员类
将管理员和用户抽象成People类
package pers.chenjiahao.multiple;/** * @Author ChenJiahao * @Date 2021/8/18 21:45 */public abstract class People { String name; public abstract void sendMessage(String message);}
People(抽象类)
User(消息实体)
Manager(消息实体)
ChatRoom(中介者)
package pers.chenjiahao.multiple;/** * @Author ChenJiahao * @Date 2021/8/18 21:39 */public class Main { public static void main(String[] args) { new User(\"Jiahao Chen\").sendMessage(\"hi yang\"); new User(\"Qinhang Yang\").sendMessage(\"hi chen\"); new Manager(\"Kun Fang\").sendMessage(\"hi chen、hi yang\"); /*People chen = new User(\"Jiahao Chen\"); People yang = new User(\"Qinhang Yang\"); People fang = new Manager(\"Kun Fang\"); chen.sendMessage(\"hi yang\"); yang.sendMessage(\"hi chen\"); fang.sendMessage(\"hi chen、hi yang\");*/ }}
多实体测试
调停者/中介者(Mediator)
允许向一个现有的对象添加新的功能,同时又不改变其结构
动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
*****总体思想*****:以往是直接new一个对象去调对应的方法A,现在将这个对象包起来,用包起来的这个对象去执行方法A其中,在包起来的对象中,对方法A进行了重写,在不改变原有内在逻辑的基础上,增加了新的业务
不用对象去调用方法,用装饰器去调用方法,在装饰器中增加了业务方法
可代替继承
一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀
解决问题
孙悟空有 72 变,当他变成\"庙宇\"后,他的根本还是一只猴子,但是他又有了庙宇的功能。
不论一幅画有没有画框都可以挂在墙上,但是通常都是有画框的,并且实际上是画框被挂在墙上。在挂在墙上之前,画可以被蒙上玻璃,装到框子里;这时画、玻璃和画框形成了一个物体。
我们将创建一个 Shape 接口和实现了 Shape 接口的实体类。
然后我们创建一个实现了 Shape 接口的抽象装饰类 ShapeDecorator,并把 Shape 对象作为它的实例变量。
RedShapeDecorator 是实现了 ShapeDecorator 的实体类。
DecoratorPatternDemo 类使用 RedShapeDecorator 来装饰 Shape 对象。
实体类抽象出来的接口
实现接口的实体类
内嵌一个接口的对象,再给一个有参构造对此对象进行赋值
在再实现的方法中使用上述对象去调用
实现接口的抽象类(装饰器类)
重写接口中的方法,在原有的方法基础上,增加新的方法,实现了装饰
继承装饰器类的实体类(扩展装饰器)
package pers.chenjiahao.decorator;/** * @Author ChenJiahao * @Date 2021/8/19 21:22 */public interface Shape { void draw();}
Shape(接口)
package pers.chenjiahao.decorator;/** * @Author ChenJiahao * @Date 2021/8/19 21:23 */public class Circle implements Shape { @Override public void draw() { System.out.println(\"shape circle\"); }}
package pers.chenjiahao.decorator;/** * @Author ChenJiahao * @Date 2021/8/19 21:23 */public class Rectangle implements Shape { @Override public void draw() { System.out.println(\"shape Rectangle\"); }}
实体类(实现接口)
package pers.chenjiahao.decorator;/** * @Author ChenJiahao * @Date 2021/8/19 21:23 */public abstract class ShapeDecorator implements Shape { protected Shape decoratedShape; public ShapeDecorator(Shape decoratedShape) { this.decoratedShape = decoratedShape; } @Override public void draw() { decoratedShape.draw(); }}
ShapeDecorator(实现接口的抽象类)
package pers.chenjiahao.decorator;/** * @Author ChenJiahao * @Date 2021/8/19 21:31 */public class RedShapeDecorator extends ShapeDecorator { public RedShapeDecorator(Shape decoratedShape) { super(decoratedShape); } @Override public void draw() { decoratedShape.draw(); setRedBorder(decoratedShape); } private void setRedBorder(Shape decoratedShape){ System.out.println(\"Border Color: Red\"); }}
RedShapeDecorator(继承装饰器的扩展装饰器类)
package pers.chenjiahao.decorator;/** * @Author ChenJiahao * @Date 2021/8/19 21:23 */public class Test { public static void main(String[] args) { Shape circle = new Circle(); // ShapeDecorator redCircle = new RedShapeDecorator(new Circle()); // ShapeDecorator redRectangle = new RedShapeDecorator(new Rectangle()); Shape redCircle = new RedShapeDecorator(new Circle()); Shape redRectangle = new RedShapeDecorator(new Rectangle()); System.out.println(\"Circle with normal border\"); circle.draw(); System.out.println(\"\Circle of red border\"); redCircle.draw(); System.out.println(\"\Rectangle of red border\"); redRectangle.draw(); }}
需要给装饰器类中增加无参构造
扩展装饰器中,重写方法的时候不需要再super了,单独写自己的方法即可
具体对象的基本方法在链中调用一次即可
链式装饰器相当于将包裹了具体对象的装饰器又包裹了一层
******注:一旦使用链式,就无法再单独使用装饰器了
链式之后的问题再单独使用一个装饰器的话,会丢失原先的方法,如果让每个装饰器在调用的时候都调用一下具体对象的基本方法的话,会造成具体对象的基本方法被重复调用所以只能让装饰器中只能有自己新增的装饰方法,这一点与上述实例中的装饰器有所不同
package pers.chenjiahao.chaindecorator;/** * @Author ChenJiahao * @Date 2021/8/19 21:22 */public interface Shape { void draw();}
package pers.chenjiahao.chaindecorator;/** * @Author ChenJiahao * @Date 2021/8/19 21:23 */public class Rectangle implements Shape { @Override public void draw() { System.out.println(\"shape Rectangle\"); }}
package pers.chenjiahao.chaindecorator;/** * @Author ChenJiahao * @Date 2021/8/19 21:23 */public class Circle implements Shape { @Override public void draw() { System.out.println(\"shape circle\"); }}
package pers.chenjiahao.chaindecorator;/** * @Author ChenJiahao * @Date 2021/8/19 21:23 */public abstract class ShapeDecorator implements Shape { protected Shape decoratedShape; public ShapeDecorator(Shape decoratedShape) { this.decoratedShape = decoratedShape; } protected ShapeDecorator() { } @Override public void draw() { decoratedShape.draw(); }}
package pers.chenjiahao.chaindecorator;/** * @Author ChenJiahao * @Date 2021/8/19 21:31 */public class RedShapeDecorator extends ShapeDecorator { public RedShapeDecorator() { } public RedShapeDecorator(Shape decoratedShape) { super(decoratedShape); } @Override public void draw() { setRedBorder(decoratedShape); } private void setRedBorder(Shape decoratedShape){ System.out.println(\"Border Color: Red\"); }}
package pers.chenjiahao.chaindecorator;/** * @Author ChenJiahao * @Date 2021/8/19 21:39 */public class BlackShapeDecorator extends ShapeDecorator { public BlackShapeDecorator(Shape decoratedShape) { super(decoratedShape); } public BlackShapeDecorator() { } @Override public void draw() { setBlackBorder(decoratedShape); } private void setBlackBorder(Shape decoratedShape){ System.out.println(\"Border Color: Black\"); }}
BlackShapeDecorator(继承装饰器的扩展装饰器类)
package pers.chenjiahao.chaindecorator;import java.util.ArrayList;import java.util.List;/** * @Author ChenJiahao * @Date 2021/8/19 21:48 */public class ShapeDecoratorChain extends ShapeDecorator { List<ShapeDecorator> shapeDecorators = new ArrayList<>(); public ShapeDecoratorChain(Shape decoratedShape) { super(decoratedShape); } public ShapeDecoratorChain addDecorator(ShapeDecorator shapeDecorator){ shapeDecorators.add(shapeDecorator); return this; } @Override public void draw() { super.draw(); for (ShapeDecorator shapeDecorator : shapeDecorators) { shapeDecorator.draw(); } }}
ShapeDecoratorChain(继承装饰器的链式装饰器类)
package pers.chenjiahao.chaindecorator;/** * @Author ChenJiahao * @Date 2021/8/19 21:23 */public class Test { public static void main(String[] args) { Shape shapeDecoratorChain = new ShapeDecoratorChain(new Circle()) .addDecorator(new RedShapeDecorator()) .addDecorator(new BlackShapeDecorator()); System.out.println(\"\chain------------------------------\"); shapeDecoratorChain.draw(); }}
设想:链式装饰器
装饰器(Decorator)
责任链模式是我个人认为比较重要的一种设计模式,在我日常的编码中也较常用到,责任链模式的本质就是过滤器,责任链模式可以使过滤条件自由组合,降低了代码的耦合度,并且扩展性非常高。
本质就是过滤器
1、有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。2、在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。 3、可动态指定一组对象处理请求。
后台要经过信息处理才能发表或进入数据库
例如:在论坛中发表文章
过滤器接口
多个实现过滤器接口的具体过滤器
实现过滤器接口的具体责任链(在责任链的doFilter中可以提前就对过滤器进行注册)
消息实体(往过滤器中传入的)
额外的一个测试方法
含有有一个普通方法doFilter(Msg m)
interface Filter{ boolean doFilter(Msg m);}
创建过滤器接口Filter
实现Filter
将:) 替换为 ^V^
class FaceFilter implements Filter{ @Override public boolean doFilter(Msg msg) { // 处理msg String r = msg.getMsg(); r = r.replace(\":)\
FaceFilter
将< 替换为 [将> 替换为 ]
HtmlFilter
将996 替换为 955
class SensitiveFilter implements Filter{ @Override public boolean doFilter(Msg msg) { // 处理msg String r = msg.getMsg(); r = r.replace(\"996\
SensitiveFilter
含有一个Filter集合
额外增加一个add方法
重写doFilter方法对Filter集合进行遍历
class FilterChain implements Filter{ List<Filter> filters = new ArrayList<>(); public FilterChain add(Filter f){ filters.add(f); return this; } public boolean doFilter(Msg msg){ for (Filter filter : filters) { if (!filter.doFilter(msg)){ return false; } } return true; }}
实现过滤器接口的具体责任链
class Msg{ String name; String msg; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } @Override public String toString() { return \"Msg{\" + \"msg='\" + msg + '\\'' + '}'; }}
消息实体
需要注意的是,提前将过滤器添加到责任链中,面向责任链过滤,而不面向具体的过滤器去过滤
/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/1 13:50 */public class Main { public static void main(String[] args) { Message message = new Message(); message.setMsg(\"大家好:),<script>,我是程序员五条,大家都是996\"); // 过滤表情和HTML的链条 FilterChain filterChain = new FilterChain(); filterChain.add(new FaceFilter()).add(new HtmlFilter()); // 过滤996的链条 FilterChain filterChain1 = new FilterChain(); filterChain1.add(new SensitiveFilter()); // 两个链条合并(也可以单用) filterChain.add(filterChain1); // 执行链条 filterChain.doFilter(message); System.out.println(message); }}
测试类(也在责任链的doFilter中可以提前就对过滤器进行注册,这样在测试类中就不用再注册过滤器了)
也可以用来做链式拦截
定义多条拦截规则
只要其中一条不符合,这条消息就无法持久化
注,这里我已经提前将多个拦截规则提前注册到拦截链中了,所以在调用的时候直接传入消息即可
/** * SQL校验接口(拦截器) * @Author ChenJiahao * @Date 2021/8/17 15:05 */public interface SqlInterceptor { void doInterceptor(String sql) throws SqlCheckException;}
拦截接口SqlInterceptor
/** * SQL非法关键字校验 * @Author ChenJiahao * @Date 2021/8/17 15:08 */@Componentpublic class IllegalKeywordsInterceptor implements SqlInterceptor{ static List<String> illegalKeywordsList = new ArrayList<>(); static { illegalKeywordsList.add(\"update \"); illegalKeywordsList.add(\"drop \"); illegalKeywordsList.add(\"alter \"); illegalKeywordsList.add(\"delete \"); illegalKeywordsList.add(\"insert \"); illegalKeywordsList.add(\"create \"); illegalKeywordsList.add(\"grant \"); illegalKeywordsList.add(\"perpare \"); illegalKeywordsList.add(\"execute \"); illegalKeywordsList.add(\"deallocate \"); illegalKeywordsList.add(\"truncate \"); } @Override public void doInterceptor(String sql) { // SQL全部转小写 String lowerSql = sql.toLowerCase(); for (String illegalKeywords : illegalKeywordsList) { if (lowerSql.contains(illegalKeywords)){ throw new SqlCheckException(\"SQL中包含非法关键字:\" + illegalKeywords); } } }}
(只允许查询select)SQL非法关键字校验:IllegalKeywordsInterceptor
/** * SQL长度校验 * @Author ChenJiahao * @Date 2021/8/20 11:20 */public class SqlLengthInterceptor implements SqlInterceptor{ @Override public void doInterceptor(String sql) throws SqlCheckException { if (sql.length() < 20) { throw new SqlCheckException(\"SQL长度至少需要大于等于20\"); } }}
SQL长度校验:SqlLengthInterceptor
拦截接口的实现类
/** * SQL校验链 * @Author ChenJiahao * @Date 2021/8/17 15:09 */@Componentpublic class SqlCheckInterceptorChain implements SqlInterceptor{ List<SqlInterceptor> interceptors = new ArrayList<>(); private SqlCheckInterceptorChain add(SqlInterceptor sqlInterceptor){ interceptors.add(sqlInterceptor); return this; } @Override public void doInterceptor(String sql) { add(new IllegalKeywordsInterceptor()); add(new SqlLengthInterceptor()); for (SqlInterceptor interceptor : interceptors) { interceptor.doInterceptor(sql); } }}
拦截链SqlCheckInterceptorChain
/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/17 15:15 */public class Main { public static void main(String[] args) { new SqlCheckInterceptorChain().doInterceptor(\"update public.user set username = '程序员五条' where id = 1\");}
实例:校验SQL
扩展
责任链(Chain Of Responsibility)
本质就是监听器
!!!事件处理模型:事件处理通常使用观察者+责任链
小孩睡醒了哭,饿了!
配一个或多个观察者
在被观察者中聚合监听器(链式)和事件
被观察者中产生事件,传入到监听器中,监听器去执行
事件接口
在构造方法中可以进行一些信息的设置,便于后面使用
具体事件
在方法中传入事件
观察者接口
进行观察到事件后进行的响应操作
观察者
内聚事件对象
源对象(事件本身)
定义一个观察者的集合对象,责任链的思想(责任链:https://blog.csdn.net/qq_42874315/article/details/120229565)
定义一个触发方法,这个方法中主要是进行事件的触发,然后通过遍历责任链(同时传入事件)去做出响应
被观察者
场景就是小孩子随时可能会哭,爸爸、妈妈、Dog三个监听者,去做相应的监听动作
创建抽象类Event
其中含有一个抽象方法
/** * 事件抽象类 * @Author ChenJiahao(程序员五条) * @Date 2021/9/11 15:20 */public abstract class Event<T> { abstract T getSource();}
Event(事件接口)
继承事件接口
定义一下事件
WakeUpEvent(具体事件)
创建接口Observer,还要创建一个监听的方法,参数为事件
/** * 观察者接口 * @Author ChenJiahao(程序员五条) * @Date 2021/9/11 15:23 */public interface Observer { void actionOnWakeUp(Event event);}
Observer(监听器接口)
实现Observer
重写actionOnWakeUp方法
增加自己监听后要执行的方法,加入到actionOnWakeUp方法中
/** * 观察者 * @Author ChenJiahao(程序员五条) * @Date 2021/9/11 15:24 */public class Dad implements Observer{ public void feed(){ System.out.println(\"dad feed................\"); } @Override public void actionOnWakeUp(Event event) { WakeUpEvent wakeUpEvent = (WakeUpEvent) event; System.out.println(\"地点:\" + wakeUpEvent.loc + \
Dad
/** * 观察者 * @Author ChenJiahao(程序员五条) * @Date 2021/9/11 15:24 */public class Mum implements Observer{ public void hug(){ System.out.println(\"mum hugging......\"); } @Override public void actionOnWakeUp(Event event) { WakeUpEvent wakeUpEvent = (WakeUpEvent) event; System.out.println(\"地点:\" + wakeUpEvent.loc + \
Mum
/** * 观察者 * @Author ChenJiahao(程序员五条) * @Date 2021/9/11 15:24 */public class Dog implements Observer{ public void wang(){ System.out.println(\"dog wangwangwang...\"); } @Override public void actionOnWakeUp(Event event) { WakeUpEvent wakeUpEvent = (WakeUpEvent) event; System.out.println(\"地点:\" + wakeUpEvent.loc + \
具体观察者
创建一个Child类
添加属性List<Observer> observers,监听器链
将具体的监听器注入到监听器链中
创建一个方法,作为触发唤醒监听事件在这个方法中需要进行事件的创建,以及监听器链的遍历触发
Child(被观察者)
创建Child对象
执行wakeUp()方法,作为触发事件
/** * @Author ChenJiahao(程序员五条) * @Date 2021/9/11 15:29 */public class Test { public static void main(String[] args) { Child child = new Child(\"五条宝宝\"); child.wakeUp(); }}
上述代码中有个缺陷,在Child中就提前加入了观察者,不是很灵活,当然可以在调用的时候手动传入,但是这样手写也不是很优雅可以提前将组合策略写好,直接传入Child的有参构造中即可
引入策略模式,管理观察者的组合方式,自由组合观察者
实现图
Child(不再进行观察者的注册了)
/** * 观察者策略 * @Author ChenJiahao(程序员五条) * @Date 2021/9/11 15:46 */public interface ObserverStrategy { List<Observer> getObserverStrategy();}
ObserverStrategy(策略接口)
/** * 观察者Dad、Mum、Dog * @Author ChenJiahao(程序员五条) * @Date 2021/9/11 15:50 */public class AllObserverStrategy implements ObserverStrategy { @Override public List<Observer> getObserverStrategy() { List<Observer> observers = new ArrayList<>(); observers.add(new Dad()); observers.add(new Mum()); observers.add(new Dog()); return observers; }}
AllObserverStrategy(观察者Dad、Mum、Dog)
/** * 观察者Dad、Dog * @Author ChenJiahao(程序员五条) * @Date 2021/9/11 15:49 */public class DadAndDogStrategy implements ObserverStrategy { @Override public List<Observer> getObserverStrategy() { List<Observer> observers = new ArrayList<>(); observers.add(new Dad()); observers.add(new Dog()); return observers; }}
DadAndDogStrategy(观察者Dad、Dog)
/** * 观察者Dad、Mum * @Author ChenJiahao(程序员五条) * @Date 2021/9/11 15:49 */public class DadAndMumStrategy implements ObserverStrategy { @Override public List<Observer> getObserverStrategy() { List<Observer> observers = new ArrayList<>(); observers.add(new Dad()); observers.add(new Mum()); return observers; }}
DadAndMumStrategy(观察者Dad、Mum)
/** * 观察者Mum、Dog * @Author ChenJiahao(程序员五条) * @Date 2021/9/11 15:49 */public class MumAndDogStrategy implements ObserverStrategy { @Override public List<Observer> getObserverStrategy() { List<Observer> observers = new ArrayList<>(); observers.add(new Mum()); observers.add(new Dog()); return observers; }}
MumAndDogStrategy(观察者Mum、Dog)
策略接口实现类
Event(未修改)
Observer(未修改)
/** * 观察者 * @Author ChenJiahao(程序员五条) * @Date 2021/9/11 15:24 */public class Dad implements Observer { public void feed(){ System.out.println(\"dad feed................\"); } @Override public void actionOnWakeUp(Event event) { WakeUpEvent wakeUpEvent = (WakeUpEvent) event; System.out.println(\"地点:\" + wakeUpEvent.loc + \
/** * 观察者 * @Author ChenJiahao(程序员五条) * @Date 2021/9/11 15:24 */public class Mum implements Observer { public void hug(){ System.out.println(\"mum hugging......\"); } @Override public void actionOnWakeUp(Event event) { WakeUpEvent wakeUpEvent = (WakeUpEvent) event; System.out.println(\"地点:\" + wakeUpEvent.loc + \
/** * 观察者 * @Author ChenJiahao(程序员五条) * @Date 2021/9/11 15:24 */public class Dog implements Observer { public void wang(){ System.out.println(\"dog wangwangwang...\"); } @Override public void actionOnWakeUp(Event event) { WakeUpEvent wakeUpEvent = (WakeUpEvent) event; System.out.println(\"地点:\" + wakeUpEvent.loc + \
具体观察者(未修改)
/** * @Author ChenJiahao(程序员五条) * @Date 2021/9/11 15:29 */public class Test { public static void main(String[] args) { // 今天爸爸不在家,只有妈妈和狗狗可以作为观察者 Child child = new Child(\"五条宝宝\
示例
上述引入的策略不是很优雅,可以再引入工厂,这里不再详细概述了,详情参考下述链接(优雅的干掉if-else)
https://blog.csdn.net/qq_42874315/article/details/119877790
再扩展
对实例的拓展设想1
可以将wakeUp方法抽象成一个触发类,这样就方便后期的扩展了
在每个触发类中,可以灵活的去创建事件
在触发类中,应对监听者做一些判断,例如,需要哪些监听者就添加,不需要就不添加,而不是像上述代码一样,全部添加进监听链
对实例的拓展设想2
Observer
Listener
Hook
Callback
以上都指的是观察者
在很多系统中,Observer模式往往和责任链共同负责对于事件的处理,其中的某一个Observer负责是否将事件进一步传递
观察者(Observer)
组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。树状结构专用模式
组合模式依据树形结构来组合对象,用来表示部分以及整体层次。
这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
一个抽象结点Node
存储一个或多个叶子的集合
还包含一个add方法,主要的作用是向集合中添加叶子的
继承Node的枝
存储内容
继承Node的叶子
测试类中包含一个打印树状格式的方法
/** * @Author ChenJiahao(程序员五条) * @Date 2021/9/14 19:35 */public abstract class Node { abstract public void print();}
Node
/** * @Author ChenJiahao(程序员五条) * @Date 2021/9/14 19:42 */public class BranchNode extends Node{ List<Node> nodes = new ArrayList<>(); String name; public BranchNode(String name) { this.name = name; } @Override public void print() { System.out.println(name); } public void add(Node node){ nodes.add(node); }}
BranchNode
/** * @Author ChenJiahao(程序员五条) * @Date 2021/9/14 19:50 */public class LeafNode extends Node{ String content; public LeafNode(String content){ this.content = content; } @Override public void print() { System.out.println(content); }}
LeafNode
/** * @Author ChenJiahao(程序员五条) * @Date 2021/9/14 19:42 */public class Test { public static void main(String[] args) { BranchNode root = new BranchNode(\"root\"); BranchNode chapter1 = new BranchNode(\"chapter1\"); BranchNode chapter2 = new BranchNode(\"chapter2\"); Node c11 = new LeafNode(\"c11\"); Node c12 = new LeafNode(\"c12\"); BranchNode b21 = new BranchNode(\"section21\"); Node c211 = new LeafNode(\"c211\"); Node c212 = new LeafNode(\"c212\
组合(Composite)
重复利用对象,将几个对象提前放入在池中,使用的时候直接拿,用完了再放回来就好,效率很高,省去了创建对象的事件,Java中的String就是享元模式
需要某个对象时不用new,直接拿
连接池、线程池、常量池都是这个原理
作用
享元+组合是很好的搭配,但是难度很高
创建一个 Shape 接口和实现了 Shape 接口的实体类 Circle。定义工厂类 ShapeFactory。ShapeFactory 有一个 Circle 的 HashMap,其中键名为 Circle 对象的颜色。无论何时接收到请求,都会创建一个特定颜色的圆。ShapeFactory 检查它的 HashMap 中的 circle 对象,如果找到 Circle 对象,则返回该对象,否则将创建一个存储在 hashmap 中以备后续使用的新对象,并把该对象返回到客户端。在测试类中使用 ShapeFactory 来获取 Shape 对象。它将向 ShapeFactory 传递信息(red / green / blue/ black / white),以便获取它所需对象的颜色。
常规
享元+组合
接口
接口实现类
工厂中定义一个map属性,这个map就是存储实现类的池
工厂
通过工厂构造出实现类(在构造实现类的时候调用了池)
/** * @Author ChenJiahao(程序员五条) * @Date 2021/9/13 20:40 */public interface Shape { void draw();}
Circle(实现类)
ShapeFactory
/** * @Author ChenJiahao(程序员五条) * @Date 2021/9/13 20:44 */public class Test { private static final String colors[] = { \"Red\
package pers.chenjiahao.flyweight;public class TestString { public static void main(String[] args) { String s1 = \"abc\"; String s2 = \"abc\"; String s3 = new String(\"abc\"); String s4 = new String(\"abc\"); System.out.println(s1 == s2); System.out.println(s1 == s3); System.out.println(s3 == s4); System.out.println(s3.intern() == s1); System.out.println(s3.intern() == s4.intern()); }}
结果:truefalsefalsetruetrue
字符串常量池测试
享元(Flyweight)
和装饰器很像,都是给目标对象增加额外的方法,核心思想就是AOP,在不改变原有方法逻辑和代码的情况下,动态的给某个/某些方法去添加功能,由此引出面向切面编程的思想
代理分为:静态代理和动态代理
区别:动态代理看不到代理的代码 静态代理可以看到代码(自己写的代码)
种类
在主方法中面向代理去执行
被代理的类抽象出的接口
被代理的类(实现接口)
代理类(实现接口,聚合接口,构造的时候就赋值)
测试类(面向代理去调用)
注:代理类中,在构造的时候对聚合的接口进行赋值,这样可以实现链式代理(像责任链)
public interface Movable { void move();}
Movable接口
public class Tank implements Movable{ @Override public void move() { System.out.println(\"Tank moving claclacla....\"); try { // 随机睡眠10秒 Thread.sleep(new Random().nextInt(10000)); } catch (InterruptedException e) { e.printStackTrace(); } }}
Tank(被代理的实体类,实现Movable)
public class TankTimeProxy implements Movable { Movable m; public TankTimeProxy(Movable m) { this.m = m; } @Override public void move() { long start = System.currentTimeMillis(); m.move(); long end = System.currentTimeMillis(); System.out.println(\"移动时间为:\" + (end - start) + \"毫秒\"); }}
TankTimeProxy
public class TankLogProxy implements Movable{ Movable m; public TankLogProxy(Movable m) { this.m = m; } @Override public void move() { System.out.println(\"Start moving ....\"); m.move(); System.out.println(\"End moving ....\"); }}
TankLogProxy
代理类(实现Movable)
public class Test { public static void main(String[] args) { new TankTimeProxy( new TankLogProxy( new Tank() ) ).move(); }}
静态代理
JDK动态代理执行过程
实现JDK中的Proxy类
newProxyInstance()会返回一个代理对象
// 第一个参:(跟被代理对象用同一个就好)用哪个Classloader来把将来返回来的代理对象漏到内存 // 第二个参:(被new出来的对象要实现这个接口)代理对象应该实现哪些接口 // 第三个参:调用处理器 其实就是实现了InvocationHandler的对象
也可以写成匿名内部类的形式,在调newInstance方法的时候,第三个参数写成匿名的
代理类(实现InvocationHandler接口)
public class Tank implements Movable { @Override public void move() { System.out.println(\"Tank moving claclacla....\"); try { // 随机睡眠10秒 Thread.sleep(new Random().nextInt(10000)); } catch (InterruptedException e) { e.printStackTrace(); } }}
Tank(实现Movable)
LogHandler(在invoke中横切代码与业务逻辑不分离)
代理类:实现InvocationHandler接口
动态代理
与动态代理中的实例区别为:修改代理类中的invoke方法
before和after中的非业务逻辑本身是直接存在于invoke方法中的,现在抽离出来
将本来在invoke方法中写的非业务逻辑,抽离成一个个方法,去调用,这样结构就比较明确了,这就是Spring@Around环绕通知的原理了
(AOP:横切代码与业务逻辑代码分离)
LogHandler修改后(抽离出before方法和after方法)
动态代理引AOP的概念
代理(Proxy)
迭代器模式主要用于顺序访问集合对象的元素,不需要知道集合对象的底层表示。
访问一个聚合对象的内容而无须暴露它的内部表示。
需要为聚合对象提供多种遍历方式。
为遍历不同的聚合结构提供一个统一的接口。
使用场景
创建一个 Iterator 接口和一个返回迭代器的 Container 接口。实现了 Container 接口的实体类将负责实现 Iterator 接口。
Test类中使用实体类 NamesRepository 来打印 NamesRepository 中存储为集合的 Names。
迭代器接口
面向用户的接口(让用户获取迭代器的)
实际需要迭代的实体类,实现Container接口
实体类的内部迭代器类,实现迭代器接口(通常会将这个类设置在实体类的内部,制定自己独特的迭代规则)
对需要迭代的实体类进行赋值,然后通过用户获取迭代器的接口中的getIterator方法获取迭代器,最后遍历迭代器
package pers.chenjiahao.iterator.v6;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/28 21:02 */public interface Iterator { public boolean hasNext(); public Object next();}
Iterator(迭代器接口)
package pers.chenjiahao.iterator.v6;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/28 21:02 */public interface Container { public Iterator getIterator();}
Container(用户获取迭代器的接口)
package pers.chenjiahao.iterator.v6;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/28 21:03 */public class NameRepository implements Container { public String[] names; public NameRepository(String[] names) { this.names = names; } @Override public Iterator getIterator() { return new NameIterator(); } private class NameIterator implements Iterator{ int index; @Override public boolean hasNext() { if(index < names.length){ return true; } return false; } @Override public Object next() { if(this.hasNext()){ return names[index++]; } return null; } }}
NameRepository(实际需要迭代的实体类,实现Container接口)
NameIterator(NameRepository的内部迭代器类,实现迭代器接口)
package pers.chenjiahao.iterator.v6;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/28 21:05 */public class Main { public static void main(String[] args) { String[] names = {\"Robert\
迭代器(Iterator)
在结构不变的情况下动态改变对于内部元素的动作
根据不同的访问者,调用相同的动作,产生不同的结果
优点:可扩展性较好
缺点:对象经常变化的话,修改过程会非常复杂
1、对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。
2、需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作\"污染\"这些对象的类,也不希望在增加新操作时修改这些类。
场景为批发水果,访问者分为个体户和经销商,他们批发水果得到的折扣肯定是不一样的
创建一个定义接受操作的 FruitPart 接口。Apple、Banana、Watermelon是FruitPart 接口的实体类。定义另一个接口Visitor,它定义了访问者类的操作。定义一个Fruit类来聚合三种具体的实现类(方便调用)。Fruit使用实体访问者来执行相应的动作。
实体类的抽象类
面向的类(聚合了多种具体实体类),面向这个类进行调用
访问者接口
实现Visitor接口,编写一些自己的属性逻辑,实现三个具体的方法,在这里进行对不同访问者的不同操作
具体访问者
测试类:面向Fruit类进行调用
/** * 水果抽象类 * @Author ChenJiahao * @Date 2021/8/1 20:06 */public abstract class FruitPart { abstract void accept(Visitor visitor); abstract double getPrice();}
FruitPart(实体类的抽象)
需要注意的是,在accept方法中,调用访问者的具体相关自身实体类的方法
/** * @Author ChenJiahao * @Date 2021/8/1 20:08 */public class Apple extends FruitPart { @Override void accept(Visitor visitor) { visitor.visitApple(this); } @Override double getPrice() { return 5.00; }}
Apple
/** * @Author ChenJiahao * @Date 2021/8/1 20:08 */public class Banana extends FruitPart { @Override void accept(Visitor visitor) { visitor.visitBanana(this); } @Override double getPrice() { return 15.00; }}
Banana
/** * @Author ChenJiahao * @Date 2021/8/1 20:08 */public class Watermelon extends FruitPart { @Override void accept(Visitor visitor) { visitor.visitWatermelon(this); } @Override double getPrice() { return 50.00; }}
Watermelon
/** * @Author ChenJiahao * @Date 2021/8/1 20:09 */public class Fruit{ private FruitPart banana = new Banana(); private FruitPart apple = new Apple(); private FruitPart watermelon = new Watermelon(); public void accept(Visitor visitor){ banana.accept(visitor); apple.accept(visitor); watermelon.accept(visitor); }}
Fruit(聚合了多种具体实体的类)
/** * @Author ChenJiahao * @Date 2021/8/1 20:07 */public interface Visitor { void visitBanana(Banana banana); void visitApple(Apple apple); void visitWatermelon(Watermelon watermelon);}
Visitor(访问者接口)
/** * @Author ChenJiahao * @Date 2021/8/1 20:12 */public class PersonalVisitor implements Visitor{ double totalPrice = 0.0; @Override public void visitBanana(Banana banana) { totalPrice += banana.getPrice(); } @Override public void visitApple(Apple apple) { totalPrice += apple.getPrice(); } @Override public void visitWatermelon(Watermelon watermelon) { totalPrice += watermelon.getPrice(); }}
PersonalVisitor
/** * @Author ChenJiahao * @Date 2021/8/1 20:12 */public class Wholesaler implements Visitor{ double totalPrice = 0.0; @Override public void visitBanana(Banana banana) { totalPrice += banana.getPrice() * 0.7; } @Override public void visitApple(Apple apple) { totalPrice += apple.getPrice() * 0.7; } @Override public void visitWatermelon(Watermelon watermelon) { totalPrice += watermelon.getPrice() * 0.7; }}
Wholesaler
/** * @Author ChenJiahao * @Date 2021/8/1 20:16 */public class Main { public static void main(String[] args) { // 个人原价 70 PersonalVisitor personalVisitor = new PersonalVisitor(); new Fruit().accept(personalVisitor); System.out.println(personalVisitor.totalPrice); // 商户七折 49 Wholesaler wholesaler = new Wholesaler(); new Fruit().accept(wholesaler); System.out.println(wholesaler.totalPrice); }}
可以使用访问者+工厂
将面向调用的类工厂化
访问者(Visitor)
构建复杂对象用,一个对象可以有多种组合方式,构建不同组合方式的对象
分离复杂对象的构建和表示,同样的构建过程可以创建不同的表示
缺点:构造的模板一旦确定,再修改的时候不是很好修改,无法保障OCP原则
感觉就像是set注入方法:set了哪几个属性,哪几个就注入了,没set的属性就无
在构建对象时,如果有些属性没有的话,还得写,使用构造器就不用
去肯德基,汉堡、可乐、薯条、炸鸡翅等是不变的,而其组合是经常变化的,生成出所谓的\"套餐\"。
Java中的StringBuilder
Java中典型的Builder模式实现图
复杂构造者模式实现图
需要构造的实体类
实体类中需要一个具体的构造类
构造类中需要聚合一个需要构造的实体类对象
构造类中需要写各种构造该实体类中不同属性的方法
构造类中还需要一个构造结束的方法build(),返回的对象是内聚的实体类
注:构造类中方法的返回值均为构造类本身(return this),这样做的目的是为了能够链式构造
其中包含所有需要构造的属性的方法,返回值均为该构造器
额外还有一个构造结束的方法build(),返回值为具体需要构造的实体类
构建器接口
实现构造器接口TerrainBuilder
实现TerrainBuilder中的方法(每个方法中编写对于属性的赋值原则,使用内聚的Terrain对象的属性进行赋值操作)
build()方法中返回内聚的Terrain对象
构造器接口实现类(里面要聚合一个将要构造的具体对象)
复杂构造者模式
Person(需要构造的类,构造器放在其内部,用静态内部类表示)
Location(Person中以用的类)
/** * 构建器接口 * @Author ChenJiahao(程序员五条) * @Date 2021/9/22 22:20 */public interface TerrainBuilder { TerrainBuilder buildWall(); TerrainBuilder buildFort(); TerrainBuilder buildMine(); Terrain build();}
TerrainBuilder(构造器接口)
/** * 要构造的实体类 * @Author ChenJiahao(程序员五条) * @Date 2021/9/22 22:25 */public class Terrain { Wall w; Fort f; Mine m;}
Wall
Fort
Mine
这里将Terrain的属性引用了三个类
Terrain(需要构造的实体类)
ComplexTerrainBuilder(构造器接口实现类)
/** * 测试类 * @Author ChenJiahao(程序员五条) * @Date 2021/9/22 22:35 */public class Test { public static void main(String[] args) { TerrainBuilder builder = new ComplexTerrainBuilder(); Terrain t = builder.buildFort() .buildMine() .buildWall() .build(); System.out.println(t); }}
构造器(Builder)
接口转换器
跟装饰器有点类似
电压转换头:220V的插头在110V上用不了,这时就需要一个转接头了
生活中的场景
Java中想去使用SQLServer,但是Java提供的是JDBC,SQLServer提供的是ODBC
这时在它们之间就需要一个转换器:JDBC-ODBC-Bridge
代码中的场景
常见的Adapter类反而不是Adapter(包装器),只是一种方便的编程方式
如:WindowAdapter、KeyAdapter都不是
常见误区
创建一个MediaPlayer 接口和一个实现了MediaPlayer 接口的实体类 AudioPlayer。默认情况下,AudioPlayer 可以播放 mp3 格式的音频文件。
创建高级媒体接口AdvancedMediaPlayer 和实现了 AdvancedMediaPlayer 接口的实体类。该类可以播放 vlc 和 mp4 格式的文件。
创建一个适配器让AudioPlayer可以播放其他媒体,MediaPlayer 接口的适配器类 MediaAdapter,并使用 AdvancedMediaPlayer 对象来播放所需的格式。
基本实体类的接口
高级实体类的接口(要适配的目标)
高级实体类(实现高级接口)
转换器(实现基本接口,因为是要将基本转换为高级,所以要转换器要实现基本接口)需要聚合一个高级实体类
基本实体类(实现基本接口,聚合适配器,如果是高级类型的就用适配器去调用)
MediaPlayer(基本实体类的接口)
package pers.chenjiahao.mediaadapter;/** * 高级媒体 * @Author ChenJiahao * @Date 2021/8/21 17:36 */public interface AdvancedMediaPlayer { public void playVlc(String fileName); public void playMp4(String fileName);}
AdvancedMediaPlayer(高级实体类的接口)
package pers.chenjiahao.mediaadapter;/** * @Author ChenJiahao * @Date 2021/8/21 17:38 */public class VlcPlayer implements AdvancedMediaPlayer { @Override public void playVlc(String fileName) { System.out.println(\"Playing vlc file. Name: \"+ fileName); } @Override public void playMp4(String fileName) { }}
VlcPlayer
package pers.chenjiahao.mediaadapter;/** * @Author ChenJiahao * @Date 2021/8/21 17:38 */public class Mp4Player implements AdvancedMediaPlayer { @Override public void playVlc(String fileName) { } @Override public void playMp4(String fileName) { System.out.println(\"Playing mp4 file. Name: \"+ fileName); }}
Mp4Player
package pers.chenjiahao.mediaadapter;/** * 基本媒体的适配器 * @Author ChenJiahao * @Date 2021/8/21 17:40 */public class MediaAdapter implements MediaPlayer { private AdvancedMediaPlayer advancedMusicPlayer; public MediaAdapter(String audioType) { if(\"vlc\".equalsIgnoreCase(audioType)){ advancedMusicPlayer = new VlcPlayer(); } else if (\"mp4\
MediaAdapter(转换器)
AudioPlayer(基本实体类,本来只支持播放MP3)
package pers.chenjiahao.mediaadapter;/** * @Author ChenJiahao * @Date 2021/8/21 17:53 */public class Main { public static void main(String[] args) { AudioPlayer audioPlayer = new AudioPlayer(); audioPlayer.play(\"mp3\
问题1:AdvancedMediaPlayer中有多种方法,对于不需要的实现类来说,必需得实现,空着放在那儿
问题2:AudioPlayer中如果不是基本的mp3,就不用再进行判断了,直接调用适配器,将原来别的mp4、vlc判断交给适配器,这样AudioPlayer的代码就不用再改变了(如果不这样做的话,后面再加一种mp5,AudioPlayer代码就得改了)
问题3:适配器MediaAdapter中,if太多了,应该想办法消灭一些
问题
合并AdvancedMediaPlayer中的playVlc()和playMp4(),使用playAdvanced()代替
问题1
AudioPlayer中只留下一个if和一个else,表示基本和高级
问题2
修改MediaAdapter的play()中为advancedMusicPlayer.playAdvanced(fileName);原先的if else全部去除
MediaAdapter的无参构造中,只保留一个if和一个else,用来判断有没有高级实体类(mp4、vlc)给MediaAdapter中聚合的AdvancedMediaPlayer赋值,采用工厂进行赋值
创建一个生产高级实体类的工厂
问题3
解决方案
上述实例中存在的一些问题和解决方案
package pers.chenjiahao.mediaadapterplus;/** * 高级媒体 * @Author ChenJiahao * @Date 2021/8/21 17:36 */public interface AdvancedMediaPlayer { void playAdvanced(String fileName);}
package pers.chenjiahao.mediaadapterplus;/** * @Author ChenJiahao * @Date 2021/8/21 17:38 */public class VlcPlayer implements AdvancedMediaPlayer { @Override public void playAdvanced(String fileName) { System.out.println(\"Playing vlc file. Name: \"+ fileName); }}
package pers.chenjiahao.mediaadapterplus;/** * @Author ChenJiahao * @Date 2021/8/21 17:38 */public class Mp4Player implements AdvancedMediaPlayer { @Override public void playAdvanced(String fileName) { System.out.println(\"Playing mp4 file. Name: \"+ fileName); }}
package pers.chenjiahao.mediaadapterplus;/** * 基本媒体的适配器 * @Author ChenJiahao * @Date 2021/8/21 17:40 */public class MediaAdapter implements MediaPlayer { private AdvancedMediaPlayer advancedMusicPlayer; public MediaAdapter(String audioType) { // 判断一下工厂中是否有这个高级类型 if (AdvancedMediaPlayerFactory.advancedMediaFactory.containsKey(audioType)){ advancedMusicPlayer = AdvancedMediaPlayerFactory.advancedMediaFactory.get(audioType); } else { System.out.println(\"Invalid media. \" + audioType + \" format not supported\
AdvancedMediaPlayerFactory(创造高级实体类的工厂)
package pers.chenjiahao.mediaadapterplus;/** * @Author ChenJiahao(五条) * @Date 2021/8/21 17:53 */public class Main { public static void main(String[] args) { // 这里模拟Springboot启动后自动注册高级对象到工厂map的操作 // springboot场景中,可以让AdvancedMediaPlayer接口去继承InitializingBean // 这样子类都必须要实现一个方法afterPropertiesSet(),这个方法实在程序之后的时候,默认要执行一次的方法,在这里可以进行一下的注册操作 // 详见项目:24-eliminate-if-else 文章:利用设计模式优雅的干掉if-else AdvancedMediaPlayerFactory.registerAdvancedMedia(\"mp4\
修改后的实例(引入工厂)
包装器/转换器(Adapter/Wrapper)
桥接是用于把抽象化与实现化解耦,使得二者可以独立变化。
分离抽象和具体的实现,让他们可以独自发展
可以理解为实体类中聚合的桥接接口
一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
人类有你我他.......饭也有很多种,油泼面、炒米饭......
人类是一个抽象类
饭是一个抽象类
人类中有一个属性,将要吃的饭有一个方法,吃
从上述描述中可以得知,人类的子类是无穷无尽的,饭的子类也是无穷无尽的,可以认为人类和饭都是要进行扩展的,这就是两个维度都要扩展
人吃饭
我们有一个作为桥接实现的 DrawAPI 接口和实现了 DrawAPI 接口的实体类 RedCircle、GreenCircle。
Shape 是一个抽象类,将使用 DrawAPI 的对象。BridgePatternDemo 类使用 Shape 类来画出不同颜色的圆。
关系描述:人用水彩笔去画画,不同的人可以用不同的水彩笔去画画,双维度扩展
桥接接口(水彩笔)
桥接实现类(蓝色水彩笔,红色水彩笔......)
抽象类(人类,聚合了桥接接口,在抽象类中构造了这个接口)
抽象类的实现类(你,我,他,在实现抽象类的方法中使用聚合的接口去调用)
DrawAPI(桥接接口)
GreenCircle
RedCircle
桥接实现类
package pers.chenjiahao.newcodebridge;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/23 21:49 */public abstract class Shape { protected DrawAPI drawAPI; public Shape(DrawAPI drawAPI) { this.drawAPI = drawAPI; } public abstract void draw();}
Shape(抽象类)
Shape的实现类
如果Shape再多一个Square的实现类,对应的可以再加一个BlackSquare的桥接实现类
但是由于Shape的实现类和桥接的实现类之间没有限制,所以可能会造成,new Square(new RedCircle)的情况
这样的话还是会输出,红色的圈
这样就出问题了- -
所以应该一个DrawApi的子类接口,例如CircleDrawAPI或SquareDrawApi,让具体的桥接实现类去实现这些子接口
同时让Shape的实现类在构造时,不再使用DrawApi接口进行构造,而使用具体的子接口进行构造
这样每种类型就规范
例如:Square就只能去拿BlackSquare Circle就只能去拿RedCircle
这样做就不会出现混乱了
实例中的缺陷设想
DrawAPI(未改变,桥接接口)
package pers.chenjiahao.bridgeplus.bridge;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/23 22:16 */public interface CircleDrawAPI extends DrawAPI {}
CircleDrawAPI
package pers.chenjiahao.bridgeplus.bridge;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/23 22:16 */public interface SquareDrawAPI extends DrawAPI{}
SquareDrawAPI
桥接接口的子接口(新增)
实现CircleDrawAPI
BlackSquare
实现SquareDrawAPI
桥接接口的子接口的实现类(修改:原本是直接实现桥接接口)
package pers.chenjiahao.bridgeplus;import pers.chenjiahao.bridgeplus.bridge.DrawAPI;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/23 21:49 */public abstract class Shape { protected DrawAPI drawAPI; public Shape(DrawAPI drawAPI) { this.drawAPI = drawAPI; } public abstract void draw();}
Shape(未改变,抽象类)
Circle(修改构造中的DrawAPI为CircleDrawAPI)
Square(修改构造中的DrawAPI为SquareDrawAPI)
抽象类的实现类(修改:原先构造时是面向DrawAPI,现在面向ArawAPI的子类)
测试类(用Squaer去调用GreenCircle会报错)
实例改进
桥接(Bridge)
命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式。
请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。
将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。
在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。
顺序:调用者→命令→接受者。
比如要对行为进行\"记录、撤销/重做、事务\"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将\"行为请求者\"与\"行为实现者\"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
我们首先创建作为命令的接口 Order,然后创建作为请求的 Stock 类。实体命令类 BuyStock 和 SellStock,实现了 Order 接口,将执行实际的命令处理。创建作为调用对象的类 Broker,它接受订单并能下订单。
Broker 对象使用命令模式,基于命令的类型确定哪个对象执行哪个命令。CommandPatternDemo 类使用 Broker 类来演示命令模式。
命令接口
请求类(写几个方法,供命令接口的实现类去调用)
命令接口的实现类(聚合请求类,构造对其赋值,在实现方法中,利用请求类去调用对应的方法)
命令调用类(一个List用于存储命令,一个添加命令,一个遍历执行命令)
测试类(使用命令调用类来接收和执行命令)
package pers.chenjiahao.commandplus;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/25 20:22 */public interface Order { void execute();}
Order(命令接口)
package pers.chenjiahao.commandplus;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/25 20:23 */public class Stock { private String name = \"ABC\"; private int quantity = 10; public void buy(){ System.out.println(\"Stock [ Name:\" + name + \"Quantity:\" + quantity + \"] bought\"); } public void sell(){ System.out.println(\"Stock [ Name:\" + name + \"Quantity:\" + quantity + \"] sold\"); }}
Stock(请求类)
package pers.chenjiahao.commandplus;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/25 20:29 */public class SellStockOrder implements Order{ private Stock abcStock; public SellStockOrder(Stock abcStock) { this.abcStock = abcStock; } @Override public void execute() { abcStock.sell(); }}
SellStockOrder
package pers.chenjiahao.commandplus;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/25 20:27 */public class BuyStockOrder implements Order{ private Stock abcStock; public BuyStockOrder(Stock abcStock) { this.abcStock = abcStock; } @Override public void execute() { abcStock.buy(); }}
BuyStockOrder
命令接口的实现类
package pers.chenjiahao.commandplus;import java.util.ArrayList;import java.util.List;/** * 命令调用类 * @Author ChenJiahao(程序员五条) * @Date 2021/8/25 20:33 */public class Broker { private List<Order> orderList = new ArrayList<>(); public void takeOrder(Order order){ orderList.add(order); } public void placeOrders(){ for (Order order : orderList) { order.execute(); } orderList.clear(); }}
Broker(命令调用类)
package pers.chenjiahao.commandplus;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/25 20:35 */public class Main { public static void main(String[] args) { Broker broker = new Broker(); broker.takeOrder(new BuyStockOrder(new Stock())); broker.takeOrder(new SellStockOrder(new Stock())); broker.placeOrders(); }}
每加一个命令,就需要再写一行代码,不太美观
引入责任链的概念,实现链式添加命令
上述实例中存在的问题
其实上述实例中,有责任链的概念,只是更改一下调用方式,使其更美观
只需要修改原Broker类中takeOrder方法的返回值为Broker然后return this 即可
package pers.chenjiahao.commandpluschain;import java.util.ArrayList;import java.util.List;/** * 命令调用类 * @Author ChenJiahao(程序员五条) * @Date 2021/8/25 20:33 */public class Broker { private List<Order> orderList = new ArrayList<>(); public Broker takeOrder(Order order){ orderList.add(order); return this; } public void placeOrders(){ for (Order order : orderList) { order.execute(); } orderList.clear(); }}
修改后的Broker
package pers.chenjiahao.commandpluschain;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/25 20:35 */public class Main { public static void main(String[] args) { Broker broker = new Broker(); /*broker.takeOrder(new BuyStockOrder(new Stock())); broker.takeOrder(new SellStockOrder(new Stock()));*/ // 修改为链式添加 broker.takeOrder(new BuyStockOrder(new Stock())) .takeOrder(new SellStockOrder(new Stock())); broker.placeOrders(); }}
新的测试类
修改实例,实现链式添加命令
如果想增加新的命令,当然命令接口的实现类比较好添加,也符合OCP原则但是想添加具体的命令内容,就得修改Stock了,不符合OCP原则
在构造命令的时候容易造成混乱,例如BuyStockOrder可以调用sell()方法,这样是不允许的,通过将代码修改为工厂+策略后,这个问题将解决
还存在一些问题
工厂+策略详见:https://blog.csdn.net/qq_42874315/article/details/119877790
package pers.chenjiahao.commandpluschainfactory;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/25 20:22 */public interface Order { void execute();}
Order接口
package pers.chenjiahao.commandpluschainfactory;import java.util.ArrayList;import java.util.List;/** * 命令调用类 * @Author ChenJiahao(程序员五条) * @Date 2021/8/25 20:33 */public class Broker { private List<Order> orderList = new ArrayList<>(); public Broker takeOrder(Order order){ orderList.add(order); return this; } public void placeOrders(){ for (Order order : orderList) { order.execute(); } orderList.clear(); }}
Broker
无变化
OperateHandler
创建操作策略接口
BuyOperateHandler
SellOperateHandler
创建操作策略接口的实现类
StockOperateFactory
创建操作工厂
新增
删除原buy方法和sell方法
创建新方法operateStock
package pers.chenjiahao.commandpluschainfactory;import pers.chenjiahao.commandpluschainfactory.factory.StockOperateFactory;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/25 20:23 */public class Stock { private String name = \"ABC\
修改后
修改Stock
修改execute中调用buy方法改为调用operateStock方法,并传入对应的操作字符串buy(工厂map中的key)
package pers.chenjiahao.commandpluschainfactory;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/25 20:27 */public class BuyStockOrder implements Order { private Stock abcStock; public BuyStockOrder(Stock abcStock) { this.abcStock = abcStock; } @Override public void execute() { abcStock.operateStock(\"buy\"); }}
修改BuyStockOrder
修改execute中调用sell方法改为调用operateStock方法,并传入对应的操作字符串sell(工厂map中的key)
package pers.chenjiahao.commandpluschainfactory;/** * @Author ChenJiahao(程序员五条) * @Date 2021/8/25 20:29 */public class SellStockOrder implements Order { private Stock abcStock; public SellStockOrder(Stock abcStock) { this.abcStock = abcStock; } @Override public void execute() { abcStock.operateStock(\"sell\"); }}
修改SellStockOrder
将操作handler手动注册到工厂map中
package pers.chenjiahao.commandpluschainfactory;import pers.chenjiahao.commandpluschainfactory.factory.StockOperateFactory;import pers.chenjiahao.commandpluschainfactory.handler.BuyOperateHandler;import pers.chenjiahao.commandpluschainfactory.handler.SellOperateHandler;/** * 链式调用 * @Author ChenJiahao(程序员五条) * @Date 2021/8/25 20:35 */public class Main { public static void main(String[] args) { Broker broker = new Broker(); // 向工厂map中手动注册操作handler(这里注册的操作在Springboot项目中可以让操作接口去继承InitializingBean,子类在afterPropertiesSet()中进行自动注册) StockOperateFactory.registerOperateStockHandler(\"buy\
修改测试类
修改
引入工厂+策略
命令模式(Command/Action/Transaction)
new一个对象,指定属性
如果新的对象和原来的对象属性差不多,这时可以从原对象克隆
创建对象有两种方式
1.实现原型模式需要实现标记型接口Cloneable
2.重写clone()方法如果只重写clone()方法,而没实现接口,调时会报异常
3.一般用于一个对象的属性已经确定,需要生产更多相同对象时。
4.需要区分深克隆的浅克隆
Java自带的原型模式
克隆后的对象中的属性和被克隆对象的属性指向同一个引用,也就是说被克隆对象改变属性,克隆对象也会变
浅克隆
克隆对象和被克隆对象指向的地址不同,一个对象属性的改变不会造成被克隆对象属性的改变
深克隆
浅克隆原理图
深克隆原理图
深克隆和浅克隆
注意:深克隆中String不用管,new的String要管
需要被克隆的类实现Cloneable接口
重写clone方法
注意,深克隆和浅克隆的区分就在clone方法中
在clone方法中:return super.clone()
在clone方法中:新建一个对象新对象 = super.clone()return 新对象
package pers.chenjiahao.prototype.v1;/** * 这样克隆有个问题 * 17行p1对象改变了,18行p2对象也变了,不能这样 * 浅克隆 两个对象指向同一个引用,会有影响 */public class Test { public static void main(String[] args) { Person p1 = new Person(); try { Person p2 = (Person) p1.clone(); System.out.println(p2.age + \" \" + p2.score ); System.out.println(p2.loc); System.out.println(p1.loc == p2.loc); p1.loc.street = \"sh\"; System.out.println(p2.loc); } catch (CloneNotSupportedException e) { e.printStackTrace(); } }}class Person implements Cloneable{ int age = 8; int score = 100; Location loc = new Location(\"bj\
package pers.chenjiahao.prototype.v3;/** * 深克隆处理 * 31行、32行 */public class Test { public static void main(String[] args) { Person p1 = new Person(); try { Person p2 = (Person) p1.clone(); System.out.println(p2.age + \" \" + p2.score ); System.out.println(p2.loc); System.out.println(p1.loc == p2.loc); p1.loc.street = \"sh\"; System.out.println(p2.loc); } catch (CloneNotSupportedException e) { e.printStackTrace(); } }}class Person implements Cloneable{ int age = 8; int score = 100; Location loc = new Location(\"bj\
原型模式/克隆模式(Prototype)
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。
后悔药
打游戏时的存档
Windows 里的 ctri + z
IE 中的后退
数据库的事务管理
备忘录模式使用三个类 Memento、Originator 和 CareTaker。
Memento 包含了要被恢复的对象的状态。
Originator 创建并在 Memento 对象中存储状态。
Caretaker 对象负责从 Memento 中恢复对象的状态。
MementoPatternDemo,我们的演示类使用 CareTaker 和 Originator 对象来显示对象的状态恢复。
需要备份的属性,属性与目标对象一致
有参构造对其赋值
get方法,可以获取到这些属性
备忘录(不面向用户,用户面向管理者去调用)
自身的属性
set/get方法
将当前状态存储到备忘录的方法,返回值为备忘录类型
从备忘录中获取属性的方法,当前自身对象进行赋值(入参为备忘录,没有返参数)
要备份的目标对象
一个List用来存储多个备忘录
aadd方法,给list中添加备忘录
get方法,通过index获取备忘录
备忘录管理者(链式备忘录)
给目标对象赋值
通过目标对象中的保存到备忘录的方法,获取到一个Memento对象
将这个memento对象添加到备忘录管理者中(通过管理者的添加方法)
package pers.chenjiahao.designpattern.memento;/** * @Author ChenJiahao(五条) * @Date 2021/8/27 17:53 */public class Memento { private String state; public Memento(String state) { this.state = state; } public String getState(){ return state; }}
Memento(备忘录)
package pers.chenjiahao.designpattern.memento;/** * @Author ChenJiahao(五条) * @Date 2021/8/27 17:54 */public class Originator { private String state; public void setState(String state) { this.state = state; } public String getState(){ return state; } public Memento saveStateToMemento(){ return new Memento(state); } public void getStateFromMemento(Memento memento){ state = memento.getState(); }}
Originator(目标对象)
package pers.chenjiahao.designpattern.memento;import java.util.ArrayList;import java.util.List;/** * @Author ChenJiahao(五条) * @Date 2021/8/27 17:57 */public class CareTaker { private List<Memento> mementoList = new ArrayList<>(); public void add(Memento mementoState){ mementoList.add(mementoState); } public Memento get(int index){ return mementoList.get(index); }}
CareTaker(管理者)
package pers.chenjiahao.designpattern.memento;/** * @Author ChenJiahao(五条) * @Date 2021/8/27 17:59 */public class Main { public static void main(String[] args) { Originator originator = new Originator(); CareTaker careTaker = new CareTaker(); originator.setState(\"State #1\"); originator.setState(\"State #2\"); careTaker.add(originator.saveStateToMemento()); originator.setState(\"State #3\"); careTaker.add(originator.saveStateToMemento()); originator.setState(\"State #4\"); System.out.println(\"Current State: \" + originator.getState()); originator.getStateFromMemento(careTaker.get(0)); System.out.println(\"First saved State: \" + originator.getState()); originator.getStateFromMemento(careTaker.get(1)); System.out.println(\"Second saved State: \" + originator.getState()); }}
备忘录(Memento)
钩子函数
核心思想:调用一个方法A的时候,这个方法A中又有方法B和方法C......我们可以只调用方法A,就达到了调用方法B和方法C的目的
凡是重写方法,系统帮我们自动调用的,都可以称为模板方法,在调用一个函数的时候里面自动又调了别的方法
总结成一句话:面向抽象调用,子类不重写抽象父类的非抽象方法,直接调用
1、有多个子类共有的方法,且逻辑相同。
2、重要的、复杂的方法,可以考虑作为模板方法。
注意事项:为防止恶意操作,一般模板方法都加上 final 关键词。
创建一个定义操作的 Game 抽象类,其中,模板方法设置为 final,这样它就不会被重写。
GameSubclass是扩展了 Game 的实体类,重写了抽象类的方法。
抽象类(定义多个抽象方法,和一个非抽象方法)这个非抽象方法就是模板方法,它调用多个抽象方法
继承抽象类的子类
测试类:子类只需要调用父类的一个模板方法,就可以达到执行自身多个已经实现了的抽象方法的作用
含有两个抽象方法op1和op2
一个模板方法doOp1AndOp2()
模板方法上加了final
/** * doOp1AndOp2()为模板方法 * @Author ChenJiahao(程序员五条) * @Date 2021/9/28 23:59 */public abstract class Game { // 防止恶意操作,模板方法上都会加上final final void doOp1AndOp2(){ op1(); op2(); } abstract void op1(); abstract void op2();}
Game(抽象类)
实现抽象类的抽象方法
/** * @Author ChenJiahao(程序员五条) * @Date 2021/9/29 0:00 */public class GameSubclass extends Game { @Override void op1() { System.out.println(\"op1\"); } @Override void op2() { System.out.println(\"op2\"); }}
GameSubclass(Game的子类)
/** * @Author ChenJiahao(程序员五条) * @Date 2021/9/29 0:00 */public class Test { public static void main(String[] args) { Game gameSubclass = new GameSubclass(); // 不使用模板方法 gameSubclass.op1(); gameSubclass.op2(); // 使用模板方法 gameSubclass.doOp1AndOp2(); }}
一个抽象类中可能会有几十个抽象方法
如果一个实现类都想调用的话,需要一个一个去写,太麻烦了
面向抽象父类的模板方法去调用,只需要去调一个模板方法即可,省略了很多代码
对于模板方法的理解
疑问:模板方法不加上final会怎样自己的理解:防止子类重写模板方法
模板方法(TemplateMethod)
在状态模式(State Pattern)中,类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式。
允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
打篮球的时候运动员可以有正常状态、不正常状态和超常状态。
行为随状态改变而改变的场景。
条件、分支语句的代替者。
我们将创建一个 State 接口和实现了 State 接口的实体状态类。Context 是一个带有某个状态的类。
在测试类中使用 Context 和状态对象来演示 Context 在状态改变时的行为变化。
状态接口(方法的参数为下述的内容类)
内容类(聚合状态接口,使用set对其进行赋值,使用get获取该对象)
状态的实现类(实现接口方法,同时利用传入的内容类设置其内聚的State对象(this))
先调用接口的实现类中的方法(传入内容类)
再通过内容类内聚的State接口,去调用具体的State中对应的其他方法
package pers.chenjiahao.designpattern.state;/** * @Author ChenJiahao(五条) * @Date 2021/8/27 17:26 */public interface State { public void doAction(Context context);}
State(状态接口)
package pers.chenjiahao.designpattern.state;/** * @Author ChenJiahao(五条) * @Date 2021/8/27 17:26 */public class Context { private State state; public Context() { state = null; } public State getState() { return state; } public void setState(State state) { this.state = state; }}
Context(内容类,聚合State)
package pers.chenjiahao.designpattern.state;/** * @Author ChenJiahao(五条) * @Date 2021/8/27 17:27 */public class StartState implements State { @Override public void doAction(Context context) { System.out.println(\"Player is in stop state\"); context.setState(this); } public String toString(){ return \"Stop State\"; }}
StartState
package pers.chenjiahao.designpattern.state;/** * @Author ChenJiahao(五条) * @Date 2021/8/27 17:28 */public class StopState implements State { @Override public void doAction(Context context) { System.out.println(\"Player is in start state\"); context.setState(this); } public String toString(){ return \"Start State\"; }}
StopState
State的实现类
package pers.chenjiahao.designpattern.state;/** * @Author ChenJiahao(五条) * @Date 2021/8/27 17:29 */public class Main { public static void main(String[] args) { Context context = new Context(); StartState startState = new StartState(); startState.doAction(context); System.out.println(context.getState().toString()); StopState stopState = new StopState(); stopState.doAction(context); System.out.println(context.getState().toString()); }}
状态模式(State)
解释器模式(Interpreter Pattern)提供了评估语言的语法或表达式的方式,它属于行为型模式。
这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等。
可利用场景比较少,JAVA 中如果碰到可以用 expression4J 代替。
我们将创建一个接口 Expression 和实现了 Expression 接口的实体类。
定义作为上下文中主要解释器的 TerminalExpression 类。
其他的类 OrExpression、AndExpression 用于创建组合式表达式。
测试类中使用 Expression 类创建规则和演示表达式的解析。
表达式接口
存储表达式数据的实现类
判断每个规则之间关系的(判断多个上方实现类的关系)
接口的实现类(分为两种实现类)
测试类中使用规则类去判断存储数据的实现类
具体的规则
package pers.chenjiahao.designpattern.interpreter;/** * @Author ChenJiahao(五条) * @Date 2021/8/27 14:42 */public interface Expression { public boolean interpret(String context);}
Expression(表达式接口)
package pers.chenjiahao.designpattern.interpreter;/** * @Author ChenJiahao(五条) * @Date 2021/8/27 16:36 */public class TerminalExpression implements Expression{ private String data; public TerminalExpression(String data) { this.data = data; } @Override public boolean interpret(String context) { if (context.contains(data)){ return true; } return false; }}
TerminalExpression
存储数据的
OrExpression
AndExpression
判断规则之间关系的
// 规则:Robert和John是男性 public static Expression getMaleExpression(){ Expression robert = new TerminalExpression(\"Robert\"); Expression john = new TerminalExpression(\"John\
规则
package pers.chenjiahao.designpattern.interpreter;/** * @Author ChenJiahao(五条) * @Date 2021/8/27 16:53 */public class Main { // 规则:Robert和John是男性 public static Expression getMaleExpression(){ Expression robert = new TerminalExpression(\"Robert\"); Expression john = new TerminalExpression(\"John\
完整测试代码
解释器(Interpreter)
23种设计模式大demo!!!
Design pattern
策略+工厂+模板方法(用状态模式也可以搞定)
spring的InitializingBean接口,有一个afterPropertiesSet方法这个方法可以在类加载完毕之后执行一个方法(目前来看最大的作用是将策略自动注册到策列接口的map中)
传入一个name作为判断值
if(\"zhangsan\".equals(name)){ sout(\"xxxx\");}
以往的if-else判断
将\"zhangsan\".equals(name)封装成一个策略Handler
将sout(\"xxxx\")作为策略中的方法(例如方法doSome(name))
创建一个工厂:工厂中含有一个策略Map和一个注册到Map的方法和一个获取Map的方法
去掉if-else的思路
Factory.getHandler(name).doSome(name);
改造完毕的代码
一个简单的例子
策略Map
获取Map的方法(具体的返回值为Handler,也可以返回Map,就是测试类中得多一步map.get(key)方法,没必要)
注册Handler到Map的方法
策略接口
策略实现类(自动注册到工厂的Map中,利用InitializingBean的afterPropertiesSet方法)
通过参数(传入的判断)调工厂的获取map方法,得到具体的handler,然后执行handler内的方法,实现原if中的方法
需要:核心思想(策略+工厂)
模板方法中只写throw new UnsupportedOperationException();
即可实现子类可以选择性的继承
引入模板方法(策略中的方法不是都想继承,每种策略响应的结果可能不同)
工厂:HandlerFactory
package pers.chenjiahao.self.handler;import org.springframework.beans.factory.InitializingBean;/** * @Author ChenJiahao * @Date 2021/8/2 0:42 */public interface Handler extends InitializingBean { void doSome();}
package pers.chenjiahao.self.handler;import org.springframework.stereotype.Component;import pers.chenjiahao.self.factory.HandlerFactory;/** * @Author ChenJiahao * @Date 2021/8/2 0:42 */@Componentpublic class AppleHandler implements Handler{ @Override public void doSome() { System.out.println(\"判断到了 Apple ~~~~~~\"); } @Override public void afterPropertiesSet() throws Exception { HandlerFactory.registerHandler(\"apple\
AppleHandler
package pers.chenjiahao.self.handler;import org.springframework.stereotype.Component;import pers.chenjiahao.self.factory.HandlerFactory;/** * @Author ChenJiahao * @Date 2021/8/2 0:42 */@Componentpublic class BananaHandler implements Handler { @Override public void doSome() { System.out.println(\"判断到了 Banana ~~~~~~\"); } @Override public void afterPropertiesSet() throws Exception { HandlerFactory.registerHandler(\"banana\
BananaHandler
策略实现类
Spring启动类
注:如果不想用Spring的话,普通的main方法中,在执行前先利用static{}将策略注册到HandlerFactory
实例1(策略+工厂)
注:这里将策略接口改为策略抽象类因为接口中不允许有方法体
将实例1中的Handler接口改为抽象AbstractHandler
定义两个不同的非抽象方法作为模板方法
每个模板方法中只的代码只有throw new UnsupportedOperationException();这行代码的目的是为了使子类可以随意的继承
package pers.chenjiahao.handler2;import org.springframework.beans.factory.InitializingBean;/** * 模板方法设计模式 */public abstract class AbstractHandler implements InitializingBean { public void AAA(String name) { throw new UnsupportedOperationException(); } public String BBB(String name) { throw new UnsupportedOperationException(); }}
AbstractHandler
不同的实现类可以实现AbstractHandler中不同的方法,以此达到处理不同返回的方式的目的
package pers.chenjiahao.handler2;import org.springframework.stereotype.Component;@Componentpublic class ZhangSanHandler extends AbstractHandler { @Override public String BBB(String name) { // 业务逻辑B return \"张三完成任务B\"; } @Override public void afterPropertiesSet() throws Exception { Factory2.register(\"张三\
例如ZhangSanHandler实现方法BBB
package pers.chenjiahao.handler2;import org.springframework.stereotype.Component;@Componentpublic class KangBaHandler extends AbstractHandler { @Override public void AAA(String name) { // 业务逻辑A System.out.println(\"亢八完成任务\"); } @Override public void afterPropertiesSet() throws Exception { Factory2.register(\"亢八\
例如KangBaHandler实现方法AAA
其余别的都一样
实例2(策略+工厂+模板)
这个例子中有个问题,就是关于这里使用的模板和我之前理解的模板有些不太一样,也可能是另外一种模板方式吧
由于现在有两种结果方法的调用,一套调用模板已经不能够满足了
能不能在AbstractHandler抽象类中再增加一个模板方法(例如doSome方法),判断调用的是方法AAA还是方法BBB,去调用不同的方法
测试类中就可以面向这个doSome方法去调用了
一套调用模板又可以满足了
关于实例2的设想
利用设计模式优雅的干掉if-else(所有设计模式中都可以引入Map)
Factory Method
Abstract Factory
Builder
Prototype
Singleton
创造型
Adapter/Wrapper
Bridge
Composite
Decorator
Facade
Flyweight
Proxy
结构型
Chain Of Responsibility
Command
Interpreter
Iterator
Mediator
Memento
State
Strategy
Template Method
Visitor
行为型
设计模式列表
可维护性(Maintainability)在修改功能时,需要改动的地方越少,可维护性就越好
可复用性(Reusability)代码可以被以后重复使用,写出自己总结的类库
可扩展性(Extensibility/Scalability)添加功能无需修改原来代码、少量修改
灵活性(flexibility/mobility/adaptability)代码接口可以灵活调用
指导思想
Single Resposibility Principle
一个类别太大、别太累,负责单一的职责
例如:Person、PersonManager
高内聚、低耦合
1.单一职责原则
Open-Closed Principle
对扩展开放,对修改关闭
尽量不修改原来代码的情况下进行扩展
抽象化和多态是开闭原则的关键
2.开闭原则
Liscov Substitution Principle
所有使用父类的地方,必须能够透明的使用子类对象
3.里氏替换原则
Dependency Inversion Principle
依赖抽象、而不是依赖具体
面向抽象编程
面向接口编程
4.依赖倒置原则
Interface Segregation Principle
每一个接口应该承担独立的角色,不干不该自己干的事儿
避免子类实现不需要实现的方法
需要对客户提供接口的时候,只需要暴露最小的接口
5.接口隔离原则
Law Of Demeter
核心:尽量不要和陌生人说话
对象本身this
以参数形式传入的对象
当前对象的成员对象
如果当前对象是一个集合,集合中的其他元素也都是朋友
当前对象所创建的对象
对于一个对象来说,非陌生人包括
好处:和其他类的耦合度变低
6.迪米特法则
六大设计原则
OCP
类的职责要单一
SRP
子类可以透明替换父类
LSP
DIP
接口的职责要单一
ISP
降低耦合
LOD
设计模式
java语言中的数组是一种引用数据类型,不属于基本数据类型,数组的父类是Object
数组实际上是一个容器,可以同时容纳多个元素,数组是一个数据的集合
数组:字面意思是一组数据
数组中可以存储\"基本数据类型\"的数据、也可以存储\"引用数据类型\"的数组
数组因为是引用类型,所以数组对象是在堆内存中存储的
数组当中如果存储的是\"java对象\"的话,实际上存储的是对象的引用地址
数组一旦创建,java中规定,长度不可变(数组的长度不可变)
数组的分类:一维数组、二维数组、三维数组、多维数组.... 一维数组常用,二维数组偶尔用
所有数组对象都有一个length属性(java自带的),用来获取数组中元素的个数
java中的数组要求数组中元素的类型统一,比如int类型数组只能存储int类型,Person类型数组只能存储Person类型例:购物袋中只能装苹果,不能同时装苹果和橘子
数组在内存方面存储的时候,数组中的元素内存地址是连续的,存储的每一个元素都是又规则的按着排列的,这是数组存储元素的特点,数组是一种简单的数据结构
所有的数字都是拿第一个小方框的内存地址作为整个数组对象的内存地址,数组中首元素的内存地址作为整个对象的内存地址
数组中每一个元素都是有下标的,以1递增,最后一个为length-1
查询检索某个下标的元素时,效率极高
每一个元素的内存地址在空间存储上是连续的
每个元素类型相同,所有占用空间大小一样
知道第一个元素的内存地址,知道每个元素的占用空间大小,又知道下标,所以通过一个数学表达式就可以得到任意个下标上的元素的内存地址
为什么检索效率高?
增删效率低
不能存储大数据量,因为很难在内存空间上找到一块特别大的连续的内存空间
int[] arr;
定义一维数组
int[] arr = new int[5]
初始化一维数组
数组的扩容
二维数组,同理一维数组
java.util.Arrays
所有的方法都是静态的,可以直接用类名调用
主要使用的是两个方法:二分法查找,排序
数组工具类
数组
char charAt(int index)
int compareTo(String anotherString)
用法\"hello\".contains(\"he\")
boolean contains(CharSequence s)
String concat(String str)
boolean endsWith(String suffix)
boolean equalsIgnoreCase(String anotherString)
byte[] getBytes() :将字符串对象转换成字节数组
int indexOf(String str)
int length()
String[] split(String regex)
boolean startsWith(String prefix)
String substring(int beginIndex)
char[] toCharArray():将字符串转换成char数组
String toLowerCase():将字符串转换为小写
String toUpperCase():将字符串转换为大写
String trim();
String.valueOf()
注:能够打印出来输出到控制台的都是String,因为pringln方法里调用了valueOf()方法,valueOf()方法中红调用了toString()方法
底层为byte[]数组,网Strinbuffer中放字符串,实际上是放到了byte数组当中了,(底层调用Arraycopy实现扩容)
初始化容量是16个byte[]数组
进行字符串拼接,如果byte数组满了,会自动调用Arraycopy进行扩容
Stringbuffer.append(String s)
在创建的时候尽可能给定一个初始化容量
最好减少底层数组的扩容次数,预估一下,给一个大一点的初始化容量
关键点:给一个合适的初始化容量,可以提高程序的执行效率
优化Stringbuffer的性能
StringBuffer
使用StringBuilder也可以完成字符串的拼接
StringBuilder中没有synchronized,是线程不安全的
StringBuffer中有synchronized,线程安全
StringBuilder和StringBuffer的区别
StringBuilder
String
java中为八种基本数据类型准备了八种包装类型
为什么引入包装类的概念,因为在一些方法需要参数的时候,需要传入引用类型,基本数据类型就无法满足了,所以引入了包装类
基本数据类型 包装类型byte java.lang.Byteshort java.lang.Shortint java.lang.Integer long java.lang.Longfloat java.lang.Float double java.lang.Doubleboolean java.lang.Booleanchar java.lang.Character
对照表
除了Boolean和Character的父类是Object,其余的父类先是Number再是Object
JDK1.5之后才有的
Integer x = 100;// 自动装箱 int 转为Integerint y = x;// 自动拆箱 Integer转为int
自动拆箱和自动装箱
在Integer类加载的时候会初始化整数型常量池中的256个对象
static String toBinaryString(int i) // 十进转二
toOctalString(int i) 十转八
toHexString(int i) 十转十六
在Integer中
int、Integer、String的相互转换
包装类
java.lang.reflect.*
通过java语言中的反射机制可以操作字节码文件(可以读和修改)
通过反射机制可以操作代码片段(class文件)
java.lang.Class:代表字节码文件(整个字节码,代表一个类型)
java.lang.reflect.Method:代表字节码中的方法字节码
java.lang.reflect.Constructor:代表字节码中的构造方法字节码
java.lang.reflect.Filed:代表字节码中的属性字节码
反射机制相关的类:(重要的类)
三种方式
Class c = Class.forName(\"完整类名带包名\")
Class c = 对象.getClass()
Class c = 类.class;
要操作一个类的字节码,需要首先获得到这个类的字节码
Class c = Class.forName(\"com.test.bean.User\"); // c代表User
User u = c.newInstance;
newInstance()方法会调用User这个类的无参构造方法
通过反射实例化对象
反射机制的灵活性:解耦、对扩展开放、对修改关闭
只想让一个类的静态代码块执行的时候可以使用Class.forName()Class.forName():这个方法会导致类加载,类加载的时候静态代码块执行
使用前提,文件必须在类路径下,类路径就是src下,src是类的根路径
String path = Thread.currentThread().getContextClassLoader().getResource(\".....\").getPath;
getContextClassLoader() 线程对象的方法,获取到当前线程类的加载器对象
以流的方式直接返回:InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(\"....\");
获取类路径的方法
java.utii包下
获取属性配置文件内信息的神器
只能绑定xxxx.properties文件
这个文件必须在类路径下
文件扩展名并须是.properties
在类路径时不能写扩展名
ResourceBundle bundle = ResourceBundle.getBundle(\"db\");String className = bundle.getString(\"className\")
资源绑定器
JDK自带的
类加载器就是专门负责加载类的工具。ClassLoader
启动加载器(父加载器):rt.jar中
扩展加载器(母加载器):ext/*.jar中
应用类加载器:classpath中
JDK中自带了三个类加载器
String s = \"abc\";代码在开始执行之前,会将所需要的类全部加载到JVM中,通过类加载器加载,看到以上代码类加载器会找String.class文件,找到就加载
一:通过\"启动类加载器\"加载 启动类加载器专门加载rt.jar,它是JDK最核心的类库
二:如果通过\"启动类加载器\"找不到时,就会启动\"扩展类加载器\"加载 扩展类加载器专门加载ext\\*.jar
三:如果通过\"扩展类加载器\"也加载不到时,会启动\"应用类加载器\"加载 应用类加载器专门加载classpath(环境变量)中的jar包(class文件)
具体加载过程
java为了保证类加载带安全,使用了双亲委派机制,优先从启动类中加载,如果无法加载,再去扩展类加载器中加载,称为双亲委派,如果害加载不到才会考虑去应用类加载器中加载,直到加载到为止
假设
类加载器描述
Filed翻译为字段,就是java中的属性
类.getName() 获取完整带包名称
类.getSimpleNmae() 获取简易类名
类.getFileds() 获取public修饰的Field
类.getDeclaredFiled() 获取所有的Field
Field.length 获取Field个数
Field.getModifiers(0 获取修饰符,返回的就是修饰符的代号
Modifier.toString(代号),将修饰符代号转为字符串
Field.getType()获取属性类型
Field.getName()获取属性名
Field.getType().getName() 获取类型完整名
Field.getType.getSimpleName() 获取类型简易名
创建一个StringBuilder来拼接字符换
Class c = Class.forName(\"pers.chenjiahao.daily.other.Student\"); StringBuilder sb = new StringBuilder(); sb.append(Modifier.toString(c.getModifiers()) + \" class \" + c.getSimpleName() + \"{\\"); Field[] fields = c.getDeclaredFields(); for (Field field : fields) { sb.append(\"\\t\"); sb.append(Modifier.toString(field.getModifiers())); sb.append(\" \"); sb.append(field.getType().getSimpleName()); sb.append(\" \"); sb.append(field.getName() + \";\"); sb.append(\"\\"); } sb.append(\"}\"); System.out.println(sb);
反射完整方法
反射属性Field
Class studentClass = Class.forName(\"com.test.bean.Student\");
Object obj = studentClass.newInstance();// obj就是Student对象
获取name属性Field nameField = studentClass.getDeclaredField(\"name\")
赋值三要素:obj对象,name属性,\"zhangsan\"值
读属性的值nameField.get(obj)
例如name属性加了private
需要打破封装,这样才可以访问私有属性
Field nameField = studentClass.getDeclaredField(\"name\");// 打破封装nameField.setAccessible(\"true\
访问私有属性
(反编译Field)通过反射机制访问一个java对象的属性
int... args 这就是可变长度参数
语法:类型... (注:一定是三个点)
可变长度参数要求参数个数是:0~N个
可变长度在参数列表中必须在最后一个位置,且可变长度参数只能有一个
可变长度参数可以当作一个数组来看待
可以传一个数组
可变长参数
类.getDeclaredMethods(); 获取所有的method
Methods.length 获取方法的长度(有几个方法)
method.getModifiers() 获取修饰符代号
Modifier.toString(代号) 代号转String
method.getReturnType() 获取方法的返回值类型
method.getName() 获取方法名
method.getParamterTypes() 获取参数列表
反射方法Method
反射机制调用方法
获取类:Class userServiceClass = Class.forName(\"com.test.service.UserService\")
创建对象:Object obj = userServiceClass.newInstance();
获取method:Method loginMethod = userServiceClass.getDeclaredMethod(\"login\
反编译Method
根Field和Method类似
Class c = Class.forName(\"pers.chenjiahao.daily.other.Student\");
直接调用c.newInstance()
无参
Object newObj = con.newInstance(\"jack\
有参
反编译构造方法
Class stringClass = class.forName(\"java.lang.String\");
获取父:Class superClass = stringClass.getSuperclass();
获取所有接口:Class[] interfaces = stringClass.getInterfaces()
通过反射来获取这个类的父类,以及实现了哪些接口
让代码更加通用
可变化的内容都写在配置文件中,将来修改配置文件后,创建的对象不一样了,调用的方法也不同了,但是java代码不需要做任何改动
反射
进程(Process)是计算机中的程序关于某数据集合上的依次运行活动,是操作系统进行资源分配与调度的基本单位。
可以把进程简单的理解为正在操作系统中运行的一个程序
进程
线程(thread)是进程的一个执行单元,一个线程就是进程中一个单一顺序的控制流,线程就是进程的一个执行分支
进程是线程的容器,一个进程至少有一个线程,一个进程中也可以有多个线程
在操作系统中是以进程为单位分配资源的,例如:虚拟存储空间,文件扫描符等,每个线程都有各自的线程栈,都有自己的寄存器环境,都有自己的线程本地存储
线程
JVM启动时会创建一个主线程,该主线程负责执行main方法,简单来说主线程就是运行main方法的线程
Java中的线程不是孤立的,线程之间也存在一些联系,如果在A线程中创建了B线程,称B线程是A线程的子线程,相应的A线程就是B线程的父线程,线程之间的关系都是相对的
主线程与子线程
假设有三个任务:任务A准备5分钟,等待10分钟任务B准备2分钟,等待8分钟任务C准备10分钟
串行(Sequential),先做任务A,完成后再左任务B,完成后在做任务C,所有的任务逐个完成,共耗时:15 + 10 + 10 = 35 分钟任务A -> 任务B -> 任务C
串行
并发(Concurrent),先做任务A,准备了5分钟后,在等待A完成的这段时间就开始做任务B,任务B准备了2分钟,在等待B完成的过程中开始做任务C,10分钟后任务C结束,共耗时:5 + 2 + 10 = 17分钟任务A 任务B 任务C
并发
并行(Parallel),三个任务同时开始,总耗时取决于需要时间最长的那个任务总耗时:15分钟任务A 任务B 任务C
并行
并发可以提高对事物的处理效率,即一段时间内可以处理或者完成更多的事情
并行是一种更为严格,理想的并发
从硬件角度来说:如果单核CPU,一个处理器依次只能执行一个线程的情况下,处理器可以使用时间片轮转技术,可以让CPU快读的在各个线程之间进行切换,对于用户来说,感觉是三个线程在同时执行如果多核CPU,可以为不同的线程分配不同的CPU内核
串行、并发、并行
相关概念
在Java中创建一个线程就是创建一个Thread类(子类)的对象(实例)
1.定义Thread类的子类
2.定义一个Runnable接口的实现类
3.实现Callable<T>接口
1和2这两种创建线程的方式没有本质的区别
有时候一个类已经有父类的,就不能再继承Thread了,所以只能通过实现Runnable来实现线程
既然1和2本质是一样的,为什么会有这两种方法?
Thread thread = new MyThread();thread.start()
创建继承Thread的线程
Runnable runnable = new MyRunnable();Thread thread = new Thread(runnable);thread.start()因为有构造方法new Thread(Runnable runnable)
创建实现Runnable的线程
Thread thread2 = new Thread(new Runnable() { @Override public void run() { for (int i = 1; i <= 100; i++) { System.out.println(\"sub thread2 -->\" + i); } }}); thread2.start();
有时调用Thread(Runnable)构造方法时,实参也会传递匿名内部类对象
Thread类有两个常用的构造方法:Thread()与Thread(Runnable),对应的创建线程的两种方式
创建与启动
Thread.currentTuread()可以获取当前线程
Java中的任何一段代码都是执行在某个线程当中的,执行当前代码的线程就是当前线程
t1.start(); // 当前线程是Thread-0t1.run(); // 当前线程是main
1.currentThread()
thread.setName(线程名称) // 设置线程名称thread.getName() // 返回线程名称
通过设置线程名称,有助于程序调试,提高程序的可读性,建议为每个线程都设置一个能够体现线程功能的名称
2.setName()/getName()
thread.isAlive()判断当前线程是否处于活动状态
活动状态就是线程已开启并且尚未终止
3.isAlive()
让当前线程休眠指定的毫秒数
当前线程是指Thread.currentThread()
package pers.chenjiahao.threadmethod.p3sleep;/** * 使用线程休眠Thread.sleep完成一个简易的计时器 */public class SimpleTimer { public static void main(String[] args) { int remaining = 60; // 从60s开始计时 // 读取main方法的参数 if (args.length > 0) remaining = Integer.parseInt(args[0]); while (remaining >= 0){ System.out.println(\"Remaining:\" + remaining--); try { Thread.sleep(1000); // 线程休眠 } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(\"Done!!!\"); }}
利用sleep设计一个倒计时器
4.sleep(millis)
thread.getId()可以获得线程的唯一标识
某个编号的线程运行结束后,该编号可能被后续创建的线程使用
重启了JVM后,同一个线程的编号可能不一样
5.getId()
Thread.yield()方法的作用是放弃当前的CPU资源
6.yield()
thread.setPriority(num) // 设置线程的优先级
Java线程的优先级取值范围是1~10,如果超出这个范围会抛出异常IllegalArgumentException
在操作系统中,优先级较高的线程被先执行的概率高
线程优先级本质上只是给线程调度器一个提示信息,以便线程调度器决定先调度那些线程
注意:不能保证优先级高的线程先运行
Java有现成设置不当或滥用可能会导致某些线程永远无法得到运行,即产生了线程饥饿
线程的优先级具有继承性,例:在A线程中创建了B线程,则B线程的优先级与A线程一样
7.setPriority()
中断线程,注意调用interrupt()方法仅仅是在当前线程打一个停止标志,并不是真正的停止线程
如果想彻底终止线程,可以通过判断中段标志来判断,线程有 isInterrupted(),该方法返回线程的中断标志
8.interrupt()
Java中的线程分为用户线程与守护线程
守护线程是为其他线程提供服务的线程,如垃圾回收器(GC)就是一个典型的守护线程
守护线程不能单独运行,当JVM中没有其他用户线程,只有守护线程时,守护线程会自动销毁,JVM会退出
9.setDaemon()
线程的生命周期是线程对象的生老病死,即线程的状态
状态图
生命周期
1.提高系统的吞吐率(Throughout).多线程编程可以使一个进程有多个并发(concurrent,即同时运行的)的操作
2.提高响应性(Responsiveness).Web服务器会采用一些专门的线程负责用户的请求处理,缩短了用户的等待时间
3.重复利用多核(Multicore)处理器资源.通过多线程可以充分的利用CPU资源
优势
1.线程安全(Thread safe)问题,多线程共享数据时,如果没有采取正确的并发访问控制措施,就可能会产生数据一致性问题,如读取脏数据(过期的数据),如丢失数据更新
(1)死锁(Deadlock):类似鹬蚌相争
(2)锁死(Lockout):类似于睡美人故事中如果王子死了,睡美人活着但是醒不来了
(3)活锁(Livelock):类似于小猫咬自己尾巴
(4)饥饿(Starvation):类似于见状的雏鸟总是从母鸟嘴中抢到食物
2.线程活性(Thread liveness)问题,由于程序自身的缺陷或者有资源稀缺性导致线程一直处于非RUNNABLE状态,这就是线程活性问题,常见的活性故障有以下几种
3.上下文切换(Context Switch):处理器从执行一个线程切换到执行另外一个线程
4.可靠性:可能会有一个线程导致JVM意外终止,其他的线程也无法执行了
问题与风险
多线程编程的优势与存储的风险
线程概述
非线程安全主要是指多个线程对同一个对象的实例变量进行操作时,会出现值被更改,值不同步的情况
线程安全问题表现为三个方面:原子性、可见性、有序性
原子(Atomic)就是不可分割的意思,原子操作的不可分割有两层含义 (1)访问(读、写)某个共享变量的操作从其他线程来看,该操作要么已经执行完毕, 要么尚未发生,即其他线程看不到当前操作的中间结果 (2)访问同一组共享变量的原子操作是不能够交错的 如现实生活中从ATM机取款,对于用户来说, 要么操作成功,用户拿到钱,余额减少,增加一条交易记录 要么没拿到钱,相当于取款操作没有发生
锁具有排它性,保证共享变量在某一时刻只能被一个线程访问
一种是锁
CAS之类直接在硬件(处理器和内存)层次上实现,看作是硬件锁
一种是利用处理器的CAS(Compare and Swap)指令
Java有两种方式实现原子性
在Java中提供了一个线程安全的AtomicInteger类(这个类继承了Number),保证了操作的原子性
原子性
在多线程环境中,一个线程对某个共享变量更新之后,后续线程可能无法立即读取到这个更新后的结果,这就是线程安全问题的另外一种形式:可见性(visibility),就是其他线程可能会读取到旧数据
如果一个线程对共享变量更新后,后续访问该变量的其他线程可以读到更新的结果,称这个线程对共享变量的更新对其他线程可见,否则成这个线程对共享变量的更新对其他线程不可见
多线程程序因为可见性问题可能会导致其他线程读取到了旧数据(脏数据)
可见性
有序性(Ordering)是指在什么情况下一个处理器上运行的一个线程所执行的内存访问操作在另外一个处理器上运行的其他线程看来是乱序的(Out of Order).
乱序是指内存访问操作顺序看起来发生了变化
在多核处理器的环境下,编写的顺序结构,这种操作执行的顺序可能是没有保障的: 编译器可能会改变两个操作的先后顺序; 处理器也可能不会按照目标代码的顺序执行; 这种一个处理器上执行的多个操作,在其他处理器来看它的顺序与目标代码指定的顺序可能不一样,这种现象称为重排序
重排序是对内存访问有序操作的一种优化,可以在不影响单线程程序正确的情况下提升程序的性能,但是,可能对多线程程序的正确性产生影响,即可能导致线程安全问题(单线程中没问题。多线程中可能有问题)
重排序与可见性问题类似,不是必然出现的
源代码顺序:就是源码中指定的内存访问顺序
程序顺序:处理器上运行的目标代码所指定的内存访问顺序
执行顺序:内存访问操作在处理器上的实际执行顺序
感知顺序:给定处理器所感知到的该处理器及其他处理器的内存访问操作的顺序
与内存操作顺序有关的几个概念
在源码顺序与程序顺序不一致,或程序顺序与执行顺序不一致的情况下,我们就说发生了指令重排序(Instruction Reorder)
指令重排是一种动作,确实对指令的顺序做了调整,重排序的对象指令
javac编译器一般不会执行指令重排序,而JIT编译器可能会指令重排序
处理器也可能执行指令重排序,使得执行顺序与程序顺序不一致
指令重排不会对单线程程序的结果正确性产生影响,但是可能会导致多线程程序出现非预期的结果
指令重排序
是由高速缓存,写缓冲器引起的,感知顺序与执行顺序不一致
存储子系统是指写缓冲器与高速缓存
高速缓存(Cache)是CPU中为了匹配与主内存处理速度不匹配而设计的
写缓冲器(Store buffer、Write buffer)用来提高写高速缓存操作的效率
即使处理器严格按照程序顺序执行两个内存访问操作,在存储子系统的作用下,其他处理器对这两个操作的感知顺序与程序顺序不一致,即这两个操作的执行顺序看起来像是发生了变化,这种现象成为存储子系统重排序
存储子系统重排序并没有真正的对指令执行顺序进行调整,而是造成一种指令执行顺序被调整的假象
存储子系统重排序对象是内存操作的结果
存储子系统重排序
可以把重排序分为两种
重排序
LoadLoad重排序:在一个处理器上先后执行两个读操作L1,L2,其他处理器对这两个内存操作的感知顺序可能是L2,L1
StoreStore重排序:在一个处理器上先后执行两个读操作W1,W2,其他处理器对这两个内存操作的感知顺序可能是W2,W1
LoadStore重排序:在一个处理器上先后执行读内存操作L1再执行写内存操作W1,其他处理器对这两个内存操作的感知顺序可能是W1,L1
StoreLoad重排序:在一个处理器上先后执行写内存操作W1再执行读内存操作L1,其他处理器对这两个内存操作的感知顺序可能是L1,W1
内存重排序与具体的处理器微架构有关,不同架构的处理器所允许的内存重排序不同
内存重排序可能会导致线程安全问题 假设有两个共享变量 int data = 0; boolean ready = false; 处理器1 处理器2 data = 1; ready = true; while(!ready){ sout(date); }存在问题:如果重排序,处理器2可能读取不到处理器1修改后的ready的值,导致一致死循环
从处理器角度来看,读内存就是从指定的RAM地址中加载数据到寄存器,称为Load操作; 写内存就是把数据存储到指定的地址表示的RAM存储单元中,称为Store操作, 内存重排序的四种可能
JIT编译器、处理器、存储子系统是按照一定的规则对指令或内存操作的结果进行重排序,给单线程程序造成一种假象----指令是按照源码的顺序执行的,这种假象称为貌似串行语义
只能保证单线程重排序后的结果的正确性,不能保证多线程环境程序的正确性
存在依赖关系例如: x = 1 ; y = x + 1; // 后一条语句的操作数包含前一条语句的执行结果; y = x ; x = 1; // 先读取x变量,再更新x变量的值 x = 1 ; x = 2; // 两条语句同时对一个变量进行写操作
如果不存再数据依赖关系则可能重排序,如: double price = 45.8; // 1 int quantity = 10; // 2 double sum = price * quantity; // 3 语句1和语句2可能会重排 语句3不会重排
存在控制依赖关系的语句允许重排,一条语句(指令)的执行结果会影响另一条语句(指令)能否被执行,这两条语句(指令)存在控制依赖关系(Control Dependency),如再if语句中允许重排,可能存在处理器先执行if代码块,再判断if条件是否成立
为了保证貌似串行语义,有数据依赖关系的语句不会被重排序,只有不存在数据依赖关系的语句才会被重排序,如果两个操作(指令)访问同一个变量,且其中一个操作(指令)为写操作,那么这两个操作之间就存在数据依赖关系(Data dependency)
貌似串行语义
可以使用volatile关键字和synchronized关键字实现有序性
保证内存访问的顺序性
有序性
内存模型图
1.每个线程都有独立的栈空间
2.每个线程都可以访问堆内存
3.计算机的CPU不直接从主内存中读取数据,CPU读取数据时, 把主内存的数据读到Cache缓存中,再把Cache中的数据读取到Register寄存器中, CPU从Register中读取
4.JVM中共享的数据可能会被分配到Register寄存器中,每个CPU都有自己的Register寄存器, 一个CPU不能读取其他CPU寄存器中的内容。如果两个线程分别运行在不同的处理器(CPU上), 而这个共享的数据被分配到寄存器上,会产生可见性问题
5.即使JVM中的共享数据分配到主内存中,也不能保证数据的可见性,CPU不直接对主内存访问, 而是通过Cache高速缓存进行的,一个处理器上运行的线程对数据的更新可能只是更新到处理器的写缓冲器(Store Buffer), 还没有到达Cache缓存,更不用说主内存了,另外一个处理器不能读取到该处理器写缓冲器上的内容, 会山城运行在另外一个处理器上的线程无法看到该处理器对共享数据的更新
6.一个处理器的Cache不能直接读取另外一个处理器的Cache,但是一个处理器可以通过缓存一致性协议(Cache Coherence Protocol)来读取其他处理器缓存中的数据,并将读取的数据更新到该处理器的Cache中,这个过程称为缓存同步。缓存同步使得一个处理器上运行的线程可以读取到另外一个处理器上运行的线程对共享数据所做的更新,即保障了可见性,为了保障可见性,必须使一个处理器对共享数据的更新最终被写入该处理器的Cache,这个过程称为冲刷处理器缓存
解释
完整内存模型
抽象内存模型图
1.每个线程之间的共享数据都存储在主内存中
2.每个线程都有一个私有的本地内存(工作内存),线程的工作内存是抽象的概念, 不是真实存在的,它涵盖写缓冲器,寄存器,其他硬件的优化
3.每个线程从主内存中把数据读取到本地工作内存中,在工作内存中保存共享数据的副本
4.线程在自己的工作内存中处理数据,仅对当前线程可见,对其他线程是不可见的
抽象内存模型
Java内存模型
线程安全问题
线程同步机制是一套用于协调线程之间的数据访问的机制,该机制可以保障线程安全
锁
volatile关键字
final关键字
static关键字
Object.wait()
Object.notifiy()
等等
Java平台提供的线程同步机制包括
线程同步机制
线程安全问题的产生前提是多个线程、并发访问、共享数据
将多个线程对共享数据的并发访问转换为串行访问,即一个共享数据一次只能被一个线程访问,锁就是用这种思路来保障线程安全的
锁(Lock)可以理解为对共享数据进行保护的许可证,对于同一个许可证保护的共享数据来说,任何线程想要访问这些共享数据,必须先持有该许可证,一个线程只有在持有许可证的情况下才能对这些共享数据进行访问,并且一个许可证一次只能被一个线程持有,持有许可证的线程在结束对共享数据的访问后必须释放其持有的许可证
一个线程在访问共享数据前必须先获得锁;获得锁的线程称为:锁的持有线程;一个锁子一次只能被一个线程持有,锁的持有线程在获得锁之后和释放锁之前这段时间所执行的代码称为临界区(Critical Section)
锁的执行流程
内部锁是通过synchronized关键字实现
显示锁是通过java.concurrent.locks.Lock接口的实现类实现的
JVM把锁分为内部锁和显示锁两种
锁可以实现对共享数据的安全访问,锁可以保障线程的原子性、可见性、有序性
锁是通过互斥保障原子性的,一个锁只能被一个线程持有,这就保证了临界区的代码一次只能被一个线程执行,使得临界区代码所执行的操作自然而然的具有不可分割的特性,即具备了原子性
可见性的保障是通过写线程冲刷处理器的缓存和读线程刷新处理器缓存的这两个动作实现的。在Java平台中,锁的获得隐含着刷新处理器缓存的动作,锁的释放隐含着冲刷处理器缓存的动作(获得锁的时候会刷新,释放锁的时候会冲刷)
锁能保障有序性,写线程在临界区所指向的在读线程所执行的临界区看来像是完全按照源码顺序执行的
注意:使用锁来保障线程的安全性,必须满足以下条件: 1.这些线程在访问共享数据时必须使用同一个锁 2.即使是读取共享数据的线程也需要使用同步锁
锁的作用
可重入性(Reentrancy)描述这样一个问题:一个线程持有该锁的时候能否再次(多次)申请该锁
void methodA(){ 申请a锁 methodB(); 释放a锁}void methodB(){ 申请a锁 。。。。 释放a锁}
如果一个线程持有一个锁的时候还能够继续成功申请该锁,称该锁为可重入的,否则就称该锁子为不可重入的
可重入性
Java平台中内部锁属于非公平锁,而显示Lock锁既支持公平锁又支持非公平锁
锁的争用与调度
一个锁可以保护的共享数据的数量大小称为锁的粒度
锁保护共享数据量大,称该锁的粒度粗,否则就成该锁的粒度细
锁的粒度过粗会导致线程在申请锁的时候会进行不必要的等待,锁的粒度过细会增加锁调度的开销
例:一个银行柜台啥都能办,那么所有业务的人都去这里办理了,那么等待的时间就会长; 如果开户一个窗口,销户一个窗口,资料变更一个窗口,原来一个窗口的事,现在需要三个窗口,增加了开销
锁的粒度
锁相关的概念
Java中的每个对象都有一个与之关联的内部锁(Intrinsic lock),这种锁也称为监视器(Monitor),这种内部锁是一种排他锁,可以保障原子性,可见性与有序性
内部锁是通过synchronized关键字实现的,synchronized关键字修饰代码块,修饰方法
修饰代码块的语法: synchronized(对象锁){ 同步代码块,可以在同步代码块中访问共享数据 }
修饰实例方法就称为同步实例方法
package pers.chenjiahao.intrinsiclock;/** * synchronized同步代码块 * @Author ChenJiahao * @Date 2021/2/5 15:17 */public class Test01 { public static void main(String[] args) { // 创建两个线程,分别调用mm()方法 // 先创建Test01对象,通过对象名调用mm()方法 Test01 obj = new Test01(); new Thread(new Runnable() { @Override public void run() { obj.mm(); // 使用的锁对象this就是obj对象 } }).start(); new Thread(new Runnable() { @Override public void run() { obj.mm(); // 使用的锁对象this就是obj对象 } }).start(); } // 定义方法,打印100行字符串 public void mm(){ synchronized (this) { // 经常使用this当前对象作为锁对象 for (int i = 1; i <= 100; i++) { System.out.println(Thread.currentThread().getName() + \" --> \" + i); } } }}
经常使用this当前对象作为锁对象
package pers.chenjiahao.intrinsiclock;/** * synchronized同步代码块 * 一个对象一把锁,两个对象两把锁,无法实现同步 * 想同步,必须使用同一个锁对象 * @Author ChenJiahao * @Date 2021/2/5 15:17 */public class Test02 { public static void main(String[] args) { // 创建两个线程,分别调用mm()方法 Test02 obj = new Test02(); Test02 obj2 = new Test02(); new Thread(new Runnable() { @Override public void run() { obj.mm(); // 使用的锁对象this就是obj对象 } }).start(); new Thread(new Runnable() { @Override public void run() { obj2.mm(); // 使用的锁对象this就是obj2对象 } }).start(); } // 定义方法,打印100行字符串 public void mm(){ synchronized (this) { // 经常使用this当前对象作为锁对象 for (int i = 1; i <= 100; i++) { System.out.println(Thread.currentThread().getName() + \" --> \" + i); } } }}
一个对象一把锁,两个对象两把锁,无法实现同步,如果想同步,必须使用同一个锁对象,(这个例子没有实现同步)
package pers.chenjiahao.intrinsiclock;/** * synchronized同步代码块 * 使用常量作为锁对象 * @Author ChenJiahao * @Date 2021/2/5 15:17 */public class Test03 { public static void main(String[] args) { // 创建两个线程,分别调用mm()方法 Test03 obj = new Test03(); Test03 obj2 = new Test03(); new Thread(new Runnable() { @Override public void run() { obj.mm(); // 使用的锁是OBJ常量 } }).start(); new Thread(new Runnable() { @Override public void run() { obj2.mm(); // 使用的锁是OBJ常量 } }).start(); } public static final Object OBJ = new Object(); // 定义一个常量 // 定义方法,打印100行字符串 public void mm(){ synchronized (OBJ) { // 使用常量对象作为锁对象 for (int i = 1; i <= 100; i++) { System.out.println(Thread.currentThread().getName() + \" --> \" + i); } } }}
使用常量作为锁对象
package pers.chenjiahao.intrinsiclock;/** * synchronized同步代码块 * 只要是同一个锁对象就可以同步 * @Author ChenJiahao * @Date 2021/2/5 15:17 */public class Test04 { public static void main(String[] args) { // 创建两个线程,分别调用mm()方法 Test04 obj = new Test04(); Test04 obj2 = new Test04(); new Thread(new Runnable() { @Override public void run() { obj.mm(); // 使用的锁对象是OBJ常量 } }).start(); new Thread(new Runnable() { @Override public void run() { obj2.mm(); // 使用的锁对象是OBJ常量 } }).start(); new Thread(new Runnable() { @Override public void run() { sm(); } }).start(); } public static final Object OBJ = new Object(); // 定义一个常量 // 定义方法,打印100行字符串 public void mm(){ synchronized (OBJ) { // 使用常量对象作为锁对象 for (int i = 1; i <= 100; i++) { System.out.println(\"mm\" + Thread.currentThread().getName() + \" --> \" + i); } } } // 定义静态方法,打印100行字符串 public static void sm(){ synchronized (OBJ) { // 使用常量对象作为锁对象 for (int i = 1; i <= 100; i++) { System.out.println(\"sm\" + Thread.currentThread().getName() + \" --> \" + i); } } }}
只要是同一个锁对象就可以同步
synchronized同步代码块
package pers.chenjiahao.intrinsiclock;/** * synchronized同步实例方法 * 把整个方法体作为同步代码块 * 默认的锁对象是this * @Author ChenJiahao * @Date 2021/2/5 15:17 */public class Test05 { public static void main(String[] args) { // 先创建Test01对象,通过对象名调用mm()方法 Test05 obj = new Test05(); // 一个线程调用mm方法 new Thread(new Runnable() { @Override public void run() { obj.mm(); // 使用的锁对象this就是obj对象 } }).start(); // 一个线程调用mm22方法 new Thread(new Runnable() { @Override public void run() { obj.mm22(); // 使用的锁对象this就是obj对象 // new Test05().mm22(); // 这样就同步不了了,不是同一个锁对象 } }).start(); } // 定义方法,打印100行字符串 public void mm(){ synchronized (this) { // 经常使用this当前对象作为锁对象 for (int i = 1; i <= 100; i++) { System.out.println(\"mm \" + Thread.currentThread().getName() + \" --> \" + i); } } } // 使用synchronized修饰实例方法,同步实例方法,默认的锁对象是this public synchronized void mm22(){ for (int i = 1; i <= 100; i++) { System.out.println(\"mm22 \" + Thread.currentThread().getName() + \" --> \" + i); } }}
synchronized同步实例方法把整个方法体作为同步代码块默认的锁对象是this
同步方法锁的粒度粗,执行效率低同步代码块锁的粒度细,执行效率高
package pers.chenjiahao.intrinsiclock;/** * 同步方法与同步代码块如何选择 * 同步方法锁的粒度粗,执行效率低 * 同步代码块锁的粒度细,执行效率高 * @Author ChenJiahao * @Date 2021/2/6 13:43 */public class Test07 { public static void main(String[] args) { Test07 obj = new Test07(); new Thread(new Runnable() { @Override public void run() { obj.doLongTimeTask(); } }).start(); new Thread(new Runnable() { @Override public void run() { obj.doLongTimeTask(); } }).start(); } // 同步方法,锁的粒度粗,执行效率低 public synchronized void doLongTimeTask(){ System.out.println(\"Task Begin\"); try { Thread.sleep(3000); // 模拟任务需要准备3秒钟 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(\"开始同步\"); for (int i = 1; i <= 100; i++) { System.out.println(Thread.currentThread().getName() + \" --> \" + i); } System.out.println(\"Task End\"); } // 同步代码块,锁的粒度细,执行效率高 public void doLongTimeTask2(){ System.out.println(\"Task Begin\"); try { Thread.sleep(3000); // 模拟任务需要准备3秒钟 } catch (InterruptedException e) { e.printStackTrace(); } synchronized (this){ System.out.println(\"开始同步\"); for (int i = 1; i <= 100; i++) { System.out.println(Thread.currentThread().getName() + \" --> \" + i); } System.out.println(\"Task End\"); } }}
同步方法与同步代码块如何选择
同步方法
package pers.chenjiahao.intrinsiclock;/** * 脏读 * 出现读取属性值出现了一些意外,读取的是中间值,而不是修改之后的值 * 出现脏读的原因是 对共享数据的修改与对共享数据的读取不同步 * 解决方法:给getValue和setValue加上synchronized就可以同步了 * @Author ChenJiahao * @Date 2021/2/6 13:56 */public class Test08 { public static void main(String[] args) { // 开启子线程,设置用户名和密码 PublicValue publicValue = new PublicValue(); SubThread t1 = new SubThread(publicValue); t1.start(); try { // 为了确定设置成功 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 在main线程中读取用户名,密码 publicValue.getValue(); } // 定义线程,设置用户名和密码 static class SubThread extends Thread{ private PublicValue publicValue; public SubThread(PublicValue publicValue) { this.publicValue = publicValue; } @Override public void run() { publicValue.setValue(\"bjpowernode\
出现读取属性值出现了一些意外,读取的是中间值,而不是修改之后的值,出现脏读的原因是对共享数据的修改与对共享数据的读取不同步
脏读
package pers.chenjiahao.intrinsiclock;/** * 同步过程中线程出现异常,会自动释放锁对象 * * @Author ChenJiahao * @Date 2021/2/5 15:17 */public class Test09 { public static void main(String[] args) { // 先创建Test01对象,通过对象名调用mm()方法 Test09 obj = new Test09(); // 一个线程调用mm方法 new Thread(new Runnable() { @Override public void run() { obj.m1(); // 使用的锁对象是Test06.class } }).start(); // 一个线程调用mm22方法 new Thread(new Runnable() { @Override public void run() { sm22(); // 使用的锁对象是Test06.class } }).start(); } // 定义方法,打印100行字符串 public void m1(){ // 使用当前类的运行时类对象作为锁对象,可以简单的理解为把Test06类的字节码文件作为锁对象 synchronized (Test09.class) { for (int i = 1; i <= 100; i++) { System.out.println(\"mm \" + Thread.currentThread().getName() + \" --> \" + i); if (i == 50) { Integer.parseInt(\"abc\"); // 创造一个异常 } } } } // 使用synchronized修饰静态方法,同步静态方法,默认当前运行时类Test06.class作为锁对象 public synchronized static void sm22(){ for (int i = 1; i <= 100; i++) { System.out.println(\"mm22 \" + Thread.currentThread().getName() + \" --> \" + i); } }}
同步过程中线程出现异常,会自动释放锁对象
在多线程程序中,同步时可能需要使用多个锁,如果获得锁的顺序不一致,可能会导致死锁
相当于鹬蚌相争
避免死锁:当需要获得多个锁时,所有线程获得锁的顺序保持一致即可
死锁
内部锁:synchronized关键字
volatile关键的作用使变量在多个线程之间可见,volatile的作用可以强制线程从公共内存(主内存)中读取变量的值,而不是从工作内存中读取
1.volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好; volatile只能修饰变量,而synchronized还可以修饰方法、代码块。随着JDK新版本的发布, synchronized的执行效率也有较大的提升,在开发中使用synchronized的比率还是很大的
2.多线程访问volatile变量不会发生阻塞,而synchronized可能会阻塞
3.volatile能保证数据的可见性,但是不能保证原子性;而synchornized可以保证原子性,也可以保证可见性
4.关键字volatile解决的是变量在多个线程之间的可见性;synchornized关键字解决多个线程之间访问公共资源的同步性
volatile与synchronized比较
volatile的作用
volatile关键字增加了实例变量在多个线程之间的可见性,但是不具备原子性
10个线程对同一个数字进行累加,每次输出应该都是1000的倍数,因为volatile无法保证原子性,所以输出的结果有些不会是1000的倍数
package pers.chenjiahao.volatilekw;/** * volatile不具备原子性 * @Author ChenJiahao * @Date 2021/2/6 16:08 */public class Test03 { public static void main(String[] args) { // 在main线程中创建10个子线程 for (int i = 0; i < 10; i++) { new MyThread().start(); } } static class MyThread extends Thread{ // volatile关键字仅仅是表示所有线程从主内存中读取count变量的值 // volatile public static int count; // 给addCount()加上synchronized之后没有必要再加volatile了 public static int count; /* 这段代码运行后不是线程安全的,想要线程安全,需要使用synchronized进行同步 public static void addCount(){ for (int i = 0; i < 1000; i++) { // count++不是原子操作 count++; } System.out.println(Thread.currentThread().getName() + \" count= \" + count); }*/ public synchronized static void addCount(){ for (int i = 0; i < 1000; i++) { // count++不是原子操作 count++; } System.out.println(Thread.currentThread().getName() + \" count= \" + count); } @Override public void run() { addCount(); } }}
代码(证明volatile不具备原子性)
volatile非原子特性
原来是用++或者--来进行自增和自减的,在多线程中多用原子类来进行自增和自减
原子类能够保障原子性是因为CAS
我们知道i++操作不是原子操作,除了使用synchronized进行同步外,也可以使用AtomicInteger、AtomicLong原子类进行实现
private static AtomicInteger count = new AtomicInteger();
count.getAndIncrement(); // 相当于count++count.incrementAndGet(); // 相当于++count
使用AtomicInteger进行自增
package pers.chenjiahao.volatilekw;import java.util.concurrent.atomic.AtomicInteger;/** * 使用原子类进行自增 * @Author ChenJiahao * @Date 2021/2/6 16:08 */public class Test04 { public static void main(String[] args) { // 在main线程中创建10个子线程 for (int i = 0; i < 100; i++) { new MyThread().start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(MyThread.count.get()); } static class MyThread extends Thread{ // 使用AtomicInteger对象 private static AtomicInteger count = new AtomicInteger(); public static void addCount(){ for (int i = 0; i < 1000; i++) { count.getAndIncrement(); // 相当于count++ // count.incrementAndGet(); // 相当于++count } System.out.println(Thread.currentThread().getName() + \" count= \" + count.get()); } @Override public void run() { addCount(); } }}
常用的原子类进行自增自减操作
轻量级同步机制:volatile关键字
CAS(Compare And Swap)是由硬件实现的
CAS可以将read-modify-write这类的操作转换为原子操作
i++自增操作包括三个子操作: 第一步:从主内存读取i变量值 第二步:对i的值加1 第三步:再把加1之后的值保存到主内存中CAS可以将这三个操作转换为一个操作
CAS原理:在把数据更新到主内存时,再次读取主内存变量的值,如果现在变量的值与期望的值(操作起始时读取的值)一样,就更新。 通俗来说:第三步更新之前再次获取主内存中变量的值,若跟第一步得到的一样(主内存中的初始值别的线程没动过),就更新(可能会有ABA问题) 如果值不同,所有操作撤销,可能会重做
理想状态下的执行顺序
顺序不是理想(导致结果有问题)
CAS保障原子性,顺序不对可以撤销,不会提交
CAS实现线程安全的计数器
CAS实现原子操作背后的一个假设,共享变量的当前值与当前线程提供的期望值相同,就认为这个变量没有被其他线程修改过<br>
实际上这种加假设不一定总是成立的,如有共享变量count = 0 A线程对count值修改为10 B线程对count值修改为20 C线程对count值修改为0当前线程看到count变量的值现在是0,现在是否认为count变量的值没有被其他线程更新呢?这种结果是否能接受?这就是CAS中的ABA问题,即共享变量经历了A -> B -> A的更新是否能接受ABA问题跟实现的算法有关
问题的解决
CAS中的ABA问题
CAS
原子变量类是基于CAS实现的,当对共享变量进行read-modify-write更新操作时,通过原子变量类可以保障操作的原子性与可见性,对变量的read-modify-write更新操作是指当前操作不是一个简单的赋值,而是变量的*****新值依赖变量的旧值*****,如自增操作i++。
由于volatile只能保证可见性,无法保障原子性,原子变量类内部就是借助一个volatile变量,并且保障了该变量的read-modify-write操作的原子性,有时把原子变量类看作增强的volatile变量
原子变量类有12个
这里每一类只举一个例子,其余的类似
package pers.chenjiahao.atomics.atomiclong;import java.util.concurrent.atomic.AtomicLong;/** * 使用原子变量类定义一个计数器 * 该计数器,在整个程序中都能使用,并且所有的地方都使用这一个计数器,这个计数器可以设计为单例 * @Author ChenJiahao * @Date 2021/2/27 15:07 */public class Indicator { // 构造方法私有化 private Indicator(){} // 定义一个私有的本类静态的对象 private static final Indicator INSTANCE = new Indicator(); // 提供一个公共静态方法返回该类唯一实例 public static Indicator getInstance(){ return INSTANCE; } // 使用原子变量类保存请求总数,成功数,失败数 private final AtomicLong requestCount = new AtomicLong(0); // 记录请求总数 private final AtomicLong successCount = new AtomicLong(0); // 处理成功总数 private final AtomicLong failureCount = new AtomicLong(0); // 处理失败总数 // 有新的请求 public void newRequestReceive(){ requestCount.incrementAndGet(); } // 处理成功了 public void requestProcessSuccess(){ successCount.incrementAndGet(); } // 处理失败了 public void requestProcessFailure(){ failureCount.incrementAndGet(); } // 查看请求总数 public long getRequestCount(){ return requestCount.get(); } //查看成功数 public long getSuccessCount(){ return successCount.get(); } // 查看失败数 public long getFailureCount(){ return failureCount.get(); }}
计数器类
模拟请求计数器
AtomicLong
原子更新数组
AtomicIntegerArray
可以对原子整数字段进行更新
要求: 1.字段必须使用volatile修饰,使线程之间可见 2.只能是实例变量,不能是静态变量,也不能使用final修饰
用户类
线程类
更新字段
AtomicIntegerFieldUpdater
package pers.chenjiahao.atomics.atomicreference;import java.util.concurrent.atomic.AtomicReference;/** * 使用AtomicReference原子读写一个对象 * @Author ChenJiahao * @Date 2021/2/28 14:35 */public class Test01 { // 创建一个AtomicReference对象 static AtomicReference<String> atomicReference = new AtomicReference<>(\"abc\"); public static void main(String[] args) { // 创建100个线程修改字符串 for (int i = 0; i < 100; i++) { new Thread(new Runnable() { @Override public void run() { if (atomicReference.compareAndSet(\"abc\
可以原子读写一个对象
package pers.chenjiahao.atomics.atomicreference;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicReference;/** * 演示AtomicReference可能会出现CAS的ABA问题 * @Author ChenJiahao * @Date 2021/2/28 14:42 */public class Test02 { private static AtomicReference<String> atomicReference = new AtomicReference<>(\"abc\"); public static void main(String[] args) { // 创建第一个线程,先把abc字符串改为\"def\",再把字符串还原为abc Thread t1 = new Thread(new Runnable() { @Override public void run() { atomicReference.compareAndSet(\"abc\
AtomicReference中的ABA问题
ABA问题的解决:使用AtomicStampedReference和AtomicMarkableReference
AtomicReference
package pers.chenjiahao.atomics.atomicstampedreference;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicStampedReference;/** * AtomicStampedReference原子类可以解决CAS中的ABA问题 * 在AtomicStampedReference原子类中有一个整数标记值stamp,每次执行CAS操作时,需要对比它的版本,即比较stamp的值 * @Author ChenJiahao * @Date 2021/2/28 15:02 */public class Test { // private static AtomicReference<String> atomicReference = new AtomicReference<>(\"abc\"); // 定义AtomicStampReference引用操作\"abc\"字符串,指定初始化版本号为0 private static AtomicStampedReference<String> stampedReference = new AtomicStampedReference<>(\"abc\
解决CAS中的ABA问题
AtomicStampedReference
AtomicStampedReference和AtomicReference本质上是一样的只是AtomicStampedReference比AtomicReference多了一个版本号,可以用来解决ABA问题
原子变量类
线程同步
在单线程变成中,要执行的操作需要满足一定的条件才能执行,可以把这个操作放在if语句块中
在多线程编程中,可能A线程的条件没有满足只是暂时的,稍后其他的B线程可能会更新条件,使得A线程的条件得到满足
也就是说可以将A线程暂停,直到它的条件得到满足后再将A线程唤醒,它的伪代码如下 atomics{ // 原子操作 while(条件不成立){ 等待 } 当线程被唤醒条件满足后,继续执行下面的操作 操作.............. }
什么是等待/通知机制
Object类中的wait()方法可以使执行当前代码的线程等待(暂停执行),直到接到通知或被中断为止
wait()方法只能在同步代码块当中由锁对象调用
调用wait()方法,当前线程会释放锁
//在调用wait()方法前需要获得对象的内部锁 synchronized(锁对象){ while(条件不成立){ // 通过锁对象调用wait()方法暂停线程,会释放锁对象 锁对象.wait(); } // 线程的条件满足了,继续向下执行 操作.......... }
伪代码
wait()
Object类的notify()可以唤醒线程,该方法也必须在同步代码块中由锁对象调用,没有使用锁对象调用wait()或notify()会抛出ilegalMonitorStateException异常
如果有多个等待的线程,notify()方法只能唤醒其中的一个
在同步代码块中调用notify()中不会立即释放锁对象,需要等当前同步代码块执行完后才会释放锁对象
一般将notify()方法放在同步代码块的最后
synchronized(锁对象){ // 执行修改保护条件的代码 // 唤醒其他线程 锁对象.notify();}
notify()
等待/通知机制的实现
package pers.chenjiahao.waitandnotify;import java.util.ArrayList;import java.util.List;import java.util.concurrent.TimeUnit;/** * notify()不会立即释放锁对象 * @Author ChenJiahao * @Date 2021/3/1 14:50 */public class Test04 { public static void main(String[] args) { // 定义一个List集合存储String数据 List<String> list = new ArrayList<>(); // 定义第一个线程,当list集合中元素的数量不等于5时,线程等待 Thread t1 = new Thread(new Runnable() { @Override public void run() { synchronized (list){ if (list.size() != 5){ System.out.println(\"线程t1开始等待:\" + System.currentTimeMillis()); try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(\"线程t1被唤醒:\" + System.currentTimeMillis()); } } } }); // 定义第二个线程,向list集合中添加元素 Thread t2 = new Thread(new Runnable() { @Override public void run() { synchronized (list){ for (int i = 0; i < 10; i++) { list.add(\"data--\" + i); System.out.println(\"线程t2添加第\" + (i + 1) + \"个数据\"); // 判断元素的数量是否满足唤醒线程t1 if (list.size() == 5){ list.notify(); // 唤醒线程t1(需要等当前同步代码块全部执行完之后才能释放锁对象) System.out.println(\"线程t2发出唤醒通知\"); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }); t1.start(); // 为了确保t2在t1之后开启,即让t1线程先睡 try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); }}
notify()方法执行后不会立即释放锁
当线程处于wait()等待状态时,调用线程对象的interrupt()方法会中断线程的等待状态,会产生InterruptedException异常
interrupt()方法会中断wait()
都是为了唤醒线程
notify()一次只能唤醒一个线程,如果有多个等待的线程,只能随机唤醒其中的莫i一个线程
package pers.chenjiahao.waitandnotify;/** * notify()与notifyAll() * @Author ChenJiahao * @Date 2021/3/1 16:26 */public class Test06 { public static void main(String[] args) { Object lock = new Object(); SubThread t1 = new SubThread(lock); SubThread t2 = new SubThread(lock); SubThread t3 = new SubThread(lock); t1.setName(\"t1\"); t2.setName(\"t2\"); t3.setName(\"t3\"); t1.start(); t2.start(); t3.start(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } // 调用notify() 唤醒子线程 synchronized (lock){ // lock.notify(); // 调用一次notify()只能唤醒其中的一个线程,对于等待的线程来说,错过了通知信号,这种现象也叫做信号丢失 lock.notifyAll(); // 唤醒所有的线程 } } static class SubThread extends Thread{ // 定义实例变量作为锁对象 private Object lock; public SubThread(Object lock) { this.lock = lock; } @Override public void run() { synchronized (lock){ try { System.out.println(Thread.currentThread().getName() + \" -- begin wait.....\"); lock.wait(); System.out.println(Thread.currentThread().getName() + \" -- end wait.....\"); } catch (InterruptedException e) { e.printStackTrace(); } } } }}
notifyAll()可以唤醒所有等待的线程
notify()与nofifyAll()
wait(long)带有long类型参数的wait(等待),如果在参数指定的时间内没有被唤醒,超时后会自动唤醒
package pers.chenjiahao.waitandnotify;/** * wait(long) * @Author ChenJiahao * @Date 2021/3/1 16:36 */public class Test07 { public static void main(String[] args) { final Object obj = new Object(); Thread t1 = new Thread(new Runnable() { @Override public void run() { synchronized (obj){ try { System.out.println(\"thread begin wait\"); obj.wait(5000); System.out.println(\"thread end wait\"); } catch (InterruptedException e) { e.printStackTrace(); } } } }); t1.start(); }}
wait(long)
线程wait()等待后,可以调用notify()唤醒线程,如果notify()唤醒的过早,在等待之前就调用了notify()可能会打乱程序正常的运行逻辑
package pers.chenjiahao.waitandnotify;/** * notify()通知过早 * @Author ChenJiahao * @Date 2021/3/1 16:41 */public class Test08 { public static void main(String[] args) { // 定义一个对象作为锁对象 final Object lock = new Object(); Thread t1 = new Thread(new Runnable() { @Override public void run() { synchronized (lock){ try { System.out.println(\"begin wait\"); lock.wait(); System.out.println(\"end wait\"); } catch (InterruptedException e) { e.printStackTrace(); } } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { synchronized (lock){ System.out.println(\"begin notify\"); lock.notify(); System.out.println(\"end notify\"); } } }); // 如果先开启t1,再开启t2线程,大多数情况下,t1先等待,t2再把t1唤醒// t1.start();// t2.start(); // 如果嫌开启t2通知线程,再开启t1等待线程,就可能会出现t1线程没有收到唤醒通知 t2.start(); t1.start(); }}
通知过早
通知早了就不等待了
通知过早的问题
在使用wait/notify模式时,注意:wait条件发生了变化,也可能会造成程序逻辑的混乱
package pers.chenjiahao.waitandnotify;import java.util.ArrayList;import java.util.List;/** * wait条件发生变化 * 定义一个集合 * 定义一个线程向集合中添加数据,添加完数据后通知另外的线程从集合中取数据 * 定义一个线程从集合中取数据,如果集合中没有数据就等待 * @Author ChenJiahao * @Date 2021/3/1 16:56 */public class Test10 { public static void main(String[] args) { // 定义添加数据的线程对象 AddThread addThread = new AddThread(); // 定义取数据的线程 SubtractThread subtractThread = new SubtractThread(); subtractThread.setName(\"subtract 1\
wait等待条件发生了变化
在Java中,负责产生数据的模块是生产者,负责使用数据的模块是消费者,生产者消费者解决数据的平衡问题,即先有数据然后才能使用,没有数据时,消费者需要等待
package pers.chenjiahao.producerdata;/** * @Author ChenJiahao * @Date 2021/3/2 12:35 */public class ValueOP { private String value = \"\"; // 定义方法修改value字段的值 public void setValue(){ synchronized (this){ // 如果value值不是\"\"空串就等待 if (!value.equals(\"\")){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 如果value是空串,就设置value的值 String value = System.currentTimeMillis() + \"-\" + System.nanoTime(); System.out.println(\"set设置的值是:\" + value); this.value = value; this.notify(); } } // 定义方法读取字段值 public void getValue(){ synchronized (this){ // 如果value是空串就等待 if (value.equals(\"\
ValueOP类
package pers.chenjiahao.producerdata;/** * 定义线程类,模拟生产者 * @Author ChenJiahao * @Date 2021/3/2 12:43 */public class ProducerThread extends Thread { // 生产者生产数据就是调用ValueOP类的setValue方法给value字段赋值 private ValueOP obj; public ProducerThread(ValueOP obj) { this.obj = obj; } @Override public void run() { while (true){ obj.setValue(); } }}
ProducerThread类
package pers.chenjiahao.producerdata;/** * 定义线程类,模拟消费者 * @Author ChenJiahao * @Date 2021/3/2 12:43 */public class ConsumerThread extends Thread { // 消费者使用数据,就是使用ValueOP类的value字段值 private ValueOP obj; public ConsumerThread(ValueOP obj) { this.obj = obj; } @Override public void run() { while (true){ obj.getValue(); } }}
ConsumerThread类
package pers.chenjiahao.producerdata;/** * 测试一生产,一消费的情况 * @Author ChenJiahao * @Date 2021/3/2 12:34 */public class Test { public static void main(String[] args) { ValueOP valueOP = new ValueOP(); ProducerThread p = new ProducerThread(valueOP); ConsumerThread c = new ConsumerThread(valueOP); p.start(); c.start(); }}
一生产一消费
注:要使用notifyAll()否则可能会导致生产者唤醒生产者,消费者唤醒消费者 ValueOP中的if条件要改为while条件(再判断一次)
package pers.chenjiahao.producerdata;/** * @Author ChenJiahao * @Date 2021/3/2 12:35 */public class ValueOP2 { private String value = \"\"; // 定义方法修改value字段的值 public void setValue(){ synchronized (this){ // 如果value值不是\"\"空串就等待 while(!value.equalsIgnoreCase(\"\")){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } // 如果value是空串,就设置value的值 String value = System.currentTimeMillis() + \"-\" + System.nanoTime(); System.out.println(\"set设置的值是:\" + value); this.value = value; // this.notify(); 多生产,多消费环境中,notify不能保证唤醒的是生产者还是消费者,如果生产者唤醒了还是生产者,可能会出现假死 this.notifyAll(); } } // 定义方法读取字段值 public void getValue(){ synchronized (this){ // 如果value是空串就等待 while (value.equals(\"\
ValueOP2类
package pers.chenjiahao.producerdata;/** * 定义线程类,模拟生产者 * @Author ChenJiahao * @Date 2021/3/2 12:43 */public class ProducerThread2 extends Thread { // 生产者生产数据就是调用ValueOP类的setValue方法给value字段赋值 private ValueOP2 obj; public ProducerThread2(ValueOP2 obj) { this.obj = obj; } @Override public void run() { while (true){ obj.setValue(); } }}
ProducerThread2类
package pers.chenjiahao.producerdata;/** * 定义线程类,模拟消费者 * @Author ChenJiahao * @Date 2021/3/2 12:43 */public class ConsumerThread2 extends Thread { // 消费者使用数据,就是使用ValueOP类的value字段值 private ValueOP2 obj; public ConsumerThread2(ValueOP2 obj) { this.obj = obj; } @Override public void run() { while (true){ obj.getValue(); } }}
ConsumerThread2类
package pers.chenjiahao.producerdata;/** * 测试多生产,多消费的情况 * @Author ChenJiahao * @Date 2021/3/2 12:34 */public class Test2 { public static void main(String[] args) { ValueOP2 valueOP = new ValueOP2(); ProducerThread2 p1 = new ProducerThread2(valueOP); ProducerThread2 p2 = new ProducerThread2(valueOP); ProducerThread2 p3 = new ProducerThread2(valueOP); ConsumerThread2 c1 = new ConsumerThread2(valueOP); ConsumerThread2 c2 = new ConsumerThread2(valueOP); ConsumerThread2 c3 = new ConsumerThread2(valueOP); p1.start(); p2.start(); p3.start(); c1.start(); c2.start(); c3.start(); }}
多生产多消费
使生产者把数据存储到List集合中,消费者从List集合中取数据,使用List集合模拟栈
package pers.chenjiahao.producerstack;import java.util.ArrayList;import java.util.List;/** * 模拟栈 * @Author ChenJiahao * @Date 2021/3/2 13:21 */public class MyStack { // 定义集合来模拟栈 private List list = new ArrayList(); // 定义集合的最大容量 private static final int MAX = 1; // 定义方法模拟入栈 public synchronized void push(){ // 当栈中的数据已满 就等待 if (list.size() >= MAX){ System.out.println(Thread.currentThread().getName() + \"begin wait ......\"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } String data = \"Data--\" + Math.random(); System.out.println(Thread.currentThread().getName() + \"添加了数据:\" + data); list.add(data); // 通知出栈 this.notify(); } // 定义方法模拟出栈 public synchronized void pop(){ // 如果没有数据就等待 if (list.size() == 0){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + \"出栈数据:\" + list.remove(0)); // 通知入栈 this.notify(); }}
MyStack
package pers.chenjiahao.producerstack;/** * 消费者线程 * @Author ChenJiahao * @Date 2021/3/2 13:34 */public class ConsumerThread extends Thread{ private MyStack stack; public ConsumerThread(MyStack stack) { this.stack = stack; } @Override public void run() { while (true){ stack.pop(); } }}
ConsumerThread
package pers.chenjiahao.producerstack;/** * 生产者线程 * @Author ChenJiahao * @Date 2021/3/2 13:34 */public class ProducerThread extends Thread{ private MyStack stack; public ProducerThread(MyStack stack) { this.stack = stack; } @Override public void run() { while (true){ stack.push(); } }}
ProducerThread
package pers.chenjiahao.producerstack;/** * 测试一生产一消费的情况 * @Author ChenJiahao * @Date 2021/3/2 13:21 */public class Test { public static void main(String[] args) { MyStack stack = new MyStack(); ProducerThread p = new ProducerThread(stack); ConsumerThread c = new ConsumerThread(stack); p.start(); c.start(); /* 运行结果是两个线程交替执行,一个线程负责生产,通知另外一个线程负责消费 */ }}
Test
一生产一消费操作栈
package pers.chenjiahao.producerstack;import java.util.ArrayList;import java.util.List;/** * 模拟栈 * @Author ChenJiahao * @Date 2021/3/2 13:21 */public class MyStack2 { // 定义集合来模拟栈 private List list = new ArrayList(); // 定义集合的最大容量 private static final int MAX = 1; // 定义方法模拟入栈 public synchronized void push(){ // 当栈中的数据已满 就等待 while (list.size() >= MAX){ System.out.println(Thread.currentThread().getName() + \"begin wait ......\"); try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } String data = \"Data--\" + Math.random(); System.out.println(Thread.currentThread().getName() + \"添加了数据:\" + data); list.add(data); // 通知出栈 // this.notify(); 多个生产者多个消费者使用notifyAll this.notifyAll(); } // 定义方法模拟出栈 public synchronized void pop(){ // 如果没有数据就等待 while (list.size() == 0){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + \"出栈数据:\" + list.remove(0)); // 通知入栈 // this.notify(); 多个生产者多个消费者使用notifyAll this.notifyAll(); }}
package pers.chenjiahao.producerstack;/** * 消费者线程 * @Author ChenJiahao * @Date 2021/3/2 13:34 */public class ConsumerThread2 extends Thread{ private MyStack2 stack; public ConsumerThread2(MyStack2 stack) { this.stack = stack; } @Override public void run() { while (true){ stack.pop(); } }}
package pers.chenjiahao.producerstack;/** * 生产者线程 * @Author ChenJiahao * @Date 2021/3/2 13:34 */public class ProducerThread2 extends Thread{ private MyStack2 stack; public ProducerThread2(MyStack2 stack) { this.stack = stack; } @Override public void run() { while (true){ stack.push(); } }}
package pers.chenjiahao.producerstack;/** * 测试一生产多消费的情况 * @Author ChenJiahao * @Date 2021/3/2 13:21 */public class Test2 { public static void main(String[] args) { MyStack2 stack = new MyStack2(); ProducerThread2 p1 = new ProducerThread2(stack); ProducerThread2 p2 = new ProducerThread2(stack); ProducerThread2 p3 = new ProducerThread2(stack); ConsumerThread2 c1 = new ConsumerThread2(stack); ConsumerThread2 c2 = new ConsumerThread2(stack); ConsumerThread2 c3 = new ConsumerThread2(stack); c1.setName(\"消费者1号\"); c2.setName(\"消费者2号\"); c3.setName(\"消费者3号\"); p1.setName(\"生产者1号\"); p2.setName(\"生产者2号\"); p3.setName(\"生产者3号\"); p1.start(); p2.start(); p3.start(); c1.start(); c2.start(); c3.start(); }}
多生产多消费操作栈
操作栈
生产者消费者模式
等待/通知机制
在java.io包中的PipeStream管道流用于在线程之间传送数据,一个线程发送数据到输出管道,另外一个线程从输入管道中读取数据
相关的类包括:PipedInputStream和PipedOutputStream,PipedReader和PipedWriter
通过管道实现线程间的通信
join()
除了控制资源的访问外,还可以通过增加资源的方式来保障线程安全,ThreadLocal主要解决为每个线程绑定自己的值
举例,一根笔,100个学生要签字,只能排队一个一个来,要么会出现线程安全问题,这就是synchronized
100个学生要签字,准备100根比,这就是ThreadLocal
package pers.chenjiahao.threadlocal;/** * ThreadLocal的基本使用 * @Author ChenJiahao * @Date 2021/3/2 15:11 */public class Test { // 定义ThreadLocal对象 static ThreadLocal threadLocal = new ThreadLocal(); // 定义线程类 static class SubThread extends Thread{ @Override public void run() { for (int i = 0; i < 20; i++) { // 设置线程关联的值 threadLocal.set(Thread.currentThread().getName() + \" - \" + i); // 调用get()方法读取关联的值 System.out.println(Thread.currentThread().getName() + \" value = \" + threadLocal.get()); } } } public static void main(String[] args) { SubThread t1 = new SubThread(); SubThread t2 = new SubThread(); t1.start(); t2.start(); }}
代码1
package pers.chenjiahao.threadlocal;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Date;/** * 在多线程环境中,把字符串转换为日期对象,多个线程使用同一个SimpleDateFormat,可能会产生线程安全问题 * 为每个线程指定自己的SimpleDateFormat对象,使用ThreadLocal * @Author ChenJiahao * @Date 2021/3/2 15:15 */public class Test02 { // 定义SimpleDateFormat对象,该对象可以把字符串转换为日期 private static SimpleDateFormat sdf = new SimpleDateFormat(\"yyyy年MM月dd日 HH:mm:ss\"); static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>(); // 定义Runnable接口的实现类 static class ParseDate implements Runnable{ private int i = 0; public ParseDate(int i) { this.i = i; } @Override public void run() { try { // 构建日期字符串 String text = \"2068年3月2日 15:18:\" + (i % 60);// Date date = sdf.parse(text); // 把字符串转换为日期// System.out.println(i + \" -- \" + date); // 先判断当前线程是否有SimpleDateFormat对象,如果当前线程没有SimpleDateFormat就创建一个,如果有就直接使用 if (threadLocal.get() == null){ threadLocal.set(new SimpleDateFormat(\"yyyy年MM月dd日 HH:mm:ss\")); } Date date = threadLocal.get().parse(text); System.out.println(i + \" -- \" + date); } catch (ParseException e) { e.printStackTrace(); } } } public static void main(String[] args) { // 创建100个线程 for (int i = 0; i < 100; i++) { new Thread(new ParseDate(i)).start(); } }}
代码2
ThreadLocal的基本使用
定义一个ThreadLocal的子类
重写initialValue方法,设置初始值
创建子类对象
步骤
package pers.chenjiahao.threadlocal;import java.util.Date;import java.util.Random;/** * ThreadLocal初始值,定义ThreadLocal的子类,在子类中重写initialValue()方法指定初始值,再第一次调用get()方法就不会返回null了 * @Author ChenJiahao * @Date 2021/3/2 15:28 */public class Test03 { // 1.定义一个ThreadLocal的子类 static class SubThreadLocal extends ThreadLocal<Date>{ // 2.重写initialValue方法,设置初始值 @Override protected Date initialValue() { // 3.把当前日期设置为初始值 return new Date(System.currentTimeMillis() - 1000*60*15); } } // 4.定义ThreadLocal对象 // static ThreadLocal threadLocal = new ThreadLocal(); static ThreadLocal threadLocal = new SubThreadLocal(); // 定义线程类 static class SubThread extends Thread{ @Override public void run() { for (int i = 0; i < 10; i++) { // 第一次调用threadLocal的get方法会返回null System.out.println(\"-----------\" + Thread.currentThread().getName() + \"value = \" + threadLocal.get()); // 如果没有初始值就设置当前日期 if (threadLocal.get() == null){ threadLocal.set(new Date()); } try { Thread.sleep(new Random().nextInt(500)); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) { SubThread t1 = new SubThread(); t1.start(); SubThread t2 = new SubThread(); t2.start(); }}
ThreadLocal初始值
ThreadLocal
线程间的通信
在JDK5中新增了Lock锁接口,有ReentrantLock实现类,ReentrantLock锁称为可重入锁,它的功能比synchronized多
锁的可重入性是指,当一个线程获得了一个对象锁后,再次请求该对象锁时是可以获得该对象的锁的
package pers.chenjiahao.lock.reentrant;/** * 演示锁的可重入性 * @Author ChenJiahao * @Date 2021/3/2 17:41 */public class Test01 { public synchronized void sm1(){ System.out.println(\"同步方法1\"); // 线程执行sm1()方法,默认this作为锁对象,在sm1()方法中调用了sm2()方法,注意当前线程还是持有this锁对象的 // sm2()同步方法默认的锁对象也是this对象,要执行sm2()必须先获得this锁对象,当前this对象被当前线程持有,可以再次获得锁对象 // 这就是锁的可重入性,假设锁不可重入的话,可能会造成死锁 sm2(); } private synchronized void sm2() { System.out.println(\"同步方法2\"); sm3(); } private synchronized void sm3() { System.out.println(\"同步方法3\"); } public static void main(String[] args) { Test01 obj = new Test01(); new Thread(new Runnable() { @Override public void run() { obj.sm1(); } }).start(); }}
上述代码中,如果锁不可重入,会造成sm1()拿着锁,去调需要锁的sm2(),但是此时sm1()没执行完,锁无法释放,所以会造成死锁
锁的可重入性
package pers.chenjiahao.lock.reentrant;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * lock锁的基本使用 * @Author ChenJiahao * @Date 2021/3/2 17:57 */public class Test02 { // 定义显示锁 static Lock lock = new ReentrantLock(); // 定义方法 public static void sm(){ // 先获锁 lock.lock(); // 也就是说for循环就是同步代码块 for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + \" -- \" + i); } // 释放锁 lock.unlock(); } public static void main(String[] args) { Runnable r = new Runnable() { @Override public void run() { sm(); } }; // 启动三个线程 new Thread(r).start(); new Thread(r).start(); new Thread(r).start(); }}
调用lock()方法获得锁,调用unlock()释放锁(一般lock()放在try中,unlock()放在finally中)
package pers.chenjiahao.lock.reentrant;import java.util.Random;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * 使用Lock锁同步,不同方法中的同步代码块 * @Author ChenJiahao * @Date 2021/3/2 18:00 */public class Test03 { // 定义锁对象 static Lock lock = new ReentrantLock(); public static void sm1(){ // 经常在try代码块中获得Lock锁,在finally子句中释放锁 try{ // 获得锁 lock.lock(); System.out.println(Thread.currentThread().getName() + \" -- method 1 -- \" + System.currentTimeMillis()); Thread.sleep(new Random().nextInt(1000)); System.out.println(Thread.currentThread().getName() + \" -- method 111 -- \" + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 释放锁 lock.unlock(); } } public static void sm2(){ // 经常在try代码块中获得Lock锁,在finally子句中释放锁 try{ // 获得锁 lock.lock(); System.out.println(Thread.currentThread().getName() + \" -- method 2 -- \" + System.currentTimeMillis()); Thread.sleep(new Random().nextInt(1000)); System.out.println(Thread.currentThread().getName() + \" -- method 222 -- \" + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 释放锁 lock.unlock(); } } public static void main(String[] args) { Runnable r1 = new Runnable() { @Override public void run() { sm1(); } }; Runnable r2 = new Runnable() { @Override public void run() { sm2(); } }; new Thread(r1).start(); new Thread(r1).start(); new Thread(r1).start(); new Thread(r2).start(); new Thread(r2).start(); new Thread(r2).start(); }}
使用Lock锁同步在不同方法中的同步代码块(只要是同一个lock,就可以)
package pers.chenjiahao.lock.reentrant;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * ReentrantLock锁的可重入性 * @Author ChenJiahao * @Date 2021/3/2 18:10 */public class Test04 { static class SubThread extends Thread{ // 定义锁对象 不加static的话,new一个一个锁对象 // private Lock lock = new ReentrantLock(); private static Lock lock = new ReentrantLock(); // 定义一个变量 public static int num = 0; @Override public void run() { for (int i = 0; i < 10000; i++) { try { // 可重入锁指可以反复获得该锁 lock.lock(); lock.lock(); num++; } finally { // 注:获得几次释放几次!!! lock.unlock(); lock.unlock(); } } } } public static void main(String[] args) { SubThread t1 = new SubThread(); SubThread t2 = new SubThread(); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(SubThread.num); }}
ReentrantLock锁的可重入性(获得几次就要释放几次,要么会产生死锁)
注:一个实例一个对象锁,如果把变量静态化,所有实例共享一个锁
ReentrantLock的基本使用
lockInterruptibly()方法的作用:如果当前线程未被中断则获得锁,如果当前线程被中断则出现异常
package pers.chenjiahao.lock.reentrant;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * lockInterruptibly()方法的作用:如果当前线程未被中断则获得锁,如果当前线程被中断则出现异常 * @Author ChenJiahao * @Date 2021/3/3 20:51 */public class Test05 { static class Service{ // 定义锁对象 private Lock lock = new ReentrantLock(); public void serviceMethod(){ try { // 获得锁 // lock.lock(); // 获得锁定,即使调用了线程的interrupt()方法,也没有真正的中断线程 lock.lockInterruptibly(); // 如果线程被中断了。不会获得锁,会产生异常 System.out.println(Thread.currentThread().getName() + \" -- begin lock \"); // 执行一段耗时的操作 for (int i = 0; i < Integer.MAX_VALUE; i++) { new StringBuilder(); } System.out.println(Thread.currentThread().getName() + \" -- end lock\"); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 释放锁 lock.unlock(); System.out.println(Thread.currentThread().getName() + \"***** 释放锁\"); } } } public static void main(String[] args) { Service s = new Service(); Runnable r = new Runnable() { @Override public void run() { s.serviceMethod(); } }; Thread t1 = new Thread(r); t1.start(); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } Thread t2 = new Thread(r); t2.start(); try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } t2.interrupt(); // 中断t2线程 }}
代码解释:如果lock是通过lock.lock()获得的,线程使用interrupt()方法是无法中断的 如果lock是通过lock.lockInterruptibly()获得的,线程使用interrupt()方法可以中断
对于synchronized内部锁来说,如果一个线程在等待锁,只有两个结果:要么该线程获得锁继续执行;要么就保持等待,如果一直得不到锁就一直等待对于ReentrantLock可重入锁来说,提供另外一种可能,在等待锁的过程中,程序可以根据需要取消对锁的请求
Lock使用lock.lock()获得锁的时候,线程的interrupt()没用使用lock.lockInterruptibly()获得锁的时候,线程的interrupt()才有用
总结:lockInterruptibly()可以解决死锁的问题
package pers.chenjiahao.lock.reentrant;import java.util.Random;import java.util.concurrent.locks.ReentrantLock;/** * 通过ReentrantLock锁的lockInterruptibly()方法可以避免死锁的产生 * @Author ChenJiahao * @Date 2021/3/3 21:10 */public class Test06 { static class IntLock implements Runnable{ // 创建两个锁对象 public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); // 定义整数变量,决定使用哪个锁 int lockNum; public IntLock(int lockNum) { this.lockNum = lockNum; } @Override public void run() { try { if (lockNum % 2 == 1){ // 奇数 // lock1.lock(); 无法使用interrupt来中断 lock1.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + \"获得锁1,还需要获得锁2\"); Thread.sleep(new Random().nextInt(500)); lock2.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + \"同时获得锁1与锁2.。。\"); }else { // 偶数 lock2.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + \"获得锁2,还需要获得锁1\"); Thread.sleep(new Random().nextInt(500)); lock1.lockInterruptibly(); System.out.println(Thread.currentThread().getName() + \"同时获得锁2与锁1.。。\"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (lock1.isHeldByCurrentThread()) lock1.unlock(); if (lock2.isHeldByCurrentThread()) lock2.unlock(); System.out.println(Thread.currentThread().getName() + \"线程退出\"); } } } public static void main(String[] args) { IntLock intLock1 = new IntLock(11); IntLock intLock2 = new IntLock(22); Thread t1 = new Thread(intLock1); Thread t2 = new Thread(intLock2); t1.start(); t2.start(); // 在main线程,等待3000秒,如果还有线程没有结束就中断该线程 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } // 实际开发中不需要中断两个线程,可以中断任何一个线程来解决死锁问题 // if (t1.isAlive()) t1.interrupt(); if (t2.isAlive()) t2.interrupt(); }}
代码(解决死锁)
lockInterruptibly()
获得锁返回true,没有获得就返回false
下述代码中,线程1先获得锁,执行4秒的任务,而tryLock设置的是3秒,所以线程2在尝试3秒没有获得锁之后就放弃了
(尝试申请锁,不等)仅在调用时锁定未被其他线程持有的锁,如果调用方法时,锁对象被其他线程持有,则放弃,调用方法尝试获得锁,如果该锁没有被其他线程占用,则返回true表示锁定成功,如果锁被其他线程占用则返回false
package pers.chenjiahao.lock.reentrant;import java.util.concurrent.locks.ReentrantLock;/** * tryLock() * 当锁对象没有被其他线程持有的情况下才会获得该锁定 * @Author ChenJiahao * @Date 2021/3/3 21:50 */public class Test08 { static class Service{ private ReentrantLock lock = new ReentrantLock(); public void serviceMethod(){ try { if (lock.tryLock()){ System.out.println(Thread.currentThread().getName() + \"获得锁\"); Thread.sleep(3000);// 模拟执行任务的时长 }else { System.out.println(Thread.currentThread().getName() + \"没有获得锁定\"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (lock.isHeldByCurrentThread()){ lock.unlock(); } } } } public static void main(String[] args) { Service service = new Service(); Runnable r = new Runnable() { @Override public void run() { service.serviceMethod(); } }; Thread t1 = new Thread(r); t1.start(); try { Thread.sleep(50);// 睡眠50毫秒,确保t1线程锁定 } catch (InterruptedException e) { e.printStackTrace(); } Thread t2 = new Thread(r); t2.start(); }}
package pers.chenjiahao.lock.reentrant;import java.util.Random;import java.util.concurrent.locks.ReentrantLock;/** * 使用tryLock()可以避免死锁 * @Author ChenJiahao * @Date 2021/3/3 21:55 */public class Test09 { static class IntLock implements Runnable{ private static ReentrantLock lock1 = new ReentrantLock(); private static ReentrantLock lock2 = new ReentrantLock(); private int lockNum; // 用于控制锁的顺序 public IntLock(int lockNum) { this.lockNum = lockNum; } @Override public void run() { if (lockNum % 2 == 0){ // 偶数先锁1.再锁2 while (true){ try { if (lock1.tryLock()){ System.out.println(Thread.currentThread().getName() + \"获得锁1,还想获得锁2\"); Thread.sleep(new Random().nextInt(100)); try { if (lock2.tryLock()){ System.out.println(Thread.currentThread().getName() + \"同时获得锁1与锁2\"); return; //完成任务 结束run()方法,即当前线程结束 } } finally { if (lock2.isHeldByCurrentThread()){ lock2.unlock(); } } } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (lock1.isHeldByCurrentThread()){ lock1.unlock(); } } } }else { // 奇数先锁2,再锁1 while (true){ try { if (lock2.tryLock()){ System.out.println(Thread.currentThread().getName() + \"获得锁2,还想获得锁1\"); Thread.sleep(new Random().nextInt(100)); try { if (lock1.tryLock()){ System.out.println(Thread.currentThread().getName() + \"同时获得锁2与锁1\"); return; //完成任务 结束run()方法,即当前线程结束 } } finally { if (lock1.isHeldByCurrentThread()){ lock1.unlock(); } } } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (lock2.isHeldByCurrentThread()){ lock2.unlock(); } } } } } } public static void main(String[] args) { IntLock intLock1 = new IntLock(11); IntLock intLock2 = new IntLock(22); Thread t1 = new Thread(intLock1); Thread t2 = new Thread(intLock2); t1.start(); t2.start(); // 运行后,使用tryLock()尝试获得锁,不会傻傻的等待,而是通过循环不停的再次尝试,如果等待的时间足够长,线程总是会获得想要的资源 }}
tryLock()可以用来避免死锁
tryLock()
关键字synchronized与wait()/notify()这两个方法一起使用可以实现等待/通知模式,Lock锁的newContition()方法返回Condition对象,Condition类也可以实现等待/通知模式
使用notify()通知时,JVM会随机唤醒某个等待的线程,使用Condition类可以进行选择性通知
await()会使当前线程等待,同时会释放锁,当其他线程调用signal()时线程会尝试重新获得锁并继续执行相当于wait()
signal()用于唤醒一个等待的线程相当于notify()
注意:在调用Condition的await()/signal()方法前,也需要线程持有相关的Lock锁,调用await()后线程会释放这个锁,在singal()调用后线程会从当前Condition对象的等待队列中唤醒一个线程,唤醒的线程会尝试获得锁,一旦获得锁成功就继续执行
package pers.chenjiahao.lock.condition;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * Condition等待与通知 * @Author ChenJiahao * @Date 2021/3/3 22:24 */public class Test01 { // 定义锁 static Lock lock = new ReentrantLock(); // 获得Condition对象 static Condition condition = lock.newCondition(); // 定义线程子类 static class SubThread extends Thread{ @Override public void run() { try { lock.lock(); // 在调用await()前必须先获得锁 System.out.println(\"method lock\"); condition.await(); // 等待 System.out.println(\"method await\"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); // 释放锁 System.out.println(\"method unlock\"); } } } public static void main(String[] args) { SubThread t = new SubThread(); t.start(); // 子线程启动后,会转入等待状态 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } // 主线程在睡眠3秒后,唤醒子线程的等待 // 唤醒之前必须要先持有锁 try { lock.lock(); condition.signal(); } finally { lock.unlock(); } }}
(代码)使用await()使线程等待,使用signal()唤醒线程
package pers.chenjiahao.lock.condition;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.ReentrantLock;/** * 多个Condition实现通知部分线程 * @Author ChenJiahao * @Date 2021/3/3 22:31 */public class Test02 { static class Service{ // 定义锁对象 private ReentrantLock lock = new ReentrantLock(); // 定义两个Condition对象 private Condition conditionA = lock.newCondition(); private Condition conditionB = lock.newCondition(); // 定义方法,使用conditionA等待 public void waitMethodA(){ try { lock.lock(); System.out.println(Thread.currentThread().getName() + \" begin wait:\" + System.currentTimeMillis()); conditionA.await(); System.out.println(Thread.currentThread().getName() + \" end wait:\" + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } // 定义方法,使用conditionB等待 public void waitMethodB(){ try { lock.lock(); System.out.println(Thread.currentThread().getName() + \" begin wait:\" + System.currentTimeMillis()); conditionB.await(); System.out.println(Thread.currentThread().getName() + \" end wait:\" + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } // 定义方法唤醒conditionA对象上的等待 public void signalA(){ try { lock.lock(); System.out.println(Thread.currentThread().getName() + \"signal A time = \" + System.currentTimeMillis()); conditionA.signal(); System.out.println(Thread.currentThread().getName() + \"signal A time = \" + System.currentTimeMillis()); } finally { lock.unlock(); } } // 定义方法唤醒conditionA对象上的等待 public void signalB(){ try { lock.lock(); System.out.println(Thread.currentThread().getName() + \"signal B time = \" + System.currentTimeMillis()); conditionB.signal(); System.out.println(Thread.currentThread().getName() + \"signal B time = \
(代码)多个Condition实现通知部分线程
package pers.chenjiahao.lock.condition;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * 使用Condition实现生产者/消费者两个线程交替打印 * @Author ChenJiahao * @Date 2021/3/3 23:26 */public class Test03 { static class MyService{ // 创建锁对象 private Lock lock = new ReentrantLock(); // 创建Condition private Condition condition = lock.newCondition(); // 定义交替打印的标记 private boolean flag = true; // 定义方法只打印---横线 public void printOne(){ try { // 获得锁 lock.lock(); while (flag){ // 当flag未true等待 condition.await(); } // flag为false时打印 System.out.println(Thread.currentThread().getName() + \"---------------\"); // 将标志修改为true flag = true; // 通知另外的线程打印 condition.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } // 定义方法只打印 ***星号 public void printTwo(){ try { // 获得锁 lock.lock(); while (!flag){ // 当flag未true等待 condition.await(); } // flag为true时打印 System.out.println(Thread.currentThread().getName() + \"************\"); // 将标志修改为true flag = false; // 通知另外的线程打印 condition.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public static void main(String[] args) { MyService myService = new MyService(); // 打印------ new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { myService.printOne(); } } }).start(); // 打印******** new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { myService.printTwo(); } } }).start(); }}
使用Condition实现生产者/消费者两个线程交替打印
package pers.chenjiahao.lock.condition;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;/** * 使用Condition实现生产者/消费者两个线程交替打印,多对多,即有多个线程打印---,多个线程打印*** * 实现--与**的交替打印 * @Author ChenJiahao * @Date 2021/3/3 23:26 */public class Test04 { static class MyService{ // 创建锁对象 private Lock lock = new ReentrantLock(); // 创建Condition private Condition condition = lock.newCondition(); // 定义交替打印的标记 private boolean flag = true; // 定义方法只打印---横线 public void printOne(){ try { // 获得锁 lock.lock(); while (flag){ // 当flag未true等待 condition.await(); } // flag为false时打印 System.out.println(Thread.currentThread().getName() + \"---------------\"); // 将标志修改为true flag = true; // 通知另外所有等待的线程打印 condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } // 定义方法只打印 ***星号 public void printTwo(){ try { // 获得锁 lock.lock(); while (!flag){ // 当flag未true等待 condition.await(); } // flag为true时打印 System.out.println(Thread.currentThread().getName() + \"************\"); // 将标志修改为true flag = false; // 通知另外所有等待的线程打印 condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public static void main(String[] args) { MyService myService = new MyService(); for (int i = 0; i < 10; i++) { // 打印------ new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { myService.printOne(); } } }).start(); // 打印******** new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100; i++) { myService.printTwo(); } } }).start(); } }}
使用Condition实现多对多的生产者消费者模式
Condition比较常用的两个方法:
newCondition()
大多数情况下,锁的申请都是非公平的,如果线程1与线程2都在请求锁A,当锁A可用时,系统只是会从阻塞队列中随机的选择一个线程,不能保证其公平性
公平的锁会按照时间先后顺序,保证先到先得,公平锁的这一特点不会让线程饥饿,
synchronized内部锁就是非公平的,而ReentrantLock重入锁提供了一个构造方法,可以将锁设置为公平的
如果是非公平锁,系统倾向于让已经持有锁的线程再次获得锁,这种分配策略是高效的,但是非公平 如果是公平锁,多个线程不会发生同一个线程连续多次获得锁的可能,保证了锁的公平性
package pers.chenjiahao.lock.method;import java.util.concurrent.locks.ReentrantLock;/** * 公平锁与非公平锁 * @Author ChenJiahao * @Date 2021/3/4 12:47 */public class Test01 { // 默认是非公平锁 // static ReentrantLock lock = new ReentrantLock(); static ReentrantLock lock = new ReentrantLock(true); public static void main(String[] args) { Runnable r = new Runnable() { @Override public void run() { while (true){ try { lock.lock(); System.out.println(Thread.currentThread().getName() + \" 获得了锁对象 \"); }finally { lock.unlock(); } } } }; for (int i = 0; i < 5; i++) { new Thread(r).start(); } /* 运行程序 如果是非公平锁,系统倾向于让已经持有锁的线程再次获得锁,这种分配策略是高效的,但是非公平 如果是公平锁,多个线程不会发生同一个线程连续多次获得锁的可能,保证了锁的公平性 */ }}
ReentrantLock() 无参的默认非公平ReentrantLock(boolean fair),实参传递true,可以把该锁设置为公平锁
公平锁看起来很公平,但是要实现公平锁,必须要求系统维护一个有序的队列,所以公平锁的实现成本较高,性能也低,因此默认情况下锁是非公平的,不是特别的需求,一般不使用公平锁
公平锁和非公平锁
返回当前线程调用lock()方法的次数
package pers.chenjiahao.lock.method;import java.util.concurrent.locks.ReentrantLock;/** * int getHoldCount() 方法可以返回当前线程调用lock()方法的次数 * @Author ChenJiahao * @Date 2021/3/4 12:58 */public class Test02 { // 定义锁对象 static ReentrantLock lock = new ReentrantLock(); public static void m1(){ try { lock.lock(); // 打印线程调用lock()的次数s System.out.println(Thread.currentThread().getName() + \"m1 hold count:\" + lock.getHoldCount()); // 调用m2()方法,ReentrantLock是可重入锁,所以在m2()方法中可以再次获得该锁对象 m2(); } finally { lock.unlock(); } } public static void m2(){ try { lock.lock(); // 打印线程调用lock()的次数s System.out.println(Thread.currentThread().getName() + \"m2 hold count:\" + lock.getHoldCount()); } finally { lock.unlock(); } } public static void main(String[] args) { // main线程调用m1() m1(); }}
int getHoldCount()
返回正等待获得锁的线程数(预估)
package pers.chenjiahao.lock.method;import java.util.concurrent.locks.ReentrantLock;/** * int getQueueLength() 返回等待获得锁的线程预估数 * @Author ChenJiahao * @Date 2021/3/4 13:03 */public class Test03 { static ReentrantLock lock = new ReentrantLock(); public static void sm(){ try { lock.lock(); System.out.println(Thread.currentThread().getName() + \"获得锁,执行方法,估计等待获得锁的线程数\" + lock.getQueueLength()); // 睡眠1秒,模拟执行时间 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void main(String[] args) { Runnable r = new Runnable() { @Override public void run() { Test03.sm(); } }; // 开启10个线程,执行sm()方法 for (int i = 0; i < 10; i++) { new Thread(r).start(); } }}
int getQueueLength()
返回与Condition条件相关的等待的线程预估数
package pers.chenjiahao.lock.method;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.ReentrantLock;/** * int getWaitQueueLength(Condition condition) 返回在Condition条件上等待的线程预估数 * @Author ChenJiahao * @Date 2021/3/4 13:09 */public class Test04 { static class Service{ // 定义锁对象 private ReentrantLock lock = new ReentrantLock(); // 返回锁给定的condition private Condition condition = lock.newCondition(); public void waitMethod(){ try { lock.lock(); System.out.println(Thread.currentThread().getName() + \" 进入等待前,现在该condition条件上等待的线程预估数:\" + lock.getWaitQueueLength(condition)); condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void notifyMethod(){ try { lock.lock(); // 唤醒所有的等待 condition.signalAll(); System.out.println(\"唤醒所有的等待后,condition条件上等待的线程预估数:\" + lock.getWaitQueueLength(condition)); } finally { lock.unlock(); } } } public static void main(String[] args) { Service service = new Service(); Runnable r = new Runnable() { @Override public void run() { service.waitMethod(); } }; // 创建10个线程调用waitMethod for (int i = 0; i < 10; i++) { new Thread(r).start(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 唤醒所有的的等待 service.notifyMethod(); }}
int getWaitQueueLength(Condition condition)
查询参数指定的线程是否在等待获得锁
package pers.chenjiahao.lock.method;import java.util.concurrent.locks.ReentrantLock;/** * boolean hasQueuedThread(Thread thread) 查询指定的线程是否在等待获得锁 * boolean hasQueuedThreads() 查询是否有线程在等待获得锁 * @Author ChenJiahao * @Date 2021/3/4 13:50 */public class Test05 { // 定义锁 static ReentrantLock lock = new ReentrantLock(); public static void waitMethod(){ try { lock.lock(); System.out.println(Thread.currentThread().getName() + \"获得了锁\"); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(Thread.currentThread().getName() + \"释放了锁对象。。。。。。\"); lock.unlock(); } } public static void main(String[] args) { Runnable r = new Runnable() { @Override public void run() { Test05.waitMethod(); } }; // 定义线程数组 Thread[] threads = new Thread[5]; // 给线程数组的元素赋值,每个线程都调用waitMethod(),并启动线程 for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(r); threads[i].setName(\"thread -\" + i); threads[i].start(); } // 睡眠3秒 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } // 判断数组中的每个线程对象是否在等待获得锁 System.out.println(lock.hasQueuedThread(threads[0])); System.out.println(lock.hasQueuedThread(threads[1])); System.out.println(lock.hasQueuedThread(threads[2])); System.out.println(lock.hasQueuedThread(threads[3])); System.out.println(lock.hasQueuedThread(threads[4])); // 睡2秒 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } // 判断是否还有线程在等待获得该锁 System.out.println(lock.hasQueuedThreads()); }}
boolean hasQueuedThread(Thread thread)
查询是否还有线程在等待获得锁
代码(同上代码)
boolean hasQueuedThreads()
查询是否有线程正在等待指定的Condition条件
package pers.chenjiahao.lock.method;import java.util.Random;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Condition;import java.util.concurrent.locks.ReentrantLock;/** * boolean hasWaiters(Condition condition) * @Author ChenJiahao * @Date 2021/3/4 15:40 */public class Test06 { // 创建锁对象 static ReentrantLock lock = new ReentrantLock(); // 返回锁定的条件 static Condition condition = lock.newCondition(); static void sm(){ try { lock.lock(); System.out.println(\"是否有线程正在等待当前Condition条件?:\" + lock.hasWaiters(condition) + \" -- 等待的个数\" + lock.getWaitQueueLength(condition)); System.out.println(Thread.currentThread().getName() + \" waiting ... \
boolean hasWaiters(Condition condition)
判断是否为公平锁
package pers.chenjiahao.lock.method;import java.util.Random;import java.util.concurrent.locks.ReentrantLock;/** * boolean isHeldByCurrentThread() 判断当前线程是否持有锁 * boolean isFair() 判断是否为公平锁 * @Author ChenJiahao * @Date 2021/3/4 15:48 */public class Test07 { static class Service{ private ReentrantLock lock; // 通过构造方法接收布尔值,确定当前锁是否公平 public Service(boolean isFair){ this.lock = new ReentrantLock(isFair); } public void serviceMethod(){ try { System.out.println(\"是否公平锁?:\" + lock.isFair() + \"---\" + Thread.currentThread().getName() + \
boolean isFair()
判断当前线程是否持有锁
代码(同上)
boolean isHeldByCurrentThread()
查询当锁是否被线程持有
package pers.chenjiahao.lock.method;import java.util.concurrent.locks.ReentrantLock;/** * boolean isLocked() 判断锁是否被线程持有 * @Author ChenJiahao * @Date 2021/3/4 15:57 */public class Test08 { static ReentrantLock lock = new ReentrantLock(); static void sm(){ try { System.out.println(\"before lock() -- \" + lock.isLocked()); lock.lock(); System.out.println(\"after lock() -- \" + lock.isLocked()); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } finally { if (lock.isHeldByCurrentThread()){ lock.unlock(); } } } public static void main(String[] args) { System.out.println(\"11 -- \" + lock.isLocked()); // 开启线程 调用sm()方法 new Thread(new Runnable() { @Override public void run() { sm(); } }).start(); // 睡3秒,确保子线程执行结束 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(\"22 ---\" + lock.isLocked()); }}
boolean isLocked()
几个常用的方法
ReentrantLock
synchronized内部锁与ReentrantLock锁都是独占锁(排它锁),同一时间只允许一个线程执行同步代码块,可以保证线程的安全性,但是执行效率低
ReentrantReadWriteLock读写锁是一种改进的排他锁,也可以称为共享/排它锁它允许多个线程同时读取共享数据,但是一次只允许一个线程对共享数据进行更新
读写锁通过读锁与写锁来完成读写操作,线程在读取共享数据前必须先持有读锁,该读锁可以同时被多个线程持有,即它是共享的,写锁是排他的,线程在修改共享数据前必须先持有写锁
可以理解为在读数据前需要持有读锁(共享的)修改共享数据必须先持有写锁(排它的),一个线程持有写锁时,别的线程无法获得相应的锁这样设计的目的是:保证线程在读取数据期间么有其他线程对数据进行更新,使得读线程能够读到数据的最新值,保证在读数据期间共享变量不被修改
注:读写锁允许读读共享,读写互斥,写写互斥
注:readLock()与writeLock()方法返回的锁对象是不同一个锁的两个角色,不是分别获得两个不同的锁。 也就是说ReadWriteLock接口的实例可以充当两个角色
基本使用方法
ReadWriteLock读写锁可以实现多个线程同时读取共享数据,即读读共享,可以提高程序读取数据的效率
package pers.chenjiahao.lock.readwrite;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;/** * ReadWriteLock读写锁可以实现:读读共享,允许多个线程同时获得读锁 * @Author ChenJiahao * @Date 2021/3/4 16:47 */public class Test01 { static class Service{ // 创建读写锁 ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // 获得写锁 Lock writeLock = readWriteLock.writeLock(); // 定义方法读取数据 public void read(){ try { // 获得读锁 readWriteLock.readLock().lock(); System.out.println(Thread.currentThread().getName() + \"获得读锁,开始读取数据的时间--\" + System.currentTimeMillis()); // 模拟读取数据的用时 TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName() + \"获得读锁,结束读取数据的时间--\" + System.currentTimeMillis()); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 释放锁 readWriteLock.readLock().unlock(); } } } public static void main(String[] args) { Service service = new Service(); // 创建5个线程调用read()方法 for (int i = 0; i < 5; i++) { new Thread(new Runnable() { @Override public void run() { service.read(); } }).start(); } // 运行程序后,多个线程几乎可以同时获得读锁,执行lock()后面的代码 }}
读读共享
通过ReadWriteLock读写锁中的写锁,只允许一个线程执行lock()后面的代码
package pers.chenjiahao.lock.readwrite;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;/** * 演示ReadWriteLock的writeLock()写锁是互斥的,只允许有一个线程持有 * @Author ChenJiahao * @Date 2021/3/4 16:57 */public class Test02 { static class Service{ // 先定义读写锁 ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // 定义方法修改数据 public void write(){ // 申请获得写锁 readWriteLock.writeLock().lock(); System.out.println(Thread.currentThread().getName() + \
写写互斥
写锁是独占锁,是排他锁,读线程与写线程也是互斥的
一个线程获得读锁时,写线程等待一个线程获得写锁时,其他线程等待
package pers.chenjiahao.lock.readwrite;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;/** * 演示ReadWriteLock的读写互斥 * 一个线程获得读锁时,写线程等待 * 一个线程获得写锁时,其他线程等待 * @Author ChenJiahao * @Date 2021/3/4 16:57 */public class Test03 { static class Service{ // 先定义读写锁 ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // 定义读锁 Lock readLock = readWriteLock.readLock(); // 定义写锁 Lock writeLock = readWriteLock.writeLock(); // 定义方法读取 public void read(){ // 申请获得读锁 readLock.lock(); System.out.println(Thread.currentThread().getName() + \
读写互斥
ReentrantReadWriteLock
Lock显示锁
类似于在计算机中使用文件夹管理文件,也可以使用线程组来管理线程,在线程组中定义一组相似(相关)的线程,在线程组中也可以定义子线程组
Thread类有几个构造方法允许在创建线程时指定线程组,如果在创建线程时没有指定线程组则该线程就属于父线程所在的线程组,JVM在创建main线程时会为它指定一个线程组,因此每个Java线程都有一个线程组与之关联,可以调用线程的getThreadGroup()方法返回线程组
线程组开始是出于安全的考虑设计用来区分不同的Applet,然而ThreadGroup并未实现这一目标,在新开发的系统中,已经不常用线程组了,现在一般会将一组相关的线程存入一个数组或一个集合中,如果仅仅是用来区分线程时,可以使用线程名称来区分,多数情况下,可以忽略线程组
如果不指定所属线程,则自动归属到当前线程所属的线程组中
new ThreadGroup(name) 指定线程组的名字
第一种方式
指定线程组的名字和父线程
第二种方式
返回当前线程组的名字:Thread.currentThread().getThreadGroup();
package pers.chenjiahao.threadgroup;/** * 演示创建线程组 * @Author ChenJiahao * @Date 2021/3/7 20:35 */public class Test01 { public static void main(String[] args) { // 1.返回当前main线程的线程组 ThreadGroup mainGroup = Thread.currentThread().getThreadGroup(); System.out.println(mainGroup); // 2.定义线程组,如果不指定所属线程,则自动归属到当前线程所属的线程组中 ThreadGroup group1 = new ThreadGroup(\"group1\
创建线程组
void setDaemon(boolean daemon):设置线程组为守护线程组
boolean parentOf(ThreadGroup g):判断当前线程组是否为参数线程组的父线程
void list():将当前线程组中的活动线程打印出来
boolean isDaemon():判断当前线程组是否为守护线程组
void interrupt() 中断线程组当中的所有线程
ThreadGroup getParent():返回父线程组
String getName():返回线程组的名称
int getMaxPriority():返回线程组的最大优先级,默认是10
int enumerate(ThreadGroup[] list):将当前线程组中的活动线程组复制到参数数组中
int enumerate(Thread[] list):将当前线程组中的活动线程复制到参数数组中
int activeGroupCount():返回当前线程组及子线程组当中活动线程组的数量(近似值)
int activeCount():返回当前线程组及子线程组中活动线程的数量(预估的适量,近似值)
基本操作
package pers.chenjiahao.threadgroup;/** * 演示线程组的基本操作 * @Author ChenJiahao * @Date 2021/3/14 14:14 */public class Test02 { public static void main(String[] args) { ThreadGroup mainGroup = Thread.currentThread().getThreadGroup(); // 再定义线程组 ThreadGroup group = new ThreadGroup(\"group\"); // 默认group的父线程组为main线程组 Runnable r = new Runnable() { @Override public void run() { while (true){ System.out.println(\"----------------当前线程:\
线程组的基本操作
enumerate(Thread[] list):把当前线程组和子线程组中所有的线程复制到参数数组中
enumerate(ThreadGroup[] list):把当前线程组和子线程组中所有的线程组复制到参数数组中
package pers.chenjiahao.threadgroup;/** * 演示复制线程组中的内容 * @Author ChenJiahao * @Date 2021/3/14 14:35 */public class Test03 { public static void main(String[] args) { // 返回main线程的main线程组 ThreadGroup mainGroup = Thread.currentThread().getThreadGroup(); // 在main线程组中定义了两个子线程组 ThreadGroup group1 = new ThreadGroup(\"group1\
复制线程组中的线程及子线程组
线程组的interrupt()可以给该线程组中所有的活动线程添加中断标志
package pers.chenjiahao.threadgroup;/** * 线程组的批量中断 * @Author ChenJiahao * @Date 2021/3/14 14:52 */public class Test04 { public static void main(String[] args) { Runnable r = new Runnable() { @Override public void run() { System.out.println(\"当前线程:\" + Thread.currentThread() + \"开始循环\"); // 当线程没有被中断就一直循环 while (!Thread.currentThread().isInterrupted()){ System.out.println(Thread.currentThread().getName() + \"---------\"); /*try { Thread.sleep(500); } catch (InterruptedException e) { // 如果中断睡眠中的线程,产生中断异常,同时会清除中断标志 e.printStackTrace(); }*/ } System.out.println(Thread.currentThread().getName() + \"循环结束\"); } }; // 创建线程组 ThreadGroup group = new ThreadGroup(\"group\
线程组批量中断
守护线程是为其他线程提供服务的,当JVM中只有守护线程的时候,守护线程会自动销毁,JVM会退出
调用线程组的setDaemon(true)可以把线程组设置为守护线程组,当守护线程组中没有任何活动线程时,守护线程组会自动销毁
注意:线程组的守护属性,不影响线程组中线程的守护属性,或者说守护线程组中的线程可以是非守护线程
package pers.chenjiahao.threadgroup;/** * 演示设置守护线程组 * @Author ChenJiahao * @Date 2021/3/14 15:04 */public class Test05 { public static void main(String[] args) { // 先定义线程组 ThreadGroup group = new ThreadGroup(\"group\
设置守护线程组
线程组
在线程的run方法中,如果有受检异常必须进行捕获处理,如果想获得run()方法中出现的运行时异常信息,可以通过回调UncaughtExceptionHandler接口获得哪个线程出现了运行时异常
getDefaultUncaughtExceptionHandler()获得全局的(默认的)UncaughtExceptionHandler
getUncaughtExceptionHandler()获得当前线程的UncaughtExceptionHandler()
setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh) 设置全局的UncaughtExceptionHandler
setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)设置当前线程的UncaughtExceptionHandler
示例代码
在Thread类中有关处理运行异常的方法有
捕获线程的执行异常
Hook线程也称为钩子线程,当JVM退出的时候会执行Hook线程,经常在程序启动时创建一个.lock文件,用.lock文件来校验程序是否启动,在程序退出(JVM退出)时,删除该.lock文件,在Hook线程中除了防止重新启动进程外,还可以做资源释放,尽量避免在Hook线程中进行复杂的操作
通过Hook线程防止程序重复启动
package pers.chenjiahao.hook;import java.io.IOException;import java.nio.file.Path;import java.nio.file.Paths;import java.util.concurrent.TimeUnit;/** * 通过Hook线程防止程序重复启动 * @Author ChenJiahao * @Date 2021/3/14 22:52 */public class Test { public static void main(String[] args) { // 1.注入一个Hook线程,在程序退出时,删除.lock文件 Runtime.getRuntime().addShutdownHook(new Thread(){ @Override public void run() { System.out.println(\
注入Hook钩子线程
可以以new Thread(() -> {线程执行的任务}).start();这种形式开启一个线程,当run()运行结束,线程对象会被GC释放
在真实的生产环境中,可能需要很多的线程来支撑整个应用,当线程数量非常多时,反而会耗尽CPU资源,如果不对线程进行控制与管理,反而会影响程序的性能,线程开销主要包括: 创建与启动线程的开销; 线程销毁的开销 线程调度的开销 线程数量受限CPU处理器的数量
线程池就是有效使用线程的一种常用方式,线程池内部可以预先创建一定数量的工作线程,客户端代码直接将任务作为一个对象提交给线程池,线程池将这些任务缓存在工作队列中,线程池中的工作线程不断地从任务队列当中取出任务并执行
线程池工作原理图
什么是线程池
JDK提供了一套Executor框架,可以帮助开发人员有效的使用线程池
图示
创建5个线程大小的线程池ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
向线程池中提交任务fixedThreadPool.execute(Runnable)
线程池的基本使用
创建一个有调度功能的线程池 ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
完整代码
线程池的计划任务
JDK对线程池的支持
ThreadPoolExecutor的构造方法
corePoolSize:指定线程池中核心线程的数量 maxinumPoolSize:指定线程池中最大的线程数量 keepAliveTime:当线程池线程的数量超过corePoolSize时,多余的空闲线程的存活时长,空闲线程在多长时间内销毁 unit:是keepAliveTime的时长单位 workQueue:任务队列,把任务提交到该任务队列中等待执行 threadFactory:线程工厂,用于创建线程的 handler:拒绝策略,当任务太多来不及处理时,如何拒绝
各个参数的含义
直接提交队列:由SynchronousQueue对象提供,该队列没有容量,提交给线程池的任务不会被真实的保存,总是将新的任务提交给线程执行,如果 没有空闲线程,则尝试创建新的线程,如果线程数量已经达到maxinumPoolSize规定的最大值,则执行拒绝策略
有界任务队列:由ArrayBlockingQueue实现,在创建ArrayBlockingQueue对象时,可以指定一个容量,当有任务需要执行时,如果线程池中线程数 小于corePoolSize核心线程熟则创建新的线程;如果大于corePoolSize核心线程数则加入等待队列,如果队列已满则无法加入,在线 程数小于maxinumPoolSize指定的最大线程数前提下会创建新的线程来执行,如果线程熟大于maxinumPoolSize最大线程数则执行拒绝策略
无界任务队列:由LinkedBlockingQueue对象实现,与有界队列相比,除非系统资源耗尽,否则无界队列不存在任务入队失败的情况,当有新的任务时,在系统线程数下雨corePoolSize核心线程数则创建新的线程来执行任务;当线程池中线程数量大于corePoolSize核心线程数,则把任务加入到阻塞队列中
优先任务队列:由PriorityBlockingQueue对象实现,是带有任务优先级的队列,是一个特殊的无界队列,不管是ArrayBlockingQueue队列还是LinkedBlockingQueue队列都是按照先进先出算法处理任务的,在PriorityBlockingQueue对垒中可以根据任务优先级顺序先后执行
workQueue工作队列是指提交未执行的任务队列,它是BlockingQueue接口的对象,仅用于存储Runnable任务,根据队列功能分类,在ThreadPoolExecutor构造方法中可以使用以下几种阻塞队列:
ThreadPoolExecutor
newCachedThreadPool():该线程池在极端情况下,每次提交新的任务都会创建新的线程执行,适合用来执行大量耗时短并且提交频繁的任务
newFixedThreadPool():核心线程数等于最大线程数,不会增长,固定的,指定多少就是多少,超过的会使用无界队列来缓存
newSingleThreadExcecutor():核心线程数和最大线程数都是1,在任何情况下都是一个线程在执行一个任务,如果有多个任务,放在阻塞队列中 生产者消费者模式就可以使用这个
核心线程池的底层实现
ThreadPoolExecutor构造方法的最后一个参数指定了拒绝策略,当提交给线程池的任务量超过实际承载能力时,如何处理?即线程池中的线程已经用完了,等待队列也满了,无法为新提交的任务服务,可以通过拒绝策略来处理这个问题
AbortPolicy策略:会抛出异常
CallorRunsPolicy策略:只要线程池没有关闭,会在调用者线程中运行当前被丢弃的任务
DiscardOldestPolicy策略:将任务队列中最老的任务丢弃,尝试再次提交新任务
DiscardPolicy策略:直接选择丢弃这个无法处理的任务
Executor工具类提供的静态方法返回的线程池默认的拒绝策略是AbortPolicy抛出异常
package pers.chenjiahao.threadpool;import java.util.Random;import java.util.concurrent.*;/** * 自定义拒绝策略 * @Author ChenJiahao * @Date 2021/3/18 21:39 */public class Test03 { public static void main(String[] args) { // 定义一个任务 Runnable r = new Runnable() { @Override public void run() { int num = new Random().nextInt(5); System.out.println(Thread.currentThread().getId() + \"--\" + System.currentTimeMillis() + \"开始睡眠\" + num + \"秒\
如果内置的策略无法满足实际需求,可以扩展RejectedExecutionHandler接口
JDK提供了四种拒绝策略(都是ThreadPoolExecutor的内部类)
拒绝策略
线程池中的线程从哪来的?由ThreadFactory创建的
ThreadFactory是一个接口,只有一个用来创建线程的方法:Thread new Thread(Runnable r);
当线程池当中需要创建线程时就会调用该方法
package pers.chenjiahao.threadpool;import java.util.Random;import java.util.concurrent.SynchronousQueue;import java.util.concurrent.ThreadFactory;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;/** * 自定义线程工厂 * @Author ChenJiahao * @Date 2021/3/18 21:51 */public class Test04 { public static void main(String[] args) { Runnable r = new Runnable() { @Override public void run() { int num = new Random().nextInt(10); System.out.println(Thread.currentThread().getId() + \"--\" + System.currentTimeMillis() + \"开始睡眠:\" + num + \"秒\"); try { TimeUnit.SECONDS.sleep(num); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getId() + \"--\" + System.currentTimeMillis() + \"结束睡眠睡眠\
ThreadFactory
ThreadPoolExecutor提供了一组方法用于监控线程池
int getActiveCount() 获得线程池中当前活动线程的数量
long getCompletedTaskCount() 返回线程池完成任务的数量
int getCorePoolSize() 返回线程池中核心线程的数量
int getLargestPoolSize() 返回线程池曾经达到的线程的最大数
int getMaxinumPoolSize() 返回线程池的最大容量
int getPoolSize() 当前线程池的大小
BloockingQueue<Runnable> getQueue() 返回阻塞队列
long getTaskCount() 返回线程池收到的任务总数
ThreadFactory getThreadFactory() 返回线程工厂
监控线程池
有时需要对线程池进行扩展,如在监控每个任务的开始和结束时间,或者自定义一些其他增强的功能
ThreadPoolExecutor线程池提供了两个方法
在线程池执行某个任务前会调用beforeExecute()方法,在任务结束后(任务异常退出)会执行afterExecute()方法<br>
查看ThreadPoolExecutor源码,在该类中定义了一个内部类Worker,ThreadPoolExecutor线程池中的工作线程就是Worker类的实例,worker实例在执行时也会调用beforeExecute()与afterExecute()方法
package pers.chenjiahao.threadpool;import java.util.concurrent.LinkedBlockingQueue;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;/** * 扩展线程池 * @Author ChenJiahao * @Date 2021/3/18 22:39 */public class Test06 { // 定义任务类 private static class MyTask implements Runnable{ String name; public MyTask(String name) { this.name = name; } @Override public void run() { System.out.println(name + \"任务正在被线程\" + Thread.currentThread().getId() + \"执行\
扩展线程池
线程池大小对系统性能是有一定影响的吗,过大或者过小都无法发挥最优的系统性能,线程池大小不需要做的非常精确,只要避免极大或者极小的情况即可,一般来说,线程池大小需要考虑CPU数量,内存大小等因素,在<Java Concurrency in Practice>书中给出一个估算线程池大小的公式
线程池大小 = CPU的数量 * 目标CPU的使用率 * ( 1 + 等待时间与计算时间的比)
优化线程池大小
如果在线程池当中的任务A在执行过程中又向线程池提交了任务B,任务B添加到线程池的等待队列中,如果任务A的结束需要等待任务B的执行结果,就有可能会出现这种情况,线程池中所有的工作线程都处于等待任务处理结果,而这些任务在阻塞队列中等待执行,线程池中没有可以对阻塞队列中的任务进行处理的线程,这种等待会一直持续下去,从而造成死锁
适合给线程池提交相互独立的任务,而不是彼此依赖的任务,对于彼此依赖的任务,可以考虑分别提交给不同的线程池来执行
线程池死锁
在使用ThreadPoolExecutor进行submit提交任务时,有的任务抛出了异常,但是线程池并没有进行提交,即线程池把任务中的异常给吃掉了
代码:演示异常被吃掉的情况
把submit提交改为execute执行
可以对ThreadPoolExecutor线程池进行扩展(对提交的任务进行包装)
解决方法
代码:演示解决方法
线程中的异常处理
“分而治之”是一个有效的处理大数据的方法,著名的MapReduce就是采用这种分而治之的思路
简单点说,如果要处理1000个数据,但是我们不具备处理1000个数据的能力,只处理10个数据;可以把1000个数据分阶段处理100次,每次处理10个,把100次处理结果进行合成,形成最后这1000个数据的处理结果
把一个大任务调用fork()方法分解为若干小的任务,把小任务的处理结果进行join()合并为大任务的结果
系统对ForkJoinPool线程池进行了优化,提交的任务数量与线程的数量不一定是一对一关系,在多数情况下,一个物理线程实际上需要处理多个逻辑任务
ForkJoinPool线程池中最常用的方法是: <T>ForkJoinTask<T> submit(ForkJoinTask<T> task) 向线程池提交一个ForkJoinTask任务, ForkJoinTask任务支持fork()分解与join()等待的任务
ForkJoinTask有两个重要的子类: RecursiveAction、RecursiveTask 它们的区别在于RecursiveAction任务没有返回值 RecursiveTask任务有返回值
代码(使用ForkJoinTask线程池模拟数列求和)
ForkJoinPool线程池
线程池
线程管理
从面向对象设计的角度出发介绍几种保障线程安全的设计技术吗,这些技术可以使得我们在不必借助锁的情况下保障线程安全,避免锁可能导致的问题及开销
Java运行时(Java runtime)空间可以分为栈区、堆区、方法区(非堆空间)
栈空间(Stack Space)为线程的执行准备一段固定大小的存储空间,每个线程都有独立的线程栈空间,创建线程时就为线程分配栈空间,线程栈中每调用一个方法就给方法分配一个栈帧,栈帧用于存储方法的局部变量,返回值等私有数据,即局部变量存储在栈空间中,基本类型变量也是存储在栈空间中,引用类型变量值也是存储在栈空间中的,引用的对象存储在堆中。由于线程栈是独立的,一个线程不能访问另外一个线程的栈空间,因此线程堆局部变量以及只能通过当前线程的局部变量才能访问的对象进行的操作具有固定的线程安全性总结:局部变量在操作的时候是线程安全的
堆空间(Heap Space)用于存储对象,是在JVM启动时分配的一段可以动态扩容的内存空间,创建对象时,在堆空间中给对象分配存储空间,实例变量就只存储在堆空间中,堆空间是多个线程之间可以共享的空间,因此实例变量可以被多个线程共享总结:多个线程同时操作实例变量,可能存在线程安全问题
方法区/非堆空间(Non-Heap Space)用于存储常量,类的元数据等,非堆空间也是在JVM启动时分配的一段可以动态扩容的存储空间,类的元数据包括静态变量,类有哪些方法及这些方法的元数据(方法名、参数、返回值等)。非堆空间也是多个线程可以共享的总结:访问非堆空间中的静态变量也可能存在线程安全问题
堆空间与非堆空间是线程可以共享的空间,即实例变量与静态变量是线程可以共享的,可能存在线程安全问题,栈空间是线程私有的存储空间,局部变量存储在栈空间中,局部变量具有固有的线程安全性
Java运行时存储空间
对象就是数据及堆数据操作的封装,对象所包含的数据称为对象的状态(State),实例变量与静态变量称为状态变量
状态就是数据
如果一个类的同一个实例被多个线程共享并不会使这些线程存在共享的状态,那么该类的梳理就称为无状态对象(Stateless Object),反之如果一个类的实例被多个线程共享会使这些线程存在共享状态,那么该类的实例称为有状态对象,实际上无状态对象就是不包含任何实例变量的对象,也不包含任何静态变量的对象
线程安全问题的前提是多个线程存在共享的数据,实现线程安全的一种办法就是避免在多个线程之间共享数据,使用无状态对象就是这种办法
无状态对象
不可变对象是指一经创建它的状态就保持不变的对象(状态就是数据),不可变对象具有固有的线程安全性,当不可变对象现实实体的状态发生变化时,系统会创建一个新的不可变对象,就如String字符串对象。
类本身使用final修饰,防止通过创建子类来改变它的定义
所有的字段都是final修饰的,final字段在创建对象时必须显示初始化,不能被修改
如果字段引用了其他状态可变的对象(集合,数组),则这些字段必须是private私有的
一个不可变对象需要满足这些条件
被建模对象的状态变化不频繁
同时对一组相关数据进行写操作,可以应用不可变对象,即可以保障原子性也可以避免锁的使用
使用不可变对象作为安全可靠的Map键,HashMap键值对的存储位置与键的hashCode有关,如果键的内部状态发生了变化,会导致键的哈希码不同,可能会影响键值对的存储位置,如果HashMap的键是一个不可变对象,则hashCode()方法的返回值是恒定的,存储位置是固定的
不可变对象主要的应用场景
不可变对象
我们可以选择不共享非线程安全的对象,对于非线程安全的对象,每个线程都创建一个该对象的实例,各个线程访问各自创建的实例,一个线程不能访问另外一个线程创建的实例,这种各个线程创建各自的实例,一个实例只能被一个线程访问的对象就称为线程特有对象,县城特有对象既保障了堆非线程安全对象的访问的线程安全,又避免了锁的开销,线程特有对象也具有固有的线程安全性
ThreadLocal<T>类相当于线程访问其特有对象的代理,即各个线程通过ThreadLocal对象可以创建并访问各自的线程特有对象,泛型T指定了线程特有对象的类型,一个线程可以使用不同的ThreadLocal实例来创建并访问不同的线程特有对象
线程特有对象
装饰器模式可以用来实现线程安全,基本思想是为非线程安全的对象创建一个相应的线程安全的外包装对象,客户端代码不直接访问非线程安全的对象而是访问它的外包装对象。外包装对象与非线程安全的对象具有相同的接口,即外包装对象的使用方式与非线程安全对象的使用方式相同,而外包装对象内部通常会借助锁,以线程安全的方式调用相应的非线程安全对象的方法
在java.util.Collections工具中提供了一组synchronizedXXX(xxx)方法,可以把不是线程安全的xxx集合转换为线程安全的集合,它就是采用了这种装饰器模式,这个方法的返回值就是指定集合的外包装对象,这类集合又称为同步集合
使用装饰器模式的一个好处就是实现关注点分离,在这种设计中,实现同一组功能的对象的两个版本:非线程安全的对象与线程安全的对象,对于非线程安全的对象在设计时只关注要实现的功能,对于线程安全的版本只关注线程安全性
装饰器模式
保障线程安全的设计技术
对于使用锁进行并发控制的应用程序来说,如果单个线程持有锁的时间过长,会导致锁的竞争更加激烈,会影响系统的性能,在程序中需要尽可能减少线程堆锁的持有时间,如下面代码: public synchronized void syncMethod(){ otherCode1(); mutexMethod(); otherCode(); } 在syncMethod同步方法中,假设只有mutexMethod()方法是需要同步的,otherCode1()方法与otherCode2()方法不需要同步,如果otherCode1() 与otherCode2()这两个方法需要花费较长的cpu时间,在并发量较大的情况下,这种同步方案会导致等待线程的大量增加 优化方案: 只在必要时进行同步,可以减少锁的持有时间,提高系统的吞吐量,如把上面的代码改为: public void syncMethod(){ otherCode1(); synchronized(this){ mutexMethod(); } otherCode(); } 支队mutexMethod()方法进行同步,这种减少锁持有时间有助于降低锁冲突的可能性,提升系统的并发能力
减少锁持有的时间
一个锁保护的共享数据的数量大小称为锁的粒度,如果一个锁保护的共享数据的数量大,就称该锁的粒度粗,否则称该锁的粒度细,锁的粒度过粗会导致线程在申请锁时需要进行不必要的等待,例如:银行不同的柜台办理不同的业务
减少锁粒度是一种削弱多线程锁竞争的一种手段,可以提高系统的并发性
在JDK7前,java.util.concurrent.ConcurrentHashMap类采用分段锁协议,可以提高程序的并发性
Hashtable是锁了整个表,ConcurrentHashMap是锁了每个集合中的位置
举例:张三上厕所 去了Hashtable厕所,11个坑位,张三进厕所了把厕所的大门锁住了,11个坑位,只能他一个人上 去了ConcurrentHashMap厕所,16个坑位,张三进了坑位把坑位的门锁了,还有15个坑位可以让他人使用这就减少了锁的粒度,可以提高系统的并发性
减小锁的粒度
使用ReadWriteLock读写分离锁可以提高系统性能,使用读写分离锁也是减小锁粒度的一种特殊情况,第二条建议是通过分割数据结构实现减小锁的粒度,读写锁是对系统功能点的分割。
多数情况下都允许多个线程同时读,在写的时候采用独占锁,在读多写少的情况下,使用读写锁可以大大提高系统的并发能力
使用读写分离锁代替独占锁
将读写锁的思想进一步延伸就是锁分离,读写锁是根据读写操作功能上的不同进行了锁分离,根据应用程序功能的特点,也可以对独占锁进行分离,如java.util.concurrent.LinkedBlockingQueue类中take()与put()方法分别从队头取数据,把数据添加到队尾,虽然这两个方法都是对队列进行修改操作,由于操作的主体是链表,take()操作的是链表的头部,put()操作的是链表的尾部,两者并不冲突,如果采用独占锁的话,这两个操作不能同时并发,在该类中就采用锁分离,take()取数据时有取锁,put()添加数据时有自己的添加锁,这样take()与put()相互独占实现了并发
锁分离
为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽量短,但是凡是都有一个度,如果对同一个锁不断的进行请求,同步和释放,也会消耗系统资源,如: public void method1(){ synchronized(lock){ 同步代码块1 } synchronized(lock){ 同步代码块2 } }JVM在遇到一连串不断地对同一个锁进行请求和释放操作时,会把所有的锁整合成对锁的一次请求,从而减少对锁的请求次数,这个操作叫锁的粗化,这样上述代码会整合为: public void method1(){ synchronized(lock){ 同步代码块1 同步代码块2 } }
在开发过程中,也应该有意识的在合理的场合进行锁的粗化,尤其在循环体内请求锁时,如: for(int i = 0;i < 100; i++){ synchronized(lock){} }这种情况下,意味着每次循环都需要申请和释放锁,所以一种更合理的做法就是在循环外请求一次锁,如: synchronized(lock){ for(int i = 0;int < 100; i++){ } }
锁粗化
有助于提高锁性能的几点建议
锁偏向是一种针对加锁操作的优化,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,这样可以节省有关锁申请的时间,提高了程序的性能
锁偏向再没有锁竞争的场合可以有较好的优化效果,对于锁竞争比较激烈的场景,效果不佳,锁竞争激烈的情况下可能是每次都是不同的线程来请求锁,这时偏向模式失效
锁偏向
如果锁偏向失败,JVM不会立即挂起线程还会使用一种称为轻量级锁的优化手段,会将共享对象的头部作为指针,指向持有锁的线程堆栈内部,来判断一个线程是否持有对象锁,如果线程获得轻量级锁成功,就进入临界区。如果获得轻量级锁失败,表示其他线程抢到了锁,那么当前线程的锁的请求就膨胀为重量级锁,当前线程就撞到阻塞队列中变为阻塞状态。
偏向锁、轻量级锁都是乐观锁,重量级锁都是悲观锁
一个对象刚开始实例化时,没有任何线程访问它,它是可偏向的,即它认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程。偏向第一个线程,这个线程在修改对象头称为偏向锁时使用CAS操作,将对象头中ThreadId改成自己的ID,之后再访问这个对象时,只需要对比ID即可,一旦有第二个线程访问该对象,因为偏向锁不会主动释放,所以第二个线程可以查看对象的偏向状态,当第二个线程访问对象时,表明再这个对象上已经存在竞争了,检查原来持有对象锁的线程是否处于活动状态,如果挂了则将对象变为无锁状态,然后重新偏向新的线程;如果原来的线程依然存活,马上执行原来线程的栈,检查该对象的使用情况,如果仍然需要偏向锁,则偏向锁升级为轻量级锁
轻量级锁认为竞争存在,但是竞争的程度很轻,一旦两个线程对同一个锁的操作会错开,或者稍微等待一下(自旋)另外一个线程就会释放锁,当自旋超过一定次数,或者一个线程池持有锁,一个线程再自旋,又来第三个线程访问时,轻量级锁会膨胀为重量级锁,重量级锁除了持有锁的线程外,其他的线程都阻塞
轻量级锁
JVM对锁的优化
锁的优化及注意事项
原文链接:https://www.cnblogs.com/liuxiaozhi23/p/10880147.html
Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。使用Lambda 表达式可以使代码变的更加简洁紧凑。
语法:(parameters) -> expression或(parameters) ->{statements;}
可选类型声明:不需要声明参数类型,编译器可以统一识别参数值
可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号
可选的大括号:如果主体包含了一个语句,就不需要使用大括号
可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值
重要特征
Lambda 表达式主要用来定义行内执行的方法类型接口,例如,一个简单方法接口。在上面例子中,我们使用各种类型的Lambda表达式来定义MathOperation接口的方法。然后我们定义了sayMessage的执行。
Lambda 表达式免去了使用匿名方法的麻烦,并且给予Java简单但是强大的函数化的编程能力。
注意两点
lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在lambda 内部修改定义在域外的局部变量,否则会编译错误。
package pers.chenjiahao.lambda;/** * @Author ChenJiahao * @Date 2021/6/29 21:13 */public class Java8Tester1 { final static String salutation = \"Hello!\"; public static void main(String args[]) { GreetingService greetService1 = message -> System.out.println(salutation + message); greetService1.sayMessage(\"Runoob\");//====================相当于下面============================== GreetingService g = new GreetingService() { @Override public void sayMessage(String message) { System.out.println(salutation + message); } }; g.sayMessage(\"jack\"); } interface GreetingService { void sayMessage(String message); }}
可以直接在lambda 表达式中访问外层的局部变量
lambda 表达式的局部变量可以不用声明为 final,但是必须不可被后面的代码修改即隐性的具有final 的语义)
变量作用域
Lambda 表达式
方法引用通过方法的名字来指向一个方法。方法引用可以使语言的构造更紧凑简洁,减少冗余代码。方法引用使用一对冒号 :: 。
构造器引用语法:Class< T >::new
静态方法引用:Class::static_method
特定类的任意对象的方法引用:Class::method
特定对象的方法引用:instance::method
package pers.chenjiahao.methodreference;import java.util.Arrays;import java.util.List;/** * @Author ChenJiahao * @Date 2021/6/29 21:22 */public class Car { @FunctionalInterface public interface Supplier<T> { T get(); } //Supplier是jdk1.8的接口,这里和lamda一起使用了 public static Car create(final Supplier<Car> supplier) { return supplier.get(); } public static void collide(final Car car) { System.out.println(\"Collided \" + car.toString()); } public void follow(final Car another) { System.out.println(\"Following the \" + another.toString()); } public void repair() { System.out.println(\"Repaired \" + this.toString()); } public static void main(String[] args) { // 构造器引用:它的语法是Class::new,或者更一般的Class< T >::new实例如下: System.out.println(\"==================构造器引用=====================\
package pers.chenjiahao.methodreference;import java.util.ArrayList;import java.util.List;/** * @Author ChenJiahao * @Date 2021/6/29 21:33 */public class Java8Tester { public static void main(String[] args) { List names = new ArrayList(); names.add(\"Google\"); names.add(\"Runoob\"); names.add(\"Taobao\"); names.add(\"Baidu\"); names.add(\"sina\"); names.forEach(System.out::println); }}
System.out::println 静态方法代码示例
方法引用
函数式接口(FunctionalInterface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。函数式接口可以被隐式转换为lambda表达式。函数式接口可以现有的函数友好地支持 lambda。
java.lang.Runnable· java.util.concurrent.Callable· java.security.PrivilegedAction· java.util.Comparator· java.io.FileFilter· java.nio.file.PathMatcher· java.lang.reflect.InvocationHandler· java.beans.PropertyChangeListener· java.awt.event.ActionListener· javax.swing.event.ChangeListener
JDK 1.8之前已有的函数式接口
JDK 1.8 新增加的函数接口
它包含了很多类,用来支持 Java的函数式编程,该包中的函数式接口有:
java.util.function
Predicate <T> 接口是一个函数式接口,它接受一个输入参数 T,返回一个布尔值结果。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非)。该接口用于测试对象是 true 或 false。
n是一个参数传递到Predicate接口的test方法n如果存在则test方法返回truePredicate<Integer> predicate = n -> true;代码如下
n是一个参数传递到Predicate接口的test方法如果n%2==0 test方法返回truePredicate<Integer> predicate = n -> n % 2 == 0;代码如下
n是一个参数传递到Predicate接口的test方法如果n大于3 test方法返回truePredicate<Integer> predicate = n -> n > 3;代码如下
函数式接口实例
Java 8 新增了接口的默认方法。简单说,默认方法就是接口可以有实现方法,而且不需要实现类去实现其方法。我们只需在方法名前面加个default关键字即可实现默认方法
首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的java 8之前的集合框架没有foreach方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。
为什么要有这个特性?
默认方法语法格式,接口中的方法前加default:public interface Vehicle { default void print() { System.out.println(\"我是一辆车!\"); }}
默认方法
一个接口有默认方法,考虑这样的情况,一个类实现了多个接口,且这些接口有相同的默认方法,以下实例说明了这种情况的解决方法
public interface Vehicle { default void print() { System.out.println(\"我是一辆车!\"); }}
Vehicle接口
public interface FourWheeler { default void print() { System.out.println(\"我是一辆四轮车!\"); }}
FourWheeler接口
第一个解决方案是创建自己的默认方法,来覆盖重写接口的默认方法
第二个解决方案是使用super来调用接口指定的默认方法
多个默认方法
package pers.chenjiahao.defaultmethod;/** * @Author ChenJiahao * @Date 2021/6/29 21:58 */public interface Vehicle { default void print(){ System.out.println(\"我是一辆车!\"); } // 静态方法 static void blowHorn(){ System.out.println(\"按喇叭\"); }}
Java 8 的另一个特性是接口可以声明(并且可以提供实现)静态方法
package pers.chenjiahao.defaultmethod;/** * @Author ChenJiahao * @Date 2021/6/29 22:07 */public class Java8Tester { public static void main(String[] args) { Vehicle vehicle = new Car(); vehicle.print(); } public interface Vehicle { default void print(){ System.out.println(\"我是一辆车!\"); } // 静态方法 static void blowHorn(){ System.out.println(\"按喇叭\"); } } public interface FourWheeler { default void print() { System.out.println(\"我是一辆四轮车!\
综合代码
java集合运算和表达,整个Stream像SQL一样
Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。Stream使用一种类似用SQL语句从数据库查询数据的直观方式来提供一种对Java集合运算和表达的高阶抽象。Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选,排序,聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。
和以前的Collection操作不同,Stream操作还有两个基础的特征
什么是 Stream
public static void main(String[] args) { List<String> strings = Arrays.asList(\"abc\
代码这个代码相当于把第一个集合中的空元素过滤掉了,然后重新生成了集合
生成流:Object.stream()
Stream 提供了新的方法 'forEach' 来迭代流中的每个数据。以下代码片段使用forEach 输出了10个随机数:
package pers.chenjiahao.stream;import java.util.Random;/** * @Author ChenJiahao * @Date 2021/6/29 22:23 */public class Java8Tester2 { public static void main(String[] args) { Random random = new Random(); random.ints().limit(10).forEach(System.out::println); }}
forEach
map 方法用于映射每个元素到对应的结果,以下代码片段使用 map 输出了元素对应的平方数,然后用distinct去重
map
filter 方法用于通过设置条件过滤出元素。以下代码片段使用filter 方法过滤出空字符串:
取到false的就不要了
package pers.chenjiahao.stream;import java.util.Arrays;import java.util.List;import java.util.stream.Collectors;/** * @Author ChenJiahao * @Date 2021/6/29 22:36 */public class Java8Tester4 { public static void main(String[] args) { List<String> strings = Arrays.asList(\"abc\
filter
limit 方法用于获取指定数量的流。以下代码片段使用 limit 方法打印出 10 条数据
package pers.chenjiahao.stream;import java.util.Random;/** * @Author ChenJiahao * @Date 2021/6/29 22:40 */public class Java8Tester2 { public static void main(String[] args) { Random random = new Random(); random.ints().limit(10).forEach(System.out::println); }}
limit
sorted 方法用于对流进行排序。以下代码片段使用 sorted 方法对输出的 10 个随机数进行排序:
package pers.chenjiahao.stream;import java.util.Random;/** * @Author ChenJiahao * @Date 2021/6/29 22:41 */public class Java8Tester5 { public static void main(String[] args) { Random random = new Random(); random.ints().limit(10).sorted().forEach(System.out::println); }}
sorted
parallelStream 是流并行处理程序的代替方法。以下实例我们使用parallelStream 来输出空字符串的数量:
package pers.chenjiahao.stream;import java.util.Arrays;import java.util.List;/** * @Author ChenJiahao * @Date 2021/6/29 22:42 */public class Java8Tester6 { public static void main(String[] args) { List<String> strings = Arrays.asList(\"abc\
parallel(并行程序)
Collectors 类实现了很多归约操作,例如将流转换成集合和聚合元素。Collectors可用于返回列表或字符串:
package pers.chenjiahao.stream;import java.util.Arrays;import java.util.List;import java.util.stream.Collectors;/** * @Author ChenJiahao * @Date 2021/6/29 22:46 */public class Java8Tester7 { public static void main(String[] args) { List<String> strings = Arrays.asList(\"abc\
代码筛选和合并
Collectors
一些产生统计结果的收集器也非常有用。它们主要用于int、double、long等基本类型上,它们可以用来产生类似如下的统计结果
统计
package pers.chenjiahao.stream;import java.util.*;import java.util.stream.Collectors;/** * @Author ChenJiahao * @Date 2021/6/29 22:56 */public class Java8Tester9 { public static void main(String args[]) { System.out.println(\"使用 Java 7: \"); // 计算空字符串 List<String> strings = Arrays.asList(\"abc\
Stream
Optional类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。Optional是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。Optional类的引入很好的解决空指针异常。
类的声明:public final class Optional<T>
类方法
Optional.offNullable 允许传递为null的参数
Optional.of 如果传递的参数是null,抛出异常NullPointerException
Optional.isPresent 判断值是否存在
Optional.orElse 如果值存在,返回它,否则返回默认值
Optional.get 获取值,值需要存在
主要方法
Optional
Nashorn JavaScript
Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理
非线程安全 − java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一
设计很差 − Java的日期/时间类的定义并不一致,在java.util和java.sql的包中都有日期类,此外用于格式化和解析的类在java.text包中定义。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计。
时区处理麻烦 − 日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。
在旧版的Java 中,日期时间API 存在诸多问题
Local(本地) − 简化了日期时间的处理,没有时区的问题 Zoned(时区) − 通过制定的时区处理日期时间。
新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。
Java 8 在 java.time 包下提供了很多新的 API。以下为两个比较重要的 API
package pers.chenjiahao.datetimeAPI;import java.time.*;/** * @Author ChenJiahao * @Date 2021/6/29 23:15 */public class Java8Tester { public static void main(String[] args) { Java8Tester java8Tester = new Java8Tester(); java8Tester.testLocalDateTime(); } private void testLocalDateTime() { // 获取当前的日期时间 LocalDateTime currentTime = LocalDateTime.now(); System.out.println(\"当前时间:\
LocalDate/LocalTime 和 LocalDateTime 类可以在处理时区不是必须的情况。代码如下:
package pers.chenjiahao.datetimeAPI;import java.time.ZoneId;import java.time.ZonedDateTime;/** * @Author ChenJiahao * @Date 2021/6/29 23:29 */public class Java8Tester2 { public static void main(String[] args) { Java8Tester2 java8Tester = new Java8Tester2(); java8Tester.testZoneDateTime(); } private void testZoneDateTime() { // 获取当前时间日期 ZonedDateTime date1 = ZonedDateTime.parse(\"2015-12-03T10:15:30+05:30[Asia/Shanghai]\"); System.out.println(date1); ZoneId id = ZoneId.of(\"Europe/Paris\"); System.out.println(id); ZoneId currentZone = ZoneId.systemDefault(); System.out.println(currentZone); }}
如果我们需要考虑到时区,就可以使用时区的日期时间API:代码如下
日期时间 API
Base64
JDK8新特性
源文章:blog.csdn.net/qq_41463655/article/details/100839629
Quartz表达式生成地址: http://cron.qqe2.com/
xxl-job
springboot 的 @Scheduled
Quartz 框架
使用Quartz表达式的定时任务
package pers.chenjiahao.scheduletask.jobthread;/** * @Author ChenJiahao * @Date 2021/6/30 21:17 */public class JobThread { public static class Test01{ static long count = 0; public static void main(String[] args) { Runnable runnable = new Runnable() { @Override public void run() { while (true){ try { Thread.sleep(1000); count++; System.out.println(count); } catch (InterruptedException e) { e.printStackTrace(); } } } }; Thread thread = new Thread(runnable); thread.start(); } }}
使用线程创建 job 定时任务
使用 TimerTask 创建job定时任务
使用线程池创建 job定时任务
<!-- quartz -->\t\t<dependency>\t\t\t<groupId>org.quartz-scheduler</groupId>\t\t\t<artifactId>quartz</artifactId>\t\t\t<version>2.2.1</version>\t\t</dependency>\t\t<dependency>\t\t\t<groupId>org.quartz-scheduler</groupId>\t\t\t<artifactId>quartz-jobs</artifactId>\t\t\t<version>2.2.1</version>\t\t</dependency>
添加依赖
package pers.chenjiahao.scheduletask.quartzschedule;import org.quartz.*;import org.quartz.impl.StdSchedulerFactory;import java.util.Date;/** * @Author ChenJiahao * @Date 2021/6/30 21:44 */public class JobQuartz { public static void main(String[] args) { // 1.创建Scheduler工厂 SchedulerFactory schedulerFactory = new StdSchedulerFactory(); // 2.从工厂中获取调度器实例 Scheduler scheduler = null; try { scheduler = schedulerFactory.getScheduler(); } catch (SchedulerException e) { e.printStackTrace(); } // 3.创建JobDetail JobDetail jobDetail = JobBuilder.newJob(MyJob.class) // job的描述 .withDescription(\"this is a job\") // job的name和组 .withIdentity(\"jobName\
任务调度类
类上要加三个注解@Component@Configuration // 主要用于标记配置类@EnableScheduling // 开启定时任务
package pers.chenjiahao.scheduletask.scheduledannotation;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.annotation.EnableScheduling;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;import java.time.LocalDateTime;/** * @Author ChenJiahao * @Date 2021/6/30 22:16 */@Component@Configuration // 1.主要用于标记配置类@EnableScheduling // 2.开启定时任务public class StaticScheduleTask { @Scheduled(cron = \"0/5 * * * * ?\") // 3.添加定时任务 // @Scheduled(fixedDelay = 5000) // 直接指定时间间隔,例如:5秒 private void configureTasks(){ System.out.println(\"执行静态定时任务时间:\" + LocalDateTime.now()); }}
配置类
package pers.chenjiahao.scheduletask.scheduledannotation;import org.springframework.context.annotation.AnnotationConfigApplicationContext;/** * @Author ChenJiahao * @Date 2021/6/30 22:21 */public class Main { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(StaticScheduleTask.class); }}
springboot 的 @Scheduled 注解
job 定时任务的五种创建方式
@Scheduled(cron = \"\") 中的corn包含了执行周期
springboot项目启动时@Scheduled自动生效?
xxl-job 任务调度后台 Admin
定时任务
引自https://www.cnblogs.com/jpfss/p/10273129.htmlhttps://blog.csdn.net/weixin_39985472/article/details/110527134
但是在处理与第三方系统交互的时候,容易造成响应迟缓的情况,之前大部分都是使用多线程来完成此类任务,其实,在spring 3.x之后,就已经内置了@Async来完美解决这个问题。
在解释异步调用之前,我们先来看同步调用的定义;同步就是整个处理过程顺序执行,当各个过程都执行完毕,并返回结果。 异步调用则是只是发送了调用的指令,调用者无需等待被调用的方法完全执行完毕;而是继续执行下面的流程。
何为异步调用
在Java中,一般在处理类似的场景之时,都是基于创建独立的线程去完成相应的异步调用逻辑,通过主线程和不同的线程之间的执行流程,从而在启动独立的线程之后,主线程继续执行而不会产生停滞等待的情况。
常规的异步调用处理方式
在Spring中,基于@Async标注的方法,称之为异步方法;这些方法将在执行的时候,将会在独立的线程中被执行,调用者无需等待它的完成,即可继续其他的操作。
@Async介绍
@Configuration@EnableAsyncpublic class SpringAsyncConfig { ... }
基于Java配置的启用方式:
<task:executor id=\"myexecutor\" pool-size=\"5\" /><task:annotation-driven executor=\"myexecutor\"/>
基于XML配置文件的启用方式
@Componentpublic class AsyncTask { private static Random random = new Random(); @Async public void doTaskOne() throws Exception { System.out.println(\"开始做任务一\"); long start = System.currentTimeMillis(); Thread.sleep(random.nextInt(10000)); long end = System.currentTimeMillis(); System.out.println(\"完成任务一,耗时:\" + (end - start) + \"毫秒\"); } @Async public void doTaskTwo() throws Exception { System.out.println(\"开始做任务二\"); long start = System.currentTimeMillis(); Thread.sleep(random.nextInt(10000)); long end = System.currentTimeMillis(); System.out.println(\"完成任务二,耗时:\" + (end - start) + \"毫秒\"); } @Async public void doTaskThree() throws Exception { System.out.println(\"开始做任务三\"); long start = System.currentTimeMillis(); Thread.sleep(random.nextInt(10000)); long end = System.currentTimeMillis(); System.out.println(\"完成任务三,耗时:\" + (end - start) + \"毫秒\"); }}
1.假如我们有一个Task类,其中有三个任务需要异步执行,那么我们就可以将这些任务方法标上@Async注解,使其成为异步方法
2.为了让@Async注解能够生效,还需要在Spring Boot的主程序中配置@EnableAsync
@RunWith(SpringJUnit4ClassRunner.class)@SpringApplicationConfiguration(classes = Application.class)public class ApplicationTests { @Autowired private Task task; @Test public void test() throws Exception { task.doTaskOne(); task.doTaskTwo(); task.doTaskThree(); }}
3.写一个单元测试进行测试一下
此时可以反复执行单元测试,您可能会遇到各种不同的结果,比如: - 没有任何任务相关的输出 - 有部分任务相关的输出 - 乱序的任务相关的输出
原因是目前doTaskOne、doTaskTwo、doTaskThree三个函数的时候已经是异步执行了。主程序在异步调用之后,主程序并不会理会这三个函数是否执行完成了,由于没有其他需要执行的内容,所以程序就自动结束了,导致了不完整或是没有输出任务相关内容的情况。
结论
注:@Async所修饰的函数不要定义为static类型,这样异步调用不会生效。
如何在Spring中启用@Async
@Async //标注使用public void asyncMethodWithVoidReturnType() { System.out.println(\"Execute method asynchronously - \"+ Thread.currentThread().getName());}
基于@Async无返回值调用
@Asyncpublic Future<String> asyncMethodWithReturnType() { System.out.println(\"Execute method asynchronously - \"+ Thread.currentThread().getName());try { Thread.sleep(5000); return new AsyncResult<String>(\"hello world !!!!\");} catch (InterruptedException e) { //} return null;}
分析: 这些获取异步方法的结果信息,是通过不停的检查Future的状态来获取当前的异步方法是否执行完毕来实现的。
基于@Async返回值的调用
在异步方法中,如果出现异常,对于调用者caller而言,是无法感知的。如果确实需要进行异常处理,则按照如下方法来进行处理:
在这里定义处理具体异常的逻辑和方式。
自定义实现AsyncTaskExecutor的任务执行器
配置由自定义的TaskExecutor替代内置的任务执行器
handle()就是未来我们需要关注的异常处理的地方。
<task:annotation-driven executor=\"exceptionHandlingTaskExecutor\" scheduler=\"defaultTaskScheduler\" /><bean id=\"exceptionHandlingTaskExecutor\" class=\"nl.jborsje.blog.examples.ExceptionHandlingAsyncTaskExecutor\"> <constructor-arg ref=\"defaultTaskExecutor\" /></bean><task:executor id=\"defaultTaskExecutor\" pool-size=\"5\" /><task:scheduler id=\"defaultTaskScheduler\" pool-size=\"1\" />
配置文件中的内容:
分析: 这里的配置使用自定义的taskExecutor来替代缺省的TaskExecutor。
基于@Async调用中的异常处理机制
在@Async标注的方法,同时也适用了@Transactional进行了标注;在其调用数据库操作之时,将无法产生事务管理的控制,原因就在于其是基于异步处理的操作。
那该如何给这些操作添加事务管理呢?可以将需要事务管理操作的方法放置到异步方法内部,在内部被调用的方法上添加@Transactional.
方法A,使用了@Async/@Transactional来标注,但是无法产生事务控制的目的。
方法B,使用了@Async来标注, B中调用了C、D,C/D分别使用@Transactional做了标注,则可实现事务控制的目的。
@Async调用中的事务处理机制
Future是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果的接口。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
什么是Future类型
尝试取消执行此任务。
如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。
boolean cancel(boolean mayInterruptIfRunning)
如果此任务在正常完成之前被取消,则返回 true 。
boolean isCancelled()
任务是否已经完成,若任务完成,则返回true
boolean isDone()
方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回
V get()
用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。
主要的五个方法
判断任务是否完成
能够中断任务
能够获取任务执行结果
Future提供了三种功能
Task类
@Async public Future<String> doTaskOne() throws Exception { System.out.println(\"开始做任务一\"); long start = System.currentTimeMillis(); Thread.sleep(random.nextInt(10000)); long end = System.currentTimeMillis(); System.out.println(\"完成任务一,耗时:\" + (end - start) + \"毫秒\"); return new AsyncResult<>(\"任务一完成\"); }
对于上面的Task类,我们也可以简单修改一下,判断上述三个异步调用是否已经执行完成。类似于如下doTaskOne()代码进行修改:
@Test public void asyncTaskTest() throws Exception { long start = System.currentTimeMillis(); Future<String> task1 = asyncTask.doTaskOne(); Future<String> task2 = asyncTask.doTaskTwo(); Future<String> task3 = asyncTask.doTaskThree(); // 三个任务都调用完成,退出循环等待 while (!task1.isDone() || !task2.isDone() || !task3.isDone()) { Thread.sleep(1000); } long end = System.currentTimeMillis(); System.out.println(\"任务全部完成,总耗时:\" + (end - start) + \"毫秒\"); }
单元测试方法:
如果在业务场景中我们有异步以及需要知道异步方法执行结构的需求,那么@Async以及Future的组合会是个不错的选择。
作用总结
Future
@async和Future
两台计算机通过网络实现文件共享行为
什么是互联网通信
客户端计算机:用于发送请求,来索要资源文件的计算机
服务端计算机:用于接收请求,并提供对应的资源文件计算机
互联网通信过程角色划分
客户端软件专门安装在客户端计算机上
帮助客户端计算机向指定服务器端计算机发送请求,索要资源文件
帮助客户端计算机将服务端计算机发送回来的二进制数据解析为文字、数字、图片、视频、命令等
C:client software;客户端软件
安装在服务端计算机上
服务器软件用于接收来自于特定的客户端软件发送的请求
服务器软件在接收到请求之后自动的在服务端计算机上定位被访问的资源文件
自动将定位的文件内容解析为二进制数据,发给客户端
S:server software;服务端软件
普遍用于个人娱乐时长,例:微信,淘宝,京东,B站,大型游戏等等
企业办公领域相对应用较少
C/S模型使用场景
安全性较高
有效降低服务端计算机工作压力
增加客户获得服务的成本
更新较为繁琐
C/S
浏览器安装在客户端计算机上
可以向任意服务器发送请求,索要资源文件
可以将服务器返回的二进制数据解析为:文字、数字、图片、视频、命令等等
B:browser;浏览器
服务器软件专门安装在服务端计算机上
S:server software 服务器软件
广泛适用于企业日常活动
也可以适用于个人娱乐市场
B/S模型适用场景
不会增加用户获取服务的成本
几乎不需要更新浏览器
几乎无法有效对服务端计算机资源文件进行保护
服务端计算机工作压力异常巨大
B/S
互联网通信模型
共享资源文件就是可以通过网络进行传输的文件,所有的文件都是共享资源文件
文件内容固定,这种文件可以被称为静态资源文件,例:文档、图片、视频
如果文件存放的不是内容,而是命令,这种命令只能在浏览器编译与执行,这种文件可以被称为静态资源文件,例:html、css、js
静态资源文件
如果文件存放命令,并且命令不能再浏览器编译与执行;只能在服务端计算机执行,这样的文件可以被称为动态资源文件,例:.class
动态资源文件
Http服务器下对于共享资源文件的分类
静态文件被索要时:Http服务器直接通过输出流将静态文件中内容或命令以二进制形式推送给发起请求的浏览器
动态文件被索要时:Http服务器需要创建当前class文件的实例对象,通过实例对象调用对应的方法处理请求,通过输出流将运行结果以二进制形式推送给发起请求的浏览器
调用动态文件的例子
静态资源文件与动态资源文件调用的区别
共享资源文件
互联网通信流程图(第一版)
控制地址:<a></a> 、 <form></form>
控制请求方式:get/post
控制参数
控制浏览器行为
开发动态资源文件来解决用户请求
开发人员在互联网通信流程担任职责
互联网通信流程介绍
在网络中传递信息都是以二进制形式存在的
接收方(浏览器/服务器)在接收到信息后,要做的第一件事就是将二进制数据进行编译(文字、图片、视频、命令等等)
传递信息数据量往往较大,导致接收方很难在一组连续的二进制得到对应数据例:浏览器发送一个请求:http://192.168.81.128:8080/index.html 这个请求信息以二进制形式发送:010101010101010101.......... Http服务器很难从二进制数据得到相关信息
网络协议包是一组有规律的二进制数据,在这组数据中存在固定空间,每一个空间专门存放特定信息,这样接收方在接收网络协议包之后就可以到固定空间得到对应信息,网络协议包的出现极大降低了接收方二进制数据编译难度例: 0000(ip) 0000(端口号) 0000(资源文件名) 0000
网络协议包
FTP网络协议
Http网络协议
常见的网络协议
在基于B/S结构下互联网通信过程中,所有在网络中传递信息都是保存在http网络协议包中
在浏览器准备发送请求时,负责创建一个Http请求协议包,浏览器将请求信息以二进制形式保存在http请求协议包的各个空间,由浏览器负责将http请求协议包推送到指定服务端计算机
自上而下,分为四个空间
1.请求行:[ url:请求地址 method:请求方式(post/get)]
2.请求头:[ 请求参数信息(get)]
3.空白行:[ 没有任何内容,只起到隔离作用]
4.请求体:[ 请求参数信息(post)]
空间划分
http请求协议包内部空间
http请求协议包
http服务器在定位到被访问的资源文件中hi后,负责创建一个http响应协议包,http服务器将定位文件内容协议包各个空间由http服务器负责将http响应协议包推送回发起请求的浏览器上
1.状态行:[ http状态码]
2.响应头:[ content-type:指定浏览器采用相应编译器对响应体二进制数据进行解析]
3.空白行:[ 没有任何内容,只起到隔离作用]
4.响应体:[ 可能被访问的静态资源文件内容 可能被访问的静态资源文件命令 可能被访问的动态资源文件运行结果 (以上都为二进制形式)]
http响应协议包内部空间
http相应协议包
Http网络协议包
在tomcat安装地址/webapps文件夹,创建一个网站(例:myweb)
将一个静态资源文件添加到网站,(例:car.jpg)
启动tomcat
启动浏览器,命令浏览器向tomcat索要car.jpg
URL格式:网络协议包://服务端计算机IP:Http服务器端口号/网站名/资源文件名
例:http://localost:8080/myWeb/car.jpg
模拟一次互联网通信
new Module
java enterprise
Web application
-src文件夹:存放作为动态资源文件的java文件-web文件夹:存放静态资源文件(图、文本、html、css、js) -WEB-INF -lib:存放jar包 -web.xml:通知tomcat,当前网站哪些java类是动态资源文件
网站内部结构
Deployment就是把网站交给Tomcat管理,这就是发布(Deployment下方为网站别名)
IDEA创建网站
Http协议与Tomcat服务器
Servlet规范来自于JavaEE规范中的一种
在Servlet规范中,指定动态资源文件开发步骤
在Servlet规范中,指定HTTP服务器调用动态资源文件规则
在Servlet规范中,指定HTTP服务器管理动态资源文件实例对象规则
Servlet规范
Servlet接口来自于Servlet规范下一个接口,这个接口存在HTTP服务器提供jar包
Tomcat服务器下lib文件有一个servlet-api.jar,它存放着Servlet接口(javax.servlet.Servlet接口)
Servlet规范中,HTTP服务器能调用的动态资源文件必须是一个Servlet接口实现类例:class Student{} // 不是动态资源文件,Tomcat无权调用例:class Teacher implements Servlet{ // 合法资源文件,Tomcat可以调用 Servlet obj = new Teacher(); obj.doGet(); }
Servlet接口实现类
创建一个Java类继承HttpServlet父类,使之称为一个Servlet实现类接口
Java类-(继承)->HttpServlet-(继承)->GnericServlet-(实现)->Servlet和ServletConfig
子类--->父类--->A接口此时:子类也是A接口的实现类
第一步
重写HttpServlet父类两个方法,doGet()或doPost()
浏览器--(get)-->Java实现类.doGet()
浏览器--(post)-->Java实现类.doPost()
第二步
将Servlet接口实现类信息注册(写到)到Tomcat服务器中
<servlet> <serlvet-name>mm </serlvet-name> <serlvet-class>com.bjpowernode.controller.OneServlet </serlvet-class></serlvet>// tomcat中 String mm = \"com.bjpowernode.controller.OneServlet\";
1.将Servlet接口实现类的类路径地址交给Tomcat
<servlet-mapping> <serlvet-name>mm</serlvet-name> <url-pattern>/one</url-pattern></servlet-mapping>
2.为了降低用户访问Servlet接口实现类的难度,需要设置简短请求别名
3.如果现在浏览器向Tomcat索要OneServlet时,地址为:http://localhost:8080/myWeb/one
<serlvet-name>声明一个变量存储servlet接口实现类的类路径
<serlvet-class>声明servlet接口实现类类路径
<url-pattern>设置简短请求别名,别名在书写的时候必须以\"/\"为开头
在当前网站下的web.xml中写
第三步
Servlet接口实现类开发步骤
1.网站中所有的Servlet接口实现类的实例对象,只能由Http服务器负责创建
<serlvet> <servlet-name> .... </servlet-name> <servlet-class> .... </servlet-class> <load-on-startup> 1 <load-on-startup> </serlvet>
// <load-on-startup>默认为0,手动配置的话,随便输入一个大于0的数即可
手动配置
1)load-on-startup元素标记容器是否在启动的时候就加载这个servlet(实例化并调用其init()方法)。2)它的值必须是一个整数,表示servlet应该被载入的顺序3)当值为0或者大于0时,表示容器在应用启动时就加载并初始化这个servlet;4)当值小于0或者没有指定时,则表示容器在该servlet被选择时才会去加载。5)正数的值越小,该servlet的优先级越高,应用启动时就越先加载。6)当值相同时,容器就会自己选择顺序来加载。
关于load-on-startup
2.在默认情况下,Http服务器接收到对于当前Servlet接口实现类的第一次请求时,自动创建这个Servlet接口实现类的实例对象, 在手动配置情况下,要求Http服务器在启动时自动创建某个Servlet接口实现类的实例对象
3.在HTTP服务器运行期间,一个Servlet接口实现类只能被创建出一个实例对象
4.在HTTP服务器关闭时,自动将网站中所有的Servlet对象进行销毁
Servlet对象声明周期
HttpServletResponse接口来自于Servlet规范中,在Tomcat中存在于servlet-api.jar中
HttpServletResponse接口实现类由Http服务器负责提供
HttpServletResponse接口负责将doGet()/doPost()方法执行结果写入到响应体中,交给浏览器
开发人员习惯将HttpServletResponse接口修饰的对象称为响应对象
介绍
将执行结果以二进制形式写入到响应体
设置响应头中content-type属性值,从而控制浏览器使用对应编译器将响应体二进制数据编译为文字、图片
设置响应头中location属性,将一个请求地址赋值给location,从而控制浏览器向指定服务器发送请求
通过响应对象,向Tomcat索要输出流PrintWriter out = response.getWriter();
通过输出流,将执行结果以二进制形式写入到响应体out.write(result);
注:如果result为97,则浏览器显示a 如果result为50,则浏览器显示2因为:ASCII码: a ...... 97 2 ...... 50
在实际开发中都是通过out.print()将真实数据写入到响应体
将响应对象的结果写入到响应体中步骤
如果result为\"java<b r>MySQL</br>HTML\" ,浏览器的结果就是这,因为br被当成字符串了,没有被当成命令
浏览器在接收到响应包之后,根据响应头中content-type属性的值,来采用对应编译器对响应体中二进制内容进行编译
在默认情况下,content-type的属性的值为\"text\",即:content-tyoe=\"text\"
此时浏览器将会采用文本编译器对响应体二进制数据进行解析文本编辑器:解析为英文、中文、字母
被当成字符串的原因
*****在得到输出流之前*****,通过响应对象对响应头中content-type属性进行重新赋值,用于指定浏览器采用正确编译器
response.setContentType(\"text/html\") // 设置响应头text/html:使用文本编译器和HTML命令编译器
设置响应头的content-type
如果result为中文:\"红烧肉\"
在网页中显示为?????
设置字符集
response.setContentType(\"text/html;charset=utf-8\");
重定向
String result = \"http://www.baidu.com\";
response.sendRedirect(result)
此时响应头中:location=\"http://www.baidu.com\"
浏览器在接收到响应包之后,如果发现响应头中存在location属性,自动通过地址栏向location指定网站发送请求
sendRedirect方法远程控制浏览器请求行为(请求地址,请求方式,请求参数)
通过响应对象,将地址赋值为响应头中location属性
具体操作实现
主要功能
HttpServletResponse接口
HttpServletRequest接口来自于Servlet规范中,在Tomcat中存在于servlet-api.jar中
HttpServletRequest接口实现类由Http服务器负责提供
HttpServletRequest接口负责在doGet()/doPost()方阿飞运行时读取Http请求协议包中信息
开发人员习惯于将HttpServletRequest接口修饰的对象称为请求对象
可以读取Http请求协议包中请求行信息
可以读取保存在Http请求协议包中请求头或请求体中请求参数信息
可以代替浏览器向Http服务器申请资源文件调用
String url = request.getRequestURL().toString();
通过请求对象,读取请求行中url信息
String method = request.getMethod();
通过请求对象,读取请求行中method信息
URI:资源文件精准地位地址,在请求行中并没有URI这个属性。实际上URL中截取一个字符串,这个字符串的格式为\"/网站吗/资源文件名\"URI用于让Http服务器对被访问的资源文件进行定位
String uri = request.getRequestURL(); // 相当于subString
通过请求对象,读取请求行中uri信息
Enumeration paramNames = request.getParameterNames()
通过请求对象,获得请求头中所有请求参数名
String value = request.getParameter(paramName); // 利用迭代器获得
通过请求对象,获得指定的请求参数的值
String value = request.getPparameter(\"username\"); // 和头一样
以GET方式放松中文参数内容时,得到正常结果
以POST方式发送中文参数内容时,得到乱码的结果
浏览器以为GET方式发送请求,请求参数保存在请求头,在HTTP请求协议包到达HTTP服务器后,先解码,请求头二进制内容由Tomcat负责解码,Tomcat9.0默认使用utf-8
浏览器以POST方式发送请求,请求参数保存在请求体,在HTTP请求协议包到达Http服务器后,先解码,请求体二进制内容由当前请求对象(request)解码,request默认使用[IOS-8859-1]字符集,此时如果请求体内容是中文,将无法解码
原因
在POST请求下,在读取请求体内容之前,应该通知请求对象使用utf-8字符集对请求体内容进行一次重新解码
request.setCharacterEncoding(\"utf-8\")
通过请求对象,获得请求体中的具体参数信息
HttpServletRequest接口
在Http服务器接收到浏览器发送的Http请求协议包之后,自动为当前的Http请求协议包生成一个请求对象和一个响应对象
在Http服务器调用doGet()/doPost()方法时,负责将请求对象和响应对象作为参数传递给方法,确保doGet()和doPost()正确执行
在Http服务器准备推送Http响应协议包之前,负责将本次请求关联的请求对象和响应对象销毁
请求对象和响应对象生命周期贯穿一次请求的整个处理过程,它们相当于用户在服务端的代言人
请求对象和响应对象生命周期
注册
1.调用请求对象读取请求体参数信息
2.调用Dao将查询验证命名发送到数据库服务器
3.根据验证结果,将对应[资源文件地址]发送给浏览器
资源文件地址就是,localtion:请求地址
登录
查询
1.调用请求对象:读取请求头参数(用户编号)
2.调用Dao:将用户编号填充到delete命令中,并发送到数据库服务器
3.调用响应对象:将处理结果以二进制写入到响应体,交给浏览器
删除
demo(注册、登录、查询、删除)
前提:用户可以记住网站名,但是不会记住网站资源文件名
用户发送了一个针对某个网站的默认请求时,此时由http服务器自动从当前网站返回的资源文件
正常请求:http://localhost:8080/myWeb/index.html
默认请求:http://localhost:8080/myWeb/
默认欢迎资源文件
规则位置:Tomcat安装位置/conf/web.xml
<welcome-file-list> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> <welcome-file>index.jsp</welcome-file></welcome-file-list>
上述代码的意思是,先找index.html,如果没找到,再找index.htm,如果没找到,再找index.jsp,如果全部没找到,报404
规定命令
Tomcat对默认欢迎资源文件定位规则
规则位置:网站/web/WEB-INF/web.xml
<welcome-file-list> <welcome-file>login.html</welcome-file></welcome-file-list>
也可以写成动态的,例如:<welcome-file>user/find</welcome-file>
规则命令
网站设置自定义默认文件定位规则,此时Tomcat自带定位规则将失效
设置当前网站的默认欢迎资源文件规则
欢迎资源文件
由三位数字组成的一个符号
Http服务器再推送响应包之前,根据本次请求处理情况将Http状态码写入到响应包中的状态行上
如果HTTP服务器针对本次请求,返回了对应的资源文件。通过http状态码通知浏览器应该如何处理这个结果
如果Http服务器针对本次请求,无法返回对应的资源文件,通过HTTP状态码向浏览器解释不能提供服务的原因
组成:100~599;分为5个大类
最有特征的是100,通知浏览器本次返回的资源文件并不是一个独立的资源文件,需要浏览器再接收响应包之后,继续向http服务器索要依赖的其他资源文件
1XX
最有特征的是200,通知浏览器本次返回的资源文件是一个完整独立资源文件,浏览器再接收到之后不需要索要其他相关联文件
2XX
最有特征的是302,通知浏览器本次返回的不是一个资源文件内容而是一个资源文件地址,需要浏览器根据这个地址自动发起请求来索要这个资源文件例:response.sendRedirect(\"资源文件地址\"),将地址写入响应头中的location中,而这个行为导致Tomcat将302状态码写入到状态行中
3XX
最有特征的是404、405
通知浏览器,由于再服务器端没有定位到被访问的资源文件,因此无法提供帮助
404
通知浏览器,在服务端已经定位到被访问的资源文件(必须是servlet),但是这个servlet对于浏览器采用的请求方式不能处理例:servlet中去掉doGet(),网页中通过地址栏(相当于get请求)找这个Servlet(),这时会出现405
405
4XX
最有特征的是500,通知浏览器在服务端已经定位到被访问的资源文件(servlet),这个servlet可以接收浏览器采用的请求方式,但是servlet在处理请求中,java代码出异常了
5XX
分类
总结:4XX和5XX都是非正常的
Http状态码
某些来自浏览器发送的请求,往往需要服务端中多个Servlet协同处理,但是浏览器一次只能访问一个Servlet,导致用户需要手动通过浏览器发起多次请求才能得到服务
这样增加了用户获取服务的难度,导致用户放弃访问当前网站
前提条件
无论本次请求设计到多少个Servlet,用户只需要手动发起一次请求
提高用户使用感受规则
请求转发
多个Servlet之间调用规则
多个Servlet之间的调用规则
用户第一次通过手动方式通知浏览器访问OneServlet,OneServlet工作完毕后,将TwoServlet地址写入到响应头的location属性中,导致Tomcat将302状态码写入到状态行。在浏览器接收到响应包之后,会读取到302状态。此时浏览器自动根据响应头中location属性地址发起第二次请求,访问TwoServlet去完成请求中剩余任务
工作原理
response.sendRedirect(\"请求地址\")
将地址写入到响应包中的响应头中的location属性
实现命令
既可以把当前网站内部的资源文件地址发送给浏览器(/网站名/资源文件名),也可以把其他网站资源文件地址发送给浏览器(http://ip地址:端口号/网站名/资源文件名)
请求地址
浏览器至少发送两次请求,但是只有第一次请求是用户手动发送,后续请求都是浏览器自动发送的
请求次数
重定向是通过地址栏通知浏览器发起下一次请求,因此通过重定向解决方案调用的资源文件接收的请求方式一定是GET
请求方式
需要浏览器与服务器之间进行多次往返
大量时间消耗在往返次数上,增加用户等待服务时间
特征
用户第一次通过手动方式要求浏览器访问OneServlet,OneServlet工作完毕后,通过当前的请求对象代替浏览器,向Tomcat发送请求,申请调用TwoServlet,Tomcat在接收到这个请求后,自动调用TwoServlet来完成剩余任务
请求对象代替浏览器向Tomcat发送请求
RequestDispatcher report = request.getRequestDispatcher(\"/资源文件名\")
1.通过当前请求对象生成资源文件申请报告对象
report.forward(当前请求对象,当前响应对象)
2.将报告对象发送给Tomcat
request.getRequestDispatcher(\"/资源文件名\").forward(当前请求对象,当前响应对象)
二合一
无论涉及到多少个Servlet,用户只需发一次请求
Servlet之间调用发生在服务端计算机上,节省了服务端与浏览器之间往返次数,增加处理服务速度
浏览器只发了一次请求
只能向Tomcat服务器申请调用当前网站下资源文件地址(/资源文件名),注:不要写网站名
在请求转发过程中,浏览器只发送了一个Http请求协议包,参与本次请求的所有Servlet共享同一个请求协议包,因此这些Servlet接收的请求方式与浏览器发送的请求方式保持一致
OneServlet工作完毕后,将产生数据交给TwoServlet来使用
数据共享
ServletContext接口
Cookie类
HttpSession类
Servlet规范中提供了四种数据共享方案
来自于Servlet规范中一个接口,在Tomcat中存在servlet-api.jar中,在Tomcat中负责提供这个接口实现类
如果两个Servlet来自于同一个网站,彼此之间通过网站的ServletContext实例对象实现数据共享
开发人员习惯于将ServletContext对象称为全局作用域对象
每个网站都有一个全局作用域对象,这个全局作用域对象相当于一个Map,在这个网站中OneServlet可以将一个数据存入到全局作用域对象,其它Servlet可以从全局作用域中得到这个数据
1.在Http服务器启动时,自动为当前网站在内存中创建全局作用域对象
2.在Http服务器运行时,一个网站只有一个全局作用域对象
3.在Http服务器运行时,全局作用域对象一直处于存活状态
4.在Http服务器准备关闭时,负责将当前网站中全局作用域对象销毁
总结:生命周期贯穿网站整个运行期间
全局作用域对象生命周期
例:同一个网站中,OneServlet将数据共享给TwoServlet
1.在OneServlet中:通过请求对象向Tomcat索要当前网站中全局作用域对象 ServletContext application = request.getServletContext();
2.在OneServlet中:将数据添加到全局作用域对象作为共享数据application.setAttribute(\"key1\
3.在TwoServlet中:索要全局作用域对象ServletContext application = request.getServletContext();
4.从全局作用域对象得到指定关键字对应数据Object 数据 = application.getAttribute(\"key1\");
命令实现
存在于请求头/响应头中
1.Cookie来自于Servlet规范中一个工具类,存在于Tomcat提供的servlet-api.jar中
2.如果两个Servlet来自于同一个网站,并且为同一个浏览器/用户提供服务,此时借助Cookie对象进行数据共享
3.Cookie存放当前用户的私人数据,在共享数据过程中提高服务质量
4.在现实生活场景中,Cookie相当于用户在服务端得到的会员卡
用户通过浏览器第一次向myWeb网站发送请求申请OneServlet,OneServlet在运行期间创建一个Cookie存储与当前用户相关数据,OneServlet工作完毕后,将Cookie写入到响应头,交给当前浏览器。浏览器收到相应包之后,将Cookie存储在浏览器的缓存中,一段时间之后,用户通过同一个浏览器再次向myWeb网站发送请求申请TwoServlet时。浏览器需要无条件的将myWeb网站之前推送过来的Cookie写入到请求头中发送出去。
此时TwoServlet在运行时,就可以通过读取请求头中Cookie中信息,得到OneServlet提供的共享数据
同一个网站中OneServlet与TwoServlet借助于Cookie实现数据共享
1.创建一个Cookie对象,保存共享数据(当前用户数据)Cookie card = new Cookie(\"key1\
2.[发卡],将Cookie写入到响应头中,交给浏览器resp.addCookue(care); // 有几个Cookie,写几行
响应包------------------------->浏览器/用户[200][cookie:key1=abc][空包行][处理结果(TwoServlet)]
浏览器向myWeb网站发送请求访问TwoServlet请求包-------------------------->后台[url:/myWeb/tw method:get][请求参数:xxxxx Cookie:key1=abc][空包行][请求体]
在OneServlet中
1.调用请求对象从请求头得到浏览器返回的CookieCookie[] cookieArray = request.getCookies();
2.循环遍历数据得到每一个cookie的key和valuefor(Cookie card:cookieArray){ String key = card.getName(); // 读key String value = card.getValue(); // 读value // 提供服务....................}
在TwoServlet中
1.在默认情况下,cookie对象存放在浏览器的缓存中,因此只要网页关闭,cookie对象就被销毁掉
2.在手动情况下,可以要求浏览器将接收的Cookie存放在客户端计算机硬盘上,同时需要指定Cookie在硬盘上的存活时间,在存活时间范围内,关闭浏览器、关闭客户端、关闭服务器,都不会导致Cookie被销毁,在存活时间到达时,Cookie自动从硬盘上被删除
设置Cookie在硬盘上的存活时间cookie.setMaxAge(60); //cookie在硬盘上存活1分钟
Cookie销毁时机
Cookie
解决:Cookie中存储数据单一,存储数据太少
1.HttpSession接口来资源Servlet规范下一个接口,存在于Tomcat中servlet-api.jar中,它的实现类由Http服务器提供,Tomcat提供实现类存在于servlet-api.jar中
2.如果两个Servlet来自于同一个网站,并且为同一个浏览器/用户提供服务,此时借助于HttpSession对象进行数据共享
3.开发人员习惯于将HttpSession接口修饰的对象称为会话作用域对象
一个天上一个地下
Cookie:存放在客户端的计算机中(浏览器/硬盘)
HttpSession:存放在服务端计算机内存
存储位置
Cookie对象存储共享数据类型只能是String
HttpSession对象可以存储任意类型的共享数据Object
数据类型
一个Cookie对象只能存储一个共享数据
HttpSession使用map集合存储共享数据,所以可以存储任意数量共享数据
数据数量
Cookie相当于客户在服务端的会员卡
HttpSession相当于客户在服务端的私人保险柜
参照物
HttpSession和Cookie的区别
同一个网站下OneServlet将数据传递给TwoServlet
调用请求对象向Tomcat索要当前用户在服务端的私人储物柜HttpSession session = request.getSession();
将数据添加到用户私人储物柜session.setAttribute(\"key1\
浏览访问/myWeb中TwoServlet
从会话作用域对象得到OneServlet提供的共享数据Object 共享数据 = session.getAttribute(\"key1\");
通过cookie
新建session时会生成一个编号,这个编号存放在cookie中例:cookie:JSessionID = 110620 // 编号
Http服务器如何将用户与HttpSession关联起来
如果当前用户有session则取出,如果没有的话,Tomcat会为其创建一个
getSession()
如果有session则取出,如果没有的话,Tomcat返回null
getSession(false)
用户与HttpSession关联时使用Cookie只能存在于浏览器的缓存中
在浏览器关闭时,意味着用户与它的HttpSession关系被切断
由于Tomcat无法检测浏览器何时关闭,因此在浏览器关闭时不会导致Tomcat将浏览器关联的HttpSession进行销毁
为了解决这个问题,Tomcat为每一个HttpSession对象设置了空闲时间,默认为30min,如果30min不用,则Tomcat自动销毁HttpSession
HttpSession销毁时机
在当前网站下,web.xml中:<session-config> <session-timeout>5 </session-timeout></session-config>这里的5就是5分钟
HtpSession空闲时间手动设置
HttpSession接口
1.在同一个网站中,如果两个Servlet之间通过请求转发方式进行调用,彼此之间共享同一个请求协议包。而一个请求协议包只对应一个请求对象,因此servlet之间共享同一个请求对象,此时可以利用这个请求对象在两个Servlet之间实现数据共享
2.在请求对象实现Servlet之间数据共享功能时,开发人员将请求对象称为请求作用域对象
OneServlet通过请求转发申请调用TwoServlet时,需要给TwoServlet提供共享数据
将数据添加到(请求作用域对象)中attribute属性request.setAttribute(\"key1\
向Tomcat申请调用TwoServletrequest.getRequestDispatcher(\"/two\
从当前请求对象中得到OneServlet写入的共享数据Object 数据 = request.getAttribute(\"key1\");
注:在重定向和请求转发中,一般都是由最后一个Servlet来将最终数据写入到响应体中
HttpServletRequest接口实现数据共享
多个Servlet之间数据共享实现方案
Servlet
URI 表示资源,资源一般对应服务器端领域模型中的实体类
URI 是地址也是资源
URI里边带上版本号、后缀来区分表述格式
用名词、不用动词
层级结构明确、用/来表示
用?用来过滤资源
必备约束
资源与URI
标准HTTP方法包含:GET、POST、PUT、DELETE、Patch,他们的使用功能如下列表所示
put是完整的对象
patch是部分对象
统一资源接口
Rest
JavaWeb
Java
0 条评论
回复 删除
下一页