Java基础 - 易错知识点整理(待更新)
Java基础 - 易错知识点整理(待更新)
Note:这里根据 CSDN Java技能树 整理的Java易错题(不带问
),以及摘录了博主"爱编程的大李子",“哪吒” ,JavaGuide博客的Java面试题整理(带问
),学习过程中可以参考Java8 API在线文档。
Java进阶知识点 参考 Java进阶 - 知识点整理(待更新)
文章目录
- Java基础 - 易错知识点整理(待更新)
- 一、Java简介
- 二、操作符,基本类型,结构语句 与 函数
- 三、类、对象和泛型
- 四、字符串
- 五、异常处理
- 六、集合
- 七、IO(传统IO,即BIO)
- 八、数据库连接
- 九、NIO
- 十、网络编程
- 十一、类型信息(反射 和 动态代理)
- 十二、注解
- 十三、并发编程(Thread,ThreadPool,Synchronized,AQS,ThreadLocal)
- 十四、行为抽象和Lambda
- 十五、设计模式
- 十六、JVM(类加载,运行时数据区,垃圾回收算法,垃圾回收器,JVM调优)
一、Java简介
- 【问】Java 语言有哪些特点?(面向对象,跨平台,java三大特征:封装,继承和多态的特性,支持多线程编程)
- 【问】JVM vs JDK vs JRE,参考JAVA的一次编译,到处运行
Note:JVM
有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果,因此对于第一次编译(javac
)的相同的字节码(.class
文件),在第二次编译(应该说是“解释”)时是在JVM
中执行,为的是输出适应不同平台的机器码,实现跨平台;
JVM 并不是只有一种!只要满足 JVM 规范,每个公司、组织或者个人都可以开发自己的专属 JVM。JDK
是 Java Development Kit 缩写,它是功能齐全的 Java SDK。它拥有JRE
所拥有的一切,还有编译器(javac
)和工具加粗样式(如javadoc
和jdb
)。它能够创建和编译程序。JRE
是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。
- 【问】什么是字节码?采用字节码的好处是什么?(可以实现"一次编译,随处运行"),参考JAVA的一次编译,到处运行
Note:JVM
可以理解的代码就叫做字节码(即扩展名为.class
的文件,是由javac
编译成的中间代码,属于第一次编译成的文件),它不面向任何特定的处理器,只面向虚拟机。JIT
(just-in-time compilation) 编译器属于运行时编译,会将.class
文件编译成机器码;当JIT
编译器完成第一次编译(第二次编译)后,其会将字节码对应的机器码保存下来,下次可以直接使用。
机器码的运行效率肯定是高于 Java 解释器的,这也解释了我们为什么经常会说 Java 是编译与解释共存的语言。.java
源代码 →\rightarrow→javac
编译 →\rightarrow→.class
字节码 →\rightarrow→JVM
(JIT
解释器)→\rightarrow→ 机器码
- 【问】Oracle JDK vs OpenJDK(OpenJDK 的代码是从 Oracle JDK 代码派生出来的,Oracle JDK 比 OpenJDK 更稳定,提供更好的性能; OpenJDK 优点是其开源,而Oracle JDK 并非所有版本免费)
- 【问】Java 和 C++ 的区别?(Java不支持用指针来访问内存;Java类只允许单继承,接口可多继承,主要原因是接口中的方法是抽象的;Java有内存管理和垃圾回收机制(GC)),参考Java中的接口可以多继承以及
default
冲突解决
更多内容整理 参考《我要进大厂》- Java基础夺命连环18问,你能坚持到第几问?(基础概念 | 基本语法 | 基本数据类型)
二、操作符,基本类型,结构语句 与 函数
- 【问】字符型常量和字符串常量的区别?(字符用单引号
''
,字符串用双引号""
;字符相当于整型值,可运算,而字符串是一个对象,代表一个地址值) - 【问】标识符和关键字的区别是什么?(标识符是编程时对类,变量,方法区的名字,而关键字是Java赋予的特殊标识符,注意关键字都是小写)
- 【问】Java 语言关键字有哪些?(this、super、static等)
Note:- 访问控制:private,protected,public
- 类,方法和变量修饰符:abstract,class,extends,final,implements,interface,native,new,static,strictfp, synchronized,transient,volatile,enum
- 程序控制:break,continue,return,do,while,if else,for,instanceof,switch,case,default,assert
- 错误处理:try,catch,throw,throws,finally
- 包相关:import,package
- 基本类型:boolean,byte,char,double,float,int,long,short
- 变量引用:super,this,void
- 保留字:goto,const
- 【问】静态方法为什么不能调用非静态成员?(JVM类加载的过程)
Note:- 静态只能访问静态。 2、非静态既可以访问非静态的,也可以访问静态的。
- 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问;
而非静态成员属于实例对象,只有在对象实例化之后,才会分配内存,需要通过类的实例对象去访问。 - 在类的非静态成员不存在的时候静态成员就已经存在了,此时静态方法 调用 在内存中 还不存在的非静态成员,属于非法操作。
- 【问】静态方法和实例方法有何不同?
Note:- 在调用方式上:
- 在外部调用静态方法时,可以使用
类名.方法名
的方式,也可以使用对象.方法名
的方式; - 实例方法只有
对象.方法名
这种方式;
- 在外部调用静态方法时,可以使用
- 在访问类成员时:
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法)
- 实例方法(非静态方法)既可以访问静态成员,也可以访问实例成员。
- 在调用方式上:
- 【问】什么是可变长参数?(Java5支持定义可变长参数,即在调用方法时传入不定长度的参数
String... args
,要求只能作为函数的最后一个参数;在使用重载方法时,优先使用固定参数的方法,而非可变长参数方法) - 【问】Java 中的几种基本数据类型了解么?(4个整型(1,2,4,8 byte),2个浮点型(4,8 byte) +
char
(2 byte) +bool
(1bit)) - 【问】基本类型和包装类型的区别?(默认值的区别;基本数据类型局部变量存放在
JVM
栈中的局部变量表中,成员变量(未被static
修饰 )存放在JVM
堆中;包装类型属于对象类型,对象实例都存在JVM
堆中) - 【问】包装类型的缓存机制了解么?(为指定区间内每个整数 / 每个字符定义一个单例静态对象),参考JZ68 二叉搜索树的最近公共祖先题解(
Integer
的缓存机制让我在进行整数判断时踩了大坑,对于包装类值的比较,建议要么先强转成基本类型,要么使用equals
)
Note:Byte,Short,Integer,Long
这 4 种包装类默认创建了数值[-128,127]
的相应类型的缓存数据,Character
创建了数值在[0,127]
范围的缓存数据,Boolean
直接返回True
或者False
。如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。- 浮点数类型的包装类
Float
,Double
并没有实现缓存机制。 - 装箱操作是指,
Integer i1=40
等价于Integer i1=Integer.valueOf(40)
,如果该值在[-128~127]
,则使用Integer
缓冲区中的对象,否则创建新对象,因此Integer i1 = 33; Integer i2 = 33; System.out.println(i1 == i2);// 输出 trueInteger i1 = 40; //使用缓存中的包装类 Integer i2 = new Integer(40); //创建新的包装类 System.out.println(i1==i2); // 输出 falseFloat i11 = 333f; Float i22 = 333f; System.out.println(i11 == i22);// 输出 falseDouble i3 = 1.2; Double i4 = 1.2; System.out.println(i3 == i4);// 输出 false
- 【问】自动装箱与拆箱了解吗?原理是什么?(装箱是指将基本类型用它们对应的引用类型包装起来;拆箱是指将包装类型转换为基本数据类型)
- 关系操作符 (对象比较和地址比较)
- 按位操作符
- 移位操作符
- 字符串操作符,参考 java中字符串,整数,字符相加转换
- 变量的声明和初始化
Note:String username,address,phone,tel; // 声明多个变量 int num1=12,num2=23,result=35; // 声明并初始化多个变量
- 数组初始化二维数组
Note:- 在静态初始化数组时,左端声明数组时,不用传入维数,右端
new
时也不用指明维数,维数通过初始化值确定。//这么写是错误的 public int[2][2] postion = {{-1,-1},{-1,0}, }; //这么写也是错误的 public int[][] postion = new int[2][2]{{-1,-1},{-1,0}, }; //这么写是正确的 public int[][] postion = new int[][]{{-1,-1},{-1,0}, };
- 在动态初始化时,左端不需要传入维数,右端
new
时需要指明维数:int[][] arr = new int[3][2];
- 在静态初始化数组时,左端声明数组时,不用传入维数,右端
- switch结构
- switch语句的应用
- 递归实现全排列
更多内容整理 参考《我要进大厂》- Java基础夺命连环18问,你能坚持到第几问?(基础概念 | 基本语法 | 基本数据类型)
三、类、对象和泛型
【问】this与super的区别(
this
访问本类中的成员或方法,如果没找到,则从父类中找;而super
则直接从父类中找;this()
访问本类的构造器,而super()
访问的是父类的构造器)
Note:- 如果函数的形参与类中的成员数据同名,这时需用
this
来指明成员变量名:例如this.name = name;
this(参数)
调用(转发)的是当前类中的构造器;super(参数)
用于确认要使用父类中的哪一个构造器;
this()
和super()
都只能写在构造函数的第一行;this()
和super()
不能存在于同一个构造函数中;
this()
和super()
都指的是对象,所以,均不可以在static
环境中使用。包括:static
变量,static
方法,static
语句块。- 从本质上讲,
this
是一个指向本对象的指针, 然而super
是一个Java关键字。
- 如果函数的形参与类中的成员数据同名,这时需用
【问】面向对象和面向过程的区别?(面向过程把解决问题的过程拆成一个个方法,按顺序执行来解决问题;面向对象会先抽象出对象,然后用对象执行的方法来解决问题)
【问】面向对象三大特征(封装/继承/多态/抽象:封装是对于一个类;继承是对于父类和子类;多态是指父类对子类实例的引用,引用类型的行为具有多态性),参考对象类型和引用类型(对象的引用)的区别(用父类引用子类实例,即泛化)
Note:- 封装:封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
- 继承:
1)子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有;
2)子类可以拥有自己属性和方法,即子类可以对父类进行扩展;
3)子类可以重写父类的方法。 - 多态:一个对象具有多种的状态,具体表现为父类的引用指向子类的实例(泛化)
1)对象类型和引用类型之间具有继承(类)/实现(接口) 的关系;
2)引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间(为对象实例分配内存后)才能确定;
3)多态 不能调用 “只在子类存在但在父类不存在”的方法;
4) 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
【问】接口和抽象类有什么共同点和区别?(共同点是不能被实例化;含抽象方法;可以有默认实现(java8在接口中用
default
实现))
Note - 区别:- Java接口是行为性的,也就是说接口只是定义某个行为的名称具体的实现动作,都在实现类本身这里;抽象类主要用于代码复用,强调的是所属关系。
- 一个类只能继承一个类(类的多继承会有冲突,
Java
直接不支持);
一个类可以实现多个接口(实现多个抽象方法,java8
引入了default
,需要解决冲突);一个类实现的多个接口,有相同签名的default方法会怎么办
一个接口可以继承多个接口(java8
引入了default
,需要解决冲突);Java中的接口可以多继承以及default
冲突解决 - 接口中的成员变量只能是
public static final
类型的,不能被修改且必须有初始值,而抽象类的成员变量默认default
,可在子类中被重新定义,也可被重新赋值。
【问】Java类实现某个接口后,是否需要实现接口中的所有方法?
Note:- 并不是所有的类实现接口后都必须实现接口的所有方法
- 当Java抽象类实现某个接口后没必要实现所有的方法
- 当Java普通类实现接口后必须实现接口中的所有方法
- 实例说明:
- 这一点可以结合抽象工厂模式进行理解(一个接口,多个抽象类,
N
个实现类) - 比如在
ThreadPoolExecutors
中,ThreadPoolExecutors
实现了Executor
接口的execute
方法,而其继承的抽象父类AbstractExecutorService
则实现了Executor
接口的submit
方法,没有实现execute
方法。
- 这一点可以结合抽象工厂模式进行理解(一个接口,多个抽象类,
- 并不是所有的类实现接口后都必须实现接口的所有方法
【问】深拷贝和浅拷贝区别了解吗?什么是引用拷贝?(引用拷贝即直接赋值,实现对象的引用;浅拷贝对引用对象里基本类型数据直接拷贝,对其引用类型数据不进行拷贝;深拷贝则对所有数据类型都进行拷贝),参考一文读懂Java深拷贝浅拷贝引用拷贝
【问】为什么要使用克隆?如何实现对象克隆?(保留原有的对象进行接下来的操作;实现
Cloneable
接口,重写clone
方法实现浅 / 深拷贝;实现Serializable
接口,进行序列化,完成深拷贝),参考java重写clone()方法
Note:- java浅克隆,即在实现
Clonable
接口时,重写Object
的clone
方法,并转化成子类类型对象(Person p = (Person) super.clone()
),但如果子类对象中有引用类型(Address
),新clone
的对象p
与原型对象所指向的Address
对象是相同的。 - java深克隆则要求所有
Person
对象的所有引用类型都要重写clone
方法,比如p=(Person)super.clone();p.address = address.clone();
- 原型模式的核心是一个
clone
方法,通过该方法进行对象的拷贝,Java提供了一个Cloneable
接口来标示这个对象是可拷贝的,Cloneable
接口只是一个标记作用,在JVM
中具有这个标记的对象才有可能被拷贝。在调用clone()
方法时,JVM
会根据这个标识在堆内存中以二进制的方式拷贝对象,重新分配一个内存块,并没有执行构造函数(new
)
- java浅克隆,即在实现
【问】java为什么要使用序列化?如何实现序列化?序列化ID的作用?,参考java序列化详解,java序列化看这篇就够了
Note:- 序列化是指将对象转化为二进制,用于保存,或者网络传输。而反序列化是将二进制转化成对象。
- 序列化的作用:
1)想把内存中的对象保存到一个文件中或者数据库中时候;
2)想用套接字在网络上传送对象(字节流)的时候;
3)所有可在网络上传输的对象都必须是可序列化的,比如RMI
(remote method invoke,即远程方法调用),传入的参数或返回的对象都是可序列化的,否则会出错;所有需要保存到磁盘的java
对象都必须是可序列化的。通常建议:程序创建的每个JavaBean类都实现Serializeable接口;参考RPC实现原理之核心技术-序列化,老王讲自制RPC框架.(四.序列化与反序列化),java序列化看这篇就够了 - JDK序列化的实现:
1)创建一个对象,实现Serializable
接口,为每个属性添加get,set
方法。
2)用对象流写一个保存对象与读取对象的工具类;通过outputstream.writeObject(object)
保存对象,通过Object object = inputstream.readObject()
读取对象
3)服务器端给客户端发送序列化对象数据并非加密的,如果对象中有一些敏感数据比如密码等,那么在对密码字段序列化之前,最好做加密处理, 这样可以一定程度保证序列化对象的数据安全。 - 序列化ID的作用:
java的序列化机制是通过判断运行时类的private static final long serialVersionUID
来验证版本一致性的,在进行反序列化时,JVM
会把传进来的字节流中的serialVersionUID
与本地实体类中的serialVersionUID
进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。 Protobuf
是Google
推出的开源序列库,它是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持Java
、Python
、C++
、Go
等多种语言;框架比如流行的protostuff
总结一些优点如下:
1)Java序列化对象时不需要通过属性的get, set
方法或其它无关序列化内部定义的方法(比如readObject
,writeObject
是内置的序列化方法),序列化也不需要get set方法支持,反序列化是构造对象的一种手段。
2)Java序列化时类型必须完全匹配(全路径类名+序列化id)。
3)Protostuff
反序列化时并不要求类型匹配,比如包名、类名甚至是字段名,它仅仅需要序列化类型A
和反序列化类型B
的字段类型可转换(比如int
可以转换为long
)即可。
4)反序列化必须拥有.class
文件(字节码文件) ,但随着项目的升级,.class
文件也会升级,序列化怎么保证升级前后的兼容性呢?java序列化提供了一个private static final long serialVersionUID
的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。
【问】Object 类的常见方法有哪些?
Note:getClass()
:native
方法,用于返回当前运行时对象的Class
对象,使用了final
关键字修饰,故不允许子类重写;hashCode()
:native
方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的HashMap。equals(Object obj)
:用于比较 2 个对象的内存地址是否相等,String
类对该方法进行了重写以用于比较字符串的值是否相等。clone()
:naitive
方法,用于创建并返回当前对象的一份浅拷贝。toString()
:返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。finalize()
:实例被垃圾回收器回收的时候触发的操作- 还有
notify()
,notifyAll()
,3个重载的wait()
【问】== 和 equals() 的区别?(
==
对基本类型比值,对对象类型比地址;equals
不能判断基本类型,未重写equals
则等价于==
)【问】hashCode() 有什么用?(用于获取哈希码(
int
),也称为散列码,哈希码可确定该对象在哈希表中的索引位置;但两个对象的hashCode
值相等并不代表两个对象就相等,可通过线性探测解决哈希碰撞)【问】为什么重写 equals() 时必须重写 hashCode() 方法?(两个相等的对象的
hashCode
值必须是相等,也就是说如果equals
方法判断两个对象是相等的,那这两个对象的hashCode
值也要相等;如果不重写,在对equal
值相等的对象使用hashmap
,hashset
会存储多个相同的对象,不符合set
概念)【问】什么是泛型?有什么作用?(泛型的本质就是"参数化类型",泛型可避免强转的操作,在编译器完成类型转化,避免了运行错误,如果不用泛型,则
list
中add
了整型和字符串就会运行报错,但不会编译报错),参考java 泛型全解 - 绝对最详细,Java泛型详解,史上最全图文详解,泛型类型不能是基本类型
Note:- 保证了类型的安全性:如果不用泛型,则
list
中同时add
了整型和字符串就会运行报错,但不会编译报错 - 消除强制类型转换:如果不用泛型,则
list
中取出的元素是Object
,需要强转成指定类型。 - 提高了代码的重用性:泛型的本质是参数化类型(不能是基本类型),对于泛型方法,可以由各种特定类型来调用,提高了扇入,进而提高了代码的复用性。
JVM
并不知道泛型,所有的泛型在编译阶段都已经被处理成了普通类和方法,处理方法叫做类型擦除。
- 保证了类型的安全性:如果不用泛型,则
【问】泛型的使用方式有哪几种?(泛型类、泛型接口、泛型方法(参数或返回值为泛型))
【问】项目中哪里用到了泛型?(自定义接口通用返回结果
CommonResult<T>
(自定义的集合类) ,通过参数T
可根据具体的返回类型动态指定结果的数据类型;构建集合工具类(参考Collections
中的sort
,binarySearch
方法))修改Data类的定义(类由属性和方法构成)
重写父类方法(不重写的话,子类对象无法访问父类的public方法),可参考C++中的public,protected,private继承机制 - (C++默认使用私有继承,三者区别在于 派生类会将父类的成员转换成那种类型 & 对象访问权限),java的继承机制,java中的public,protected,private,default详解(对象中的成员是否可访问),java只有公有继承
Note:C++
默认使用私有继承,因此子类将父类的public
,protected
方法变成子类的private
方法,此时需要重写父类中的这些public
,protected
方法,才能让对象访问到;而Java
默认使用公有继承,父类中的public
成员和方法,子类的对象都可以访问到。- 对于java的成员属性和方法,如何不加任何访问修饰符,通常称为
default
访问模式。该模式下,只允许在同一个包中进行访问,访问权限仅次于private
。 - 与C++不同的是,java只允许单继承一个父类,但是接口允许多继承。
关于抽象类的描述,可参考Java面向对象
实现抽象类方法(抽象类方法重写,子类不带abstract)
关于接口的描述(接口不能有构造方法),可参考Java:接口和抽象类,傻傻分不清楚?,Java中接口的default方法(Java8为了兼容源代码提出的)
匿名内部类(该题把java8中的lambda表达式看做是匿名内部类的语法糖,实现某个抽象类或接口的方法),可参考深入理解java嵌套类、内部类以及内部类builder构建构造函数,Lambda表达式与匿名内部类
更多内容整理 参考
- 《我要进大厂》- Java基础夺命连环10问,你能坚持到第几问?(面向对象基础篇)
- 《我要进大厂》- Java基础夺命连环14问,你能坚持到第几问?(Object类 | String类)
- 《我要进大厂》- Java基础夺命连环10问,你能坚持到第几问?(异常 | 泛型)
四、字符串
- 【问】String、StringBuffer、StringBuilder 的区别?(
String
字符串不可变;StringBuffer
和StringBuilder
都继承于AbstractStringBuilder
,其中用字符数组保存字符串,可通过append、insert
来修改字符串内容而非引用,数组可扩容,相比String
节省常量池空间;StringBuffer
加锁(Synchronized
)线程安全,StringBuilder
线程不安全但效率高,可用ThreadLocal
解决线程安全问题),可参考彻底弄懂StringBuffer与StringBuilder的区别
Note:StringBuffer
与StringBuilder
的应用场景- 1)
StringBuffer
多线程安全,但是对buffer
缓冲区加了synchronized,其效率低。故适用于多线程下,并发量不是很高的场景 - 2)
StringBuilder
没有加任何锁,其效率高,适用单线程场景,但同时也适用于高并发场景中,提高高并发场景下程序的响应性能,至于线程安全问题可以通过其它手段解决,如ThreadLocal
,CAS
操作等。 - 3)所以对于高并发场景下,若有用到二者,还是建议优先使用StringBuilder的。
- 1)
- 在 Java 9 之后,
String
、StringBuilder
与StringBuffer
的实现改用byte
数组存储字符串(byte8字节,char 16字节,用Latin1
进行编码)。
- 【问】String 为什么是不可变的?(
final
是根本原因) - 【问】字符串拼接用“+” 还是 StringBuilder?(
+
和+=
是java中唯二的两个重载的运算符,+
其实是通过StringBuilder
的append
实现)
Note:- 如果在循环中使用字符串
+
运算,则会创建多个StringBuilder
对象;如果直接使用StringBuilder
对象就不会有这个问题。
- 如果在循环中使用字符串
- 【问】String#equals() 和 Object#equals() 有何区别?(
Object
的equals用于比较地址是否相同;String
的重写了Object
的equals,用于比较两字符串内容是否相等) - 【问】字符串常量池的作用了解吗?(JVM专门为
String
创建的一块内存区域,内容相同的字符串其地址相同,减少内存开销)
Note:// 在堆中创建字符串对象”ab“ // 将字符串对象”ab“的引用保存在字符串常量池中 String aa = "ab"; // 直接返回字符串常量池中字符串对象”ab“的引用 String bb = "ab"; System.out.println(aa==bb);// true
- 【问】
String s1 = new String("abc");
这句话创建了几个字符串对象?(堆中会创建1或2个对象:每次new String()
都会创建一个引用对象;如果abc
在常量池中存在,则不会重新创建字符串常量,引用对象指向该常量地址) - 【问】
String.intern
方法有什么作用?
Note:结合上一问的字节码理解String.intern()
调用native
本地方法,用于返回引用对象所指向的字符串常量的第一个引用对象(需要通过上一问的字节码理解),如果是String
的对象池(不是String
常量池)中有该类型的值,则直接返回对象池(堆内存)中的对象,可以实现对象复用。// 在堆中创建字符串对象”Java“ // 将字符串对象”Java“的引用保存在字符串常量池中(s1为"java"的第一个引用对象) String s1 = "Java"; // 直接返回字符串常量池中字符串对象”Java“对应的引用 String s2 = s1.intern(); // s1 和 s2 指向的是堆中的同一个对象 System.out.println(s1 == s2); // true // 会在堆中在单独创建一个字符串对象 String s3 = new String("Java"); (s3为"java"的第二个引用对象) // 直接返回字符串常量池中字符串对象”Java“对应的引用 String s4 = s3.intern(); (返回"java"的第一个引用对象s1) // s3 和 s4 指向的是堆中不同的对象 System.out.println(s3 == s4); // false// s1 和 s4 指向的是堆中的同一个对象 System.out.println(s1 == s4); //true
- 【问】String 类型的变量和常量做“+”运算时发生了什么?(常量池和堆)
Note:- 对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
final String str1 = "str"; final String str2 = "ing"; // 下面两个表达式其实是等价的 String c = "str" + "ing"; //常量池中的对象,编译器会给你优化成 String c = "string"; 。 String d = str1 + str2; //常量池中的对象 System.out.println(c == d); //true
- 而引用的值在程序编译期是无法确定的,编译器无法对其进行优化(虽然字符串
+
是StringBuilder
实现的)。String str4 = new StringBuilder().append(str1).append(str2).toString();
下面的
str2
在运行时才可确定值,因此str2
放在堆(运行时创建对象)中,而非常量池final String str1 = "str"; final String str2 = getStr(); String c = "str" + "ing";// 常量池中的对象 String d = str1 + str2; // 在堆上创建的新的对象 System.out.println(c == d);// false public static String getStr() {return "ing"; }
- 对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。
更多内容整理 参考《我要进大厂》- Java基础夺命连环14问,你能坚持到第几问?(Object类 | String类)
五、异常处理
- 【问】Exception 和 Error 有什么区别?(都继承
Throwable
接口;Exception
为程序可以处理的异常,可用catch
捕获;而Error
属于程序无法处理的错误,不建议用catch
捕获(捕获后也没用),例如虚拟机内存不够错误(OutOfMemoryError
,StackOverFlowError
)、类定义错误(NoClassDefFoundError
),此时JVM会选择终止线程) - 【问】Checked Exception 和 Unchecked Exception 有什么区别?
Note:两者是针对编译和运行期间而言的:- 对于
Checked Exception
(检查型异常)也称编译时异常,比如除了RuntimeException
及其子类外,其余的Exception
都是检查型异常,如果没有被catch / throw
则没法编译通过; RuntimeException
及其子类都为Unchecked Exception
(非检查型异常),即运行时异常,常见的非检查型异常包括:NullPointerException
(空指针错误)IllegalArgumentException
(参数错误比如方法入参类型错误)NumberFormatException
(字符串转换为数字格式错误,IllegalArgumentException
的子类)ArrayIndexOutOfBoundsException
(数组越界错误)ClassCastException
(类型转换错误)
…
- 对于
- 【问】常见的异常类有哪些?(见上一问)
- 【问】Throwable 类常用方法有哪些?
Note:String getMessage()
: 返回异常发生时的简要描述String toString()
: 返回异常发生时的详细信息void printStackTrace()
: 在控制台上打印Throwable
对象封装的异常信息
- 【问】throw 和 throws 的区别?(
throw
用于方法体内的异常处理;throws
作用于方法声明上,用于向外抛出异常) - 【问】final、finally、finalize 有什么区别?
- 被
final
关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。
在程序开发中,确认只需要一次赋值的属性则设置为final
类型,避免无意修改导致逻辑混乱,特别是Session级的常量或变量 finally
用于抛异常,finally代码块内语句无论是否发生异常,都会执行finally,常用于一些流等资源的关闭。比如InputStream
、OutputStream
、Scanner
、PrintWriter
等的资源都需要我们调用close()
方法来手动关闭;finalize
方法用于垃圾回收,有些时候需要实现finalize
方法来关闭整个生命周期内存在的资源(不推荐使用);在调用finalize
时并不意味着gc
会立即回收对象,而在真正调用时可能该对象不需要被回收了。
- 被
- 【问】try-catch-finally 如何使用?(见下)
- 【问】try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?(会)
- 【问】try-catch-finally 中哪个部分可以省略?(
catch
可省略)
Note:try
块用于捕获异常。其后可接零个或多个catch
块,用于捕获try
块的异常,如果没有catch
块,则必须跟一个finally
块;- 当在
try
块或catch
块中遇到return
语句时,finally
语句块将在方法返回之前被执行。 - 不要在 finally 语句块中使用 return! 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。
- 【问】
finally
中的代码一定会执行吗?(不一定,在finally之前System.exit(1)
,释放CPU或者线程关闭时finally
块不执行) - 【问】如何使用 try-with-resources 代替 try-catch-finally?
Note:- Java7后可通过使用分号分隔,可以在
try-with-resources
块中声明多个资源:try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {int b;while ((b = bin.read()) != -1) {bout.write(b);} } catch (IOException e) {e.printStackTrace(); }
- Java7后可通过使用分号分隔,可以在
- 【问】异常使用有哪些需要注意的地方?(抛出的异常信息一定要有意义,比如
NumberFormatException
的异常就不适合用其父类,比如Exception
来抛出) - 自定义运行时异常(RuntimeException),可参考 深入理解java异常处理机制
- 自定义可查异常(Checked Exception),可参考java(3)-深入理解java异常处理机制
- 资源对象管理 (try_with_resource),可参考最透彻解析try-with-resource
Note: - 不可查异常(Unchecked Exception)指在运行时发生的异常。这些也被称为运行时异常(Runtime Exception)。其中包括程序逻辑错误或API使用不当等,例如ArrayIndexOutOfBoundsException、IllegalStateException与NullPointerException等,此类异常将在编译时忽略,仅在运行时处理。
- 可查异常(Checked Exception)是编译器在编译时会进行检查,也称为编译时异常(Compile Time Exception)。 这些异常往往具备可预见性,且不能简单地被忽略,编程者应当对其进行妥善处理。
- finally覆盖catch(开头引子的例子):
1)如果finally
有return
会覆盖catch
里的throw
,同样如果finally
里有throw
会覆盖catch
里的return
。
2) 如果catch
里和finally
都有return
,finally
中的return
会覆盖catch
中的。throw也是如此。
更多内容整理 参考
- 《我要进大厂》- Java基础夺命连环10问,你能坚持到第几问?(异常 | 泛型)
- Java面试题总结(分类版)
- 10万字208道Java经典面试题总结(附答案)
六、集合
【问】Java 集合概览(集合包括
Collection
(存储单一元素)和Map
(存储键值对),Collection
最主要的子接口包括List
、Set
和Queue
;LinkedList
既实现了List
接口,又实现了queue
接口)
【问】说说 List, Set, Queue, Map 四者的区别?(
list
有序可重复;set
无序不可重复;queue
有序可重复,指定排队规则;Map
中key
是无序不可重复的,value
是无序可重复的)【问】集合框架底层数据结构总结
Note:- List:
ArrayList
和Vector
使用Object[]
对象数组;LinkedList
使用双向链表(java7去掉了循环链表) - Set:
HashSet
(无序,唯一)使用HashMap
实现;LinkedHashSet
是HashSet
子类,基于LinkedHashMap
实现;TreeSet
(有序,唯一)基于红黑树(自平衡的排序二叉树)实现; - Queue:
PriorityQueue
使用Object[]
对象数组实现二叉堆;
ArrayQueue
基于Object[]
数组和双指针实现; - Map:
1)Java1.8之前
HashMap
由数组+链表组成;
2)Java1.8之后HashMap
在解决哈希冲突时有了较大的变化,先判断是否扩容数组,再将链表转为红黑树:
当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间(二叉查找树在某种意义上会退化成线性结构,搜索效率低)。
LinkedHashMap
继承自HashMap
,由数组+链表/红黑树组成,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序(不能保证元素上的有序)。Hashtable
由数组+链表组成的,链表主要为了解决哈希冲突而存在的TreeMap
基于红黑树实现(自平衡的排序二叉树)
- List:
【问】如何选用集合?(根据集合的特点来选用,比如在使用键值对时,为了key有序,则使用
TreeMap
;为了保证线程安全,则使用ConcurrentHashMap
)【问】为什么要使用集合?(数组的缺点是 一旦声明之后,长度就不可变了,而且声明数组时的数据类型也决定了该数组存储的数据的类型,存储元素单一;而集合(容器)提高了数据存储的灵活性(数据类型,数据存储长度))
【问】Arraylist 和 Vector 的区别?(底层都是数组,但
ArrayList
线程不安全;Vector
线程安全,即在add()
等方法中加了synchronized
)【问】Arraylist 与 LinkedList 区别?(
ArrayList
支持随机访问;LinkedList
会占用更多空间;底层结构看第二问)
Note:ArrayList
在指定位置i
插入和删除元素的话(add(int index, E element)
)时间复杂度就为O(n-i)
- 不要下意识地认为
LinkedList
作为链表就最适合元素增删的场景,LinkedList
仅仅在头尾插入或者删除元素的时候时间复杂度近似O(1)
,其他情况增删元素的时间复杂度都是O(n)
。 ArrayList
除了头部插入(删除)比LinkedList
效率低,其他方面都是超于LinkedList
。 所以实际项目开发中,建议使用ArrayList。
【问】说一说 ArrayList 的扩容机制吧?(当前容量1.5倍 / Collection的实际元素个数),可参考<Java八股文面试>ArrayList源码 | Iterator源码 | LinkedList和ArrayList对比
Note:ArrayList()
的扩容机制:ArrayList()
会使用长度为零的数组;ArrayList(int initialCapacity)
会使用指定容量的数组;在使用ArrayList()
无参构造时,add(Object o)
首次扩容为 10,再次扩容为上次容量的 1.5 倍public ArrayList(Collection<? extends E> c)
会使用 c 的大小作为数组容量- 在使用
ArrayList()
无参构造时,addAll(Collection c)
没有元素时,扩容为 Math.max(10, 实际元素个数),有元素时为 Math.max(原容量的1.5 倍, 实际元素个数),即下次扩容容量 和 实际元素个数之间选一个最大值。
ArrayList
扩容的关键方法grow()
:(add()
和addAll(Collection c)
都用到grow()
方法)- 扩容:把原来的数组复制到另一个内存空间更大的数组中
复制(添加)元素到新数组:把新元素添加到扩容以后的数组中 - 先在原容量上乘
1.5
倍扩容;接着判断newCapacity
是否大于实际的元素个数minCapacity
,如果小于则直接设置为minCapacity
;如果数组的连续空间不够用出现溢出(newCapacity > MAX_ARRAY_SIZE
),则将elementData
指向新的连续内存空间,并将原数组中元素复制到新数组中。
//扩容方法private void grow(int minCapacity) {//获取到ArrayList中elementData数组的内存空间长度int oldCapacity = elementData.length;//计算扩容后的大小:扩容至原来的1.5倍int newCapacity = oldCapacity + (oldCapacity >> 1);// 再判断一下新数组的容量够不够,够了就直接使用这个长度创建新数组. 如果不够,则直接使用minCapacity 的值,这样可以避免重复扩容. if (newCapacity - minCapacity < 0)// 不够就将数组长度设置为需要的长度newCapacity = minCapacity;//若预设值大于默认的最大值检查是否溢出if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// 调用Arrays.copyOf方法将elementData数组指向新的内存空间时newCapacity的连续空间// 并将elementData的数据复制到新的内存空间elementData = Arrays.copyOf(elementData, newCapacity);}
- 扩容:把原来的数组复制到另一个内存空间更大的数组中
【问】comparable 和 Comparator 的区别?,可参考java中Comparable讲解,使用Comparator对ArrayList集合中的元素进行排序
Note:Comparable
(带able
)接口作用于实体,在实体本身中扩展其功能,因此更适合对多个字段的排序规则进行制定;
Comparator
是在实体外对实体进行排序的工具,并非实体本身持有,因此更适合单一字段(Integer
,String
都实现了Comparable
接口)排序规则的指定;Comparable
接口实际上是出自java.lang
包 它有一个compareTo(Object obj)
方法用来排序;实体(Person)实现该接口Comparable<Person>
并重写compareTo()
,可以对实体的不同对象按某种规则进行比较,一般配合TreeMap
,Arrays.sort()
使用。class Person implements Comparable<Person>{...@Overridepublic int compareTo(Person o) {// TODO Auto-generated method stubif(this.age>o.age){return 1;}else if(this.age<o.age){return -1;}//当然也可以这样实现// return Integer.compare(this.age, o.age);return 0;}}public static void main(String[] args) {Person []persons = new Person[]{new Person("张三",15),new Person("李四",25),new Person("王五",20)};Arrays.sort(persons);}
Comparator
接口实际上是出自java.util
包它有一个compare(Object obj1, Object obj2)
方法用来排序,一般需要自定义比较器,配合Collections.sort(collection,comparator)
使用;public class CompareName implements Comparator<Person>{//按照姓名进行排序@Overridepublic int compare(Person p1, Person p2) {// TODO Auto-generated method stub//String实现了Serializable, CharSequence, Comparable<String>return p1.getName().compareTo(p2.getName()); } } public static void main(String[] args){CompareName cn = new CompareName();//CompareBirthday cb = new CompareBirthday();//CompareAge ca = new CompareAge();System.out.println("\n按姓名排序:");//其中\n表示换行Collections.sort(value,cn); }
【问】无序性和不可重复性的含义是什么?(无序性并非随机性,而是数据底层的存储顺序由数据的
hash
值决定;不可重复性是指添加的元素按照 equals()判断时 ,返回 false,需要同时重写equals()
方法和HashCode()
方法)【问】比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同?(都是
Set
接口的实现类,都是非线程安全的;在底层数据结构上,HashSet
用到哈希表,LinkedHashSet
用到哈希表+链表,TreeSet
用到红黑树;在场景上,HashSet
用于不需要保证元素插入和取出顺序的场景,LinkedHashSet
用于保证元素的插入和取出顺序满足FIFO
的场景,TreeSet
用于支持对元素自定义排序规则的场景)【问】Queue 与 Deque 的区别?(
Queue
单端队列,一端插入一端删除,支持FIFO
,常见方法包括抛出异常(add
)和返回值(offer
);Deqeue
双端队列,扩展了 Queue 的接口, 增加了在队首和队尾进行插入和删除,常见方法包括抛出异常(addFirst
)和返回值(offerFirst
);Deque 还提供有push()
和pop()
等其他方法,可用于模拟栈;)【问】ArrayDeque 与 LinkedList 的区别?,可参考ArrayDeque方法总结
Note:ArrayDeque
是基于可变长的数组和双指针(tail,head)来实现的循环队列,而LinkedList
则通过链表来实现。ArrayDeque
不支持存储 NULL 数据,但LinkedList
支持。ArrayDeque
是在 JDK1.6 才被引入的,而LinkedList
早在 JDK1.2 时就已经存在。ArrayDeque
插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然 LinkedList 不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。- 当
ArrayDeque
被当作栈使用时比Stack
快,当作队列使用时比LinkedList
快。
【问】说一说 PriorityQueue?(
PriorityQueue
与Queue
的区别在于元素出队顺序是与优先级相关的,使用了二叉堆数据结构,底层通过数组实现)
Note:PriorityQueue
是非线程安全的,且不支持存储NULL
和non-comparable
的对象。PriorityQueue
默认是小顶堆,但可以接收一个Comparator
作为构造参数,从而来自定义元素优先级的先后。PriorityQueue
常用于堆排序、求第K大的数、带权图的遍历等问题求解
【问】HashMap 和 Hashtable 的区别(
Hashtable
线程安全,但建议使用ConcurrentHashMap
)
Note:HashMap
可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable
不允许有 null 键和 null 值,否则会抛出NullPointerException
。- 初始容量大小和每次扩充容量大小的不同 :
- 1)创建时如果不指定容量初始值,
Hashtable
默认的初始大小为 11,之后每次扩充,容量变为原来的2n+1
。HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的2
倍。 - 2)创建时如果给定了容量初始值,那么
Hashtable
会直接使用你给定的大小,而HashMap
会将其扩充为2
的幂次方大小(HashMap
中的tableSizeFor()
方法保证)。也就是说HashMap
总是使用 2 的幂作为哈希表的大小。
主要原因是hash值的范围很大(共40亿个),不能直接放在内存中,需要通过hash%length
的方式来计算数组下标,而对hash%length
的计算则等价于(length - 1)& hash
(前提是length
是 2 的 n 次方),参考HashMap 的长度为什么是 2 的幂次方
- 1)创建时如果不指定容量初始值,
【问】HashMap 和 HashSet 区别(HashSet基于HashMap实现,除了
clone()
、writeObject()
、readObject()
是 HashSet 自己不得不实现之外,其他方法都是直接调用HashMap
中的方法)HashMap HashSet 实现了 Map 接口 实现 Set 接口 存储键值对 仅存储对象 调用 put() 向 map 中添加元素 调用 add() 方法向 Set 中添加元素 HashMap 使用键(Key) 计算 hashcode HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals()方法用来判断对象的相等性 【问】HashMap 和 TreeMap 区别(
TreeMap
和HashMap
都继承自AbstractMap
,但TreeMap
它还实现了NavigableMap
接口和SortedMap
接口)
Note:- 实现
NavigableMap
接口让TreeMap
有了对集合内元素的搜索的能力。 - 实现
SortedMap
接口让TreeMap
有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。示例代码如下:public static void main(String[] args) {TreeMap<Person, String> treeMap = new TreeMap<>(new Comparator<Person>() {@Overridepublic int compare(Person person1, Person person2) {int num = person1.getAge() - person2.getAge();//return Integer.compare(num, 0); //Compares two int values numerically.return Integer.compareTo(num, 0); //Compares two Integer objects numerically.}});treeMap.put(new Person(3), "person1");treeMap.put(new Person(18), "person2");treeMap.put(new Person(35), "person3");treeMap.put(new Person(16), "person4");treeMap.entrySet().stream().forEach(personStringEntry -> {System.out.println(personStringEntry.getValue());}); }
- 实现
【问】HashSet 如何检查重复(先调用对象的
hashcode
比hash值,再调用equals()
比内容),参考如何正确重写hashCode方法?
Note:Integer
,String
都重写了hashcode
方法:Integer
是返回当前int值,String
返回当前每个字符hash*31
,最后累加;- 自定义对象在重写
hashcode
方法时,规定相等的对象应该具有相同的hashCode; - Object中的
hashCode()
是调用的本地native
方法,Java 中Object对象的hashcode()
返回值一定通过了对 Object对象的内存地址 的计算,与内存地址有关,这说明hashcode()
返回的不是对象在内存中的地址。
【问】HashMap 的底层实现(数组扩容机制,hashmap链式转红黑树,看第三问和下一问)
【问】HashMap 的长度为什么是 2 的幂次方(
hash%length
的计算在length
是 2 的 n 次方的前提下等价于(length - 1)& hash
,看前几问)【问】HashMap 多线程操作导致死循环问题(原因是在并发下的 Rehash 会造成元素之间会形成一个循环链表,建议使用
ConcurrentHashMap
)【问】HashMap 有哪几种常见的遍历方式?(7种方式:Iterator.
EntrySet
,Iterator.KeySet
,foreach EntrySet,foreach KeySet,Lambda 表达式,StreamAPI单线程,StreamAPI多线程),可参考HashMap 的 7 种遍历方式与性能分析!
Note:EntrySet
的性能比KeySet
的性能高出了一倍,因为KeySet
相当于循环了两遍 Map 集合,而EntrySet
只循环了一遍,之后通过代码“Entry<Integer, String> entry = iterator.next()
”把对象的key
和value
值都放入到了Entry
对象中;- 1)我们不能在遍历中使用集合
map.remove()
来删除数据,这是非安全的操作方式,但我们可以使用迭代器的iterator.remove()
的方法来删除数据,这是安全的删除集合的方式。
2)同样的我们也可以使用Lambda
中的removeIf
来提前删除数据,或者是使用Stream
中的filter
过滤掉要删除的数据进行循环,这样都是安全的(不能在lambda
循环或者Stream
循环中删除元素,非安全),当然我们也可以在 for 循环前删除数据再遍历也是线程安全的。
【问】为什么在同时进行List的遍历和删除操作时会抛出 ConcurrentModificationException(索引值modCount未修正),以及Iterator、正常for循环为什么可以解决这个问题
Note:- foreach在遍历时,其实使用的是iterator的
hasnext()
和next()
,只不过调用next()
时会调用checkForComodification()
,用于比较modCount
和expectedModCount
是否相等,如果不等则抛出java.util.ConcurrentModificationException
异常。而在调用remove(obj)
时,会调用fastremove
自增modCount
,从而导致不一致。 - 如果在正常for循环中
remove(index)
或者在iterator中remove()
则不会出现异常;Iterator的remove()
每次删除一个元素,都会将modCount
的值重新赋值给expectedModCount
,这样2个变量就相等了,不会触发java.util.ConcurrentModificationException
异常。
- foreach在遍历时,其实使用的是iterator的
【问】ConcurrentHashMap 和 Hashtable 的区别?(
CncurrentHashMap
在java1.7
使用分段锁,即将数组分段 + HashEntry 数组 + 链表处理,对不同段加锁;而java1.8
之后使用Node节点 + 链表/红黑树实现,对数组上不同节点加锁,CAS + synchronized实现;而HashTable
基于数组+链表,使用synchronized
控制线程安全,现不维护(deprecated))
【问】ConcurrentHashMap 线程安全的具体实现方式/底层具体实现?(ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 锁定当前链表或红黑二叉树的首节点,其余和
hashmap
类似)【问】Collections 工具类 的 排序操作(
reverse
,suffle
,和sort
等)【问】Collections 工具类 的 查找,替换操作(
binarySearch
,max
,replaceAll
等)【问】Collections 工具类 的 同步控制(
synchronizedCollection(Collection<T> c)
和synchronizedList(List<T> list)
等,但不建议使用效率非常低)【问】迭代器 Iterator 是什么?(用到了迭代器设计模式,内置增/删/遍历等方法,专门处理集合中的元素)
【问】Iterator 和 ListIterator 有什么区别?(
ListIterator
继承Iterator
,扩展其功能,多了add
、hasPrevious()
、previous()
等方法;Iterator
可以迭代所有集合;ListIterator
只能用于List
及其子类)【问】怎么确保一个集合不能被修改?(使用
Collections
包下的unmodifiableMap(Map)
,Collections.unmodifiableList(List)
和
Collections.unmodifiableSet(Set)
方法)【问】栈,队列 和 deque的具体操作和含义(栈进和取为:push pop;队列的进和取为:offer poll,push和offer都是尾部进),参考关于push()、pop()、offer()、poll()
equals 和 hashcode,可参考equals和hashCode
Java的Set接口有哪些,可参考 JAVA 集合类(包括Collection和Map接口),Java中的Map List Set等集合类
Java的Map接口,可参考TreeMap原理实现及常用方法,深入理解HashMap和TreeMap的区别
Arrays和List的使用,可参考Arrays.asList()方法详解(array是final不可变类型,没有add,remove)
关于ListIterator描述(ListIterator只适用于List及其子类的遍历和修改),可参考ListIterator,ListIterator和Iterator详解与辨析(Iterator应用于Set,Map等)
Note:- 在同时进行集合遍历和删除操作时,要使用
iter.next()
和iter.remove()
,如果使用iter.next()
和list.remove(a)
会抛出java.util.ConcurrentModificationException
异常
- 在同时进行集合遍历和删除操作时,要使用
Set的存储顺序:LinkedHashSet / SortedSet / TreeSet,可参考SortedSet接口解析(只有TreeSet一个实现),Java中的Map List Set等集合类,深度剖析LinkedHashSet(内部使用LinkedHashMap来维持插入顺序,不维护访问顺序)
优先级队列: PriorityQueue,可参考一文掌握Comparator的数十种用法(实现Comparable接口/提供Comparator对象),Comparator 升序、降序(> ;return 1;升序)
关于HashMap的描述,可参考哈希理解、哈希碰撞(hash冲突)及处理(装填因子α表示hash表的饱和度),解决Hash冲突四种方法(开链再溢),Hash表的介绍以及哈希冲突以及解决方法
关于Collections的描述,可参考Collections.unmodifiableCollection 该集合不可修改,Collections.synchronizedCollection 该集合线程安全
List通过stream计算按部门进行分组,参考java 8 lambda表达式list操作分组 / 过滤 / 求和 / 最值 / 排序 / 去重,Java中map.getOrDefault()方法的使用(如果值存在则不使用默认值)
更多内容整理 参考
- 《我要进大厂》- Java集合夺命连环14问,你能坚持到第几问?(集合概述 | List | Set | Queue)
- 《我要进大厂》- Java集合夺命连环13问,你能坚持到第几问?(Map | Collections)
- Java面试题总结(分类版)
- 10万字208道Java经典面试题总结(附答案)
七、IO(传统IO,即BIO)
- 【问】什么是序列化?什么是反序列化?(序列化指数据结构或对象转换成二进制字节流的过程;反序列化相反)
- 【问】Java 序列化中如果有些字段不想进行序列化,怎么办?(
transient
关键字修饰变量不被持久化,反序列化时则置为类型的默认值,比如int为0;static
修饰的变量不属于对象,也不被持久化;transient
无法修饰类或方法) - 【问】获取用键盘输入常用的两种方法(
Scanner
用nextline()
/BufferReader
用readline()
) - 【问】Java 中 IO 流分为几种?(字节流/字符流;输入流/输出流;),参考Java IO输入输出流
Note:常见使用的字符流类包括:
- Reader:InputStreamReader(子类FileReader),BufferedReader(有缓冲区)
- Writer:outputStreamWriter(子类FileWriter),BufferedReader(有缓冲区)
常见使用的字节流类包括:
- InputStream:FileInputStream,BufferedInputStream(有缓冲区)
- OutputStream:FileOutputStream,BufferedOutputStream(有缓冲区)
其中
Reader
,Writer
,InputStream
,OutputStream
是实现了Closeable
接口的抽象类,而后者分别是前者的实现类。
IO流按操作对象分类结构图
- 1.Memory 1)从/向内存数组读写数据: CharArrayReader、 CharArrayWriter、ByteArrayInputStream、ByteArrayOutputStream
2)从/向内存字符串读写数据 StringReader、StringWriter、StringBufferInputStream - 2.Pipe管道 实现管道的输入和输出(进程间通信): PipedReader、PipedWriter、PipedInputStream、PipedOutputStream
- 3.File 文件流:对文件进行读、写操作 :FileReader、FileWriter、FileInputStream、FileOutputStream
- 4.ObjectSerialization 对象输入、输出 :ObjectInputStream、ObjectOutputStream
- 5.DataConversion数据流 按基本数据类型读、写(处理的数据是Java的基本类型(如布尔型,字节,整数和浮点数)):DataInputStream、DataOutputStream
- 6.Printing 包含方便的打印方法 :PrintWriter、PrintStream
- 7.Buffering缓冲 在读入或写出时,对数据进行缓存,以减少I/O的次数:BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream
- 8.Filtering 滤流,在数据进行读或写时进行过滤:FilterReader、FilterWriter、FilterInputStream、FilterOutputStream过
- 9.Concatenation合并输入 把多个输入流连接成一个输入流 :SequenceInputStream
- 10.Counting计数 在读入数据时对行记数 :LineNumberReader、LineNumberInputStream
- 11.Peeking Ahead 通过缓存机制,进行预读 :PushbackReader、PushbackInputStream
- 12.Converting between Bytes and Characters 按照一定的编码/解码标准将字节流转换为字符流,或进行反向转换(Stream到Reader,Writer的转换类):InputStreamReader、OutputStreamWriter
- 1.Memory 1)从/向内存数组读写数据: CharArrayReader、 CharArrayWriter、ByteArrayInputStream、ByteArrayOutputStream
- 【问】既然有了字节流,为什么还要有字符流?(换句话说,字节为什么要转化成字符)
Note:- 字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。
- 如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好;
- 【问】Files的常用方法都有哪些?(
createFile
,createDirectory
,write
,read
,copy
等) - 字节流类有哪些,可参考Java IO输入输出流
- RandomAccessFile随机流,可参考随机流(RandomAccessFile)的使用介绍
- Input字节流/字符流的正确使用(read返回int,readLine返回字符串),可参考字节流和字符流详解,Java输入流
read()
和readline()
方法对比分析 - FileRead - 按行读取文件,可参考JAVA基础知识之BufferedReader流(默认一次性可以读8K个字符)
- Object Serializable - 序列化和反序列化(注意强制类型转换),可参考java序列化详解
- ZipOutputStream的使用(将文件写入到压缩包中),ZipOutputStream的使用(注意关闭流),可参考JAVA压缩流(ZipOutputStream)的简单使用
- ZipInputStream的使用(解压压缩包),可参考ZipInputStream类
- GZIPInputStream的使用(解压流),可参考Java使用GZIP进行String字符串压缩和解压缩
更多内容整理 参考 《我要进大厂》- Java基础夺命连环9问,你能坚持到第几问?(反射 | 注解 | IO )
八、数据库连接
- JDBC概述,可参考学习JDBC这一篇就够了
- JDBC四种驱动类型,可参考JDBC的四种驱动类型(JDBC-ODBC等)
- JDBC CRUD,可参考Java中JDBC的使用详解
- JDBC Transaction事务出错回滚(注意要先将事务设置为手动提交),可参考学习JDBC这一篇就够了
九、NIO
- 【问】BIO、NIO、AIO 有什么区别?,参考BIO、NIO、NIO2的整体理解
Note:- 同步阻塞IO(BIO):传统的
java.io
包,它基于流模型实现,提供了我们最熟知的一些IO功能,比如File抽 象、输入输出流等,交互方式是同步、阻塞的方式。也就是说,在读取输入流或者写入输出流 时,在读、写动作完成之前,线程会一直阻塞在那里,比如客户端向服务器发送请求,客户端线程需要阻塞,一直等待服务器的响应。 - 同步非阻塞IO(NIO):在
Java 1.4
中引入了 NIO 框架(java.nio
包),提供了Channel
、Selector
、Buffer
等 新的抽象,可以构建多路复用的、同步非阻塞IO程序,同时提供了更接近操作系统底层的高性能数据操作方式。
Buffer
:高效的数据容器,除了布尔类型,所有原始数据类型都有相应的Buffer实现。Channel
是操作系统底层的 一种抽象,可以通过DMA(直接内存访问)实现;Selector
是NIO实现多路复用的基础,它提供了一种高效的机制,可以检测到注册在Selector
上的多个Channel
中,是否有Channel处于就绪状态 ,进而实现了单线程对多 Channel的高效管理。
Selector
同样是基于底层操作系统机制,不同模式不同版本都存在区別,比如linux使用epoll
,windows通过iocp
实现多路复用。
- 异步非阻塞IO(AIO或NIO2):
- 在Java 7中,NIO有了进一步改进,也就是NIO 2,引入了异步非阻塞方式,也
有很多人叫它AIO (Asynchronous IO)。异步IO操作基于事件和回调的机制,可以简
单理解为,应用操作直接返回,而不会阻塞那里(关于应用的
thread1
不会一直等待Selector
,而是通过创建另一个thread2
来监听selector
,然后等待thread2
的返回结果,如果thread1
执行完还没等到thread2
的结果,则会阻塞),当后台处理完成,操作系统会通知相应线程进行后续工作。 - NIO2利用事件和回调,处理 Accept、Read等操作。
- 异步回调在java中可以通过实现
Callable
接口,执行FutureTask
任务来实现:参考Java多线程回调接口Callable,Java多线程编程:Callable、Future和FutureTask浅析(多线程编程之四)Java 5
引入了多线程编程的一个新方法,不需要直接new Thread ()
创建一个新的线程。只要创建一个ExecutorService
的线程池,并将实现了 Callable 接口的任务(task)提交到线程池,就能得到带有回调结果的Future
对象,通过操作Future
得到结果。FutureTask
除了实现了Future
接口外还实现了Runnable
接口,FutureTask
可以直接提交给Executor
执行,也可以调用线程直接执行(FutureTask.run()
);在FutureTask
计算完成时才能获取到结果,如果计算尚未完成,调用FutureTask
的线程会阻塞get
方法。
- 在Java 7中,NIO有了进一步改进,也就是NIO 2,引入了异步非阻塞方式,也
有很多人叫它AIO (Asynchronous IO)。异步IO操作基于事件和回调的机制,可以简
单理解为,应用操作直接返回,而不会阻塞那里(关于应用的
- 同步阻塞IO(BIO):传统的
- NIO概述(java1.4),可参考什么是NIO?NIO的原理是什么机制?,NIO原理详解,BIO / NIO的整体理解
- NIO的buffer类型(涵盖8大基本类型中的7个),可参考NIO原理详解
- NIO中buffer的使用,可参考NIO原理详解
- NIO中Channel的使用,可参考NIO原理详解,NIO Channel详解,Java NIO三组件:Selector/Channel实现原理解析,Java NIO之Channel(IO通道是DMA方式的发展)
- NIO中Channel的类型,可参考NIO Channel详解,Java NIO三组件:Selector/Channel实现原理解析
- NIO Path的使用,NIO Files的使用,可参考NIO学习-05(Path/Paths/Files)
十、网络编程
- UDP编程,可参考Java UDP通信:DatagramPacket与DatagramSocket 详解,DatagramSocket 类与 DatagramPacket 类
- TCP编程,可参考Socket和DatagramSocket的区别,Java网络编程 - TCP通信
- http请求使用代理
- HTTPClient的使用,可参考HttpClient详细使用示例,httpclient架构原理介绍 & 连接池详解
- WebSocket概述,可参考WebSocket实现实时通信
十一、类型信息(反射 和 动态代理)
- 【问】何为反射?(在运行时通过对象或字符串,获取一个类的变量或方法信息,进而创建该类的新对象),可参考类加载器(3个) & 类加载机制 & 反射
Note:- 反射的概念:反射是指在运行时去获取一个类的变量和方法信息,然后通过获取到的信息来创建对象,调用方法的一种机制。
- 获取
Class
类对象:- 通过类的
class
属性、或者运行时的对象、或者字符串来获取一个类对象
- 通过类的
// 方法1:使用类的class属性来获取该类对应的Class对象Class<Student> c1 = Student.class;System.out.println(c1);// 方法2:调用对象的getClass()方法,返回该对象所属类对应的Class对象Class<? extends Student> c2 = new Student().getClass();System.out.println(c2);// 方法3:使用Class类中的静态方法forName(String className)Class<?> c3 = Class.forName("Student");System.out.println(c3);
- 通过
Class
类对象获取构造器:- 通过
getConstructors()
返回一个包含Constructor
对象的数组,不包含私有构造; - 通过
getConstructor(Class<?>… parameterTypes)
返回一个指定的Constructor
对象,不包含私有构造; - 通过
getDeclaredConstructors()
返回一个包含Constructor
对象的数组,包含私有构造; Constructor
对象通过newInstance()
创建对象;
Class<Student> c = Student.class; // 获取所有公开的构造方法 Constructor<?>[] constructors = c.getConstructors(); for (Constructor<?> constructor : constructors) {System.out.println(constructor); } System.out.println("--------------------");// 获取指定参数且公开的构造方法 Constructor<Student> constructor = c.getConstructor(String.class, int.class, String.class); System.out.println(constructor); System.out.println("--------------------");// 获取所有权限的构造方法 Constructor<?>[] declaredConstructors = c.getDeclaredConstructors(); for (Constructor<?> declaredConstructor : declaredConstructors) {System.out.println(declaredConstructor); }
- 通过
- 通过
Class
类对象获取成员变量:- 通过
getFields()
返回一个包含Field
对象的数组,不包含私有变量 getField(String name)
返回一个指定的Field
对象,不包含私有变量getDeclaredField(String name)
返回一个指定的Field
对象,包含私有变量Field
对象通过setAccessible(true)
可以无视Java语言访问检查当前使用的反射对象,通过set
实现成员变量的设置;
- 通过
- 通过
Class
类对象获取成员方法:- 通过
getMethods()
返回一个包含Method
对象的数组,不包含私有成员方法 - 通过
getMethod(String name, Class<?>… parameterTypes)
返回一个包含Method
对象,不包含私有成员方法; Method
对象通过setAccessible(true)
可以无视Java语言访问检查当前使用的反射对象,通过invoke()
来执行成员方法;
- 通过
- 综合案例:
public class Main {public static void main(String[] args) throws Exception {// 获取学生类类对象Class<Student> c = Student.class;// 通过无参构造创建Constructor<Student> constructor = c.getConstructor();Student newStudent = constructor.newInstance();System.out.println(newStudent);System.out.println("--------------------");// 反射设置成员变量Field name = c.getDeclaredField("name");name.setAccessible(true);name.set(newStudent, "张三丰");Field age = c.getDeclaredField("age");age.setAccessible(true);age.set(newStudent, 55);Field address = c.getDeclaredField("address");address.setAccessible(true);address.set(newStudent, "武当山");System.out.println(newStudent);System.out.println("--------------------");// 反射执行成员方法Method getName = c.getDeclaredMethod("getName");getName.setAccessible(true);getName.invoke(newStudent);Method setAge = c.getDeclaredMethod("setAge", int.class);setAge.setAccessible(true);setAge.invoke(newStudent, 60);System.out.println(newStudent);} }
- 【问】反射机制优缺点?
Note:- 优点:增加代码的灵活性
- 缺点:增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过对于框架来说实际是影响不大的。Java Reflection: Why is it so slow?
- 【问】反射的应用场景
Note:- 像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。
比如下面是通过 JDK 实现动态代理的示例代码,先通过java.lang.reflect.Proxy
的newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
创建代理对象(该方法会返回实现了指定接口的代理类实例),其中代理对象在执行方法时就使用了反射类Method
来调用指定的方法。 - 如下代码中,为
UserDao
创建了一个代理对象,该代理对象既包含了UserDao
的类信息也包含了接口信息,代理对象在调用UserDao
原对象方法(add()
)时,会自动调用MyInvocationHandler
对象的invoke()
,进而调用原对象(target
)的add()
方法,而在MyInvocationHandler
对象的invoke()
中可以增加其他逻辑实现功能增强(下面会说明为什么会自动调用handler
对象的invoke()
)。import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy;interface UserDao {public abstract void add();public abstract void delete();public abstract void update();public abstract void find(); }class UserDaoImpl implements UserDao {@Overridepublic void add() {System.out.println("添加功能");}@Overridepublic void delete() {System.out.println("删除功能");}@Overridepublic void update() {System.out.println("修改功能");}@Overridepublic void find() {System.out.println("查找功能");} }class MyInvocationHandler implements InvocationHandler {private Object target;public MyInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("权限校验");Object result = method.invoke(target, args);System.out.println("日志记录");System.out.println();return result;} }public class Main {public static void main(String[] args) throws Exception {UserDao ud1 = new UserDaoImpl();ud1.add();ud1.delete();ud1.update();ud1.find();System.out.println("----------");UserDao ud2 = new UserDaoImpl();MyInvocationHandler handler = new MyInvocationHandler(ud2);UserDao ud2Proxy = (UserDao) Proxy.newProxyInstance(ud2.getClass().getClassLoader(), ud2.getClass().getInterfaces(), handler);ud2Proxy.add();ud2Proxy.delete();ud2Proxy.update();ud2Proxy.find();} }
- 另外,像 Java 中的一大利器 注解 的实现也用到了反射。在使用
Spring
的时候 ,一个@Component
注解就声明了一个类为Spring Bean
(IOC
容器最基本的技术就是“反射(Reflection)”编程),通过一个@Value
注解就读取到配置文件中的值,其实背后是基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。
- 像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。
- 【问】怎么实现动态代理?
Note:参考上面的动态代理代码- 在理解
newProxyInstance(ud2.getClass().getClassLoader()
时,可以对比反射中的newInstance
,容易将newProxyInstance
理解成通过constructor
对象反射创建了代理对象。 - 第一步:在底层代码
Class<?> cl = getProxyClass0(loader, intfs);
中,通过传入的加载的类loader
和接口intfs
来构建出相应的代理类,这样代理类继承了UserDemoImpl
类、实现了UserDao
接口,拥有add()
。 - 第二步:代理类的构造器为
final Constructor<?> cons = cl.getConstructor(constructorParams);
,传入的参数是一个数组即{InvocationHandler.class}
,因此handler
对象作为代理对象的成员属性。 - 第三步:在调用代理对象的
add()
时,会先调用handler
对象的invoke()
,而handler
对象中存放着原始UserDao
对象target
,在执行target
的add()
时会通过反射来调用。
- 在理解
- 【问】动态代理是什么?有哪些应用?,可参考JDK 动态代理(
AOP
)使用及实现原理分析 - 对象类型判断(obj.getClass()的使用)
- 关于Class对象的描述,可参考类加载器(3个) & 类加载机制 & 反射,Java 中的Class.getClassLoader(委托机制)
- 关于Instanceof的描述(注意继承关系),Instanceof 和 isInstance的区别,可参考instanceof关键字详解(如果无法强转,则抛ClassCastException)
- 关于反射的描述,反射例子(假设没有重写equals方法),可参考类加载器(3个) & 类加载机制 & 反射
- 关于动态代理的描述,动态代理例子,可参考JDK动态代理和cglib动态代理以及区别,JDK 动态代理(AOP)使用及实现原理分析
- Java的空(null)对象(没有赋值,但在内存中存在)
更多内容整理 参考 《我要进大厂》- Java基础夺命连环9问,你能坚持到第几问?(反射 | 注解 | IO )
十二、注解
- 【问】何为注解?(注解本质是 继承Annotation接口的接口;反射注解通过包扫描、反射、动态代理来实现)),参考java注解原理 :反射 & 动态代理
Note:- Java 中有以下几个元注解:
@Target
:注解的作用目标
@Retention
:注解的生命周期,与JVM有关
@Documented
:注解是否应当被包含在 JavaDoc(API) 文档中
@Inherited
:是否允许子类继承该注解 - Java 3大内置注解:
@Override
,@Deprecated
,@SuppressWarnings
Class
类通过实现AnnotatedElement
接口来反射注解:Java反射机制解析注解主要是通过java.lang.reflect
包下的提供的AnnotatedElement
接口,Class<T>
实现了该接口定义的方法,返回本元素/所有的注解(Annotation接口)。- 反射注解的工作原理:
- 1)首先,我们通过键值对的形式可以为注解属性赋值,像这样:
@Hello(value = "hello")
。 - 2)接着,你用注解修饰某个元素,编译器将在编译期扫描每个类或者方法上的注解,会做一个基本的检查,你的这个注解是否允许作用在当前位置,最后会将注解信息写入元素的属性表。
- 3)然后,当你进行反射的时候,虚拟机将所有生命周期在
RUNTIME
的注解取出来放到一个 map 中,并创建一个AnnotationInvocationHandler
实例,把这个map
传递给它。 - 4)最后,虚拟机将采用 JDK 动态代理机制生成一个目标注解的代理类,并初始化好处理器。
- 1)首先,我们通过键值对的形式可以为注解属性赋值,像这样:
- Java 中有以下几个元注解:
- Java的Annotation注解,可参考十分钟深度学习Java注解
Note:RetentionPolicy.SOURCE
注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。 - 注解处理器(APT,javac自带的一个工具),可参考【Annotation】Processing-Tool详解
- junit中常见注解(@Before即每个@Test测试方法都会运行一次),可参考JUnit详解
十三、并发编程(Thread,ThreadPool,Synchronized,AQS,ThreadLocal)
参考java线程,多线程和线程池,并发编程面试题(2020最新版)
【问】如何让 Java 的线程彼此同步?(
volatile
,synchronized
,JUC(java.util.concurrent
)工具包等)【问】创建线程的四种方法?(主要分两大类:Thread类和ExecutorService接口,而Thread有3种实现方法),可参考多线程 - Thread、Executor
Note:小总结:
- 创建一个线程的核心是
Thread
和Runnable
,如果需要用到线程返回值(异步调用)则考虑Callable
; FutureTask
为了创建线程需要实现Runnable
,为了实现异步调用则需要实现Callable
;- 实现
Runnable
和Callable
接口只是对线程业务逻辑的定义,创建线程仍然需要使用new Thread(Runnable)
来初始化线程对象,接着通过start()
来提醒JVM
该线程准备运行,JVM
才会去调用run()
;
- 创建一个线程的核心是
对
Thread
类进行派生并覆盖run
方法(Thread t1 = new MyThread(); t1.start();
)。实现
Runnable
接口,重写run
方法(Thread t2 = new Thread(Runnable); t2.start();
);当你打算多重继承时,优先选择实现Runnable
比实例化Thread
的派生类更灵活有效(直接将A实体类变成一个Runnable
实例,即可以通过A类对象创建一个线程对象)。class ThreadType implements Runnable{ public void run(){ …… } } Runnable rb = new ThreadType (); Thread td = new Thread(rb); //通过 Runnable 的实例创建一个线程对象 td.start();
实现
Callable
接口:利用Callable
实现类创建FutureTask
对象,FutureTask
实现了Future
和Runnable
接口,因此可利用new Thread(Runnable)
将任务装配到线程中,进而创建线程;通过futuretask.get()
来获取线程的返回值。class MyCallable implements Callable<Integer> {@Overridepublic Integer call() {System.out.println(Thread.currentThread().getName() + "Callable call()方法");return 1;} }public class Main {public static void main(String[] args) {//2.以myCallable为参数创建FutureTask对象(call返回值为Integer)FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());//3.将FutureTask作为参数创建Thread对象Thread thread = new Thread(futureTask);thread.start(); //4.执行try {Thread.sleep(1000);//5.通过futuretask可以得到myCallable的call()的运行结果: futuretask.get();System.out.println("MyCallable:" + futureTask.get());} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " main方法执行完成");}}
ExecutorService
是线程池的抽象接口,使用Executors
工具类提供创建线程池的工厂方法,创建简化版的线程池,也可以使用ThreadPoolExecutor
创建旗舰版的线程池。下面分别给出Executors
和ExecutorService
,ThreadPoolExecutor
和ExecutorService
的继承关系。ThreadPoolExecutor
与Executor
,ExecutorService
的关系:
Executor
是一个顶层接口,在它里面只声明了一个方法void execute(Runnable)
ExecutorService
接口继承了Executor
接口,并声明了一些方法:submit
、invokeAll
、invokeAny
以及shutDown
等,是对线程池的抽象;- 抽象类
AbstractExecutorService
实现了ExecutorService
接口,基本实现了ExecutorService
中声明的所有方法(实现submit()
用于任务的提交); ThreadPoolExecutor
继承了类AbstractExecutorService
,实现execute()
用于任务的提交。
Executors
只继承于Object
类,这个包中定义的Executor、ExecutorService、ScheduledExecutorService、ThreadFactory和Callable类的工厂方法和实例方法。这个类支持以下类型的方法:
Executors
创建的SingleThreadExecutor
,CachedThreadPool
,FixedThreadPoo
和ScheduledThreadPool
均通过ThreadPoolExecutor
创建。
关于
Executors
和ThreadPoolExecutor
的详细使用,以下会详细说明。
【问】Thread和Runnable的区别?(Thread实现了Runnable,Thread是对线程的唯一抽象)
Note:Thread
才是 Java 里对线程的唯一抽象(thread.start()
告诉JVM
启动线程,JVM
才会调用run()
方法执行任务),Runnable
只是对任务(业务逻辑)的抽象(自定义的MyThread
在重写run()
时其实是对Runnable.run()
的重写)。Thread
可以接受任意一个Runnable
的实例并执行。Thread
自身实现了Runnable
接口- 一般情况下使用
Runnable
方式创建线程。除非你需要重写Thread
类除了run()
方法外的其他方法来自定义线程,否则不建议使用继承Thread
的方式来创建。
【问】Future和FutureTask的区别?
Note:Future
只是一个接口,可以理解是返回结果<T>
的封装类,用于同步/异步返回结果<T>
;FutureTask
通过实现了RunnableFuture
,进而实现了Future
和Runnable
两接口;
【问】为什么要使用线程池?(降低创建和销毁线程对象的次数,因此有了"池化资源"技术比如数据库连接池,线程池)
Note:线程池的作用- 降低资源消耗(线程创建和销毁需要完成用户态和核心态的切换,有开销);
- 提高响应速度,当任务到达时,任务可以不需要等到线程创建就可以立即执行
- 使用线程池可以对线程进行统一分配、调优和监控。
【问】Java 中线程池的四个基本组成部分(
SingleThreadExecutor
、CachedThreadPool
、FixedThreadPool
和ThreadPoolExecutor
)
Note:- 1)线程池管理器(
ThreadPool
):用于创建并管理线程池。包含 创建线程池,销毁线程池,加入新任务(线程池初始化时,poolsize
为0); - 2)工作线程(
PoolWorker
):线程池中线程,在没有任务时处于等待状态。能够循环的运行任务; - 3)任务接口(
Task
):每一个任务必须实现的接口,以供工作线程调度任务的运行。它主要规定了任务的入口,任务运行完后的收尾工作,任务的运行状态等(是直接交给工作线程执行,还是放入BlockingQueue
)。 - 4)任务队列(
taskQueue
):用于存放没有处理的任务。提供一种缓冲机制。
- 1)线程池管理器(
【问】ThreadPoolExecutor 有几个核心构造参数?(7大参数,排队策略,拒绝策略),参考带你了解下SynchronousQueue
Note:ThreadPoolExecutor
提供了4个构造器,其余3个都是对下面这个构造器的调用// Java线程池的完整构造函数 public ThreadPoolExecutor(int corePoolSize, // 线程池长期维持的最小线程数,即使线程处于Idle状态,也不会回收。int maximumPoolSize, // 线程数的上限long keepAliveTime, // 线程最大生命周期。TimeUnit unit, //时间单位 BlockingQueue<Runnable> workQueue, //任务队列。当线程池中的线程都处于运行状态,而此时任务数量继续增加,则需要一个容器来容纳这些任务,这就是任务队列。ThreadFactory threadFactory, // 线程工厂。定义如何启动一个线程,可以设置线程名称,并且可以确认是否是后台线程等。RejectedExecutionHandler handler // 拒绝任务处理器。由于超出线程数量和队列容量而对继续增加的任务进行处理的程序。 )
- corePoolSize(阈值1): 默认情况下,在创建了线程池后,线程池中的线程数(
poolsize
为0),当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目(poolsize
)达到corePoolSize
后,就会把到达的任务放到缓存队列当中; - maximumPoolSize(阈值2):线程池最大线程数;
- keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止(销毁线程释放资源)。默认情况下,只有当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止。
- unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性
- workQueue:一个阻塞队列(
BlockingQueue<Runnable>
),用来存储等待执行的任务,一般来说,这里的阻塞队列有以下几种选择:ArrayBlockingQueue; LinkedBlockingQueue; SynchronousQueue; 阻塞队列提供了可阻塞的put
和take
方法,它们与同步队列offer
和poll
是等价的。如果队列满则put
阻塞,如果队列是空的则take
方法阻塞。- 1)
ArrayBlockingQueue
: 有界的数组队列(长度至少为1) - 2)
LinkedBlockingQueue
: 可支持有界/无界的队列,使用链表实现(默认大小为Integer.MAX_VALUE
) - 3)
PriorityBlockingQueue
: 优先队列,可以针对任务排序(无界) - 4)
SynchronousQueue
: 同步队列长度为1(与其说是队列,不如说是个锁),不能peek()
查看队列元素;和Array有点区别就是:client thread提交到block queue会是一个阻塞过程,直到有一个worker thread连接上来poll task。
- 1)
- threadFactory:线程工厂,主要用来创建线程;
- handler:表示当拒绝处理任务时的策略,有以下四种取值(abort,discard,caller):
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
- corePoolSize(阈值1): 默认情况下,在创建了线程池后,线程池中的线程数(
【问】ThreadPoolExecutor逻辑结构? 或者说ThreadPoolExecutor线程池的执行流程(
corePoolSize
和maximumPoolsize
双阈值控制poolsize
)
Note:- 第一步:初始的
poolSize < corePoolSize
,提交的Runnable
任务,会直接通过new Thread(Runnable)
,创建并执行线程。 - 第二步:当提交的任务数超过了
corePoolSize
,就进入了第二步操作。会将当前的Runnable
提交到一个block queue
中(使用不同的阻塞队列实现排队策略)。 - 第三步:如果
block queue
是个有界队列,当队列满了之后就进入了第三步。如果poolSize < maximumPoolsize
时,会尝试new Thread(Runnable)
进行救急处理,立马执行对应的Runnable
任务。 - 第四步:如果第三步救急方案也无法处理了(
poolSize > maximumPoolsize
),就会走到第四步执行reject
操作(使用任务拒绝策略,abort / discard / caller)。
- 第一步:初始的
【问】ThreadPoolExecutor的任务排队策略和拒绝策略(见前2问解析)
【问】ThreadPoolExecutor类的execute()和submit()的区别?(
submit()
会调用的execute()
)
Note:- execute()方法: 实际上是
Executor
中声明的方法,在ThreadPoolExecutor
进行了实现,通过这个方法ThreadPoolExecutor
可以向线程池提交一个任务,交由线程池去执行。 - submit()方法: 是在
ExecutorService
中声明的方法,在AbstractExecutorService
就已经有了具体的实现,在ThreadPoolExecutor
中并没有对其进行重写,用来向线程池提交任务;submit()
会调用的execute()
,只不过它利用了Future
来获取任务执行结果。 shutdown()
和shutdownNow()
是用来关闭线程池的。
- execute()方法: 实际上是
【问】ThreadPoolExecutor线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?(利用
new Thread(Runnable)
创建 + 排队策略 + 拒绝策略;双阈值控制poolsize,见上几问解析)
Note:- 默认情况下,在使用
ThreadPoolExecutor
创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。 - 在实际中如果需要,可以通过以下两个方法,在线程池创建之后立即创建线程:
prestartCoreThread():初始化一个核心线程; prestartAllCoreThreads():初始化所有核心线程
- 默认情况下,在使用
【问】如何在 Java 线程池中提交线程?(
execute()
或submit()
;submit()
有返回结果,见前2问解析)【问】ThreadPoolExecutor线程池的关闭?(
ThreadPoolExecutor
提供shutdown()
和shutdownNow()
用于线程池的关闭)
Note:shutdown()
:不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务shutdownNow()
:立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。
【问】ThreadPoolExecutor线程池容量如何动态调整?(
setCorePoolSize(
)和setMaximumPoolSize()
)【问】既然提到可以通过配置不同参数创建出不同的线程池,那么 Java 中默认实现好的线程池又有哪些呢?请比较它们的异同?(
Executors
类提供的创建线程的工厂方法)
Note:Executors
是一个提供了一系列用于创建线程池的工厂方法的类,线程池都通过ThreadPoolExecutor
来创建,并返回一个ExecutorService
接口。Executors.newSingleThreadExecutor()
:只有一个线程的线程池,该线程永不超时(没有keepAliveTime
),当有多个任务需要处理时,会将它们放置到一个无界阻塞队列中逐个处理;Executors.newCachedThreadPool()
:建立了一个线程池,而且线程数量是没有限制的(当然,不能超过Integer的最大值),新增一个任务即有一个线程处理(复用或new
),线程空闲时长超过keepAliveTime
则终止。Executors.newFixedThreadPool()
:固定线程数量的线程池,初始化线程的最大数量,若任务数超过线程的处理能力,则建立阻塞队列容纳多余的任务。
- 阿里巴巴编码规范里面提到:线程池最好不要使用
Executors
去创建(线程池简化版),而是通过ThreadPoolExecutor
(线程池旗舰版)的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险,Executors
各个方法的弊端:- 1)
newFixedThreadPool
和newSingleThreadExecutor
:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM
。 - 2)
newCachedThreadPool
和newScheduledThreadPool
:主要问题是线程数最大数是Integer.MAX_VALUE
,可能会创建数量非常多的线程,甚至OOM
。
- 1)
【问】Synchronized 用过吗,其原理是什么?(特性:可重入性,不可中断性;原理:在编译成字节码时,会通过字节码命令或者标志位判断告诉JVM,要访问的对象的
monitor
(管程)是否被其它线程占用),可参考深入理解Java并发之synchronized实现原理
Note:Synchronized
的3种应用方式:- 1)修饰实例方法:为实例对象加锁(实例对象锁,一个对象配一个
monitor
) - 2)修饰静态方法:为当前类对象加锁(class对象锁,一个对象配一个
monitor
) - 3)修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
- 1)修饰实例方法:为实例对象加锁(实例对象锁,一个对象配一个
Synchronized
的实现原理:在JVM中,对象在内存中的布局分为三块区域:对象头(
Mark word,klass word
)、实例数据和对齐填充;重量级锁(即Java1.6之前未引入轻量级锁和偏向锁概念的
synchronized
的对象锁),锁标识位为10,其中指针指向的是monitor
对象(也称为管程或监视器锁)的起始地址。每个对象(Object
)都存在着一个monitor
与之关联,对象与其monitor
之间的关系有存在多种实现方式:monitor
可以与对象一起创建销毁- 当线程试图获取对象锁(
synchronized
)时自动生成monitor
,但当一个monitor
被某个线程持有后,它便处于锁定状态。
在Java虚拟机(HotSpot)中,
monitor
是由ObjectMonitor
实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp
文件,C++实现的)monitor
对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized
锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait
等方法存在于顶级对象Object
中的原因。Java 虚拟机中的同步(
Synchronization
)基于进入和退出管程(Monitor
)对象实现, 无论是显式同步(有明确的monitorenter
和monitorexit
指令,即同步代码块)还是隐式同步(通过ACC_SYNCHRONIZED
标志实现,即同步方法)都是如此。在 Java 语言中,同步用的最多的地方可能是被synchronized
修饰的同步方法。同步方法并不是由monitorenter
和monitorexit
指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的ACC_SYNCHRONIZED
标志来隐式实现的。synchronized
代码块原理:(monitorenter
和monitorexit
是字节码命令,在JVM中能保证monitorenter
和monitorexit
配对执行,即获取到锁,执行完毕或异常后会释放锁;monitor.count = 0
获取锁成功;可重入monitor.count ++
;释放锁monitor.count = 0
)public class SyncCodeBlock {public int i;public void syncTask(){//同步代码库synchronized (this){i++;}} }
- 1)当执行
monitorenter
指令时,当前线程将试图获取objectref
(即对象锁) 所对应的monitor
的持有权,当objectref
的monitor
的进入计数器为 0,那线程可以成功取得monitor
,并将计数器值设置为 1,取锁成功。 - 2)如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时(访问当前实例对象的其它
synchronized
方法)计数器的值也会加 1。 - 3)倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即
monitorexit
指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有monitor
。 - 4)为了保证在方法异常完成时
monitorenter
和monitorexit
指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行monitorexit
指令。
- 1)当执行
synchronized
方法原理:通过方法表结构中的ACC_SYNCHRONIZED
标识(flag
)来判断monitor
是否被其它线程占用。
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的ACC_SYNCHRONIZED
访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的ACC_SYNCHRONIZED
访问标志是否被设置,如果设置了,执行线程将先持有monitor
(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor
。
Synchronized
的特性:- 可重入性:访问当前实例对象的其它
synchronized
方法(包括当前实例对象的父类的同步方法) - 不可中断性:对于线程的中断,需要在
Thread.run(){while(true){ if (this.isInterrupted()){break;} }}
中进行中断判断,否则在main
中通过thread.interrupt()
也无法得到响应(详细代码参考深入理解Java并发之synchronized实现原理)
而线程的中断操作对于正在等待获取的锁对象的synchronized
方法或者代码块并不起作用,也就是对于synchronized
来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效。
- 可重入性:访问当前实例对象的其它
【问】多线程中 synchronized 锁升级的原理是什么?,参考synchronized的偏向锁、轻量级锁和重量级锁,Synchronized偏向锁、轻量级锁、重量级锁详解,Java程序员装X必备词汇之Mark Word!,对象头由两部分组成:mark word和klass word
Note:对象头介绍: 对象头是堆中对象的头结构,它由两个部分组成:
mark word
和klass word
mark word
:
- mark word的大小为64bit,在对象的5种状态中(无锁,偏向锁,轻量级锁,重量级锁,GC标记),mark word的结构有所不同
- 在 无锁 状态时,mark word的前56bit存储对象的hashcode信息。(前25bit为未使用,后31bit存储hashcode);后三位001表示无锁状态。
- 在 偏向锁 状态时,mark word的前54bit存储获取锁的线程相关信息;后三位101表示偏向锁
- 在 轻量级锁 状态时,mark word的前62bit存储线程的栈的指针;后2位00表示轻量级锁
- 在 重量级锁 状态时,markword的前62bit存储一个monitor对象信息;后2位10表示重量级锁
可以通过JOL
查看对象在堆内存的存储布局(前8位object header
为mark word
,后4位object header
为指针压缩后的klass word
):
klass word
:- klassword 的32位或者64位代表元数据的指针。
- klassword的大小为64bit,如果开启了指针压缩,大小为32bit
其中
mark word
是后面学习并发编程,了解各种锁的基础。三种锁的比较(分别对应不同的并发场景,锁的升级是动态自适应的,可以动态适应不同场景):
- 偏向锁:适合对象没有竞争的场景,表示该对象目前属于某个线程所有,过段时间后偏向锁可能会偏向其他线程,主要通过比较
mark word
记录的线程ID
是否与第一次CAS时的一致来实现。只有存在竞争或者撤销次数达到阈值时才会解锁偏向锁,升级为轻量级锁。 - 轻量级锁:只能处理线程之间交替加锁(没有竞争)的场景(T1加锁A解锁A,T2加锁B解锁B,T1加锁B解锁B),通过查看
Mark Word
和lock Record
,以及synchronized
解决。 - 重量级锁:处理线程之间竞争互斥的场景,即JDK1.6之前的
synchronized
实现(最高级别的锁,更新Mark Word
状态,但不会操作lock Record
),底层原理为monitor
管程,阻塞队列(竞争的线程)和等待队列(wait()
被挂起)。
synchronized
锁的升级只能从低级(偏向锁)升级为高级(重量级锁),主要通过mark word
锁状态和lock record
锁记录(线程ID地址),来监控当前对象是否存在线程间的竞争关系。- 偏向锁:适合对象没有竞争的场景,表示该对象目前属于某个线程所有,过段时间后偏向锁可能会偏向其他线程,主要通过比较
偏向锁:不会主动进行解锁,出现竞争或撤销次数达到阈值时才会解锁(
101
→\rightarrow→001
),这样做的目的是下一次同一个线程来获取锁时,直接检查mark word
的锁记录就可以了。- JDK1.6之后默认使用偏向锁,即第一次使用CAS将线程的
ID
放置在访问对象的Mark word
中时,下一次如果发现访问的线程ID
仍然是自己,则不再使用CAS,该对象由该线程所有,偏向锁状态设置成功(101
);偏向锁会在当前线程的栈帧中创建锁记录(Lock Record
),使这个锁记录指向锁对象; - 如果发现该对象锁释放之后,通过锁记录查看到有其它线程指向锁对象(获得锁资源),则将该对象的对象头(
Mark word
)设置为无锁状态(001
),从偏向锁升级为轻量锁,即00
(并发情况中等) - 如果通过锁记录发现该对象锁还没被释放就有其它线程来抢占资源,则将该对象的对象头(
Mark word
)设置为无锁状态(001
),再从偏向锁升级为重量级锁,即10
(并发情况严重) - 调用
wait/notify
方法时,偏向锁直接升级为重量级锁(10
),因为只有重量级锁才有该方法。 - 关于偏向锁的批量重偏向:
如果一个类的大量对象被一个线程T1
执行了同步操作,也就是大量对象先偏向了T1
,T1同步结束后,另一个线程也将这些对象作为锁对象进行操作,会导致偏向锁重偏向的操作。 - 关于偏向锁的撤销:
- 在B线程获取偏向锁时,查看
mark word
的线程id
不是自己的,那么B线程就会向VM的线程队列发送一个撤销偏向锁的任务,VM线程会不断检测是否有任务要执行,当检测到这个任务后,就需要在安全点去执行(安全点时,JVM内的所有线程都会被阻塞,只有VM线程处于运行状态,它可以执行一些特殊的任务,如full gc
就是此时执行) - 待全局安全点(在这个时间点上没有正在运行的字节码)。它会首先暂停拥有偏向锁的线程,然后检测这个线程是否存活。如果不存活的话,那么就先将对象头设置为无锁状态,并偏向提交撤销锁的那个线程。
- 如果存活且存在竞争(线程交替加锁),那么就先将对象头设置为无锁状态,并升级为轻量级锁。
- 当偏向锁的撤销次数超过40次后,会直接升级为轻量级锁。
- 在B线程获取偏向锁时,查看
- JDK1.6之后默认使用偏向锁,即第一次使用CAS将线程的
轻量级锁:只能处理线程之间交替加锁的场景,通过
Mark Word
和lock Record
监控,通过synchronized
代码块(用法仍然是synchronized
,但没有用到monitor
)实现。- 轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。如果直接升级成重量级锁(解决互斥) 的话,是没有必要的。
- 轻量锁的操作步骤:
创建锁记录(
Lock Record
)对象存放线程ID
地址,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word
;让锁记录中
Object reference
指向锁对象,并尝试用 CAS 替换Object
的Mark Word
,将Mark Word
的值存入锁记录;如果 CAS 失败(内存快照A != 内存当前值V),有两种情况:
- 如果是其它线程已经持有了该
Object
的轻量级锁,这时表明有竞争,进入锁膨胀过程。 - 如果是自己执行了
synchronized
锁重入,那么再添加一条Lock Record
作为重入的计数。
- 如果是其它线程已经持有了该
当退出
synchronized
代码块(解锁时)如果有取值为null
的锁记录(线程地址引用已被当前线程替换,之前的引用被解除),表示有重入,这时重置锁记录,表示重入计数减一。当退出
synchronized
代码块(解锁时)锁记录的值不为null
,这时使用CAS
将Mark Word
的值(内存快照A )恢复给对象头。- 成功,则解锁成功。
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。
- 轻量级锁的作用:
- 轻量级锁并不提供线程的互斥性,它的指定条件是多个线程交替去获取;
- 偏向锁存在竞争的时候,会先撤销到无锁的状态,之后升级为轻量级锁,而轻量级锁升级时直接升级成重量级锁。
重量级锁:即JDK1.6以前的
synchronized
,底层使用Monitor
管程对象(看上面synchronized
底层原理),处于竞争的线程在等待队列或阻塞队列中。
- 刚开始 Monitor 中 Owner 为 null。
- 当
Thread-2
执行 synchronized(obj) 就会将Monitor
的所有者 Owner 置为Thread-2
,Monitor中只能有一个Owner。 - 在
Thread-2
上锁的过程中,如果Thread-3
,Thread-4
,Thread-5
也来执行 synchronized(obj),就会进入EntryList BLOCKED
。 Thread-2
执行完同步代码块的内容,然后唤醒EntryList
中等待的线程来竞争锁,竞争时是非公平的。- 图中
WaitSet
中的Thread-0
,Thread-1
是之前获得过锁,但条件不满足进入调用wait()
方法进入 WAITING 状态的线程。当调用notifyAll()
方法之后,WaitSet
中的Thread-0
,Thread-1
进入EntryList BLOCKED
。
【问】为什么代码会重排序?(为了提供性能,处理器和
JIT
编译器常常会对指令进行重排序,需要满足以下两个条件:1)在单线程环境下不能改变程序运行的结果;2)存在数据依赖关系的不允许重排序)【问】什么是自旋?(因为线程阻塞涉及到用户态和内核态切换的问题,不如让线程忙循环(自旋)等待锁的释放,如果做了多次循环发现还没有获得锁,再阻塞)
【问】volatile 关键字的作用?(工作内存中的操作结果立刻写回主存中),可参考volatile关键字最全总结,
Note:- 在Java的内存模型中分为主内存(物理内存) 和 工作内存(高速缓存),Java内存模型规定所有的变量存储在主内存中,每条线程都有自己的工作内存。
结合CAS
算法就很好理解,volatile
作用是禁止指令重排序,能够将线程在工作内存(新值B
)中的操作结果立刻写回主存(内存值V
)中,其他线程每次在读取时都能访问最新的值,但不能保证原子性;而且volatile
只能作用于变量 。 - 线程的三个概念:
- 原子性:对基本数据类型的变量的读取和赋值操作是原子性操作
- 可见性:当一个共享变量被
volatile
修饰时,他会保证修改的值会立刻被更新到主存,当以后其他线程需要读取时,它会去内存中读取新值。 - 有序性:在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的运行,却会影响到多线程并发执行的正确性。
- 应用场景:
volatile
作为一个轻量级同步锁,可用的场景较少,要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:- 对变量的写操作不依赖于当前值(加锁)
- 该变量没有包含在具有其他变量的不变式中
- 单例模式的双重锁需要加
volatile
public class TestInstance{private volatile static TestInstance instance;public static TestInstance getInstance(){ if(instance == null){ //1 synchronized(TestInstance.class){ //2 if(instance == null){ //3 instance = new TestInstance(); //4} //5}}return instance; }//6}
在并发情况下,如果没有
volatile
关键字,在第5行会出现问题。instance = new TestInstance()
;可以分解为3行伪代码:a. memory = allocate() //分配内存b. ctorInstanc(memory) //初始化对象c. instance = memory //设置instance指向刚分配的地址
上面的代码在编译运行时,可能会出现重排序从
a-b-c
排序为a-c-b
。在多线程的情况下会出现以下问题。当线程A
在执行第5行代码时,B
线程进来执行到第2行代码。假设此时A
执行的过程中发生了指令重排序,即先执行了a
和c
没有执行b
, 那么由于A
线程执行了c
导致instance指向了一段地址,所以B
线程判断instance不为null,会直接跳到第6行并返回一个未初始化的对象。因此需要使用volatile
修饰instance。
- 在Java的内存模型中分为主内存(物理内存) 和 工作内存(高速缓存),Java内存模型规定所有的变量存储在主内存中,每条线程都有自己的工作内存。
【问】JVM 对 Java 的原生锁做了哪些优化?
Note:- 线程阻塞时,自旋锁自旋次数默认10次;自适应自旋锁自旋次数不固定,由前一次自旋次数和锁的拥有者的状态决定;
- 锁消除:在动态编译同步代码块的时候,
JVM
中的JIT
编译器借助逃逸分析技术来判断对象是否存在线程同步的情况,是否只被一个线程访问,若不存在同步情况则可以取消锁,省去了加锁解锁的开销。
逃逸分析是指当前加锁的对象是否会逃出它的作用域?比如如下代码:- 在代码1中,
append
方法用了synchronized
关键字,如果只有一个线程对这个StringBuffer
对象进行操作时(不存在同步),在线程内部可以把StringBuffer
当做局部变量使用,StringBuffer
仅在方法内作用域有效,因此是线程安全的,可以进行锁消除。@Override public synchronized StringBuffer append(String str) {toStringCache = null;super.append(str);return this; }
- 在代码2中,
sBuf
会逃出当前线程,作为外部的全局变量使用(存在同步操作),因而是线程不安全的,不能对sBuf
的append()
进行锁消除。public static String createStringBuffer(String str1, String str2) {StringBuffer sBuf = new StringBuffer();sBuf.append(str1);// append方法是同步操作sBuf.append(str2);return sBuf.toString(); }
- 对于第一段代码,可以通过
JIT
编译器将其优化,将锁消除,前提是Java必须运行在server模式,同时必须开启逃逸分析;-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks其中+DoEscapeAnalysis表示开启逃逸分析,+EliminateLocks表示锁消除。
- 在代码1中,
- 锁粗化:锁的请求、同步、释放都会消耗一定的系统资源,如果高频的锁请求反而不利于系统性能的优化。当
JIT
编译器发现一系列的操作都对同一个对象反复加锁解锁,甚至加锁操作出现在循环中,此时会将加锁同步的范围粗化到整个操作系列的外部。
目的是让能够一次性执行完的代码不要多次对同一个变量加锁执行。 - 锁粒度:不要锁住一些无关的代码。
【问】为什么说 Synchronized 是非公平锁?(公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,而Synchronized锁是非公平锁,属于抢占式)
【问】什么是锁消除和锁粗化?(见上两问)
【问】为什么说 Synchronized 是一个悲观锁?(不管是否产生竞争,任何数据的操作都必须加锁)
【问】乐观锁的实现原理又是什么?什么是 CAS,它有什么特性?乐观锁一定就是好的吗?,可参考乐观锁之CAS算法
Note:Synchronized
关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE
状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。Java1.6
为Synchronized
做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度(其中也用到了CAS
)原子操作类是
Atomic
开头的包装类(AtomicBoolean
,AtomicInteger
,AtomicLong
)在进行自增时,性能比Synchronized
好,其原理是使用CAS
机制。Lock
系列的底层实现也用到了CAS
。CAS
机制即比较并交换,CAS机制当中使用了3个基本操作数:内存地址V(共享内存空间),旧的预期值A(此前的内存快照),要修改的新值B(线程的操作)。线程1在将B写入内存时,会比较此刻内存中的值V和A是否相等,如果相等,则写入;如果不等,则重新获取内存中的值V,再重新尝试操作,此过程为自旋。CAS
优缺点:
优点:CAS
无锁和非阻塞,性能好,没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销。
缺点:
- CPU开销大:在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,不断的进行自旋,循环往复,会给CPU带来很大的压力。
- 不能保证整个代码块的原子性:CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性;比如需要保证3个变量共同进行原子性的更新,就不得不使用
Synchronized
了。 - 存在ABA问题:如果以提款存款为例子,假设此时存款100,线程1和线程2都在提款50(A都为100),线程1提款成功(V为50),此时又有另一个线程在存款50(A为50)将V被修改为100,此时线程2开始运行时发现自己的A=此时的V,把V修改为50,但却没有把钱吐出来,导致用户账号里少钱了。
ABA的解决方法:更为严谨的
CAS
算法应该加一个版本号,用于比较前后V相同时,版本号是否相同,如果两者都相同,才进行写入,否则自旋。AtomicInteger
自增方法incrementAndGet
源码如下:public final int incrementAndGet() {for (;;) {int current = get();int next = current + 1;if (compareAndSet(current, next))return next;} } private volatile int value; public final int get() {return value; }
其中的
compareAndSet
也是原子操作,底层是通过JVM调用的后门程序unsafe
,实现硬件层面的原子操作。Synchronized
属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守,比如比如mysql的行锁,表锁,读锁和写锁;CAS
属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。比如版本控制软件git,svn,cvs。
【问】请对比下 volatile,Synchronized,CAS(乐观锁,非阻塞)的异同?,参考并发编程面试题(2020最新版)
【问】跟 Synchronized 相比,可重入锁 ReentrantLock 其实现原理有什么不同?(在使用
synchronized
时,其锁对象monitor
内部有一个计数器,在monitorenter()
和monitorexit()
内部如果当前线程访问则计数器+1
)
Note:ReentrantLock
使用内部类Sync
来管理锁,所以真正的获取锁是由Sync
的实现类控制的。Sync
有两个实现,分别为NonfairSync
(非公公平锁)和FairSync
(公平锁)。// Sync继承于AQS abstract static class Sync extends AbstractQueuedSynchronizer {... } // ReentrantLock默认是非公平锁 public ReentrantLock() {sync = new NonfairSync();} // 可以通过向构造方法中传true来实现公平锁 public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync(); }
Sync
通过继承AQS
实现,在AQS
中维护了一个private volatile int state
来计算重入次数,避免频繁的持有释放操作带来的线程问题。- 当一个线程在获取锁过程中,先判断
state
的值是否为0,如果是表示没有线程持有锁,就可以尝试获取锁。 - 当
state
的值不为0时,表示锁已经被一个线程占用了,这时会做一个判断current==getExclusiveOwnerThread()
,这个方法返回的是当前持有锁的线程,如果是自己,那么将state
的值+1
,表示重入返回即可。
- 当一个线程在获取锁过程中,先判断
【问】synchronized 和 ReentrantLock 区别是什么?
Note:- 1)相似点:它们都是阻塞式的同步,也就是说一个线程获得了对象锁,进入代码块,其它访问该同步块的线程都必须阻塞在同步代码块外面等待,而进行线程阻塞和唤醒的代码是比较高的。
- 2)功能区别:
Synchronized
是java语言的关键字,是原生语法层面的互斥,需要JVM实现;ReentrantLock
是JDK1.5
之后提供的API层面的互斥锁,需要lock
和unlock()
方法配合try/finally
代码块来完成。Synchronized
使用较ReentrantLock
便利一些;- 锁的细粒度和灵活性:
ReentrantLock
强于Synchronized
;
- 3)性能区别:
Synchronized
引入偏向锁,自旋锁之后,两者的性能差不多,在这种情况下,官方建议使用Synchronized
。- 两者的重入区别看上一问
ReentrantLock
是java.util.concurrent
包下提供的一套互斥锁,相比Synchronized
,ReentrantLock类提供了一些高级功能,主要有如下三项:- 等待可中断:持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于
Synchronized
避免出现死锁的情况。通过lock.lockInterruptibly()
来实现这一机制; - 公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,
Synchronized
锁是非公平锁;ReentrantLock
默认也是非公平锁,可以通过参数true
设为公平锁,但公平锁表现的性能不是很好; - 锁绑定多个条件:一个
ReentrantLock
对象可以同时绑定多个对象。ReentrantLock
提供了一个Condition
(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像Synchronized
要么随机唤醒一个线程,要么唤醒全部线程。
- 等待可中断:持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于
【问】那么请谈谈 AQS 框架是怎么回事儿?,可参考并发编程面试题(2020最新版)
Note:java的
Lock
体系其实是基于AQS
框架实现的,Lock
体系中的锁对象其实一个资源(独占/共享),在加锁和释放锁是通过CAS
算法对state+1
或state-1
实现。在使用Semaphore
,Reentrantlock
锁资源时,需要配合try-catch
手动释放锁资源,而CountDownLatch
,CyclicBarrier
不需要。AQS
全称是AbstractQueuedSynchronizer
(抽象队列同步器),是一个独占锁/共享锁的实现框架(ReentrantLock
、CountDownLatch
、CyclicBarrier
、ReadWriteLock
),其核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS
是用CLH队列锁
实现的,即将暂时获取不到锁的线程加入到队列中。CLH(Craig,Landin,and Hagersten)
队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node
)来实现锁的分配。AQS
原理图如下:
AQS
使用一个int
成员变量state
来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。private volatile int state;//共享变量,使用volatile修饰保证线程可见性
状态信息通过protected类型的
getState
,setState
,compareAndSetState
进行操作//返回同步状态的当前值 protected final int getState() { return state; }// 设置同步状态的值 protected final void setState(int newState) { state = newState; } //原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值) protected final boolean compareAndSetState(int expect, int update) {return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
AQS
定义两种资源共享方式Exclusive
(独占):只有一个线程能执行,如ReentrantLock
。又可分为公平锁和非公平锁:- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share
(共享):多个线程可同时执行,如Semaphore、CountDownLatch、CyclicBarrier、ReadWriteLock
。
ReentrantReadWriteLock
可以看成是组合式(共享 + 独占),因为ReentrantReadWriteLock
也就是读写锁允许多个线程同时对某一资源进行读。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源
state
的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS
已经在顶层实现好了。AQS
底层使用了模板方法设计模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):- 使用者继承
AbstractQueuedSynchronizer
并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放) - 将
AQS
组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
默认情况下,每个方法都抛出
UnsupportedOperationException
。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS
类中的其他方法都是final
,所以无法被其他类使用,只有这几个方法可以被其他类使用。- 使用者继承
以
ReentrantLock
为例,state
初始化为0,表示未锁定状态。A
线程lock()
时,会调用tryAcquire()
独占该锁并将state+1
。此后,其他线程再tryAcquire()
时就会失败,直到A线程unlock()
到state=0
(即释放锁)为止,其它线程才有机会获取该锁。
当然,释放锁之前,A
线程自己是可以重复获取此锁的(锁重入时state
会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state
是能回到0的。再以
CountDownLatch
以例,任务分为N
个子线程去执行,state
也初始化为N
(注意N
要与线程个数一致)。这N
个子线程是并行执行的,每个子线程执行完后countDown()
一次,state
会通过CAS
减1(N
不大,CAS
开销也不大)。
等到所有子线程都执行完后(即state=0
),会unpark()
主调用线程,然后主调用线程就会从await()
函数返回,继续后余动作。一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现
tryAcquire-tryRelease
、tryAcquireShared-tryReleaseShared
中的一种即可。但AQS
也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock
。
【问】除了 ReetrantLock,你还接触过 JUC 中的哪些并发工具?,参考多线程工具类:CountDownLatch、CyclicBarrier、Semaphore
Note:具体代码参考以上链接CountDownLatch
:假如有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以(countDownLatch.await();
)。CountDownLatch
是JDK为我们提供的一个计数器,核心是通过countDown()
实现减1操作,它主要方法如下://构造方法,接收计数器的数量 public CountDownLatch(int count) //持续等待计数器归零 public void await() //最多等待unit时间单位内timeout时间 public boolean await(long timeout, TimeUnit unit) //计数器减1 public void countDown() //返回现在的计数器数量 public long getCount()
CyclicBarrier
:CyclicBarrier
(循环栅栏)可以完成CountDownLatch
的全部功能,但是相比CountDownLatch
,它可以一次性执行多个线程,接着通过await()
等待一次性释放锁资源,即一组线程相互等待。常用方法如下://构造方法,第一个参数为栅栏的长度,第二个是Runnable对象 public CyclicBarrier(int parties, Runnable barrierAction) public CyclicBarrier(int parties) //获取现在的数量 public int getParties() //持续等待栅栏归零 public int await() //最多等待unit时间单位内timeout时间 public int await(long timeout, TimeUnit unit)
当
CyclicBarrier
的任务执行完一轮(5个thread)以后,如果构造时传入了Runnable
对象,则先执行Runnable
对象,然后在完成瞬间释放所有任务的锁,接着再加入新的任务执行。Semaphore
:允许多个线程同时访问,相比于CyclicBarrier
,Semaphore
(信号量)并没有限制一次性只能执行N个线程并一次性释放N锁资源;synchronized
和ReentrantLock
是独占锁,同一时刻只允许一个线程访问,Semaphore
(信号量)是共享锁。Semaphore
基本能完成ReentrantLock
的所有工作,使用方法也与之类似,通过acquire()
与release()
方法来获得和释放临界资源。经实测,Semaphone.acquire()
方法默认为可响应中断锁,与ReentrantLock.lockInterruptibly()
作用效果一致,也就是说在等待临界资源的过程中可以被Thread.interrupt()
方法中断(synchronized
则不允许线程中断)。- 此外,
Semaphore
也实现了可轮询的锁请求与定时锁的功能,除了方法名tryAcquire
与tryLock
不同,其使用方法与ReentrantLock
几乎一致。Semaphore
也提供了公平与非公平锁的机制,也可在构造函数中进行设定。常用方法如下://创建具有给定许可数的信号量 Semaphore(int permits):构造方法,创建 //拿走1个许可 void acquire() //拿走多个许可 void acquire(int n) //释放一个许可 void release() //释放n个许可 void release(int n): //当前可用的许可数 int availablePermits():
【问】请谈谈 ReadWriteLock 和 StampedLock?,可参考ReadWriteLock和StampedLock
Note:ReadWriteLock
是一个读写锁接口,读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock
是ReadWriteLock
接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的。ReentrantReadWriteLock
的构造函数如下:public class ReentrantReadWriteLockimplements ReadWriteLock, java.io.Serializable {public ReentrantReadWriteLock() {this(false);} public ReentrantReadWriteLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();readerLock = new ReadLock(this);writerLock = new WriteLock(this);} }
ReadLock
的加锁方法是基于AQS
同步器的共享模式。public void lock() {sync.acquireShared(1); }
WriteLock
的加锁方法是基于AQS
同步器的独占模式。public void lock() {sync.acquire(1); }
StampedLock
(邮戳锁不可重入):但是读写锁ReadWriteLock
容易引起饥饿写的问题。饥饿写即在使用读写锁的时候,读线程的数量要远远大于写线程的数量,导致锁对象(this
)长期被读线程持有,写线程无法获取锁对象(this
)的写操作权限而进入饥饿状态。因此JDK1.8
引入了StampedLock
。StampedLock
在获取锁的时候会返回一个long型的数据戳,该数据戳用于稍后的锁释放参数,如果返回的数据戳为0则表示锁获取失败。StampedLock
是不可重入的,即使当前线程持有了锁再次获取锁还是会返回一个新的数据戳,所以要注意锁的释放参数,使用不小心可能会导致死锁。
【问】什么是 Java 的内存模型(JMM),Java 中各个线程是怎么彼此看到对方的变量的?(见
volatile
解析)【问】Java8开始ConcurrentHashMap,为什么舍弃分段锁?(
ConcurrentHashMap
的原理是引用了内部的Segment ( ReentrantLock )
分段锁,但在Java8使用synchronized+CAS
,原因是加入多个分段锁浪费内存空间)【问】ThreadLocal 是什么?有哪些使用场景?,参考史上最全ThreadLocal 详解,拼多多面试官没想到ThreadLocal我用得这么溜,人直接傻掉
Note:ThreadLocal
是线程变量,是每个线程执行时的局部变量表,通过每个线程的ThreadLocalMap
来存储,在Java8中key为ThreadLocal
,value为值;ThreadLocal
提供了线程本地的实例。它与普通变量的区别在于,每个线程使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal
变量通常被private static
修饰。当一个线程结束时,它所使用的所有ThreadLocal
相对的实例副本都可被回收。
ThreadLocalMap
没有实现Map接口,而是基于数组实现,其中数组中的每个元素表示一个ThreadLocal
副本,用Entry<ThreadLocal,Object>
来存储,因此ThreadLocalMap
中不同的threadlocal
可以用于存储不同类型的对象(Entry<ThreadLocal,Integer>
,Entry<ThreadLocal,Character>
等)ThreadLocalMap
在处理冲突时:1)会先通过key.threadLocalHashCode & (len-1);
计算当前ThreadLocal
的hash值,接着到数组中查找该位置是否为空:2)如果为空,则直接初始化Entry
并插入;3)如果非空,则比较当前位置上的key是否为当前的ThreadLocal
对象,如果是,则完成value
的更新;如果不是则通过线性探测法搜索下一个未冲突的位置。
ThreadLocal
适用于如下两种场景- 1、每个线程需要有自己单独的实例(线程A不能访问线程B的工作内存,具有隔离性)
- 2、实例需要在多个方法中共享,但不希望被多线程共享;
场景包括:
1)数据库连接:每个线程通过ThreadLocal
创建一个JDBC Connection
,线程A
不能close()
线程B
的connection);
2)处理数据库事务
3)用户session管理;
4)Spring使用ThreadLocal解决线程安全问题 ;
【问】请谈谈 ThreadLocal 是怎么解决并发安全的?与
Sychronized
,Lock
的比较
Note:- 在处理并发问题时,
Synchronized
和Lock
用时间换空间的方式,让一个线程执行,其他线程等待,使不同线程串行执行; ThreadLocal
通过创建线程局部变量,用空间换时间的方式,让不同线程并发执行(实现简单,很多开源项目比如Spring都是用ThreadLocal
来处理并发问题)。
- 在处理并发问题时,
【问】很多人都说要慎用 ThreadLocal,谈谈你的理解,使用 ThreadLocal 需要注意些什么?,参考ThreadLocal 你真的用不上吗?
Note:- 线程之间的
threadLocal
变量是互不影响的; - 使用
private final static
进行修饰,防止多实例时内存的泄露问题 - 线程池环境下使用后将
threadLocal
变量remove
掉或设置成一个初始值
- 线程之间的
【问】什么是上下文切换?(时间片轮转,线程3个状态)
Note:- 多线程编程中
Thread
的创建个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
- 多线程编程中
【问】什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )?
【问】为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用?,参考wait、notify、notifyAll的理解与使用
Note:- 在调用
wait()
,notify
和notifyAll
时,线程必须要获得该对象的对象监视器锁,否则会抛出IllegalMonitorStateException
异常; notifyAll()
使所有原来在该对象上wait
的线程退出WAITTING
状态,使得他们全部从等待队列中移入到同步队列中去(阻塞状态转就绪状态)
- 在调用
【问】object的wait,notify,notifyAll与Condition的await,signal,signalAll的区别,参考用lock condition实例,与await区别,await为何必须用在lock()里面
Note:- 在
Condition
中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll() Condition
需要在共享资源中创建:public class Car {private boolean waxStatus = false;//车的上蜡状态private Lock lock = new ReentrantLock();Condition conditionC = lock.newCondition();//消费者的conditionCondition conditionP = lock.newCondition();//生产者的condition....}
- 使用
synchronized/wait()
只有一个阻塞队列,notifyAll
会唤起所有阻塞队列下的线程,而使用lock/condition
可以实现多个阻塞队列,signalAll
只会唤起某个阻塞队列下的阻塞线程。
- 在
【问】Thread 类中的 yield 方法有什么作用?,参考Thread.yield()详解
Note:yield
是一个静态的原生(native
)方法;Thread.yield();
让当前线程从运行状态 转为 就绪状态(不是等待状态),把运行机会交给线程池中/其他拥有相同优先级的线程;- 无法保证
yield()
达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
【问】Java 如何实现多线程之间的通讯和协作?(生产者消费者问题)
Note:
Java中线程通信协作的最常见的两种方式:- 1)
synchronized
加锁的线程,通过Object
类的wait()/notify()/notifyAll()
完成线程间的通讯;只能随机唤醒一个线程(notify)或者唤醒所有线程(notifyAll) - 2)
ReentrantLock
类加锁的线程的,通过Condition
类的await()/signal()/signalAll()
完成线程间的通讯;可以分组唤醒需要唤醒的线程; - 通过管道进行线程间通信:1)字节流;2)字符流
- 1)
【问】Java Concurrency API 中的 Lock 接口(Lock interface)是什么?对比synchronized它有什么优势?即ReentrantLock与Synchronized的区别?
Note:Lock
是synchronized
的扩展版,Lock
提供了无条件的、可轮询的(lock.tryLock
方法)、定时的(lock.tryLock
带参方法)、可中断的(lock.lockInterruptibly
)、多条件阻塞队列的(lock.newCondition
方法)锁操作。Lock
的实现类基本都支持非公平锁(默认)和公平锁,synchronized
只支持非公平锁,当然,在大部分情况下,非公平锁是高效的选择。
并发的定义
Java多线程,可参考线程基本定义 & java创建线程的4种方法,常见的线程安全问题
Note:t.setDaemon(true)
将线程转换成守护线程。守护线程的唯一用途是为其他线程提供服务。比如说,JVM的垃圾回收、内存管理等线程都是守护线程。线程状态(sleep不释放,wait主动释放,yield将该线程从运行转入到就绪状态),可参考线程5个状态的转换
生产者消费者,可参考Java多种方式解决生产者消费者问题(十分详细)
DeadLock例子(多核无法避免死锁),可参考多核/单核 死锁 问题
集合线程安全,可参考Collections.synchronizedCollection 该集合线程安全,java中哪些集合是线程安全的,哪些是线程不安全的
计数器(volatile,lock,synchronized),可参考并发编程面试题(2020最新版),Java中18把锁
Future(实现类为FutureTask) 和 线程池 的使用,可参考多线程和线程池(Future,Executor,ThreadPoolExecutor),java线程池详解(corePoolSize, maximumPoolSize, workQueue 解析线程池执行流程)
Note:
Runnable
不会返回结果,无法抛出返回结果的异常;Callable
功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future
拿到。- 阿里巴巴编码规范里面提到:线程池最好不要使用
Executors
去创建,而是通过ThreadPoolExecutor
的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 - 线程与协程的区别(协程是在线程基础上进一步的抽象,之前线程是和程序绑定在一起的,多个程序的运行会轮流使用线程池中的线程,虽然线程池可以避免线程的创建和销毁次数,但是每一次创建难免存在上下文切换开销大,而多了协程这层抽象之后,用户在逻辑层面上将一个程序与一个协程绑定,而对线程池的管理由之前的用户管理交给底层去实现,如果线程执行完毕的话,多个协程可以同时使用同一个线程执行(协程与线程为多对多关系),少去了由用户来完成线程间的上下文切换,线程的切换在底层的核心态内来完成,而协程只起到与线程的绑定作用)。 参考总结:协程与线程,协程与线程的区别,多线程和线程池,Java 19 正式发布,虚拟线程来了!
当我们的程序是 IO 密集型时(如 web 服务器、网关等),为了追求高吞吐,有两种思路:
- 为每个请求开一个线程处理,为了降低线程的创建开销,可以使用线程池技术,理论上线程池越大,则吞吐越高,但线程池越大,CPU花在切换上的开销也越大。
- 使用异步非阻塞的开发模型,用一个进程或线程接收请求,然后通过 IO 多路复用让进程或线程不阻塞,省去上下文切换的开销;
这两个方案,优缺点都很明显:方案1实现简单,但性能不高;方案2性能非常好,但实现起来复杂。
协程需要解决线程遇到的几个问题:
内存占用要小,且创建开销要小
- 用户态的协程,可以设计的很小,可以达到 KB 级别。是线程的千分之一。
- 线程栈空间通常是MB级别, 协程栈空间最小KB级别。
减少上下文切换的开销:
- 让可执行的线程尽量少,这样切换次数必然会少
- 让线程尽可能的处于运行状态,而不是阻塞让出时间片
- 多个协程多个协程绑定一个或者多个线程上
- 当一个协程执行完成后,可以选择主动让出,让另一个协程运行在当前线程之上(分时复用)。
即使有协程阻塞,该线程的其他协程也可以被 runtime 调度,转移到其他可运行的线程上。
降低开发难度
- goroutine是golang中对协程的实现
- goroutine底层实现了少量线程干多事,减少切换时间等,程序员可以轻松创建协程,无需去关注底层性能优化的细节
协程和线程的区别
- 线程是操作系统的资源,由OS创建切换和停止,而协程是用户级线程,由编程语言本身来实现
- 协程是异步机制,而线程和进程是同步机制;
- 线程抢占式,协程非抢占式(由用户释放);
- 线程开辟数量限制在千的级别,而协程可以达到上万级别
更多内容整理 参考
- Java面试题总结(分类版)
- 10万字208道Java经典面试题总结(附答案)
十四、行为抽象和Lambda
- 关于Stream流式计算的描述,可参考函数式接口&Stream流,java1.8 流式计算:利用接口的函数式编程 + 链式编程,IntStream用法(特殊的stream,可对int进行相关的流式计算)
- 整数流求和,可参考Java8中Stream详细用法大全,java 8 lambda表达式list操作分组 / 过滤 / 求和 / 最值 / 排序 / 去重
- lambda表达式的含义,可参考Java语法— Lambda表达式(lambda可以看作是匿名内部类的语法糖,无法捕获改变的变量)
- 用lambda表达式简化构造对象(接口中只有一个函数,可以和lambda表达式进行绑定)
- stream实现mapReduce(并行实现浮点数求和计算),可参考Stream 20个实例, 玩转集合的筛选、归约、分组、聚合
- stream转换成list,stream转换成set,stream转换成Map,可参考java stream中Collectors的用法(配合stream.collect使用),java8新特性:stream流中collect用法(配合Collectors使用)
- List通过stream计算按部门进行分组,参考java 8 lambda表达式list操作分组 / 过滤 / 求和 / 最值 / 排序 / 去重,Java中map.getOrDefault()方法的使用(如果值存在则不使用默认值)
- 对stream进行分区(二分),参考Stream流对象只能被消费一次
- Optional应用1,Optional应用2,可参考Java 8 Optional 详细用法(创建,获取,判断,过滤,映射)
Note:
- 流(Stream)是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。集合讲的是数据,流讲的是计算。
- stream计算主要包括两个动作:中间动作(
filter
,map
,flatMap
等)和结束动作(reduce
,collect
,forEach
等) Sensor::getNum
表示Sensor
类中的getNum
方法filter()
传入的是Predicate<? super T>
断定式函数接口- 在匿名内部类中,一定是程序在运行的过程当中没有发生改变的量,即无法捕获在匿名内部类改变的变量,lambda表达式也是如此。
- Stream流属于管道流,该对象只能被消费(使用)一次;第一个stream流调用完毕方法, 数据就会流转到下一个Stream上(streamB),而这时第一个stream流(streamA)已经使用完毕,就会关闭了;如果再次使用streamA,则会抛出:
java.lang.IllegalStateException: stream has already been operated upon or closed
Optional
类是一个对象容器,可以对对象进行进一步封装,可以提示用户该对象是否为null
,并使用filter
,orElse
等进行处理,简化if else
代码。
十五、设计模式
- 【问】请列举出在 JDK 中几个常用的设计模式?
- 【问】什么是设计模式?你是否在你的代码里面使用过任何设计模式?
Note:个人理解设计模式是把面向对象的封装(继承,多态,接口实现)及方法调用(子类方法,接口方法还是父类方法作为用户调用的方法入口)玩得淋漓尽致的高质量代码,是前辈总结的智慧结晶,可以用来通用地解决某一类现实问题,写出来的代码泛化性强,可扩展性好,便于因需求的变动而更新代码。 - 【问】Java 中什么叫单例设计模式?请用 Java 写出线程安全的单例模式
- 【问】在 Java 中,什么叫观察者设计模式(observer design pattern)?
- 【问】使用工厂模式最主要的好处是什么?在哪里使用?
- 【问】举一个用 Java 实现的装饰模式(decorator design pattern)?它是作用于对象层次还是类层次?(在IO流中,
FilterInputStream
作为装饰器,内部封装了InputStream
基础构件,其子类BufferedInputStream
调用其read()
读取数据时会委托InputStream
基础构件来进行更底层的操作,而它自己所起的装饰作用就是缓冲) - 【问】设计一个 ATM 机,请说出你的设计思路?
- 【问】举例说明什么情况下会更倾向于使用抽象类而不是接口
- 【问】构建型模式有哪几种?
Note:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。- 【问】如何编写简单工厂模式?(存在抽象产品类,工厂里可以生产该类产品,可用抽象产品类创建子类产品,实现泛化)
- 【问】如何编写抽象工厂模式?(存在多个工厂实现类,可用抽象工厂类创建子类工厂,实现泛化)
- 【问】如何编写生成器模式?(工厂模式关注的是创建单个产品,而建造者模式则关注将各种产品集中起来进行管理,利用用户给定的部件组装顺序创建复合对象,不同用户所有求的创建过程由指定的
Builder
实现)
Note:- 建造者的优点:客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者, 用户使用不同的具体建造者即可得到不同的产品对象 。
增加新的具体建造者无须修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则”。 - 建造者模式的缺点:建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大。
- 如果一个公司需要1000辆不同型号的奔驰和500辆不同型号的宝马,则可以创建一个
Director
来指导不同相对独立的builder
(对应一个型号的汽车)工作。
- 建造者的优点:客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
- 【问】如何编写原型模式?(实现
Cloneable
接口,完成对象快速复制) - 【问】如何编写单例模式?(
private static Lazy lazy; private Lazy(); private static Lazy getInstance();
,构造方法私有化,不让用户创建对象;饿汉式,懒汉式)
- 【问】结构型模式有哪几种?
Note:结构型模式用于描述如何将类或对象按某种布局组成更多的结构(除了适配器模式,其他6种结构型模式都是关于 类和对象 的布局调整)- 【问】如何编写组合模式?(将多个对象组合在一起进行操作,常用于表示树形结构中,例如二叉树,链表等)
- 【问】如何编写适配器模式?(适配的方式有3种,共同点都在于用
Adapter
类对多个接口/类,或者1个复杂接口进行再次的结构性封装,并没有增加功能,以满足调用者的不同需求)- 类的适配器模式:有一个
Source
类,拥有一个方法,待适配,目标接口是Targetable
,通过Adapter
类继承Source
并实现Targetable
,将Source
的功能扩展到Targetable
里。(当希望将一个类转换成满足另一个新接口的类时) - 对象的适配器模式:基本思路和类的适配器模式相同,只是将
Adapter
类作修改,这次不继承Source
类,而是持有Source
类的实例,以达到解决兼容性的问题。(一个对象转换成满足另一个新接口的对象时) - 接口的适配器模式:有时我们写的一个接口中有多个抽象方法,当我们写该接口
Sourceable
的实现类时,必须实现该接口的所有方法,有些时候是不必要的;因此借助于一个抽象类,该抽象类Wrapper
实现了该接口,实现了所有的方法,而我们不和原始的接口打交道,只和该抽象类Wrapper
取得联系。(当不希望实现一个接口中所有的方法时)
- 类的适配器模式:有一个
- 【问】如何编写装饰器模式?(装饰模式就是给一个对象动态增加一些新的功能,要求装饰对象和被装饰对象实现同一个接口,而且装饰者对象包含被装饰者的引用;而继承的功能是静态的,不能动态增删)
- 【问】如何编写代理者模式?(为某个对象提供一个代理,并由这个代理对象控制对原对象的访问。可以对被代理类的进行增强;和装饰者的区别在于代理模式并不包含对象的引用,而是自己
new
出来) - 【问】如何编写外观模式?(不同的类对象(CPU,memory,disk)的创建及其之间的关联操作均由一个类(computer)实现,降低了类与类之间的耦合度)
- 【问】如何编写桥接模式?(桥接模式就是把事物和其具体实现分开,使他们可以各自独立的变化,用意是将抽象化与实现化解耦,典型例子是JDBC(两抽象,抽象类
Bridge
中用统一的Sourceable
接口接入source
实例)和 mysql和sqlserver(两实现,source
类,MyBridge
类)之间的关系 - 【问】如何编写享元模式?(通常与工厂模式一起使用,
FlyWeightFactory
负责创建和管理享元单元,当一个客户端请求时,工厂需要检查当前对象池中是否有符合条件的对象;适用于作共享的一些个对象,他们有一些共有的属性,就拿JDBC连接池来说,url、driverClassName、username、password及dbname,这些属性对于每个连接来说都是一样的,所以就适合用享元模式来处理,建一个工厂类,将上述类似属性作为内部数据,其它的作为外部数据)
- 【问】行为型模式有哪几种?
Note:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。
1)父类与子类:策略模式,模板方法模式
2)两个类之间:观察者模式,迭代器模式,责任链模式 和 命令模式
3)类的状态:备忘录模式,状态模式
4)通过中间类:访问者模式,中介者模式,解释器模式
该分类方式的划分依据:对向Test
暴露的接口类进行分析,分析其在各类封装过程中起到的作用。- 【问】如何编写策略模式?(策略模式定义了一系列算法(
plus
,min
,multiply
),并将每个算法封装起来(都继承于AbstractCalculator
、拥有其功能,并实现了ICalculator
接口来功能扩展),使它们可以相互替换(可以用AbstractCalculator
去实例化不同的算法对象),多用在算法决策系统中,外部用户只需要决定用哪个算法即可) - 【问】如何编写模板方法模式?(模板方法模式相较于策略模式,少了接口的实现,它定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中(子类重写
abstract
类提供的多个辅助方法,完成算法步骤构建),而父类有一个主方法作为不同子类中整个算法的调用入口。即:一个抽象类中,有一个主方法(calculate
),再定义1...n
个辅助方法,用于完成一系列的算法功能(比如calculate1
,calculate2
,calculate3
),接着在创建子类对象时,只需调用父类的calculate
即可获得算法结果(calculate
中按顺序调用了calculate1
,calculate2
,calculate3
) - 【问】如何编写观察者模式?(多个对象间存在一对多关系,当一个对象(被观察者
MySubject
)发生改变时,会把这种改变通知给其他多个对象(观察者们observer1
,observer2
…),从而影响观察者们的行为。比如说MySubject
类是主对象(即被观察者),Observer1
和Observer2
是依赖于MySubject
的对象,当MySubject
变化时,Observer1
和Observer2
必然变化。AbstractSubject
类中定义着观察者对象列表,可以对其进行修改(增加或删除观察者对象))
Note:- 观察者模式在现实生活中挺好理解:观察者们是股东们,被观察者是公司的CEO,CEO有拍板决策的权利,但是需要股东们监督;公司每季度业绩出来之后,CEO会发邮件通知所有的股东,股东们才会决定投资还是撤资。
- 观察者模式容易和监听者模式混淆,其实没有监听者模式,只有监听事件机制,它是观察者模式的理论实践。可以参考这篇评论(别看文章解释)
- 【问】如何编写迭代器模式?(提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。这句话中包括两个对象:一是需要遍历的对象,即聚集对象,二是迭代器对象,用于对聚集对象进行遍历访问。实现思路是:
MyCollection
中定义了集合(比如list
)的一些操作,MyIterator
中定义了一系列迭代操作,且持有Collection
实例并具有对其遍历的功能,而MyCollection
有Iterator
的实例对象,可以向外暴露;一个特定的MyCollection
的迭代功能由一个特定的MyIterator
来实现。) - 【问】如何编写责任链模式?(责任链模式把请求从链中的一个对象传到下一个对象,直到请求被响应为止(链接上的请求可以是一条链,可以是一个树,还可以是一个环,模式本身不约束这个,但在一个时刻,命令只允许由一个对象传给另一个对象,而不允许传给多个对象)。比如说,
MyHandler
继承了AbstractHandler
中的get
、set
方法,实现了Handler
中的operator
方法,当初始化三个AbstractHandler
对象:h1,h2,h3
之后,在实际调用时,h1
封装了h2
,h2
封装了h3
,这样h1
在执行operator
的时候,会依次调用链上的h2,h3
的operator
,实现流水线操作。通过这种方式去除对象之间的耦合,发出者并不清楚最终到底哪个对象会处理该请求,所以,责任链模式可以实现在隐瞒客户端的情况下,对系统进行动态的调整) - 【问】如何编写命令模式?(命令模式将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开,比如说:我的命令类
MyCommend
,是针对指定人群(Reciever
类)而言的(MyCommend
拥有Reciever
的实例,只对Reciever
类有效),Reciever
是一个守纪律的工种,他只会完成一类工作(action
),而且只有接收到命令(MyCommend
的exe
命令)之后才会工作,而实际在下发命令的是我们的领导(Invoker
拥有MyCommend
的下发权)) - 【问】如何编写备忘录模式?(在不破坏封装性的前提下,通过备忘录类(
Memento
类)获取并保存一个对象(Original
类)的内部状态(value
),以便以后恢复它;这里使用Storage
类来管理Memento
类,即调用Storage
的getMemento
,将备份对象传入Original
的数据恢复方法(restoreMemento
)中) - 【问】如何编写状态模式?(状态模式允许一个对象(
context
上下文信息类)在其内部状态(包含State
对象,State
对象的不同取值(value
)决定着不同的行为(method1
,method2...
),Context
可以根据State
取值,决定使用method1
还是method2
)发生改变时改变其行为的能力,比如说QQ,有几种状态:在线、隐身、忙碌等,每个状态对应不同的操作(method1
,method2...
),而且你的好友也能看到你的状态(getState
)) - 【问】如何编写访问者模式?(访问者模式提供一个作用于某对象结构中的各元素的操作表示,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。简单来说,访问者模式就是一种分离对象数据结构与行为的方法,通过这种分离,可达到为一个被访问者动态添加新的操作而无需做其它的修改的效果;比方说,主体
Subject
和访问者Visitor
是好朋友,Visitor
可以到Subject
家里做客,但前提是Visitor
需要向Subject
报备,Subject
允许其到访(subject.accept
)之后,Visitor
才会开车去他家(visit(subject)
),因此想访问subject
的visitors
都要收到subject
的允许才可以出发;访问者模式适用于数据结构相对稳定(subject
家地址和号码是固定的)但算法又易变化(subject
经常出差)的系统,因为访问者模式(accept
机制)使得算法操作增加变得容易;若系统数据结构对象易于变化(subject
家地址和号码经常更换,即使subject
不出差,访问者也不会来,因为visitor
只有subject
的旧地址),经常有新的数据对象增加进来,则不适合使用访问者模式。这里要与观察者模式区分开:观察者模式是被观察者通过回调通知观察者们,观察者们接下来会做出相应动作;而访问者模式强调的是被访问者允许访问者访问自己的方法,访问者只有访问行为,没有其他多余操作) - 【问】如何编写中介者模式?(中介者模式定义一个中介对象(
Mediator
接口)来简化原有对象(user1
和user2
)之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。中介者模式也是用来降低类类之间的耦合的,因为如果类类之间有依赖关系的话,不利于功能的拓展和维护,因为只要修改一个对象,其它关联的对象都得进行修改。如果使用中介者模式,只需关心和Mediator
类的关系,具体类类之间的关系及调度交给Mediator
就行,这有点像spring容器
的作用。这里Mediator
好比是中介公司,而MyMediator
好比是中介公司里的员工,负责user1
和user2
的之间的调度工作。要注意点是,中介者模式对外仅暴露Mediator
对象,Mediator
与user1
,user2
在一开始就绑定好了,外部仅对Mediator
是否完成调停工作感兴趣,对user1
和user2
之间的交互并不关心;注意区分中介模式和代理模式,前者是多个类之间的封装,而后者是两个类的封装) - 【问】如何编写解释器模式?(解释器模式提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。一般主要应用在OOP开发中的编译器开发中,所以适用面比较窄。
Context
类是一个上下文环境类,包含上下文各种信息,Plus
和Minus
实现Expression
接口(interpret
方法传入Context
对象),分别用来实现计算功能)
- 【问】如何编写策略模式?(策略模式定义了一系列算法(
更多内容整理参考
- Java面试题总结(分类版)
- 10万字208道Java经典面试题总结(附答案)
- Java中常用的设计模式(23种)
- JAVA实现23种设计模式
十六、JVM(类加载,运行时数据区,垃圾回收算法,垃圾回收器,JVM调优)
参考Java虚拟机(JVM)你只要看这一篇就够了!,全面阐述JVM原理
【问】说一下 JVM 的主要组成部分?及其作用?(
JVM
包括类加载子系统、运行时数据区(堆、方法区、虚拟机栈、本地(native
)方法栈、程序计数器)、直接内存、垃圾回收器、执行引擎),可参考Java虚拟机(JVM)你只要看这一篇就够了!
Note:- JVM包含两个子系统和两个组件,两个子系统为
Classloader
(类装载)、Execution engine
(执行引擎);
两个组件为Runtime data area
(运行时数据区)、Native Interface
(本地接口)。 Class loader
(类装载):根据给定的全限定名类名(如:java.lang.Object
)来装载class文件到Runtime data area中的method area。Execution engine
(执行引擎):执行classes中的指令(JIT
编译器)。Native Interface
(本地接口):与native libraries交互,是其它编程语 言交互的接口。Runtime data area
(运行时数据区域):这就是我们常说的JVM的内存。
- JVM包含两个子系统和两个组件,两个子系统为
【问】说一下 JVM 运行时数据区?(运行时数据区包括堆、方法区、虚拟机栈、本地方法栈、程序计数器)
Note:堆(动,线程共享),区(静,线程共享),栈(动,线程私有),计数器(线程私有)- 堆:对象实例或数组,是垃圾回收器管理的主要区域。(
new
) - 方法区:方法区可以认为是堆的一部分,用于存储已被
JVM
加载的类信息,常量、静态变量、即时编译器JIT
编译后的代码。(反射,String
常量,对象类型数据) - 虚拟机栈:栈解决的是程序运行的问题,栈里面存的是栈帧,栈帧里面存的是局部变量表、操作数栈、动态链接、方法出口等信息。(方法递归
recursive
)- 栈帧:每个方法从调用到执行的过程就是一个栈帧在虚拟机栈中入栈到出栈的过程。
- 局部变量表:用于保存函数的参数和局部变量。
- 操作数栈:操作数栈又称操作栈,大多数指令都是从这里弹出数据,执行运算,然后把结果压回操作数栈。
- 本地方法栈:本地方法栈执行的是本地方法,一个Java调用非Java代码的接口。 (
native
方法) - 程序计数器(PC寄存器):存放的是当前线程所执行的字节码(
.class
文件)的行数。JVM
工作时就是通过改变这个计数器的值来选取下一个需要执行的字节码指令。(读取.class
文件的指令行)
- 堆:对象实例或数组,是垃圾回收器管理的主要区域。(
【问】对象的内存布局?(对象头,实例数据,对齐填充)
Note:- 对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为
Mark Word
。第二部分是类型指针(Klass word
),即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。
另外,如果是 Java 数组,对象头中还必须有一块用于记录数组长度的数据,因为普通对象可以通过 Java 对象元数据确定大小,而数组对象不可以。 - 实例数据(Instance Data):程序代码中所定义的各种类型的字段内容(包含父类继承下来的和子类中定义的)。
- 对齐填充(Padding):不是必然需要,主要是占位,保证对象大小是某个字节的整数倍。
- 对象头(Header):包含两部分,第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,32 位虚拟机占 32 bit,64 位虚拟机占 64 bit。官方称为
【问】关于对象的访问定位?
Note:使用对象时,通过栈上的
reference
数据来操作堆上的具体对象。- 通过句柄访问:Java 堆中会分配一块内存作为句柄池。
reference
存储的是句柄地址(二次寻址)。
- 使用直接指针访问:
reference
中直接存储对象地址(一次寻址)。
两者比较:使用句柄的最大好处是
reference
中存储的是稳定的句柄地址,在对象移动(GC
)时只改变实例数据指针地址,reference
自身不需要修改。直接指针访问的最大好处是速度快,节省了一次指针定位的时间开销。
如果是对象频繁 GC 那么句柄方法好(通过判断对象不可达到GC Roots
进而GC),如果是对象频繁访问则直接指针访问好。- 通过句柄访问:Java 堆中会分配一块内存作为句柄池。
【问】什么是类加载器,类加载器有哪些?(启动类 / 扩展类 / 应用程序类加载器,了解之间的继承关系)
Note:- 类加载器的作用:将
.class
文件字节码内容加载到内存中,并将这些静态数据转换成方法区运行时的数据,然后在堆中生成一个代表这个类的Java.lang.class
对象,作为方法区中类数据的访问入口。 - JVM有三种类加载器(B,E,A):
- 启动类加载器:该类没有父加载器,用来加载Java的核心类,启动类加载器的实现依赖于底层操作系统,属于虚拟机实现的一部分,它并不继承自
java.lang.classLoader
。 - 扩展类加载器:它的父类为启动类加载器,扩展类加载器是纯java类,是
ClassLoader
类的子类,负责加载JRE
的扩展目录。 - 应用程序类加载器:它的父类为扩展类加载器,它从环境变量
classpath
或者系统属性java.lang.path
所指定的目录中加载类,它是自定义的类加载器的父加载器。
- 启动类加载器:该类没有父加载器,用来加载Java的核心类,启动类加载器的实现依赖于底层操作系统,属于虚拟机实现的一部分,它并不继承自
- 类加载器的作用:将
【问】说一下类加载的执行过程?(加载类对象,包括成员变量,成员方法等) ,参考类加载的过程
Note:
当程序主动使用某个类时,如果该类.class
还未被加载到内存中,JVM
主要会通过类加载、类连接、类初始化3个步骤对该类进行类加载。- 类加载:将类的
.class
文件读入到内存中,并在堆中为之创建一个java.lang.Class
对象,作为类数据的访问入口。类的加载由类加载器完成,类加载器由JVM提供,开发者也可以通过继承ClassLoader
基类来创建自己的类加载器。类加载机制包括全盘负责,双亲委派和缓存机制,下面会具体说明。 - 类连接:当类被加载之后,系统为之生成一个对应的
Class
对象,接着进入连接阶段,连接阶段负责将类的二进制数据合并到JRE
中。- 验证:是连接的第一步,确保
.class
文件的字节流中包含的信息符合当前虚拟机要求,包括:文件格式验证,元数据验证,字节码验证,符号引用验证。如果无法通过符号引用验证将抛出一个java.lang.IncompatibleClass.ChangeError
异常的子类。如java.lang.IllegalAccessError
、java.lang.NoSuchFieldError
、java.lang.NoSuchMethodError
等。 - 准备:这个阶段正式为类分配内存并设置类变量初始值
- 解析:这个阶段是JVM 将常量池内的符号引用(常量名)替换为直接引用(地址) 的过程。
- 验证:是连接的第一步,确保
- 类初始化:JVM对类进行初始化
以下五种情况必须对类进行初始化(而加载、验证、准备自然需要在此之前完成):
- 遇到
new
、getstatic
、putstatic
或invokestatic
这 4 条字节码指令时触发初始化。使用场景:使用new
关键字实例化对象、读取一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。 - 使用
java.lang.reflect
包的方法对类进行反射调用的时候。 - 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需先触发其父类的初始化。
- 当虚拟机启动时,用户需指定一个要加载的主类(包含
main()
方法的那个类),虚拟机会先初始化这个主类。 - 当使用
JDK 1.7
的动态语言支持时,如果一个java.lang.invoke.MethodHandle
实例最后的解析结果REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需先触发其初始化。
- 类加载:将类的
【问】JVM的类加载机制是什么?(全盘负责,双亲委派,缓存机制)
Note:- 全盘负责:类加载器加载某个
class
时,该class
所依赖的和引用其它的class
也由该类加载器载入。 - 双亲委派:先让父加载器加载该
class
,父加载器无法加载时才考虑自己加载。因此如果自定义String
类 - 缓存机制:缓存机制保证所有加载过的
class
都会被缓存,当程序中需要某个class
时,先从缓存区中搜索,如果不存在,才会读取该类对应的二进制数据,并将其转换成class
对象,存入缓存区中。
- 全盘负责:类加载器加载某个
【问】什么是双亲委派模型?(如果父加载器存在其父加载器,则进一步向上委托,依次递归,请求将最终到达顶层的启动类加载器;如果父加载器无法完成加载任务,子加载器才会尝试自己去加载;可避免重复加载问题),参考JVM类加载器是否可以加载自定义的String
Note:JVM
出于安全性的考虑,全限定类名相同的String
是不能被加载的。但是如果加载了,会出现什么样的结果呢?下面分别通过全限定类名不同 和 全限定类名相同做一下实验:- 全限定类名不同:自定义一个
com.example.demojava.String
package com.example.demojava.loadclass; public class String {public static void main(String[] args) {System.out.println("我是自定义的String");} } --- 错误: 找不到或无法加载主类 src.main.java.com.example.demojava.loadclass.String
主要原因是参数
String
和自定义String
冲突,修改如下就可以加载自定义String
了:package com.example.demojava.loadclass;public class String {public static void main(java.lang.String[] args) {System.out.println("我是自定义的String");}}---我是自定义的String
- 全限定类名相同:自定义包名为
java.lang
package java.lang;public class String {public static void main(java.lang.String[] args) {System.out.println("我是自定义的String");} } --- Connected to the target VM, address: '127.0.0.1:63569', transport: 'socket' 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:public static void main(String[] args) 否则 JavaFX 应用程序类必须扩展javafx.application.Application
【问】怎么判断对象是否可以被回收?或者GC 对象的判定方法(
GC Roots
可达判断:GC Roots
主要来自栈和区,先删GC Roots
,再删除堆对象)
Note:- 引用计数算法:
- 判断对象的引用数量:每个对象实例都有一个引用计数器,被引用
+1
,完成引用-1
;
任何引用计数为0
的对象实例可以被当做垃圾回收; - 优缺点:
- 优点:执行效率高,程序受影响较小;
- 缺点:无法检测出循环引用的情况,导致内存泄漏;
- 判断对象的引用数量:每个对象实例都有一个引用计数器,被引用
- 可达性分析算法(
GC Roots
):- 通过判断对象的引用链是否可达来决定对象是否可以被回收。对于不可达状态的判断,需要用到
GC roots
,也就是根对象,如果一个对象无法到达根对象的路径,或者说从根对象无法引用到该对象,该对象就是不可达的。 - 以下三种对象在JVM中被称为GC roots,来判断一个对象是否可以被回收。
- 1)虚拟机栈的栈帧:每个方法在执行的时候,
JVM
都会创建一个相应的栈帧(操作数栈、局部变量表、运行时常量池的引用),当方法执行完,该栈帧就从栈中弹出,这样一来,方法中临时创建的独享就不存在了,或者说没有任何GC roots
指向这些临时对象,这些对象在下一次GC
的时候便会被回收。 - 2)方法区中的静态属性:静态属性数据类属性,不属于任何实例,因此该属性自然会作为
GC roots
。只要这个class
在,该引用指向的对象就一直存在,因此class
对象也有被回收的时候。
class何时会被回收?- 堆中不存在该类的任何实例
- 加载该类的
classLoader
已经被回收 - 该类的
java.lang.class
对象没有在任何地方被引用,也就是说无法通过反射访问该类的信息
- 3)本地方法栈引用的对象。
- 1)虚拟机栈的栈帧:每个方法在执行的时候,
- 通过判断对象的引用链是否可达来决定对象是否可以被回收。对于不可达状态的判断,需要用到
- 引用计数算法:
【问】java 中都有哪些引用类型?(强引用(不被
GC
),软引用(内存够不被GC
),弱引用,虚引用【问】说一下 JVM 有哪些垃圾回收算法?,可参考全面阐述JVM原理
Note:- 对象是否已死算法:引用计数器算法,可达性分析算法
- JVM的垃圾回收算法有三种(会用到可达性分析算法):
- 标记-清除:容易产生内存碎片,进而再一次出发
GC
- 标记-复制:Java堆中新生代的垃圾回收算法,新生代对象一般很少存活,将不回收的对象复制到新内存空间上效率高。
- 标记-压缩:Java堆中老生代的垃圾回收算法(
Major GC
),老生代大部分对象会存活,将不回收的对象压缩到内存一端,避免碎片化。
- 标记-清除:容易产生内存碎片,进而再一次出发
【问】说一下 jvm 有哪些垃圾回收器?(单线程(停)/ 多线程(停)/ CMS(不停)/ G1;主要在并发,标记策略上存在不同),可参考全面阐述JVM原理
Note:- 串行垃圾回收器:
JDK1.3
之前,单线程回收器是唯一的选择,在它进行垃圾回收的时候,必须暂停其它所有的工作线程(Stop The World,STW),直到它收集完成。- 串行的垃圾收集器有两种,
Serial
(新生代,使用标记-复制算法)和Serial Old
(老生代,使用标记-压缩算法),一般两者搭配使用。 -XX:+UseSerialGC
开启串行垃圾回收器
- 并行垃圾回收器:(配合CMS收集器使用)
- 并行垃圾回收器是通过多线程进行垃圾收集的。也会暂停其它所有的工作线程(Stop The World,STW),一般会和
JDK1.5
之后出现的CMS搭配使用 - 并行的垃圾回收器有以下几种:
ParNew
(Serial
收集器的多线程版本),运行数量可以通过修改ParallelGCThreads
设定;Parallel Scavenge
: 关注吞吐量,吞吐量优先,吞吐量=代码运行时间/(代码运行时间+垃圾收集时间);用于新生代收集,复制算法。Parllel Old
:Parallel Scavenge
的老年代版本,JDK 1.6
开始提供的。
- 并行垃圾回收器是通过多线程进行垃圾收集的。也会暂停其它所有的工作线程(Stop The World,STW),一般会和
- CMS收集器:(多次标记,一次清除;不用暂停用户的工作线程,配合并行垃圾回收器使用)
- CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。从名字就能知道它是标记-清除算法的。但是它比一般的标记-清除算法要复杂一些,分为以下4个阶段:
- 初始标记:标记一下
GC Roots
能直接关联到的对象,会"Stop The World"。 - 并发标记:
GC Roots Tracing
,可以和用户线程并发执行。 - 重新标记:标记期间产生的对象存活的再次判断,修正对这些对象的标记,执行时间相对并发标记短,会“Stop The World”。
- 并发清除:清除对象,可以和用户线程并发执行。
- 初始标记:标记一下
- CMS(Concurrent Mark Sweep) 收集器存在的问题:
- 由于它是基于标记-清除算法的,那么就无法避免空间碎片的产生。
- CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。 所谓浮动垃圾,在CMS并发清理阶段用户线程还在运行着,伴随程序运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只能留待下一次GC时再清理掉。
- CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。从名字就能知道它是标记-清除算法的。但是它比一般的标记-清除算法要复杂一些,分为以下4个阶段:
- G1垃圾收集器:JDK 7发布,并在JDK 9中成为了默认的垃圾回收器,G1收集器特性如下:
- 1)并行与并发:G1收集器能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop The World停顿时间。部分其他收集器原本需要暂停Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。【能充分利用多CPU、多核环境的硬件优势,缩短停顿时间;能和用户线程并发执行】- 2)分代收集:虽然G1收集器可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间的对象,熬过多次GC的旧对象以获取更好的收集效果。
- 3)空间整合:与CMS收集器的“标记-清除”算法不同,G1收集器整体上看采用“标记-整理“算法,局部看采用“复制”算法(两个Region之间),不会有内存碎片,不会因为大对象(
full
)找不到足够的连续空间而提前触发GC(触发全局GC -Full GC
),这点优于CMS收集器; - 4)可预测的停顿:这是G1收集器相对于CMS收集器的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不超N毫秒,这点优于CMS收集器。
- 串行垃圾回收器:
【问】详细介绍一下 CMS 垃圾回收器?(上一问)
【问】新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?(新生代回收器:
Serial
、ParNew
、Parallel Scavenge
,采用标记-复制算法;老年代回收器:Serial Old
、Parallel Old
、CMS
,采用标记-清除算法)【问】简述分代垃圾回收器是怎么工作的?(新生代分3个区,老年代)
Note:- 分代回收器分为新生代和老年代,新生代大概占
1/3
,老年代大概占2/3
; - 新生代包括
Eden
、From Survivor
、To Survivor
;
Eden
区和两个survivor
区的 的空间比例 为8:1:1
; - 垃圾回收器的执行流程:
- 1)把
Eden + From Survivor
存活的对象放入To Survivor
区; - 2)清空
Eden + From Survivor
分区,From Survivor
和To Survivor
分区交换; - 3)每熬过一次
Minor GC
对象年龄就加1的对象年龄+1
,到达15
,升级为老年代,大对象会直接进入老年代; - 4)老年代中当空间到达一定占比,会触发全局回收(
Full GC
),老年代一般采取标记-压缩算法。
- 1)把
Minor GC
触发的条件:- 1)Eden区域满;
- 2)新创建的对象大小大于Eden区所剩空间大小(如果
Minor GC
时,对象大小大于To Survivor
可用内存,则会进入老年代;如果大于老年代剩余内存,则会Full GC
)
Full GC
触发条件:- 1)老年代所剩空间不足;
- 2)方法区空间不足;
- 3)调用
System.gc()
方法; - 4)通过
Minor GC
后进入老年代的平均大小大于老年代的可用内存; - 5)由
Eden
区、From Survivor
区向To Survivor
区复制时,对象大小大于To Survivor
可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。
- 分代回收器分为新生代和老年代,新生代大概占
【问】GC 是什么? 为什么要有 GC?(Gabage Collection垃圾收集,清除掉没用的对象,为新创建的对象腾出空间,看前几问解析)
【问】简述 Java 垃圾回收机制(GC对象的判定方法,3大垃圾回收算法,4大垃圾回收器,看前几问解析)
【问】如何判断一个对象是否存活?(GC 对象的判定方法:引用计数法,
GC Roots
可达分析法;看前几问解析)【问】Java 中会存在内存泄漏吗,请简单描述,可参考Java中的内存泄露问题
Note:- 内存泄漏是指:对象已经不被使用,但对象仍存在被引用状态,导致垃圾回收器无法将其回收。久而久之,不能被回收的内存越来越多,最终导致内存溢出
OOM(OutOfMemoryError)
。 - 1)静态类型的对象的引用会导致Java内存泄漏。
预防方法:我们需要格外注意对关键词static
的使用,对任何集合或者是庞大的对象进行static
声明都会使其声明周期与JVM
的生命周期同步,从而使其无法回收。 - 2)第二种常见内存泄漏发生在对字符串的操作上,尤其是使用到
String.intern()
接口时。
预防方法:- 我们一定要记住
Interned String
是被储存在永久代(Java7
的版本)里的,如果我们想要对超大字符串进行操作,我们便需要增大原空间的内存大小。 - 第二种解决方法是使用
Java8
,永久代被原空间取代了,即使使用interned string
也不会发生OOM内存泄漏的情况。
- 我们一定要记住
- 3)忘记关闭流也是一种导致内存泄漏发生的常见情况。
Java7
由于引入了try-with-resources
以后部分解决了流未能关闭导致的内存泄漏。 - 4)未关闭连接,例如数据库,FTP服务器等连接会导致内存泄漏。
- 5)把没有
hashCode()
和equals()
的实例对象加到HashSet
里面,由于可以把重复的对象添加到集合中,从而导致内存泄漏。@Test(expected = OutOfMemoryError.class) public void givenMap_whenNoEqualsNoHashCodeMethods_thenOutOfMemory()throws IOException, URISyntaxException {Map<Object, Object> map = System.getProperties();while (true) {map.put(new Key("key"), "value"); //Object的`hashCode()`默认用地址求`hash`,//导致每次new Key("key")是不一样的对象} }
- 内存泄漏是指:对象已经不被使用,但对象仍存在被引用状态,导致垃圾回收器无法将其回收。久而久之,不能被回收的内存越来越多,最终导致内存溢出
【问】System.gc() 和 Runtime.gc() 会做什么事情?
Note:java.lang.System.gc()
只是java.lang.Runtime.getRuntime().gc()
的简写,两者的行为没有任何不同;System.gc()
开启回收器,主动通知虚拟机进行垃圾回收,但是回收器不一定会马上回收;
【问】串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?(单线程和多线程,
Parallel Scavenge
关注吞吐量优先;看前几问解析)【问】简述 Java 内存分配与回收策略以及 Minor GC 和 Major GC。(GC的执行流程,Minor GC 和 Major GC触发条件,见前几问解析)
【问】VM 的永久代中会发生垃圾回收么?(Major GC=Full GC,见前几问解析)
【问】Java 中垃圾收集的方法有哪些?(垃圾收集GC = 垃圾回收,包括对象已死算法;标记-清除,标记-复制,标记-压缩,见前几问解析)
【问】finalize() 方法什么时候被调用?析构函数 (finalization) 的目的是什么?,参考finalize()方法和finalization
Note:- 析构函数:是一个对象被撤销时自动调用的,析构与构造函数相反,当对象所在的函数一调用完毕,系统自动执行析构函数,往往用来做"清理善后"的工作;
- 每个对象的
finalize()
方法只能被执行一次,第二次就会直接跳过finalize()
方法,目的是避免对象无限复活(调用了finalize()
又有新的引用)。 finalize()
执行的时间是不固定的,由GC决定,极端情况下,没有GC就不会执行finalize()方法。由于只能被执行一次,因此不建议使finalize()
,交给GC即可。
【问】如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存?(不会立即,未达到触发GC的条件(G1垃圾回收器) / 或者说只有当用户线程运行到安全点(safe point)或者安全区域才会扫描对象引用关系(Stop The World, STW,比如serial,parNew,CMS))
【问】JMM和JVM的区别,可参考Java 内存模型(JMM)
Note:JMM
是围绕原子性,有序性、可见性展开。JMM描述了线程内的工作内存与主存之间的访问情况。JMM
与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM
中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。
【问】说一下 JVM 调优的工具?(
JConsole
看内存,JProfiler
看CPU资源,jmeter
是java测试工具),参考面试官:如何进行 JVM 调优(附真实案例),jmeter 入门到精通【问】常用的 JVM 调优的参数都有哪些?,参考面试官:如何进行 JVM 调优(附真实案例),JVM调优总结 -Xms -Xmx -Xmn -Xss
Note:通常来说,我们的 JVM 参数配置大多还是会遵循
JVM
官方的建议,例如:
-XX:NewRatio=2
:年轻代:老年代=1:2
-XX:SurvivorRatio=8
:eden:survivor=8:1
-Xmx3550m
:设置JVM最大可用内存为3550M
-Xms3550m
:设置JVM最小内存为3550M
-Xmn2g
:设置年轻代大小为2G。
-Xss128k
:设置每个线程的堆栈大小
堆内存设置为物理内存的3/4左右
等等当然,更重要的是,大部分的应用
QPS
都不到10,数据量不到几万,这种低压环境下,想让 JVM 出问题,说实话也挺难的。大部分同学更常遇到的应该是自己的代码 bug 导致OOM
、CPU load
高、GC
频繁啥的,这些场景也基本都是代码修复即可,通常不需要动 JVM。JVM 有哪些核心指标?合理范围应该是多少?
这个问题没有统一的答案,因为每个服务对
AVG/TP999/TP9999
等性能指标的要求是不同的,因此合理的范围也不同。为了防止面试官追问,对于普通的 Java 后端应用来说,我这边给出一份相对合理的范围值。以下指标都是对于单台服务器来说:
jvm.gc.time
:每分钟的GC耗时在1s以内,500ms以内尤佳jvm.gc.meantime
:每次YGC耗时在100ms以内,50ms以内尤佳jvm.fullgc.count
:FGC最多几小时1次,1天不到1次尤佳jvm.fullgc.time
:每次FGC耗时在1s以内,500ms以内尤佳
通常来说,只要这几个指标正常,其他的一般不会有问题,如果其他地方出了问题,一般都会影响到这几个指标。
JVM 核心指标配置监控告警:CPU指标,内存指标,GC指标等
更多内容整理参考
- Java面试题总结(分类版)
- 10万字208道Java经典面试题总结(附答案)
Java基础 - 易错知识点整理(待更新)相关推荐
- Java进阶3 - 易错知识点整理(待更新)
Java进阶3 - 易错知识点整理(待更新) 该章节是Java进阶2- 易错知识点整理的续篇: 在前一章节中介绍了 ORM框架,中间件相关的面试题,而在该章节中主要记录关于项目部署中间件,监控与性能优 ...
- Linux - 易错知识点整理(待更新)
Linux - 易错知识点整理(待更新) 本文根据CSDN Linux进阶技能树整理的易错知识点(带练),参考资料Linux常用命令大全(非常全!!!),Linux面试题(2020最新版)(带问/练) ...
- MySQL初阶 - 易错知识点整理(待更新)
MySQL初阶 - 易错知识点整理(待更新) Note:这里根据 CSDN Mysql技能树 整理的易错题,可参考MySQL 有这一篇就够,MySQL详细学习教程(建议收藏),MySQL 菜鸟教程 文 ...
- 苏大计算机考研 操作系统常见易错知识点整理
苏大计算机考研 操作系统常见易错知识点整理 大家好,我叫亓官劼(qí guān jié ),在CSDN中记录学习的点滴历程,时光荏苒,未来可期,加油~博主目前仅在CSDN中写博客,唯一博客更新的地 ...
- JavaScript 易错知识点整理
前言 本文是我学习JavaScript过程中收集与整理的一些易错知识点,将分别从变量作用域,类型比较,this指向,函数参数,闭包问题及对象拷贝与赋值这6个方面进行由浅入深的介绍和讲解,其中也涉及了一 ...
- JavaScript易错知识点整理
前言 本文是我学习JavaScript过程中收集与整理的一些易错知识点,将分别从变量作用域,类型比较,this指向,函数参数,闭包问题及对象拷贝与赋值这6个方面进行由浅入深的介绍和讲解,其中也涉及了一 ...
- Java基础易错面试题,初级程序员面试必看!(会不断更新)
写在前面: 我是「扬帆向海」,这个昵称来源于我的名字以及女朋友的名字.我热爱技术.热爱开源.热爱编程.技术是开源的.知识是共享的. 这博客是对自己学习的一点点总结及记录,如果您对 Java.算法 感兴 ...
- 081200计算机科学与技术——408计算机学科专业基础——操作系统,知识点整理【更新中】
文章目录 操作系统的定义 操作系统的功能和目标 操作系统的特征 操作系统的发展与分类 os的运行机制和体系结构 中断 系统调用 进程 进程的状态与状态转换 更新中_P10 操作系统的定义 操作系统(O ...
- 浮点数不能全等比较吗php,php的一些易错知识点整理 | 木凡博客
1. 取模运算结果的正负仅取决于被除数 被除数为正,结果为正:被除数为负,结果为负. echo ((-8)%3); // 将输出-2 echo (8%(-3)); // 将输出2 2 ...
- 南理工计算机考研877专业课——操作系统易错知识点整理
虚拟存储部分 页面分配策略: 局部置换 全局置换 固定分配 Y N 可变分配 Y Y 页面置换算法: 简单的CLOCK算法: 增加一个使用位,置换时扫描使用位为0的帧,并将使用位为1的置0. 改进的C ...
最新文章
- 明晚8点公开课 | 用AI给旧时光上色!详解GAN在黑白照片上色中的应用
- 基于WebSocket协议实现Broker
- 使用Scikit-learn,Spotify API和Tableau Public进行无监督学习
- 空间数据索引RTree完全解析及Java实现
- 消息队列NetMQ 原理分析2-IO线程和完成端口
- Spring Boot 是什么,有什么用。
- 关于Socket通信服务的心跳包(转) -感觉系统
- 标榜 AI 的百度又玩区块链,跟风布局“加密猫”?
- RAID5阵列掉盘显示未初始化---解决过程
- 阿里天猫亿级浏览型网站静态化架构演变
- 大象跳舞系列之Spark on HDInsight (1)
- python标准库math中用来计算平方根的函数是_Python程序设计的复习题资料合集免费下载...
- 计算机毕设周记20篇,电子与计算机毕业设计周记.doc
- windows 技巧篇-解除共享文件夹占用方法,解决共享文件被占用导致不可修改问题,查看共享文件被谁占用方法
- java PDF模板生成并导出(文字、表格、图片)
- UVA815 洪水Flooded
- 基于语义关联的中文查询纠错框架
- SafePoint是什么
- rtk定位权限_RTK定位原理概述
- 群晖ds216j如何安装迅雷软件
热门文章
- ECharts百度图表
- 网易视频云技术分析:IOS工程常见问题解决方法
- 高中计算机生涯规划,计算机职业生涯规划书
- c++程序添加资源文件及释放文件
- 行业认证标准:IEC 61508电气/电子产品功能安全“通用”国际标准
- Android Killer反编译apk报错
- 微型计算机原理第三版期末试题,《微机原理A (闭卷)》期末试题含答案.doc
- c语言中的空字符常量,C ++中的空字符常量
- 模拟c语言的软件下载,c语言软件下载(C/C++模拟学习)
- python webqq机器人_使用Python的Tornado框架实现一个简单的WebQQ机器人