Note:这里根据 CSDN Java技能树 整理的Java易错题(不带问
),以及摘录了博主"爱编程的大李子",“哪吒” ,JavaGuide博客的Java面试题整理(带问
),学习过程中可以参考Java8 API在线文档。
Java进阶知识点 参考 Java进阶 - 知识点整理(待更新)
JVM
有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果,因此对于第一次编译(javac
)的相同的字节码(.class
文件),在第二次编译(应该说是“解释”)时是在JVM
中执行,为的是输出适应不同平台的机器码,实现跨平台;JDK
是 Java Development Kit 缩写,它是功能齐全的 Java SDK。它拥有 JRE
所拥有的一切,还有编译器(javac
)和工具加粗样式(如 javadoc
和 jdb
)。它能够创建和编译程序。JRE
是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。JVM
可以理解的代码就叫做字节码(即扩展名为 .class
的文件,是由javac
编译成的中间代码,属于第一次编译成的文件),它不面向任何特定的处理器,只面向虚拟机。JIT
(just-in-time compilation) 编译器属于运行时编译,会将.class
文件编译成机器码;当 JIT
编译器完成第一次编译(第二次编译)后,其会将字节码对应的机器码保存下来,下次可以直接使用。.java
源代码 → \rightarrow → javac
编译 → \rightarrow → .class
字节码 → \rightarrow → JVM
(JIT
解释器) → \rightarrow → 机器码default
冲突解决更多内容整理 参考《我要进大厂》- Java基础夺命连环18问,你能坚持到第几问?(基础概念 | 基本语法 | 基本数据类型)
''
,字符串用双引号""
;字符相当于整型值,可运算,而字符串是一个对象,代表一个地址值)类名.方法名
的方式,也可以使用 对象.方法名
的方式;对象.方法名
这种方式;String... args
,要求只能作为函数的最后一个参数;在使用重载方法时,优先使用固定参数的方法,而非可变长参数方法)char
(2 byte) + bool
(1bit))JVM
栈中的局部变量表中,成员变量(未被static
修饰 )存放在JVM
堆中;包装类型属于对象类型,对象实例都存在JVM
堆中)Integer
的缓存机制让我在进行整数判断时踩了大坑,对于包装类值的比较,建议要么先强转成基本类型,要么使用equals
)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);// 输出 true
Integer i1 = 40; //使用缓存中的包装类
Integer i2 = new Integer(40); //创建新的包装类
System.out.println(i1==i2); // 输出 false
Float i11 = 333f;
Float i22 = 333f;
System.out.println(i11 == i22);// 输出 false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 输出 false
String username,address,phone,tel; // 声明多个变量
int num1=12,num2=23,result=35; // 声明并初始化多个变量
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];
更多内容整理 参考《我要进大厂》- 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:
【问】接口和抽象类有什么共同点和区别?(共同点是不能被实例化;含抽象方法;可以有默认实现(java8在接口中用default
实现))
Note - 区别:
Java
直接不支持);java8
引入了default
,需要解决冲突);一个类实现的多个接口,有相同签名的default方法会怎么办java8
引入了default
,需要解决冲突);Java中的接口可以多继承以及 default
冲突解决public static final
类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default
,可在子类中被重新定义,也可被重新赋值。【问】Java类实现某个接口后,是否需要实现接口中的所有方法?
Note:
N
个实现类)ThreadPoolExecutors
中,ThreadPoolExecutors
实现了Executor
接口的execute
方法,而其继承的抽象父类AbstractExecutorService
则实现了Executor
接口的submit
方法,没有实现execute
方法。【问】深拷贝和浅拷贝区别了解吗?什么是引用拷贝?(引用拷贝即直接赋值,实现对象的引用;浅拷贝对引用对象里基本类型数据直接拷贝,对其引用类型数据不进行拷贝;深拷贝则对所有数据类型都进行拷贝),参考一文读懂Java深拷贝浅拷贝引用拷贝
【问】为什么要使用克隆?如何实现对象克隆?(保留原有的对象进行接下来的操作;实现Cloneable
接口,重写clone
方法实现浅 / 深拷贝;实现Serializable
接口,进行序列化,完成深拷贝),参考java重写clone()方法
Note:
Clonable
接口时,重写Object
的clone
方法,并转化成子类类型对象(Person p = (Person) super.clone()
),但如果子类对象中有引用类型(Address
),新clone
的对象p
与原型对象所指向的Address
对象是相同的。Person
对象的所有引用类型都要重写clone
方法,比如 p=(Person)super.clone();p.address = address.clone();
clone
方法,通过该方法进行对象的拷贝,Java提供了一个Cloneable
接口来标示这个对象是可拷贝的,Cloneable
接口只是一个标记作用,在JVM
中具有这个标记的对象才有可能被拷贝。在调用clone()
方法时,JVM
会根据这个标识在堆内存中以二进制的方式拷贝对象,重新分配一个内存块,并没有执行构造函数(new
)【问】java为什么要使用序列化?如何实现序列化?序列化ID的作用?,参考java序列化详解,java序列化看这篇就够了
Note:
RMI
(remote method invoke,即远程方法调用),传入的参数或返回的对象都是可序列化的,否则会出错;所有需要保存到磁盘的java
对象都必须是可序列化的。通常建议:程序创建的每个JavaBean类都实现Serializeable接口;参考RPC实现原理之核心技术-序列化,老王讲自制RPC框架.(四.序列化与反序列化),java序列化看这篇就够了Serializable
接口,为每个属性添加get,set
方法。outputstream.writeObject(object)
保存对象,通过Object object = inputstream.readObject()
读取对象private static final long serialVersionUID
来验证版本一致性的,在进行反序列化时,JVM
会把传进来的字节流中的serialVersionUID
与本地实体类中的serialVersionUID
进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。Protobuf
是 Google
推出的开源序列库,它是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持 Java
、Python
、C++
、Go
等多种语言;框架比如流行的protostuff
总结一些优点如下:get, set
方法或其它无关序列化内部定义的方法(比如readObject
,writeObject
是内置的序列化方法),序列化也不需要get set方法支持,反序列化是构造对象的一种手段。Protostuff
反序列化时并不要求类型匹配,比如包名、类名甚至是字段名,它仅仅需要序列化类型A
和反序列化类型B
的字段类型可转换(比如int
可以转换为long
)即可。.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
可根据具体的返回类型动态指定结果的数据类型;构建集合工具类(参考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
成员和方法,子类的对象都可以访问到。default
访问模式。该模式下,只允许在同一个包中进行访问,访问权限仅次于private
。关于抽象类的描述,可参考Java面向对象
实现抽象类方法(抽象类方法重写,子类不带abstract)
关于接口的描述(接口不能有构造方法),可参考Java:接口和抽象类,傻傻分不清楚?,Java中接口的default方法(Java8为了兼容源代码提出的)
匿名内部类(该题把java8中的lambda表达式看做是匿名内部类的语法糖,实现某个抽象类或接口的方法),可参考深入理解java嵌套类、内部类以及内部类builder构建构造函数,Lambda表达式与匿名内部类
更多内容整理 参考
String
字符串不可变;StringBuffer
和StringBuilder
都继承于AbstractStringBuilder
,其中用字符数组保存字符串,可通过append、insert
来修改字符串内容而非引用,数组可扩容,相比String
节省常量池空间;StringBuffer
加锁(Synchronized
)线程安全,StringBuilder
线程不安全但效率高,可用ThreadLocal
解决线程安全问题),可参考彻底弄懂StringBuffer与StringBuilder的区别StringBuffer
与StringBuilder
的应用场景
StringBuffer
多线程安全,但是对buffer
缓冲区加了synchronized,其效率低。故适用于多线程下,并发量不是很高的场景StringBuilder
没有加任何锁,其效率高,适用单线程场景,但同时也适用于高并发场景中,提高高并发场景下程序的响应性能,至于线程安全问题可以通过其它手段解决,如ThreadLocal
,CAS
操作等。String
、StringBuilder
与 StringBuffer
的实现改用 byte
数组存储字符串(byte8字节,char 16字节,用Latin1
进行编码)。final
是根本原因)+
和+=
是java中唯二的两个重载的运算符,+
其实是通过StringBuilder
的append
实现)+
运算,则会创建多个StringBuilder
对象;如果直接使用StringBuilder
对象就不会有这个问题。Object
的equals用于比较地址是否相同;String
的重写了Object
的equals,用于比较两字符串内容是否相等)String
创建的一块内存区域,内容相同的字符串其地址相同,减少内存开销)// 在堆中创建字符串对象”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
方法有什么作用?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
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";
}
更多内容整理 参考《我要进大厂》- Java基础夺命连环14问,你能坚持到第几问?(Object类 | String类)
Throwable
接口;Exception
为程序可以处理的异常,可用catch
捕获;而Error
属于程序无法处理的错误,不建议用catch
捕获(捕获后也没用),例如虚拟机内存不够错误(OutOfMemoryError
,StackOverFlowError
)、类定义错误(NoClassDefFoundError
),此时JVM会选择终止线程)Checked Exception
(检查型异常)也称编译时异常,比如除了RuntimeException
及其子类外,其余的Exception
都是检查型异常,如果没有被catch / throw
则没法编译通过;RuntimeException
及其子类都为Unchecked Exception
(非检查型异常),即运行时异常,常见的非检查型异常包括:
NullPointerException
(空指针错误)IllegalArgumentException
(参数错误比如方法入参类型错误)NumberFormatException
(字符串转换为数字格式错误,IllegalArgumentException
的子类)ArrayIndexOutOfBoundsException
(数组越界错误)ClassCastException
(类型转换错误)String getMessage()
: 返回异常发生时的简要描述String toString()
: 返回异常发生时的详细信息void printStackTrace()
: 在控制台上打印 Throwable
对象封装的异常信息throw
用于方法体内的异常处理;throws
作用于方法声明上,用于向外抛出异常)final
关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。final
类型,避免无意修改导致逻辑混乱,特别是Session级的常量或变量finally
用于抛异常,finally代码块内语句无论是否发生异常,都会执行finally,常用于一些流等资源的关闭。比如InputStream
、OutputStream
、Scanner
、PrintWriter
等的资源都需要我们调用close()
方法来手动关闭;finalize
方法用于垃圾回收,有些时候需要实现finalize
方法来关闭整个生命周期内存在的资源(不推荐使用);在调用finalize
时并不意味着gc
会立即回收对象,而在真正调用时可能该对象不需要被回收了。catch
可省略)try
块用于捕获异常。其后可接零个或多个 catch
块,用于捕获try
块的异常,如果没有 catch
块,则必须跟一个 finally
块;try
块或 catch
块中遇到 return
语句时,finally
语句块将在方法返回之前被执行。finally
中的代码一定会执行吗?(不一定,在finally之前System.exit(1)
,释放CPU或者线程关闭时finally
块不执行)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();
}
NumberFormatException
的异常就不适合用其父类,比如Exception
来抛出)finally
有return
会覆盖catch
里的throw
,同样如果finally
里有throw
会覆盖catch
里的return
。catch
里和finally
都有return
, finally
中的return
会覆盖catch
中的。throw也是如此。更多内容整理 参考
【问】Java 集合概览(集合包括Collection
(存储单一元素)和Map
(存储键值对),Collection
最主要的子接口包括List
、Set
和 Queue
;LinkedList
既实现了List
接口,又实现了queue
接口)
【问】说说 List, Set, Queue, Map 四者的区别?(list
有序可重复;set
无序不可重复;queue
有序可重复,指定排队规则;Map
中key
是无序不可重复的,value
是无序可重复的)
【问】集合框架底层数据结构总结
Note:
ArrayList
和Vector
使用Object[]
对象数组;LinkedList
使用双向链表(java7去掉了循环链表)HashSet
(无序,唯一)使用HashMap
实现;LinkedHashSet
是HashSet
子类,基于LinkedHashMap
实现;TreeSet
(有序,唯一)基于红黑树(自平衡的排序二叉树)实现;PriorityQueue
使用Object[]
对象数组实现二叉堆;ArrayQueue
基于Object[]
数组和双指针实现;1)Java1.8之前HashMap
由数组+链表组成;
2)Java1.8之后HashMap
在解决哈希冲突时有了较大的变化,先判断是否扩容数组,再将链表转为红黑树:
当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间(二叉查找树在某种意义上会退化成线性结构,搜索效率低)。
LinkedHashMap
继承自 HashMap
,由数组+链表/红黑树组成,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序(不能保证元素上的有序)。
Hashtable
由数组+链表组成的,链表主要为了解决哈希冲突而存在的
TreeMap
基于红黑树实现(自平衡的排序二叉树)
【问】如何选用集合?(根据集合的特点来选用,比如在使用键值对时,为了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
并重写compareTo()
,可以对实体的不同对象按某种规则进行比较,一般配合TreeMap
,Arrays.sort()
使用。 class Person implements Comparable<Person>{
...
@Override
public int compareTo(Person o) {
// TODO Auto-generated method stub
if(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>{
//按照姓名进行排序
@Override
public int compare(Person p1, Person p2) {
// TODO Auto-generated method stub
//String实现了Serializable, CharSequence, Comparable
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
。Hashtable
默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1
。HashMap
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2
倍。Hashtable
会直接使用你给定的大小,而HashMap
会将其扩充为 2
的幂次方大小(HashMap
中的tableSizeFor()
方法保证)。也就是说 HashMap
总是使用 2 的幂作为哈希表的大小。hash%length
的方式来计算数组下标,而对hash%length
的计算则等价于(length - 1)& hash
(前提是 length
是 2 的 n 次方),参考HashMap 的长度为什么是 2 的幂次方【问】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>() {
@Override
public 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;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 entry = iterator.next()
”把对象的 key
和 value
值都放入到了 Entry
对象中;map.remove()
来删除数据,这是非安全的操作方式,但我们可以使用迭代器的 iterator.remove()
的方法来删除数据,这是安全的删除集合的方式。Lambda
中的 removeIf
来提前删除数据,或者是使用 Stream
中的 filter
过滤掉要删除的数据进行循环,这样都是安全的(不能在lambda
循环或者Stream
循环中删除元素,非安全),当然我们也可以在 for 循环前删除数据再遍历也是线程安全的。【问】为什么在同时进行List的遍历和删除操作时会抛出 ConcurrentModificationException(索引值modCount未修正),以及Iterator、正常for循环为什么可以解决这个问题
Note:
hasnext()
和next()
,只不过调用next()
时会调用checkForComodification()
,用于比较modCount
和expectedModCount
是否相等,如果不等则抛出java.util.ConcurrentModificationException
异常。而在调用remove(obj)
时,会调用fastremove
自增modCount
,从而导致不一致。remove(index)
或者在iterator中remove()
则不会出现异常;Iterator的remove()
每次删除一个元素,都会将modCount
的值重新赋值给expectedModCount
,这样2个变量就相等了,不会触发java.util.ConcurrentModificationException
异常。【问】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
和synchronizedList(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()方法的使用(如果值存在则不使用默认值)
更多内容整理 参考
transient
关键字修饰变量不被持久化,反序列化时则置为类型的默认值,比如int为0;static
修饰的变量不属于对象,也不被持久化;transient
无法修饰类或方法)Scanner
用 nextline()
/ BufferReader
用readline()
)常见使用的字符流类包括:
常见使用的字节流类包括:
其中Reader
,Writer
,InputStream
,OutputStream
是实现了Closeable
接口的抽象类,而后者分别是前者的实现类。
IO流按操作对象分类结构图
createFile
,createDirectory
,write
,read
,copy
等)read()
和readline()
方法对比分析更多内容整理 参考 《我要进大厂》- Java基础夺命连环9问,你能坚持到第几问?(反射 | 注解 | IO )
java.io
包,它基于流模型实现,提供了我们最熟知的一些IO功能,比如File抽
象、输入输出流等,交互方式是同步、阻塞的方式。也就是说,在读取输入流或者写入输出流
时,在读、写动作完成之前,线程会一直阻塞在那里,比如客户端向服务器发送请求,客户端线程需要阻塞,一直等待服务器的响应。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
实现多路复用。thread1
不会一直等待Selector
,而是通过创建另一个thread2
来监听selector
,然后等待thread2
的返回结果,如果thread1
执行完还没等到thread2
的结果,则会阻塞),当后台处理完成,操作系统会通知相应线程进行后续工作。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
方法。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);
}
}
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 {
@Override
public void add() {
System.out.println("添加功能");
}
@Override
public void delete() {
System.out.println("删除功能");
}
@Override
public void update() {
System.out.println("修改功能");
}
@Override
public void find() {
System.out.println("查找功能");
}
}
class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public 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();
}
}
Spring
的时候 ,一个@Component
注解就声明了一个类为 Spring Bean
(IOC
容器最基本的技术就是“反射(Reflection)”编程),通过一个 @Value
注解就读取到配置文件中的值,其实背后是基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。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()
时会通过反射来调用。AOP
)使用及实现原理分析更多内容整理 参考 《我要进大厂》- Java基础夺命连环9问,你能坚持到第几问?(反射 | 注解 | IO )
@Target
:注解的作用目标@Retention
:注解的生命周期,与JVM有关@Documented
:注解是否应当被包含在 JavaDoc(API) 文档中@Inherited
:是否允许子类继承该注解@Override
,@Deprecated
,@SuppressWarnings
Class
类通过实现AnnotatedElement
接口来反射注解:Java反射机制解析注解主要是通过java.lang.reflect
包下的提供的AnnotatedElement
接口,Class
实现了该接口定义的方法,返回本元素/所有的注解(Annotation接口)。@Hello(value = "hello")
。RUNTIME
的注解取出来放到一个 map 中,并创建一个 AnnotationInvocationHandler
实例,把这个 map
传递给它。RetentionPolicy.SOURCE
注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。参考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> {
@Override
public 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:
【问】为什么要使用线程池?(降低创建和销毁线程对象的次数,因此有了"池化资源"技术比如数据库连接池,线程池)
Note:线程池的作用
【问】Java 中线程池的四个基本组成部分(SingleThreadExecutor
、CachedThreadPool
、FixedThreadPool
和ThreadPoolExecutor
)
Note:
ThreadPool
):用于创建并管理线程池。包含 创建线程池,销毁线程池,加入新任务(线程池初始化时,poolsize
为0);PoolWorker
):线程池中线程,在没有任务时处于等待状态。能够循环的运行任务;Task
):每一个任务必须实现的接口,以供工作线程调度任务的运行。它主要规定了任务的入口,任务运行完后的收尾工作,任务的运行状态等(是直接交给工作线程执行,还是放入BlockingQueue
)。taskQueue
):用于存放没有处理的任务。提供一种缓冲机制。【问】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 // 拒绝任务处理器。由于超出线程数量和队列容量而对继续增加的任务进行处理的程序。
)
poolsize
为0),当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目(poolsize
)达到corePoolSize
后,就会把到达的任务放到缓存队列当中;BlockingQueue
),用来存储等待执行的任务,一般来说,这里的阻塞队列有以下几种选择:ArrayBlockingQueue; LinkedBlockingQueue; SynchronousQueue; 阻塞队列提供了可阻塞的 put
和 take
方法,它们与同步队列 offer
和 poll
是等价的。如果队列满则 put
阻塞,如果队列是空的则take
方法阻塞。
ArrayBlockingQueue
: 有界的数组队列(长度至少为1)LinkedBlockingQueue
: 可支持有界/无界的队列,使用链表实现(默认大小为Integer.MAX_VALUE
)PriorityBlockingQueue
: 优先队列,可以针对任务排序(无界)SynchronousQueue
: 同步队列长度为1(与其说是队列,不如说是个锁),不能peek()
查看队列元素;和Array有点区别就是:client thread提交到block queue会是一个阻塞过程,直到有一个worker thread连接上来poll task。ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
【问】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:
Executor
中声明的方法,在ThreadPoolExecutor
进行了实现,通过这个方法ThreadPoolExecutor
可以向线程池提交一个任务,交由线程池去执行。ExecutorService
中声明的方法,在AbstractExecutorService
就已经有了具体的实现,在ThreadPoolExecutor
中并没有对其进行重写,用来向线程池提交任务;submit()
会调用的execute()
,只不过它利用了Future
来获取任务执行结果。shutdown()
和shutdownNow()
是用来关闭线程池的。【问】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
各个方法的弊端:
newFixedThreadPool
和newSingleThreadExecutor
:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM
。newCachedThreadPool
和newScheduledThreadPool
:主要问题是线程数最大数是Integer.MAX_VALUE
,可能会创建数量非常多的线程,甚至OOM
。【问】Synchronized 用过吗,其原理是什么?(特性:可重入性,不可中断性;原理:在编译成字节码时,会通过字节码命令或者标志位判断告诉JVM,要访问的对象的monitor
(管程)是否被其它线程占用),可参考深入理解Java并发之synchronized实现原理
Note:
Synchronized
的3种应用方式:
monitor
)monitor
)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++;
}
}
}
monitorenter
指令时,当前线程将试图获取 objectref
(即对象锁) 所对应的 monitor
的持有权,当 objectref
的 monitor
的进入计数器为 0,那线程可以成功取得 monitor
,并将计数器值设置为 1,取锁成功。synchronized
方法)计数器的值也会加 1。monitorexit
指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor
。monitorenter
和 monitorexit
指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit
指令。synchronized
方法原理:通过方法表结构中的 ACC_SYNCHRONIZED
标识(flag
)来判断monitor
是否被其它线程占用。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
JOL
查看对象在堆内存的存储布局(前8位object header
为mark word
,后4位object header
为指针压缩后的klass word
):klass word
:
其中mark word
是后面学习并发编程,了解各种锁的基础。
三种锁的比较(分别对应不同的并发场景,锁的升级是动态自适应的,可以动态适应不同场景):
mark word
记录的线程ID
是否与第一次CAS时的一致来实现。只有存在竞争或者撤销次数达到阈值时才会解锁偏向锁,升级为轻量级锁。Mark Word
和lock Record
,以及synchronized
解决。synchronized
实现(最高级别的锁,更新Mark Word
状态,但不会操作lock Record
),底层原理为monitor
管程,阻塞队列(竞争的线程)和等待队列(wait()
被挂起)。synchronized
锁的升级只能从低级(偏向锁)升级为高级(重量级锁),主要通过mark word
锁状态和lock record
锁记录(线程ID地址),来监控当前对象是否存在线程间的竞争关系。
偏向锁:不会主动进行解锁,出现竞争或撤销次数达到阈值时才会解锁(101
→ \rightarrow →001
),这样做的目的是下一次同一个线程来获取锁时,直接检查mark word
的锁记录就可以了。
ID
放置在访问对象的Mark word
中时,下一次如果发现访问的线程ID
仍然是自己,则不再使用CAS,该对象由该线程所有,偏向锁状态设置成功(101
);偏向锁会在当前线程的栈帧中创建锁记录(Lock Record
),使这个锁记录指向锁对象;Mark word
)设置为无锁状态(001
),从偏向锁升级为轻量锁,即00
(并发情况中等)Mark word
)设置为无锁状态(001
),再从偏向锁升级为重量级锁,即10
(并发情况严重)wait/notify
方法时,偏向锁直接升级为重量级锁(10
),因为只有重量级锁才有该方法。T1
执行了同步操作,也就是大量对象先偏向了T1
,T1同步结束后,另一个线程也将这些对象作为锁对象进行操作,会导致偏向锁重偏向的操作。mark word
的线程id
不是自己的,那么B线程就会向VM的线程队列发送一个撤销偏向锁的任务,VM线程会不断检测是否有任务要执行,当检测到这个任务后,就需要在安全点去执行(安全点时,JVM内的所有线程都会被阻塞,只有VM线程处于运行状态,它可以执行一些特殊的任务,如full gc
就是此时执行)轻量级锁:只能处理线程之间交替加锁的场景,通过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
底层原理),处于竞争的线程在等待队列或阻塞队列中。
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:
CAS
算法就很好理解,volatile
作用是禁止指令重排序,能够将线程在工作内存(新值B
)中的操作结果立刻写回主存(内存值V
)中,其他线程每次在读取时都能访问最新的值,但不能保证原子性;而且volatile
只能作用于变量 。volatile
修饰时,他会保证修改的值会立刻被更新到主存,当以后其他线程需要读取时,它会去内存中读取新值。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。【问】JVM 对 Java 的原生锁做了哪些优化?
Note:
JVM
中的JIT
编译器借助逃逸分析技术来判断对象是否存在线程同步的情况,是否只被一个线程访问,若不存在同步情况则可以取消锁,省去了加锁解锁的开销。append
方法用了 synchronized
关键字,如果只有一个线程对这个StringBuffer
对象进行操作时(不存在同步),在线程内部可以把StringBuffer
当做局部变量使用,StringBuffer
仅在方法内作用域有效,因此是线程安全的,可以进行锁消除。@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
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表示锁消除。
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
无锁和非阻塞,性能好,没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销。缺点:
Synchronized
了。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:
Synchronized
是java语言的关键字,是原生语法层面的互斥,需要JVM实现;ReentrantLock
是JDK1.5
之后提供的API层面的互斥锁,需要lock
和unlock()
方法配合try/finally
代码块来完成。Synchronized
使用较ReentrantLock
便利一些;ReentrantLock
强于Synchronized
;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 ReentrantReadWriteLock
implements 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
来存储,因此ThreadLocalMap
中不同的threadlocal
可以用于存储不同类型的对象(Entry
,Entry
等)
ThreadLocalMap
在处理冲突时:1)会先通过 key.threadLocalHashCode & (len-1);
计算当前ThreadLocal
的hash值,接着到数组中查找该位置是否为空:2)如果为空,则直接初始化Entry
并插入;3)如果非空,则比较当前位置上的key是否为当前的ThreadLocal
对象,如果是,则完成value
的更新;如果不是则通过线性探测法搜索下一个未冲突的位置。
ThreadLocal
适用于如下两种场景
场景包括:
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();//消费者的condition
Condition conditionP = lock.newCondition();//生产者的condition
....
}
synchronized/wait()
只有一个阻塞队列,notifyAll
会唤起所有阻塞队列下的线程,而使用lock/condition
可以实现多个阻塞队列,signalAll
只会唤起某个阻塞队列下的阻塞线程。【问】Thread 类中的 yield 方法有什么作用?,参考Thread.yield()详解
Note:
yield
是一个静态的原生(native
)方法;Thread.yield();
让当前线程从运行状态 转为 就绪状态(不是等待状态),把运行机会交给线程池中/其他拥有相同优先级的线程;yield()
达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。【问】Java 如何实现多线程之间的通讯和协作?(生产者消费者问题)
Note:
Java中线程通信协作的最常见的两种方式:
synchronized
加锁的线程,通过Object
类的wait()/notify()/notifyAll()
完成线程间的通讯;只能随机唤醒一个线程(notify)或者唤醒所有线程(notifyAll)ReentrantLock
类加锁的线程的,通过Condition
类的await()/signal()/signalAll()
完成线程间的通讯;可以分组唤醒需要唤醒的线程;【问】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
的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。当我们的程序是 IO 密集型时(如 web 服务器、网关等),为了追求高吞吐,有两种思路:
这两个方案,优缺点都很明显:方案1实现简单,但性能不高;方案2性能非常好,但实现起来复杂。
协程需要解决线程遇到的几个问题:
内存占用要小,且创建开销要小
减少上下文切换的开销:
降低开发难度
协程和线程的区别
更多内容整理 参考
Note:
filter
,map
,flatMap
等)和结束动作(reduce
,collect
,forEach
等)Sensor::getNum
表示Sensor
类中的getNum
方法filter()
传入的是Predicate super T>
断定式函数接口java.lang.IllegalStateException: stream has already been operated upon or closed
Optional
类是一个对象容器,可以对对象进行进一步封装,可以提示用户该对象是否为null
,并使用filter
, orElse
等进行处理,简化if else
代码。FilterInputStream
作为装饰器,内部封装了InputStream
基础构件,其子类BufferedInputStream
调用其read()
读取数据时会委托InputStream
基础构件来进行更底层的操作,而它自己所起的装饰作用就是缓冲)Builder
实现)Director
来指导不同相对独立的builder
(对应一个型号的汽车)工作。Cloneable
接口,完成对象快速复制)private static Lazy lazy; private Lazy(); private static Lazy getInstance();
,构造方法私有化,不让用户创建对象;饿汉式,懒汉式)Adapter
类对多个接口/类,或者1个复杂接口进行再次的结构性封装,并没有增加功能,以满足调用者的不同需求)
Source
类,拥有一个方法,待适配,目标接口是Targetable
,通过Adapter
类继承Source
并实现Targetable
,将Source
的功能扩展到Targetable
里。(当希望将一个类转换成满足另一个新接口的类时)Adapter
类作修改,这次不继承Source
类,而是持有Source
类的实例,以达到解决兼容性的问题。(一个对象转换成满足另一个新接口的对象时)Sourceable
的实现类时,必须实现该接口的所有方法,有些时候是不必要的;因此借助于一个抽象类,该抽象类Wrapper
实现了该接口,实现了所有的方法,而我们不和原始的接口打交道,只和该抽象类Wrapper
取得联系。(当不希望实现一个接口中所有的方法时)new
出来)Bridge
中用统一的Sourceable
接口接入source
实例)和 mysql和sqlserver(两实现,source
类,MyBridge
类)之间的关系FlyWeightFactory
负责创建和管理享元单元,当一个客户端请求时,工厂需要检查当前对象池中是否有符合条件的对象;适用于作共享的一些个对象,他们有一些共有的属性,就拿JDBC连接池来说,url、driverClassName、username、password及dbname,这些属性对于每个连接来说都是一样的,所以就适合用享元模式来处理,建一个工厂类,将上述类似属性作为内部数据,其它的作为外部数据)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
类中定义着观察者对象列表,可以对其进行修改(增加或删除观察者对象))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
之间的交互并不关心;注意区分中介模式和代理模式,前者是多个类之间的封装,而后者是两个类的封装)Context
类是一个上下文环境类,包含上下文各种信息,Plus
和Minus
实现Expression
接口(interpret
方法传入Context
对象),分别用来实现计算功能)更多内容整理参考
参考Java虚拟机(JVM)你只要看这一篇就够了!,全面阐述JVM原理
【问】说一下 JVM 的主要组成部分?及其作用?(JVM
包括类加载子系统、运行时数据区(堆、方法区、虚拟机栈、本地(native
)方法栈、程序计数器)、直接内存、垃圾回收器、执行引擎),可参考Java虚拟机(JVM)你只要看这一篇就够了!
Note:
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 运行时数据区?(运行时数据区包括堆、方法区、虚拟机栈、本地方法栈、程序计数器)
Note:堆(动,线程共享),区(静,线程共享),栈(动,线程私有),计数器(线程私有)
new
)JVM
加载的类信息,常量、静态变量、即时编译器JIT
编译后的代码。(反射,String
常量,对象类型数据)recursive
)
native
方法).class
文件)的行数。JVM
工作时就是通过改变这个计数器的值来选取下一个需要执行的字节码指令。(读取.class
文件的指令行)【问】对象的内存布局?(对象头,实例数据,对齐填充)
Note:
Mark Word
。第二部分是类型指针(Klass word
),即对象指向它的类的元数据指针,虚拟机通过这个指针确定这个对象是哪个类的实例。【问】关于对象的访问定位?
Note:
【问】什么是类加载器,类加载器有哪些?(启动类 / 扩展类 / 应用程序类加载器,了解之间的继承关系)
Note:
.class
文件字节码内容加载到内存中,并将这些静态数据转换成方法区运行时的数据,然后在堆中生成一个代表这个类的Java.lang.class
对象,作为方法区中类数据的访问入口。【问】说一下类加载的执行过程?(加载类对象,包括成员变量,成员方法等) ,参考类加载的过程
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
等。以下五种情况必须对类进行初始化(而加载、验证、准备自然需要在此之前完成):
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
指向这些临时对象,这些对象在下一次GC
的时候便会被回收。GC roots
。只要这个class
在,该引用指向的对象就一直存在,因此class
对象也有被回收的时候。classLoader
已经被回收java.lang.class
对象没有在任何地方被引用,也就是说无法通过反射访问该类的信息【问】java 中都有哪些引用类型?(强引用(不被GC
),软引用(内存够不被GC
),弱引用,虚引用
【问】说一下 JVM 有哪些垃圾回收算法?,可参考全面阐述JVM原理
Note:
GC
Major GC
),老生代大部分对象会存活,将不回收的对象压缩到内存一端,避免碎片化。【问】说一下 jvm 有哪些垃圾回收器?(单线程(停)/ 多线程(停)/ CMS(不停)/ G1;主要在并发,标记策略上存在不同),可参考全面阐述JVM原理
Note:
JDK1.3
之前,单线程回收器是唯一的选择,在它进行垃圾回收的时候,必须暂停其它所有的工作线程(Stop The World,STW),直到它收集完成。Serial
(新生代,使用标记-复制算法)和Serial Old
(老生代,使用标记-压缩算法),一般两者搭配使用。-XX:+UseSerialGC
开启串行垃圾回收器JDK1.5
之后出现的CMS搭配使用ParNew
(Serial
收集器的多线程版本),运行数量可以通过修改ParallelGCThreads
设定;Parallel Scavenge
: 关注吞吐量,吞吐量优先,吞吐量=代码运行时间/(代码运行时间+垃圾收集时间);用于新生代收集,复制算法。Parllel Old
:Parallel Scavenge
的老年代版本,JDK 1.6
开始提供的。GC Roots
能直接关联到的对象,会"Stop The World"。GC Roots Tracing
,可以和用户线程并发执行。full
)找不到足够的连续空间而提前触发GC(触发全局GC - Full GC
),这点优于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
;Eden + From Survivor
存活的对象放入 To Survivor
区;Eden + From Survivor
分区,From Survivor
和 To Survivor
分区交换;Minor GC
对象年龄就加1的对象年龄+1
,到达15
,升级为老年代,大对象会直接进入老年代;Full GC
),老年代一般采取标记-压缩算法。Minor GC
触发的条件:
Minor GC
时,对象大小大于To Survivor
可用内存,则会进入老年代;如果大于老年代剩余内存,则会Full GC
)Full GC
触发条件:
System.gc()
方法;Minor GC
后进入老年代的平均大小大于老年代的可用内存;Eden
区、From Survivor
区向To Survivor
区复制时,对象大小大于To Survivor
可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。【问】GC 是什么? 为什么要有 GC?(Gabage Collection垃圾收集,清除掉没用的对象,为新创建的对象腾出空间,看前几问解析)
【问】简述 Java 垃圾回收机制(GC对象的判定方法,3大垃圾回收算法,4大垃圾回收器,看前几问解析)
【问】如何判断一个对象是否存活?(GC 对象的判定方法:引用计数法,GC Roots
可达分析法;看前几问解析)
【问】Java 中会存在内存泄漏吗,请简单描述,可参考Java中的内存泄露问题
Note:
OOM(OutOfMemoryError)
。static
的使用,对任何集合或者是庞大的对象进行static
声明都会使其声明周期与JVM
的生命周期同步,从而使其无法回收。String.intern()
接口时。Interned String
是被储存在永久代(Java7
的版本)里的,如果我们想要对超大字符串进行操作,我们便需要增大原空间的内存大小。Java8
,永久代被原空间取代了,即使使用interned string
也不会发生OOM内存泄漏的情况。Java7
由于引入了try-with-resources
以后部分解决了流未能关闭导致的内存泄漏。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指标等
更多内容整理参考