JavaSE知识点
第1章 初识java
java文件 程序员认识 计算机不认识 只认识0和1
javac命令 编译 将java文件编译成.class文件
java命令 运行 输出结果
第2章 数据类型
数据类型: 1、基本数据类型 byte 1字节 short 2字节 int 4字节 long 8字节
float 4字节 double 8字节
boolean 1字节 char 2字节
2、引用数据类型 除了基本的数据类型 剩下的数据类型都是引用数据
定义变量: 数据类型 变量名 = 值
数据类型之间的转换: 他们之间能进行转换
改写说明
硬性类型的转换机制:当一个数据类型的内存需求较高时(如大整数),系统会将其自动转换为内存需求较低的数据类型(如小整数)。
目标类型 变量名 = (目标类型)变量值
第3章 运算符
算数运算符: + 和 /
当加号左右两边都是数字类型时,加号才是数学意义中的加号
如果在运算符两边其中之一是字符串的情况下,在这种情况下,加号执行连接操作,并且其结果仍是一个字符串。
/:当除数和被除数都是整数时,结果只取到小数部分中的整数部分
当除数和被除数有一个为浮点类型数据时,结果是整数部分+小数部分
比较运算符: 比较后的结果为boolean类型
java 中比较两个数是否相等: == = 赋值运算符
javaScript 脚本语言 === ==
赋值运算符: += -= /= *= 默认包含了强制类型转换 byte a = 1; a+=1;// a =(byte)(a+1);
自增运算符: ++ --
当变量单独进行自增或自减操作时,在前后位置的效果相同,并使变量值增加1或减少1
当变量单独进行自增或自减操作时,在前后位置的效果相同,并使变量值增加1或减少1
2、当变量参与其他操作时 ++或者--在前或者在后效果是不一样的
++或者--在前边时 先执行++或者--的操作 然后再执行其他的操作
++或者--在后边时 先执行其他的操作 然后再执行++或者--的操作
对于逻辑运算符而言:其前后操作数必须为布尔类型,并且计算结果也必须是布尔类型。
当逻辑运算符前后两边的运算结果均为true时,整个运算结果才是true;如果任一边为false,则整个运算结果为false
只有在运算符前后两侧的结果均为false时整个运算才会返回false否则若任一侧运算结果为true则整体运算结果即为true
! 取反 将表达式的结果进行取反 !true false !false true
^ 异或 表达式两边的值相同时 结果为false 不同时为true
该短路逻辑运算符在两侧操作数均为真时才会导致最终结果为真;若任一操作数为假,则最终整体结果必然是假。
在短路逻辑中使用||(OR)操作符时,在运算符两侧的操作都返回假(false)时整个表达式的最终结果才是假(false);只要任一侧的操作返回真(true),则整个表达式的最终结果即为真(true)。
不同点:
&和&& & 当前面的结果为false时,后面表达式依然执行
&& 当前面的结果为false时,后面表达式不执行
|和|| | 当前面的结果为true时,后面表达式依然执行
|| 当前面的结果为true时,后面表达式不执行
三元运算操作符:expression?expression1:expression2。若该表达式的计算结果为真,则执行expression1;否则,则执行$expression2。
第4章 分支机构
if(condition){ //其中condition为布尔类型变量或表达式 可执行逻辑判断操作 也可进行关系比较运算操作
代码块 若计算结果为逻辑值true,则执行该if语句中的操作;否则不会执行该if语句中的操作
}
2、if(条件表达式){ 当表达式的结果为true时,执行if中的代码块,
当表达式的结果为false时,执行else中的代码块
代码块1
}else{
代码块2
}
首先进行条件表达式1的判断;如果结果为true,则进入对应的代码块;后续的所有判断都不会执行。
代码块1 如果结果为false 继续后面的判断 判断流程和上述一致
}elseif(条件表达式2){ 如果所有条件均不满足,则进入else块的处理过程.
代码块2
}elseif(条件表达式3){
代码块3
}...else{ else 可以有 也可以没有
代码块n
}
4、switch
使用表达式来控制switch语句的分支。
需要注意的是,在每个case后的值必须唯一。
默认情况可放置于中间位置,并且仅允许出现一次。
case 值1:
代码块;
[break];
case 值2:
代码块;
[break];
case 值3:
代码块;
[break];
....
default: 可以有 也可以没有
代码块;
[break];
}
执行过程: 1、先计算switch后面的表达式的值
2、逐个与case后面的值进行比较;如果比较结果为真,则执行相应的处理流程;当遇到break时会终止循环;流程何时结束?
如果没有遇到,执行下一个case中的代码,再次判断是否遇到break。
如果计算式的值与后续case的值均不相等,则会跳转至默认处理模块内的代码段。
第5章 循环结构
1、for循环
基本: for(循环变量初始化;循环条件判断;改变循环变量){
循环体;
}
1、循环变量的初始化
2、循环条件的判断,当判断的结果为true,执行循环体,执行第三步
当判断结果为false时,结束循环
3、改变循环变量 再次执行第二步
2、while
初始化循环变量;
while(循环条件判断){
循环体;
改变循环变量;
}
1、初始化循环变量
2、循环条件的判断, 如果判断为true,执行第三步
如果判断为false,结束循环
3、执行{}中的代码,在执行第二步
3、do...while
初始化循环变量;
do{
循环体;
改变循环变量;
}while(循环条件判断);
1、初始化循环变量
2、执行do中的代码
判定循环条件的结果是否为true,如果是,则进行第二步操作;否则,退出当前循环.
4、循环辅助语句
break: 结束当前整个循环,后面的循环也都不执行了
continue: 结束当前某一次循环,后面的循环依然执行
return: 结束方法;返回结果
第6章 数组
引用数据类型
一块连续的内存空间,并且可以存储多个元素
获取元素根据数组的下标获取:
数组的下标从0开始 数组下标的最大值=数组的长度-1
数组的长度:数组名.length
如何定义数组:
1、数据类型[] 数组名 = new 数据类型[数组的长度]
2、数据类型[] 数组名 = {元素1,元素2..}
3、数据类型[] 数组名 = new 数据类型[]{元素1,元素2..}
问题: 一旦数组的长度被固定后,若超出容量范围,会导致索引越界错误
ArrayIndexOutOfBoundsException
某个位置添加元素: 数组名[索引] = 值
获取元素: 数组名[索引]
第7章 方法
1、如何定义方法
public static 返回值类型 方法名(参数列表){
如果有返回值 return 返回值
没有返回值 不用写return
}
1、有参无返回值
public static void 方法名(参数列表){ 参数: 数据类型 变量名 形参
方法体;
[return;]
}
2、有参有返回值 返回的结果的类型必须是返回值的类型
public static 返回值类型 方法名(参数列表){ 参数: 数据类型 变量名 形参
方法体;
return 返回的结果;
}
3、无参无返回值
public static void 方法名(){ 参数: 数据类型 变量名 形参
方法体;
[return;]
}
4、无参有返回值 返回的结果的类型必须是返回值类型规定的类型
public static 返回值类型 方法名(){ 参数: 数据类型 变量名 形参
方法体;
return 返回的结果;
}
2、方法的调用:
返回值类型 变量名 = 方法名(实参列表)
3、实参和形参的区别:
形参:在方法定义的时候创建的参数
实参:在方法调用的时候传入的参数
1、实参的个数和形参的个数一致
位于相同位置处的实际参数及其数据类型与形式参数及其数据类型的定义应当保持一致。
4、参数的传递
基本数据类型的参数: 对形参的改变,不影响实际的值
引用数据类型的参数: 对形参的改变,会影响实际的值
5、方法的重载
名称相同的方法,在其输入参数的数量或数据类型发生变化时,则会形成重载现象;这一过程与该方法是否返回值以及参数的具体名称无关。
6、递归调用
方法自己调用自己
1、递归一定要有出口 在合适的时机结束方法
2、即使有出口,调用的次数也不能过多
错误: StackOverflowError
第8章 面向对象
1、面向对象的三大特征: 封装 继承 多态
2、1、类 模板 属性 : 类有什么特征 数据类型 变量 = 【值】
行为 : 类能干什么 方法 将方法中的static关键字去掉
2、对象 类名 对象名 = new 类名(参数) new的后面的跟的是构造方法
对象的创建是需要构造方法执行完毕后,才有对象吗?不是
当new关键字执行时就会生成一个对象,在创建完成后,对象中的属性均为默认值
使用构造方法对默认值进行初始化操作
3、构造方法 也可以重载
如果没有显式地为类定义一个构造方法,默认情况下jvm将为该类生成一个带有零个参数的构造方法。
语法: public 类名(形参列表){
}
如果在一个类中显式地定义了一个构造方法(即public static constructor),那么JVM就不会为我们自动创建默认构造方法(default constructor)。
的时候,只能使用我们自己创建好的构造方法了
4、this关键字
变量的分类:
局部变量: 在方法中定义的变量 称之为局部变量
栈帧中的局部变量表位置,栈帧没有了,局部变量也就消失了
全局变量: 在类中定义的变量 称之为全局变量 属性/全局变量/成员变量
跟对象保存在一起,放在堆中 对象被回收了 全局变量也就没有了
局部变量和全局变量重名:
调用方法时,优先访问的是局部变量.
想访问全局变量: 【将其中一个变量改名】
this关键字指带的是当前的对象
【this指代是new关键字执行的时候所开辟的那块堆内存】
this除了上述的功能,还可以显示的调用某个构造方法
this(实参) 根据参数的个数和类型调用对应的构造方法
5、封装
使其成为私有属性,在其前面添加一个访问权限修饰符private的原因是因为只有被标记为private的成员方法/变量才能仅限于本类内部使用
2、提供set/get方法 set方法给属性赋值 get获取属性值
提高了代码的安全性
6、访问/权限修饰符 private 默认的 protected public
第9章 继承
在Java语言中, 仅支持单一继承模式, 每个类型最多只能有一个直接上层类型; 若未显式声明上层类型, 则该类型的上层类型将被默认为Object类型。
但是java支持多层继承,
继承: class A extend B 子类就拥有了父类中非私有的属性和方法
当子类对继承的属性或方法感到不满时,可以选择自行进行修改,在这一过程中需确保与父类的方法保持一致.
当再次调用时,调用的是重写后的方法。
super关键字:
可以通过调用super超关键字访问父类中的属性和方法。
在Java编程中,在子类invoke父类构造方法之前,请确保先执行父类中的构造方法;若希望在子类环境中明确显示使用某个特定的构造方法,则应按照规定的顺序进行操作。
super(实参列表)
final: 最终的
属性: 代表这个属性是一个常量
基本数据类型: 变量中的值不能改了
引用数据类型:标识符所对应的存储位置不可更改,但其存储位置所对应的值是可以修改的
方法: 最终的方法 这个方法就不能被重写了 但是可以重载
类: 最终的类 这个类就不能被继承了
static属性表示,在类加载过程中立即执行以确保仅执行一次,并且与类保持在同一声明阶段。
属性: static property 被当前类创建的对象 共享该属性 非nonstatic修饰的 是每个对象私有的
方法: 静态方法 1、对象名.方法名 2、类型.方法名
静态方法: 只能访问静态的属性或者方法
非静态方法: 无论是静态的还是非静态的都可以访问
如果一个对象变成垃圾对象而被垃圾回收器回收之后,那么它的static type引用的对象会不会也被回收呢?原因是什么呢?
对象不会被回收的原因是因为它与类绑定在一起;当一个类不再存在时,其对应的静态属性就会随之消失。
第10章 抽象类/接口/多态
1、如何定义抽象类:
在类上加上abstract关键字 public abstract Person{}
如何定义抽象方法:
通常情况下,如果一个程序里的某个类没有其对应的方法体,那么这个程序肯定是无法运行的.我们将其定义为抽象类,从而使得该类不需要编写具体的方法体.
为了使该方法成为抽象的方法,我们只需在方法中添加abstract关键字即可。如果没有后跟的大括号,则表示该方法没有体。
public abstract void get();抽象方法只能位于抽象类中.
在抽象类中不需要存在任何抽象方法,在这些抽象方法必须存在于其所属的 abstract 类之中
当一个具体类继承了抽象基团时,该具体类必须实现其定义在该抽象基团中的虚函数.
如果某个子类继承自一个抽象类,那么这个子类就可以选择性地实现或省略其所属的抽象方法
抽象类可以有构造方法
2、当抽象类中的所有的方法都是抽象的,那么就可以定义成接口了
当我们使用接口时, 必须按照实现的方式, implements 接口名, 支持实现多个接口。
当一个正常的类实现了接口,必须实现接口中所有的抽象方法
当子类是一个抽象类继承了接口类型时,则必须实现其中的所有公共 abstract methods;然而,在某些情况下可以选择不实现。
3、父类的引用可以指向子类的对象 多态
接口及其父类之间可以通过对象名称(用于引用其所属父类)的方式进行关联,并通过new关键字创建其所属实现接口(即子类)实例或直接创建其所属子类实例
父类的引用禁止使用子类独有的属性或功能,若要获取子类独有的属性或功能,则应采取措施将
父类的引用强制转换为子类的类型
第11章 内部类
匿名内部类: 多线程相关
有了匿名内部类, 我们可以通过创建新的匿名内部类来实现对抽象类和接口的实例化。
同时, 我们也可以使用new关键字来直接创建这些抽象类型或接口类型的实例。
只需要实现其中的抽象方法就可以。
============================================
需要将UserDaoService 加载到内存中
UserDaoService userDaoService = new UserDaoService();
有了内部类后,我们可以直接new 接口
UserDao userDao = new UserDao(){
@Override
public void add() {
}
}
第12章 常用API
String 类
1、位于java.lang包下 用的时候我们不需要导包
2、String类代表的是字符串 ""包裹的内容属于字符串
3、Immutable string remains unalterable once created. Any operation performed on it won't affect the original object's value and will result in a new object.
4、是否允许String类进行继承? 由于其字段被final修饰, 因此无法进行继承.
String类的构造方法形成了重载
public String() 无参数构造方法 所生成的对象没有任何内容 其默认值为: ""
该方法将接受一个字符类型的数组,并将其中各元素连接成一个完整的字符串。
public String(String original) 要求接收一个字符串类型的参数 创建的新字符串内容与传入的参数一致 是一种常见的实现方式
String 变量名 = 值 使用直接赋值的形式创建字符串 最常见
字符串的比较:
两种情况的比较:
== 比较
双等号两边为基本数据类型,那么比较的是数据的值是否相等
双等号两边为引用数据类型,那么比较的是对象的内存地址是否相同
内容: equals 比较两个字符串的内容 区分大小写
1、问==和equasl的区别?
字符串
== 比较的是两个字符串的内存地址 equals比较的是字符串的内容
自定的类 不重写equals的前提下:
==和equals比较都是内存地址
String采用的是一个char array来保存字符串信息 private final char value[];该值用于存储字符的信息
StringBuilder类概述 线程不安全的 效率高
这是一个代表一个可变字符串的对象类,在对StringBuilder对象执行任何操作时, 该对象的值会受到影响. 此外, 该对象所存储的内容是changeable的.
String类 它的内容是不可变的
StringBuilder类 它的内容是可变的
public StringBuilder():StringBuilder对象的默认值为""
public StringBuilder(String str): 表示传入字符串为参数
StringBuffer类概述 线程安全的 效率低
该字符串类型的实例是可变的;任何操作都可能影响到该StringBuilder对象自身的内容;该对象的内容是可以被修改的。
所有的操作和StringBuilder是一模一样的
面试题:
1、直接赋值方式创建
使用指定的方式给出的字符串,在JVM中如果字符序列完全相同(不区分大小写),则不管在程序代码中出现多少次都会只创建一个String对象,并进行维护。
1、String str = new String("abc") 创建几个对象?
2对象 一个在堆中创建 一个常量池中
2、String str = new String("abc"),String str1 = new String("abc")创建几个对象?
3个对象
3、String 是否可以被继承?原因?
4、String 存储的值是否可变?原因?
5、String str1 = "abc"; String str2 = "ab" + "c"; str1==str2是true吗?原因?
答案确定是。
因为当创建 String str2 为 "ab" + "c" 时,
它会在常量池中查找是否存在内容为 "abc" 的字符串对象。
如果存在,则直接引用该对象;
否则,
默认情况下会创建新的字符串实例,
即当创建 String str1 为 'abc' 时,
它会生成一个新的字符串实例,
而不是引用已有的对象
提到了,在常量池中将创建一个名为"abc"的对象。因此,在此之后引用的变量str1都会指向这个对象;同样地,在后续的操作中也会不断引用同一个对象。这意味着在比较这两个变量时(即比较str1和str2),它们的引用结果将完全相同。
String str1 = "abc"; String str2 = "ab"; String str3 = str2 + "c"; str1==str3是false吗?原因?
回答:是。由于String str3 = str2 + "c"涉及变量(并非全部为常量)的相加操作, 因此会产生新的对象
其内部实现包括先创建一个StringBuilder对象;接着依次拼接str2字符串和字符'c';最后将str3赋值为此处操作的结果。
第13章 异常
Throwable
Error: 错误 人为解决不了的 jvm层面的错误 StackOverflowError
Exception: 异常 程序在运行过程中出现的问题 称之为异常
.java文件
编译 编译时异常
.class文件
运行 运行时异常
结果
自定义异常:
编译时异常 自定义的类继承Exception 就是编译时异常/受检异常
运行时异常 自定义的类继承RuntimeException 就是运行时异常
如何解决异常:
1、try{
可能发生异常的代码
}catch(异常类型1 变量名){
//处理异常的过程
}catch(异常类型2 变量名){
//处理异常的过程
}...finally{ 可以有 可以没有
无论是否发生异常,finally中的代码都会执行
}
运行流程: 第一步,在try块内部的代码未触发异常的情况下,catch块不会捕获异常;最后,在finally块中的代码会继续运行。
如果try块中的代码出现错误,并被catch捕获了一个异常事件,则依次比较后续可能出现的异常类型。
当成功发生时, 从而导致相应的异常处理代码被调用, 最终的catch块将不会被触发
如果都不成功,交给jvm处理。
2、throws 放在方法的上边, 将异常抛出, 交给该方法的调用者处理。
调用该方法可能会有异常,提前先做好处理,能不能发生不一定
3、throw 满足条件是 不抛异常 不满足条件时 抛出异常
第14章 集合
14.1 单列集合Collection
14.1.1 List集合
List项: 集合元素按插入顺序排列(其元素按照插入时所确定的顺序排列),允许重复出现,并支持通过索指引针访问每个元素
常用的实现类:
- ArrayList 集合
原理:底层数组,查找和增加快,删除和插入慢
Object[] elementData属性数组存储集合中的元素
jdk7版本被用于在创建集合时初始化一个长度为10的数组,并将该数组赋值给elementData变量。当向集合中添加元素的数量超过现有数组容量时,则对该数组执行扩容操作以使其容量达到原有容量的1.5倍。
jdk8版本中,在创建集合时会初始化一个长度为0的动态增长数组,并将其赋值给elementData变量。当向集合中插入第一个元素时,默认会初始化一个大小固定的初始数组(如容量为10),并将该初始数组赋值给elementData变量。一旦向集合中插入的元素数量超过当前数组容量,则自动触发扩容操作,将现有数据复制到新申请的一个容量更大的新数组中(具体扩容倍数可设置为原来的1.5倍)。
集合进行缩容: 不能自动发生,只能手动解决trimToSize 可以进行缩容。
构造方法:
public ArrayList(int initialCapacity) allocates an array of size initial allocation size for storing elements.
public ArrayList()默认创建指定长度的数组
面试题:哈罗: ArrayList 这种扩容方式弊端? 堆内存溢出
当创建集合时,明确设置数组长度,以防止新数组在扩展过程中被生成,从而避免内存溢出
2、LinkedList集合
原理: 双向链表 插入和删除快 增加和查找慢
3、Vector集合
原理: 底层数组 查找和增加快 删除和插入慢
Object[] elementData 用于存储集合中的数据,在创建集合时初始化一个长度为10的数组,并将其赋值给elementData属性;当数组填满时会自动进行扩容,默认情况下扩容至当前容量的两倍。
面试题:
1、三者的区别:ArrayList、LinkedList、Vector
1.1、ArrayList和Vector
相同点: 底层都是数组 查找和增加数据快 删除和插入速度慢
不同点:
ArrayList: 当元素数量达到或超过现有容量时,通过扩展容量至原容量的1.5倍来实现增长。非线程安全。
Vector: 当数组填满时会自动扩展,并且默认扩展至原有容量的两倍。线程安全。
1.2、ArrayList和LinkedList
相同点: 单列集合
不同点:ArrayList:底层数组结构:支持快速的数据查询与插入操作;尽管删除与插入操作较慢,但该结构会自动扩容以适应负载需求。LinkedList:基于双向链表的设计;其删除与插入操作效率较高;然而数据查询与新增操作相对较低效,并且无需动态扩容以维持性能稳定性。
集合遍历:
1、普通的for循环要求对应的集合必须有索引
2、增强for循环 有无索引都可以 底层还用迭代器
语法: for(集合中的数据类型 变量:集合名){
变量代表集合中元素
}
3、迭代器 有无索引都可以
1、获取到迭代器 通过集合iterator方法获取到迭代器
2、判断当前位置是否有元素 hasNext()
3、如和将当前位置的元素取出 next()
14.1.3 Set集合
Set集合:无序(不代表乱序) 不可重复, 没有索引,不能用普通的for进行遍历
常用的实现类:
1、HashSet集合
特性: 不可重复 没有索引 无序
原理: jdk7 数组+链表
jdk8 数组+链表+红黑树
源码:
1、计算传入对象的hash值
key是我们传入的对象 value就是一个常量
public V put(K key, V value) {
hash(key) 生成输入对象的哈希值,并基于输入对象调用hashCode方法。
return putVal(hash(key), key, value, false, true);
}
2、创建长度为16的数组Node的数组并且赋值给table属性
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
// resize() 创建长度为16的数组 并且将数组赋值给table属性
// 局部变量tab和属性table指向同一块堆内存
n = (tab = resize()).length;
3、基于输入对象的哈希值及其数组长度进行相关运算以确定目标存储位置,并检查计算所得位置是否已存在数据?若无数据,则将对应元素及其哈希和自身信息存入该位置。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
4、如果对应的位置上有值,比较传入的对象的hash值是否相等:
当两个对象的哈希值一致时,在检查它们的equals方法返回的结果是否相同的情况下进行添加操作。
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
hash不相等 利用循环遍历当前位置上的链表
for (int binCount = 0; ; ++binCount) {
p.next 代表当前元素的下一个节点;如果p.next为null,则表示我们已经遍历到链表末尾;这表明在整个链表中并没有任何一个节点与新插入的元素相等
if ((e = p.next) == null) {
把新的元素添加到链表上
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
下一个节点对象 如果不为空时 继续比较下一个节点元素的hash值与equals方法的结果 如果发现两个对象相等 则退出循环 无法将新元素添加到集合中
if (e.hash == hash &&(k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 添加失败执行这里
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
扩容: 当数组元素数量超过指定阈值时进行扩容操作。该操作将使内存空间扩大至当前规模的两倍。其中加载因子设定期初值为0.75。具体而言,在初始状态下(数组长度为16),阈值计算结果即:threshold = 数组长度 \times 加载因子 = 16 \times 0.75 = 12。而当初始数组长度为16时,默认情况下该阈值也被设置为其两倍。
将链表转换为红黑树的条件是:当链表总长度超过8且数组长度达到或超过64时会直接导致将该链表转换为红黑树;如果链表总长度超过8但数组长度未达到64则会触发进行扩容操作。
当自定义类型对象的属性一致时,如果希望将该对象加入到集合中存储一份数据,则需要重新实现该对象的hashCode和equals方法。
面试题:
1、hashCode相等,equals一定相等吗?不一定
2、equals相等,hashCode一定相等吗?一定
3、基于hashset是基于hashmap实现这一前提,请解释一下在hashset实现过程中add方法中为何会在map.put操作时将val字段赋值为引入一个object类型的静态常量PRESENT?
set中默认调用map的put方法实现其add函数。若插入操作成功,则返回null;若插入操作失败,则会返回相应的PRESENT值;当PRESENT为null时(不论是插入成功的状态还是插入未成功的状态),都将被视为插入操作完成;当PRESENT不为空时,则可明确判断该插入操作是否已完成。
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
- LinkedHashSet 集合
LinkedHashSet是HashSet的子类
特点:
1、存储元素也是不重复的
2、存储过程和HashSet(Node)存储过程是一样的
相较于HashSet中的Node结点多出一个指针,这个指针用于指示添加的下一个元素的位置
3、TreeSet集合
特性:1、存储元素是不重复的
2、把添加的元素按照一定的规则进行排序(升序排列)
树结构:
红黑树: 1) 每个节点仅有两个子节点;2) 该节点左子树的所有键值均小于该键值;右子树的所有键值均大于该键值;3) 树上任一结点到叶子结点的最大深度差不超过1;4) 当新元素被插入导致某个路径的高度超过1时,则需执行相应的旋转操作以恢复平衡状态;5) 其他特性请参考教学笔记中的相关内容
自定义排序规则
1、自然排序
将自定义类放入TreeSet中进行排序操作,只需让我们的类实现Comparable接口,并编写public int compareTo(Object o)方法即可
/**
-
返回值为0 新的元素和旧的元素相同 新的元素不进行添加
-
返回值为>0 将新的元素方法旧的元素右边的子节点上
-
返回值为<0 将新的元素方法旧的元素左边的子节点上
*/
@Override
public int compareTo(Person o) {
if (this.getAge() -o.getAge() == 0){
if (this.getName().equals(o.getName())) {
return 0;
}else{
return -1;
}
}else{
return this.getAge() - o.getAge();
}
}
2、比较器排序Comparator
1、利用匿名内部类创建Comparator的对象,实现其中的compare方法,
返回值为0 新的元素和旧的元素相同 新的元素不进行添加
返回值为>0 将新的元素方法旧的元素右边的子节点上
返回值为<0 将新的元素方法旧的元素左边的子节点上
2、将Comparator对象作为方法的实参,传入到TreeSet的构造方法中
TreeSet
当上述的二者皆存在时,以比较器的排序为主.
14.2 泛型
如果未指定泛型,则集合允许存储任何类型的数据,默认类型为Object,在运行期间可能带来安全风险。当提供明确的泛型时,则仅允许存储指定数据类型的元素,其他数据类型无法存入该集合,在编译阶段进行检查:若新增数据不符合要求将报错。
定义格式:
<类型>: 指定一种类型的格式.尖括号里面可以任意书写,一般只写一个字母.例如:
<类型1,类型2…>: 指定多种类型的格式,多种类型之间用逗号隔开.例如: <E,T> <K,V>
泛型类:
修饰符 class 类名<类型> { }
单个泛型:
public class Student
public String name;
public E age;
}
多个泛型: 创建对象时,泛型的类型要么都写,要么都不写
public class Student<E,T> {
public String name;
public E age;
public T sex;
}
例子: Student<Integer,Character> student = new Student<>();
student.age = 100;
student.sex ='男';
泛型方法 参数位置:
定义格式
修饰符 <类型> 返回值类型 方法名(类型 变量名) { }
public
System.out.println(t);
}
根据传入的实参决定形参的类型
泛型接口
定义格式
修饰符 interface 接口名<类型> { }
例子:
public interface UserDao
public void add(T t);
}
实现了接口后仍然支持泛型
public class UserServiceDao
2、该类不具备pangyang能力 需要在接口处指定相应的pangyang 必须在定义实现类时明确具体类型的pangyang
publicclassUserServiceDaoimplementsUserDao
泛型方法 返回值位置:
public interface UserDao
public T add(Integer integer);
}
泛型通配符:
类型通配符: <?>
ArrayList<?>: 表示为元素类型不确定的ArrayList,其元素可匹配任意类型的.
类型通配符上限: <? extends 类型>
ArrayListList <? extends Number>: 它表示的类型是Number或者其子类型
类型通配符下限: <? super 类型>
ArrayListList <? super Number>: 它表示的类型是Number或者其父类型
14.3 双列集合 Map 集合
特性:Key - Value 对与身份证号码对应关系表中显示的是一个人的信息可通过 Key 获取 Value 该表中 Key 是唯一且无序不重复而 Value 是可重复出现
Map集合的基本方法:
V put(K key,V value) 向字典中添加一个键值对。如果该键已经存在于字典中,则会将新键-新值对替换当前键-旧值对。
V remove(Object key) 根据键删除键值对元素
void clear() 移除所有的键值对元素
boolean containsKey(Object key) 判断集合是否包含指定的键
boolean containsValue(Object value) 判断集合是否包含指定的值
boolean isEmpty() 判断集合是否为空
int size() 集合的长度,也就是集合中键值对的个数
V get(Object key) 根据键获取值 如果对应的key不存在 返回null值
Set
Collection
Set<Map.Entry<K,V>> entrySet() 获取所有键值对对象的集合
常用的实现类
1、hashMap集合
原理: 数组+链表+红黑树 线程不安全的
生成key的哈希值,并通过putval方法将其传递给包含计算得到的哈希值、键以及对应的值的对象。
if ((tab = table) == null || (n = tab.length) == 0)
2、创建长度为16的数组,并且将数组的值赋值给table属性
n = (tab = resize()).length;
基于给定的哈希值与数组长度进行计算得到存储位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
4.1 假设该位置存在数值,则比较该单元格内的数值与其输入数值的hash与equals属性是否相同。只有当它们都等于时,才将对应的单元格设置为e
Node<K,V> e; K k;
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
4.3 当 hash 值相等但 equals 不一致时 或者 hash 值不一致时, 对该链表的位置执行循环检查, 同样需要比较添加的元素与链表中的元素的 hash 和 equals 是否一致
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
当新增的一个元素不与整个链表中的任何一个相等时,请将其附加到链表末尾
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
4.2 因为e不等于null, 获取对应对象的value,替换成新的 value 值,并返回原有的 value 值。
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
注意点:
- 当自定义类作为我们的键时(即当我们将自定义类用作键的对象属性完全相同时),希望在Map中仅保留一个实例作为该键,则该类必须实现hashCode和equals方法。
- 其中的键值对都可以取null值。
当自定义类被用作键值对的值时,请检查该Map中是否存在具有相同属性的对象,并相应地重新实现equals方法
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
通过equals判断value是否相等
if ((v = e.value) == value ||(value != null && value.equals(v))) 通
return true;
}
}
}
return false;
2、Hashtable集合
使用数组与链表结合的数据结构:预设数量为11个元素的数组,默认负载因子设置在0.75。不允许为空的关键值域与值域,并且该数据结构具有高度的线程安全特性
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
int hash = key.hashCode();
}
一旦数组中的元素数量超过数组长度的75%,就会被系统自动触发扩容操作,并将容量增长到当前数量的两倍加一
int newCapacity = (oldCapacity << 1) + 1;
面试题:
HashMap和Hashtable的区别:
1、HashMap的key和value都可以存储null值,而hashtable都不行
2、HashMap线程不安全 Hashtable 线程安全
Hashtable typically initializes its array with a default size of 11 and expands its capacity to twice the original size plus one. The HashMap class typically initializes its array with a default size of 16 and expands its capacity by doubling the original size.
3、LinkedHashMap集合
它们的存储机制是相同的,在原有的数据结构上新增了一个链表结构体, 从而使得每个当前元素能够明确指向其下一个被新增的元素位置。
4、TreeMap集合
TreeMap是一个排好序的Map,根据key排序,底层用的红黑树
key可以选择满足Comparable接口的方式或者在构建集合时指定排序依据,在Comparable接口与基于比较方法并存的场景下,默认采用基于比较的方法进行排序。
面试题:1、Collection和Collections的区别:
Collections 是操作集合的工具类 帮助我们操作集合
Collection是所有单列集合的父类
2、使用增强for循环,能边遍历集合中元素,边添加或者删除吗?
会导致在遍历期间执行检查,并确保修改次数与预期一致;如果不一致,则会触发ConcurrentModificationException。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException(); }
如何改进? 使用迭代器
Iterator
while (it.hasNext()) {
System.out.println(it.next());
it.remove();
}
使用迭代器 边遍历边删除
第15章 IO流
File代表文件或目录。无论该File是否存在,都可以通过相关的方法生成一个新的File.
Java中的File(String pathname)通过将其参数字符串解析为抽象路径名来生成一个新的File实例
The File class is instantiated with a parent and child path string to generate a new File instance.
该类方法通过指定其父抽象路径名称和子路径名称字符串来生成该文件实例
常用方法:
exists() 判断文件或者目录是否有存在 不存在返回false 存在返回true
createNewFile() 用来创建文件
public boolean mkdir() 创建目录 / 文件夹
public boolean mkdirs() 创建目录 文件夹 递归创建
delete() 删除文件或者问价夹
isFile() 判断是是否为文件
isDirectory() 判断是是否为目录
绝对路径: 包括完整的路径名+文件名
相对路径: 相对于某个位置(基准点)进行下一步的寻找
public String getAbsolutePath() 实现了此抽象路径名对应的绝对路径字符串
public String getPath() 将此抽象路径名转换为路径名字符串
public String getName() 返回由此抽象路径名表示的文件或目录的名称
public File[] listFiles(String abstractPath) 返回该表示所指 directory 中的文件与子文件夹的File对象数组
IO流:
流向: 输入流 读数据
输出流 写数据
类型: 字节流
字节输入流 超类/父类
子类: FileInputStream 使用该类创建输入流对象
public FileInputStream(String name) 从那个文件读数据
public FileInputStream(File file)
方法: read 读数据 public int read() 每次只读一个字节 返回对应的字节数
read(byte b[]) 返回值为实际返回字节数量,并将读取的数量存储在数组中。
如果读不到返回值为-1
public int read(byte b[], int off, int len) 该方法会将读取的字节数存储到数组中,并从指定offset位置开始存储length个字节;剩余未被覆盖的位置将被设置为零值
terminate the stream is used to close it; employ a try-catch-finally block to handle I/O exceptions; regardless of whether an exception occurs, the stream can be terminated.
将关闭流的操作放到finally里面
字节输出流 超类/父类 OutputStream 抽象类
子类: FileOutputStream 使用该类创建输出流对象
public.FileSystem类的构造方法用于指定文件路径
public FileOutputStream(File file)
在Java中,在public类中有一个FileOutputStream字段String name用于指定文件名,并且包含一个布尔变量append来指示该操作是否为追加行为
public FileOutputStream(File file, boolean append)
方法:write 写数据 public void write(int b) 一次写一个字节
public void write(byte b[]) 一次写一个字节数组的内容
public void write(byte b[], int off, int len) 在指定的位置起始地进行书写,并随后记录书写次数
close the stream, a method to ensure the flow is shut down properly. By utilizing try-catch-finally blocks, we can systematically handle Io exceptions. Regardless of whether an error occurs or not, the flow remains closed.
将关闭流的操作放到finally里面
字节缓冲流:
BufferedOutputStream(OutputStream out) 创建字节缓冲输出流对象
BufferedInputStream(InputStream in) 创建字节缓冲输入流对象
常用方法和字节输入和输出流的常用方法是一样的
字符流
字符输入流
Reader: 用于读取字符流的抽象父类
FileReader: 用于读取字符流的常用子类
常用的构造方法:
FileReader(File file) 基于从该 File 中读取数据的情况生成一个新的 FileReader.
该函数用于生成一个名为FileReader的对象,在获取相关文件的信息时会创建一个新的实例。
常用方法:
read() 一次只读一个字符 返回对应的字符的编码 读不到返回-1
read(char cbuf[]) 一次性获取一批字符,并将这些字符存储到指定的字符数组中。函数返回本次成功读取的字符数量。
public int read(char[] cbuf, int offset, int length) 一次性读取一批字符 并将其存入目标字符数组中
从off指定的索引位置开始放,放len个
调用close方法会使这条数据流被关闭;一旦这条数据流被关闭后,则无法继续读取任何内容,并将抛出异常:java.io.IOException: Stream closed。
字符输出流
Writer: 用于写入字符流的抽象父类 常用的实现类是FileWriter
构造方法:
FileWriter(File file) 根据给定的 File 对象构造一个 FileWriter 对象
WriterFile接收指定文件对象和布尔值append参数并创建一个WriterFile实例
FileWriter(String fileName) 通过给定文件名来生成一个 FileWriter 对象
Writer(String fileName, boolean append) 根据指定文件名及追加操作指示创建Writer实例.
常用方法: 写 线程安全的 加了synchronized关键字
public void write(int c) 一次写出一个字符
public void write(char cbuf[]) 将字符数组中的内容全部写出
public virtual void write(char[] cbuf, int offsetVariable, int numberOfChars) 该方法将字符从指定位置存储到字符数组中指定数量
public void write(String str) 一次写出整个字符串的内容
public void write(String str, int off, int len) 从指定位置开始插入len个字符
该方法将使输出流停止运行 一旦输出流被关闭 则不能再进行任何写操作 这会导致抛出java.io.IOException异常:流已关闭
flush() 不会关闭流 将流中的数据强制刷新出来 可以接着写数据
二者都能输出数据的原因是由于调用了StreamEncoder.writeBytes来输出数据
字符缓冲流:
BufferedWriter: 字符缓冲输出流 线程安全的
1、 public BufferedWriter(Writer out) {
this(out, defaultCharBufferSize);
}
需要一个Writer对象,创建一个默认的缓冲区大小 默认大小为8192 / 8kb
public static bufferSize(Writer out, int bufferSize) 中的第二个参数out表示自行指定缓冲区的大小
sz参数要>0,小于等于0会抛异常
常用方法:
newLine() 换行 ===> write("\n"); newLine() 根据当前系统决定使用哪个符号进行换行
public void write(int c) 一次写出一个字符
public void write(char cbuf[]) 将字符数组中的内容全部写出
public void write(char cbuf[], int off, int len) 从指定位置开始将cbuf中的字符写入, 共len个字符
public void write(String str) 一次写出整个字符串的内容
public void namedWrite(String str, int offset, int length) 从该字符串指定起始位置进行操作,并设置长度为length
关闭流:
close() 函数用于关闭流的同时清除缓存区,并且由于可能存在缓存区内存不足的情况导致无法向外输出数据以及缓存区未被彻底清空而导致残留数据的存在风险
flush() 清空缓冲区 不关闭流 。
缓冲区满了以后,也会自动将数据写到指定的文件中
if (nextChar >= nChars)
flushBuffer(); 清空缓冲区 将内容写到文件中
字符缓冲输入流:
BufferedReader: 字符缓冲输入流
构造方法:
public static final BufferedReader defaultReader = new BufferedReader(Reader.class);要求接受一个Reader对象,并通过调用该类提供的一组静态工厂方法来设置其缓冲区大小,默认情况下会初始化为8192字节(即8千字节)。其中一个是具有默认值8192字节(即8千字节)。
public BufferedReader(Reader in, int sz) 其中第二个参数用于表示自行设置缓冲区的大小
sz参数要>0,小于等于0会抛异常
常用方法:
readLine() 一次读取一行数据 如果读取不到返回null
read() 一次只读一个字符 返回对应的字符的编码 读不到返回-1
函数read(char cbuf[])一次性读取若干个字符,并将其存储于指定的字符数组中,并返回本次读取的数量。
public int read(char cbuf[], int off, int len) 一次性读取一批字符 并将其存入目标字符数组,
从off指定的索引位置开始放,放len个
转换流:
该类通过默认字符编码生成InputStreamReader对象
该方法通过指定的字符编码来构造InputStreamReader实例
当使用默认字符编码时,默认情况下会生成一个OutputStreamWriter对象
OutputStreamWriter(OutputStream out, String charset) 通过给定字符编码生成OutputStreamWriter对象
Properties : 基础类型同样是Map类型的存储机制;其中Map用于存储键值对,并且支持所有数据类型的键和值
Properties用于存储键值对,并且其中的键和值必须是String类型;此外,该系统确保线程安全。
配置key和value属性:调用setProperty方法(即setProperty(String key, String value)),其中要求参数均为String类型
获取value的值: getProperty(key) 通过key可以获取到value值
收集所有键值:stringPropertyNames() 该方法为我们提供了Set
Properties和IO流相结合的方法
和IO流结合的方法
void load(Reader reader) 从输入字符流读取属性列表(键和值)
void store(Writer writer, String comments) 负责将property中的信息写入指定的文件中。其中第二个参数用于对内容进行解释说明,并具有特定的功能描述。
对象序列化流: 对象以二进制形式编码并存储在预设的本地存储介质中。该技术方案也在网络传输过程中的数据编码应用中被广泛采用。
把对象保存进文件中,序列化,从文中读取数据,保存成对象,反序列化
序列化流: ObjectOutputStream(OutputStream out)
1、对象必须实现java.io.Serializable接口
ObjectOutputStream.writeObject(Object obj)将该对象序列化地保存到特定的本地文件中
反序列化流程: 解析并还原为原始数据结构的本地存储的文件, 构造相应的对象实例, 该流程仅适用于来自已序列化的文件
反序列化流: ObjectInputStream(InputStream in)
1、对象必须实现java.io.Serializable接口
2、ObjectOutputStream.readObject()将文件的内容反序列化成一个对象
序列化完毕后,如果改变了类的属性名,那么就不能在反序列化回来
默认情况下: 当进行序列化操作时,基于对象的属性(非transient修饰)会生成一个序列化id,同时我们进行序列化保存到我们的
当反序列化发生时,在本地文件中,会比较本地文件中的序列化ID与当前类新生成的序列化ID(该类非transient属性生成)进行对比
如果一样,说明类的属性没有改过,可以反序列化成功,否则,抛异常
在改变类中的属性的情况下,还想反序列化成功,1、重新序列化
2、自己设定一个序列化id,无论是否改变属性,序列化id是不变的
private static final long serialVersionUID = 42L;
transient:
对于成员变量而言,在其被 transient 关键字修饰时,在线性代数中这些变量将不在序列化过程中被处理。
加入指定的关键字后不会影响反序列化过程;只有当所选对象的序号未发生变化时才能实现反序列化的操作。
该成员变量在获取时就是默认值。
第16章 多线程
16.1 线程的创建方式
1、继承Thread类
public class MyThread extends Thread{
@Override
public void run() {
// 想让线程帮你做的事情写到run方法中
}
}
1.2、创建线程对象
MyThread m1 = new MyThread()
1.3、启动线程
通过start方法启动线程,在此过程中,m1始终代表当前的线程对象。在启动之后,触发其运行的方法,并且由于m1继承并覆盖了原本的run方法,从而使得后续的事件处理基于修改后的功能进行
2、实现Runnable接口
public class MyRunnable implements Runnable{
@Override
public void run() {
// 想让线程帮你做的事情写到run方法中
}
}
2.2、创建Runnable类型的对象
Runnable runnable = new MyRunnable();
2.3、生成一个Thread类的对象,并将其作为参数传递给构造函数
Thread thread = new Thread(runnable);
public Thread(Runnable target, String name) 是一个用于创建受控子线程的对象;其中参数target指定可执行任务;参数name指定字符串类型的名字;名称name用于为线程命名
2.4、启动线程
start方法
当启动该线程时,在其回调函数中执行的run方法,在初始化阶段将接收的可执行任务传递给指定属性
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
this.target = target;
}
@Override
public void run() {
if (target != null) {
// 因为target不等于null,执行传入的runnable中run方法
target.run(); }
}
Thread.currentThread().getName()用于获取当前的主线程名称,在该主线程执行该方法时会返回哪一个子线程的名称
Returns a reference to the currently executing thread object
currentThread() 该方法返回的是当前正在运行的线程的对象
setName 线程设置名字 getName 获取当前线程的名字
3、实现Callable接口
3.1、自定义的类实现 Callable接口,并且实现接口中的抽象方法
public class MyCallable implements Callable
// call方法是线程要执行的方法,并且可以得到方法的返回值
@Override
public Integer call() throws Exception {
return null;
}
}
生成一个FutureTask实例,并将上述Callable对象作为参数传递给该实例
FutureTask
3.3、创建Thread对象
public class FutureTask
public interface RunnableFuture
该Runnable类型的实例FutureTask通过继承机制间接继承了该接口,并自定义实现了run方法
Thread thread = new Thread(futureTask);
当线程执行时会触发Thread中的run函数;随后,在futureTask内部也会被调用其内部的run函数;具体来说,在运行过程中会调用Callable类型的call函数。最终可以通过get函数来获取该操作的结果。
3.4、使用get获取返回值
Integer sum = futureTask.get(); 让当前的线程处于阻塞状态
常用方法:
该静态方法负责让当前正在执行的任务(线程)在给定的时间长度内暂停其执行
此方法在哪个线程中,就让哪个线程休眠
面试题: Thread、Callable、Runnable的区别?
Thread本质上代表线程,并且必须继承;run方法不返回任何值,并且无法通过run方法抛出异常;仅能使用try-catch结构来处理异常。
外部无法获取到异常信息
Runnable is an interface that can only be implemented. Its run method does not return a value and cannot throw exceptions; it must be enclosed within try-catch blocks.
外部无法获取到异常信息
Callable serves as an interface that can only implement the call method with a return value. It can obtain the return value by calling the get method. The call method may throw exceptions, which can be detected by external entities.
只能放到FutureTask中执行
子进程:主进程运行时会触发此子进程,并伴随子进程完成运行。不论子进程的业务逻辑是否完全完成,在主线进程中都会触发子进程退出并被配置为setDaemon(true)的状态。
非守护型线程中,在主程序运行完成之后,并不意味着分线程会立即停止运行。相反地,在分线程的角度来看,并不会终止运行而是会持续进行下去,并最终完成其中的业务逻辑处理。
16.2 线程优先级
线程调度
两种调度方式
分时调度模型是一种机制,在此机制中各线程轮流获取CPU权值,并通过均匀分配的方式确保每个线程在CPU资源上的公平使用。
2、采用抢占式调度策略的模型旨在将高优先级的任务分配给CPU资源;当多个任务拥有相同的优先级别时,则会通过随机方式选择其中一个;而具有较高 priority 的任务将能够获得更多的 CPU time slices.
Java使用的是抢占式调度模型、随机性
如果一个计算机系统中只有一个CPU核心,在某一特定的时间段内该CPU只能处理一条指令序列;线程要想执行操作就必须先获得CPU的时间片(即使用权限),这样才能继续运行指令。
因此,多线程程序的运行具有不确定性,其原因不明
设置和获取优先级
final int getPriority() 返回此线程的优先级
final void setPriority(int newPriority) 该方法将此线程的默认优先级设置为5,并规定该线程的优先级范围限定在1至10之间。
16.3 线程同步之Synchronized
线程没有同步,会出现同票、负票、0号票 安全安全问题
出现线程安全问题的前提: 多个线程共享同一个变量 会出线程安全问题
加锁: synchronized 同步锁 修饰代码块 代码块执行完,才会释放锁
synchronized(锁对象){
}
所有线程监控同一个锁对象
确保在所有操作此同步代码块的线程中是唯一,并且只允许一个线程对该代码块进行操作
只有当前线程释放锁了,下一个线程才能进来执行相应的代码块
好处:解决了多线程的数据安全问题
缺点是在多线程场景下,由于每个线程都会检查同步锁的存在与否这一操作带来了较高的资源消耗,并导致串行化执行
效率变低了
修饰方法: 每次只能由单个线程执行;其他线程无法获取锁;被阻塞;完成后释放锁
实现Runnable接口:
单线程模式:在默认情况下使用当前类(class)作为同步监视器(lock object),整个内存空间内是唯一的一个
对于非静态方法而言,默认情况下会将当前对象设为同步监视器;当某类实例调用该方法时,默认情况下该实例将成为同步监视器
继承Thread类
非静态: 默认谁调用该非静态方法,同步监视器就是谁 不能用
在默认情况下,默认采用当前类.class作为同步监视器使用,在整个内存空间内唯一存在
实现Callable接口:
静态类:在默认情况下将同步监视器设置为当前类.class,并确保在整个内存空间中每个对象都是唯一的。
non-static: By default, the method is synchronized with the current instance. The corresponding instance acts as the synchronized one when the method is called by whoever invokes it.
该Callable类型必须具有唯一性,并规定每个线程应分配独特的FutureTask实例,在整个运行期间需持续遵守这一原则
会判断FutureTask的状态,该状态会随着线程的执行发生改变
synchronized存在的问题: 1、具体加锁和解锁的过程我们看不见
2、对于多个线程来说,获取到锁是不公平的
16.4 线程同步之Lock
Lock: 锁 接口
ReentrantLock 实现类 可重入锁 默认是非公平的锁
多个线程共用同一个Lock对象
1、需要创建一个Lock类型的对象
2、使用Lock对象进行加锁 lock()
3、使用Lock对象进行解锁 unLock()
当使用Lock对象作为锁对象时,必须将该对象包含在try...catch...finally代码块内以完成加锁及解锁操作
即使发生异常也能自动解锁,在上述代码块之外同样可以实现该功能;但如果在代码块内部出现异常则无法自动解锁。
生成 lock 对象时,在构造函数中设置为 true 的情况下,则这种情况下使用的将是公平锁定机制。这种情况下将确保多个线程能够轮流访问 lock 资源以防止 deadlocks 的发生。
该方法:tryLock()旨在获取锁。若无参数,则无法立即获取,则会离开当前线程而不阻塞lock()。
如果有参数,第一个代表尝试获取锁的时间,第二个代表时间的单位,
在设置的时间内没有获取到锁,不会阻塞线程,直接进行下一布,
在设置的时间内获取到锁,执行相应的业务逻辑
注意点: 解锁之前需要要加锁并且加多少次锁就需要解多少次锁
在多线程环境中,当多个线程相互之间占有彼此所需的资源而无法释放时,就会形成所谓的"死锁".值得注意的是,这并不是一个错误状态.为了避免这种潜在的问题,应该采取适当的措施来预防和解决可能导致"死锁"发生的问题.
案例:
Object o1 = new Object();
Object o2 = new Object();
new Thread(){
@Override
public void run() {
synchronized (o1){
try {
Thread.sleep(2000);
synchronized (o2){
System.out.println("线程1执行完毕");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread(){
@Override
public void run() {
synchronized (o2){
try {
Thread.sleep(2000);
synchronized (o1){
System.out.println("线程2执行完毕");
}
} catch (InterruptedException e){
e.printStackTrace();
}
}
}
}.start();
16.5 线程的生命周期
1、新建状态 new Thread这个过程新建一个线程
在ready state(表示线程已准备好执行)的情况下创建完成了线程对象之后调用start方法之后将处于 ready state
3、运行状态(线程正在运行) 处于准备状态的线程一旦获得处理器资源后会立即启动执行
在运行阶段过程中,如果当前线程在时间片结束时无法抢占CPU执行权,则返回就绪状态
调用yield方法也能让线程回到就绪状态
4、阻塞状态 通过调用相关的方法来实现线程暂时停止运行,并使线程释放了CPU资源而不占用竞争的CPU资源。
sleep方法会释放cpu资源,不会释放锁
wait方法会释放cpu资源,会释放锁
当阻塞完毕后,从阻塞状态回到了就绪状态,得到CPU后继续运行
5、消亡: 线程中的run执行完毕或者调用stop方法(),处于死亡状态
16.6 线程间通信
多个线程之间相互协作完成一些事情。
synchronized:
void wait() 当监视器对象调用wait()方法会导致当前线程被阻塞,停止运行,放弃对CPU的控制权,会释放相应的锁
当该监视器对象被通知执行notify方法时, 会从当前因调用wait而导致阻塞的线程中随机选择并唤醒其中一个, 使其恢复到就绪状态
得到CPU执行权,继续执行
每当监视器被激活时,在等待操作后会自动唤醒所有相关的线程。
让所有的线程都回到就绪状态,得到CPU后,继续执行
当调用notify()或notifyAll()时,会使正在执行的线程重新获取锁对象,并使得其他等待该锁的线程无法获得该锁。
注意点: 1、前文提到的三项技术应包含在被synchronized修饰的方法或代码块内。
2、上述的三个方法必须使用同一个同步监视器对象(锁对象)
3、wait(参数)被阻塞的时间为指定的毫秒数,在此被阻塞的时间段内若有其他线程唤醒本线程,则该线程在获得锁后会继续执行。
在没有其他线程进行唤醒的情况下,在规定时间自动进行唤醒操作;如果能够成功获得锁,则继续执行当前操作。
lock:实现线程间的通信
1、需要多个线程共享同一个锁对象
生成Condition类的对象,并启动相关的操作流程以使得主线程暂时暂停执行;同时释放被阻塞的子线程以便继续执行。
public class MyThread extends Thread{
// 同一个锁对象
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
}
通过Condition类的对象调用await函数使得主线程被阻塞。在调用此操作之前,请确保进行锁获取与释放操作。
try {
lock.lock();
for (int i = 0;i<100;i++){
if (i==50){
condition.await(); //是当前线程阻塞
}
System.out.println(Thread.currentThread().getName()+"=====>"+i);
}
}catch (Exception e){
System.out.println("发生异常了");
}finally {
lock.unlock();
}
通过上述Condition类型的对象调用signal方法来唤醒一个被await()阻塞的线程
signalAll方法唤醒所有因为调用await()阻塞的线程
在调用signal()[notify()]或signalAll()方法前一步骤必须进行锁定操作,在完成后必须释放锁定。
lock.lock();
condition.signalAll();//notifyAll()
lock.unlock();
5、注意:上述的三个方法必须使用同一个Condition类型的对象。
面试题:
假设存在三个线程a、b、c,则要求这三个线程同时进入就绪态,在执行过程中必须严格按照a→b→c的顺序进行。
一旦a或b线程陷入阻塞状态,必然依照a→b→c的顺序运行.如何才能确保这一需求得以实现?
线程精确唤醒:在运行过程中会用到,基于上述Condition条件变量实现线程的精确唤醒
public class M16 {
private int num = 1;
private Lock lock = new ReentrantLock();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
private Condition condition3 = lock.newCondition();
// 线程1执行method1
public void method1(){
try{
lock.lock();
while (num!=1){
condition1.await();
}
Thread.sleep(2000);
System.out.println("=========1==========");
num = 2;
condition2.signal();
}catch (Exception e){
}finally {
lock.unlock();
}
}
// 线程2执行method2
public void method2(){
try{
lock.lock();
while (num!=2){
condition2.await();
}
Thread.sleep(2000);
System.out.println("=========2==========");
num = 3;
condition3.signal();
}catch (Exception e){
}finally {
lock.unlock();
}
}
// 线程3执行method3
public void method3(){
try{
lock.lock();
while (num!=3){
condition3.await();
}
Thread.sleep(2000);
System.out.println("=========3==========");
num = 1;
condition1.signal();
}catch (Exception e){
}finally {
lock.unlock();
}
}
}
当执行条件判断操作时,在决定是否调用await()方法的过程中,采用while循环机制来执行条件判断,以避免不必要的唤醒事件发生。
注释中也建议放在while循环中
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
16.7 volatile关键字
MESI缓存一致性协议
MESI缓存一致性协议是一种用于多处理器系统中实现数据一致性的机制。在该机制下,多个CPU从主内存中读取同一个数据并将其存储在各自的高速缓存中。一旦某个CPU修改了其缓存中的数据值,则该数据变更会被立即同步回至主内存。其他CPU通过总线上的特定机制(称为嗅探)能够检测到这一变化,并相应地失效自己的缓存副本。当这些失效的CPU检测到变化后,在等待新数据的过程中会自动从主内存重新加载最新的数据到工作内存中以保持系统的一致性。使用volatile关键字时,默认情况下会启用总线上的MESI缓存一致性协议

通过总线传递后,将修改写入主内存。为了确保数据一致性,在每个CPU启动时会启用总线上的MESI一致性协议来实现volatile功能。当一个CPU检测到其他CPU对变量进行了修改时,它会立即失效并停止使用自己的工作内存中的值,并等待从主内存中重新获取最新的数据。在存储操作前会加锁以防止其他操作干扰,并在锁住期间阻止其他所有线程访问主内存中的数据;一旦存储完成并释放锁后即可继续执行其他操作。由于锁的粒度非常小,在实际应用中其影响可忽略不计。

怎么保障读取是最新得值呢?
主要通过汇编locker前缀指令完成volatile缓存可见性的底层实现工作,在此过程中该指令会锁定期该内存区域的缓存(具体为缓存行层面的锁定),并最终将这些被锁住的缓存返回至主内存进行处理
volatile关键字的作用
1、volatile开启总线MESI缓存一致性协议,每个cpu 都会监听总线
2、将工作内存中的更改后的变量及时刷新回到主内存
通知其他相关线程在对应的工作内存中发现某个变量已失效,并主动从主内存中获取最新的数据值。
synchronized关键字的作用
1、线程获得锁
2、清空变量副本
3、拷贝共享变量最新的值到变量副本中
4、执行代码
5、将修改后变量副本中的值赋值给共享数据
6 、释放锁
可见性: 一个线程对共享变量的修改,对于其他线程是可见的
不允许进行指令重新排序:当不影响最终结果时,JVM会对我们的代码进行重组以达到优化效果。被synchronized修饰的方法或代码块内的代码不允许进行指令重排,则会按照编写时的顺序执行,而volatile关键字同样可以实现这一功能。
原子性: synchronized可以保证原子性 volatile 不能保证原子性
16.8 CAS
CAS 全称CompareAndSwap 比较并交换
需要3个值: 主内存的值 V 期望值 A 要改变的值 B
从系统资源管理器中读取当前进程列表并将其存储到工作空间中
随后生成相应的配置文件
这些配置文件将在后续步骤中被逐一加载到系统资源管理器中
如果主机内存当前的数值与预期数值不一致,则会使用目标参数来替代主机内存中的当前数值,并返回错误信息。
该失败现象归因于在随后的获取过程中,其他线程修改了主内存的值,从而使得前后两次获取结果不一致
因此更改失败
AtomicInteger:自旋锁+CAS
Exploring the Mechanism and Efficiency: A Step-by-Step Examination of the AtomicInteger Decrement-and-Get Method, Understanding Its Role in Concurrency Control
unsafe.objectFieldOffset
通过调用unsafe.objectFieldOffset方法可以获取到value字段在对象中的位置偏移量(实际上这是一个字段相对于对象头部的位置差值,在获得这个位置差值之后就可以快速确定该字段的位置)
static {
try {
value偏移量等于调用Unsafe类中的objectFieldOffset方法获取AtomicInteger类中声明字段"值"的位置
} catch (Exception ex) {
throw new Error(ex);
}
}
定义value为volatile类型,保证value在多线程中的可见性
private volatile int value;
decrementAndGet
调用unsafe.getAndAddInt,参数为当前对象,偏移量,操作值
/**
-
Atomically decrements by one the current value.
-
@return the updated value
*/public final int decrementAndGet() {
//最后-1是由于getAndAddInt获取的值为交换前的值
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
getAndAddInt
通过compareAndSwapInt返回循环getIntVolatile获取最新内存值
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
getIntVolatile
该方法涉及两个参数:目标对象和偏移量;通过从指定内存地址中提取指向的整数,并遵循volatile语义进行操作
public native int getIntVolatile(Object var1, long var2);
compareAndSwapInt
该方法涉及四个参数:当前对象、偏移量、期望值和新值。如果当前对象对应偏移量的值等于期望值,则设置当前对象为新值并返回true;否则返回false。
comparable_and_swap_int方法作为原子操作执行,在同一时间只能有一个线程对该对象进行比较和交换。即使在发生中断的情况下也不会影响其他线程的执行。当对该对象进行比较和交换时,在此之前会添加一个Lock指令以确保数据的一致性;该Lock指令的主要作用在于:通过锁机制保证多个线程不会同时对同一个对象进行修改或删除操作;这种设计有助于提高系统的并发性能和稳定性
1、对内存的读-改-写操作原子执行
2、禁止该指令与之前和之后的读和写指令重排序。
3、把写缓冲区中的所有数据刷新到内存中。
第1点确保了CAS操作是一个单一的操作;第2和3点确保了CAS同时具备被支持的volatile读和write特性
第1点确保了 CAS 操作是一个单一的操作;第 2 和 3 点确保了 CAS 同时具备被支持的 volatile 读与 write 特性
该boolean类型的确定性静态方法接受四个参数:Object、long、int和int类型
面试题:1、CAS原理是什么?会出现什么问题?(ABA)
2、什么是ABA问题?及解决方法?如下链接
该领域中经典的ABA问题是具有重要学术价值的问题之一
ABA问题在软件开发中尤为突出
3、AtomicInteger的源码流程
16.9 并发工具类
HashMap存在线程不安全问题,在多线程操作时可能会抛出ConcurrentModificationException异常
1、Hashtable 数组+链表 线程安全替换HashMap
该问题在于Hashtable对整个hash表进行锁定,当有3个线程同时对hash表进行操作时,必须依次等待队列位置才能完成操作
效率低下。即使3个线程向不同的位置插入数据,也需要依次执行。
在多线程场景下,某个线程访问某个位置时,对该位置进行加锁操作即可,无需对整个表进行加锁
比如,线程1和线程2,用到hash表中的第2个位置,此时给第2个位置加锁
即可,
线程3引用表中的第5个位置,在该位置上实施加锁操作即可;无需对整个表进行加锁操作以提升并发处理能力
2、Collections工具类,可以将线程不安全的集合类,变成安全的集合类
HashMap<String,Integer> hashMap = new HashMap<>();
Map<String,Integer> maps = Collections.synchronizedMap(hashMap);
for (int i = 0;i<100;i++){
new Thread(){
@Override
public void run() {
maps.put(System.currentTimeMillis()+"",1);
System.out.println(maps);
}
}.start();
}
3、ConcurrentHashMap线程安全 分段锁
1、key和value都不能存储为null,有一个为null,就会抛出空指针异常
2、源码分析
该构造方法未做任何操作,并将所有属性均初始化为默认值;并使用volatile关键字进行了修饰;单个线程对属性的更改会被其他线程所知悉;其余线程则会基于volatile特性获取最新数据存入本地内存。
public ConcurrentHashMap() {
}
调用put方法添加元素,此时多个线程,多个线程同时执行put方法
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
计算添加的元素的key的hash值
int hash = spread(key.hashCode());
int binCount = 0;
将table属性赋值给局部变量tab,table默认值为null,tab也是null
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
因为tab=null,if的判断结果为true
if (tab == null || (n = tab.length) == 0)
进行初始化操作【initTable具体方法】
U.compareAndSwapInt(this, SIZECTL, sc, -1) 基于CAS算法防止只有一个线程进入该操作,并且执行的是一个原子操作。
if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY;默认值为16
@SuppressWarnings("unchecked")
创建长度为16的Node类型的数组,并且赋值给table属性
Node<K,V>[] nt = (Node<K,V>[])new Node,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}】
局部变量tab和table属性指向同一块内存,因为此时的for循环是一个
死循环,导致tab一直都是有值的,
tab = initTable();
由于是死循环,在再次执行循环时,基于计算出的索引位置检查当前索引位置是否存有值。
如果没有值,直接在当前位置插入一个值[单线程]
在多线程运行过程中,在各个子进程中(子进程或子节点),每个进程都会计算出一致性的索引。当多个进程访问同一存储单元时(即同一索引位置),均无数据存在,则会触发else if条件。当执行插入操作时,在使用CAS算法的情况下(即Compare-and-Swap机制),确保只有一个子进程能够完成该操作。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
期望值 null 内存值 对位索引位置的值[tab, ((long)i << ASHIFT) + ABASE]
预期值与内存单元相同,则将对应索引处赋以数值v,这样对应的索引处将存储数值v
其他线程再次接入时,** 期望值仍为null, 内存中存储的值并非null. 导致此次替换未能成功完成.
继续执行循环,来到else代码块中
casTabAt源码:【U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v)】
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
在上述else if结构中(f = \text{tabAt}(tab, i = (n - 1) \& \text{hash}) == null)已经正确赋值了。其中f表示当前索引位置对应的值,并进行了加锁操作。
Assume that the next subthread obtains an f object. If the two subthreads obtain different f objects, they belong to separate locks and can execute in parallel.
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
依次检查各链表的位置,在比较时,只有当key的哈希值与equals均相等时
说明是同一个key,用新的value替换旧的value
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
最后整个遍历的过程中,链表上的所有的key值和所添加的key都不相等,
创建新的节点,放在该链表最后一个位置。
Node<K,V> pred = e;
if ((e = e.next) == null) {
创建新的节点,放在该链表最后一个位置。
pred.next = new Node<K,V>(hash, key,value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
当链表的长度>=8时,变成红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
会进行扩容,扩容为原来的2倍,多个线程会协助扩容
addCount(1L, binCount);
return null;
}
2、ArrayList线程不安全,如何变安全
1、使用Vector替换ArrayList
2、Collections工具类,可以将线程不安全的集合类,变成安全的集合类
ArrayList
List
for (int i = 0;i<100;i++){
new Thread(){
@Override
public void run() {
list.add(1);
System.out.println(list.toString());
}
}.start();
}
3、CopyOnWriteArrayList
所采用的技术是:基于write-at-once技术和read-write分离策略,在添加新元素时会使用Lock机制进行锁定操作。一旦执行完毕后就会立即释放锁以供其他线程正常访问。具体流程如下:首先将原有数据结构复制一份(副本),新副本的数据长度比原始数据多一位;随后将新增的数据元素依次添加到新副本中;最后完成所有操作后会用新副本取代旧的数据结构以释放内存资源。由于采用了read-write分离技术,在读取数据时不涉及新副本中的内容而只在老副本中进行读取操作;当需要更新数据时则会在新副本中进行修改而不会影响其他线程的工作状态
因为array属性受volatile修饰,所以一个线程对其做出修改行为,会对其他线程产生可见的影响
public boolean add(E e) {
添加元素时,使用Lock锁进行锁定
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
现将原有的数组复制一份(副本), 新复制的数组比原有的数组长度大1
Object[] newElements = Arrays.copyOf(elements, len + 1);
将新添加的元素放到新的数组中,添加完毕后,用新的数组替换旧的数组
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
3、HashSet线程不安全
1、使用Collections工具类将线程不安全的类包装成线程安全的类
Collections.synchronizedSet
16.10 线程池
1、带有缓存的线程池newCachedThreadPool()
ExecutorService executorService = Executors.newCachedThreadPool();
MyThreadPool myThreadPool = new MyThreadPool();
executorService.execute(myThreadPool);
executorService.execute(myThreadPool);
Thread.sleep(5000);
System.out.println("============================================");
//在来新任务时,线程池如果有空闲的线程,会直接执行该任务
executorService.execute(myThreadPool);
2、带有固定线程数量的线程池newFixedThreadPool
ExecutorService executorService = Executors.newFixedThreadPool(5);
MyThreadPool myThreadPool = new MyThreadPool();
for (int i = 0;i<6;i++){
executorService.execute(myThreadPool);
}
在恒定规模的多核处理器上运行的一个进程会触发多个子进程以并行处理资源分配问题,在资源耗尽的情况下系统会暂停当前运行的任务并将剩余资源用于解决其他问题
3、自定义参数的线程池
参数一:核心线程数量
参数二:最大线程数
参数三:空闲线程最大存活时间
参数四:时间单位
参数五:任务队列
参数六:创建线程工厂
参数七:任务的拒绝策略
- ThreadPoolExecutor.AbortPolicy: 当前策略允许指定的任务被丢弃并触发RejectedExecutionException异常。此为系统的默认行为。
- ThreadPoolExecutor.DiscardPolicy: 在这种情况下,系统会放弃指定的任务处理而不触发任何异常,这种方法并不被推荐使用。
- ThreadPoolExecutor.CallerRunsPolicy: 该策略通过直接执行run()方法实现了跨线程通信功能。
- ThreadPoolExecutor.DiscardOldestPolicy: 此策略会移除队列中最长时间未处理的任务,并将当前待处理的任务插入到队列顶部位置。
注:明确线程池对多可执行的任务数 = 队列容量 + 最大线程数
16.11 阻塞队列
ArrayBlockingQueue
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity]; 创建长度为1的数组用来保存元素
lock = new ReentrantLock(fair); 创建锁
notEmpty = lock.newCondition(); 创建两个条件变量 进行线程的阻塞和唤醒
notFull = lock.newCondition();
}
arrayBlockingQueue.put("abc")源码:
public void put(E e) throws InterruptedException {
checkNotNull(e); 检查传入的元素不能为null 如果为null 会抛出异常
final ReentrantLock lock = this.lock; 创建锁
lock.lockInterruptibly();如果线程没有中断,则线程获取锁
try {
count数组中元素的个数 items.length数组的长度
相等说明数组满了,对应线程进行等待notFull.await(),释放锁
当两个值不等于时,则会将要添加的元素放入数组中,并会唤醒负责获取数据的线程调用notEmpty.signal()。
唤醒那些被notEmpty.await()阻塞的线程
enqueue(e)该方法做的事情
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
System.out.println(arrayBlockingQueue.take());
arrayBlockingQueue.take()源码:
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); //当前获取数据的线程获取锁
try {
如果元素的个数为0,数组中没有元素,对应的线程阻塞
如果元素不等于0,数组中有元素,获取元素dequeue()
1、从数组中拿数据 2、唤醒那些添加数据的线程 notFull.signal()
唤醒那些被notFull.await()阻塞的线程
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
额外扩展:
JMM模型将计算机内存划分为两个区域,其中一部分为工作缓存区,另一部分为主内存区域
运行机制: CPU在运行时所需的数据由一级缓存和二级缓存提供,这些缓存数据又依赖于主内存,因此需要将主内存中的原始数据复制一份至临时存储区域
工作内存与主内存相互刷新的时机:
1、工作内存的数据刷新到主内存
1、释放锁之前,需要将工作内存的数据同步到主内存
2、给工作内存的变量值修改以后,也需要同步到主内存
2、主内存复制数据到工作内存
1、获取锁的时候,会强制将主内存的数据刷新到工作内存
当当前运行的线程进入另一个线程执行后,在切换回主内存的过程中,系统会将主内存中的数据重新加载到工作内存中
当执行当前线程的cpu处于空闲状态时,会从主内存中获取最新的数据并将其传输到工作内存中
发生异常会不会释放锁?
会释放锁
代码:
Object o = new Object();
for (int i=0; i<3;i++){
new Thread(){
@Override
public void run() {
synchronized (o) {
System.out.println(Thread.currentThread().getName()+"执行了");
int i = 10/0;
}
}
}.start();
}
java中 sleep yield join区别
Sleep方法由线程类提供,并必须携带一个时间参数。该方法负责将当前线程休眠进入阻塞状态并释放CPU资源(阿里面试题 Sleep释放CPU),而wait也会释放cpu资源,在在线程处于running状态时才会获取cpu片段。
join: The current thread yields the CPU scheduler, and the join() method will wait for the called thread to complete before it can continue executing.
该函数会将当前主线程释放并重新加入CPU调度队列,在某些情况下可能会立即被再次调度;需要注意的是,在使用该函数时必须确保主线程处于可运行状态;值得注意的是,在使用该函数时必须确保主线程处于可运行状态下;需要注意的是,在使用该函数时必须确保主线程处于可运行状态下;需要注意的是,在使用该函数时必须确保主线程处于可运行状态下;
通知线程调度器我的工作已近完成,请允许其他具有相同优先级的线程使用CPU,并无任何机制确保会被采纳。
4、wait:让出CPU调度,并且释放掉锁
java中sleep和wait的区别
sleep与Wait的区别:
1、sleep是线程方法,wait是object方法;
2、sleep释放cpu资源,不释放锁资源,wait释放cpu资源,也释放锁资源
3、当 sleep 到达时间时,它会自行从阻塞过渡到就绪的状态;然而,在这种情况下, wait 无法自行完成这一转变.
才会将线程唤醒,从阻塞回到就绪状态
wait操作需要将synchronized被修饰的方法或者放置在代码块中进行处理,sleep任务的位置都可以
5、sleep需要捕获或者抛出异常,而wait/notify/notifyAll则不需要。
当 sleep 调用完成后,在停止运行期间仍保持同步锁定。到时会继续执行。而 wait 调用则会放弃当前对象锁,并加入等待队列。当被通知唤醒指定的线程或所有线程时(通过调用 notify() 或 notifyAll()),只有在重新获得该对象锁之后才会转回运行状态。在未重新获得该对象锁之前不会立即执行。
synchronized和Lock的区别:
1、Lock是java中的一个接口,synchronized内置的关键字
2、synchronized发生异常时,会自动释放锁,因此不会出现死锁现象的发生
我们必须手动实现加锁与解锁操作,并且如果无法进行解锁操作,则会导致死锁问题。因此,在使用时,请确保在 finally 块中适当处理 lock 的释放。
3、基于synchronized实现的一种不公锁类型称为 unfair synchronized lock。而默认情况下,java.util.Lock 也是不公的。通过传递特定参数可以使该锁转换为公平lock。
4、Lock可以提高多线程进行读操作的效率
借助Lock中的tryLock方法可以确认是否成功获取到锁,而synchronized无法实现这一功能
6、锁可以通过条件变量Condition实现精准唤醒,而synchronized无法进行精准唤醒
Lock可以让被阻塞的线程被及时唤醒, synchronized无法实现这一功能, 等待的线程将无法被及时唤醒, 无法解除其阻塞状态。
ReadWriteLock读写锁:
排他lock/互斥lock:一旦当前thread成功取得一个lock实例后,其他thread将无法立即取得该资源对应的lock权限,唯有当前thread释放在所持资源之后,其他thread方能再次取得独占权
synchronized、ReentrantLock、写锁
共享锁: 多个线程可以共同享有该锁 读锁 不会造成线程阻塞
要点:一旦某个线程获得了write lock,那么其他所有线程将无法再获得任何类型的lock,包括write lock和read lock,并且在等待read操作时会触发deadlock情况
当一个线程获得了读锁时,其他线程也可以获得该读锁,但它们无法获得相应的写lock.
仅在同一个lock对象内的write lock与read lock相互排斥,并且write lock之间也相互排斥。而不同lock对象之间的write lock并不互相排斥。
读-读不互斥
案例:
public class AK47 extends Thread{
private static ReadWriteLock lock = new ReentrantReadWriteLock();
private static Lock readLock = lock.readLock(); //获取读锁
@Override
public void run() {
try{
readLock.lock();
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName());
}catch (Exception e){
}finally {
readLock.unlock();
}
}
}
写-写互斥
public class AK47 extends Thread{
private static ReadWriteLock lock = new ReentrantReadWriteLock();
private static Lock writeLock = lock.writeLock();
@Override
public void run() {
try{
writeLock.lock();
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName());
}catch (Exception e){
}finally {
writeLock.unlock();
}
}
}
读-写互斥
案例:
public class AK47 extends Thread{
private ReadWriteLock lock ;
private Lock readLock; //获取读锁
public AK47(ReadWriteLock lock){
this.readLock = lock.readLock();
}
@Override
public void run() {
try{
readLock.lock();
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName());
}catch (Exception e){
}finally {
readLock.unlock();
}
}
}
案例:
public class AK74 extends Thread{
private ReadWriteLock lock ;
private Lock writeLock;
public AK74(ReadWriteLock lock){
this.writeLock = lock.writeLock();
}
@Override
public void run() {
try{
writeLock.lock();
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName());
}catch (Exception e){
}finally {
writeLock.unlock();
}
}
}
测试:
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
AK47 ak47 = new AK47(readWriteLock);
ak47.setName("线程1");
ak47.start();
AK74 ak74 = new AK74(readWriteLock);
ak74.setName("线程2");
ak74.start();
结论: 第一个案例 线程名同时打印出来 读读不互斥
每隔我们设定的时间间隔打印一次;同时,在读操作与写操作之间必须互斥
ReentrantReadWriteLock的实现里面有以下几个特性
fairness: non-fair lock (default). read threads do not have any lock operations, thus read operations lack both fairness and non-fairness.
在执行写操作时, 由于可能会立即获得锁而导致会被推迟一些读操作或者一些写操作. 非公平锁的吞吐量显著高于公平锁.
重入性:读写锁遵循特定规则,在请求到来后会允许相应的线程(包括读线程和写线程)按照请求顺序重新获取相应的读锁或写锁。
只有写线程释放了锁,读线程才可以获取重入锁,
当一个线程获得Write Lock后,在其执行过程中允许其他线程在随后的周期中获得Read Lock;然而Read Lock持有者则无法在后续周期中再次获得Write Lock。
读锁可以嵌套读锁和写锁,一旦嵌套了写锁,对应写锁代码是进不来的
案例:
public void run() {
try{
readLock.lock();
test(); // 因为读锁嵌套了写锁,导致线程阻塞在test方法
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName());
}catch (Exception e){
}finally {
readLock.unlock();
}
}
public void test(){
try{
writeLock.lock();
System.out.println(System.currentTimeMillis());
}catch (Exception e){
}finally {
writeLock.unlock();
}
}
写锁可以嵌套读锁和写锁,没有任何问题 可以输出当前时间和线程名
案例:
public void run() {
try{
writeLock.lock();
test();
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName());
}catch (Exception e){
}finally {
writeLock.unlock();
}
}
public void test(){
try{
readLock.lock();
System.out.println(System.currentTimeMillis());
}catch (Exception e){
}finally {
readLock.unlock();
}
}
3、锁升级:read lock无法直接升阶为write lock, 若要取得write lock, 必须先解除所有的read locks
@Override
public void run() {
try{
readLock.lock();
readLock.unlock();
writeLock.lock(); // 到此该线程持有的是写锁了 锁升级
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName());
}catch (Exception e){
}finally {
writeLock.unlock();
}
}
4、降级机制:当一个write thread通过获取write lock后也可以获取read lock,并随后释放write lock,则实现了从write lock到read lock的转换过程(即完成了降级操作),此时该线程持有的便是read lock
@Override
public void run() {
try{
writeLock.lock();
readLock.lock();
writeLock.unlock();//变成了该线程持有读锁 锁降级
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName());
}catch (Exception e){
}finally {
readLock.unlock();
// writeLock.unlock(); 写锁为主
}
}
5、锁获取中断:读取锁和写入锁都支持获取锁期间被中断 都支持tryLock
6、访问条件变量:写锁允许访问[ConditionVariable]对象,而读取锁定不支持访问条件变量,如果不遵循正确操作流程可能会触发UnsupportedOperationExcetpion异常
7、读写锁的数量:读取锁和写入锁的数量最大分别只能是65535
