导图社区 Java基础面试突击
自本人学习Java以来一直保持更新,包含多线程,JVM,IO流等重点模块,依靠该导图成功突围阿里云、京东等大厂后端开发岗位,可应付95%以上面试问题
编辑于2022-10-26 15:32:51 惠灵顿Java基础面试突击
基础语法
基本数据类型
整数类
byte(8位)
short(16位)
int(32位)
整数类型默认直接量为int
long(64位)
有符号数
小数类
float(32位)
double(64位)
小数类型默认直接量为double
字符
char(16位)
汉字也是16位,所以一个汉字对应一个char
布尔
boolean(1位)
布尔类型不可与其他数据类型进行类型转换!!!
数据类型转换
顺序:byte、short、int、long、float、double
由小到大,称为拓宽类型,无需强转
由大到小,称为缩窄类型,必须强转
方法
方法组成
修饰符(public)
返回值类型(int)
方法名
参数列表
方法签名
方法体
方法重载和方法重写的区别
重载
同一个方法对于不同数据输入作出不同处理的过程
同名即可,参数可不同,返回值可不同
重写(覆盖)
当子类继承自父类的相同方法,输入数据一样,但要作出有别于父类的响应时,需要重写父类方法
同名同参同返回,权限修饰符不能比父类更封闭,抛出的异常不能更宽泛
歧义调用
getSum(1, 2) 可以同时匹配两个方法,任何一个方法都不比另一个方法更匹配,因此为歧义调用,导致编译错误。
递归
优点
代码简洁
缺点
不易debug
消耗空间资源
每次调用方法都需在内存栈中分配空间,过多次递归会导致内存溢出
消耗时间资源
如果子问题存在重叠,则在不加记忆化的前提下,可能产生重复计算,消耗时间资源
尾递归
https://blog.csdn.net/Vermont_/article/details/84557065
最后return的表达式仅为递归函数本身,不包含其他操作
传统地递归过程就是函数(或方法)调用,每调用一次就会占一份内存,而且如果递归链过长,可能会stack overflow
当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。
Java代码编译到执行的过程
JDK(Java开发工具包)
如果需要使用Java开发,必须安装JDK
JRE(Java运行时环境)
如果只是为了运行依赖Java的软件,安装JRE即可
JVM(Java虚拟机)
java.exe
基本的java类库
javac.exe(编译器)

面向对象
面向对象和面向过程的区别
面向过程
将问题分解成步骤,然后按照步骤实现函数,执行时依次调用函数。数据和对数据的操作是分离的。
优点
性能比面向对象高,不需要面向对象的实例化
缺点
不容易维护、复用和扩展
面向对象
将问题分解成对象,描述事物在解决问题的步骤中的行为。对象与属性和行为是关联的。
优点
具有封装、继承、多态的特性,因而容易维护、复用和扩展
缺点
调用对象需要实例化,开销比较大,因此性能低
Java四大特性
继承
继承是借助已有类定义来创建新类的技术,也是子类自动共享父类数据和方法的机制,增强了代码的复用性和扩展性
封装
封装就是将类的数据域声明为私有,避免从类的外部直接访问私有域,封装的目的就是实现软件内部“高内聚,低耦合”。
多态
多态是指引用变量所指向的具体类型,和通过该引用变量所调用的方法,在编程时并不确定,而是在程序运行期间确定。这样无需修改源代码,就能使引用变量绑定到多个类实现上,让程序可以选择多个运行状态,增强软件的灵活性和扩展性。
抽象
抽象就是找出事物的相似之处并且将它们归为一个类
值传递VS引用传递
值传递
方法接受到的是调用者提供的值拷贝
无法改变原参数值
引用传递
方法接受到的是调用者提供的地址
构造方法
1. 构造方法名称必须与类名完全相同
2. 使用new调用构造方法
3. 可以被重载,不可被重写
4. 没有返回值
默认构造方法
当没有显性声明任何构造方法时,会隐性声明一个无参构造器
如果人为定义了(显性声明)构造方法,则不产生默认构造器
所以如果你想让无参构造器和带参构造器同时存在,必须把两个都写上
静态VS实例
静态(方法/变量)
static修饰
依赖类存在
可以在实例方法里调static类成员,因为static类成员依赖类存在,而类加载先于任何对象实例化
无法在static方法中调用同一个类的实例方法,因为实例方法依赖对象存在,要想调用必须先实例化对象
实例(方法/变量)
无修饰
依赖实例存在
如何判断类成员(method/field)应该被定义为静态还是实例?
取决于类成员是否依赖于具体实例
初始化块
https://blog.csdn.net/weixin_43510080/article/details/104108957
静态初始化块
static修饰
在类加载时执行
只执行一次
普通初始化块
无修饰
在实例化类时执行
每次实例化对象都要执行
与构造器的区别
1. 无法接受任何参数
2. 相当于构造器的补充
可以减少多个构造器重载时重复的代码
3. 总在构造器之前执行
执行顺序:全部类的静态初始化块(由父类到子类)、父类普通初始化块、父类构造器、子类普通初始化块,子类构造器
This关键字
关键字 this 代表当前对象的引用
不可以在静态方法或者代码块中使用this
因为静态xx不能依赖对象,this又恰恰指代当前对象
this可以用来在一个构造器中调用另一个构造器
this(1)必须放在constructor的第一行
super关键字
关键字super指向当前对象父类的引用
this和super不能同时出现在一个构造函数中
和this一样,super只能放在第一行
和this一样,不能在静态代码块使用super
修饰符
public
是唯一可以从不同包访问的
protected
默认修饰符(啥都没有)
private
final
final修饰的变量不可被修改
基本数据类型不能修改
引用数据类型不可指向新的对象
final修饰的方法不可被重写
所有private方法被隐式指定为final
final修饰的类不可被继承
final类中所有方法被隐式指定为final
native
表示实现方法的编程语言不是java
String大类
https://www.cnblogs.com/weibanggang/p/9455926.html
String
源码中使用了final修饰的char数组存储字符串
Java中对String对象进行的操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢
那么String为什么要设计成不可变的呢?我觉得是因为出于安全性的考量,试想一下,在一个程序中,有多个地方同时引用了一个相同的String对象,但是你可能只是想在一个地方修改String的内容,要是String是可变的,导致了所有的String的内容都改变了,万一这是在一个重要场景下,比如传输密码什么的,不就出大问题了吗。所以String就被设计成了不可变的。
StringBuffer
使用了同步锁
线程安全
效率低
StringBuilder
未使用同步锁
非线程安全
单线程时效率高
都继承自 AbstractStringBuilder
使用char[ ]存储字符串,但没有final修饰
所以是可变类型,不会因为修改而创建新的对象
继承关系
子类与父类间应该是is-a关系
Object类常用方法
toString
equals
与==区别
==只判断reference是否相等
equals方法默认采用==判断reference是否相等,所以如果不重写的话,equals仍会根据地址判定对象是否相等
重写equals就是按照我们自己的喜好定制“相等”的含义
String类已经重写过equals了
hashCode
native修饰
hashCode方法用于将地址转换为整数返回(所以不是地址,但和地址有关),称为散列码
为什么很多重写hashcode的方程都用31*result
因为任何数n*31都可以被JVM优化为 ( n<<5 ) - n
移位和减法的操作效率比乘法高
hashcode的作用:在使用HashSet/HashMap/HashTable等哈希数据结构时,缩小查找成本
以hashset如何判断重复为例
当一个对象加入hashset时,会首先调用hashcode定位到插入位置
如果插入位置有值了,则可能出现哈希冲突,这时会调用equals二次确认
如果equals也认为两个对象相等,说明真的相等,禁止插入
如果equals认为两个对象不相等,说明hashcode出现误判,此时重新计算散列码并插入到别的位置
如果重写了equals,必须重写hashCode
解释1:借助哈希查找对象
因为哈希算法查找对象是先根据hashcode判断在array中对象的下标(相当于书架),再根据equals方法判断array这个单元格存储的list或者tree中(相当于书架的每一层)是否有相等的对象,如果不重写hashcode方法,在array里就无法定位,也就永远都查找不到目标
解释2:权威解释!
所有涉及到哈希运算的共同流程是,先调用hashcode判断对应位置是否有值,如果有值再调用equals二次比较,而hashcode的默认实现是为堆上每个对象生成绝不重复的散列码,如果不重写hashcode方法(比如只根据学号和姓名生成),即使两个完全相同的对象(两个学号和姓名完全一致的人)也会被默认hashcode判断为不相等,造成没有机会比较equals,两个对象永远无法相等
finalize
在GC回收对象本身之前调用
可用于释放被回收对象持有的资源
也可用于挽救对象不被回收:在finalize里将对象赋值给一个外部引用
默认实现是什么都不做
clone
克隆出一个相同的对象,并创建一块新内存空间放置它
只有实现了Cloneable接口才能调用clone
但clone属于浅拷贝!
虽然确实创建了一个新对象,但对象内部的引用类型变量还是指向原来的内存空间
可实现不完全的深拷贝:对象内所有引用类型全部实现Cloneable接口
彻底实现深拷贝的方法:调工具类将对象json化,如Fastjson
深拷贝VS浅拷贝
浅拷贝:创建一个新的引用指向原来的地址空间
深拷贝:真正创建一个一模一样的新对象

getClass
用native修饰
用来获取类的元对象
元对象是包含类信息的对象,比如类名,构造器等
一个类只有一个元对象
同一个类创建的多个对象元对象相同
抽象类和接口的区别
抽象类
无法被实例化
可包含变量
单继承
包含构造方法
abstract关键字不能和那些关键字共存?
final是最终不变的,意味着不能重写 与 abstract矛盾
private 私有的方法无法继承,也不存在重写
static修饰的方法是静态方法,其可以直接被类所调用。而abstract修饰的方法为抽象方法,即无方法体的方法,不能够被直接调用
接口
无法被实例化
不包含变量
只包含public的常量和抽象方法
Java 8开始,允许default方法
Java 9开始,允许private方法
多继承:一个子接口可以继承多个父接口
无构造方法
Comparable和Comparator
对Collection内对象排序时使用
Comparable<>接口
如何使用:让待排序的class实现这个接口
需重写int comparaTo(T other)
升序
this大:返回1
相等:返回0
this小:返回-1
降序相反
加到TreeSet里会自动排序
或者调用Collection.sort(xx集合)手动排序
Comparator<>接口
如何使用:new一个比较器,内部重写compare方法,在需要的地方代入
需重写int compare(T o1, T o2)
升序
o1>o2: 返回1
o1=o2: 返回0
o1<o2: 返回-1
降序相反
调用Collection.sort(xx集合,比较器)手动排序
包装类
只能通过构造方法赋值
包装类只有带参构造方法
通过构造器传的值就是基本数据类型:int 1=Integer(1)
装箱:基本类型=>包装类
拆箱:包装类=>基本类型
Java1.5之后可以进行自动拆装箱
UML
依赖----->:在一方的method中调用另一方
关联——>:在一方的field中调用另一方
聚合 ◇:部分可脱离整体存在(汽车和司机,一个汽车可以有多个司机)
组合 ◆:部分不能脱离整体存在(司机和身份证,一对一绑定)
关系由弱到强
什么时候用继承,什么时候用接口?强是/弱是
强是关系:描述直接继承关系,用继承实现
弱是关系:描述一个类具有某些属性,用接口实现
什么时候用继承,什么时候用聚合
is-a关系:用继承
has-a关系:用聚合
类的设计原则
高内聚低耦合
内聚:一个class或模块内部元素的紧密程度
耦合:多个class或模块之间的紧密程度
六大设计原则
单一责任原则
一个类只专注一件事
开放封闭原则
对扩展开放,对修改关闭
里氏替换原则
在软件中将一个父类对象替换成其子类对象,程序将不会产生任何错误和异常
依赖倒置原则
面向接口编程。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。
迪米特原则
设计系统时,尽量减少对象之间的交互
接口隔离原则
不仅类要设计的尽可能小,接口也是如此。尽量为每一个类建立单独的接口(或者为同一个类创建多个接口),接口内的方法尽可能少,避免臃肿的接口。
序列化/反序列化
https://blog.csdn.net/xlgen157387/article/details/79840134
序列化:java对象=>字节序列
序列化的用途
可以将对象永久存储至本地磁盘(类似.class文件)
网络通信时,可将对象序列化传输
进程之间通信
序列化时,只对数据域序列化
不对方法序列化
不对static修饰的数据域序列化
因为static不依赖对象
不对transient修饰的数据域序列化
transient代表对象的临时数据
用ObjectOutputStream实现
反序列化:字节序列=>java对象
用ObjectInputStream实现
Serializable
一个标识接口,里面不含任何方法
需要序列化的对象所属类必须实现serializable接口,或其子接口
可选择重写(来自ObjectInputStream或ObjectOutputStream的)readObject和writeObject方法:自定义序列化实现细节
无需无参构造器
Externalizable
继承自serializable,实现该接口也可以完成序列化
必须重写readExternal和writeExternal方法:自定义序列化实现细节
需要无参构造器
serialVersionUID
用途:保持版本升级前后对象的一致性
假设序列化之前你的对象是一个样子,过几天你对象做改动了,比如属性命名变了,这时候再反序列化会出问题。 这是因为编译器会自动指定版本号,解决方法是我们强制指定一个final的版本号,这样编译器就不会自动指定了。
常见序列化协议
XML
可读性比较好,效率低
JSON
可读性和效率介于两者之间
protobuf
效率高,可读性差
反射机制
是啥:在程序运行时动态获取类的信息(类名是啥,有哪些方法,哪些构造器等)
如何实现:Class类
作用:在程序运行时保存类的信息,一个Class类的对象保存一个类的信息
比如我现在有一Person和一个Animal类,他们的对象在程序跑起来的时候,就各自对应一个Class对象: person.getClass拿到的是一个Class类的对象,这个对象内再存储着Person类的信息。
动态拿到Class对象:
任意类.class
任意对象.getClass
Class.forName(类名)
最常用
常用方法
getName
拿到String类型的类名
getFields、getMethods 和 getConstructors
拿到所有以public修饰的数据域、方法或构造器
getDeclaredFields、getDeclaredMethods 和 getDeclaredConstructors
拿到所有数据域、方法或构造器
通过反射机制创建对象
调用元对象的newInstance方法
要求对应类有默认构造方法
调用构造器对象的newInstance方法
可以指定用哪个构造器来创建对象
注解
注释是给人看的,注解是给程序看的
注解的本质就是接口
分类
JDK内置
自定义注解
框架中的注解
元注解:注解其他注解的注解
@Target
规定了注解修饰的范围(方法?类?本地变量?等等)
@Retention
规定了注解的生命周期
@Documented
用于制作Javadoc
@Inherited
标记当前注解自动继承:如果一个class被当前注解标记了,它的子类也被这个注解标记
泛型
将类型参数化
T E K V
代表类型的占位符
?
通配符,表示任何类型
类型擦除
编译后,生成的.class文件中是不携带泛型信息的。编译器在编译时去掉类型参数的过程,就叫类型擦除。
异常处理
错误
语法错误
也叫编译错误,由javac发现
运行错误
由JVM发现,会引发各种异常(比如除0异常)
如果不对异常进行捕获,程序会非正常终止
逻辑错误
程序逻辑上的问题
异常
步骤
声明异常
在方法签名后 throws 可能的异常
抛出异常
在需要抛出异常的地方throw异常
捕获异常
try-catch-finally
可以有多个catch,catch块中级别越高的Exception越要放在最后
如果try-catch里有return,finally会在return之前执行,并且finally中的值会覆盖之前的

f(2)将返回0
3种情况finally不会被执行
finally之前执行了System.exit(int),作用是终止JVM执行,所以finally也不会执行
程序所在线程死亡
关闭CPU
分类
非检查异常:不会被javac检查是否预处理的异常,通常是由开发者引起的异常,比如除0和null pointer异常,java认为这些异常是你可以避免的,所以编译器不会管你是否try-catch了这些异常。可选择catch,也可以不catch
RunTimeException: 运行时异常
NullPointerException:空指针异常。
ArrayIndexOutOfBoundsException:数组下标越界异常。
ArithmeticException:算术运算异常。
NumberFormatException:数字格式异常。
检查异常:由javac检查是否预处理的异常,通常是由外因引起的异常,比如IO异常,文件不存在异常,java认为这些异常是有概率发生,且无法由编程者避免的,所以会由编译器检查你是否预先处理了这些异常。最终必须catch处理(或者throw)
自定义异常一般也是检查异常
IOException:输入输出异常
SQLException:数据库异常
FileNotFoundException:文件未找到异常
继承关系

Throwable
https://blog.csdn.net/weixin_39608988/article/details/110722665
Exception
RuntimeException
IOException
SQLException
FileNotFoundException
除了RuntimeException都是检查异常
Error
代表了JVM本身的错误,开发者无法用程序处理(所以throw或者catch了也没有用),比如OOM,ThreadDeath等。这些错误发生时JVM一般直接终止线程,无法捕获处理。
常用方法
getMessage 打印异常简要描述
printStackTrace 打印异常栈
JVM
JVM结构
类加载器
加载
boot strap,引导类加载器
负责加载JVM核心类(jre/lib/rt.jar中的类)
就是JVM的原代码,属于商业机密,开发者无法操作
extension,扩展类加载器
负责加载JVM扩展类(jre/lib/ext目录中的类)
开发者可以操作
application,系统类加载器
负责加载开发者自己写的类(CLASSPATH指定的类)
开发者可以使用
user,用户自定义类加载器
负责加载任意来源的类
继承自ClassLoader类
JDK1.2后 双亲委派模型
https://www.bilibili.com/video/BV1X5411K7cw?share_source=copy_web
运行过程
遇到一个类,自己先不加载,先一直向上传递给父级加载器(注意不是父类!)
这样所有类都会先传递给最上层的Bootstrap类加载器
父级加载器先尝试自己能不能加载,无法加载时,再向下委派给子级加载器尝试加载
无法加载是指:根据类的限定名,在自己负责的区域内,没有找到该类
好处
避免类的重复加载
如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,多个类加载器都去加载这个类到内存中,系统中将会出现多个不同的Object类,那么类之间的比较结果及类的唯一性将无法保证
安全
链接
验证
验证类信息是否正确可以被JVM运行,比如class文件魔数、版本号、语义检查等
准备
验证通过后,正式为静态变量分配内存
在方法区分配
注意仅仅是static静态变量,实例变量依赖于对象,所以会被存储在java堆
设置初始值(比如Boolean类型默认为false)
解析
将.class文件中,符号引用替换为直接引用的过程
初始化
执行类中的构造函数、初始化代码块、以及显式赋值
运行时数据区
程序计数器( PC 寄存器 )
作用:发条。指示当前线程所执行到的字节码行号
特征:
每个线程都会独立存储一个程序计数器
各线程PC不互相影响
是唯一不会OOM的内存区域
Java虚拟机栈
作用:记事本。记录每次方法调用
特征:
每个线程都有一个Java虚拟机栈
生命周期与线程同步
StackOverflowError 异常
当虚拟机栈不可动态扩展时
线程需要的栈深度>虚拟机允许的栈深度
OutOfMemoryError 异常
当虚拟机栈可以动态扩展时
无法申请到足够的内存
栈帧
作用:记事本的每一页。每个方法被执行的时候都会创建一个栈帧
特征:
“一个方法被调用至执行完毕的过程对应一个栈帧在虚拟机栈中入栈和出栈的过程”
存啥?
局部变量表
作用:小纸条。存储局部变量
操作数栈
作用:草稿纸。执行运算的地方
动态链接
作用:作弊耳机。需要用到某个引用时,实时从常量池里拿
方法出口
作用:记下回家的路。记录执行完本方法后,返回至主方法哪一行。
本地方法栈
作用:功能与Java虚拟机栈类似
与操作系统和外部环境交互
异同:
服务于native修饰的方法,即非java代码
如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈
也会抛出StackOverFlowError和OutOfMemioryError异常
Java堆
作用:仓库。专门存放实例化对象。
特征:
所有线程共享
GC主要负责清理的就是这儿,所以有时候也叫GC堆
为了更好的清理垃圾,堆又细分为:
年轻代
可以直接设置大小
Eden区
对象优先在此区域分配,Eden区空间不足时发起Minor GC,回收Eden区+from区,存活的对象进入survivor的to区,下次from区和to区对调
Survivor区: from区
Survivor区: to区
两个区大小一样,同一时间只有一个区可用
老年代
无法直接设置大小,需要用堆空间大小-年轻代大小来设置
大对象直接进入老年代
大对象需要连续的内存空间存储,比如很长的String或者数组。大对象会导致GC提前被触发。
长期存活的对象进入老年代
对象在Eden区出生,每经过一次Minor GC年龄+1,到一定阈值后进入老年代
阈值默认为15,但JVM不严格遵循这个阈值。
动态对象年龄判定:Survivor区中,若某一固定年龄的全部对象所占空间 > 总空间一半大小,该年龄对象全部晋升

方法区
作用:仓库。专门存放加载的.class文件相关信息
特征:
所有线程共享
可以固定大小,也可扩展
你写的类越多,方法区撑得越大,过多的类会导致方法区OOM
OutOfMemoryError 异常
当JVM加载的类信息>方法区允许的大小
JDK1.7之前:永久代
使用JVM内存
字符串存储在永久代容易OOM
给GC带来难度
永久代会发生垃圾回收
永久代空间不足时会直接full GC
一旦永久代大小设置小了,会频繁触发full GC
有时候,类的大小难以预先估算,不好指定永久代大小
永久代到底算是Java堆还是方法区的?
永久代在物理层面,是在堆空间的;而在逻辑层面,是在方法区的;因为方法区其实物理上也是在堆中的,但是由于功能和作用的区别,逻辑上方法区是独立于堆的。 当然jdk8以后就没有方法区了,只有元空间了,所以jdk8以后,永久代逻辑上是在元空间的
JDK1.8之后:元空间
使用本地内存
元空间大小仅受本地内存限制
元空间会发生垃圾回收
metaspace在空间不足时,会进行扩容,并逐渐达到设置的MetaspaceSize。Metaspace扩容到 -XX:MetaspaceSize 参数指定的量,就会发生FGC。如果配置了 -XX:MetaspaceSize,那么触发 FGC 的阈值就是配置的值
也可以指定
存啥?
类型信息
字段信息
方法信息
运行时常量池
作用:专门为JVM运行服务的常量池,由class文件中的常量池创建
特征:
OutOfMemoryError 异常
当运行时常量池无法申请到足够内存时
存啥?
字面量(静态变量)
String site="aaa"
不会改变
符号引用
String site="aaa"
可以改变
位置
1.6及以前
位于方法区->永久代
1.7
位于方法区->永久代
1.8及以后
位于方法区->元空间
字符串常量池 Stringtable
原理
本质上是一个hashtable,往里塞字符串的时候一旦出现hash冲突就判定为重复字符串
String.intern( )方法
https://zhuanlan.zhihu.com/p/260939453
位置
1.6及以前
位于方法区->永久代->运行时常量池
回收需要full GC
1.7
位于堆(独立于运行时常量池)
major/minor GC就可以回收
1.8及以后
位于堆(独立于运行时常量池)
major/minor GC就可以回收
直接内存
特征:
不属于运行时数据区,也不属于JVM定义的内存!!!
大小不受Java堆的限制,但仍然受到本机内存的限制
访问直接内存的速度一般高于访问Java堆,所以频繁读写的场合可能会用到
不受GC管理,回收困难
OutOfMemoryError 异常
当直接内存超出本机物理内存上限时
执行引擎
解释器(涮火锅)
翻译一行执行一行,效率稍低
JIT编译器(做好再吃)
一次性翻译完再执行
都是将.class文件“翻译”成本地机器码,二者有何区别?
热点代码:指被频繁调用的代码
为何需要解释器与JIT编译器并存?
当程序需要迅速启动时,解释器可以先着手翻译代码,省去编译时间
随着程序运行,编译器逐渐发挥作用。编译器翻译一次热点代码后,这部分代码就无须被反复“翻译”了。当越来越多的热点代码被翻译成可以直接执行的机器码,程序执行效率就会越来越高。
垃圾回收器
https://www.cnblogs.com/wccchen/p/7252150.html
回收哪儿?
程序计数器、Java栈、本地方法栈的生命周期和线程一致,因此无需GC关注
Java堆是重点关注区!
如何调用?
RunTime类
gc是实例方法
System类
gc是静态方法
gc由JVM自动调用,无需人为调用
步骤
判断对象是否可回收
引用计数算法
原理:给每个对象添加引用计数器,计数值为0代表对象可回收(已经没人引用它)
缺点:对于循环引用的对象,计数值永不为0,该算法无法回收这类对象
根搜索算法(Java采用)
原理:从若干GC roots对象开始搜索,无法到达的对象即为可回收对象
什么对象可以作为GC Roots
Java栈中局部变量表引用的对象
本地方法栈中引用的对象
方法区中静态属性引用的对象
field中被标记为static的对象
方法区中常量引用的对象
field中被标记为final的对象
不可到达的对象不一定必死
https://blog.csdn.net/luzhensmart/article/details/81431212
第一次标记:筛选出没必要执行finalize方法的对象,宣告死亡
当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收
第二次标记:对于有必要执行finalize的对象,放入一个叫F-Queue的队列中,并稍后由一条JVM自动创建的低优先级线程执行其中每个对象的finalize方法。
如果finalize使对象与GC Root产生联系,逃脱死亡
如果finalize也没能挽救对象,进行第二次标记
只有经过2次标记的对象才必死
同时基于引用分类判断对象是否可被回收
JDK1.2以后引用分为四种
强引用
我们常用的A a = new A()
GC绝对不会回收,即使报OOM,除非不可达
软引用
由SoftReference创建
GC只在Java堆内存不够的时候才回收
弱引用
由WeakReference创建
GC在下次触发时必定回收
虚引用
由PhantomReference创建
是否GC和虚引用没关系,虚引用仅用来在对象被回收时接收通知
垃圾回收算法
标记清除算法
标记-清除算法(Mark-Sweep)是最基础的收集算法。首先标记出所需要回收的对象,在标记完成后统一回收所有被标记的对象。
标记和清除效率都不高
会产生大量不连续的内存空间碎片,以后想存大对象就难以找到足够的连续内存
复制算法
为了解决效率问题,复制算法(Copying)出现了,他将内存分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就把存活的对象复制到另一块上,然后把这一块空间全部清理掉。这样每次对整个半区进行内存回收,内存分配时也不用考虑内存碎片的复杂情况,只需要移动指针,按顺序分配内存即可。
内存缩小到原来的一半
大对象或对象存活率较高时复制次数增多,导致效率降低
研究显示90%以上的对象会在新生代死亡,所以没必要将内存按1:1分配
因此JVM按照8:1:1分出了三个区
https://www.bilibili.com/video/BV12V411r7Zy?share_source=copy_web
标记整理算法
标记过程和“标记-清除”算法一样,但是后续步骤不是直接对可回收部分进行清理,而是把存活对象都向一端移动,然后直接清理掉端以外的内存。
分代收集算法
分为老年代和新生代
Minor GC
新生代对象存活率较低,用复制算法
会频繁执行,回收速度也比较快
Major GC
老年代对象存活率较高,用标记清除或者标记整理算法
很少执行,回收速度也比较慢
空间分配担保:每次发生Minor GC前,JVM会先检查老年代最大可用连续空间是否 > 新生代所有对象所占空间总和(防止新生代被一个对象撑满的极端情况)
大于: 执行Minor GC
小于: 执行Major GC
在hotspot中的实现:

选型

连线表示可以在新生代-老年代同时使用的回收器组合
不同的回收器采用不同的算法,可以用指令设置JVM期望选择的回收器
主流JVM
HotSpot
oracle
J9
IBM
JRockit
JDK6以后就终止了
多线程
程序、线程和进程
程序
程序是包含代码和数据的文件,相当于静态的代码
进程
进程是程序运行的基本单位,代表一个程序的执行过程。系统运行一个程序对应进程从创建到执行到死亡的过程(简单来说进程就是跑起来的程序)
对Java来说,运行main函数就是启动了JVM进程,而main函数所在线程被称为主线程
你可以理解为一个.exe即是一个进程
各进程是独立的
线程
线程是比进程更小的单位,一个进程下可以有多个线程
与进程不同的是,同一进程下的多个线程共享一块内存和系统资源
在Java中,多个线程共享Java堆和方法区,但程序计数器,虚拟机栈和本地方法栈都是线程私有的
各线程不一定是独立的,同一进程下的线程可能互相影响
线程之间切换和调度的成本远小于进程
上下文切换
CPU的一个核心上同时只能跑一个线程,而一般线程个数都>CPU核心数。为了让所有线程都能均匀分配到核心运行,CPU会给每个线程分配时间片。当前线程时间片用完以后会进入到就绪状态,然后把核心让出去,切到下个线程的时间片上,这个过程就是上下文切换。
每次上下文切换都是纳秒级的(对CPU来说已经是很耗时的操作了),会消耗大量系统资源
LINUX系统相比其他系统的优势之一就是上下文切换时间非常短
并行和并发
并发
一个进程内,同时有多个线程在交替运行
同一个时间区间有多个任务在执行,但不保证单位时间内有多个任务执行
并行
在多个处理器上,同时有多个进程在运行
单位时间内有多个任务在执行
线程的6个状态
初始状态 (NEW)
线程已经创建好,尚未启动。通过start( )进入就绪状态
运行状态 (RUNNABLE)
包括就绪(READY)和运行中(RUNNING)两种状态
进入就绪状态并不一定直接运行,得等调度器挑中你
阻塞状态 (BLOCKED)
线程被锁阻塞
等待状态 (WAITING)
线程等待一个唤醒它的操作才会继续执行,比如notify( )
超时等待状态 (TIMED_WAITING)
线程等待一段时间,呆够了它自己醒
终止状态 (TERMINATED)
线程死亡
创建线程的3种方式
继承Thread类
资源:数据域存的东西,比如票数
一条线程一个资源,线程只能用自己携带的资源
实现Runnable接口
多个线程可以共享一个资源(Runnable对象)
实现Callable接口
多个线程可以共享一个资源(Callable对象)
允许获取最终的执行结果(通过FutureTask对象.get方法拿)
常用方法
start
调用start后线程进入就绪状态,等待调度,分配到时间片后会自动调用run方法执行
run
run方法是自动调用的,如果直接执行run方法相当于把run方法当成main线程下的普通方法执行
sleep
线程抱着资源休息指定时间
不释放锁
wait(Object方法)
线程被一棒子打晕丢掉了资源
释放锁
join
呼叫某个线程主动插自己的队
谁阻塞?
执行A.join( )的线程
谁先跑?
A.join( )中的A
被插队的线程啥时候跑?
A销毁以后
释放锁
yield(静态方法)
线程怂了主动让路
谁阻塞?
执行Thread.yield( )的线程
谁先跑?
不知道,由CPU决定,甚至有可能是他自己
不释放锁
interrupt
仅仅改变线程对象内自带的标记位,不会实际中断线程
调用 isInterrupted() 方法判线程是否被中断
正确停止线程的方法
标志位判断+抛异常或return

stop
立即终止线程,立即释放锁
导致清理工作无法完成
导致对象锁不一致
已被弃用
destroy
仅仅抛出异常,不会实际终止线程
已被弃用
suspend
https://www.cnblogs.com/zhengbin/p/6505971.html
停止线程,但不释放资源
直到resume()执行才会继续当前线程
极容易导致死锁
已被弃用
线程同步
锁
对于所有锁的概述: https://zhuanlan.zhihu.com/p/271917055
每个对象都自带一把锁
通过锁,确保一个资源同时只有一个线程能访问
如何实现?
synchronized
同步代码块
可以指定想锁定的资源(也叫同步监视器)
一般都是this,但也可以是其他对象
底层代码
使用monitorenter指令指向同步代码块开始的位置
执行monitorenter时尝试获取对象锁monitor
如果锁的计数器为0表示锁可以被获取
获取锁以后计数器设为1
使用monitorexit指令指向同步代码块结束的位置
执行monitorexit后将锁的计数器设为0
同步方法
无法指定想锁定的资源,只能为this
底层代码
没有monitorenter和monitorexit指令
而是使用了ACC_SYNCHRONIZED标识
JVM通过该标识来辨别一个方法是否为同步方法,从而执行相应的同步调用
锁定时,其实是拿到了“同步监视器”也就是待锁定资源对象的“内置锁” 代码块或方法体内写的,是锁定以后,我们想对资源做的操作
static synchronized
synchronized是实例锁,锁住的是实例
static synchronized是全局锁,锁住的是class,无论实例化多少对象,所有线程共享该锁
Lock接口(推荐)
ReentrantLock
显式加锁,必须手动解锁
使用lock锁,JVM将花费较少的时间调度线程,性能更高
ReadWriteLock
锁的种类
公平锁/非公平锁
公平锁
多个线程按照申请锁的顺序来获取锁
非公平锁
线程获取锁的顺序不按照申请锁的顺序
优势:吞吐量大于公平锁
ReentrantLock默认是非公平锁
synchronized只能是非公平锁
可重入锁
同一个线程下,外层方法获取锁以后,内部方法自动获取同一个锁
ReentrantLock是可重入锁
synchronized是可重入锁
独享锁/共享锁
独享锁
该锁一次只能被一个线程持有
synchronized是独享锁
ReentrantLock是独享锁
ReadWriteLock中的写锁是独享锁
共享锁
该锁可同时被多个线程所持有
ReadWriteLock中的读锁是共享锁
互斥锁/读写锁(其实就是独享锁/共享锁的具体实现)
互斥锁在Java中的具体实现就是ReentrantLock
读写锁在Java中的具体实现就是ReadWriteLock
乐观锁/悲观锁(不是具体的锁类型,是一种编程思想)
乐观锁认为,不加锁的并发操作是ok的
乐观锁在Java中就是无锁编程
无锁编程不是不考虑并发,而是在底层用了CAS保证操作的原子性,以达到类似于加锁的效果
悲观锁认为,不加锁的并发操作一定会出问题
悲观锁在Java中就是利用各种锁
分段锁
ConcurrentHashMap中的Segment继承了ReentrantLock
偏向锁/轻量级锁/重量级锁 (专指synchronized的锁状态)
偏向锁
如果一段同步代码反复被一个线程访问,那么该线程会倾向于始终占有该锁(而不是在同步代码区结束后立即释放锁)
这样如果下一次还是该线程访问这把锁,就不需要转移锁的持有权
轻量级锁
当锁是偏向锁时,被其他线程访问,产生锁竞争,竞争不到锁的线程将自旋(循环反复尝试获取锁),这时偏向锁膨胀为轻量级锁(告诉其他向访问这个资源的线程,这个资源有点抢手,你们做好自旋等待的准备)
重量级锁
当锁是轻量级锁时,如果某个线程达到了最大自旋次数(默认是10),说明这个资源太卷了大家都想抢它,此时轻量级锁膨胀为重量级锁,后续再想竞争这把锁的其他线程直接阻塞,而不是自旋空转CPU浪费资源(告诉后来的线程,这资源太卷了,站着排太累,坐下等)
自旋锁
指尝试获取锁的线程不会立即阻塞,而是采用循环的方式反复尝试获取
好处是减少上下文切换
但循环会消耗CPU
死锁
形成死锁的四个必要条件
互斥条件
一个资源只能被一个线程同时使用
请求与保持条件
一个线程因请求资源阻塞时,对已获得的资源保持不放
不剥夺条件
线程已获得的资源,不能被强行剥夺
循环等待条件
线程之间形成你等我我等你的首尾相接局面
如何避免死锁:想办法破坏这四个条件
互斥条件
没法破坏,因为我们使用锁就是想让资源被线程独占
请求与保持条件
一次性申请所有资源
不剥夺条件
占有一部分资源的线程申请其他资源申请不到时,为防止死锁,主动释放其他资源
循环等待条件
对资源排序,所有线程都按顺序申请资源,反序释放资源,没申请到上一个资源禁止提前申请下个资源
原子类(JDK 1.5引入)
是什么?
对标synchronized和Lock接口,一种开销更小的保证资源同步的方式
为什么要用?
有时候只需要对一个简单的变量进行加减操作,每次都synchronized一下开销太大了
原子类底层是CAS实现(乐观锁),其实没有使用任何锁,但达到了锁的效果,所以开销小很多
CAS
compare and swap,比较并替换
将赋值过程分为2步
设V=内存中的实时值,A=对V的预期值,B=基于A计算出的未来值
比较:希望更改V时,先将V的值同步至A,根据A计算B,(这中间可能有其他线程插进来捣乱)赋值前再比较一遍V是否等于A
设置:如果是,将A更新为B,如果不是,则更新A为最新值,并重新计算B,重复比较过程
ABA问题
如果另一个线程将V的值由1更改为2再更改为1,当前线程的CAS操作无法分辨V的值是否变化过
会破坏掉CAS的原子性
解决方法
添加版本号
都有什么子类?(JUC包中的原子类是哪四类)
基本类型
AtomicInteger
AtomicLong
AtomicBoolean
使用原子方式更新基本类型
数组类型
AtomicIntegerArray
AtomicLongArray
使用原子方式更新数组里的某个元素
引用类型
AtomicReference
原子更新引用类型
AtomicStampedReference
原子更新带有版本号的引用类型
对象属性修改类型
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
JDK 1.8 以后
LongAdder
使用AtomicLong时,如果大量线程同时竞争一个原子变量,其他线程会自旋,浪费海量CPU资源
而LongAdder将原子值分割成一个数组(比如20分成5,5,5,5),线程访问时通过hash算法映射到不同元素进行运算,最后的计数结果是数组里所有数字的和
常用方法
compareAndSet
核心方法
addAndGet
以原子方式将当前值加上delta,返回更新后的值
getAndAdd
以原子方式将当前值加上delta,返回更新前的值
getAndSet
以原子方式将当前值设置为给定value
get
获取当前值
线程通信
围绕当前被锁定资源操作:
Object方法
wait( )
作用:
1. 释放当前资源的锁
2. 告诉当前线程,等待
注意:
只能在synchronized中调用
需要在try-catch中使用
notify( )
随机唤醒一条围绕当前资源工作的线程
notifyAll( )
唤醒所有围绕该资源操作的线程
Condition方法
https://blog.csdn.net/u011635492/article/details/83043212
await( )
作用:
1. 作用同原生wait( )
2. await( )的时候可以分组
使用conditionA.await( )阻塞的线程,只能使用conditionA.signal( )唤醒
使用conditionB.await( )阻塞的线程,只能使用conditionB.signal( )唤醒
注意:
只能在Lock锁住的部分调用
需要在try-catch中使用
signal( )
signalAll( )
生产者消费者模式
管程法
需要一个容器作为缓冲区
以变量判断缓冲区水位
信号灯法
无需缓冲区,仅能实时通信一份资源
以标记位判断
线程池
经常创建和销毁线程,对性能影响很大。可以提前创建好多个线程放入线程池,使用时直接获取,用完放回去。
如何使用线程池?
执行execute和submit方法的区别:
execute
无返回值,用于提交无需返回值的任务
但无法感知任务执行成功与否
一般代入Runnable对象
submit
有返回值,用于提交需要返回值的任务
可通过.get方法获取返回值
一般代入Callable对象
如何创建线程池?
Executors工具类
可创建4种线程池
https://www.zhihu.com/question/321819178/answer/1356171106
newCachedThreadPool
可缓存线程池。无限大。线程池长度过大时回收,需要时创建新线程。
newFixedThreadPool
定长线程池,规定了最大线程数
newScheduledThreadPool
以设定周期循环执行任务
以设定时间延迟执行任务
newSingleThreadExecutor
线程池里只有一条线程
《阿里开发手册》明确禁止通过此方法创建线程池
因为可能出现资源耗尽OOM
new ThreadPoolExecutor()
所需参数
corePoolSize
核心线程数(低水位线,超过水位线进入警戒区,也就是阻塞队列)
maximumPoolSize
最大线程数(极限值,超过就报错)
keepAliveTime
线程数目大于corePoolSize时启用判断:如果一个非核心线程的空闲时间大于keepAliveTime,线程终止
unit
keepAliveTime的单位
workQueue
https://blog.csdn.net/ye17186/article/details/89467919
阻塞队列,储存等待执行的任务
JDK提供了4种
ArrayBlockingQueue
基于数组的有界队列,防止资源耗尽
LinkedBlockingQuene
基于链表的无界队列,容量无限,因此使用此队列时maximumPoolSize参数失效
SynchronousQuene
无缓存队列,相当于容量为0(或者说没有使用队列)
PriorityBlockingQueue
具有优先级的无界队列,优先级通过Comparator参数实现
handler
https://blog.csdn.net/ye17186/article/details/89467919
拒绝处理任务时的策略
JDK提供了4种
AbortPolicy(默认)
放弃任务,抛出RejectedExecutionException异常
DiscardPolicy
放弃任务,啥都不做
DiscardOldestPolicy
放弃最早入队的任务,把当前任务入队
CallerRunsPolicy
由递交任务的线程执行被拒绝任务的run方法(员工手头没空,老板亲自处理)
execute提交任务后,线程池会做如下操作:
1. 如果正在运行的线程数量
< corePoolSize ,创建核心线程,执行这个任务
>= corePoolSize ,将这个任务放入阻塞队列
2. 如果阻塞队列满了,且正在运行的线程数量
< maximumPoolSize, 创建非核心线程,执行这个任务
> maximumPoolSize, 说明线程池真的满了,准备执行拒绝策略
好处:
降低资源消耗
无需频繁创建和销毁
提升响应速度
直接拿
维护系统稳定性
频繁创建销毁线程会降低系统稳定性
关闭线程池
shutdown
将线程池的状态设置成 SHUTDOWN
正在执行的任务继续执行
没执行的任务被中断
shutdownNow
将线程池的状态设置成 STOP
正在执行的任务停止
没执行的任务返回
volatile关键字
https://zhuanlan.zhihu.com/p/138819184
JMM(java内存模型)
与JVM运行时数据区不同:JMM描述的是并发状态下各线程的内存模型
共享变量:线程对象内部field内的变量(不包括局部变量),当需要被除了自己的其他线程访问时,即成为共享变量
主内存:存储所有共享变量
本地内存:也叫工作内存,存储从主内存拷贝过来的共享变量副本
线程最开始会从主内存拷贝一份共享变量到本地内存里,需要读时直接从本地内存读副本,需要写时先更改本地内存里的副本,然后再回写到主内存里。
这样就可能出现:主内存共享变量已经被更改后,某个线程仍在使用自己旧的共享变量副本,造成数据不一致
比如:一个线程在更改完副本还没来得及回写之时,如果另一个线程直接从本地内存读取了副本,这个副本就不是最新的。
解决方法:volatile
作用:
1. 保证变量在多个线程之间的可见性
添加volatile后:如果某个共享变量如果被其中一个线程给修改了,其他线程能够立即知道这个共享变量已经被修改了,当其他线程要读取这个变量的时候,会去主内存中读取,而不是从自己的工作空间中读取
其实就是,添加volatile以后,告诉JVM,这个变量是不稳定的,需要去主内存去读
2. (防止指令重排序)保证代码执行的顺序不变
为了提高执行程序时的性能,编译器和处理器会对指令进行重排序优化,因此代码的执行顺序和编写代码的顺序可能不一致
为什么要防止指令重排序?
单线程环境下,重排序会考虑指令间的数据依赖性
多线程环境下线程交替执行,由于重排序机制,多个变量能否保持一致性是不确定的
和synchronized的区别
volatile是轻量级synchronized,性能优于synchronized
volatile只能修饰变量,synchronized可以修饰方法和代码块
volatile不会发生阻塞,synchronized可能发生阻塞
volatile解决的是共享变量在多个线程之间的可见性,synchronized解决的是资源在多个线程之间的同步问题
volatile保证变量可见性,但不保证原子性,synchronized都能保证
什么是原子性?
一整套操作(多行代码),要么不被打断全部完成,要么就都完不成
比如在synchronized代码块中,实现账户A向B转账100,必然包含A-100和B+100两个操作,这两个操作也必须放在一套里,要么都完成,要么都失败
什么是可见性?
当一个线程修改了某个变量的值,其它线程总是能知道这个变量变化
JMM模型中,当前线程本地内存是感知不到其他线程本地内存存储细节的,这种情况称为不可见;使用了volatile以后,不同线程本地内存可以感知到(被修饰的)共享变量的变化,这时称为可见
ThreadLocal
个人理解:并非JMM的源代码,但可以实现共享变量副本机制:资源中同一个ThreadLocal索引,在不同线程中指向不同对象
源码
ThreadLocalMap

每个线程维护一个ThreadLocalMap对象
变量值实际存储在ThreadLocalMap里,而不是ThreadLocal里
初始值为null,只有在真正存储内容时才会初始化ThreadLocalMap

ThreadLocalMap中每个kv对的key都是ThreadLocal
ThreadLocal(实际上是存储在多个线程ThreadLocalMap中的共享索引)
set方法

先获取当前访问ThreadLocal索引的线程
再获取当前线程携带的ThreadLocalMap对象
如果当前线程有ThreadLocalMap对象:将当前ThreadLocal索引作为key,在Map中存入value(表示该索引在当前线程中的副本)
如果当前线程没有ThreadLocalMap对象:说明是首次存东西,为当前线程初始化一个Map并存入当前对象
get方法

先获取当前访问ThreadLocal索引的线程
再获取当前线程携带的ThreadLocalMap对象
在Map非空时,返回以当前ThreadLocal索引为key存储的value
应用场景
同一个索引,在不同线程下指向不同的副本

假设现在有一个资源类,其中有一个ThreadLocal变量

现在有2条线程来对这个ThreadLocal变量执行set操作
A线程执行set(user1)

B线程执行set(user2)

线程私有变量
实现机制完全同上,只不过ThreadLocal的泛型指定成了Map类型



我们的业务场景就出现过应用:多个请求对应多个线程,每个请求要求在MVC各层可随时感知请求者token
风险
内存泄露

ThreadLocalMap的key为弱引用,会在下一次GC被清掉,但value是强引用,永远不会被GC清理,进而造成大量key为null的entry堆积,引发内存泄漏
解决方案1
ThreadLocalMap的set/get/remove方法在调用时会自动清除key为null的Entry
但还是推荐在使用完ThreadLocal后,手动调用remove方法清除
解决方案2
直接将ThreadLocal变量声明为private static
存入方法区,不受GC影响
线程复用
servlet使用线程池,执行请求的线程在用完不会被销毁而是放回池中,所以请求的业务逻辑完成后必须清空ThreadLocal存储的全部内容,否则线程复用后会感知上次请求的信息
跨线程传递变量
父子线程之间的传递
用InheritableThreadLocal
线程池下/平级传递
可以尝试使用阿里的TTL(Transmittable ThreadLocal)
容器
Map (无序,映射)
常用方法
put
一次添加一个entry
putAll
一次性将另一个Map中的所有entry添加过来,重复的直接覆盖
get
获取指定key的value
remove
根据key删除指定entry
clear
清空当前Map中的所有entry
containsKey
containsValue
entrySet
返回当前Map中的所有entry的Set
keySet
返回当前Map中所有key的Set
values
返回Map中所有value的Collection对象
isEmpty
size
注意
键 ( key ) 不得重复
实现类
HashMap

JDK 1.8前
通过数组和链表实现
JDK 1.8后
链表长度大于阈值(默认8)时
如果数组长度小于64,扩容
如果数组长度大于64,转换成红黑树实现
红黑树长度小于阈值(默认6)时,改为链表实现
使用key来计算hash值
LinkedHashMap (有序)
使用双向链表维持插入顺序
HashMap是无序的,如果希望遍历时按插入顺序拿到元素,就可以用LinkedHashMap
支持两种顺序
插入顺序
设置accessOrder为false
谁先写入,谁靠前
访问顺序
设置accessOrder为true
读取、修改等操作也会更新元素顺序(可用于实现LRU缓存)
ConcurrentHashMap
https://blog.csdn.net/ym123456677/article/details/78860719
保证线程安全
由于所有访问HashTable的的线程必须竞争同一把锁,HashTable效率极其低下
JDK 1.8前
ConcurrentHashMap对数据分段,再通过分段锁来锁住每一段数据,使得多个线程可以同时访问

Segment数组
Segment是锁,每个锁负责锁一段类似HashMap的数据(若干HashEntry)
HashEntry
类似HashMap
JDK 1.8后
ConcurrentHashMap对每个数组元素(链表 or 红黑树)的首元素加锁,这样只要不产生hash冲突就不会产生并发。同HashMap一样,链表长度>8时会转换成为红黑树
二义性问题
HashMap是可避免二义性问题的,所以允许存储null值(value)
hashmap允许value为null的键值对,所以当你调用get方法返回null时,无法区分是没有这个key,还是存在这个key但value为null
这种情况下先调用containsKey先判断一波即可消除二义性
ConcurrentHashMap无法避免二义性问题,所以不允许存储null值(value)
多线程环境下,A线程调用containsKey (key)期待返回false时(不存在某个key),B线程插了一刀,先执行了put (key,null)操作,这时候再由A线程执行containsKey (key)会返回true,也就是说即使containsKey (key)也无法消除二义性
所以干脆不允许存储null值
HashTable
继承自Dictionary
与HashMap相似,但由于大多数方法都用synchronized修饰,HashTable是线程安全的
所以,在不需要保证线程安全的场合,HashMap效率高于HashTable
与HashMap的区别
1. HashMap不是线程安全的,HashTable线程安全
2. HashMap允许一个null键和多个null值,HashTable不允许键或值为null
3. HashMap从JDK1.8开始引进红黑树实现,HashTable没有这样的机制
4. 如果不指定容量初始值:HashMap初始化大小为16,每次扩容为原来的2倍;HashTable初始化大小为11,每次扩容为原来的2n+1倍
5. 如果指定容量初始值为n:HashMap将这个值扩容至2的幂次方大小,HashTable直接使用n作为大小

TreeMap(有序)
通过红黑树维持映射的有序性
可自动排序(通过Comparable或Comparator)
Iterable接口(JDK1.5)
Collection接口
List (有序,线性表)
实现类
ArrayList(基于Onject数组实现)
对于随机访问:由于底层是数组,ArrayList可以快速定位到指定下标处的元素
ArrayList优于LinkedList
对于插入和删除:如果在中间位置插入元素,之后的元素必须移动
空间占用
需要连续内存空间
结尾会预留部分空间
LinkedList(基于双向链表实现)
对于随机访问:由于底层是双向链表,LinkedList需要遍历所有元素才能拿到元素
对于插入和删除:在中间插入元素时,需要先遍历到指定位置,再更改前后元素的前驱和后继信息即可。
LinkedList优于ArrayList
空间占用
无需连续内存空间
每个元素需要额外存储前驱和后继指针
都不是同步的,不保证线程安全

Vector(List古老实现类,弃用)
所有方法都是同步的,可保证线程安全
与ArrayList基本相同
但因为同步需要时间,在无需线程安全的环境下不推荐Vector
那用什么实现线程安全的List?
SynchronizedList
来源于Collections工具类
可以将List接口下的集合转换为线程安全的集合
但所有方法都采用了加锁操作,所以并发效率也不高
CopyOnWriteArrayList
来源于JUC包

CopyOnWrite意思是复制再写入
图中是write方法,加锁
但对于get方法是不加锁的
也就是说,读操作不加锁,写操作加锁,适用于读多写少的场合
常用方法
set
将给定下标的List元素修改成给定值
get
获取List中给定下标的元素
indexOf
获取第一个匹配元素的下标
lastIndexOf
获取最后一个匹配元素的下标
subList
返回指定下标范围的子List
listIterator
默认返回一个从List起始位置开始的迭代器,如果给定数字n可以返回一个从第n个位置开始的迭代器
返回一个迭代器,与传统Iterator不同的是,可以反向遍历
与数组Array的相互转换
List转Array
xxlist.toArray( )
Array转List
Arrays.asList( array )
单向队列:Queue
双头队列:Deque
常用实现类
LinkedList(基于链表实现)
插入删除占优
ArrayDeque(基于数组实现)
随机访问占优
不分头尾随便进出,coder可根据需求自主构建:
栈
普通队列
沿用双端队列
仅支持尾入头出,仅可构建传统队列
Set (无序,集合)
注意
Set中的元素不能重复
Set没有get方法
实现类
HashSet
基于HashMap实现
使用对象来计算hash值
LinkedHashSet(有序)
继承自HashSet, 底层为LinkedHashMap
TreeSet(有序)
基于TreeMap实现
可以使用Comparable或Comparator排序
常用方法
add
一次添加一个元素
addAll
并集运算:将另一个容器的所有元素添加到当前容器
remove
删除指定元素
removeAll
差集运算:移除本容器中和另一个容器重合的元素
clear
清空当前容器
isEmpty
判断当前容器是否为空
retainAll
交集运算:只保留两个容器中都有的元素
contains
判断当前容器是否包含指定元素
containsAll
检查是否为子集:如果一个集合包含了另一个集合的全部元素,返回true
size
求集合长度
toArray
将集合转换成Array
Iterator接口(JDK1.2)
作用:迭代器本身
常用方法
hasNext
指示容器中是否还有需要迭代的元素
next
返回容器中下一个元素
remove
删除下一个元素
一般很难在遍历过程中删除元素,但调用remove方法可以自动完成这一过程
作用:使容器具备迭代元素的功能
常用方法
iterator(Iterable的核心方法)
返回一个迭代器(Iterator对象)
forEach
使用lambda表达式遍历
spliterator
并行遍历
I/O 流
File
File类只创建File对象,不在计算机上创建文件本身
创建
支持以绝对/相对路径创建
带后缀的是文件
不带后缀的是文件夹
支持以文件夹作为上级路径创建
比特流 (无论输入输出是什么类型,吸管里流淌的永远是byte)
对图片,音像,视频等数据操作时,用比特流
InputStream抽象类(用吸管吸)
FileInputStream
读上来的byte只能被转换成int返回(吸到嘴里的只能是int)
创建:吃一个File对象
DataInputStream
允许以Java中其他基本数据类型读取(吸到嘴里可以转换成任何数据类型)
创建:吃一个FileInputStream对象
核心方法
定义一口吸多少:read
int read()
一次只读上来一个byte,吸管粗细=1 byte
返回值:将读取上来的byte值转换成int
int read(byte [ ] )
byte[ ]为缓冲区,定义的越长,吸管越粗,一次性读上来的内容就越多
返回值:返回真正吸上来的byte个数
OutputStream抽象类(往吸管里吐)
FileOutputStream
往管里吐之前,嘴里只能是byte
创建:吃一个File对象
DataOutputStream
往管里吐之前,嘴里可以是各种数据类型
创建:吃一个FileOutputStream对象
核心方法
定义一口吐多少:write
write(int b)
一次只写一个byte(int会在底层转换成byte)
write(byte [ ] )
byte[ ]为缓冲区,定义的越长,吸管越粗,一次性写的内容就越多
字符流(无论输入输出是什么类型,吸管里流淌的永远是char)
对字符操作的话用字符流比较好,因为字符流帮我们完成了字节->字符的转换过程,我们无需关注编码格式问题
Reader抽象类
FileReader
底层在从磁盘读的时候,都是读一次,操作一次磁盘,性能差的一批
核心方法
一个一个读:read
int read( )
一次性读一个char,返回ASCII值
int read( char[ ] )
char[ ] 为缓冲区,返回实际读上来的个数
创建: 吃一个File对象
也可以直接吃一个表示文件路径的String的字符串
BufferedReader
在底层使用缓冲区,缓冲读满以后才输出到程序里,真正实现“一吸一大口”
缓冲区是可以设置大小的
核心方法
整行读取:readline
读取一整行
创建: 吃一个FileReader对象
Scanner
效率比BufferedReader低,因为是直接往硬盘里写东西,没有缓冲区
核心方法
逐个读
hasNext
next
nextInt
逐行读
hasNextLine
nextLine
创建:吃一个File对象
Writer抽象类
FileWriter
底层在往磁盘里写的时候,都是写一次,操作一次磁盘,性能差的一批
核心方法
什么都能写:write
write ( int c )
一次只写一个char(int代表ASC II码,会被转为char)
write ( String str )
一次性写一个String
write ( char[ ] )
创建: 吃一个File对象
也可以直接吃一个表示文件路径的String的字符串
BufferedWriter
在底层使用缓冲区,缓冲写满以后才写到磁盘,由于无需反复操作磁盘,效率比FileWriter高很多
核心方法
write:沿用FileWriter的write方法(既然write可以直接写一个string,我们也没必要定义其他方法了)
newLine ( )用于换行, 等效于使用\n
创建:吃一个FileWriter对象
PrintWriter
传统的write方法只能接受字符串、字符数组、ASCII码,PrintWriter的print系列方法可以接受任何参数
可以设置自动刷新缓存
核心方法
printf
println
创建: 吃一个File对象
流转换
输入流如何转输出流
https://blog.csdn.net/weixin_33722375/article/details/115812439
如果你曾经使用过Java IO 编程,很可能会遇到这样一个状况,在一个类中向OutputStream里面写数据,而另一个类则从InputStream中读取这些数据。这时问题就出 现了,“如何才能把OutputStream里的数据转换为InputStream里的数据呢?”
方法1:使用byte数组作为缓存
方法2:使用管道
比特流转字符流
通过将比特流对象代入InputStreamReader或OutputStreamWriter包装实现
字符流转比特流
先从字符流中获取char[ ]数组,转换为String,再调用String的getBytes方法获取byte[ ]数组,最后代入所需的比特流即可
NIO/BIO/AIO的区别
BIO
同步阻塞IO(Blocking IO)
线程发起IO请求后,一直等IO缓冲区数据就绪,等待期间啥也不干,直到数据就绪才进行下一步操作

NIO
同步非阻塞IO(Non-Blocking IO/New IO)
提供阻塞和非阻塞方式
阻塞方式:同BIO
非阻塞方式:线程发起IO请求后,一直等IO缓冲区数据就绪,等待期间可以先做别的,但会不停询问数据是否就绪,直到数据就绪后才对当前IO请求进行下一步操作

AIO
异步非阻塞IO(Asynchronous IO),也就是NIO 2
线程发起IO请求以后不关注数据是否就绪,直接返回,也不轮询,等数据OK了由操作系统主动通知刚刚的线程进行后续操作
同步VS异步(区别在于任务时序)
同步
必须等到当前任务执行完,才能执行下一个
对IO来说,就是必须等IO缓冲区数据就绪
异步
发出请求后不关注当前任务是否执行完,可以先执行下一个
对IO来说,就是无需等IO缓冲区数据就绪
阻塞VS非阻塞(同样是等待,区别在于等待时干什么)
阻塞
等待时干等着,啥也不做
非阻塞(忙轮询)
等待时可以先干别的,并时不时询问结果