Java 面试
Android面试 | Android面试2 | Kotlin | Kotlin面试 | Dart | Flutter | Flutter面试 | Hybrid App
1 Java 基础
Java基础部分,包括语法基础,泛型,注解,异常,反射和其它(如SPI机制等)。
1.1 语法基础
能在 Switch 中使用 String 吗?
从 Java 7 开始,我们可以在 switch case 中使用字符串,但这仅仅是一个语法糖。内部实现在 switch 中使用字符串的 hash code。
switch能否用String做参数?
Java1.7开始支持,但实际这是一颗Java语法糖。除此之外,byte,short,int,枚举均可用于switch,而boolean和浮点型不可以。
内部实现在 switch 中使用字符串的 hash code。
switch 是否能作用在byte 上,是否能作用在long 上,是否能作用在String 上?
在 Java 5 以前,switch(expr)中,expr 只能是 byte、short、char、int。从 Java 5 开始, Java 中引入了枚举类型, expr 也可以是 enum 类型, 从 Java 7 开始, expr 还可以是字符串( String), 但是长整型( long) 在目前所有的版本中都是不可以的。
用最有效率的方法计算 2 乘以 8?
2 << 3( 左移 3 位相当于乘以 2 的 3 次方, 右移 3 位相当于除以 2 的 3 次方) 。 补充: 我们为编写的类重写 hashCode 方法时, 可能会看到如下所示的代码, 其实我们不太理解为什么要使用这样的乘法运算来产生哈希码( 散列码), 而且为什么这个数是个素数,为什么通常选择 31 这个数? 前两个问题的答案你可以自己百度一下,选择 31 是因为可以用移位和减法运算来代替乘法,从而得到更好的性能。说到这里你可能已经想到了: 31 * num 等价于(num << 5) - num, 左移 5 位相当于乘以 2 的 5 次方再减去自身就相当于乘以 31,现在的VM 都能自动完成这个优化。
Math.round(11.5) 等于多少?Math.round(- .5)等于多少?
Math.round(11.5)的返回值是 12, Math.round(-11.5)的返回值是-11。四舍五入的原理是在参数上加 0.5 然后进行下取整。
final、finalize 和 finally 的不同之处?
- final 是一个修饰符,可以修饰变量、方法和类。如果 final 修饰变量,意味着该变量的值在初始化后不能被改变。
- Java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的,但是什么时候调用 finalize 没有保证。
- finally 是一个关键字,与 try 和 catch 一起用于异常的处理。finally 块一定会被执行,无论在 try 块中是否有发生异常。
序列化
声明为static和transient类型的数据不能被序列化, 反序列化需要一个无参构造函数
Java 中,Serializable 与 Externalizable 的区别?
Serializable 接口是一个序列化 Java 类的接口, 以便于它们可以在网络上传输或者可以将它们的状态保存在磁盘上,是 JVM 内嵌的默认序列化方式,成本高、脆弱而且不安全。 Externalizable 允许你控制整个序列化过程, 指定特定的二进制格式, 增加安全机制。
for循环中++i 和 i++ 的区别
for循环的语法定义 ++i 和 i++的结果是一样的,都要等代码块执行完毕才能执行语句3,但是性能是不同的。
在大量数据的时候++i的性能要比i++的性能好原因:
i++由于是在使用当前值之后再+1,所以需要一个临时的变量来转存。
而++i则是在直接+1,省去了对内存的操作的环节,相对而言能够提高性能
Java移位运算符?
java中有三种移位运算符
<<:左移运算符,x << 1,相当于x乘以2(不溢出的情况下),低位补0>>:带符号右移,x >> 1,相当于x除以2,正数高位补0,负数高位补1>>>:无符号右移,忽略符号位,空位都以0补齐
&和&&的区别?
&运算符有两种用法:(1)按位与; (2)逻辑与。&&运算符是短路与运算。逻辑与跟短路与的差别是非常巨大的, 虽然二者都要求运算符左右两端的布尔值都是 true 整个表达式的值才是 true。&&之所以称为短路运算是因为,如果&&左边的表达式的值是 false, 右边的表达式会被直接短路掉, 不会进行运算。很多时候我们可能都需要用&&而不是&,例如在验证用户登录时判定用户名不是 null 而且不是空字符串, 应当写为: username != null &&!username.equals(“” ), 二者的顺序不能交换,更不能用&运算符,因为第一个条件如果不成立,根本不能进行 字符串的 equals 比较, 否则会产生 NullPointerException 异常。注意: 逻辑或运算符( |) 和短路或运算符( ||) 的差别也是如此。 补充: 如果你熟悉 JavaScript, 那你可能更能感受到短路运算的强大, 想成为 JavaScript 的高手就先从玩转短路运算开始吧。
谈谈 Java 中的访问修饰符(public、private、protected、default)的作用范围和使用场景。
访问修饰符
Java 中提供了四种访问级别控制符,它们定义了类成员(如字段、方法)的可见性。
修饰符作用范围public在任何地方都可以被访问,包括其他包。protected在同一包或子类中可以被访问。default在同一包中可以被访问,没有明确声明访问修饰符时,默认是 default。private只能在定义它的类内部被访问。
使用场景
- public:当需要让一个类或者方法在任何地方都能被访问时使用。
- protected:当希望允许子类访问父类的某些属性或方法时使用。
- default:当希望仅在同一个包内提供访问权限时使用。
- private:当希望确保某个属性或方法只能在其所在类内部访问时使用。
final,finally,finalize的区别
final、finally 和 finalize 在Java中有不同的含义和用途:
关键字作用示例final用于声明变量、方法或类为最终的,不可更改。final int x = 10;finally用于异常处理中,保证无论是否发生异常都会执行的代码块。java try { ... } catch (Exception e) { ... } finally { ... }finalize已过时的方法,允许对象在垃圾回收前进行清理工作。java protected void finalize() throws Throwable { ... }
序列化的方式
序列化 是将对象的状态信息转换为可以存储或传输的形式的过程。Java提供了几种序列化方式:
- Java序列化 (
Serializable接口):Java标准库提供的序列化方式。 - XML序列化:使用XML格式进行序列化。
- JSON序列化:使用JSON格式进行序列化。
- Protocol Buffers:Google开发的高效序列化方式。
- Kryo:一种高效的二进制序列化框架。
- Hessian:一种轻量级的二进制序列化方式。
Serializable 和Parcelable 的区别
Serializable 和 Parcelable 都是用来实现对象序列化的接口,但它们在使用上有显著的区别:
接口优点缺点Serializable实现简单,不需要额外实现方法。性能较差,序列化过程较慢。Parcelable性能更高,适合Android应用中的对象传递。实现较为复杂,需要实现一系列方法。
如何将一个Java对象序列化到文件里?
Java提供了 Serializable 接口来实现对象的序列化。序列化的步骤如下:
- 实现
Serializable接口。 - 使用
ObjectOutputStream将对象写入文件。 - 使用
ObjectInputStream从文件中读取对象。
什么是Java的序列化和反序列化?
Java序列化是将对象的状态信息转换为字节流的过程,这样可以将对象存储到文件中或通过网络传输到其他地方。反序列化是序列化的逆过程,即将字节流恢复为原来的对象。
序列化的主要目的是为了实现对象的持久化和网络传输。通过实现java.io.Serializable接口,一个类的对象就可以被序列化。序列化时,对象的类信息、成员变量等都会被保存到一个流中。反序列化时,根据这些信息重建对象。
Java提供了多种序列化和反序列化的API,包括ObjectOutputStream和ObjectInputStream等。序列化和反序列化过程中,需要注意版本兼容性问题,因为类的变更可能会影响到已序列化对象的读取。
1.2 数据类型
解释 Java 中的自动装箱和拆箱机制,并举例说明。
自动装箱与拆箱机制
在 Java 中,基本数据类型(如 int、double)和它们对应的包装类(如 Integer、Double)之间可以进行自动转换。这种转换分为两种情况:
- 自动装箱:将基本数据类型自动转换为对应的包装类对象。
- 自动拆箱:将包装类对象自动转换为基本数据类型。
注意事项
- 自动装箱/拆箱发生在基本数据类型和其对应的包装类之间。
- 这种机制简化了编码过程,但需要注意性能影响,尤其是频繁操作大量对象时。
a = a + b 与 a += b 的区别
在 Java 中,a = a + b 和 a += b 的核心区别集中在 类型转换规则 和 引用类型的行为 两方面,尤其是强类型特性导致的类型转换差异,是开发中最易踩坑的点。以下结合 Java 语法规范和实际场景,详细解析两者的区别:
核心差异总览表
| 对比维度 | a = a + b | a += b |
|---|---|---|
| 类型转换规则 | 不自动窄化转换,需显式强制转换(不同类型运算时) | 隐含自动窄化转换(Java 语法规范定义),无需手动转换 |
| 运算与赋值逻辑 | 先计算 a + b 生成新值,再赋值给 a | 等价于 a = (T) ((a) + (b))(T 是 a 的类型),直接更新 a |
| 基本类型内存行为 | 生成临时值,再覆盖 a 的内存(效果同 +=) | 直接在 a 的内存地址上更新(无额外临时值,效率略优) |
| 引用类型(如 String) | 生成新对象,a 指向新对象(原对象不变) | 语法糖,等价于 a = a + b(仍生成新对象) |
| 数组 / 集合(可变引用) | 生成新对象(原对象不变),a 指向新对象 | 仅部分类型支持(如 StringBuilder),直接修改原对象 |
分场景详细解析
场景 1:基本类型(int、short、long、float 等)—— 核心差异是类型转换
Java 是强类型语言,基本类型运算时会遵循 “类型提升规则”,而 += 会隐含强制转换,a = a + b 则不会。
1.1 同类型运算(无类型转换)
两者效果完全一致,无差异:
int a = 10;
int b = 20;
a = a + b; // a = 30(计算 10+20 得 30,赋值给 a)
a += b; // a = 50(直接累加 20,结果 50)
- 原理:同类型运算不会触发类型提升,
a + b的结果类型与a一致,赋值无冲突。
1.2 不同类型运算(涉及类型提升与窄化转换)
这是最关键的区别:a += b 会自动将运算结果 窄化转换 为 a 的类型,而 a = a + b 需显式强制转换,否则编译报错。
示例:a 为 short(16 位),b 为 int(32 位)
short a = 1;
int b = 2;
// 1. a = a + b:编译报错!
// 原因:short + int 会触发「类型提升」—— short 自动转为 int,结果为 int 类型(32位)
// 直接将 int 赋值给 short(16位),可能丢失精度,Java 不允许隐式转换
a = a + b; // Error: 不兼容的类型: 从 int 转换到 short 可能会有损失
// 正确写法:显式强制转换(手动将 int 转为 short)
a = (short) (a + b); // 编译通过,结果 a = 3
// 2. a += b:编译通过,自动窄化转换
a += b; // 等价于 Java 语法规范定义的:a = (short) ((a) + (b))
// 隐含强制转换,无需手动写 (short)
1.3 溢出风险(窄化转换的副作用)
a += b 的自动窄化转换可能导致溢出(超出目标类型的取值范围),需注意:
short a = Short.MAX_VALUE; // short 最大值:32767
int b = 1;
a += b; // 等价于 a = (short) (32767 + 1)
System.out.println(a); // 输出 -32768(溢出,因为 short 是有符号16位,最大值+1后溢出为最小值)
- 而
a = a + b若不手动强制转换,编译报错,反而能避免无意识的溢出。
场景 2:引用类型(String、数组、StringBuilder 等)
引用类型的差异取决于类型是否 “可变”:不可变类型(如 String)中两者行为一致,可变类型(如 StringBuilder)中 += 直接修改原对象。
2.1 不可变引用类型(String)
String 是不可变对象(创建后内容无法修改),a = a + b 和 a += b 均生成新对象,效果完全一致(+= 是语法糖):
String a = "Hello";
String b = "World";
// 1. a = a + b:生成新 String 对象
a = a + b; // 原 "Hello" 对象被丢弃,a 指向新对象 "HelloWorld"
// 底层:通过 StringBuilder 拼接(new StringBuilder(a).append(b).toString()),产生临时对象
// 2. a += b:语法糖,等价于 a = a + b
a += b; // 同样生成新对象 "HelloWorldWorld",与上一行效果一致
- 注意:频繁用
+=拼接 String 会产生大量临时对象,效率较低,建议用StringBuilder。
2.2 可变引用类型(StringBuilder、数组)
- StringBuilder:
+=等价于append()方法,直接修改原对象;a = a + b会生成新对象:
StringBuilder a = new StringBuilder("Hello");
String b = "World";
// 1. a += b:等价于 a.append(b),直接修改原对象
a += b; // 原对象内容变为 "HelloWorld",a 仍指向原对象(无新对象生成)
// 2. a = a + b:生成新 StringBuilder 对象
a = a + b; // 等价于 new StringBuilder(a).append(b),a 指向新对象
- 数组:Java 数组长度不可变,
+=不直接支持(需手动实现),但a = a + b会生成新数组:
int[] a = {1, 2};
int[] b = {3, 4};
// a += b:编译报错(数组不支持 += 运算符)
// 需手动实现数组拼接,生成新数组
a = ArrayUtils.addAll(a, b); // 第三方工具类,生成新数组 [1,2,3,4],a 指向新数组
场景 3:特殊引用类型(包装类 Integer、Long 等)
包装类是不可变类型,且不支持 += 运算符的 “原地修改”(因为包装类对象不可变),a += b 本质是拆箱→运算→装箱→赋值,与 a = a + b 效果一致:
Integer a = 10; // 自动装箱(Integer.valueOf(10))
int b = 20;
a += b; // 等价于:a = Integer.valueOf(a.intValue() + b)
// 拆箱(a.intValue())→ 运算(30)→ 装箱(Integer.valueOf(30))→ 赋值给 a
// 原 Integer 对象 10 被丢弃,a 指向新的 Integer 对象 30
a = a + b; // 效果完全一致,同样拆箱→运算→装箱→赋值
关键结论与开发建议
- 基本类型优先关注类型转换:
- 不同类型运算时,
a += b自动窄化转换(可能溢出),a = a + b需显式强制转换(更安全)。 - 同类型运算时,两者效果一致,
+=语法更简洁。
- 引用类型关注 “可变 / 不可变”:
- 不可变类型(String、包装类):
a += b是a = a + b的语法糖,均生成新对象。 - 可变类型(StringBuilder):
a += b直接修改原对象(效率高),a = a + b生成新对象(效率低)。
- 开发建议:
- 基本类型运算:若涉及不同类型,优先用
a = (T)(a + b)显式转换,避免+=的隐式溢出。 - String 拼接:频繁拼接时用
StringBuilder.append(),而非+=(减少临时对象)。 - 包装类:避免用
+=(拆箱装箱有性能开销),直接用基本类型运算。
面试常考考点
- 问:
short a = 1; a += 1;是否编译通过?a = a + 1;呢?
答:a += 1 编译通过(自动窄化转换);a = a + 1 编译报错(int 转 short 需显式转换)。
- 问:
String a = "a"; a += "b";和a = a + "b";有区别吗?
答:无区别,均生成新 String 对象,+= 是语法糖。
- 问:
StringBuilder a = new StringBuilder("a"); a += "b";会生成新对象吗?
答:不会,+= 等价于 append(),直接修改原对象。
3*0.1 == 0.3 将会返回什么? true 还是 false?
false,因为有些浮点数不能完全精确的表示出来。
a = a + b 与 a += b 的区别
+= 隐式的将加操作的结果类型强制转换为持有结果的类型。如果两这个整型相加,如byte、short 或者 int,首先会将它们提升到 int 类型,然后在执行加法操作。如果加法操作的结果比 a 的最大值要大,则 a+b 会出现编译错误,但是a += b 没问题, 如下: byte a = 127; byte b = 127; b = a + b; // error : cannot convert from int to byte b += a; // ok
( 译者注: 这个地方应该表述的有误, 其实无论 a+b 的值为多少, 编译器都会报错,因为 a+b 操作会将 a、b 提升为 int 类型,所以将 int 类型赋值给 byte 就会编译出错)
float f=3.4;是否正确?
答 : 不正确。 3.4 是双精度数, 将双精度型( double) 赋值给浮点型( float) 属于下转型( down-casting, 也称为窄化) 会造成精度损失, 因此需要强制类型转换 float f =(float)3.4; 或者写成 float f =3.4F;。
short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1; 有错吗?
对于 short s1 = 1; s1 = s1 + 1;由于 1 是 int 类型, 因此 s1+1 运算结果也是 int 型,需要强制转换类型才能赋值给 short 型。而 short s1 = 1; s1 += 1;可以正确编译, 因为 s1+= 1;相当于 s1 = (short)(s1 + 1);其中有隐含的强制类型转换。
int 和Integer 有什么区别?
Java 是一个近乎纯洁的面向对象编程语言, 但是为了编程的方便还是引入了基本数据类型, 但是为了能够将这些基本数据类型当成对象操作, Java 为每一个基本
数据类型都引入了对应的包装类型( wrapper class),int 的包装类就是 Integer, 从 Java 5 开始引入了自动装箱/拆箱机制, 使得二者可以相互转换。 Java 为每个原始类型提供了包装类型:
原始类型: boolean,char,byte,short,int,long,float,double 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float, Double
char 型变量中能不能存贮一个中文汉字,为什么?
char 类型可以存储一个中文汉字, 因为 Java 中使用的编码是 Unicode( 不选择任何特定的编码, 直接使用字符在字符集中的编号, 这是统一的唯一方法), 一个 char 类型占 2 个字节( 16 比特), 所以放一个中文是没问题的。 补充: 使用 Unicode 意味着字符在 JVM 内部和外部有不同的表现形式, 在 JVM 内部都是 Unicode,当这个字符被从 JVM 内部转移到外部时( 例如存入文件系统中), 需要进行编码转换。所以 Java 中有字节流和字符流, 以及在字符流和字节流之间进行转换的转换流, 如 InputStreamReader 和 OutputStreamReader, 这两个类是字节流和字符流之间的适配器类, 承担了编码转换的任务; 对于 C 程序员来说, 要完成这样的编码转换恐怕要依赖于 union( 联合体/共用体) 共享内存的特征来实现了。
我能在不进行强制转换的情况下将一个 double 值赋值给 long 类型的变量吗?
不行,你不能在没有强制类型转换的前提下将一个 double 值赋值给 long 类型的变量,因为 double 类型的范围比long 类型更广,所以必须要进行强制转换。
int 和 Integer 哪个会占用更多的内存?
Integer 对象会占用更多的内存。Integer 是一个对象, 需要存储对象的元数据。但是 int 是一个原始类型的数据, 所以占用的空间更少。
Java 中应该使用什么数据类型来代表价格?
如果不是特别关心内存和性能的话,使用BigDecimal,否则使用预定义精度的 double 类型。
谈谈 Java 中的包装类(Wrapper Classes),如 Integer、Double 等的作用和特点。
包装类
Java 为每种基本数据类型都提供了相应的包装类,例如 Integer 对应 int,Double 对应 double。
作用与特点
- 自动装箱/拆箱:支持基本类型与包装类之间的自动转换。
- 提供方法:如
parseInt,valueOf,toString等方法,便于处理数值和字符串。 - 提供常量:如
Integer.MAX_VALUE。 - 实现接口:如
Serializable,Comparable等,支持序列化和比较操作。
int、char、long各占多少字节数
在Java中,基本数据类型占用的字节数是固定的。下面是int、char和long的数据类型及其所占用的字节数:
数据类型占用字节数int4char2long8
int与integer的区别
int 和 Integer 在Java中有明显的不同:
int:int是一个基本数据类型。- 它用来表示32位(4字节)的整数。
int类型变量存储的是实际的数值。- 没有默认值,使用时必须初始化。
Integer:Integer是int的包装类,属于引用类型。- 它可以表示一个
null值。 - 包含一些静态方法,如
parseInt和toString方法,用于字符串和整数之间的转换。 Integer对象可以作为参数传递给方法,而这些方法期望一个对象参数。- 默认值为
null。
utf-8编码中的中文占几个字节;int型几个字节?
- UTF-8编码中的中文:中文字符在UTF-8中通常占用3个字节。
- int型:在Java中,
int类型总是占用4个字节。
解释Java的基本数据类型及其大小
Java作为一种强类型语言,提供了一系列的基本数据类型,用于存储不同类型的数据。基本数据类型分为两大类:原始类型和引用类型。这里我们主要关注原始类型,它们包括整型、浮点型、字符型和布尔型。
- 整型:用于存储整数,包括byte、short、int和long。
byte:占用1个字节(8位),范围从-128到127。由于其存储空间较小,适用于内存使用非常关键的场合。short:占用2个字节(16位),范围从-32,768到32,767。同样适用于内存敏感的环境。int:占用4个字节(32位),范围从-2,147,483,648到2,147,483,647。这是最常用的整型,适用于大多数情况。long:占用8个字节(64位),范围从-9,223,372,036,854,775,808到9,223,372,036,854,775,807。适用于需要更大数值范围的场合。
- 浮点型:用于存储单精度和双精度浮点数,包括float和double。
float:占用4个字节(32位),提供大约6-7位十进制数的精度。double:占用8个字节(64位),提供大约15-16位十进制数的精度。由于其更高的精度,通常用于科学计算和金融计算。
- 字符型:用于存储Unicode字符。
char:占用2个字节(16位),可以表示Unicode字符集中的任意字符。
- 布尔型:用于存储逻辑值,即true或false。
boolean:不具有固定的字节大小,通常实现为int的位字段,但API上表现为1位。
1.3 String
String 是最基本的数据类型吗?
不是。Java 中的基本数据类型只有 8 个:byte、short、int、long、float、double 、char、boolean ;除了基本类型( primitive type),剩下的都是引用类型( reference type), Java 5 以后引入的枚举类型也算是一种比较特殊的引用类型 。
String、StringBuffer与StringBuilder的区别?
第一点: 可变和适用范围。String对象是不可变的,而StringBuffer和StringBuilder是可变字符序列。每次对String的操作相当于生成一个新的String对象,而对StringBuffer和StringBuilder的操作是对对象本身的操作,而不会生成新的对象,所以对于频繁改变内容的字符串避免使用String,因为频繁的生成对象将会对系统性能产生影响。
第二点: 线程安全。String由于有final修饰,是immutable的,安全性是简单而纯粹的。StringBuilder和StringBuffer的区别在于StringBuilder不保证同步,也就是说如果需要线程安全需要使用StringBuffer,不需要同步的StringBuilder效率更高。
String、StringBuffer、StringBuilder区别
Java中提供了三种处理字符串的方式:String、StringBuffer 和 StringBuilder。
类是否可变是否线程安全主要用途String不可变线程安全适用于不需要改变内容的字符串,如常量池中的字符串。StringBuffer可变线程安全适用于多线程环境中需要频繁修改的字符串。StringBuilder可变非线程安全适用于单线程环境中需要频繁修改的字符串,性能优于 StringBuffer。
总结:
- 如果你需要一个不可变的字符串,选择
String。 - 如果你在一个多线程环境中需要频繁修改字符串,选择
StringBuffer。 - 如果你在一个单线程环境中需要频繁修改字符串,选择
StringBuilder以获得更好的性能。
为什么 Java 中的 String 是不可变的(Immutable)?
Java 中的 String 不可变是因为 Java 的设计者认为字符串使用非常频繁,将字符串设置为不可变可以允许多个客户端之间共享相同的字符串。
数组有没有 length()方法?String 有没有 length()方法?
数组没有 length()方法,有 length 的属性。String 有 length()方法。JavaScript 中, 获得字符串的长度是通过 length 属性得到的, 这一点容易和 Java 混淆。
String 和StringBuilder、StringBuffer 的区别?
Java 平台提供了两种类型的字符串: String 和 StringBuffer/StringBuilder, 它们可以储存和操作字符串。其中 String 是只读字符串,也就意味着 String 引用的字符串内容是不能被改变的。而 StringBuffer/StringBuilder 类表示的字符串对象可以直接进行修改。 StringBuilder 是 Java 5 中引入的, 它和 StringBuffer 的方法完全相同, 区别在于它是在单线程环境下使用的, 因为它的所有方面都没有被synchronized 修饰, 因此它的效率也比 StringBuffer 要高。 面试题 1 - 什么情况下用+运算符进行字符串连接比调用 StringBuffer/StringBuilder 对象的 append 方法连接字符串性能更好?面试题 2 - 请说出下面程序的输出。
class StringEqualTest {
public static void main(String[] args) {
String s1 = "Programming";
String s2 = new String("Programming"); String s3 = "Program";
String s4 = "ming";
String s5 = "Program" + "ming";
String s6 = s3 + s4;
System.out.println(s1 == s2);
System.out.println(s1 == s5);
System.out.println(s1 == s6);
System.out.println(s1 == s6.intern());
System.out.println(s2 == s2.intern());
}
}
补充:解答上面的面试题需要清除两点:1. String 对象的 intern 方法会得到字符串对象在常量池中对应的版本的引用( 如果常量池中有一个字符串与 String 对象的 equals 结果是 true),如果常量池中没有对应的字符串,则该字符串将被添加到常量池中, 然后返回常量池中字符串的引用; 2. 字符串的+操作其本质是创建了 StringBuilder 对象进行append 操作,然后将拼接后的 StringBuilder 对象用toString 方法处理成 String 对象,这一点可以用 javap -c StringEqualTest.class 命令获得 class 文件对应的 JVM 字节码指令就可以看出来。
数据类型之间的转换:
如何将字符串转换为基本数据类型? 如何将基本数据类型转换为字符串?
调用基本数据类型对应的包装类中的方法 parseXXX(String)或valueOf(String)即可返回相应基本类型;
一种方法是将基本数据类型与空字符串(”“)连接(+)即可获得其所对应的字符串;另一种方法是调用 String 类中的 valueOf()方法返回相应字符 串
如何实现字符串的反转及替换?
方法很多, 可以自己写实现也可以使用 String 或 StringBuffer/StringBuilder 中的方法。有一道很常见的面试题是用递归实现字符串反转, 代码如下所示:
public static String reverse(String originStr) {
if(originStr == null || originStr.length() <= 1)
return originStr;
return reverse(originStr.substring(1)) + originStr.charAt(0);
}
String s = new String(“xyz”);创建了几个字符串对象?
两个对象, 一个是静态区的” xyz”, 一个是用 new 创建在堆上的对象。
string 转换成 integer的方式及原理
在Java中,将 String 转换成 int 类型主要有两种常用的方法:
Integer.parseInt()方法:该方法解析给定字符串并返回相应的基本类型
int值。如果字符串无法被解析为有效的整数,则会抛出
NumberFormatException。示例:
String str = "123"; int num = Integer.parseInt(str);
Integer.valueOf()方法:该方法与
parseInt()类似,但它返回一个Integer对象而不是基本类型的int。当需要返回一个
Integer对象时,可以使用此方法。示例:
String str = "123"; Integer num = Integer.valueOf(str);
是否可以继承 String 类?
String 类是 final 类, 不可以被继承。 补充:继承 String 本身就是一个错误的行为,对 String 类型最好的重用方式是关联关系( Has-A) 和依赖关系( Use-A) 而不是继承关系( Is-A)。 18、当一个对象被当作参数传递到一个方法后,此方法可改变 这个对象的属性,并可返回变化后的结果,那么这里到底是值传 递还是引用传递?
是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时, 参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。C++和 C#中可以通过传引用或传输出参数来改变传入的参数的值。在 C#中可以编写如下所示的代码, 但是在 Java 中却做不到。
using System;
namespace CS01 {
class Program {
public static void swap(ref int x, ref int y) {
int temp = x;
x = y;
y = temp;
}
public static void Main (string[] args) {
int a = 5, b = 10;
swap (ref a, ref b);
// a = 10, b = 5;
Console.WriteLine ("a = {0}, b = {1}", a, b);
}
}
}
说明:Java 中没有传引用实在是非常的不方便,这一点在 Java 8 中仍然没有得到改进,正是如此在 Java 编写的代码中才会出现大量的 Wrapper 类( 将需要通过方法调用修改的引用置于一个 Wrapper 类中, 再将 Wrapper 对象传入方法), 这样的做法只会让代码变得臃肿, 尤其是让从 C 和 C++转型为 Java 程序员的开发者无法容忍。
Java中String的了解
Java中的 String 类是一个不可变类,代表字符串。字符串一旦创建就不能改变。String 类实现了 Serializable 和 Comparable<String> 接口。字符串的创建可以通过字面量或者构造器。字符串的比较通常使用 equals 方法而不是 == 运算符。
String为什么要设计成不可变的?
在Java中,String 类被设计成不可变的(immutable),这是出于多种考虑:
- 安全性:不可变性保证了字符串一旦创建之后就不能被修改,这对于多线程环境来说是非常重要的,因为它避免了数据竞争和并发修改的问题。
- 性能:不可变性有助于提高性能,因为字符串常量池可以缓存字符串对象,当相同的字符串多次创建时,实际上只是引用同一个对象,减少了内存消耗。
- 一致性:不可变性保证了字符串的一致性,这意味着一旦一个字符串被创建,它的值在整个生命周期内都不会改变,这对于需要字符串值保持不变的场景非常有用。
- 简化实现:由于字符串不可变,因此不需要额外的同步机制来保护字符串的数据。
总结
特性描述安全性保证了字符串在多线程环境中的一致性和安全性。性能利用字符串常量池减少内存消耗,提高性能。一致性字符串创建后其值不会改变,保证了数据的一致性。简化实现不需要同步机制来保护字符串的数据。
怎样将 GB2312 编码的字符串转换为ISO-8859-1 编码的字符串?
代码如下所示: String s1 = "你好"; String s2 = new String(s1.getBytes("GB2312"), "ISO-8859-1");
深入探讨 Java 中的字符串常量池(String Constant Pool)的实现和优化
实现
字符串常量池是一个位于方法区的特殊缓存结构,用于存储字符串字面量。在 Java 中,字符串常量池的实现随着 Java 版本的不同而有所变化:
- JDK 1.6:
- 字符串常量池位于永久代(PermGen space)中。
- 如果字符串常量池满载,可能会导致永久代溢出。
- JDK 1.7:
- 引入了
-XX:+UseStringDeduplication选项,允许字符串常量池在运行时进行去重。 - 字符串常量池仍然位于永久代中。
- 引入了
- JDK 1.8:
- 字符串常量池从永久代移到了堆中。
- 这一变化提高了字符串常量池的容量,并且解决了永久代溢出的问题。
优化
- 字符串字面量的去重:通过
-XX:+UseStringDeduplication选项,可以在运行时去除重复的字符串常量。 - 缓存大小的调整:通过
-XX:StringTableSize参数调整字符串表的大小。 - 性能提升:由于字符串常量池位于堆中,因此可以利用更多的内存空间。
总结
特性JDK 1.6JDK 1.7JDK 1.8位置永久代永久代堆去重支持无有有溢出问题有有无
1.4 运算
1.5 继承
1.6 Object
对equals()和hashCode()的理解?
- 为什么在重写 equals 方法的时候需要重写 hashCode 方法?
因为有强制的规范指定需要同时重写 hashcode 与 equals 是方法,许多容器类,如 HashMap、HashSet 都依赖于 hashcode 与 equals 的规定。
- 有没有可能两个不相等的对象有相同的 hashcode?
有可能,两个不相等的对象可能会有相同的 hashcode 值,这就是为什么在 hashmap 中会有冲突。相等 hashcode 值的规定只是说如果两个对象相等,必须有相同的hashcode 值,但是没有关于不相等对象的任何规定。
- 两个相同的对象会有不同的 hash code 吗?
不能,根据 hash code 的规定,这是不可能的。
- 我们可以在 hashcode() 中使用随机数字吗?
不行,因为对象的 hashcode 值必须是相同的。
- 为什么在重写 equals 方法的时候需要重写 hashCode 方法?
因为有强制的规范指定需要同时重写 hashcode 与 equal 是方法,许多容器类, 如HashMap、HashSet 都依赖于 hashcode 与 equals 的规定。
a.hashCode() 有什么用?与 a.equals(b) 有什么关系?
简介: hashCode() 方法是相应对象整型的 hash 值。它常用于基于 hash 的集合类,如 Hashtable、HashMap、LinkedHashMap等等。它与 equals() 方法关系特别紧密。根据 Java 规范,两个使用 equals() 方法来判断相等的对象,必须具有相同的 hash code。
1、hashcode的作用
List和Set,如何保证Set不重复呢? 通过迭代使用equals方法来判断,数据量小还可以接受,数据量大怎么解决? 引入hashcode,实际上hashcode扮演的角色就是寻址,大大减少查询匹配次数。
2、hashcode重要吗
对于数组、List集合就是一个累赘。而对于hashmap, hashset, hashtable就异常重要了。
3、equals方法遵循的原则
- 对称性 若x.equals(y)true,则y.equals(x)true
- 自反性 x.equals(x)必须true
- 传递性 若x.equals(y)true,y.equals(z)true,则x.equals(z)必为true
- 一致性 只要x,y内容不变,无论调用多少次结果不变
- 其他 x.equals(null) 永远false,x.equals(和x数据类型不同)始终false
“a==b”和”a.equals(b)”有什么区别?
如果 a 和 b 都是对象, 则 a==b 是比较两个对象的引用, 只有当 a 和 b 指向的是堆中的同一个对象才会返回 true,而 a.equals(b) 是进行逻辑比较,所以通常需要重写该方法来提供逻辑一致性的比较。例如,String 类重写 equals() 方法, 所以可以用于两个不同对象, 但是包含的字母相同的比较。
Object有哪些公用方法?
clone equals hashcode wait notify notifyall finalize toString getClass 除了clone和finalize其他均为公共方法。
11个方法,wait被重载了两次
equals与==的区别
区别1. ==是一个运算符 equals是Object类的方法
区别2. 比较时的区别
- 用于基本类型的变量比较时: ==用于比较值是否相等,equals不能直接用于基本数据类型的比较,需要转换为其对应的包装类型。
- 用于引用类型的比较时。==和equals都是比较栈内存中的地址是否相等 。相等为true 否则为false。但是通常会重写equals方法去实现对象内容的比较。
两个对象值相同(x.equals(y) == true),但却可有不同的 hash code,这句话对不对?
不对,如果两个对象 x 和 y 满足 x.equals(y) == true,它们的哈希码( hash code) 应当相同。Java 对于 eqauls 方法和 hashCode 方法是这样规定的: (1)如果两个
对象相同( equals 方法返回 true), 那么它们的 hashCode 值一定要相同; (2) 如果两个对象的 hashCode 相同, 它们并不一定相同。当然, 你未必要按照要求去做, 但是如果你违背了上述原则就会发现在使用容器时, 相同的对象可以出现在 Set 集合中, 同时增加新元素的效率会大大下降( 对于使用哈希存储的系统, 如果哈希码频繁的冲突将会造成存取性能急剧下降)。 补充: 关于 equals 和 hashCode 方法, 很多 Java 程序都知道, 但很多人也就是仅仅知道而已, 在 Joshua Bloch 的大作《Effective Java》( 很多软件公司, 《 Effective Java》、《Java 编程思想》以及《重构:改善既有代码质量》是 Java 程序员必看书籍, 如果你还没看过, 那就赶紧去亚马逊 买一本吧) 中是这样介绍 equals 方法的:首先 equals 方法必须满足自反性( x.equals(x)必须返回 true)、对称性( x.equals(y)返回 true 时, y.equals(x)也必须返回 true)、传递性 ( x.equals(y)和 y.equals(z)都返回 true 时, x.equals(z)也必须返回 true) 和一致性( 当 x 和 y 引用的对象信息没有被修改时, 多次调用 x.equals(y)应该得到同样的返回值), 而且对于任何非 null 值的引用 x, x.equals(null)必须返回 false。实现高质量的 equals 方法的诀窍包括: 1. 使用==操作符检查” 参数是否为这个对象的引用”;2. 使用 instanceof 操作符检查”参数是否为正确的类型”;3. 对于类中的关键属性,检查参数传入对象的属性是否与之相匹配;4. 编写完 equals 方法后,问自己它是否满足对称性、传递性、一致性; 5. 重写 equals 时总是要重写 hashCode ;6. 不要将 equals 方法参数中的 Object 对象替换为其他的类型,在重写时不要忘掉 @Override 注解。
Object类的equal和hashCode方法重写,为什么?
在Java中,Object 类提供了 equals 和 hashCode 方法的默认实现。为了使对象能够作为散列表中的键(如 HashMap),这两个方法通常需要被重写以满足一些特定的要求:
equals方法:用于比较两个对象是否相等。如果两个对象相等,则它们的hashCode值也应该相等。hashCode方法:用于计算对象的哈希值。哈希值用于散列表中定位对象的位置。
重写的原因
- 一致性:
equals和hashCode方法需要保持一致,即如果a.equals(b)返回true,那么a.hashCode()和b.hashCode()应该返回相同的值。 - 效率:正确的
hashCode方法可以提高散列表的性能,避免不必要的equals方法调用。 - 正确性:确保散列表的正确行为,尤其是当对象作为键使用时。
总结
方法描述equals用于比较两个对象是否相等,需要确保逻辑上的相等性。hashCode用于计算对象的哈希值,需要确保相等的对象具有相同的哈希值。
如何实现对象克隆?
有两种方式:
- 实现 Cloneable 接口并重写 Object 类中的 clone()方法;
- 实现 Serializable 接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆, 代码如下。
描述 Java 中的对象克隆(Object Cloning),深克隆和浅克隆的区别以及实现方式。
对象克隆
Java 中的对象克隆是指创建一个现有对象的副本。这可以通过实现 Cloneable 接口并重写 clone() 方法来完成。
深克隆与浅克隆
- 浅克隆:只复制对象本身,不复制对象所引用的对象。
- 深克隆:不仅复制对象本身,还复制对象所引用的所有对象。
实现方式
- 浅克隆:实现
Cloneable接口并重写clone()方法。 - 深克隆:除了实现
Cloneable接口,还需要递归地克隆所有引用的对象,或者通过序列化实现。
示例代码
public class Person implements Cloneable {
private String name;
private Address address;
// 构造器、getters 和 setters 省略
@Override
public Object clone() throws CloneNotSupportedException {
Person clonedPerson = (Person) super.clone();
clonedPerson.address = (Address) this.address.clone(); // 深克隆
return clonedPerson;
}
}
什么是深拷贝和浅拷贝
浅拷贝
- 定义:浅拷贝仅复制对象的引用地址,而不是创建新的对象。
- 效果:源对象和拷贝对象共享同一份数据,对其中一个对象的修改会影响到另一个对象。
深拷贝
- 定义:深拷贝会创建一个全新的对象,且对象内的可变成员也是独立的。
- 效果:源对象和拷贝对象之间相互独立,对其中一个对象的修改不会影响到另一个对象。
示例
假设有一个对象 Person 包含一个指向 Address 对象的引用。
浅拷贝
Person person1 = new Person("Alice", new Address("123 Main St"));
Person person2 = person1; // 浅拷贝
person2.getAddress().setStreet("456 Other St");
// 结果:person1 和 person2 的 address 字段都指向同一个 Address 对象
System.out.println(person1.getAddress().getStreet()); // 输出 "456 Other St"
深拷贝
Person person1 = new Person("Alice", new Address("123 Main St"));
Person person2 = new Person(person1.getName(), new Address(person1.getAddress().getStreet())); // 深拷贝
person2.getAddress().setStreet("456 Other St");
// 结果:person1 和 person2 的 address 字段指向不同的 Address 对象
System.out.println(person1.getAddress().getStreet()); // 输出 "123 Main St"
修改对象A的equals方法的签名,那么使用HashMap存放这个对象实例的时候,会调用哪个equals方法?
在Java中,equals 方法和 hashCode 方法对于 HashMap 的工作非常重要。如果修改了 equals 方法的签名,这将违反 HashMap 的工作原则,因为它期望 equals 方法遵循特定的行为准则:
- 如果两个对象相等,那么它们应该产生相同的哈希码。
- 如果两个对象的哈希码相同,它们并不一定相等。
如果改变了 equals 方法的签名,那么 HashMap 将无法正确地工作。在尝试将对象放入 HashMap 时,它会调用对象的 equals 方法来判断是否已经有相同的键存在。如果签名改变,那么 HashMap 会调用新的 equals 方法,但这可能导致不一致的结果,因为新的方法可能不符合上述的约定。
1.7 关键字
this() & super()在构造方法中的区别?
- 调用super()必须写在子类构造方法的第一行, 否则编译不通过
- super从子类调用父类构造, this在同一类中调用其他构造均需要放在第一行
- 尽管可以用this调用一个构造器, 却不能调用2个
- this和super不能出现在同一个构造器中, 否则编译不通过
- this()、super()都指的对象,不可以在static环境中使用
- 本质this指向本对象的指针。super是一个关键字
super出现在父类的子类中。有三种存在方式
- super.xxx(xxx为变量名或对象名)意思是获取父类中xxx的变量或引用
- super.xxx(); (xxx为方法名)意思是直接访问并调用父类中的方法
- super() 调用父类构造
注: super只能指代其直接父类
this() & super()在构造方法中的区别
- 调用super()必须写在子类构造方法的第一行, 否则编译不通过
- super从子类调用父类构造, this在同一类中调用其他构造均需要放在第一行
- 尽管可以用this调用一个构造器, 却不能调用2个
- this和super不能出现在同一个构造器中, 否则编译不通过
- this()、super()都指的对象,不可以在static环境中使用
- 本质this指向本对象的指针。super是一个关键字
Java 中的final 关键字有哪些用法?
修饰类:表示该类不能被继承;
修饰方法:表示方法不能被重写;(3)修饰变 量: 表示变量只能一次赋值以后值不能被修改( 常量)。
Java 有没有 goto?
goto 是 Java 中的保留字,在目前版本的 Java 中没有使用 。( 根据 James Gosling ( Java 之父) 编写的《The Java Programming Language》一书的附录中给出了一个 Java 关键字列表, 其中有 goto 和 const, 但是这两个是目前无法使用的关键字, 因此有些地方将其称之为保留字, 其实保留字这个词应该有更广泛的意义, 因为熟悉 C 语言的程序员都知道, 在系统类库中使用过的有特殊意义的单词或单词的组合都被视为保留字)
在 Java 中,如何跳出当前的多重嵌套循环?
在最外层循环前加一个标记如 A, 然后用 break A;可以跳出多重循环。( Java 中支持带标签的 break 和 continue 语句, 作用有点类似于 C 和 C++中的 goto 语句,但是就像要避免使用 goto 一样,应该避免使用带标签的 break 和 continue, 因为它不会让你的程序变得更优雅, 很多时候甚至有相反的作用, 所以这种语法其实不知道更好)
抽象的(abstract)方法是否可同时是静态的(static),是否可同时是本地方法(native),是否可同时被 synchronized修饰?
都不能。抽象方法需要子类重写, 而静态的方法是无法被重写的, 因此二者是矛盾的。本地方法是由本地代码( 如 C 代码) 实现的方法, 而抽象方法是没有实现的, 也是矛盾的。synchronized 和方法的实现细节有关, 抽象方法不涉及实现细节, 因此也是相互矛盾的。
阐述静态变量和实例变量的区别。
静态变量是被 static 修饰符修饰的变量, 也称为类变量, 它属于类, 不属于类的任何一个对象, 一个类不管创建多少个对象, 静态变量在内存中有且仅有一个拷贝; 实例变量必须依存于某一实例, 需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。 补充: 在 Java 开发中, 上下文类和工具类中通常会有大量的静态成员。
是否可以从一个静态(static)方法内部发出对非静态(non-static)方法的调用?
不可以, 静态方法只能访问静态成员, 因为非静态方法的调用要先创建对象, 在调用静态方法时可能对象并没有被初始化。
静态属性和静态方法是否可以被继承?是否可以被重写?以及原因?
静态属性和静态方法的继承与重写:
- 继承:静态属性和静态方法不会被子类继承。
- 重写:子类不能重写父类的静态方法;子类中可以声明同名的静态方法,但这不是重写,而是完全独立的新方法。
- 原因:静态属性和静态方法属于类而不是类的实例,因此它们不受继承的影响。每个类都有自己的静态属性和静态方法的副本。
静态内部类的设计意图
静态内部类 (Static Inner Class),也称为 静态嵌套类 (Static Nested Class),是在另一个类的内部定义的类,但与非静态内部类不同的是,它不需要依赖于外部类的实例。静态内部类的设计意图主要包括:
- 封装性:可以将相关联的类放在同一个外部类中,提高代码的组织性和可读性。
- 访问外部类的静态成员:静态内部类可以访问外部类的所有静态成员,包括静态方法和静态变量。
- 避免额外的引用:由于静态内部类不依赖于外部类的实例,因此它不需要持有对外部类实例的引用,减少了潜在的内存泄漏风险。
成员内部类、静态内部类、局部内部类和匿名内部类的理解,以及项目中的应用
成员内部类 (Member Inner Class)、静态内部类 (Static Inner Class)、局部内部类 (Local Inner Class) 和 匿名内部类 (Anonymous Inner Class) 都是Java内部类的不同形式,它们各有特点和应用场景。
内部类类型描述项目中的应用成员内部类定义在外部类中的非静态内部类,可以访问外部类的所有成员。用于实现紧密相关的类,例如事件监听器模式中的事件处理器。静态内部类定义在外部类中的静态内部类,不依赖于外部类的实例。用于封装与外部类相关的类,但又不需要依赖于外部类实例的情况。局部内部类定义在方法或代码块中的内部类,只能在其定义的方法或代码块中使用。用于实现局部范围内的一次性功能,减少代码冗余。匿名内部类无名称的内部类,通常用于实现接口或继承抽象类。用于简化代码,特别是在需要快速实现接口或继承抽象类的地方。
1.8 特性
面向对象特性?
- 封装
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
优点:
- 减少耦合: 可以独立地开发、测试、优化、使用、理解和修改
- 减轻维护的负担: 可以更容易被程序员理解,并且在调试的时候可以不影响其他模块
- 有效地调节性能: 可以通过剖析确定哪些模块影响了系统的性能
- 提高软件的可重用性
- 降低了构建大型系统的风险: 即使整个系统不可用,但是这些独立的模块却有可能是可用的
以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。
注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。
public class Person {
private String name;
private int gender;
private int age;
public String getName() {
return name;
}
public String getGender() {
return gender == 0 ? "man" : "woman";
}
public void work() {
if (18 <= age && age <= 50) {
System.out.println(name + " is working very hard!");
} else {
System.out.println(name + " can't work any more!");
}
}
}
- 继承
继承实现了 IS-A 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal,从而获得 Animal 非 private 的属性和方法。
继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。
Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 向上转型 。
Animal animal = new Cat();
- 多态
多态分为编译时多态和运行时多态:
- 编译时多态主要指方法的重载
- 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定
运行时多态有三个条件:
- 继承
- 覆盖(重写)
- 向上转型
下面的代码中,乐器类(Instrument)有两个子类: Wind 和 Percussion,它们都覆盖了父类的 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法。
public class Instrument {
public void play() {
System.out.println("Instrument is playing...");
}
}
public class Wind extends Instrument {
public void play() {
System.out.println("Wind is playing...");
}
}
public class Percussion extends Instrument {
public void play() {
System.out.println("Percussion is playing...");
}
}
public class Music {
public static void main(String[] args) {
List<Instrument> instruments = new ArrayList<>();
instruments.add(new Wind());
instruments.add(new Percussion());
for(Instrument instrument : instruments) {
instrument.play();
}
}
}
Java中实现多态的机制是什么?
Java中实现多态的主要机制是继承和接口。
- 继承:子类继承父类后可以重写父类的方法,这样即使使用父类引用调用方法,实际执行的也是子类的方法实现。
- 接口:实现接口的类必须提供接口中声明的所有方法的具体实现。多个类可以实现同一个接口,从而表现出多态性。
解释Java中的多态和封装
多态是面向对象编程的三大特性之一,指的是同一个行为具有多个不同表现形式或形态的能力。在Java中,多态主要体现在方法重载、方法重写和接口实现上。
- 方法重载(Overloading):在同一个类中,可以有多个同名方法,只要它们的参数列表不同(参数的数量或类型不同)。
- 方法重写(Overriding):子类可以重写父类中的方法,实现与父类不同的行为。
- 接口实现:一个类可以实现多个接口,从而具备多个接口声明的行为。
多态的好处是提高了程序的可扩展性和可维护性,同时也让代码更加灵活。
封装是面向对象编程的另一个核心概念,指的是将数据(属性)和行为(方法)捆绑在一起,并对外界隐藏其内部实现细节。封装的目的是减少系统的复杂性,通过提供公共的接口来控制对内部结构的访问。
- 通过使用修饰符(如private、protected、public)来限制对类成员的访问。
- 通过使用getter和setter方法来间接访问和修改私有属性,而不是直接暴露属性。
封装提高了代码的安全性和可维护性,同时也促进了模块化设计。
Java 中的构造器链是什么?
当你从一个构造器中调用另一个构造器,就是 Java 中的构造器链。这种情况只在重载了类的构造器的时候才会出现。
hashCode() 方法是相应对象整型的 hash 值 。它常用于基于 hash 的集合类,如 Hashtable 、 HashMap 、 LinkedHashMap 等等。 它与 equals() 方法关系特别紧密。根据 Java 规范,两个使用 equal() 方法来判断相等的对象, 必须具有相同的 hash code。
Java 中的编译期常量是什么?使用它又什么风险?
公共静态不可变( public static final )变量也就是我们所说的编译期常量,这里的 public 可选的。实际上这些变量在编译时会被替换掉, 因为编译器知道这些变量的值, 并且知道这些变量在运行时不能改变。这种方式存在的一个问题是你使用了一个内部的或第三方库中的公有编译时常量, 但是这个值后面被其他人改变了,但是你的客户端仍然在使用老的值, 甚至你已经部署了一个新的 jar。为了避免这种情况, 当你在更新依赖 JAR 文件时, 确保重新编译你的程序。
Java中对象的生命周期
Java中对象的生命周期主要包括以下几个阶段:
- 创建:
- 通过
new关键字创建对象。 - 对象的构造器被执行,对象的状态被初始化。
- 通过
- 使用:
- 对象被引用并使用。
- 对象的状态可以根据需要进行修改。
- 垃圾收集:
- 当对象不再被任何引用所引用时,它就变成了垃圾。
- Java的垃圾收集器会自动回收这些对象所占用的内存。
- 终结:
- 对象被垃圾收集器回收之后,它的生命周期结束。
- 如果对象实现了
finalize()方法,那么在被回收之前会调用该方法。
解释Java中的类和对象的区别?
在Java中,类(Class)和对象(Object)是面向对象编程的基本概念,它们之间有着本质的区别。
类(Class):
- 类是对象的模板或蓝图,它定义了一组具有相同属性(成员变量)和行为(方法)的对象的结构和行为。
- 类是抽象的,它不占用内存空间,直到它被实例化(创建对象)。
- 类是面向对象编程的封装单元,它封装了数据和操作数据的方法。
- 类可以被继承和扩展,形成类之间的层次关系。
对象(Object):
- 对象是类的实例,它是类的具体体现,每个对象都拥有自己的属性和方法。
- 对象是具体的,它在内存中占用空间,并且可以根据类定义的属性和方法进行操作。
- 对象之间可以相互交互,通过方法调用传递信息和执行操作。
- 对象的生命周期是由程序员控制的,可以通过创建和销毁对象来管理资源。
简而言之,类是对象的类型定义,而对象是类的实例。类定义了一组规则,对象是根据这些规则创建的具体实体。
接口与抽象类的区别?
- 一个子类只能继承一个抽象类, 但能实现多个接口
- 抽象类可以有构造方法, 接口没有构造方法
- 抽象类可以有普通成员变量, 接口没有普通成员变量
- 抽象类和接口都可有静态成员变量, 抽象类中静态成员变量访问类型任意,接口只能public static final(默认)
- 抽象类可以没有抽象方法, 抽象类可以有普通方法;接口在JDK8之前都是抽象方法,在JDK8可以有default方法,在JDK9中允许有私有普通方法
- 抽象类可以有静态方法;接口在JDK8之前不能有静态方法,在JDK8中可以有静态方法,且只能被接口类直接调用(不能被实现类的对象调用)
- 抽象类中的方法可以是public、protected; 接口方法在JDK8之前只有public abstract,在JDK8可以有default方法,在JDK9中允许有private方法
构造内部类和静态内部类对象
public class Enclosingone {
public class Insideone {}
public static class Insideone{}
}
public class Test {
public static void main(String[] args) {
// 构造内部类对象需要外部类的引用
Enclosingone.Insideone obj1 = new Enclosingone().new Insideone();
// 构造静态内部类的对象
Enclosingone.Insideone obj2 = new Enclosingone.Insideone();
}
}
静态内部类不需要有指向外部类的引用。
但非静态内部类需要持有对外部类的引用。
非静态内部类能够访问外部类的静态和非静态成员。
静态内部类不能访问外部类的非静态成员,只能访问外部类的静态成员。
形参&实参
形式参数可被视为local variable.形参和局部变量一样都不能离开方法。只有在方法中使用,不会在方法外可见。 形式参数只能用final修饰符,其它任何修饰符都会引起编译器错误。但是用这个修饰符也有一定的限制,就是在方法中不能对参数做任何修改。不过一般情况下,一个方法的形参不用final修饰。只有在特殊情况下,那就是: 方法内部类。一个方法内的内部类如果使用了这个方法的参数或者局部变量的话,这个参数或局部变量应该是final。 形参的值在调用时根据调用者更改,实参则用自身的值更改形参的值(指针、引用皆在此列),也就是说真正被传递的是实参。
局部变量为什么要初始化
局部变量是指类方法中的变量,必须初始化。局部变量运行时被分配在栈中,量大,生命周期短,如果虚拟机给每个局部变量都初始化一下,是一笔很大的开销,但变量不初始化为默认值就使用是不安全的。出于速度和安全性两个方面的综合考虑,解决方案就是虚拟机不初始化,但要求编写者一定要在使用前给变量赋值。
Java语言的鲁棒性
Java在编译和运行程序时,都要对可能出现的问题进行检查,以消除错误的产生。它提供自动垃圾收集来进行内存管理,防止程序员在管理内存时容易产生的错误。通过集成的面向对象的例外处理机制,在编译时,Java揭示出可能出现但未被处理的异常,帮助程序员正确地进行选择以防止系统的崩溃。另外,Java在编译时还可捕获类型声明中的许多常见错误,防止动态运行时不匹配问题的出现。
嵌套静态类与顶级类有什么区别?
一个公共的顶级类的源文件名称与类名相同, 而嵌套静态类没有这个要求。一个嵌套类位于顶级类内部, 需要使用顶级类的名称来引用嵌套静态类, 如 HashMap.Entry 是一个嵌套静态类, HashMap 是一个顶级类, Entry 是一个嵌套静态类。
存在两个类,B 继承 A,C 继承 B,我们能将 B 转换为 C 么? 如 C = (C) B;
可以,向下转型。但是不建议使用,容易出现类型转型异常.
Java 中的编译期常量是什么? 使用它又什么风险?
变量也就是我们所说的编译期常量,这里的 public 可选的。实际上这些变量在编译时会被替换掉,因为编译器知道这些变量的值,并且知道这些变量在运行时不能改变。这种方式存在的一个问题是你使用了一个内部的或第三方库中的公有编译时常量,但是这个值后面被其他人改变了,但是你的客户端仍然在使用老的值,甚至你已经部署了一个新的jar。为了避免这种情况,当你在更新依赖 JAR 文件时,确保重新编译你的程序。
静态内部类与顶级类有什么区别?
一个公共的顶级类的源文件名称与类名相同,而嵌套静态类没有这个要求。一个嵌套类位于顶级类内部,需要使用顶级类的名称来引用嵌套静态类,如 HashMap.Entry 是一个嵌套静态类,HashMap 是一个顶级类,Entry是一个嵌套静态类。
抽象类和最终类
抽象类可以没有抽象方法, 最终类可以没有最终方法
最终类不能被继承, 最终方法不能被重写(可以重载)
获得一个类的类对象有哪些方式?
方法 1:类型.class,例如:String.class
方法 2:对象.getClass(),例如:”hello”.getClass()
方法 3:Class.forName(),例如:Class.forName(“java.lang.String”)
能不能自己写个类叫java.lang.System?
通常不可以,但可以采取另类方法达到这个需求。
为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。
但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。
接口是否可继承(extends)接口?抽象类是否可实现
(implements)接口?抽象类是否可继承具体类(concrete class)?
接口可以继承接口, 而且支持多重继承。抽象类可以实现(implements)接口, 抽象类可继承具体类也可以继承抽象类。
一个”.java”源文件中是否可以包含多个类(不是内部类)?
有什么限制?
可以, 但一个源文件中最多只能有一个公开类( public class) 而且文件名必须和公开类的类名完全保持一致。
Anonymous Inner Class(匿名内部类)是否可以继承其它类?是否可以实现接口?
可以继承其他类或实现其他接口,在 Swing 编程和 Android 开发中常用此方式来实现事件监听和回调。
内部类可以引用它的包含类(外部类)的成员吗?有没有什么限制?
一个内部类对象可以访问创建它的外部类对象的成员, 包括私有成员。
说出几条 Java 中方法重载的最佳实践?
答案:Java 方法重载的最佳实践:
参数类型差异明确:避免重载仅依赖自动装箱 / 拆箱的方法,可能导致调用歧义:
public void process(int value) { /* 处理int */ } public void process(Integer value) { /* 处理Integer */ } // 不推荐,易混淆参数数量或类型差异明显:重载方法的参数列表应有足够差异,避免仅靠参数顺序区分:
public void calculate(int a, String b) { /* 计算逻辑 */ } public void calculate(String a, int b) { /* 计算逻辑 */ } // 不推荐,易混淆保持行为一致性:重载方法应具有相似的功能语义,避免让调用者困惑:
public void add(int a, int b) { return a + b; } public void add(String a, String b) { return a.concat(b); } // 合理,均为"加法"使用可变参数:当参数数量不确定时,优先使用可变参数而非多个重载方法:
public void printValues(String... values) { /* 打印所有值 */ }重载优于参数检查:如果不同参数需要不同处理逻辑,使用重载而非条件判断:
// 推荐 public void process(File file) { /* 处理文件 */ } public void process(URL url) { /* 处理URL */ } // 不推荐 public void process(Object obj) { if (obj instanceof File) { /* 处理文件 */ } else if (obj instanceof URL) { /* 处理URL */ } }文档清晰:为重载方法提供明确的文档注释,说明参数差异和行为区别。
构造器(constructor)是否可被重写(override)?
构造器不能被继承, 因此不能被重写, 但可以被重载。
重载(Overload)和重写(Override)的区别。重载的方法能否根据返回类型进行区分?
方法的重载和重写都是实现多态的方式, 区别在于前者实现的是编译时的多态性, 而后者实现的是运行时的多态性。重载发生在一个类中, 同名的方法如果有不同的参数列表( 参数类型不同、参数个数不同或者二者都不同) 则视为重载; 重写 发生在子类与父类之间, 重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常( 里氏代换原则)。重载对返回类型没有特殊的要求。 面试题:华为的面试题中曾经问过这样一个问题 - “ 为什么不能根据返回类型来区分重载”, 快说出你的答案吧!
解释 Java 中的方法重载(Overloading)和方法重写(Overriding)的概念及区别。
方法重载与重写
- 重载(Overloading):在同一类中,方法名相同但参数列表不同。
- 重写(Overriding):子类中存在与父类同名且参数类型相同的覆盖方法。
区别
- 重载:方法名相同,参数列表不同;不改变返回类型。
- 重写:方法名、参数列表和返回类型必须完全相同;子类中的方法覆盖父类中的方法。
抽象类(abstract class)和接口(interface)有什么异同?
抽象类和接口都不能够实例化, 但可以定义抽象类和接口类型的引用。一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现, 否则该类仍然需要被声明为抽象类。接口比抽象类更加抽象, 因为抽象类中可以定义构造器, 可以有抽象方法和具体方法, 而接口中不能定义构造器而且其 中的方法全部都是抽象方法。抽象类中的成员可以是 private、默认、protected、public的,而接口中的成员全都是 public 的。抽象类中可以定义成员变量,而接口中定义的成员变量实际上都是常量。有抽象方法的类必须被声明为抽象类, 而抽象类未必要有抽象方法。
静态嵌套类(Static Nested Class)和内部类(Inner Class)的不同?
静态嵌套类(Static Nested Class)和内部类(Inner Class)是Java中两种不同的嵌套类,它们的主要区别如下:
定义位置与访问权限
- 静态嵌套类:定义在外部类内部,使用
static修饰。它属于外部类本身,而非外部类的实例。 - 内部类:直接定义在外部类内部,无
static修饰。它属于外部类的实例,必须通过外部类实例才能创建。
- 静态嵌套类:定义在外部类内部,使用
实例化方式
静态嵌套类:可直接通过外部类创建,无需先创建外部类实例。
OuterClass.StaticNestedClass nested = new OuterClass.StaticNestedClass();内部类:必须先创建外部类实例,再通过该实例创建内部类。
OuterClass outer = new OuterClass(); OuterClass.InnerClass inner = outer.new InnerClass();
对外部类成员的访问
- 静态嵌套类:只能访问外部类的静态成员(静态变量、静态方法)。
- 内部类:可以访问外部类的所有成员(包括私有成员和静态成员),因为它隐式持有外部类实例的引用。
生命周期
- 静态嵌套类:独立于外部类实例,可在外部类未实例化时创建。
- 内部类:依赖外部类实例,外部类实例销毁时,内部类也随之销毁。
编译后的命名
- 静态嵌套类:编译后生成
OuterClass$StaticNestedClass.class。 - 内部类:编译后生成
OuterClass$InnerClass.class,且包含对外部类的引用。
- 静态嵌套类:编译后生成
使用场景
- 静态嵌套类:适合作为工具类或数据结构,与外部类关联但无需访问实例成员(如Map的Entry类)。
- 内部类:需要紧密关联外部类实例,或实现回调机制(如事件监听器)。
示例代码:
public class OuterClass {
private static int staticVar = 10;
private int instanceVar = 20;
// 静态嵌套类
public static class StaticNestedClass {
public void print() {
System.out.println("静态嵌套类访问静态变量: " + staticVar);
// 无法访问 instanceVar,因为它是非静态的
}
}
// 内部类
public class InnerClass {
public void print() {
System.out.println("内部类访问静态变量: " + staticVar);
System.out.println("内部类访问实例变量: " + instanceVar);
}
}
public static void main(String[] args) {
// 创建静态嵌套类实例
OuterClass.StaticNestedClass staticNested = new OuterClass.StaticNestedClass();
staticNested.print();
// 创建内部类实例
OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.new InnerClass();
inner.print();
}
}
总结:静态嵌套类与外部类的关联较弱,主要作为逻辑分组使用;内部类则与外部类实例紧密绑定,常用于需要访问外部状态的场景。
Java 中会存在内存泄漏吗,请简单描述。
理论上 Java 因为有垃圾回收机制( GC) 不会存在内存泄露问题( 这也是 Java 被广泛使用于服务器端编程的一个重要原因); 然而在实际开发中, 可能会存在无用但可达的对象,这些对象不能被 GC 回收,因此也会导致内存泄露的发生。例如 Hibernate 的 Session( 一级缓存)中的对象属于持久态,垃圾回收器是不会回收这些对象的,然而这些对象中可能存在无用的垃圾对象,如果不及时关闭( close) 或清空( flush) 一级缓存就可能导致内存泄露。下面例子中的代码也会导致内存泄露。
import java.util.Arrays;
import java.util.EmptyStackException;
public class MyStack<T> {
private T[] elements;
private int size = 0;
private static final int INIT_CAPACITY = 16;
public MyStack() {
elements = (T[]) new Object[INIT_CAPACITY];
}
public void push(T elem) {
ensureCapacity();
elements[size++] = elem;
}
public T pop() {
if(size == 0)
throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity() {
if(elements.length == size) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}
上面的代码实现了一个栈( 先进后出( FILO)) 结构, 乍看之下似乎没有什么明显的问题, 它甚至可以通过你编写的各种单元测试。然而其中的 pop 方法却存在内存泄露的问题, 当我们用 pop 方法弹出栈中的对象时, 该对象不会被当作垃圾回收,即使使用栈的程序不再引用这些对象, 因为栈内部维护着对这些对象的过期引用 (obsolete reference)。在支持垃圾回收的语言中,内存泄露是很隐蔽的, 这种内存泄露其实就是无意识的对象保持。如果一个对象引用被无意识的保留起来了, 那么垃圾回收器不会处理这个对象, 也不会处理该对象引用的其他对象, 即使这样的对象只有少数几个, 也可能会导致很多的对象被排除在垃圾回收之外, 从而对性能造成重大影响,极端情况下会引发 Disk Paging( 物理内存与硬盘的虚拟内存交换数据), 甚至造成 OutOfMemoryError。
枚举类
JDK1.5出现 每个枚举值都需要调用一次构造函数
简述 Java 中的枚举(Enum)类型的特点和使用方法。
枚举类型
枚举类型是 Java 语言中的一种特殊类,用于表示一组固定的值。
特点
- 有限的值集:枚举类型只能拥有固定数量的实例。
- 类型安全:可以防止非法值的出现。
- 内置方法:如
values()返回所有枚举值的数组。 - 自定义行为:可以在枚举中定义方法和构造器。
使用方法
public enum Color {
RED, GREEN, BLUE;
public void printColor() {
System.out.println(this.name());
}
}
比较一下 Java 和 JavaSciprt。
JavaScript 与 Java 是两个公司开发的不同的两个产品。Java 是原 Sun Microsystems 公司推出的面向对象的程序设计语言,特别适合于互联网应用程序开发; 而 JavaScript 是 Netscape 公司的产品, 为了扩展 Netscape 浏览器的功能而开发的一 种可以嵌入 Web 页面中运行的基于对象和事件驱动的解释性语言。JavaScript 的前身是 LiveScript; 而 Java 的前身是 Oak 语言。 下面对两种语言间的异同作如下比较:
基于对象和面向对象:Java 是一种真正的面向对象的语言,即使是开发简单的程序,必须设计对象;JavaScript 是种脚本语言,它可以用来制作与网络无关的,与用户交互作用的复杂软件。它是一种基于对象(Object-Based)和 事件驱动(Event-Driven)的编程语言,因而它本身提供了非常丰富的内部对象供设计人员使用。 解释和编译:Java 的源代码在执行之前,必须经过编译。JavaScript 是一种解释性编程语言,其源代码不需经过编译,由浏览器解释执行。(目前的浏览器几乎都使用了 JIT(即时编译)技术来提升JavaScript 的运行效率) 强类型变量和类型弱变量:Java 采用强类型变量检查,即所有变量在编译之前必须作声明;JavaScript 中变量是弱类型的,甚至在使用变量前可以不作声明,JavaScript 的解释器在运行时检查推断其数据类型。 代码格式不一样。
补充: 上面列出的四点是网上流传的所谓的标准答案 。 其实 Java 和 JavaScript 最重要的区别是一个是静态语言, 一个是动态语言。目前的编程语言的发展趋势是函数式语言和动态语言。 在 Java 中类( class) 是一等公民, 而 JavaScript 中函数( function)是一等公民,因此 JavaScript 支持函数式编程,可以使用 Lambda函数和闭包( closure),当然 Java 8 也开始支持函数式编程,提供了对 Lambda 表达式以及函数式接口的支持。对于这类问题, 在面试的时候最好还是用自己的语言回答会更加靠谱, 不要背网上所谓的标准答案。
Java 中如何实现序列化,有什么意义?
序列化就是一种用来处理对象流的机制, 所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作, 也可将流化后的对象传输于网络之间。序列化是为了解决对象流读写操作时可能引发的问题( 如果不进行序列化可能会存在数据乱序的问题)。 要实现序列化, 需要让一个类实现 Serializable 接口, 该接口是一个标识性接口, 标注该类对象是可被序列化的, 然后使用一个输出流来构造一个对象输出流并通过writeObject(Object)方法就可以将实现对象写出( 即保存其状态);如果需要反序列化则可以用一个输入流建立对象输入流,然后通过 readObject 方法从流中读取对象。序列化除了能够实现对象的持久化之外, 还能够用于对象的深度克隆( 可以参考第 29 题)。
谈谈 Java 中的继承(Inheritance),包括单继承和多层继承的概念。
继承
继承是一种使得一个类可以继承另一个类的特性和行为的机制。
单继承与多层继承
- 单继承:一个类只能直接继承一个父类。
- 多层继承:一个类可以间接继承多个父类,形成继承链。
示例代码
public class Animal {
public void move() {
System.out.println("Moving...");
}
}
public class Dog extends Animal {
@Override
public void move() {
System.out.println("Running...");
}
}
public class Poodle extends Dog {
// 更多的继承...
}
描述 Java 中的构造函数(Constructor)的特点和作用。
构造函数
构造函数是一种特殊的方法,用于初始化新创建的对象。
特点
- 名称与类名相同:构造函数的名称必须与它所在的类名完全一致。
- 无返回类型:构造函数没有返回类型声明,连
void都不包括。 - 可以重载:一个类可以有多个构造函数,只要它们的参数列表不同即可。
示例代码
public class Car {
private String model;
private int year;
public Car() {
this.model = "Unknown";
this.year = 2023;
}
public Car(String model, int year) {
this.model = model;
this.year = year;
}
// getters and setters...
}
探探对java多态的理解
Java中的多态性是指一个接口或抽象类可以被不同的类实现,或者一个父类的引用可以指向其子类的对象。这允许我们编写更加通用的代码,并能够处理不同类型的对象。
多态性的主要特征包括:
- 方法重写 (Override): 当子类继承父类时,可以重新定义父类的方法,以提供特定于子类的行为。
- 方法重载 (Overload): 在同一个类中定义多个同名但参数列表不同的方法。
- 接口实现 (Implementation): 一个类可以实现多个接口,从而表现出多种行为。
多态的实现方式:
- 使用继承关系。
- 利用接口。
多态的优点:
- 提高代码的复用性和扩展性。
- 提高程序的可维护性。
- 可以写出更灵活、更抽象的代码。
什么是内部类?内部类的作用
内部类 (Inner Class) 是定义在另一个类(外部类)内部的类。内部类可以根据它是否位于静态上下文中分为两种类型:非静态内部类(也称为成员内部类)和静态内部类(也称为静态嵌套类)。
内部类的特点:
- 内部类可以直接访问外部类的成员(包括私有成员)。
- 非静态内部类的对象隐式地持有对其外部类对象的一个引用。
- 内部类可以被声明为
public、protected、private或default。 - 内部类可以访问外部类的静态成员和实例成员。
内部类的作用:
- 封装:内部类可以隐藏在外部类中,只有通过外部类才能访问它们。
- 代码组织:内部类使得代码结构更清晰,逻辑更紧凑。
- 灵活性:内部类提供了一种创建更紧密相关对象的机制。
- 匿名内部类:可以在不命名的情况下直接创建内部类的实例,通常用于实现接口或继承抽象类。
抽象类和接口区别
抽象类 (Abstract Class) 和 接口 (Interface) 在Java中都是用来实现抽象和多态的重要工具,但它们之间存在明显的差异:
特征抽象类接口实现方式可以包含抽象方法和具体方法。只能包含抽象方法(Java 8后也可包含默认方法)。访问修饰符可以指定任何访问级别。只能是 public 或默认(在同一个包内可见)。继承只能被一个类继承。一个类可以实现多个接口。构造函数可以有构造函数。不能有构造函数。成员变量可以有成员变量。只能有静态常量。实现限制一个类可以同时继承一个抽象类并实现多个接口。一个类可以实现多个接口。
抽象类的意义
抽象类 的意义在于:
- 模板设计:抽象类可以作为其他类的基础模板,定义一个类族的共同属性和行为。
- 约束实现:抽象类可以定义必须由子类实现的方法,确保子类遵循一定的规范。
- 代码复用:抽象类可以提供部分实现,子类只需关注具体的实现细节即可。
抽象类与接口的应用场景
抽象类的应用场景:
- 当需要定义一组具有相似属性和行为的类时。
- 当希望某些方法具有默认实现,而其他方法需要由子类实现时。
- 当需要利用构造函数或其他非抽象方法时。
接口的应用场景:
- 当需要定义一组操作的协议,而不关心这些操作的具体实现时。
- 当需要一个类支持多种行为时(即实现多个接口)。
- 当需要保证类的某些方法的签名一致时。
抽象类是否可以没有方法和属性?
答案: 抽象类可以没有方法和属性。
解释:
- 定义:抽象类是一种特殊的类,它可以包含抽象方法(没有实现的方法),也可以包含已实现的方法和字段。
- 抽象方法:抽象方法是没有方法体的方法,只声明了方法签名(返回类型、方法名、参数列表)。
- 抽象类的特性:
- 抽象类本身不能被实例化,只能被继承。
- 子类要么实现所有抽象方法成为具体类,要么继续声明为抽象类。
- 抽象类可以包含非抽象的方法和字段,也可以不包含任何抽象方法和字段。
示例:抽象类可以没有任何抽象方法或属性。
接口的意义
接口 在Java中具有重要的意义,它是实现多态性和抽象的一种方式。
- 定义:接口是一组抽象方法的集合,它定义了行为的规范。
- 特点:
- 接口中所有的方法默认都是公共的和抽象的。
- 从Java 8开始,接口可以包含默认方法和静态方法。
- 一个类可以实现多个接口,从而实现多重继承的效果。
- 目的:
- 规范:接口定义了实现该接口的类必须遵守的行为规范。
- 多态性:接口允许一个类拥有多个类型,增强了程序的灵活性。
- 扩展性:通过实现接口,类可以方便地扩展功能,无需修改原有代码。
- 解耦:接口使系统各个部分之间的依赖最小化,提高系统的可维护性。
泛型中extends和super的区别
在Java泛型中,extends 和 super 关键字用于限定类型参数的范围:
关键字作用示例extends指定类型参数的上限,即类型参数必须是后面跟的类的子类或自身。List<? extends Number> 表示列表中的元素类型必须是 Number 或其子类。super指定类型参数的下限,即类型参数必须是后面跟的类的超类或自身。List<? super Integer> 表示列表中的元素类型必须是 Integer 或其超类 Number。
父类的静态方法能否被子类重写
答案: 父类的静态方法不能被子类重写。
解释:
- 静态方法:静态方法属于类而不是类的实例,因此不受继承的影响。
- 重写:重写发生在子类实例方法覆盖父类实例方法的情况下。
- 规则:子类中声明相同签名的静态方法被视为新的方法,与父类的静态方法无关。
1.9 泛型
为什么需要泛型?
- 适用于多种数据类型执行相同的代码
private static int add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static float add(float a, float b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
private static double add(double a, double b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}
如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法;通过泛型,我们可以复用为一个方法:
private static <T extends Number> double add(T a, T b) {
System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
return a.doubleValue() + b.doubleValue();
}
- 泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)
看下这个例子:
List list = new ArrayList();
list.add("xxString");
list.add(100d);
list.add(new Person());
我们在使用上述list中,list中的元素都是Object类型(无法约束其中的类型),所以在取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现java.lang.ClassCastException异常。
引入泛型,它将提供类型的约束,提供编译前的检查:
List<String> list = new ArrayList<String>();
// list中只能放String, 不能放其它类型的元素
说一下泛型原理,并举例说明
Java中的泛型允许类型安全地使用容器,使得容器可以容纳任意类型的对象。泛型的基本原理包括类型参数、类型擦除、边界等。泛型的使用示例如下:
public class GenericBox<T> {
private T item;
public void set(T item) {
this.item = item;
}
public T get() {
return item;
}
}
// 使用泛型类
GenericBox<String> box = new GenericBox<>();
box.set("Hello");
String s = box.get();
泛型类如何定义使用?
- 从一个简单的泛型类看起:
class Point<T>{ // 此处可以随便写标识符号,T是type的简称
private T var ; // var的类型由T指定,即:由外部指定
public T getVar(){ // 返回值的类型由外部决定
return var ;
}
public void setVar(T var){ // 设置的类型也由外部决定
this.var = var ;
}
}
public class GenericsDemo06{
public static void main(String args[]){
Point<String> p = new Point<String>() ; // 里面的var类型为String类型
p.setVar("it") ; // 设置字符串
System.out.println(p.getVar().length()) ; // 取得字符串的长度
}
}
- 多元泛型
class Notepad<K,V>{ // 此处指定了两个泛型类型
private K key ; // 此变量的类型由外部决定
private V value ; // 此变量的类型由外部决定
public K getKey(){
return this.key ;
}
public V getValue(){
return this.value ;
}
public void setKey(K key){
this.key = key ;
}
public void setValue(V value){
this.value = value ;
}
}
public class GenericsDemo09{
public static void main(String args[]){
Notepad<String,Integer> t = null ; // 定义两个泛型类型的对象
t = new Notepad<String,Integer>() ; // 里面的key为String,value为Integer
t.setKey("汤姆") ; // 设置第一个内容
t.setValue(20) ; // 设置第二个内容
System.out.print("姓名;" + t.getKey()) ; // 取得信息
System.out.print(",年龄;" + t.getValue()) ; // 取得信息
}
}
泛型接口如何定义使用?
- 简单的泛型接口
interface Info<T>{ // 在接口上定义泛型
public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型
}
class InfoImpl<T> implements Info<T>{ // 定义泛型接口的子类
private T var ; // 定义属性
public InfoImpl(T var){ // 通过构造方法设置属性内容
this.setVar(var) ;
}
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
}
public class GenericsDemo24{
public static void main(String arsg[]){
Info<String> i = null; // 声明接口对象
i = new InfoImpl<String>("汤姆") ; // 通过子类实例化对象
System.out.println("内容:" + i.getVar()) ;
}
}
泛型方法如何定义使用?
泛型方法,是在调用方法的时候指明泛型的具体类型。
- 定义泛型方法语法格式

- 调用泛型方法语法格式

说明一下,定义泛型方法时,必须在返回值前边加一个<T>,来声明这是一个泛型方法,持有一个泛型T,然后才可以用泛型T作为方法的返回值。
Class<T>的作用就是指明泛型的具体类型,而Class<T>类型的变量c,可以用来创建泛型类的对象。
为什么要用变量c来创建对象呢?既然是泛型方法,就代表着我们不知道具体的类型是什么,也不知道构造方法如何,因此没有办法去new一个对象,但可以利用变量c的newInstance方法去创建对象,也就是利用反射创建对象。
泛型方法要求的参数是Class<T>类型,而Class.forName()方法的返回值也是Class<T>,因此可以用Class.forName()作为参数。其中,forName()方法中的参数是何种类型,返回的Class<T>就是何种类型。在本例中,forName()方法中传入的是User类的完整路径,因此返回的是Class<User>类型的对象,因此调用泛型方法时,变量c的类型就是Class<User>,因此泛型方法中的泛型T就被指明为User,因此变量obj的类型为User。
当然,泛型方法不是仅仅可以有一个参数Class<T>,可以根据需要添加其他参数。
为什么要使用泛型方法呢?因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新new一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。
泛型的上限和下限?
在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。
上限
class Info<T extends Number>{ // 此处泛型只能是数字类型
private T var ; // 定义泛型变量
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
public String toString(){ // 直接打印
return this.var.toString() ;
}
}
public class demo1{
public static void main(String args[]){
Info<Integer> i1 = new Info<Integer>() ; // 声明Integer的泛型对象
}
}
下限
class Info<T>{
private T var ; // 定义泛型变量
public void setVar(T var){
this.var = var ;
}
public T getVar(){
return this.var ;
}
public String toString(){ // 直接打印
return this.var.toString() ;
}
}
public class GenericsDemo21{
public static void main(String args[]){
Info<String> i1 = new Info<String>() ; // 声明String的泛型对象
Info<Object> i2 = new Info<Object>() ; // 声明Object的泛型对象
i1.setVar("hello") ;
i2.setVar(new Object()) ;
fun(i1) ;
fun(i2) ;
}
public static void fun(Info<? super String> temp){ // 只能接收String或Object类型的泛型,String类的父类只有Object类
System.out.print(temp + ", ") ;
}
}
如何理解Java中的泛型是伪泛型?
泛型中类型擦除 Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
什么是Java的泛型和类型擦除?
Java的泛型是一种在编译时提供类型安全检查的机制,它允许程序员在类、接口和方法中定义类型参数。泛型的主要目的是提供代码的可重用性和类型安全。通过使用泛型,可以编写出适用于多种数据类型的代码,而不需要为每种数据类型编写重复的代码。
泛型在Java中的实现是基于类型擦除的。类型擦除是Java泛型实现的一种编译器技术,它在编译时将泛型类型信息转换为它们的原始类型(擦除类型信息),并在运行时不保留这些类型信息。这意味着所有的泛型类都在运行时被当作它们的原始类型来处理,泛型参数被替换为它们的边界类型,通常是Object。
例如,List<String>在编译后会变成List,而Map<String, Integer>会变成Map。所有的泛型类型信息在运行时都丢失了,这就是类型擦除。为了在运行时能够检查泛型类型,Java编译器会在代码中插入一些类型检查和转换,这些在编译时进行,以确保类型安全。
类型擦除虽然解决了泛型的向后兼容性问题,但也带来了一些限制,比如不能创建泛型数组,因为数组在Java中是有具体类型的,而且类型擦除后无法确定原始的泛型类型。
1.10 注解
注解的作用?
注解是JDK1.5版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。它主要的作用有以下四方面:
- 生成文档,通过代码里标识的元数据生成javadoc文档。
- 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证。
- 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码。
- 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例。
注解的常见分类?
Java自带的标准注解,包括
@Override、@Deprecated和@SuppressWarnings,分别用于标明重写某个方法、标明某个类或方法过时、标明要忽略的警告,用这些注解标明后编译器就会进行检查。元注解,元注解是用于定义注解的注解,包括
@Retention、@Target、@Inherited、@Documented@Retention用于标明注解被保留的阶段@Target用于标明注解使用的范围@Inherited用于标明注解可继承@Documented用于标明是否生成javadoc文档
自定义注解,可以根据自己的需求定义注解,并可用元注解对自定义注解进行注解。
什么是 Java 的注解?列举一些常见的注解并说明其用途。
Java 注解
Java 注解是一种元数据,用于向编译器、JVM 或工具提供有关程序元素的附加信息。
常见注解
注解用途@Override表示方法覆盖了超类中的方法。@Deprecated标记过时的代码。@SuppressWarnings抑制编译警告。@FunctionalInterface标记接口为函数式接口。@SafeVarargs标记一个方法或构造函数以抑制关于可变参数数组的警告。
说说你对Java注解的理解
Java注解是一种元数据,用于为代码添加附加信息。注解不会影响代码的语义,但可以被编译器或运行时工具所使用。Java中的注解类型包括:
- 元注解:如
@Retention和@Target,用于定义注解本身的属性。 - 内置注解:如
@Override和@Deprecated,用于提供源代码级别的元数据。 - 自定义注解:用户可以定义自己的注解,然后在代码中使用。
什么是Java的注解和它们的作用?
Java的注解(Annotations)是一种用于提供元数据的特殊接口,它们可以被用于类、方法、变量、参数等Java元素上,以传递额外的信息给编译器或其他工具。注解不会直接影响程序的逻辑,但它们可以在编译时、类加载时或运行时被读取和处理。
注解的主要作用包括:
- 提供编译时的信息:注解可以被编译器用来检查代码,例如
@Override注解表明一个方法覆盖了父类中的方法,如果声明不正确,编译器会报错。 - 生成额外的代码:某些注解可以指导编译器或工具生成额外的代码。例如,
@Entity注解用于标记一个类是数据库中的一个表,ORM(对象关系映射)工具如Hibernate可以使用这个信息生成相应的SQL查询。 - 运行时信息:注解可以在运行时被读取,用于配置或改变程序的行为。例如,Spring框架使用注解来配置和管理Bean。
Java内置了一些标准注解,如@Override、@Deprecated、@SuppressWarnings等。此外,还可以自定义注解,通过定义注解的保留策略(@Retention)、目标(@Target)和使用元数据(@Documented)来控制注解的行为。
注解的使用使得代码更加简洁和灵活,它们提供了一种强大的机制来描述和处理元数据,而不需要修改业务逻辑代码。
1.11 反射
什么是反射?
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个方法和属性;
这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
简单说,反射就是在运行状态把 Java 类中的各种成分映射成相应相应的Java类,可以动态得获取所有的属性以及动态调用任意一个方法。

反射的使用?
在Java中,Class类与java.lang.reflect类库一起对反射技术进行了全力的支持。在反射包中,我们常用的类主要有Constructor类表示的是Class 对象所表示的类的构造方法,利用它可以在运行时动态创建对象、Field表示Class对象所表示的类的成员变量,通过它可以在运行时动态修改成员变量的属性值(包含private)、Method表示Class对象所表示的类的成员方法,通过它可以动态调用对象的方法(包含private)
- Class类对象的获取
@Test
public void classTest() throws Exception {
// 获取Class对象的三种方式
logger.info("根据类名: \t" + User.class);
logger.info("根据对象: \t" + new User().getClass());
logger.info("根据全限定类名:\t" + Class.forName("com.test.User"));
// 常用的方法
logger.info("获取全限定类名:\t" + userClass.getName());
logger.info("获取类名:\t" + userClass.getSimpleName());
logger.info("实例化:\t" + userClass.newInstance());
}
- Constructor类及其用法
- Field类及其用法
- Method类及其用法
getName、getCanonicalName与getSimpleName的区别?
- getSimpleName:只获取类名
- getName:类的全限定名,jvm中Class的表示,可以用于动态加载Class对象,例如Class.forName。
- getCanonicalName:返回更容易理解的表示,主要用于输出(toString)或log打印,大多数情况下和getName一样,但是在内部类、数组等类型的表示形式就不同了。
什么是 Java 的反射机制?它有哪些应用场景?
Java 反射机制
反射机制允许程序在运行时访问类的信息和操作对象的状态。
应用场景
- 动态实例化对象:根据字符串创建类的实例。
- 调用私有方法:访问类的私有方法或字段。
- 获取类的信息:例如字段、方法、构造函数等。
- 框架开发:如 Spring 框架广泛使用反射来管理 Bean 的生命周期。
说说你对Java反射的理解
Java反射允许程序在运行时获取类的信息并操纵类的对象。反射的主要功能包括:
- 获取类、构造器、方法和字段的信息。
- 创建和操作对象。
- 调用方法和设置字段值。
反射的主要类包括 Class, Constructor, Method, Field 等。
什么是Java的反射机制?
Java反射机制是Java运行时环境的一部分,允许程序在运行时访问和操作类、方法、属性以及其他类型的信息。通过反射,程序可以创建对象的实例、绑定方法调用、访问和修改字段值,即使这些信息在编译时是未知的。
反射的核心类是java.lang.reflect包中的Class类和相关类,如Method、Field、Constructor等。使用反射的典型场景包括:
- 动态类加载和实例化
- 运行时方法调用
- 动态代理的创建
反射机制虽然强大,但也有一定的性能开销,并且在编译时无法进行类型检查,因此应谨慎使用。
描述Java中的反射和内省的区别。
在 Java 中,反射(Reflection)和内省(Introspection)是密切相关但定位不同的技术—— 核心区别在于:反射是 “底层通用能力”,内省是 “基于反射的高层特定工具”,内省是反射的 “子集 + 封装”,专门用于操作 JavaBean 的属性和方法。
核心定义与定位
####### 1. 反射(Reflection)
- 定义:Java 语言提供的一种底层机制,允许程序在运行时获取类的完整信息(类名、父类、接口、字段、方法、构造器等),并能动态操作这些信息(创建对象、调用方法、修改字段值等),无需在编译时明确知道类的具体结构。
- 定位:通用型底层能力,覆盖所有类的所有成员(包括普通类、JavaBean、系统类等),灵活性极高,但使用复杂,需要直接操作
Class、Field、Method等 API。
####### 2. 内省(Introspection)
- 定义:Java 提供的一套高层 API,专门用于操作 JavaBean(遵循 “属性 getter/setter 规范” 的类),目的是简化 JavaBean 的属性访问(获取 / 设置属性值)和事件处理。
- 定位:特定场景的工具类,基于反射实现(底层依赖反射 API),屏蔽了反射的底层细节,仅关注 JavaBean 的 “属性”(而非字段),使用更简洁。
关键区别对比
| 对比维度 | 反射(Reflection) | 内省(Introspection) |
|---|---|---|
| 核心目标 | 通用型:运行时获取 / 操作任意类的任意成员 | 专用型:简化 JavaBean 的属性访问和事件处理 |
| 操作对象 | 类的所有成员(字段、方法、构造器、接口等) | JavaBean 的 “属性”(通过 getter/setter 间接映射) |
| 依赖关系 | 底层机制,不依赖其他高层 API | 基于反射实现(底层调用反射 API),是反射的封装 |
| 使用复杂度 | 复杂,需手动处理 Class/Field/Method,需处理异常(如 NoSuchMethodException) | 简单,通过 BeanInfo/PropertyDescriptor 等 API 直接操作属性,屏蔽底层细节 |
| 适用场景 | 1. 框架开发(如 Spring 容器、MyBatis);2. 动态代理、序列化;3. 需动态操作非 JavaBean 类的场景 | 1. 操作 JavaBean 的属性(如 Swing 组件绑定、JSON 序列化);2. 框架中简化 JavaBean 处理(如 Spring 注入) |
| 关注角度 | 关注 “类的结构”(字段、方法是直接成员) | 关注 “JavaBean 的逻辑属性”(属性≠字段,需通过 getter/setter 定义) |
核心差异举例(代码对比)
场景:操作 JavaBean 的属性(如 User 类的 name 属性)
假设存在 JavaBean:
// JavaBean:遵循 getter/setter 规范
class User {
private String name; // 字段
// 逻辑属性 name:通过 getter/setter 定义
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}
####### 1. 用反射操作属性(间接通过 setter/getter 方法)
反射不直接识别 “属性”,需手动找到 setName/getName 方法并调用:
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
// 1. 获取 Class 对象
Class<User> userClass = User.class;
// 2. 创建对象
User user = userClass.getConstructor().newInstance();
// 3. 找到 setName 方法(需指定参数类型)
Method setNameMethod = userClass.getMethod("setName", String.class);
// 4. 调用 setName 方法设置属性
setNameMethod.invoke(user, "张三");
// 5. 找到 getName 方法
Method getNameMethod = userClass.getMethod("getName");
// 6. 调用 getName 方法获取属性
String name = (String) getNameMethod.invoke(user);
System.out.println(name); // 输出:张三
}
}
- 缺点:需手动拼接方法名(
setName/getName)、处理参数类型,代码繁琐。
####### 2. 用内省操作属性(直接操作 “逻辑属性”)
内省直接识别 name 为属性,无需关注方法名细节:
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
public class IntrospectionDemo {
public static void main(String[] args) throws Exception {
// 1. 创建 JavaBean 对象
User user = new User();
// 2. 获取 JavaBean 的 BeanInfo(包含属性、方法等信息)
BeanInfo beanInfo = Introspector.getBeanInfo(User.class);
// 3. 获取所有属性描述符(每个 PropertyDescriptor 对应一个属性)
PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
// 4. 找到 name 属性的描述符
PropertyDescriptor nameDescriptor = null;
for (PropertyDescriptor desc : descriptors) {
if ("name".equals(desc.getName())) {
nameDescriptor = desc;
break;
}
}
// 5. 通过描述符调用 setter 设置属性
nameDescriptor.getWriteMethod().invoke(user, "李四");
// 6. 通过描述符调用 getter 获取属性
String name = (String) nameDescriptor.getReadMethod().invoke(user);
System.out.println(name); // 输出:李四
}
}
- 优点:直接操作 “属性名”(
name),无需关心setName/getName的方法名拼写,代码更简洁。
补充说明
- 内省是反射的 “专用封装”:内省的底层本质是调用反射 API(比如
PropertyDescriptor内部会通过反射查找 getter/setter 方法),但它只聚焦 JavaBean 场景,简化了开发。 - 反射的灵活性更高:如果需要操作非 JavaBean 类(比如没有 getter/setter 的普通类),或需要访问私有字段、调用私有方法,只能用反射,内省无法实现。
- 实际开发中的选择:
- 若操作 JavaBean 的属性:优先用内省(或更简洁的第三方工具,如 Apache Commons BeanUtils、Spring BeanWrapper);
- 若需要动态操作类的任意成员(如私有方法、构造器):必须用反射。
总结
- 反射是 “底层通用工具”,能做所有运行时类操作,但复杂;
- 内省是 “高层专用工具”,只做 JavaBean 相关操作,简单高效;
- 关系:内省 ≈ 反射 + JavaBean 规范封装。
1.12 异常
Java异常类层次结构?
Throwable
是 Java 语言中所有错误与异常的超类。
- Error 类及其子类:程序中无法处理的错误,表示运行应用程序中出现了严重的错误。
- Exception 程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。

- 运行时异常
都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
- 非运行时异常 (编译异常)
是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。
可查的异常(checked exceptions)和不可查的异常(unchecked exceptions)区别?
- 可查异常(编译器要求必须处置的异常):
正确的程序在运行中,很容易出现的、情理可容的异常状况。可查异常虽然是异常状况,但在一定程度上它的发生是可以预计的,而且一旦发生这种异常状况,就必须采取某种方式进行处理。
除了RuntimeException及其子类以外,其他的Exception类及其子类都属于可查异常。这种异常的特点是Java编译器会检查它,也就是说,当程序中可能出现这类异常,要么用try-catch语句捕获它,要么用throws子句声明抛出它,否则编译不会通过。
- 不可查异常(编译器不要求强制处置的异常)
包括运行时异常(RuntimeException与其子类)和错误(Error)。
throw和throws的区别?
throw 用于抛出 java.lang.Throwable 类的一个实例化对象,意思是说你可以通过关键字 throw 抛出一个 Error 或者 一个 Exception, 如: throw new IllegalArgumentException(“ size must be multiple of 2″) 而 throws 的作用是作为方法声明和签名的一部分, 方法被抛出相应的异常以便调用者能处理。Java 中, 任何未处理的受检查异常强制在 throws 子句中声明。
- 异常的申明(throws)
在Java中,当前执行的语句必属于某个方法,Java解释器调用main方法执行开始执行程序。若方法中存在检查异常,如果不对其捕获,那必须在方法头中显式声明该异常,以便于告知方法调用者此方法有异常,需要进行处理。 在方法中声明一个异常,方法头中使用关键字throws,后面接上要声明的异常。若声明多个异常,则使用逗号分割。如下所示:
public static void method() throws IOException, FileNotFoundException{
//something statements
}
- 异常的抛出(throw)
如果代码可能会引发某种错误,可以创建一个合适的异常类实例并抛出它,这就是抛出异常。如下所示:
public static double method(int value) {
if(value == 0) {
throw new ArithmeticException("参数不能为0"); //抛出一个运行时异常
}
return 5.0 / value;
}
Java 中,受检查异常 和 不受检查异常的区别?
受检查异常编译器在编译期间检查。 对于这种异常, 方法强制处理或者通过throws 子句声明。 其中一种情况是 Exception 的子类但不是RuntimeException 的子类。非受检查是 RuntimeException 的子类,在编译阶段不受编译器的检查。
Java 7 的 try-with-resource?
如果你的资源实现了 AutoCloseable 接口,你可以使用这个语法。大多数的 Java 标准资源都继承了这个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭。
public void automaticallyCloseResource() {
File file = new File("./tmp.txt");
try (FileInputStream inputStream = new FileInputStream(file);) {
// use the inputStream to read a file
} catch (FileNotFoundException e) {
log.error(e);
} catch (IOException e) {
log.error(e);
}
}
异常的底层?
提到JVM处理异常的机制,就需要提及Exception Table,以下称为异常表。我们暂且不急于介绍异常表,先看一个简单的 Java 处理异常的小例子。
public static void simpleTryCatch() {
try {
testNPE();
} catch (Exception e) {
e.printStackTrace();
}
}
使用javap来分析这段代码(需要先使用javac编译)
//javap -c Main
public static void simpleTryCatch();
Code:
0: invokestatic #3 // Method testNPE:()V
3: goto 11
6: astore_0
7: aload_0
8: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V
11: return
Exception table:
from to target type
0 3 6 Class java/lang/Exception
看到上面的代码,应该会有会心一笑,因为终于看到了Exception table,也就是我们要研究的异常表。
异常表中包含了一个或多个异常处理者(Exception Handler)的信息,这些信息包含如下
- from 可能发生异常的起始点
- to 可能发生异常的结束点
- target 上述from和to之前发生异常后的异常处理者的位置
- type 异常处理者处理的异常的类信息
什么时候用断言(assert)?
断言在软件开发中是一种常用的调试方式, 很多开发语言中都支持这种机制。一般来说,断言用于保证程序最基本、关键的正确性。断言检查通常在开发和测试时开启。为了保证程序的执行效率, 在软件发布后断言检查通常是关闭的。断言是一个包含布尔表达式的语句, 在执行这个语句时假定该表达式为 true; 如果表达式的值为 false,那么系统会报告一个 AssertionError。断言的使用如下面的代码所示: assert(a > 0); // throws an AssertionError if a <= 0 断言可以有两种形式: assert Expression1;assert Expression1 : Expression2 ;Expression1 应该总是产生一个布尔值。 Expression2 可以是得出一个值的任意表达式; 这个值用于生成显示更多调试信息的字符串消息。 要在运行时启用断言, 可以在启动 JVM 时使用-enableassertions 或者-ea 标记。要在运行时选择禁用断言, 可以在启动 JVM 时使用-da 或者-disableassertions标记。要在系统类中启用或禁用断言,可使用-esa 或-dsa 标记。还可以在包的基础上启用或者禁用断言。
注意: 断言不应该以任何方式改变程序的状态。简单的说, 如果希望在不满足某些条件时阻止代码的执行, 就可以考虑用断言来阻止它。
解释 Java 中的断言(Assertion)及其使用场景。
断言
断言是用于验证程序假设的条件。如果断言失败,则会抛出 AssertionError。
使用场景
- 调试:在开发过程中检查代码的正确性。
- 测试:在单元测试中验证预期的行为。
示例代码
assert x > 0 : "x should be positive";
Error 和Exception 有什么区别?
Error 表示系统级的错误和程序不必处理的异常,是恢复不是不可能但很困难的情况下的一种严重问题; 比如内存溢出, 不可能指望程序能处理这样的情况; Exception 表示需要捕捉或者需要程序进行处理的异常, 是一种设计或实现问题; 也就是说, 它表示如果程序运行正常, 从不会发生的情况。 面试题:2005 年摩托罗拉的面试中曾经问过这么一个问题“ If a process reports a stack overflow run-time error, what’ s the most possible cause?”, 给 了四 个 选 项 a. lack of memory; b. write on an invalid memory space; c. recursive function calling; d. array index out of boundary. Java 程序在运行
时也可能会遭遇 StackOverflowError, 这是一个无法恢复的错误, 只能重新修改代码了,这个面试题的答案是 c。如果写了不能迅速收敛的递归,则很有可能引发栈溢出的错误, 如下所示:
class StackOverflowErrorTest {
public static void main(String[] args){
main(null);
}
}
提示:用递归编写程序时一定要牢记两点:1. 递归公式;2. 收敛条件( 什么时候就不再继续递归)。
try{}里有一个 return 语句,那么紧跟在这个try 后的finally{}里的代码会不会被执行,什么时候被执行,在return前还是后?
会执行, 在方法返回调用者前执行。 注意:在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try 中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值, 然后如果在 finally 中修改了返回值, 就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,C#中直接用编译错误的方式来阻止程序员干这种龌龊的事情, Java 中也可以通过提升 编译器的语法检查级别来产生警告或错误,Eclipse 中可以在如图所示的地方进行设置,强烈建议将此项设置为编译错误。
Java 语言如何进行异常处理,关键字:throws、throw、try、catch、finally 分别如何使用?
Java 通过面向对象的方法进行异常处理, 把各种不同的异常进行分类, 并提供了良好的接口。在 Java 中, 每个异常都是一个对象, 它是 Throwable 类或其子类 的实例。当一个方法出现异常后便抛出一个异常对象, 该对象中包含有异常信息,
调用这个对象的方法可以捕获到这个异常并可以对其进行处理。 Java 的异常处理是通过 5 个关键词来实现的: try、catch、 throw、 throws 和 finally。一般情况下是用 try 来执行一段程序, 如果系统会抛出( throw) 一个异常对象, 可以通过它的类型来捕获( catch) 它, 或通过总是执行代码块( finally) 来处理; try 用来指定一块预防所有异常的程序; catch 子句紧跟在 try 块后面,用来指定你想要捕获的异常的类型; throw 语句用来明确地抛出一个异常; throws 用来声明一个方法可能抛出的各种异常( 当然声明异常时允许无病呻吟); finally 为确保一段代码不管发生什么异常状况都要被执行; try 语句可以嵌套, 每当遇到一个 try 语句,异常的结构就会被放入异常栈中,直到所有的 try 语句都完成。如果下一级的 try 语句没有对某种异常进行处理,异常栈就会执行出栈操作,直到遇到有处理这种异常的 try 语句或者最终将异常抛给 JVM。
运行时异常与受检异常有何异同?
异常表示程序运行过程中可能出现的非正常状态, 运行时异常表示虚拟机的通常操作中可能遇到的异常, 是一种常见运行错误, 只要程序设计得没有问题通常就不会发生。受检异常跟程序运行的上下文环境有关, 即使程序设计无误, 仍然可能因使用的问题而引发。 Java 编译器要求方法必须声明抛出可能发生的受检异常, 但是并不要求必须声明抛出未被捕获的运行时异常。异常和继承一样, 是面向对象程序设计中经常被滥用的东西,在 Effective Java 中对异常的使用给出了以下指导原则:
不要将异常处理用于正常的控制流(设计良好的 API 不应该强迫它的调 用者为了正常的控制流而使用异常) 对可以恢复的情况使用受检异常,对编程错误使用运行时异常 避免不必要的使用受检异常(可以通过一些状态检测手段来避免异常的发生)
优先使用标准的异常 每个方法抛出的异常都要有文档 保持异常的原子性 不要在 catch 中忽略掉捕获到的异常
列出一些你常见的运行时异常?
ArithmeticException(算术异常) ClassCastException (类转换异常) IllegalArgumentException (非法参数异常) IndexOutOfBoundsException (下标越界异常) NullPointerException (空指针异常) SecurityException (安全异常)
异常
相关的关键字 throw、throws、try...catch、finally
- throws 用在方法签名上, 以便抛出的异常可以被调用者处理
- throw 方法内部通过throw抛出异常
- try 用于检测包住的语句块, 若有异常, catch子句捕获并执行catch块
关于finally
- finally不管有没有异常都要处理
- 当try和catch中有return时,finally仍然会执行,finally比return先执行
- 不管有木有异常抛出, finally在return返回前执行
- finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,仍然是之前保存的值),所以函数返回值是在finally执行前确定的
注意: finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值
finally不执行的几种情况: 程序提前终止如调用了System.exit, 病毒,断电
受检查异常和运行时异常
- 受检查的异常(checked exceptions),其必须被try...catch语句块所捕获, 或者在方法签名里通过throws子句声明。受检查的异常必须在编译时被捕捉处理,命名为Checked Exception是因为Java编译器要进行检查, Java虚拟机也要进行检查, 以确保这个规则得到遵守。
常见的checked exception: ClassNotFoundException IOException FileNotFoundException EOFException
- 运行时异常(runtime exceptions), 需要程序员自己分析代码决定是否捕获和处理,比如空指针,被0除...
常见的runtime exception: NullPointerException ArithmeticException ClassCastException IllegalArgumentException IllegalStateException IndexOutOfBoundsException NoSuchElementException
- Error的,则属于严重错误,如系统崩溃、虚拟机错误、动态链接失败等,这些错误无法恢复或者不可能捕捉,将导致应用程序中断,Error不需要捕获。
简述 Java 中的异常处理机制,包括 try-catch-finally 结构的使用。
Java 的异常处理机制通过 try, catch, 和 finally 块来管理程序运行中的异常情况。
- try 块:包含可能抛出异常的代码。
- catch 块:捕获并处理从 try 块中抛出的异常。
- finally 块:无论是否发生异常都会执行,用于释放资源等。
详细谈Java的异常体系
Java的异常体系是Java编程语言的一个重要组成部分,用于处理程序执行过程中发生的错误。Java的异常体系主要分为两大类:检查异常 (Checked Exceptions) 和 非检查异常 (Unchecked Exceptions)。
检查异常 (Checked Exceptions)
- 定义:这些异常由Java编译器强制处理。如果方法声明抛出检查异常,调用者必须处理这些异常,要么捕获,要么继续声明抛出。
- 示例:
IOException,SQLException - 目的:确保程序的健壮性和完整性。
非检查异常 (Unchecked Exceptions)
- 定义:这些异常通常由编程错误引起,不需要也不强制处理。它们通常是运行时异常,如
NullPointerException或IndexOutOfBoundsException。 - 示例:
NullPointerException,IllegalArgumentException - 目的:帮助开发者识别和修复错误。
错误 (Errors)
- 定义:这些异常通常不可恢复,如
OutOfMemoryError或ThreadDeath。 - 示例:
OutOfMemoryError,VirtualMachineError - 目的:通常不处理,而是让程序终止。
描述Java中的异常处理机制
Java中的异常处理机制是一种错误处理机制,用于处理程序运行时发生的异常情况。异常是程序运行时出现的不正常情况,可能会导致程序终止或产生不可预期的行为。
Java中的异常分为两大类:
- 检查型异常(Checked Exception):这些异常必须在编写代码时显式处理(try-catch)或声明抛出(throws)。它们通常是外部错误,如文件未找到、无法读取等。
- 非检查型异常(Unchecked Exception):包括运行时异常(RuntimeException)和错误(Error)。这些异常通常是由程序错误引起的,如数组越界、空指针等。
异常处理机制主要包括以下几个关键字:
try:定义一个代码块,该代码块中可能会抛出异常。catch:捕获并处理特定类型的异常。finally:无论是否捕获或处理异常,都会执行的代码块。throw:用于手动抛出异常。throws:声明一个方法可能抛出的异常。
通过使用异常处理机制,程序可以更加健壮,能够优雅地处理和恢复异常情况,而不是崩溃。
1.13 SPI机制
什么是SPI机制?
SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦。
SPI整体机制图如下:

当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader。
SPI机制的应用?
- SPI机制 - JDBC DriverManager
在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要用Class.forName("com.mysql.jdbc.Driver")来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的SPI扩展机制来实现。
- JDBC接口定义
首先在java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的。
- mysql实现
在mysql的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。
- postgresql实现
同样在postgresql的jar包postgresql-42.0.0.jar中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver,这是postgresql对Java的java.sql.Driver的实现。
- 使用方法
上面说了,现在使用SPI扩展来加载具体的驱动,我们在Java中写连接数据库的代码的时候,不需要再使用Class.forName("com.mysql.jdbc.Driver")来加载驱动了,而是直接使用如下代码:
String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url,username,password);
.....
SPI机制的简单示例?
我们现在需要使用一个内容搜索接口,搜索的实现可能是基于文件系统的搜索,也可能是基于数据库的搜索。
- 先定义好接口
public interface Search {
public List<String> searchDoc(String keyword);
}
- 文件搜索实现
public class FileSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
System.out.println("文件搜索 "+keyword);
return null;
}
}
- 数据库搜索实现
public class DatabaseSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
System.out.println("数据搜索 "+keyword);
return null;
}
}
- resources 接下来可以在resources下新建META-INF/services/目录,然后新建接口全限定名的文件:
com.cainiao.ys.spi.learn.Search,里面加上我们需要用到的实现类
com.cainiao.ys.spi.learn.FileSearch
- 测试方法
public class TestCase {
public static void main(String[] args) {
ServiceLoader<Search> s = ServiceLoader.load(Search.class);
Iterator<Search> iterator = s.iterator();
while (iterator.hasNext()) {
Search search = iterator.next();
search.searchDoc("hello world");
}
}
}
可以看到输出结果:文件搜索 hello world
如果在com.cainiao.ys.spi.learn.Search文件里写上两个实现类,那最后的输出结果就是两行了。
这就是因为ServiceLoader.load(Search.class)在加载某接口时,会去META-INF/services下找接口的全限定名文件,再根据里面的内容加载相应的实现类。
这就是spi的思想,接口的实现由provider实现,provider只用在提交的jar包里的META-INF/services下根据平台定义的接口新建文件,并添加进相应的实现类内容就好。
1.14 测试
如何测试静态方法?
答案:测试 Java 静态方法的常用方式:
通过实例方法调用:若静态方法被实例方法封装,可测试实例方法:
public class MyClass { public static int sum(int a, int b) { return a + b; } public int calculate() { return sum(3, 4); } } // 测试calculate()间接验证sum()使用 PowerMock(需引入依赖):
@RunWith(PowerMockRunner.class) @PrepareForTest(MyClass.class) public class MyClassTest { @Test public void testStaticMethod() { PowerMockito.mockStatic(MyClass.class); Mockito.when(MyClass.sum(2, 3)).thenReturn(5); assertEquals(5, MyClass.sum(2, 3)); } }重构代码:将静态方法改为实例方法,提高可测试性。
直接调用:若静态方法无副作用且不依赖外部资源,可直接调用测试:
@Test public void testSum() { assertEquals(5, MyClass.sum(2, 3)); }
怎么利用 JUnit 来测试一个方法的异常?
答案:JUnit 中测试异常的方法:
使用
@Test(expected)(JUnit 4):@Test(expected = IllegalArgumentException.class) public void testDivideByZero() { int result = 1 / 0; // 期望抛出ArithmeticException }使用
try-catch块:@Test public void testInvalidInput() { try { methodThatThrowsException(null); fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { assertEquals("Input cannot be null", e.getMessage()); } }使用
assertThrows(JUnit 5):@Test public void testFileNotFound() { Exception exception = assertThrows( FileNotFoundException.class, () -> new FileInputStream("non_existent_file.txt") ); assertEquals("non_existent_file.txt (系统找不到指定的文件。)", exception.getMessage()); }使用
ExpectedException规则(JUnit 4):@Rule public ExpectedException exception = ExpectedException.none(); @Test public void testEmptyList() { exception.expect(IndexOutOfBoundsException.class); exception.expectMessage("Index: 0, Size: 0"); List<String> list = new ArrayList<>(); list.get(0); }
你使用过哪个单元测试库来测试你的 Java 程序?
答案:常用的 Java 单元测试库:
JUnit:最流行的 Java 测试框架,支持
@Test、@Before、@After等注解,最新版本为 JUnit 5(Jupiter)。Mockito:模拟对象框架,用于创建和配置 mock 对象,简化依赖复杂的测试场景。
AssertJ:提供流式断言 API,使测试代码更易读:
assertThat(list).hasSize(3).contains("a", "b", "c");Hamcrest:提供匹配器(Matcher)库,增强断言表达能力:
assertThat("test", allOf(startsWith("te"), endsWith("st")));PowerMock:扩展 Mockito,支持测试静态方法、私有方法和 final 类。
TestNG:功能丰富的测试框架,支持参数化测试、依赖测试和并行执行。
Spock:基于 Groovy 的测试框架,结合 BDD 风格,语法简洁。
个人常用组合:JUnit 5 + Mockito + AssertJ,覆盖大多数 Java 项目的测试需求。
@Before 和 @BeforeClass 有什么区别?
答案:JUnit 中@Before和@BeforeClass的区别:
| 特性 | @Before | @BeforeClass |
|---|---|---|
| 执行时机 | 每个测试方法执行前执行一次 | 所有测试方法执行前执行一次 |
| 方法修饰符 | 实例方法(无需 static) | 静态方法(必须 static) |
| 适用场景 | 初始化测试数据或对象 | 初始化重量级资源(如数据库连接) |
| 执行次数 | 测试类中有 n 个测试方法,则执行 n 次 | 无论有多少测试方法,仅执行 1 次 |
| JUnit 5 等效注解 | @BeforeEach | @BeforeAll |
示例代码:
public class MyTest {
private List<String> list;
@BeforeClass
public static void setUpClass() {
// 初始化数据库连接等重量级操作
}
@Before
public void setUp() {
list = new ArrayList<>(); // 每个测试方法前重置list
}
@Test
public void testAdd() {
list.add("a");
assertEquals(1, list.size());
}
@Test
public void testEmpty() {
assertTrue(list.isEmpty());
}
}
1.15 其它
进程和线程的区别
进程 和 线程 是操作系统中管理资源的基本单位,它们之间存在以下区别:
特征进程线程资源分配进程是系统进行资源分配和调度的基本单位。线程共享所属进程的资源,不单独分配资源。内存空间每个进程有自己的独立内存空间。同一进程内的线程共享内存空间。创建和销毁进程的创建和销毁代价较大。线程的创建和销毁代价较小。通信进程间通信(IPC)需要通过系统调用实现,如管道、消息队列等。线程间通信相对简单,可以通过共享内存等方式直接访问。调度进程切换开销较大。线程切换开销较小。并发性进程并发性较低。线程并发性较高。
说说你对依赖注入的理解
依赖注入是一种设计模式,用于减少组件之间的耦合度。依赖注入通常通过构造器、setter方法或者字段注入来实现。依赖注入框架如Spring可以帮助管理依赖关系。
谈你对解析与分派的认识
解析 (Resolution) 和分派 (Dispatching) 是面向对象编程中两个重要的概念。
解析 (Resolution)
- 定义:解析是指在编译阶段确定一个方法调用所对应的实现过程。
- 类型:静态解析(编译期解析)和动态解析(运行期解析)。
- 示例:静态解析发生在编译时,而动态解析发生在运行时。
分派 (Dispatching)
- 定义:分派是指选择一个方法的实际实现的过程。
- 类型:单分派和多分派。
- 示例:单分派基于接收者的类型决定方法实现;多分派基于多个参数的类型决定方法实现。
Java使用单分派,即方法的选择基于接收者类型。Java中的多态性主要体现在运行时分派上,通过虚拟方法调用来实现动态绑定。
讲一下常见编码方式?
常见的字符编码方式包括:
- ASCII:美国标准信息交换码,只包含了128个字符。
- ISO-8859-1:也称为Latin-1,是ISO/IEC 8859系列标准的一部分,包含了256个字符。
- UTF-8:Unicode Transformation Format,使用1到4个字节编码每个字符,广泛应用于网页和文件。
- UTF-16:使用2个或4个字节编码每个字符,适用于内部存储和处理。
- GBK:扩展了GB2312标准,用于简体中文字符集。
- GB18030:包含了GBK的所有字符,并增加了更多的汉字和符号,是目前中国国家标准的字符编码方案。
Java 中,DOM 和 SAX 解析器有什么不同?
DOM 解析器将整个 XML 文档加载到内存来创建一棵 DOM 模型树,这样可以更快的查找节点和修改 XML 结构, 而 SAX 解析器是一个基于事件的解析器, 不会将整个 XML 文档加载到内存。由于这个原因, DOM 比 SAX 更快, 也要求更多的内存, 不适合于解析大 XML 文件。
Socket 选项 TCP NO DELAY 是指什么?
答案:TCP_NODELAY是 Socket 的一个选项,用于禁用 Nagle 算法。默认情况下,TCP 为减少网络包数量,会将小数据块合并发送(Nagle 算法),但可能引入延迟。设置TCP_NODELAY=true可立即发送数据,适用于低延迟场景(如实时游戏、SSH 连接):
Socket socket = new Socket("localhost", 8080);
socket.setTcpNoDelay(true); // 禁用Nagle算法
TCP 协议与 UDP 协议有什么区别?
| 特性 | TCP | UDP |
|---|---|---|
| 连接性 | 面向连接(需三次握手建立连接) | 无连接(直接发送数据) |
| 可靠性 | 可靠传输(确认机制、重传、排序) | 不可靠(不保证送达或顺序) |
| 传输效率 | 低(需维护连接状态和重传机制) | 高(无额外开销) |
| 数据包大小 | 无限制(分段传输) | 受限于 MTU(通常≤65,507 字节) |
| 应用场景 | HTTP、FTP、SMTP 等需要可靠传输的场景 | DNS、视频流、实时游戏等低延迟场景 |
| 协议头开销 | 20 字节 | 8 字节 |
列出 5 个应该遵循的 JDBC 最佳实践
答案:JDBC 编程的最佳实践:
使用连接池:通过
HikariCP、Druid等连接池管理数据库连接,避免频繁创建和销毁连接:HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb"); config.setUsername("user"); config.setPassword("password"); HikariDataSource dataSource = new HikariDataSource(config);使用
PreparedStatement:预编译 SQL 语句,防止 SQL 注入并提高性能:String sql = "SELECT * FROM users WHERE username = ? AND role = ?"; try (PreparedStatement pstmt = connection.prepareStatement(sql)) { pstmt.setString(1, username); pstmt.setString(2, role); ResultSet rs = pstmt.executeQuery(); }资源自动关闭:使用
try-with-resources确保Connection、Statement和ResultSet被关闭:try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users")) { // 处理结果集 } // 自动关闭所有资源批处理操作:使用
addBatch()和executeBatch()执行批量插入 / 更新,减少数据库往返:String sql = "INSERT INTO users (name, age) VALUES (?, ?)"; try (PreparedStatement pstmt = connection.prepareStatement(sql)) { for (User user : userList) { pstmt.setString(1, user.getName()); pstmt.setInt(2, user.getAge()); pstmt.addBatch(); } pstmt.executeBatch(); }按列名获取结果:通过列名而非索引获取结果集数据,提高代码可读性和健壮性:
while (rs.next()) { String name = rs.getString("name"); // 优于 rs.getString(1) int age = rs.getInt("age"); }事务管理:对于需要原子性的操作,使用事务管理:
try (Connection conn = dataSource.getConnection()) { conn.setAutoCommit(false); try (Statement stmt = conn.createStatement()) { stmt.executeUpdate("UPDATE accounts SET balance = balance - 100 WHERE id = 1"); stmt.executeUpdate("UPDATE accounts SET balance = balance + 100 WHERE id = 2"); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } }异常处理:捕获并记录 SQL 异常,避免资源泄漏:
try (Connection conn = dataSource.getConnection()) { // 数据库操作 } catch (SQLException e) { logger.error("Database operation failed", e); throw new RuntimeException("Database error", e); }
什么是 DAO 模式?
DAO( Data Access Object) 顾名思义是一个为数据库或其他持久化机制提供了抽象接口的对象, 在不暴露底层持久化方案实现细节的前提下提供了各种数据访问操作。在实际的开发中, 应该将所有对数据源的访问操作进行抽象化后封装在一个公共 API 中。用程序设计语言来说, 就是建立一个接口, 接口中定义了此应用程序中将会用到的所有事务方法。在这个应用程序中, 当需要和数据源进行交
互的时候则使用这个接口, 并且编写一个单独的类来实现这个接口, 在逻辑上该类对应一个特定的数据存储。DAO 模式实际上包含了两个模式, 一是 Data Accessor( 数据访问器), 二是 Data Object( 数据对象), 前者要解决如何访问数据的问题, 而后者要解决的是如何用对象封装数据。
阐述 JDBC 操作数据库的步骤。
下面的代码以连接本机的 Oracle 数据库为例, 演示 JDBC 操作数据库的步骤。
加载驱动。
Class.forName("oracle.jdbc.driver.OracleDriver");
创建连接。
Connection con = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl","scott", "tiger");创建语句。
PreparedStatement ps = con.prepareStatement("select * from emp where sal between ? and ?");
ps.setInt(1, 1000);
ps.setInt(2, 3000);
执行语句。
ResultSet rs = ps.executeQuery();
处理结果。
while(rs.next()) {
System.out.println(rs.getInt("empno") + " - "+rs.getString("ename"));
// 关闭资源。
finally {
if(con != null) {
try {
con.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
提示: 关闭外部资源的顺序应该和打开的顺序相反, 也就是说先关闭 ResultSet、再关闭Statement、在关闭Connection。上面的代码只关闭了 Connection( 连接), 虽然通常情况下在关闭连接时, 连接上创建的语句和打开的游标也会关闭, 但不能保证总是如此, 因此应该按照刚才说的顺序分别关闭。此外, 第一步加载驱动在 JDBC 4.0 中是可以省略的( 自动从类路径中加载驱动),但是我们建议保留。
Statement 和PreparedStatement 有什么区别?哪个性能更好?
与 Statement 相比,① PreparedStatement 接口代表预编译的语句,它主要的优势在于可以减少 SQL 的编译错误并增加 SQL 的安全性( 减少 SQL 注射攻击的可能性);②PreparedStatement 中的 SQL 语句是可以带参数的,避免了用字符串连接拼接 SQL 语句的麻烦和不安全;③ 当批量处理 SQL 或频繁执行相同的查询时, PreparedStatement 有明显的性能上的优势, 由于数据库可以将编译优化后的SQL语句缓存起来, 下次执行相同结构的语句时就会很快( 不用再次编译和生成执行计划)。
补充:为了提供对存储过程的调用, JDBC API 中还提供了 CallableStatement 接口。 存储过程( Stored Procedure)是数据库中一组为了完成特定功能的 SQL 语句的集合, 经编译后存储在数据库中, 用户通过指定存储过程的名字并给出参数 ( 如果该存储过程带有参数) 来执行它。虽然调用存储过程会在网络开销、安全性、性能上获得很多好处, 但是存在如果底层数据库发生迁移时就会有很多麻烦, 因为每种数据库的存储过程在书写上存在不少的差别。
使用 JDBC 操作数据库时,如何提升读取数据的性能?如何提升更新数据的性能?
要提升读取数据的性能,可以指定通过结果集( ResultSet)对象的 setFetchSize() 方法指定每次抓取的记录数( 典型的空间换时间策略); 要提升更新数据的性能可以使用 PreparedStatement 语句构建批处理, 将若干 SQL 语句置于一个批处理中执行。
在进行数据库编程时,连接池有什么作用?
由于创建连接和释放连接都有很大的开销( 尤其是数据库服务器不在本地时, 每次建立连接都需要进行 TCP 的三次握手,释放连接需要进行 TCP 四次握手,造成的开销是不可忽视的), 为了提升系统访问数据库的性能, 可以事先创建若干连接置于连接池中, 需要时直接从连接池获取, 使用结束时归还连接池而不必关闭连接, 从而避免频繁创建和释放连接所造成的开销, 这是典型的用空间换取时间的策略( 浪费了空间存储连接, 但节省了创建和释放连接的时间)。池化技术在Java 开发中是很常见的,在使用线程时创建线程池的道理与此相同。基于 Java 的开源数据库连接池主要有: C3P 、Proxool、DBCP、BoneCP、Druid 等。 补充: 在计算机系统中时间和空间是不可调和的矛盾, 理解这一点对设计满足性能要求的算法是至关重要的。大型网站性能优化的一个关键就是使用缓存, 而缓存跟上面讲的连接池道理非常类似, 也是使用空间换时间的策略。可以将热点数据置于缓存中, 当用户查询这些数据时可以直接从缓存中得到, 这无论如何也快过去数据库中查询。当然, 缓存的置换策略等也会对系统性能产生重要影响, 对于这个问题的讨论已经超出了这里要阐述的范围。
事务的 ACID 是指什么?
原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作 的失败都会导致整个事务的失败; 一致性(Consistent):事务结束后系统状态是一致的; 隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态; 持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难性的失败。通过日志和同步备份可以在故障发生后重建数据。
补充: 关于事务, 在面试中被问到的概率是很高的, 可以问的问题也是很多的。首先需要知道的是, 只有存在并发数据访问时才需要事务。当多个事务访问同一数据时, 可能会存在 5 类问题, 包括 3 类数据读取问题( 脏读、不可重复读和幻读) 和 2 类数据更新问题( 第 1 类丢失更新和第 2 类丢失更新)。 脏读( Dirty Read):A 事务读取 B 事务尚未提交的数据并在此基础上操作,而 B 事务执行回滚, 那么 A 读取到的数据就是脏数据。
JDBC 中如何进行事务处理?
Connection 提供了事务处理的方法, 通过调用 setAutoCommit(false)可以设置手动提交事务;当事务完成后用 commit()显式提交事务;如果在事务处理过程中发生异常则通过 rollback()进行事务回滚。除此之外, 从 JDBC 3.0 中还引入了Savepoint( 保存点)的概念,允许通过代码设置保存点并让事务回滚到指定的保存点。
JDBC 能否处理 Blob 和Clob?
Blob 是指二进制大对象( Binary Large Object), 而 Clob 是指大字符对象 ( Character Large Objec),因此其中 Blob 是为存储大的二进制数据而设计的,而 Clob 是为存储大的文本数据而设计的。JDBC 的 PreparedStatement 和 ResultSet 都提供了相应的方法来支持 Blob 和 Clob 操作。下面的代码展示了如何使用 JDBC 操作 LOB: 下面以 MySQL 数据库为例, 创建一个张有三个字段的用户表, 包括编号( id)、姓名( name) 和照片( photo), 建表语句如下:
create table tb_user
(
id int primary key auto_increment,
name varchar(20) unique not null,
photo longblob
);
下面的 Java 代码向数据库中插入一条记录:
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
class JdbcLobTest {
public static void main(String[] args){
Connection con = null;
try {
// 1. 加载驱动(Java6 以上版本可以省略)
Class.forName("com.mysql.jdbc.Driver");
// 2. 建立连接
con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root","123456");
// 3. 创建语句对象
PreparedStatement ps = con.prepareStatement("insert into tb_user values (default, ?, ?)");
ps.setString(1, "骆昊"); // 将 SQL 语句中第一个占位符换成字符串
try (InputStream in = new FileInputStream("test.jpg")) {// Java 7 的 TWR
ps.setBinaryStream(2, in); // 将 SQL 语句中第二个占位符换成二进制流
// 4. 发出 SQL 语句获得受影响行数 System.out.println(ps.executeUpdate() == 1 ? "插入成功" : "插入失败");
} catch(IOException e){
System.out.println("读取照片失败!");
}
} catch (ClassNotFoundException | SQLException e) {
// Java7 的多异常捕获 e.printStackTrace();
} finally {
// 释放外部资源的代码都应当放在 finally 中保证其能够得到执行
try {
if(con != null && !con.isClosed()){
con.close(); // 5. 释放数据库连接
con = null; // 指示垃圾回收器可以回收该对象
}
} catch (SQLException e){
e.printStackTrace();
}
}
}
}
简述正则表达式及其用途。
在编写处理字符串的程序时, 经常会有查找符合某些复杂规则的字符串的需要。正则表达式就是用于描述这些规则的工具。换句话说, 正则表达式就是记录文本规则的代码。 说明: 计算机诞生初期处理的信息几乎都是数值, 但是时过境迁, 今天我们使用计算机处理的信息更多的时候不是数值而是字符串, 正则表达式就是在进行字符串匹配和处理的时候最为强大的工具, 绝大多数语言都提供了对正则表达式的支持。
Java 中是如何支持正则表达式操作的?
Java 中的 String 类提供了支持正则表达式操作的方法, 包括: matches()、replaceAll()、replaceFirst()、split()。
此外,Java 中可以用 Pattern 类表示正则表达式对象, 它提供了丰富的 API 进行各种正则表达式操作, 请参考下面面试题的代码。
面试题: - 如果要从字符串中截取第一个英文左括号之前的字符串。
例如: 北京市(朝阳区)(西城区)(海淀区), 截取结果为: 北京市, 那么正则表达式怎么写?
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class RegExpTest {
public static void main(String[] args) {
String str = "北京市(朝阳区)(西城区)(海淀区)";
Pattern p = Pattern.compile(".*?(?=\()");
Matcher m = p.matcher(str);
if(m.find()) {
System.out.println(m.group());
}
}
}
说明: 上面的正则表达式中使用了懒惰匹配和前瞻, 如果不清楚这些内容, 推荐读一下网上很有名的《正则表达式 30 分钟入门教程》。
你能写出一个正则表达式来判断一个字符串是否是一个数字吗?
一个数字字符串, 只能包含数字, 如 0 到 9 以及 +、- 开头, 通过这个信息, 你可以下一个如下的正则表达式来判断给定的字符串是不是数字。 首先要 import java.util.regex.Pattern 和 java.util.regex.Matcher
public boolean isNumeric(String str){
Pattern pattern = Pattern.compile("[0-9]*"); Matcher isNum = pattern.matcher(str);
if( !isNum.matches() ){
return false;
}
return true;
}
在多线程环境下,SimpleDateFormat 是线程安全的吗?
答案:SimpleDateFormat不是线程安全的。其内部维护了一个共享的Calendar对象,多线程并发调用时可能导致数据竞争,出现以下问题:
- 日期解析错误:例如,一个线程修改了
Calendar的状态,另一个线程使用了修改后的状态。 - 抛出异常:如
ArrayIndexOutOfBoundsException或NullPointerException。
解决方案:
使用
ThreadLocal:为每个线程创建独立的SimpleDateFormat实例:private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial( () -> new SimpleDateFormat("yyyy-MM-dd") ); public String formatDate(Date date) { return formatter.get().format(date); }使用线程安全的替代方案:
Java 8+:使用
java.time包中的
DateTimeFormatter(线程安全):
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); public String formatDate(LocalDate date) { return date.format(FORMATTER); }Joda-Time 库:
org.joda.time.format.DateTimeFormat(线程安全)。
Java 中如何格式化一个日期?如格式化为 ddMMyyyy 的形式?
答案:Java 中格式化日期的方法:
使用
SimpleDateFormat(适用于 Java 7 及以下):Date date = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("ddMMyyyy"); String formattedDate = sdf.format(date); // 输出:20062025使用 Java 8 + 的
java.timeAPI:LocalDate date = LocalDate.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("ddMMyyyy"); String formattedDate = date.format(formatter); // 输出:20062025使用 Joda-Time 库:
DateTime date = new DateTime(); DateTimeFormatter formatter = DateTimeFormat.forPattern("ddMMyyyy"); String formattedDate = formatter.print(date);
注意:SimpleDateFormat非线程安全,多线程环境下需使用ThreadLocal或DateTimeFormatter。
Java 中,怎么在格式化的日期中显示时区?
答案:在 Java 中格式化日期并显示时区的方法:
使用
SimpleDateFormat:Date date = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); String formattedDate = sdf.format(date); // 输出:2025-06-20 12:00:00 CST使用 Java 8 + 的
java.timeAPI:ZonedDateTime zdt = ZonedDateTime.now(ZoneId.of("Asia/Shanghai")); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"); String formattedDate = zdt.format(formatter); // 输出:2025-06-20 12:00:00 China Standard Time时区缩写与全称:
- 使用
z:显示时区缩写(如 CST)。 - 使用
zzzz:显示时区全称(如 China Standard Time)。 - 使用
XXX:显示 ISO 8601 时区偏移(如 + 08:00)。
- 使用
Java 中 java.util.Date 与 java.sql.Date 有什么区别?
答案:
| 特性 | java.util.Date | java.sql.Date |
|---|---|---|
| 继承关系 | 继承自java.lang.Object | 继承自java.util.Date |
| 时间精度 | 包含日期和时间(毫秒级) | 仅包含日期部分(时分秒被截断为 0) |
| 主要用途 | 通用日期时间表示 | 用于 JDBC 操作数据库日期字段 |
| 构造方法 | new Date()(当前时间) | new java.sql.Date(long time)(毫秒值) |
| 格式化 | 使用SimpleDateFormat或DateTimeFormatter | 通常转换为java.util.Date后格式化 |
| 示例值 | Fri Jun 20 12:30:45 CST 2025 | 2025-06-20 |
转换方法:
// java.util.Date -> java.sql.Date
java.util.Date utilDate = new java.util.Date();
java.sql.Date sqlDate = new java.sql.Date(utilDate.getTime());
// java.sql.Date -> java.util.Date
java.util.Date utilDate2 = sqlDate; // 子类可直接赋值给父类
Java 中,如何计算两个日期之间的差距?
答案:计算两个日期之间差距的方法:
Java 8 + 使用
java.timeAPI:LocalDate startDate = LocalDate.of(2025, 1, 1); LocalDate endDate = LocalDate.of(2025, 6, 20); // 计算天数差 long days = ChronoUnit.DAYS.between(startDate, endDate); // 169天 // 计算年、月、日差 Period period = Period.between(startDate, endDate); int years = period.getYears(); // 0 int months = period.getMonths(); // 5 int days = period.getDays(); // 19Java 7 及以下使用
java.util.Calendar:Calendar start = Calendar.getInstance(); start.set(2025, Calendar.JANUARY, 1); Calendar end = Calendar.getInstance(); end.set(2025, Calendar.JUNE, 20); long diffMillis = end.getTimeInMillis() - start.getTimeInMillis(); long days = diffMillis / (24 * 60 * 60 * 1000); // 169天使用 Joda-Time 库:
DateTime start = new DateTime(2025, 1, 1, 0, 0); DateTime end = new DateTime(2025, 6, 20, 0, 0); Days days = Days.daysBetween(start, end); // 169天
Java 中,如何将字符串 YYYYMMDD 转换为日期?
答案:将字符串YYYYMMDD转换为日期的方法:
Java 8 + 使用
java.timeAPI:String dateStr = "20250620"; DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd"); LocalDate date = LocalDate.parse(dateStr, formatter); // 2025-06-20使用
SimpleDateFormat:String dateStr = "20250620"; SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); Date date = sdf.parse(dateStr); // Fri Jun 20 00:00:00 CST 2025转换为
java.sql.Date:java.util.Date utilDate = sdf.parse(dateStr); java.sql.Date sqlDate = new java.sql.Date(utilDate.getTime()); // 2025-06-20
注意:SimpleDateFormat.parse()可能抛出ParseException,需进行异常处理。
多线程断点续传原理
断点续传是一种常见的下载技术,它允许用户在下载过程中断开连接后继续从上次断开的位置开始下载。
原理
- 分割文件:将文件分割成多个部分,每个部分可以由一个线程负责下载。
- 记录进度:记录每个线程的下载进度,通常使用文件偏移量来表示。
- 合并文件:下载完成后,将各个部分合并成完整的文件。
优势
- 稳定性:即使网络不稳定,也可以从断点处继续下载。
- 速度提升:利用多线程技术,可以同时下载文件的不同部分,提高下载速度。
断点续传的实现
步骤
- 文件分割:根据文件大小和线程数决定每个线程下载的文件范围。
- 记录进度:为每个线程创建一个进度记录文件,记录已经下载的部分。
- 下载任务:每个线程负责下载分配给它的文件部分。
- 合并文件:下载完成后,将各个部分合并成完整的文件。
2 Java 集合
容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。
解释Java中的集合框架和它们的实现。
Java集合框架(Java Collections Framework)是Java标准库的一部分,提供了一套标准的接口和实现,用于存储和操作数据集合。集合框架的基础是java.util包,它定义了各种数据结构和算法,使得程序员可以方便地实现数据的存储、检索、搜索和遍历等操作。
集合框架主要分为两大类:不同步(非线程安全)的集合和线程安全的集合。
- 不同步(非线程安全)的集合:
- List:有序集合,允许重复的元素,提供访问位置的API。常见的实现有
ArrayList(基于动态数组)和LinkedList(基于链表)。 - Set:不允许重复元素的集合。
HashSet是基于哈希表的实现,提供快速的查找和插入操作;TreeSet是基于红黑树的实现,可以保持元素的有序性。 - Queue:队列接口,用于按特定顺序处理元素。
LinkedList可以作为队列的实现,PriorityQueue是一个优先队列的实现。
- List:有序集合,允许重复的元素,提供访问位置的API。常见的实现有
- 线程安全的集合:
Vector是ArrayList的线程安全版本。ConcurrentHashMap是HashMap的线程安全版本,提供了更好的并发性能。CopyOnWriteArrayList和CopyOnWriteArraySet是在写操作时复制数据的集合,适用于读多写少的场景。
集合框架的实现考虑了性能和内存使用效率。例如,ArrayList在随机访问时性能较好,而LinkedList在插入和删除操作频繁时性能较好。HashSet和HashMap在哈希冲突较少时性能较好,而TreeSet和TreeMap在需要元素有序时是更好的选择。
集合框架还提供了一些实用工具类,如Collections和Arrays,它们提供了静态方法来操作集合和数组,如排序、搜索、同步包装等。
深入分析 Java 中的线程安全集合类,如 ConcurrentHashMap 的实现细节
ConcurrentHashMap 是一个线程安全的哈希映射,它的实现细节如下:
- 分段锁:早期版本(Java 5/6)使用了分段锁来降低锁的粒度,每个段包含一部分桶,每个段有一个锁。
- CAS + volatile:Java 7/8 及以后版本改用了 CAS 加上 volatile 的方式来实现线程安全,取消了分段锁。
- 链表转红黑树:当链表长度超过阈值时,链表会被转换成红黑树,以提高查找效率。
总结
特性描述分段锁Java 5/6 版本使用分段锁来实现线程安全。CAS + volatileJava 7/8 版本使用 CAS 加 volatile 来实现线程安全。链表转红黑树当链表长度过长时,自动转换为红黑树以提高性能。
2.1 Collection
集合有哪些类?
- Set
- TreeSet 基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN)。
- HashSet 基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
- LinkedHashSet 具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。
- List
- ArrayList 基于动态数组实现,支持随机访问。
- Vector 和 ArrayList 类似,但它是线程安全的。
- LinkedList 基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。
- Queue
- LinkedList 可以用它来实现双向队列。
- PriorityQueue 基于堆结构实现,可以用它来实现优先队列。
并发集合了解哪些?
Java 并发集合是 Java 并发包 java.util.concurrent 中提供的线程安全的集合实现。这些集合能够在多线程环境下安全地使用,而不需要额外的同步措施。Java 提供的主要并发集合包括:
ConcurrentHashMap:线程安全的哈希映射。ConcurrentLinkedQueue:基于链接节点的无界线程安全队列。ConcurrentLinkedDeque:基于链接节点的无界线程安全双端队列。CopyOnWriteArrayList:线程安全的数组列表,通过写时复制机制实现线程安全。CopyOnWriteArraySet:线程安全的集合,内部使用CopyOnWriteArrayList实现。ConcurrentSkipListSet:线程安全的跳表实现的集合。ConcurrentSkipListMap:线程安全的跳表实现的映射。BlockingQueue:阻塞队列接口,提供阻塞的take和put方法。ArrayBlockingQueue:基于数组的阻塞队列实现。LinkedBlockingQueue:基于链表的阻塞队列实现。PriorityBlockingQueue:基于优先级堆的阻塞队列实现。DelayQueue:基于优先级堆的阻塞队列实现,用于处理延迟任务。
并发集合特点
表格 还在加载中,请等待加载完成后再尝试复制
列举java的集合以及集合之间的继承关系
Java 的集合框架主要由 Collection 和 Map 接口组成。以下是 Java 集合框架的基本接口和它们的关系:
Collection:所有单值集合的顶级接口。Set:不允许重复元素的集合。List:有序的、允许重复元素的集合。Queue:队列集合。
Map:键值对的集合。
集合类以及集合框架
Java 集合框架提供了一系列接口和实现,用于组织和操作数据。以下是 Java 集合框架的主要组成部分:
Collection:所有单值集合的顶级接口。List:有序集合,允许重复元素。Set:不重复元素的集合。Queue:队列集合,用于处理先进先出(FIFO)或后进先出(LIFO)的顺序。Map:键值对集合。Iterator:用于遍历集合的迭代器。Enumeration:枚举接口,用于迭代集合元素。Arrays:提供静态方法来操作数组。Collections:提供静态方法来操作集合。
集合框架组件
表格 还在加载中,请等待加载完成后再尝试复制
List, Set, Map 的区别
List, Set, 和 Map 是 Java 集合框架中最基本的三种数据结构,它们各自有不同的特性和用途。
List:有序的集合,允许重复元素,可以通过索引访问元素。Set:不重复的集合,不允许重复元素,不保证任何特定的顺序。Map:键值对的集合,键是唯一的,值可以重复。
主要区别
表格 还在加载中,请等待加载完成后再尝试复制
List 和 Map 的实现方式以及存储方式
List
- 实现方式:
ArrayList:基于数组实现,提供了随机访问的能力。LinkedList:基于双向链表实现,适合频繁插入和删除操作。
- 存储方式:
ArrayList:使用动态数组存储数据,数组大小会随着元素增加而自动扩展。LinkedList:使用节点链表存储数据,每个节点包含元素和前后节点的引用。
Map
- 实现方式:
HashMap:基于哈希表实现,提供了高效的键值对存储和检索。TreeMap:基于红黑树实现,提供了键值对的排序能力。
- 存储方式:
HashMap:使用哈希表存储数据,每个键值对对应一个哈希表条目。TreeMap:使用红黑树存储数据,每个键值对对应树中的一个节点。
ArrayList的底层?
ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现。除该类未实现同步外,其余跟Vector大致相同。每个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。前面已经提过,Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。

ArrayList自动扩容?
每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过ensureCapacity(int minCapacity)方法来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。
数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。

ArrayList的Fail-Fast机制?
ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
List、Set、Map 和 Queue 之间的区别(答案)
List 是一个有序集合, 允许元素重复。它的某些实现可以提供基于下标值的常量访问时间, 但是这不是 List 接口保证的。Set 是一个无序集合。
poll() 方法和 remove() 方法的区别?
poll() 和 remove() 都是从队列中取出一个元素, 但是 poll() 在获取元素失败的时候会返回空, 但是 remove() 失败的时候会抛出异常。
Java 中 LinkedHashMap 和 PriorityQueue 的区别是什么?
PriorityQueue 保证最高或者最低优先级的的元素总是在队列头部, 但是 LinkedHashMap 维持的顺序是元素插入的顺序。当遍历一个 PriorityQueue 时,没有任何顺序保证,但是 LinkedHashMap 课保证遍历顺序是元素插入的顺序。
ArrayList 与 LinkedList 的不区别?
最明显的区别是 ArrrayList 底层的数据结构是数组, 支持随机访问, 而 LinkedList 的底层数据结构书链表, 不支持随机访问 。使用下标访问一个元素, ArrayList 的时间复杂度是 O(1),而LinkedList 是 O(n)。更多细节的讨论参见答案。
用哪两种方式来实现集合的排序?
你可以使用有序集合,如 TreeSet 或 TreeMap,你也可以使用有顺序的的集合,如 list, 然后通过 Collections.sort() 来排序。
Java 中怎么打印数组?
你可以使用 Arrays.toString() 和 Arrays.deepToString() 方法来打印数组。 由于数组没有实现 toString() 方法,所以如果将数组传递给 System.out.println() 方法, 将无法打印出数组的内容, 但是 Arrays.toString() 可以打印每个元素。
Java 中的 LinkedList 是单向链表还是双向链表?
是双向链表,你可以检查 JDK 的源码。在 Eclipse,你可以使用快捷键 Ctrl + T,直接在编辑器中打开该类。
Java 中的 TreeMap 是采用什么树实现的?(答案)
Java 中的 TreeMap 是使用红黑树实现的。
Hashtable 与 HashMap 有什么不同之处?
这两个类有许多不同的地方, 下面列出了一部分: a) Hashtable 是 JDK 1 遗留下来的类, 而 HashMap 是后来增加的。 b) Hashtable 是同步的, 比较慢, 但 HashMap 没有同步策略, 所以会更快。c) Hashtable 不允许有个空的 key, 但是 HashMap 允许出现一个 null key。更多的不同之处参见答案。
Java 中的 HashSet,内部是如何工作的?
HashSet 的内部采用 HashMap 来实现。由于 Map 需要 key 和 value,所以所有 key 的都有一个默认 value。类似于HashMap, HashSet 不允许重复的key,只允许有一个 null key,意思就是 HashSet 中只允许存储一个 null 对象。
写一段代码在遍历 ArrayList 时移除一个元素?
该问题的关键在于面试者使用的是 ArrayList 的 remove() 还是 Iterator 的remove()方法。这有一段示例代码, 是使用正确的方式来实现在遍历的过程中移除元素, 而不会出现 ConcurrentModificationException 异常的示例代码。
我们能自己写一个容器类,然后使用 for-each 循环码?
可以, 你可以写一个自己的容器类。如果你想使用 Java 中增强的循环来遍历, 你只需要实现 Iterable 接口。如果你实现 Collection 接口,默认就具有该属性。
ArrayList 和 HashMap 的默认大小是多数?
在 Java 7 中, ArrayList 的默认大小是 10 个元素, HashMap 的默认大小是16 个元素( 必须是 2 的幂)。这就是 Java 7 中 ArrayList 和 HashMap 类的代码片段: // from ArrayList.java JDK 1.7 private static final int DEFAULT_CAPACITY = 10; //from HashMap.java JDK 7 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
Java 中,Comparator 与 Comparable 有什么不同?
Comparable 接口用于定义对象的自然顺序,而 comparator 通常用于定义用户定制的顺序。Comparable 总是只有一个,但是可以有多个 comparator 来定义对象的顺序。
List、Set、Map 是否继承自Collection 接口?
List、Set 是,Map 不是。Map 是键值对映射容器,与 List 和 Set 有明显的区别, 而 Set 存储的零散的元素且不允许有重复元素( 数学中的集合也是如此), List 是线性结构的容器, 适用于按数值索引访问元素的情形。
阐述 ArrayList、Vector、LinkedList 的存储性能和特性。
ArrayList 和 Vector 都是使用数组方式存储数据, 此数组元素数大于实际存储的数据以便增加和插入元素, 它们都允许直接按序号索引元素, 但是插入元素要涉及数组元素移动等内存操作, 所以索引数据快而插入数据慢, Vector 中的方法由于添加了 synchronized 修饰, 因此 Vector 是线程安全的容器, 但性能上较 ArrayList 差, 因此已经是 Java 中的遗留容器 。 LinkedList 使用双向链表实现存储( 将内存中零散的内存单元通过附加的引用关联起来, 形成一个可以按序号索引的线性结构, 这种链式存储方式与数组的连续存储方式相比, 内存的利用率更 高), 按序号索引数据需要进行前向或后向遍历, 但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。 Vector 属于遗留容器( Java 早期的版本中提供的容器, 除此之外, Hashtable、 Dictionary、 BitSet、 Stack、 Properties 都是遗留容器), 已经不推荐使用, 但是由于 ArrayList 和 LinkedListed 都是非线程安全的, 如果遇到多个线程操作同一个容器的场景, 则可以通过工具类 Collections 中的 synchronizedList 方法将其转换成线程安全的容器后再使用(这是对装潢模式的应用, 将已有对象传入另一个类的构造器中创建新的对象来增强实现)。
补充:遗留容器中的 Properties 类和 Stack 类在设计上有严重的问题,Properties是一个键和值都是字符串的特殊的键值对映射, 在设计上应该是关联一个 Hashtable 并将其两个泛型参数设置为 String 类型, 但是 Java API 中的 Properties 直接继承了 Hashtable, 这很明显是对继承的滥用。这里复用代码的
方式应该是 Has-A 关系而不是 Is-A 关系,另一方面容器都属于工具类,继承工具类本身 就是一个错误的做法, 使用工具类最好的方式是 Has-A 关系( 关联) 或Use-A 关系 ( 依赖)。同理, Stack 类继承 Vector 也是不正确的。Sun 公司的工程师们也会犯这种 低级错误, 让人唏嘘不已。
Collection 和Collections 的区别?
Collection 是一个接口, 它是 Set、List 等容器的父接口; Collections 是个一个工具类,提供了一系列的静态方法来辅助容器操作, 这些方法包括对容器的搜索、排序、线程安全化等等。
List、Map、Set 三个接口存取元素时,各有什么特点?
List 以特定索引来存取元素,可以有重复元素。Set 不能存放重复元素( 用对象的 equals() 方法来区分元素是否重复) 。 Map 保存键值对( key-value pair) 映射,映射关系可以是一对一或多对一。 Set 和 Map 容器都有基于哈希存储和排序树的两种实现版本, 基于哈希存储的版本理论存取时间复杂度为 O(1), 而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键( key)构 成排序树从而达到排序和去重的效果。
TreeMap 和TreeSet 在排序时如何比较元素?
Collections 工具类中的sort()方法如何比较元素?
TreeSet 要求存放的对象所属的类必须实现 Comparable 接口, 该接口提供了比较元素的 compareTo()方法, 当插入元素时会回调该方法比较元素的大小。 TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进行排序。Collections 工具类的sort 方法有两种重载的形式,第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较; 第二种不强制性的要求容器中的元素必须可比较, 但是要求传入第二个参数, 参数是Comparator 接口的子类型( 需要重写 compare 方法实现元素的比较), 相当于一个临时定义的排序规则, 其实就是通过接口注入比较元素大小的算法, 也是对回调模式的应用( Java 中对函数式编程的支持)。
说出几点 Java 中使用 Collections 的最佳实践
答案:Java 集合使用的最佳实践:
选择合适的集合类型:
- 有序且允许重复:
ArrayList、LinkedList。 - 唯一元素:
HashSet、TreeSet(有序)。 - 键值对:
HashMap、ConcurrentHashMap(线程安全)。
- 有序且允许重复:
优先使用并发集合:在多线程环境中,使用
ConcurrentHashMap、CopyOnWriteArrayList等,避免手动同步。使用接口类型声明:通过接口操作集合,提高代码灵活性:
List<String> list = new ArrayList<>(); // 而非 ArrayList<String> list初始化时指定容量:避免集合动态扩容带来的性能损耗:
List<String> list = new ArrayList<>(100); // 初始容量100使用泛型:确保类型安全,避免运行时
ClassCastException:List<String> list = new ArrayList<>(); // 明确存储String类型避免使用原始类型:
List list = new ArrayList(); // 不推荐 List<String> list = new ArrayList<>(); // 推荐使用增强 for 循环或 Stream API:简化遍历操作:
for (String item : list) { /* 处理元素 */ } list.stream().filter(s -> s.length() > 5).forEach(System.out::println);不可变集合:使用
Collections.unmodifiableList()或 Java 9 + 的List.of()创建不可变集合,防止意外修改:List<String> immutableList = Collections.unmodifiableList(originalList);
2.2 Map
Map有哪些类?
TreeMap基于红黑树实现。HashMap1.7基于哈希表实现,1.8基于数组+链表+红黑树。HashTable和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高(1.7 ConcurrentHashMap 引入了分段锁, 1.8 引入了红黑树)。LinkedHashMap使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。
JDK7 HashMap如何实现?
哈希表有两种实现方式,一种开放地址方式(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists)。Java7 *HashMap*采用的是冲突链表方式。

从上图容易看出,如果选择合适的哈希函数,put()和get()方法可以在常数时间内完成。但在对HashMap进行迭代时,需要遍历整个table以及后面跟的冲突链表。因此对于迭代比较频繁的场景,不宜将HashMap的初始大小设的过大。
有两个参数可以影响HashMap的性能: 初始容量(inital capacity)和负载系数(load factor)。初始容量指定了初始table的大小,负载系数用来指定自动扩容的临界值。当entry的数量超过capacity*load_factor时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。
JDK8 HashMap如何实现?
根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。
为了降低这部分的开销,在 Java8 中,当链表中的元素达到了 8 个时,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

1. HashMap 是什么,它的底层数据结构是什么?
JDK 1.8 之前:数组 + 链表(链表解决哈希冲突)。 JDK 1.8 及之后:数组 + 链表/红黑树(当链表长度 ≥8 且数组长度 ≥64 时,链表转为红黑树,提高查询效率)。
由数组、链表和红黑树组成。数组(哈希桶数组)是核心,每个元素称为 “桶”(bucket)。当多个键映射到同一个桶位置(哈希冲突)时,使用链表存储冲突的键值对;当链表长度超过一定阈值(默认 8)且数组长度大于等于 64 时,链表会转换为红黑树,以提高查询效率。
2. HashMap 的 put () 方法工作原理是什么?
计算哈希码:调用键对象的 hashCode () 方法得到哈希码。为了减少哈希冲突,还会进行扰动处理 ,如 JDK 1.8 中
hash (key) = (key == null) ? 0 : key.hashCode () ^ (hashCode >>> 16)。计算数组索引:通过哈希码计算在数组中的索引位置,使用公式
int index = (hash ^ (hash >>> 16)) & (table.length - 1);,使哈希码更均匀地分布在数组中。判断数组位置状态:
- 若该索引位置为空,直接创建新的 Node 节点存储键值对。
- 若不为空,分情况处理:
- 若该位置首个元素的键与要插入的键相等,直接覆盖其值。
- 若该位置是红黑树结构,执行红黑树的添加逻辑。
- 若该位置是链表结构,遍历链表:找到相等的键则覆盖值;若遍历完链表都没找到,则在链表尾部插入新节点。插入新节点后,若链表长度大于等于 8,且数组长度大于等于 64,将链表转换为红黑树。
检查是否需要扩容:添加数据后,判断当前元素数量(size)是否大于阈值(threshold = 数组容量 * 负载因子,默认负载因子 0.75),若大于则调用 resize () 方法进行扩容。
3. HashMap 的 get () 方法工作原理是什么?
- 计算哈希码和索引:与 put () 方法类似,先计算键的哈希码,再通过相同的索引计算方式确定在数组中的位置。
- 查找对应元素:
- 若该索引位置为空,直接返回 null,表示键不存在。
- 若该位置有元素,分情况查找:
- 若该位置是红黑树结构,按照红黑树的查找方式查找匹配的键。
- 若该位置是链表结构,遍历链表,通过 equals () 方法比较键是否相等。找到匹配的键则返回对应的值,遍历完链表都没找到则返回 null。
4. 为什么 HashMap 线程不安全?
- 多线程扩容问题(JDK 1.7):多线程同时进行扩容操作时,由于扩容过程涉及元素重新计算哈希和迁移,可能导致链表形成环形结构,进而在 get () 操作时出现死循环。JDK 1.8 对此进行了优化,但仍不建议在多线程环境中直接使用 HashMap。
- 数据覆盖问题:多线程同时执行 put () 操作且发生哈希冲突时,可能会出现后插入的键值对覆盖先插入的情况。
- 可见性问题:HashMap 未使用同步机制,一个线程对其进行修改后,其他线程可能无法及时看到修改后的结果。
5. HashMap 如何解决哈希冲突?
拉链法(链地址法):主要采用链地址法(拉链法):当不同键计算出相同的哈希值(哈希冲突)时,将这些冲突的键值对存储在同一个桶位置的链表或红黑树中。随着冲突增多,链表长度增加,查询效率会降低;当链表长度超过 8 且数组长度大于等于 64 时,链表转换为红黑树,利用红黑树的特性提高查询效率。
开放寻址法:HashMap 未使用,但其他类(如 ThreadLocal)可能采用
6. 为什么链表长度超过 8 会转为红黑树?
- 设计依据:根据泊松分布,哈希冲突时链表长度达到 8 的概率极低(约 1 / 千万次) 。当链表过长时,查询时间复杂度为 O (n),效率较低;而红黑树的查询时间复杂度为 O (log n),转换为红黑树可平衡查询效率与树化成本。
- 退化条件:当红黑树节点数小于等于 6 时,会退化为链表,因为此时链表的操作(插入、删除)效率可能高于红黑树。
7. HashMap 的初始容量、负载因子与扩容机制是怎样的?
- 默认初始容量:16。可以在创建 HashMap 时指定初始容量,若不指定则使用默认值。合适的初始容量可减少扩容次数,提高性能。
- 负载因子(Load Factor):默认值为 0.75。它是一个权衡空间和时间效率的参数,负载因子较小,哈希表的平均填充程度低,空间开销大但查找成本低;负载因子较大,空间利用率高但哈希冲突的可能性增加,查找成本上升。
- 扩容机制:当 HashMap 中的元素数量大于(数组容量 * 负载因子)时,会进行扩容。扩容时创建一个新的数组,容量为原来的 2 倍,然后将原数组中的所有元素重新计算哈希并分配到新的数组位置上 。重新计算键的哈希并分配到新位置,JDK 1.8 在扩容时,链表节点保持原顺序,避免了 JDK 1.7 中可能出现的环形链表问题。
8. HashMap 允许键 / 值为 null,而 ConcurrentHashMap 不允许,为什么?
- HashMap 设计目的:主要用于单线程环境,允许 null 键和 null 值可以简化代码逻辑,比如可以用 null 值表示 “无值” 的情况。
- ConcurrentHashMap 设计目的:用于多线程环境,若允许 null 键或 null 值,在多线程操作时会产生歧义,无法区分 “键不存在” 和 “键存在但值为 null” 的情况,增加了线程安全问题的复杂性。
9. HashMap 与 Hashtable 的区别有哪些?
| 特性 | HashMap | Hashtable |
|---|---|---|
| 线程安全 | 否 | 是(方法用 synchronized 修饰) |
| null 键 / 值 | 允许 | 禁止 |
| 初始容量 | 16 | 11 |
| 扩容机制 | 2 倍 | 2 倍 +1 |
| 哈希计算 | 扰动函数优化哈希分布 | 直接使用 key.hashCode () |
| 遍历顺序 | 不保证遍历顺序 | 按照插入顺序遍历 |
| 性能 | 单线程环境中通常更快 | 多线程环境中因线程同步开销,性能可能较差 |
10. 如何设计一个高效的键类型(如自定义对象作为 Key)?
- 重写 hashCode () 和 equals () 方法:确保哈希分布均匀且相等对象返回相同哈希。例如:
import java.util.Objects;
public class MyKey {
private int id;
private String name;
public MyKey(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public int hashCode() {
return Objects.hash(id, name);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
MyKey other = (MyKey) obj;
return id == other.id && Objects.equals(name, other.name);
}
}
- 使用不可变对象作为键:不可变对象在创建后状态不会改变,其哈希码在整个生命周期内保持不变,能安全地用作 HashMap 的键,避免因键值变化导致哈希冲突和查找错误。
11. 如何解决 HashMap 的多线程问题?
- 使用 Collections.synchronizedMap () 方法:通过该方法创建一个线程安全的 Map 包装器,对 HashMap 进行包装。例如:
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
Map<String, Integer> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
这种方式虽然能保证线程安全,但由于使用了同步锁,性能较低,所有操作都需要竞争锁资源。
- 使用 ConcurrentHashMap:这是 Java 提供的线程安全的哈希表实现。JDK 1.7 中采用分段锁机制,将哈希表分为多个段(Segment),不同段可同时进行操作;JDK 1.8 及之后使用 CAS(Compare and Swap)操作结合 synchronized 关键字来保证线程安全,性能更优,适用于高并发场景。
12. HashMap 的遍历方式有哪些?
- 使用 entrySet () 遍历:效率最高,可直接获取键值对。示例代码如下:
import java.util.HashMap;
import java.util.Map;
HashMap<String, Integer> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println("Key: " + key + ", Value: " + value);
}
- 使用 keySet () 遍历:先获取键的集合,再通过键获取对应的值,可能触发多次哈希计算,效率相对较低。示例代码:
for (String key : map.keySet()) {
Integer value = map.get(key);
System.out.println("Key: " + key + ", Value: " + value);
}
- 使用 values () 遍历:只能获取值,无法获取键。示例代码:
for (Integer value : map.values()) {
System.out.println("Value: " + value);
}
- 使用 Lambda 表达式(Java 8+):通过 forEach 方法结合 Lambda 表达式遍历,代码简洁。示例代码:
map.forEach((k, v) -> System.out.println(k + ": " + v));
HashMap 的 put() 和 get() 方法工作原理
HashSet是如何实现的?
HashSet是对HashMap的简单包装,对HashSet的函数调用都会转换成合适的HashMap方法
//HashSet是对HashMap的简单包装
public class HashSet<E>
{
......
private transient HashMap<E,Object> map;//HashSet里面有一个HashMap
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
......
public boolean add(E e) {//简单的方法转换
return map.put(e, PRESENT)==null;
}
......
}
什么是WeakHashMap?
我们都知道Java中内存是通过GC自动管理的,GC会在程序运行过程中自动判断哪些对象是可以被回收的,并在合适的时机进行内存释放。GC判断某个对象是否可被回收的依据是,是否有有效的引用指向该对象。如果没有有效引用指向该对象(基本意味着不存在访问该对象的方式),那么该对象就是可回收的。这里的有效引用 并不包括弱引用。也就是说,虽然弱引用可以用来访问对象,但进行垃圾回收时弱引用并不会被考虑在内,仅有弱引用指向的对象仍然会被GC回收。
WeakHashMap 内部是通过弱引用来管理entry的,弱引用的特性对应到 WeakHashMap 上意味着什么呢?
WeakHashMap 里的entry可能会被GC自动删除,即使程序员没有调用remove()或者clear()方法。
*WeakHashMap* 的这个特点特别适用于需要缓存的场景。在缓存场景下,由于内存是有限的,不能缓存所有对象;对象缓存命中可以提高系统效率,但缓存MISS也不会造成错误,因为可以通过计算重新得到。
HashMap 的实现原理
HashMap 是一个基于哈希表实现的 Map 接口的具体实现。它提供了键值对的存储,并且保证每个键都是唯一的。
核心原理
- 哈希函数:用于将键转换为哈希码。
- 负载因子:用于确定何时需要重新哈希(扩容)。
- 哈希冲突:通过链表或红黑树解决哈希冲突。
- 哈希表结构:包含一个数组,每个数组元素是一个链表或红黑树的头节点。
重要特性
- 哈希函数:通过
hashCode()方法生成。 - 负载因子:默认为 0.75,当负载因子超过阈值时进行扩容。
- 哈希冲突:通过链表解决冲突(Java 8 之前),当链表长度达到一定阈值时转换为红黑树。
- 哈希表结构:数组 + 链表/红黑树。
哈希表结构
表格 还在加载中,请等待加载完成后再尝试复制
HashMap 数据结构?
HashMap 的数据结构基于哈希表,它由一个数组加上数组中的每个元素指向的链表或红黑树构成。
数据结构
- 数组:主数据结构,用于存储键值对。
- 链表:解决哈希冲突时使用。
- 红黑树:当链表长度达到一定阈值时使用。
结构特点
表格 还在加载中,请等待加载完成后再尝试复制
HashMap 源码理解
HashMap 的源码涉及到多个方面,包括哈希函数、扩容机制、键值对存储等。
关键部分
- 哈希函数:计算键的哈希值。
- 扩容机制:当负载因子超过阈值时进行扩容。
- 键值对存储:存储键值对到哈希表中。
源码关键点
表格 还在加载中,请等待加载完成后再尝试复制
HashMap 如何 put 数据?
HashMap 的 put 方法涉及以下步骤:
- 计算哈希码:使用键的
hashCode()方法计算哈希码。 - 确定索引:使用哈希码和哈希表长度计算索引。
- 检查冲突:检查是否有哈希冲突。
- 插入键值对:如果不存在冲突则插入键值对,否则替换旧值或添加到链表/红黑树中。
- 检查负载因子:如果负载因子超过阈值,则进行扩容。
put 数据流程
表格 还在加载中,请等待加载完成后再尝试复制
HashMap怎么手写实现?
为了实现一个简单的 HashMap,我们需要考虑以下几个核心部分:
- 哈希函数:用于计算键的哈希值。
- 哈希表:通常是一个数组,其中每个位置是一个链表或者红黑树的头节点。
- 解决哈希冲突:使用链表或红黑树来解决冲突。
- 负载因子:决定何时进行扩容。
- 扩容机制:当负载因子超过预定阈值时,需要重新分配更大的数组并迁移数据。
核心概念
表格 还在加载中,请等待加载完成后再尝试复制
ConcurrentHashMap的实现原理
ConcurrentHashMap 是一个线程安全的哈希映射,它的实现采用了分段锁和 CAS 操作来保证多线程下的安全性。
核心概念
表格 还在加载中,请等待加载完成后再尝试复制
实现原理
- 分段锁:
ConcurrentHashMap使用分段锁来减少锁竞争。 - CAS 操作:在更新操作中使用 CAS 来保证原子性。
- 哈希表结构:内部使用哈希表结构来存储键值对。
- 扩容机制:当负载因子超过阈值时进行扩容。
分段锁和CAS操作
表格 还在加载中,请等待加载完成后再尝试复制
ArrayMap和HashMap的对比
ArrayMap 是 Android 平台上的一种优化过的哈希映射实现,主要用于节省内存和提高性能。
对比
表格 还在加载中,请等待加载完成后再尝试复制
HashTable实现原理
HashTable 是一个线程安全的哈希映射实现,它使用内部同步来保证多线程下的安全性。
实现原理
- 内部同步:
HashTable使用synchronized关键字来保证线程安全。 - 哈希表结构:内部使用哈希表结构来存储键值对。
- 扩容机制:当负载因子超过阈值时进行扩容。
内部同步
表格 还在加载中,请等待加载完成后再尝试复制
TreeMap具体实现
TreeMap 是一个基于红黑树实现的有序映射。
实现原理
- 红黑树:
TreeMap使用红黑树来存储键值对。 - 键排序:键必须实现
Comparable接口或者提供一个Comparator。 - 插入操作:使用红黑树的插入算法。
- 删除操作:使用红黑树的删除算法。
红黑树
表格 还在加载中,请等待加载完成后再尝试复制
HashMap和HashTable的区别
HashMap 和 HashTable 都是键值对映射,但它们有一些关键的区别。
区别
表格 还在加载中,请等待加载完成后再尝试复制
HashMap与HashSet的区别
HashMap 和 HashSet 都是基于哈希表实现的,但它们的设计目的不同。
区别
表格 还在加载中,请等待加载完成后再尝试复制
HashSet与HashMap怎么判断集合元素重复?
HashSet 实际上是通过一个 HashMap 来实现的,因此它的重复性检查依赖于 HashMap 的行为。
判断重复性
- 哈希码:每个元素都会被计算哈希码。
- 等价性比较:使用
equals方法来比较元素。
具体过程
表格 还在加载中,请等待加载完成后再尝试复制
集合Set实现Hash怎么防止碰撞
Set 集合的实现,特别是 HashSet,使用哈希表来防止元素重复,并通过一定的策略来处理哈希碰撞。
防止碰撞
- 哈希码:通过元素的
hashCode()方法计算哈希码。 - 等价性比较:使用
equals方法来判断元素是否相等。 - 冲突解决:使用链表或红黑树来解决哈希冲突。
策略
表格 还在加载中,请等待加载完成后再尝试复制
ArrayList和LinkedList的区别,以及应用场景
主要区别
- 数据结构:
ArrayList:基于动态数组实现。LinkedList:基于双向链表实现。
- 内存使用:
ArrayList:连续内存空间,方便随机访问。LinkedList:分散内存空间,每个节点包含前驱和后继节点的引用。
- 插入和删除操作:
ArrayList:插入和删除可能需要移动大量元素,性能较差。LinkedList:插入和删除只需改变相邻节点的引用,性能较好。
- 遍历:
ArrayList:通过索引访问,快速。LinkedList:通过节点链接遍历,较慢。
- 初始化和扩容:
ArrayList:初始容量固定,扩容时需要创建新的数组并复制元素。LinkedList:每个节点单独创建,无需扩容。
应用场景
ArrayList:- 适用于需要频繁访问元素的场景。
- 适用于已知元素数量或元素数量相对稳定的场景。
LinkedList:- 适用于需要频繁插入和删除元素的场景。
- 适用于元素数量不确定或经常变化的场景。
总结
表格 还在加载中,请等待加载完成后再尝试复制
3 Java IO
Java IO相关
3.1 基础IO
如何从数据传输方式理解IO流?
从数据传输方式或者说是运输方式角度看,可以将 IO 类分为:
- 字节流, 字节流读取单个字节,字符流读取单个字符(一个字符根据编码的不同,对应的字节也不同,如 UTF-8 编码中文汉字是 3 个字节,GBK编码中文汉字是 2 个字节。)
- 字符流, 字节流用来处理二进制文件(图片、MP3、视频文件),字符流用来处理文本文件(可以看做是特殊的二进制文件,使用了某种编码,人可以阅读)。
字节是给计算机看的,字符才是给人看的
- 字节流

- 字符流

- 字节转字符?

如何从数据操作上理解IO流?
从数据来源或者说是操作对象角度看,IO 类可以分为:

Java IO设计上使用了什么设计模式?
装饰者模式: 所谓装饰,就是把这个装饰者套在被装饰者之上,从而动态扩展被装饰者的功能。
- 装饰者举例
设计不同种类的饮料,饮料可以添加配料,比如可以添加牛奶,并且支持动态添加新配料。每增加一种配料,该饮料的价格就会增加,要求计算一种饮料的价格。
下图表示在 DarkRoast 饮料上新增新添加 Mocha 配料,之后又添加了 Whip 配料。DarkRoast 被 Mocha 包裹,Mocha 又被 Whip 包裹。它们都继承自相同父类,都有 cost() 方法,外层类的 cost() 方法调用了内层类的 cost() 方法。

- 以 InputStream 为例
- InputStream 是抽象组件;
- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。

实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
Java 中怎么创建 ByteBuffer?
答案:Java 中创建ByteBuffer的常见方式:
分配堆内存缓冲区:
ByteBuffer buffer = ByteBuffer.allocate(1024); // 分配1024字节的堆内存缓冲区分配直接内存缓冲区:
ByteBuffer buffer = ByteBuffer.allocateDirect(1024); // 分配1024字节的直接内存缓冲区包装现有字节数组:
byte[] bytes = new byte[10]; ByteBuffer buffer = ByteBuffer.wrap(bytes); // 包装字节数组,操作会影响原数组
Java 中,怎么读写 ByteBuffer?
答案:ByteBuffer的读写操作基于position、limit和capacity三个指针,需切换读写模式:
写入数据:
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Hello".getBytes()); // 写入字节数据
buffer.putInt(123); // 写入int值
读取数据:
buffer.flip(); // 切换为读模式(limit=position; position=0)
String str = new String(buffer.array(), 0, buffer.limit()); // 读取字符串
int value = buffer.getInt(); // 读取int值
其他常用方法:
clear():重置指针(position=0; limit=capacity),不清除数据。compact():压缩缓冲区(将未读取数据移至头部),切换为写模式。mark()和reset():标记和恢复当前 position。
Java 采用的是大端还是小端?
答案:Java 虚拟机(JVM)内部采用大端字节序(Big Endian),即高位字节存于低地址,低位字节存于高地址。例如,整数0x12345678在内存中的存储顺序为0x12、0x34、0x56、0x78。
ByteBuffer 中的字节序是什么?
答案:ByteBuffer默认使用大端字节序(Big Endian),但可通过order(ByteOrder)方法修改:
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.order(ByteOrder.LITTLE_ENDIAN); // 设置为小端字节序
buffer.putInt(0x12345678); // 按小端序写入
字节序影响多字节数据(如int、long)的存储顺序,对单字节操作无影响。
Java 中,直接缓冲区与非直接缓冲器有什么区别?
答案:
| 特性 | 直接缓冲区(Direct Buffer) | 非直接缓冲区(Heap Buffer) |
|---|---|---|
| 内存位置 | 操作系统直接管理的物理内存(非 JVM 堆) | JVM 堆内存 |
| 创建成本 | 高(需调用系统 API 分配内存) | 低(JVM 直接分配) |
| 读写性能 | 高(避免 Java 堆与内核空间的复制) | 低(需先复制到内核空间再写入 IO 设备) |
| 适用场景 | 频繁 IO 操作(如网络 / 文件传输) | 少量数据且频繁创建的场景 |
| 内存管理 | 需手动调用System.gc()或Cleaner释放(可能导致内存泄漏) | 随 GC 自动回收 |
| 优缺点 | 减少内存复制,提升 IO 效率,但管理成本高 | 使用简单,但大数据量时性能较差 |
Java 中的内存映射缓存区是什么?
答案:内存映射缓冲区(Memory-Mapped Buffer)是ByteBuffer的特殊实现,通过FileChannel.map()将文件直接映射到内存区域,实现高效读写:
RandomAccessFile file = new RandomAccessFile("data.txt", "rw");
FileChannel channel = file.getChannel();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, file.length());
特点:
- 文件内容直接映射到虚拟内存,无需显式读写操作。
- 支持跨进程共享(如多个 JVM 实例访问同一映射文件)。
- 适合处理大文件(受限于操作系统虚拟内存大小)。
Java 中,ByteBuffer 与 StringBuffer 有什么区别?
答案:
| 特性 | ByteBuffer | StringBuffer |
|---|---|---|
| 数据类型 | 操作字节(byte) | 操作字符(char) |
| 主要用途 | 用于 NIO 数据传输、二进制数据处理 | 用于字符串拼接、修改(线程安全) |
| 线程安全性 | 非线程安全 | 线程安全(方法用synchronized修饰) |
| 实现原理 | 基于字节数组,支持直接 / 非直接内存 | 基于可变字符数组 |
| 核心方法 | put()、get()、flip() | append()、insert()、delete() |
| 适用场景 | 网络 IO、文件操作、加密等 | 多线程环境下的字符串操作 |
说出 5 条 IO 的最佳实践
答案:Java IO 的最佳实践:
使用带缓冲的 IO 类:避免频繁的磁盘或网络操作,提高效率:
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) { String line; while ((line = br.readLine()) != null) { // 处理行数据 } }使用 NIO 或 NIO.2:对于高并发场景,使用
java.nio包中的非阻塞 IO:Path path = Paths.get("data.txt"); byte[] bytes = Files.readAllBytes(path);资源自动关闭:使用
try-with-resources语句确保流资源被正确关闭:try (FileOutputStream fos = new FileOutputStream("output.txt")) { fos.write(data); } // 自动关闭fos批量操作:使用
BufferedOutputStream.write(byte[])等批量方法减少 IO 次数:byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); }内存映射文件:对于大文件,使用
MappedByteBuffer提高读写性能:try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) { MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size()); // 直接操作buffer }异步 IO:使用 Java 7 + 的
AsynchronousFileChannel处理异步 IO 操作:AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, StandardOpenOption.READ); channel.read(buffer, 0, null, new CompletionHandler<Integer, Void>() { @Override public void completed(Integer result, Void attachment) { /* 处理结果 */ } @Override public void failed(Throwable exc, Void attachment) { /* 处理异常 */ } });避免阻塞操作:在网络编程中,使用非阻塞 IO(如
Selector)避免线程长时间等待:Selector selector = Selector.open(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ);
什么是Java的NIO和AIO?
Java的NIO(New Input/Output)和AIO(Asynchronous Input/Output)是Java提供的新IO模型,它们与传统的阻塞IO模型相比,提供了更高的性能和更好的资源利用率。
NIO是Java 1.4引入的,它提供了一种非阻塞的IO模型,允许单个线程同时处理多个Channel(网络连接)。NIO的核心组件包括:
Channel:一个可以同时进行读写的通信管道。Buffer:一个容器对象,用于存储要传输的数据。Selector:一个多路复用器,可以监听多个Channel上的事件,如连接请求、数据到达等。
NIO的非阻塞特性使得线程可以在等待IO操作完成的同时去做其他事情,从而提高了线程的利用率。NIO适用于需要处理大量并发连接的网络服务器。
AIO是Java 7引入的,它提供了一种异步的IO模型,允许程序发起IO操作并立即返回,当IO操作完成时,会通过回调函数通知程序。AIO的核心组件包括:
AsynchronousFileChannel:用于异步文件读写。AsynchronousSocketChannel:用于异步网络通信。CompletionHandler:一个回调接口,用于处理IO操作的完成事件。
AIO的异步特性使得程序不需要关心IO操作的具体细节,只需要在操作完成时接收通知,这样可以进一步提高程序的响应性和性能。AIO适用于需要处理大量并发IO操作的应用程序。
NIO和AIO都是现代Java程序开发中不可或缺的部分,它们提供了更加灵活和高效的IO处理方式,使得Java程序能够更好地应对高并发和大数据量的场景。
描述Java中的同步集合和并发集合。
Java中的集合可以分为同步集合和并发集合。同步集合是线程安全的,它们在多线程环境下提供了数据的一致性和完整性。并发集合则是为并发编程设计的,它们提供了更高的性能和更好的并发性能。
同步集合: 同步集合通过内部的同步机制来保证线程安全。这些集合在多线程环境下可以安全地被多个线程访问,但同步机制也会导致性能的开销。
Vector:是ArrayList的线程安全版本,使用synchronized关键字同步方法。Hashtable:是HashMap的线程安全版本,使用synchronized关键字同步整个表。Collections.synchronizedList、Collections.synchronizedSet、Collections.synchronizedMap:这些方法可以包装任何列表、集合或映射,使其线程安全。
并发集合: 并发集合是为并发设计的高性能集合,它们提供了一系列的原子操作来支持并发访问。并发集合通常比同步集合有更好的性能,因为它们使用了更细粒度的锁或者无锁设计。
CopyOnWriteArrayList:在每次写操作时复制底层数组,读操作可以并发执行,不需要同步。CopyOnWriteArraySet:基于CopyOnWriteArrayList的集合,提供高效的读操作。ConcurrentHashMap:是HashMap的并发版本,使用锁分离技术来提高并发性能。ConcurrentLinkedQueue、LinkedBlockingQueue、ArrayBlockingQueue:这些队列提供了高效的并发操作。
在选择集合时,需要根据应用程序的并发需求和性能要求来决定使用同步集合还是并发集合。如果并发访问不是主要关注点,可以使用普通的集合类。如果需要在多线程环境下安全地访问集合,可以选择同步集合或并发集合。
描述Java中的输入输出流(IO)和缓冲流。
Java中的输入输出流(IO)是用于处理数据输入和输出的一套API。Java IO流基于字节流和字符流的概念,提供了一系列的类和接口来读取和写入不同类型的数据源,如文件、网络连接、内存缓冲区等。
- 字节流:以字节为单位处理数据的流。
InputStream和OutputStream是所有字节流的基类。常用的字节流类有:FileInputStream和FileOutputStream:用于读写文件。BufferedInputStream和BufferedOutputStream:在字节流的基础上添加了缓冲功能,提高了IO操作的效率。
- 字符流:以字符为单位处理数据的流,可以处理编码和解码。
Reader和Writer是所有字符流的基类。常用的字符流类有:FileReader和FileWriter:用于读写文件。BufferedReader和BufferedWriter:在字符流的基础上添加了缓冲功能,提高了IO操作的效率。
缓冲流是Java IO中的一个重要概念,它们通过在内存中维护一个缓冲区来提高IO操作的性能。当进行读写操作时,数据首先被存储在缓冲区中,直到缓冲区满了或者显式地刷新缓冲区,才会实际地进行IO操作。这样可以减少对底层资源的调用次数,特别是在处理大量数据时,可以显著提高性能。
例如,BufferedReader可以一次读取多个字符到缓冲区,然后可以通过调用readLine()方法高效地逐行读取文本文件。类似地,BufferedWriter可以一次性写入多个字符到缓冲区,然后一次性将缓冲区的内容写入到目标。
在使用IO流时,需要注意异常处理和资源管理。通常,使用try-with-resources语句可以自动关闭资源,避免资源泄露。
3.2 5种IO模型
IO模型 |
什么是阻塞?什么是同步?
- 阻塞IO 和 非阻塞IO
这两个概念是程序级别的。主要描述的是程序请求操作系统IO操作后,如果IO资源没有准备好,那么程序该如何处理的问题: 前者等待;后者继续执行(并且使用线程一直轮询,直到有IO资源准备好了)
- 同步IO 和 非同步IO
这两个概念是操作系统级别的。主要描述的是操作系统在收到程序请求IO操作后,如果IO资源没有准备好,该如何响应程序的问题: 前者不响应,直到IO资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当IO资源准备好以后,再用事件机制返回给程序。
什么是Linux的IO模型?
网络IO的本质是socket的读取,socket在linux系统被抽象为流,IO可以理解为对流的操作。刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
- 第一阶段:等待数据准备 (Waiting for the data to be ready)。
- 第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。
对于socket流而言,
- 第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区。
- 第二步:把数据从内核缓冲区复制到应用进程缓冲区。
网络应用需要处理的无非就是两大类问题,网络IO,数据计算。相对于后者,网络IO的延迟,给应用带来的性能瓶颈大于后者。网络IO的模型大致有如下几种:
- 同步阻塞IO(bloking IO)
- 同步非阻塞IO(non-blocking IO)
- 多路复用IO(multiplexing IO)
- 信号驱动式IO(signal-driven IO)
- 异步IO(asynchronous IO)

PS: 这块略复杂,在后面的提供了问答,所以用了最简单的举例结合Linux IO图例帮你快速理解。
什么是同步阻塞IO?
应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。
- 举例理解
你早上去买有现炸油条,你点单,之后一直等店家做好,期间你啥其它事也做不了。(你就是应用级别,店家就是操作系统级别, 应用被阻塞了不能做其它事)
- Linux 中IO图例

什么是同步非阻塞IO?
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。
- 举例理解
你早上去买现炸油条,你点单,点完后每隔一段时间询问店家有没有做好,期间你可以做点其它事情。(你就是应用级别,店家就是操作系统级别,应用可以做其它事情并通过轮询来看操作系统是否完成)
- Linux 中IO图例

什么是多路复用IO?
系统调用可能是由多个任务组成的,所以可以拆成多个任务,这就是多路复用。
- 举例理解
你早上去买现炸油条,点单收钱和炸油条原来都是由一个人完成的,现在他成了瓶颈,所以专门找了个收银员下单收钱,他则专注在炸油条。(本质上炸油条是耗时的瓶颈,将他职责分离出不是瓶颈的部分,比如下单收银,对应到系统级别也时一样的意思)
- Linux 中IO图例
使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。
它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。

有哪些多路复用IO?
目前流程的多路复用IO实现主要包括四种: select、poll、epoll、kqueue。下表是他们的一些重要特性的比较:
| IO模型 | 相对性能 | 关键思路 | 操作系统 | JAVA支持情况 |
|---|---|---|---|---|
| select | 较高 | Reactor | windows/Linux | 支持,Reactor模式(反应器设计模式)。Linux操作系统的 kernels 2.4内核版本之前,默认使用select;而目前windows下对同步IO的支持,都是select模型 |
| poll | 较高 | Reactor | Linux | Linux下的JAVA NIO框架,Linux kernels 2.6内核版本之前使用poll进行支持。也是使用的Reactor模式 |
| epoll | 高 | Reactor/Proactor | Linux | Linux kernels 2.6内核版本及以后使用epoll进行支持;Linux kernels 2.6内核版本之前使用poll进行支持;另外一定注意,由于Linux下没有Windows下的IOCP技术提供真正的 异步IO 支持,所以Linux下使用epoll模拟异步IO |
| kqueue | 高 | Proactor | Linux | 目前JAVA的版本不支持 |
多路复用IO技术最适用的是“高并发”场景,所谓高并发是指1毫秒内至少同时有上千个连接请求准备好。其他情况下多路复用IO技术发挥不出来它的优势。另一方面,使用JAVA NIO进行功能实现,相对于传统的Socket套接字实现要复杂一些,所以实际应用中,需要根据自己的业务需求进行技术选择。
什么是信号驱动IO?
应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。
- 举例理解
你早上去买现炸油条,门口排队的人多,现在引入了一个叫号系统,点完单后你就可以做自己的事情了,然后等叫号就去拿就可以了。(所以不用再去自己频繁跑去问有没有做好了)
- Linux 中IO图例

什么是异步IO?
相对于同步IO,异步IO不是顺序执行。用户进程进行aio_read系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到socket数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。IO两个阶段,进程都是非阻塞的。
- 举例理解
你早上去买现炸油条, 不用去排队了,打开美团外卖下单,然后做其它事,一会外卖自己送上门。(你就是应用级别,店家就是操作系统级别, 应用无需阻塞,这就是非阻塞;系统还可能在处理中,但是立刻响应了应用,这就是异步)
- Linux 中IO图例
(Linux提供了AIO库函数实现异步,但是用的很少。目前有很多开源的异步IO库,例如libevent、libev、libuv)

什么是Reactor模型?
大多数网络框架都是基于Reactor模型进行设计和开发,Reactor模型基于事件驱动,特别适合处理海量的I/O事件。
- 传统的IO模型?
这种模式是传统设计,每一个请求到来时,大致都会按照:请求读取->请求解码->服务执行->编码响应->发送答复 这个流程去处理。

服务器会分配一个线程去处理,如果请求暴涨起来,那么意味着需要更多的线程来处理该请求。若请求出现暴涨,线程池的工作线程数量满载那么其它请求就会出现等待或者被抛弃。若每个小任务都可以使用非阻塞的模式,然后基于异步回调模式。这样就大大提高系统的吞吐量,这便引入了Reactor模型。
- Reactor模型中定义的三种角色:
- Reactor:负责监听和分配事件,将I/O事件分派给对应的Handler。新的事件包含连接建立就绪、读就绪、写就绪等。
- Acceptor:处理客户端新连接,并分派请求到处理器链中。
- Handler:将自身与事件绑定,执行非阻塞读/写任务,完成channel的读入,完成处理业务逻辑后,负责将结果写出channel。可用资源池来管理。
- 单Reactor单线程模型
Reactor线程负责多路分离套接字,accept新连接,并分派请求到handler。Redis使用单Reactor单进程的模型。

消息处理流程:
- Reactor对象通过select监控连接事件,收到事件后通过dispatch进行转发。
- 如果是连接建立的事件,则由acceptor接受连接,并创建handler处理后续事件。
- 如果不是建立连接事件,则Reactor会分发调用Handler来响应。
- handler会完成read->业务处理->send的完整业务流程。
- 单Reactor多线程模型
将handler的处理池化。

- 多Reactor多线程模型
主从Reactor模型: 主Reactor用于响应连接请求,从Reactor用于处理IO操作请求,读写分离了。

什么是Java NIO?
NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统IO基于字节流和字符流进行操作,而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。
NIO和传统IO(一下简称IO)之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。

3.3 零拷贝
传统的IO存在什么问题?为什么引入零拷贝的?
如果服务端要提供文件传输的功能,我们能想到的最简单的方式是:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。
传统 I/O 的工作方式是,数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。
代码通常如下,一般会需要两个系统调用:
read(file, tmp_buf, len);
write(socket, tmp_buf, len);
代码很简单,虽然就两行代码,但是这里面发生了不少的事情。

首先,期间共发生了 4 次用户态与内核态的上下文切换,因为发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。
上下文切换到成本并不小,一次切换需要耗时几十纳秒到几微秒,虽然时间看上去很短,但是在高并发的场景下,这类时间容易被累积和放大,从而影响系统的性能。
其次,还发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的,下面说一下这个过程:
- 第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的。
- 第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,这个拷贝到过程是由 CPU 完成的。
- 第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的。
- 第四次拷贝,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的。
我们回过头看这个文件传输的过程,我们只是搬运一份数据,结果却搬运了 4 次,过多的数据拷贝无疑会消耗 CPU 资源,大大降低了系统性能。
这种简单又传统的文件传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能。
所以,要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。
mmap + write怎么实现的零拷贝?
在前面我们知道,read() 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,于是为了减少这一步开销,我们可以用 mmap() 替换 read() 系统调用函数。
buf = mmap(file, len);
write(sockfd, buf, len);
mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。

具体过程如下:
- 应用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区;
- 应用进程再调用 write(),操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU 来搬运数据;
- 最后,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的。
我们可以得知,通过使用 mmap() 来代替 read(), 可以减少一次数据拷贝的过程。
但这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。
sendfile怎么实现的零拷贝?
在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile(),函数形式如下:
#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。
首先,它可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。
其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换,和 3 次数据拷贝。如下图:

但是这还不是真正的零拷贝技术,如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同),我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。
你可以在你的 Linux 系统通过下面这个命令,查看网卡是否支持 scatter-gather 特性:
$ ethtool -k eth0 | grep scatter-gather
scatter-gather: on
于是,从 Linux 内核 2.4 版本开始起,对于支持网卡支持 SG-DMA 技术的情况下, sendfile() 系统调用的过程发生了点变化,具体过程如下:
- 第一步,通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;
- 第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝;
所以,这个过程之中,只进行了 2 次数据拷贝,如下图:

这就是所谓的零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。
零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。
4 Java 并发
并发和多线程
4.1 并发基础
开启线程的三种方式
方式一:继承 Thread 类
- 创建一个类继承
Thread类,并重写run()方法。 - 创建该类的对象并调用
start()方法来启动线程。
方式二:实现 Runnable 接口
- 创建一个类实现
Runnable接口,并实现run()方法。 - 创建该类的对象并传递给
Thread构造函数,再调用start()方法来启动线程。
方式三:实现 Callable 接口
- 创建一个类实现
Callable接口,并实现call()方法。 - 使用
FutureTask包装Callable对象,然后将FutureTask传递给Thread构造函数,再调用start()方法来启动线程。
示例
// 继承 Thread 类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread running");
}
}
// 实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable running");
}
}
// 实现 Callable 接口
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "MyCallable result";
}
}
public class ThreadCreationExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 方式一:继承 Thread 类
MyThread myThread = new MyThread();
myThread.start();
// 方式二:实现 Runnable 接口
Thread myRunnableThread = new Thread(new MyRunnable());
myRunnableThread.start();
// 方式三:实现 Callable 接口
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread myCallableThread = new Thread(futureTask);
myCallableThread.start();
System.out.println("Callable result: " + futureTask.get());
}
}
线程和进程的区别
表格 还在加载中,请等待加载完成后再尝试复制
为什么要有线程,而不是仅仅用进程?
- 资源开销
- 进程之间的切换和通信需要更大的开销,而线程之间切换和通信的开销要小得多。
- 内存共享
- 线程可以共享同一进程内的内存和其他资源,减少了复制和同步的开销。
- 并发性
- 线程提供了更细粒度的并发控制,有助于提高程序的响应性和效率。
- 上下文切换
- 线程之间的上下文切换开销较小,有利于提高系统的并发执行能力。
- 通信简便
- 线程之间可以直接访问共享变量,简化了进程间通信的复杂性。
- 灵活性
- 线程提供了更加灵活的并发模型,可以更容易地实现复杂的并发控制逻辑。
run()和start()方法区别
run()方法- 是
Thread类中的一个方法,或者由实现了Runnable接口的类提供。 - 直接调用
run()方法会在当前线程中执行方法体内的代码。
- 是
start()方法- 是
Thread类的方法,用于启动一个新的线程。 - 调用
start()会创建一个新的线程,并在新线程中调用run()方法。
- 是
主要区别
- 线程创建
start()方法创建了一个新的线程。run()方法在当前线程中执行。
- 并发执行
start()方法使得代码在不同的线程中并发执行。run()方法不创建新的线程,代码仍然在当前线程中顺序执行。
- 资源分配
start()方法会为新线程分配必要的资源。run()方法不会为新线程分配资源。
示例
public class RunVsStartExample {
public static void main(String[] args) {
Thread myThread = new Thread(() -> System.out.println("Thread running"));
// 调用 run() 方法
myThread.run();
System.out.println("After run()");
// 调用 start() 方法
myThread = new Thread(() -> System.out.println("Thread running"));
myThread.start();
System.out.println("After start()");
}
}
如何控制某个方法允许并发访问线程的个数
- 使用
SemaphoreSemaphore是一种信号量,可以用来控制对共享资源的访问数量。- 创建一个
Semaphore对象,并为其分配许可的数量,以此来限制并发访问的数量。
示例
import java.util.concurrent.Semaphore;
public class ConcurrentAccessControl {
private final Semaphore semaphore = new Semaphore(5); // 允许最多5个线程访问
public void concurrentMethod() {
try {
semaphore.acquire(); // 获取许可
// 这里是受保护的代码块
System.out.println("Thread " + Thread.currentThread().getName() + " is executing");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // 释放许可
}
}
public static void main(String[] args) throws InterruptedException {
ConcurrentAccessControl example = new ConcurrentAccessControl();
for (int i = 0; i < 10; i++) {
int id = i;
new Thread(() -> example.concurrentMethod()).start();
}
Thread.sleep(2000); // 等待所有线程完成
}
}
在Java中 wait 和 sleep 方法的不同
- 所属类
wait()方法属于Object类,用于实现线程之间的等待/通知机制。sleep()方法属于Thread类,用于暂停当前线程的执行。
- 锁释放
wait()方法会释放对象的锁。sleep()方法不会释放任何锁。
- 中断响应
wait()方法可以被中断,通过InterruptedException中断线程。sleep()方法也可以被中断,同样通过InterruptedException中断线程。
- 使用场合
wait()通常用于线程间的同步和通信。sleep()用于简单的延迟执行。
示例
public class WaitVsSleepExample {
public static void main(String[] args) {
Object lock = new Object();
// 使用 wait()
new Thread(() -> {
synchronized (lock) {
try {
System.out.println("Waiting...");
lock.wait();
System.out.println("Resumed!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
// 使用 sleep()
new Thread(() -> {
try {
System.out.println("Sleeping...");
Thread.sleep(1000);
System.out.println("Awake!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
谈谈 wait/notify 关键字的理解
wait()方法- 使当前线程等待,直到被其他线程唤醒。
- 必须在同步上下文中调用,通常是在
synchronized块或方法中。 - 会释放当前持有的锁,直到被唤醒。
notify()方法- 唤醒正在等待的线程中的一个。
- 也必须在同步上下文中调用。
- 释放锁,让被唤醒的线程有机会获得锁并继续执行。
notifyAll()方法- 唤醒所有等待的线程。
- 与
notify()类似,必须在同步上下文中调用。
使用场景
- 生产者-消费者模式
- 生产者调用
notify()唤醒消费者,消费者调用wait()等待产品可用。
- 生产者调用
- 同步控制
- 控制线程之间的执行顺序,确保线程安全。
注意事项
- 中断响应
wait()方法可以被中断,通过InterruptedException中断线程。- 在调用
wait()方法之前最好检查线程的中断状态。
- 同步上下文
wait()和notify()必须在同步上下文中调用,以避免IllegalMonitorStateException。
- 唤醒时机
notify()和notifyAll()只是唤醒线程,被唤醒的线程还需要重新获得锁才能继续执行。
- 正确性
- 使用
wait()和notify()时要确保线程的正确唤醒,避免死锁或活锁的情况出现。
- 使用
示例
public class WaitNotifyExample {
private static final Object lock = new Object();
private static boolean ready = false;
public static void main(String[] args) throws InterruptedException {
// 生产者
new Thread(() -> {
synchronized (lock) {
System.out.println("Producer sets ready to true and notifies.");
ready = true;
lock.notify();
}
}).start();
// 消费者
new Thread(() -> {
synchronized (lock) {
while (!ready) {
try {
System.out.println("Consumer waits for the product to be ready.");
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("Consumer: Product is ready!");
}
}).start();
}
}
什么导致线程阻塞?
线程阻塞是指线程暂时停止执行,等待某些条件满足后再恢复执行的一种状态。导致线程阻塞的原因主要有以下几种:
- I/O 操作:
- 当线程执行 I/O 操作(如读写文件或网络数据)时,如果 I/O 操作还没有完成,线程就会进入阻塞状态,直到 I/O 操作完成。
- 等待输入:
- 当线程等待用户输入时,如果没有输入,线程会处于阻塞状态,直到用户输入数据。
- 同步锁:
- 当线程试图获取一个已被其他线程持有的锁时,该线程会被阻塞,直到锁被释放。
- 等待条件:
- 当线程调用
wait()方法等待特定条件成立时,线程会被阻塞,直到其他线程调用notify()或notifyAll()方法唤醒它。
- 当线程调用
- 睡眠:
- 当线程调用
Thread.sleep(long millis)方法时,线程会被阻塞指定的时间长度。
- 当线程调用
- 等待资源:
- 当线程等待外部资源(如数据库连接)时,如果没有资源可用,线程会被阻塞,直到资源可用。
- 线程池的等待队列:
- 当线程池中的线程数量达到上限时,新提交的任务会被放入等待队列,等待空闲线程处理。
- 等待事件:
- 当线程等待特定事件(如计时器事件或外部事件)时,如果没有事件发生,线程会被阻塞。
- 等待其他线程:
- 当线程依赖于其他线程的执行结果时,如果没有结果,线程会被阻塞,直到结果可用。
线程如何关闭?
在Java中,可以通过以下几种方式来关闭线程:
- 正常退出:
- 线程执行完毕后自然退出。
- 实现
Runnable或Callable接口的run()或call()方法中,执行完所需任务后自然结束。
- 标志位控制:
- 在线程内部设置一个标志位,通过修改标志位的值来控制线程是否应该退出。
- 例如,可以使用
volatile boolean shouldRun = true;,然后在run()方法中检查该标志位的值。
- 中断线程:
- 通过调用线程的
interrupt()方法中断线程。 - 在线程的
run()方法中定期检查线程是否被中断,如果是,则退出线程。
- 通过调用线程的
- 使用
Future:- 如果线程是通过
FutureTask或其他Future实现启动的,可以调用cancel(boolean mayInterruptIfRunning)方法取消线程的执行。 - 如果
mayInterruptIfRunning为true,则尝试中断正在运行的线程。
- 如果线程是通过
- 使用
Thread.interrupted():- 检查线程的中断状态,通常在循环中使用,以确保线程能够响应中断请求。
- 使用
Thread.stop()(不推荐):Thread.stop()方法已经废弃,因为它可能导致资源泄漏和不可预测的行为。
讲一下Java中的同步的方法
Java中的同步机制主要用于解决多线程环境中的线程安全问题,主要包括以下几种方法:
synchronized关键字:- 可以用于修饰方法或代码块,确保同一时刻只有一个线程可以访问被同步的代码。
- 修饰方法时,整个方法体都被同步。
- 修饰代码块时,需要指定同步监视器(通常是对象实例或类的静态成员)。
- 显式锁:
- 使用
java.util.concurrent.locks.Lock接口提供的锁机制,如ReentrantLock。 - 显式锁提供了比
synchronized更加灵活的锁定机制,例如可重入锁、公平锁、条件变量等。
- 使用
volatile变量:- 保证变量的可见性和禁止指令重排。
volatile变量在多线程环境中可以保证最新的值对所有线程可见,但不保证原子性。
- 原子变量:
- 使用
java.util.concurrent.atomic包中的原子类,如AtomicInteger、AtomicLong、AtomicReference等。 - 原子变量提供了线程安全的变量操作,不需要额外的同步机制。
- 使用
ThreadLocal:- 为每个线程提供独立的变量副本,从而避免了线程之间的数据共享和同步问题。
- 适用于每个线程需要独立数据副本的场景。
数据一致性如何保证?
在多线程环境中,保证数据一致性通常涉及以下几种策略:
- 同步机制:
- 使用
synchronized关键字或显式锁(如ReentrantLock)来确保数据操作的原子性。 - 确保同一时刻只有一个线程可以修改共享数据。
- 使用
- 不变性约束:
- 定义对象的状态不变性约束,确保对象状态的正确性。
- 例如,确保某个字段永远不会为
null。
- 事务管理:
- 在数据库操作中使用事务来保证操作的原子性、一致性、隔离性和持久性(ACID 属性)。
- 确保一组操作要么全部成功,要么全部失败。
- 版本控制:
- 为数据项添加版本号,确保数据更新的正确性。
- 当数据版本不匹配时,可以回滚或拒绝更新。
- 并发控制:
- 使用乐观锁或悲观锁机制来控制并发访问。
- 乐观锁通常通过版本号或时间戳实现,而悲观锁则通过锁定机制实现。
如何保证线程安全?
为了保证线程安全,可以采取以下措施:
- 使用同步机制:
- 使用
synchronized关键字、显式锁(如ReentrantLock)等来确保共享资源的互斥访问。 - 确保同一时刻只有一个线程可以修改共享资源。
- 使用
- 使用不可变对象:
- 使用不可变对象(如
String、BigInteger等)来避免多线程环境中的数据修改问题。 - 不可变对象一旦创建,其状态就不会改变,因此是天然线程安全的。
- 使用不可变对象(如
- 使用
ThreadLocal:- 为每个线程提供独立的数据副本,避免共享数据带来的同步问题。
- 适用于每个线程需要独立数据副本的场景。
- 使用并发工具类:
- 使用
java.util.concurrent包中的工具类,如ConcurrentHashMap、CopyOnWriteArrayList等。 - 这些类内部已经实现了线程安全的机制。
- 使用
- 避免过度同步:
- 在可能的情况下,尽量减少同步的范围,只同步真正需要的部分。
- 使用局部变量和方法参数来传递数据,而不是共享状态。
- 使用原子变量:
- 使用
java.util.concurrent.atomic包中的原子类,如AtomicInteger、AtomicBoolean等。 - 这些类提供了线程安全的操作,不需要额外的同步机制。
- 使用
如何实现线程同步?
线程同步是为了控制多个线程对共享资源的访问,避免数据不一致的问题。实现线程同步的主要方法包括:
synchronized关键字:- 用于同步方法或同步代码块。
- 确保同一时刻只有一个线程可以访问被同步的代码。
- 显式锁:
- 使用
java.util.concurrent.locks.Lock接口提供的锁机制,如ReentrantLock。 - 显式锁提供了更高级的锁定机制,例如可重入锁、公平锁等。
- 使用
Condition条件变量:- 与显式锁结合使用,实现更精细的线程同步。
- 可以使用
await()和signal()方法来控制线程的等待和唤醒。
Semaphore:- 用于控制对共享资源的访问数量。
- 通过
acquire()和release()方法来获取和释放许可。
CyclicBarrier:- 用于多个线程需要相互等待,直到到达某个公共屏障点。
- 通常用于实现线程之间的同步点。
CountDownLatch:- 允许一个或多个线程等待其他线程完成操作。
- 通常用于等待一组操作完成。
两个进程同时要求写或者读,能不能实现?如何防止进程的同步?
在多进程环境中,两个进程同时要求写或者读是可以实现的,但需要采取措施来防止数据不一致或冲突。常用的方法包括:
- 文件锁:
- 使用文件锁(如
FileLock)来确保文件在某一时刻只能被一个进程读写。 - 通常用于防止多个进程同时修改同一文件。
- 使用文件锁(如
- 消息队列:
- 使用消息队列(如 AMQP)来实现进程间的通信。
- 一个进程写入消息,另一个进程读取消息,确保数据的顺序性和完整性。
- 共享内存:
- 使用共享内存区域来实现进程间的数据共享。
- 结合信号量机制来控制多个进程对共享内存的访问。
- 分布式锁:
- 使用分布式锁机制(如 ZooKeeper 或 Redis)来实现跨进程的锁管理。
- 确保多个进程能够正确地获取和释放锁。
- 数据库事务:
- 如果数据存储在数据库中,可以使用数据库事务来确保数据的一致性。
- 通过事务的 ACID 属性来保证数据的安全性。
线程间操作 List
在Java中,如果多个线程需要操作同一个 List,为了保证线程安全,可以采取以下几种措施:
- 使用同步容器:
- 使用
Collections.synchronizedList()方法将列表包装成同步列表。 - 对列表的所有操作都需要在同步块中进行。
- 使用
- 使用并发容器:
- 使用
java.util.concurrent.ConcurrentHashMap或CopyOnWriteArrayList等并发容器。 - 这些容器内部已经实现了线程安全的机制。
- 使用
- 使用
Vector:Vector类是一个线程安全的列表实现,它的大多数方法都是同步的。- 但是
Vector的性能通常不如其他并发容器。
- 手动同步:
- 使用
synchronized关键字或显式锁(如ReentrantLock)来同步对列表的操作。 - 确保同一时刻只有一个线程可以修改列表。
- 使用
- 使用
ConcurrentSkipListSet:- 如果需要有序集合,可以使用
ConcurrentSkipListSet。 - 这是一个线程安全的有序集合实现。
- 如果需要有序集合,可以使用
Synchronized用法
synchronized 是Java中的关键字,用于实现同步机制。它可以用于同步方法或同步代码块。
- 同步方法:
- 在类的方法声明前面加上
synchronized关键字。 - 这样做会同步整个方法体的执行。
- 在类的方法声明前面加上
- 同步代码块:
- 在代码块上使用
synchronized关键字,并指定一个同步监视器。 - 同步监视器通常是一个对象实例或类的静态成员。
- 在代码块上使用
synchronize的原理
概述
synchronized 是 Java 中的关键字之一,用于实现线程同步,确保同一时刻只有一个线程可以访问被同步的代码段或方法。synchronized 的实现依赖于 Java 虚拟机 (JVM) 的内置锁机制。
锁的分类
- 对象锁:作用于对象实例,通常用于同步实例方法或同步代码块。
- 类锁:作用于类级别,通常用于同步静态方法或同步静态代码块。
实现机制
- 监视器锁:
synchronized关键字在底层实现中使用的是监视器锁 (monitor),这是一种重量级锁。 - 轻量级锁:JVM 通过 CAS (Compare And Swap) 操作实现轻量级锁,以减少锁的开销。
- 偏向锁:为了解决无竞争情况下的锁操作,JVM 引入了偏向锁机制。
- 锁升级:当锁的竞争加剧时,JVM 会自动将锁升级为重量级锁。
例子
public class Example {
public synchronized void method() {
// 同步方法体
}
public void anotherMethod() {
synchronized (this) {
// 同步代码块
}
}
}
谈谈对Synchronized关键字,类锁,方法锁,重入锁的理解
Synchronized关键字
synchronized是 Java 中的关键字,用于确保代码块或方法在同一时刻只被一个线程访问。- 它可以作用于方法或代码块,分别称为方法锁和代码块锁。
类锁
- 类锁用于同步静态方法或静态代码块。
- 类锁作用于整个类的所有实例上,即一个类的所有实例共享一个类锁。
- 当一个线程获得了类锁后,其他线程在尝试获取同一类锁时将会被阻塞。
方法锁
- 方法锁用于同步实例方法或同步代码块。
- 对象锁是作用于对象实例上的,每个对象实例都有自己的锁。
- 当一个线程获得了对象锁后,其他线程在尝试获取同一对象锁时将会被阻塞。
重入锁
- 重入锁允许同一个线程多次获取同一把锁。
- 重入锁通常指的是
ReentrantLock类,它是一个可重入的互斥锁。 - 与
synchronized相比,ReentrantLock提供了更高级的锁定机制,如公平锁和非公平锁的选择。
static synchronized 方法的多线程访问和作用
作用
static synchronized方法用于同步类级别的操作。- 它确保同一时刻只有一个线程可以访问该静态方法。
- 由于静态方法是类级别的,所以所有实例共享同一把锁。
多线程访问
- 当一个线程正在执行
static synchronized方法时,其他线程试图访问该方法将会被阻塞。 - 如果有多个线程同时访问同一个类的不同实例上的静态方法,它们也会受到阻塞,因为所有实例共享同一把类锁。
示例
public class MyClass {
public static synchronized void staticMethod() {
// 执行代码
}
}
同一个类里面两个synchronized方法,两个线程同时访问的问题
问题说明
- 如果一个类中有两个
synchronized方法,当一个线程正在执行其中一个方法时,另一个线程试图执行另一个方法。 - 如果这两个方法都是实例方法并且作用于同一个对象实例,那么第二个线程将会被阻塞。
- 如果两个方法是静态方法或者作用于不同的对象实例,则它们可以并行执行。
示例
public class MyClass {
public synchronized void method1() {
// 执行代码
}
public synchronized void method2() {
// 执行代码
}
}
// 线程1
new Thread(() -> {
MyClass instance = new MyClass();
instance.method1();
}).start();
// 线程2
new Thread(() -> {
MyClass instance = new MyClass();
instance.method2();
}).start();
在这个例子中,如果 method1 和 method2 是在同一个 MyClass 实例上调用的,那么两个线程将依次执行方法。如果它们是作用于不同的实例,那么它们可以并行执行。
volatile的原理
概述
volatile 是 Java 中的关键字,用于标记一个变量,确保该变量的写操作会立即反映到主内存中,而读操作总是从主内存中读取最新的值。
原理
- 内存可见性:
volatile变量的写操作会强制将值写入主内存,读操作总是从主内存读取最新值。 - 禁止指令重排序:
volatile变量的读写操作不会被 JVM 优化器重排序,确保了操作的顺序性。 - 不保证原子性:
volatile本身不保证复合操作的原子性,但在某些情况下可以用于替代锁。
示例
public class VolatileExample {
private volatile boolean flag = false;
public void writer() {
flag = true;
}
public void reader() {
while (!flag) {
// 等待标志变为 true
}
// 标志为 true 后执行代码
}
}
谈谈volatile关键字的用法
用法
- 状态标记:用于标记线程间共享的状态变量,确保状态的可见性。
- 线程通信:用于线程间的简单通信,如停止标记。
- 发布/初始化:用于发布不可变对象或初始化完成的标志。
- 禁止指令重排序:用于禁止编译器和处理器对读写操作进行重排序。
示例
public class VolatileUseExample {
private volatile boolean stopFlag = false;
public void setStopFlag(boolean stopFlag) {
this.stopFlag = stopFlag;
}
public void doWork() {
while (!stopFlag) {
// 执行任务
}
}
}
谈谈volatile关键字的作用
作用
- 内存可见性:确保所有线程都能看到对
volatile变量的最新修改。 - 禁止指令重排序:确保
volatile变量的读写操作不会被编译器和处理器重排序。 - 简化线程间通信:提供了一种轻量级的线程间通信机制,适用于简单的状态共享。
注意事项
- 不保证原子性:对于复合操作,
volatile本身不保证原子性。 - 性能影响:使用
volatile可能会导致性能下降,因为它增加了内存访问的开销。
谈谈NIO的理解
概述
NIO (New IO) 是 Java 中用于高效处理 I/O 操作的 API,它引入了缓冲区、通道和选择器等概念。
核心组件
- 缓冲区 (
Buffer):用于存放数据,支持数据的读写操作。 - 通道 (
Channel):用于数据的传输,连接缓冲区和物理设备。 - 选择器 (
Selector):用于监听多个通道的 I/O 请求,提高了 I/O 处理的效率。
优点
- 非阻塞 I/O:支持非阻塞模式,提高了 I/O 操作的效率。
- 选择器:通过选择器可以同时监听多个通道的 I/O 请求。
- 内存映射文件:支持内存映射文件,可以直接在内存中操作文件数据。
示例
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class NIOExample {
public static void main(String[] args) {
try (FileChannel fileChannel = FileChannel.open(Paths.get("file.txt"), StandardOpenOption.READ, StandardOpenOption.WRITE)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据
fileChannel.read(buffer);
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
// 写入数据
buffer.clear();
buffer.put("Hello, NIO!".getBytes());
buffer.flip();
fileChannel.write(buffer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
synchronized 和volatile 关键字的区别
synchronized
- 作用:确保同一时刻只有一个线程可以访问被同步的代码段或方法。
- 实现机制:通过内置锁机制实现,通常使用重量级锁。
- 内存可见性:保证了内存可见性,但主要是通过锁机制实现的。
volatile
- 作用:确保变量的写操作会立即反映到主内存中,而读操作总是从主内存读取最新的值。
- 实现机制:通过内存屏障实现内存可见性。
- 内存可见性:直接保证了内存可见性,而不提供锁机制。
主要区别
- 同步性:
synchronized提供了同步机制,而volatile不提供同步。 - 内存可见性:
volatile保证内存可见性,而synchronized也保证内存可见性但通过锁实现。 - 原子性:
synchronized保证复合操作的原子性,而volatile不保证复合操作的原子性。 - 性能影响:
volatile通常比synchronized更高效,因为它不涉及锁的获取和释放。
synchronized与Lock的区别
synchronized
- 语法糖:
synchronized是 Java 中的关键字,使用起来更简洁。 - 锁获取与释放:隐式获取和释放锁,无需手动管理。
- 不可中断:默认情况下,
synchronized锁是不可中断的。 - 重入性:默认支持重入,即允许同一个线程多次获取同一把锁。
Lock
- 显式锁:通过
java.util.concurrent.locks.Lock接口实现,需要显式获取和释放锁。 - 锁获取与释放:需要手动调用
lock()和unlock()方法来获取和释放锁。 - 可中断:支持可中断的锁获取,可以通过
tryLock()方法尝试获取锁。 - 公平锁与非公平锁:支持公平锁和非公平锁,可以通过构造函数指定。
- 重入性:支持重入,如
ReentrantLock类。
主要区别
- 显式与隐式:
Lock需要显式管理锁的获取和释放,而synchronized是隐式的。 - 灵活性:
Lock提供了更多的灵活性,如可中断的锁获取和公平锁选项。 - 可重入性:两者都支持可重入性,但
Lock提供了更高级的控制。 - 性能:在某些情况下,
Lock可以提供更好的性能,尤其是在需要更细粒度控制的情况下。
示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private final Lock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// 执行代码
} finally {
lock.unlock();
}
}
}
ReentrantLock 、synchronized和volatile比较
ReentrantLock
- 特点:
- 是一个可重入的互斥锁,实现了
Lock接口。 - 支持公平锁和非公平锁两种模式。
- 提供了比
synchronized更高级的锁定机制,如可中断的锁获取、限时等待等。 - 锁的获取和释放需要显式调用
lock()和unlock()方法。
- 是一个可重入的互斥锁,实现了
- 应用场景:
- 当需要更细粒度的锁控制时,如需要中断等待锁的线程或在一定时间内尝试获取锁。
- 当需要实现公平锁时,即按照请求锁的顺序来获取锁。
- 当需要更高的性能时,因为
ReentrantLock在某些情况下可以提供比synchronized更好的性能。
synchronized
- 特点:
- 是 Java 关键字,用于实现代码块或方法的同步。
- 默认是非公平的可重入锁,且没有提供获取锁的高级控制。
- 锁的获取和释放是隐式的,不需要显式调用方法。
- 适用于简单的同步需求。
- 应用场景:
- 当需要简单同步代码块或方法时。
- 当需要保持代码简洁时,不需要显式管理锁的获取和释放。
- 当不需要高级锁控制功能时。
volatile
- 特点:
- 用于标记一个变量,确保该变量的写操作会立即反映到主内存中,而读操作总是从主内存中读取最新的值。
- 不提供同步机制,仅提供内存可见性保证。
- 不保证复合操作的原子性。
- 应用场景:
- 当需要确保变量的内存可见性时。
- 当需要进行线程间的简单通信时,如状态标记。
- 当需要避免指令重排序时。
总结
- 互斥性:
ReentrantLock和synchronized提供了互斥访问的能力,而volatile不提供互斥性。 - 内存可见性:
volatile提供了内存可见性保证,而ReentrantLock和synchronized也保证了内存可见性,但主要是通过锁机制实现的。 - 原子性:
ReentrantLock和synchronized保证复合操作的原子性,而volatile不保证复合操作的原子性。 - 性能:在某些情况下,
ReentrantLock可以提供更好的性能,尤其是在需要更细粒度控制的情况下。
Java 中能创建 volatile 数组吗?
能, Java 中可以创建 volatile 类型数组, 不过只是一个指向数组的引用, 而不是整个数组。我的意思是,如果改变引用指向的数组,将会受到 volatile 的保护, 但是如果多个线程同时改变数组的元素, volatile 标示符就不能起到之前的保护作用了。
volatile 能使得一个非原子操作变成原子操作吗?
一个典型的例子是在类中有一个 long 类型的成员变量。如果你知道该成员变量会被多个线程访问,如计数器、价格等,你最好是将其设置为 volatile。为什么? 因为 Java 中读取 long 类型变量不是原子的, 需要分成两步, 如果一个线程正在修改该 long 变量的值, 另一个线程可能只能看到该值的一半( 前 32 位) 。但是对一个 volatile 型的 long 或 double 变量的读写是原子。
volatile 修饰符的有过什么实践?
一种实践是用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。double 和 long 都是 64 位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,但 Java 中volatile 型的 long 或 double 变量的读写是原子的。volatile 修复符的另一个作用是提供内存屏障( memory barrier), 例如在分布式框架中的应用。简单的说,就是当你写一个 volatile 变量之前, Java 内存模型会插入一个写屏障( write barrier), 读一个 volatile 变量之前, 会插入一个读屏障( read barrier)。意思就是说,在你写一个volatile 域时,能保证任何线程都能看到你写的值,同时,
在写之前, 也能保证任何数值的更新对所有线程是可见的, 因为内存屏障会将其他所有写的值更新到缓存。
volatile 类型变量提供什么保证?
volatile 变量提供顺序和可见性保证,例如,JVM 或者 JIT 为了获得更好的性能会对语句重排序,但是 volatile 类型变量即使在没有同步块的情况下赋值也不会与其他语句重排序。 volatile 提供 happens-before 的保证, 确保一个线程的修改能对其他线程是可见的。某些情况下,volatile 还能提供原子性,如读 64 位数据类型,像 long 和 double 都不是原子的,但 volatile 类型的 double 和long 就是原子的。
Java 中,编写多线程程序的时候你会遵循哪些最佳实践?
答案:编写 Java 多线程程序的最佳实践:
线程命名:为线程设置有意义的名称,便于调试和监控:
Thread thread = new Thread(() -> { /* 任务逻辑 */ }, "DataLoaderThread");最小化同步范围:只对关键代码块同步,避免锁粒度过大:
public void process() { // 非关键代码 synchronized (this) { // 关键代码(如共享资源操作) } }优先使用
volatile:对于简单的内存可见性问题,使用volatile替代重量级锁:private volatile boolean shutdownFlag = false;使用高级并发工具:
BlockingQueue:实现生产者 - 消费者模式。ExecutorService:管理线程池,避免手动创建线程。CountDownLatch、CyclicBarrier:协调线程同步。
优先使用并发集合:
ConcurrentHashMap:替代synchronized Map。CopyOnWriteArrayList:适用于读多写少的场景。
避免死锁:
- 按固定顺序获取锁。
- 使用带超时的锁(如
Lock.tryLock())。
资源释放:
- 使用
try-with-resources确保线程安全关闭资源。 - 在线程池关闭时调用
shutdown()和awaitTermination()。
- 使用
说出至少 5 点在 Java 中使用线程的最佳实践。
答案:Java 线程使用的最佳实践:
线程命名:为线程设置有意义的名称,便于调试和监控:
Thread thread = new Thread(task, "OrderProcessorThread");使用线程池:通过
ExecutorService管理线程,避免手动创建线程:ExecutorService executor = Executors.newFixedThreadPool(10); executor.submit(() -> { /* 任务逻辑 */ });实现
Runnable或Callable:将任务与线程分离,提高代码复用性:class MyTask implements Runnable { @Override public void run() { /* 任务逻辑 */ } }处理异常:在线程中捕获异常,避免线程意外终止:
Thread.setDefaultUncaughtExceptionHandler((t, e) -> { logger.error("Thread {} terminated with exception: {}", t.getName(), e); });资源管理:确保线程执行完毕后释放资源,可使用
try-with-resources:executor.submit(() -> { try (InputStream is = new FileInputStream("data.txt")) { // 使用资源 } catch (IOException e) { e.printStackTrace(); } });线程中断处理:正确响应中断信号,优雅终止线程:
while (!Thread.currentThread().isInterrupted()) { // 任务逻辑 }避免使用
stop()和suspend():这些方法已过时,可能导致资源泄漏或死锁。线程池关闭:在应用退出时,正确关闭线程池:
executor.shutdown(); try { if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { executor.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); }
10 个线程和 2 个线程的同步代码,哪个更容易写?
从写代码的角度来说, 两者的复杂度是相同的, 因为同步代码与线程数量是相互独立的。但是同步策略的选择依赖于线程的数量, 因为越多的线程意味着更大的竞争, 所以你需要利用同步技术, 如锁分离, 这要求更复杂的代码和专业知识。
你是如何调用 wait()方法的?使用 if 块还是循环?为什么?
wait() 方法应该在循环调用, 因为当线程获取到 CPU 开始执行的时候, 其他条件可能还没有满足, 所以在处理前, 循环检测条件是否满足会更好。下面是一段标准的使用 wait 和 notify 方法的代码:
// The standard idiom for using the wait method synchronized (obj) {
while (condition does not hold)
obj.wait(); // (Releases lock, and reacquires on wakeup) ...
// Perform action appropriate to condition
}
参见 [Effective Java]第 69 条, 获取更多关于为什么应该在循环中来调用 wait 方法的内容。
什么是多线程环境下的伪共享( false sharing)?
伪共享是多线程系统( 每个处理器有自己的局部缓存) 中一个众所周知的性能问题。伪共享发生在不同处理器的上的线程对变量的修改依赖于相同的缓存行。
伪共享问题很难被发现, 因为线程可能访问完全不同的全局变量, 内存中却碰巧在很相近的位置上。如其他诸多的并发问题, 避免伪共享的最基本方式是仔细审查代码, 根据缓存行来调整你的数据结构。
什么是 Busy spin?我们为什么要使用它?
Busy spin 是一种在不释放 CPU 的基础上等待事件的技术。它经常用于避免丢失 CPU 缓存中的数据( 如果线程先暂停, 之后在其他 CPU 上运行就会丢失) 。所以, 如果你的工作要求低延迟, 并且你的线程目前没有任何顺序, 这样你就可以通过循环检测队列中的新消息来代替调用 sleep() 或 wait() 方法。它唯一的好处就是你只需等待很短的时间,如几微秒或几纳秒。LMAX 分布式框架是一个高性能线程间通信的库,该库有一个 BusySpinWaitStrategy 类就是基于这个概念实现的, 使用 busy spin 循环 EventProcessors 等待屏障。
Java 中怎么获取一份线程 dump 文件?
在 Linux 下,你可以通过命令 kill -3 PID ( Java 进程的进程 ID)来获取 Java 应用的 dump 文件。在 Windows 下, 你可以按下 Ctrl + Break 来获取。这样 JVM 就会将线程的 dump 文件打印到标准输出或错误文件中,它可能打印在控制台或者日志文件中,具体位置依赖应用的配置。如果你使用 Tomcat。
Swing 是线程安全的?
不是, Swing 不是线程安全的。你不能通过任何线程来更新 Swing 组件, 如JTable、 JList 或 JPanel, 事实上, 它们只能通过 GUI 或 AWT 线程来更新。这就是为什么 Swing 提供 invokeAndWait() 和 invokeLater() 方法来获取其他线程的 GUI 更新请求。这些方法将更新请求放入 AWT 的线程队列中, 可以一直等待, 也可以通过异步更新直接返回结果。你也可以在参考答案中查看和学习到更详细的内容。
什么是线程局部变量?
线程局部变量是局限于线程内部的变量, 属于线程自身所有, 不在多个线程间共享。Java提供 ThreadLocal 类来支持线程局部变量, 是一种实现线程安全的方式。但是在管理环境下( 如 web 服务器) 使用线程局部变量的时候要特别小心, 在这种情况下, 工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放, Java 应用就存在内存泄露的风险。
Java 中 sleep 方法和 wait 方法的区别?
虽然两者都是用来暂停当前运行的线程, 但是 sleep() 实际上只是短暂停顿, 因为它不会释放锁,而 wait() 意味着条件等待,这就是为什么该方法要释放锁,因为只有这样, 其他等待的线程才能在满足条件时获取到该锁。
什么是不可变对象(immutable object)?Java 中怎么创建一个不可变对象?
不可变对象指对象一旦被创建,状态就不能再改变。任何修改都会创建一个新的对象,如 String、Integer及其它包装类。
如何在Java中写出Immutable的类?
要写出这样的类,需要遵循以下几个原则:
1)immutable对象的状态在创建之后就不能发生改变,任何对它的改变都应该产生一个新的对象。
2)Immutable类的所有的属性都应该是final的。
3)对象必须被正确的创建,比如: 对象引用在对象创建过程中不能泄露(leak)。
4)对象应该是final的,以此来限制子类继承父类,以避免子类改变了父类的immutable特性。
5)如果类中包含mutable类对象,那么返回给客户端的时候,返回该对象的一个拷贝,而不是该对象本身(该条可以归为第一条中的一个特例)
我们能创建一个包含可变对象的不可变对象吗?
是的, 我们是可以创建一个包含可变对象的不可变对象的, 你只需要谨慎一点, 不要共享可变对象的引用就可以了, 如果需要变化时, 就返回原对象的一个拷贝。最常见的例子就是对象中包含一个日期对象的引用。
Java 中应该使用什么数据类型来代表价格?
如果不是特别关心内存和性能的话, 使用 BigDecimal, 否则使用预定义精度的 double 类型。
Java 中怎样将 bytes 转换为 long 类型?
String接收bytes的构造器转成String,再Long.parseLong
怎么将 byte 转换为 String?
可以使用 String 接收 byte[] 参数的构造器来进行转换,需要注意的点是要使用的正确的编码, 否则会使用平台默认编码, 这个编码可能跟原来的编码相同, 也可能不同。
我们能将 int 强制转换为 byte 类型的变量吗?如果该值大于 byte 类型的范围,将会出现什么现象?
是的,我们可以做强制转换,但是 Java 中 int 是 32 位的,而 byte 是 位的,所以,如果强制转化是,int 类型的高 24 位将会被丢弃, byte 类型的范围是从 -128 到128 。
哪个类包含 clone 方法?是 Cloneable 还是 Object?
java.lang.Cloneable 是一个标示性接口, 不包含任何方法, clone 方法在 object 类中定义。并且需要知道 clone() 方法是一个本地方法,这意味着它是由c 或 c++ 或 其他本地语言实现的。
Java 中 ++ 操作符是线程安全的吗?
答案: 不是线程安全的操作。它涉及到多个指令, 如读取变量值, 增加, 然后存储回内存, 这个过程可能会出现多个线程交差。
多线程的出现是要解决什么问题的? 本质什么?
CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:
- CPU 增加了缓存,以均衡与内存的速度差异;// 导致可见性问题
- 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;// 导致原子性问题
- 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致有序性问题
Java是怎么解决并发问题的?
Java 内存模型是个很复杂的规范,具体看Java 内存模型详解。
理解的第一个维度:核心知识点
JMM本质上可以理解为,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括:
- volatile、synchronized 和 final 三个关键字
- Happens-Before 规则
理解的第二个维度:可见性,有序性,原子性
- 原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。 请分析以下哪些操作是原子性操作:
x = 10; //语句1: 直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中
y = x; //语句2: 包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
x++; //语句3: x++包括3个操作:读取x的值,进行加1操作,写入新的值。
x = x + 1; //语句4: 同语句3
上面4个语句只有语句1的操作具备原子性。
也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
- 可见性
Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
- 有序性
在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。当然JMM是通过Happens-Before 规则来保证有序性的。
线程安全有哪些实现思路?
- 互斥同步
synchronized 和 ReentrantLock。
- 非阻塞同步
互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。
- CAS
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略: 先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。
乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是: 比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
- AtomicInteger
J.U.C 包里面的整数原子类 AtomicInteger,其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作。
- 无同步方案
要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。
- 栈封闭
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。
- 线程本地存储(Thread Local Storage)
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
如何理解并发和并行的区别?
并发是指一个处理器同时处理多个任务。

并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。

线程有哪几种状态? 分别说明从一种状态到另一种状态转变有哪些方式?

- 新建(New)
创建后尚未启动。
- 可运行(Runnable)
可能正在运行,也可能正在等待 CPU 时间片。
包含了操作系统线程状态中的 Running 和 Ready。
- 阻塞(Blocking)
等待获取一个排它锁,如果其线程释放了锁就会结束此状态。
- 无限期等待(Waiting)
等待其它线程显式地唤醒,否则不会被分配 CPU 时间片。
| 进入方法 | 退出方法 |
|---|---|
| 没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
| 没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
| LockSupport.park() 方法 | - |
- 限期等待(Timed Waiting)
无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒。
调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁。而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
| 进入方法 | 退出方法 |
|---|---|
| Thread.sleep() 方法 | 时间结束 |
| 设置了 Timeout 参数的 Object.wait() 方法 | 时间结束 / Object.notify() / Object.notifyAll() |
| 设置了 Timeout 参数的 Thread.join() 方法 | 时间结束 / 被调用的线程执行完毕 |
| LockSupport.parkNanos() 方法 | - |
| LockSupport.parkUntil() 方法 | - |
- 死亡(Terminated)
可以是线程结束任务之后自己结束,或者产生了异常而结束。
通常线程有哪几种使用方式?
有三种使用线程的方法:
- 实现 Runnable 接口;
- 实现 Callable 接口;
- 继承 Thread 类。
实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
基础线程机制有哪些?
- Executor
Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。
主要有三种 Executor:
- CachedThreadPool: 一个任务创建一个线程;
- FixedThreadPool: 所有任务只能使用固定大小的线程;
- SingleThreadExecutor: 相当于大小为 1 的 FixedThreadPool。
- Daemon
java 中的线程分为两种:守护线程(Daemon)和用户线程(User)。
守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
main() 属于非守护线程。使用 setDaemon() 方法将一个线程设置为守护线程。
任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bool on); true 则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon() 必须在 Thread.start()之前调用,否则运行时会抛出异常。
两者的区别:
唯一的区别是判断虚拟机(JVM)何时离开,Daemon 是为其他线程提供服务,如果全部的 User Thread 已经撤离,Daemon 没有可服务的线程,JVM 撤离。也可以理解为守护线 程是JVM 自动创建的线程(但不一定),用户线程是程序创建的线程;比如JVM 的垃 圾回收线程是一个守护线程,当所有线程已经撤离,不再产生垃圾,守护线程自然就没 事可干了,当垃圾回收线程是Java 虚拟机上仅剩的线程时,Java 虚拟机会自动离开。
扩展:
Thread Dump 打印出来的线程信息,含有daemon 字样的线程即为守护进程, 可能会有:服务守护进程、编译守护进程、windows 下的监听Ctrl+break 的守护进程、 Finalizer 守护进程、引用处理守护进程、GC 守护进程。
- sleep()
Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。
sleep() 可能会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
- yield()
对静态方法 Thread.yield() 的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。
线程的中断方式有哪些?
一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。
- InterruptedException
通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。
对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。
public class InterruptExample {
private static class MyThread1 extends Thread {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new MyThread1();
thread1.start();
thread1.interrupt();
System.out.println("Main run");
}
}
Main run
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at InterruptExample.lambda$main$0(InterruptExample.java:5)
at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
- interrupted()
如果一个线程的 run() 方法执行一个无限循环,并且没有执行 sleep() 等会抛出 InterruptedException 的操作,那么调用线程的 interrupt() 方法就无法使线程提前结束。
但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。
- Executor 的中断操作
调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。
线程的互斥同步方式有哪些? 如何比较和选择?
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。
1. 锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
2. 性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
3. 等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,而 synchronized 不行。
4. 公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
5. 锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。
线程之间有哪些协作方式?
当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。
- join()
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先于 b 线程的输出。
public class JoinExample {
private class A extends Thread {
@Override
public void run() {
System.out.println("A");
}
}
private class B extends Thread {
private A a;
B(A a) {
this.a = a;
}
@Override
public void run() {
try {
a.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("B");
}
}
public void test() {
A a = new A();
B b = new B(a);
b.start();
a.start();
}
}
public static void main(String[] args) {
JoinExample example = new JoinExample();
example.test();
}
A
B
- wait() notify() notifyAll()
调用 wait() 使得线程等待某个条件满足,线程在等待时会被挂起,当其他线程的运行使得这个条件满足时,其它线程会调用 notify() 或者 notifyAll() 来唤醒挂起的线程。
它们都属于 Object 的一部分,而不属于 Thread。
只能用在同步方法或者同步控制块中使用,否则会在运行时抛出 IllegalMonitorStateExeception。
使用 wait() 挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
wait() 和 sleep() 的区别
wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
wait() 会释放锁,sleep() 不会。
await() signal() signalAll()
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式,await() 可以指定等待的条件,因此更加灵活。
Thread 类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别?
sleep() 方法( 休眠) 是线程类( Thread) 的静态方法, 调用此方法会让当前线程暂停执行指定的时间, 将执行机会( CPU) 让给其他线程, 但是对象的锁依然保持,因此休眠时间结束后会自动恢复( 线程回到就绪状态,请参考第 66 题中的线程状态转换图)。 wait()是 Object 类的方法, 调用对象的 wait() 方法导致当前线程放弃对象的锁( 线程暂停执行), 进入对象的等待池( wait pool), 只有调用对象的 notify()方法(或 notifyAll() 方法)时才能唤醒等待池中的线程进入等锁池( lock pool), 如果线程重新获得对象的锁就可以进入就绪状态。 补充: 可能不少人对什么是进程, 什么是线程还比较模糊, 对于为什么需要多线程编程也不是特别理解。简单的说: 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动, 是操作系统进行资源分配和调度的一个独立单位; 线程是进程的一个实体,是 CPU 调度和分派的基本单位, 是比进程更小的能独立运行的基本单位。线程的划分尺度小于进程, 这使得多线程程序的并发性高; 进程在执行时通常拥有独立的内存单元,而线程之间可以共享内存。使用多线程的编程通常能够带来更好的性能和用户体验, 但是多线程的程序对于其他程序是不友好的, 因为它可能占用了更多的 CPU 资源。当然, 也不是线程越多, 程序的性能就越好, 因为线程之间的调度和切换也会浪费 CPU 时间。时下很时髦的 Node.js 就采用了单线程异步 I/O 的工作模式。
线程的 sleep()方法和yield()方法有什么区别?
① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会; yield()方法只会给相同优先级或更高优先级的线程以运行的机会; ② 线程执行 sleep()方法后转入阻塞( blocked) 状态, 而执行 yield()方法后转入就绪( ready) 状态; ③ sleep()方法声明抛出 InterruptedException, 而 yield()方法没有声明任何异常; ④ sleep()方法比 yield()方法( 跟操作系统 CPU 调度相关)具有更好的可移植性。
当一个线程进入一个对象的 synchronized 方法A 之后,其它线程是否可进入此对象的synchronized 方法B?
不能。其它线程只能访问该对象的非同步方法, 同步方法则不能进入。因为非静态方法上的 synchronized 修饰符要求执行方法时要获得对象的锁,如果已经进入A方法说明对象锁已经被取走, 那么试图进入 B 方法的线程就只能在等锁池(注意不是等待池哦) 中等待对象的锁。
请说出与线程同步以及线程调度相关的方法。
wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁; sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用 此方法要处理 InterruptedException 异常; notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关; notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
提示:关于 Java 多线程和并发编程的问题,建议大家看我的另一篇文章《 关于 Java并发编程的总结和思考》。 补充: Java 5 通过 Lock 接口提供了显式的锁机制( explicit lock), 增强了灵活性以及对线程的协调。 Lock 接口中定义了加锁( lock())和解锁( unlock())的方法,同时还提供了 newCondition() 方法来产生用于线程之间通信的Condition 对象; 此外, Java 5 还提供了信号量机制( semaphore), 信号量可以用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问之前, 线程必须得到信号量的许可( 调用 Semaphore 对象的 acquire() 方法);在完成对资源的访问后,线程必须向信号量归还许可( 调用 Semaphore 对象的 release() 方法)。
编写多线程程序有几种实现方式?
Java 5 以前实现多线程有两种实现方法: 一种是继承 Thread 类; 另一种是实现 Runnable 接口 。两种方式都要通过重写 run() 方法来定义线程的行为, 推荐使用后者, 因为 Java 中的继承是单继承, 一个类有一个父类, 如果继承了 Thread 类就无法再继承其他类了, 显然使用 Runnable 接口更为灵活。 补充:Java 5 以后创建线程还有第三种方式:实现 Callable 接口,该接口中的 call 方法可以在线程执行结束时产生一个返回值, 代码如下所示:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future;
class MyTask implements Callable<Integer> {
private int upperBounds;
public MyTask(int upperBounds){
this.upperBounds = upperBounds;
}
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 1; i <= upperBounds; i++){
sum += i;
}
return sum;
}
}
class Test {
public static void main(String[] args) throws Exception {
List<Future<Integer>> list = new ArrayList<>(); ExecutorService service = Executors.newFixedThreadPool(10); for(int i = 0; i < 10; i++) {
list.add(service.submit(new MyTask((int) (Math.random() * 100))));
}
int sum = 0;
for(Future<Integer> future : list) {
// while(!future.isDone()) ;
sum += future.get();
}
System.out.println(sum);
}
}
synchronized 关键字的用法?
synchronized 关键字可以将对象或者方法标记为同步,以实现对对象和方法的互斥访问, 可以用 synchronized( 对象 ) { … } 定义同步代码块, 或者在声明方法时将 synchronized 作为方法的修饰符。在第 60 题的例子中已经展示了 synchronized 关键字的用法。
举例说明同步和异步。
如果系统中存在临界资源( 资源数量少于竞争资源的线程数量的资源), 例如正在写的数据以后可能被另一个线程读到, 或者正在读的数据可能已经被另一个线程写过了, 那么这些数据就必须进行同步存取( 数据库操作中的排他锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法, 并且不希望让程序等待方法的返回时, 就应该使用异步编程, 在很多情况下采用异步途径往往更有效率。事实上, 所谓的同步就是指阻塞式操作, 而异步就是非阻塞式操作。
启动一个线程是调用 run()还是 start()方法?
启动一个线程是调用 start()方法, 使线程所代表的虚拟处理机处于可运行状态, 这意味着它可以由 JVM 调度并执行, 这并不意味着线程就会立即运行。run()方法是线程启动后要进行回调( callback) 的方法。
什么是线程池(thread pool)?
在面向对象编程中, 创建和销毁对象是很费时间的, 因为创建一个对象要获取内存资源或者其它更多资源。在 Java 中更是如此, 虚拟机将试图跟踪每一个对象, 以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数, 特别是一些很耗资源的对象创建和销毁, 这就是” 池化资源” 技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池( 容器) 中, 需要的时候从池中获取线程不用自行创建, 使用完毕不需要销毁线程而是放回池中, 从而减少创建和销毁线程对象的开销。 Java 5+ 中的 Executor 接口定义一个执行线程的工具 。 它的子类型即线程池接口是ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下, 因此在工具类 Executors 面提供了一些静态工厂方法, 生成一些常用的线程池, 如下所示:
newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。 newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。 newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM) 能够创建的最大线程大小。 newScheduledThreadPool:创建一个大小无限的线程池。此线程池支 持定时以及周期性执行任务的需求。 newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。
第 60 题的例子中演示了通过 Executors 工具类创建线程池并使用线程池执行线程的代码。如果希望在服务器上使用线程池, 强烈建议使用 newFixedThreadPool 方法来创建线程池, 这样能获得更好的性能。
线程的基本状态以及状态之间的关系?
说明: 其中 Running 表示运行状态, Runnable 表示就绪状态( 万事俱备, 只欠CPU),Blocked 表示阻塞状态,阻塞状态又有多种情况,可能是因为调用 wait() 方法进入等待池, 也可能是执行同步方法或同步代码块进入等锁池, 或者是调用了 sleep()方法或 join()方法等待休眠或其他线程结束,或是因为发生了 I/O 中断。
简述synchronized 和 java.util.concurrent.locks.Lock 的异同?
Lock 是 Java 5 以后引入的新的 API, 和关键字 synchronized 相比主要相同点: Lock 能完成 synchronized 所实现的所有功能; 主要不同点: Lock 有比 synchronized 更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。 synchronized 会自动释放锁, 而Lock 一定要求程序员手工释放, 并且最好在 finally 块中释放( 这是释放外部资源的最好的地方)。
在java 中守护线程和本地线程区别?
java 中的线程分为两种: 守护线程( Daemon) 和用户线程( User)。
任何线程都可以设置为守护线程和用户线程,通过方法 Thread.setDaemon(bool on); true 则把该线程设置为守护线程,反之则为用户线程。Thread.setDaemon() 必须在Thread.start()之前调用, 否则运行时会抛出异常。
两者的区别:
唯一的区别是判断虚拟机(JVM)何时离开,Daemon 是为其他线程提供服务,如果全部的 User Thread 已经撤离, Daemon 没有可服务的线程, JVM 撤离。也可以理解为守护线程是 JVM 自动创建的线程( 但不一定), 用户线程是程序创建的线程; 比如 JVM 的垃圾回收线程是一个守护线程, 当所有线程已经撤离, 不再产生垃圾, 守护线程自然就没事可干了, 当垃圾回收线程是 Java 虚拟机上仅剩的线程时, Java 虚拟机会自动离开。
扩展: Thread Dump 打印出来的线程信息, 含有 daemon 字样的线程即为守护进程,可能会有:服务守护进程、编译守护进程、windows 下的监听 Ctrl+break 的守护进程、 Finalizer 守护进程、引用处理守护进程、GC 守护进程。
线程与进程的区别?
进程是操作系统分配资源的最小单元, 线程是操作系统调度的最小单元。一个程序至少有一个进程,一个进程至少有一个线程。
什么是多线程中的上下文切换?
多线程会共同使用一组计算机上的 CPU,而线程数大于给程序分配的 CPU 数量时,为了让各个线程都有执行的机会,就需要轮转使用 CPU。不同的线程切换使用 CPU 发生的切换数据等就是上下文切换。
死锁与活锁的区别,死锁与饥饿的区别?
死锁: 是指两个或两个以上的进程( 或线程) 在执行过程中, 因争夺资源而造成的一种互相等待的现象, 若无外力作用, 它们都将无法推进下去。
产生死锁的必要条件:
1、互斥条件: 所谓互斥就是进程在某一时间内独占资源。
2、请求与保持条件: 一个进程因请求资源而阻塞时, 对已获得的资源保持不放。
3、不剥夺条件:进程已获得资源, 在末使用完之前, 不能强行剥夺。
4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
活锁: 任务或者执行者没有被阻塞, 由于某些条件没有满足, 导致一直重复尝试, 失败, 尝试, 失败。
活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“ 活”, 而处于死锁的实体表现为等待; 活锁有可能自行解开, 死锁则不能。
饥饿: 一个或者多个线程因为种种原因无法获得所需要的资源, 导致一直无法执行的状态。
Java 中导致饥饿的原因:
1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。
2、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前 持续地对该同步块进行访问。
3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法), 因为其他线程总是被持续地获得唤醒。
Java 中用到的线程调度算法是什么?
采用时间片轮转的方式。可以设置线程的优先级, 会映射到下层的系统上面的优先级上,如非特别需要, 尽量不要用, 防止线程饥饿。
什么是线程组,为什么在Java 中不推荐使用?
ThreadGroup 类,可以把线程归属到某一个线程组中,线程组中可以有线程对象, 也可以有线程组, 组中还可以有线程, 这样的组织结构有点类似于树的形式。
为什么不推荐使用? 因为使用有很多的安全隐患吧, 没有具体追究, 如果需要使用,推荐使用线程池
为什么使用Executor 框架?
每次执行任务创建线程 new Thread()比较消耗性能, 创建一个线程是比较耗时、耗资源的。
调用 new Thread()创建的线程缺乏管理,被称为野线程,而且可以无限制的创建, 线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪, 还有线程之间的频繁交替也会消耗很多系统资源。
接使用 new Thread() 启动的线程不利于扩展, 比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。
在Java 中Executor 和Executors 的区别?
Executors 工具类的不同方法按照我们的需求创建了不同的线程池, 来满足业务的需求
Executor 接口对象能执行我们的线程任务。
ExecutorService 接口继承了 Executor 接口并进行了扩展, 提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值。
使用 ThreadPoolExecutor 可以创建自定义线程池。
Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用 get()方法获取计算的结果。
什么是原子操作?在 Java Concurrency API 中有哪些原子类(atomic classes)?
原子操作( atomic operation) 意为” 不可被中断的一个或一系列操作” 。
处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。
在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。 CAS 操作— — Compare & Set,或是 Compare & Swap,现在几乎所有的 CPU 指令都支持 CAS的原子操作。
原子操作是指一个不受其他操作影响的操作任务单元。原子操作是在多线程环境
下避免数据不一致必须的手段。
int++并不是一个原子操作,所以当一个线程读取它的值并加 1 时,另外一个线程有可能
会读到之前的值, 这就会引发错误。
为了解决这个问题,必须保证增加操作是原子的,在 JDK1.5 之前我们可以使用同步技术来做到这一点。到 JDK1.5, java.util.concurrent.atomic 包提供了 int 和long 类型的原子包装类, 它们可以自动的保证对于他们的操作是原子的并且不需要使用同步。
java.util.concurrent 这个包里面提供了一组原子类。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时, 具有排他性, 即当某个线程进入方法, 执行其中的指令时, 不会被其他线程打断, 而别的线程就像自旋锁一样, 一直等到该方法执行完成, 才由 JVM 从等待队列中选择一个另一个线程进入, 这只是一种逻辑上的理解。
原子类: AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference 原子
数组: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray 原子属性
更新器: AtomicLongFieldUpdater, AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
解决 ABA 问题的原子类: AtomicMarkableReference( 通过引入一个 boolean 来反映中间有没有变过), AtomicStampedReference( 通过引入一个 int 来累加来反映中间有没有变过)
Java Concurrency API 中的Lock 接口(Lock interface) 是什么?对比同步它有什么优势?
Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。
他们允许更灵活的结构, 可以具有完全不同的性质, 并且可以支持多个相关类的条件对象。
它的优势有:
可以使锁更公平
可以使线程在等待锁的时候响应中断
可以让线程尝试获取锁, 并在无法获取锁的时候立即返回或者等待一段时间可以在不同的范围, 以不同的顺序获取和释放锁
整体上来说 Lock 是 synchronized 的扩展版, Lock 提供了无条件的、可轮询的(tryLock方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的 (newCondition 方法)锁操作。另外 Lock 的实现类基本都支持非公平锁(默认)和公平锁, synchronized 只支持非公平锁, 当然, 在大部分情况下, 非公平锁是高效的选择。
什么是 Executors 框架?
Executor 框架是一个根据一组执行策略调用, 调度, 执行和控制的异步任务的框架。
无限制的创建线程会引起应用程序内存溢出。所以创建一个线程池是个更好的的解决方案, 因为可以限制线程的数量并且可以回收再利用这些线程。利用 Executors 框架可以非常方便的创建一个线程池。
什么是阻塞队列?阻塞队列的实现原理是什么?如何使用阻塞队列来实现生产者-消费者模型?
阻塞队列( BlockingQueue) 是一个支持两个附加操作的队列。 这两个附加的操作是: 在队列为空时, 获取元素的线程会等待队列变为非空。当队列满时, 存储元素的线程会等待队列可用。 阻塞队列常用于生产者和消费者的场景, 生产者是往队列里添加元素的线程, 消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器, 而消费者也只从容器里拿元素。 JDK7 提供了 7 个阻塞队列。分别是: ArrayBlockingQueue : 一个由数组结构组成的有界阻塞队列。 LinkedBlockingQueue : 一个由链表结构组成的有界阻塞队列。 PriorityBlockingQueue : 一个支持优先级排序的无界阻塞队列。 DelayQueue: 一个使用优先级队列实现的无界阻塞队列。 SynchronousQueue: 一个不存储元素的阻塞队列。 LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。 LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。 Java 5 之前实现同步存取时, 可以使用普通的一个集合, 然后在使用线程的协作和线程同步可以实现生产者, 消费者模式, 主要的技术就是用好, wait ,notify,notifyAll,sychronized 这些关键字。而在 java 5 之后, 可以使用阻塞队列 来实现, 此方式大大简少了代码量, 使得多线程编程更加容易, 安全方面也有保障。 BlockingQueue 接口是 Queue 的子接口, 它的主要用途并不是作为容器, 而是作为线 程同步的的工具, 因此他具有一个很明显的特性, 当生产者线程试图向 BlockingQueue 放入元素时, 如果队列已满, 则线程被阻塞, 当消费者线程试图从中取 出一个元素时, 如果队列为空, 则该线程会被阻塞, 正是因为它所具有这个特性, 所以 在程序中多个线程交替向 BlockingQueue 中放入元素, 取出元素, 它可以很好的控制线 程之间的通信。 阻塞队列使用最经典的场景就是 socket 客户端数据的读取和解析,读取数据的线程不断将数据放入队列, 然后解析线程不断从队列取数据解析。
什么是 Callable 和Future?
Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果, 并且无法抛出返回结果的异常, 而 Callable 功能更强大一些, 被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。 可以认为是带有回调的 Runnable。 Future 接口表示异步任务,是还没有完成的任务给出的未来结果。所以说 Callable 用于产生结果, Future 用于获取结果。
什么是 FutureTask?使用 ExecutorService 启动任务。
在 Java 并发程序中 FutureTask 表示一个可以取消的异步运算。它有启动和取消运算、查询运算是否完成和取回运算结果等方法。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是调用了 Runnable 接口所以它可以提交给Executor 来执行。
什么是并发容器的实现?
何为同步容器:可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法, 它们将会串行执行。 比如 Vector , Hashtable ,以及 Collections.synchronizedSet, synchronizedList 等方法返回的容器。 可以通过查看 Vector, Hashtable 等这些同步容器的实现代码, 可以看到这些容器实现线程安全的方式就是将它们的状态封装起来, 并在需要同步的方法上加上关键字synchronized。 并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性, 例如在 ConcurrentHashMap 中采用了一种粒度更细的加锁机制, 可以称为分段锁, 在这种锁机制下, 允许任意数量的读线程并发地访问 map, 并且执行读操作的线程和写操作的线程也可以并发的访问 map, 同时允许一定数量的写操作线程并发地修改 map, 所以它可以在并发环境下实现更高的吞吐量。
多线程同步和互斥有几种实现方法,都是什么?
线程同步是指线程之间所具有的一种制约关系, 一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待, 直到消息到达时才被唤醒。线程互斥是指对于共享的进程系统资源, 在各单个线程访问时的排它性。当有若干个线程都要使用某一共享资源时, 任何时刻最多只允许一个线程去使用, 其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。 线程间的同步方法大体可分为两类: 用户模式和内核模式。顾名思义, 内核模式就是指利用系统内核对象的单一性来进行同步, 使用时需要切换内核态与用户态, 而用户模式就是不需要切换到内核态, 只在用户态完成操作。 用户模式下的方法有: 原子操作( 例如一个单一的全局变量), 临界区。内核模式下 的方法有: 事件, 信号量, 互斥量。
什么是竞争条件?你怎样发现和解决竞争?
当多个进程都企图对共享数据进行某种处理, 而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞争条件( race condition)。
你将如何使用 thread dump?你将如何分析 Thread dump?
新建状态( New) 用 new 语句创建的线程处于新建状态,此时它和其他 Java 对象一样,仅仅在堆区中被分配了内存。 就绪状态( Runnable)
当一个线程对象创建后,其他线程调用它的 start()方法,该线程就进入就绪状态, Java 虚拟机会为它创建方法调用栈和程序计数器。处于这个状态的线程位于可运行池中, 等待获得 CPU 的使用权。 运行状态( Running) 处于这个状态的线程占用 CPU, 执行程序代码。只有处于就绪状态的线程才有机会转到运行状态。 阻塞状态( Blocked) 阻塞状态是指线程因为某些原因放弃 CPU, 暂时停止运行。当线程处于阻塞状态时, Java 虚拟机不会给线程分配CPU。直到线程重新进入就绪状态, 它才有机会转到运行状态。 阻塞状态可分为以下 3 种: 位于对象等待池中的阻塞状态( Blocked in object’ s wait pool) : 当线程处于运行状态时, 如果执行了某个对象的 wait()方法, Java 虚拟机就会把线程放到这个对象的等待池中, 这涉及到“ 线程通信” 的内容。 位于对象锁池中的阻塞状态( Blocked in object’ s lock pool) : 当线程处于运行状态时, 试图获得某个对象的同步锁时, 如果该对象的同步锁已经被其他线程占用, Java 虚拟机就会把这个线程放到这个对象的锁池中, 这涉及到“ 线程同步” 的内容。 其他阻塞状态( Otherwise Blocked) : 当前线程执行了 sleep()方法,或者调用了其他线程的 join()方法,或者发出了 I/O 请求时, 就会进入这个状态。
死亡状态( Dead) 当线程退出 run()方法时, 就进入死亡状态, 该线程结束生命周期。
我们运行之前的那个死锁代码 SimpleDeadLock.java, 然后尝试输出信息(
/* 时间,jvm 信息 */
2017-11-01 17:36:28
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.144-b01 mixed mode):
/* 线程名称:DestroyJavaVM
编号:#13
优先级:5
系统优先级:0
jvm 内部线程 id:0x0000000001c88800
对应系统线程 id(NativeThread ID) 0x1c18
线程状态: waiting on condition [0x0000000000000000] (等待某个条件)线程详细状态:java.lang.Thread.State: RUNNABLE 及 之 后 所 有 */ "DestroyJavaVM" #13 prio=5 os prio=0 tid=0x0000000001c88800 nid=0x1c18 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Thread-1" #12 prio=5 os_prio=0 tid=0x0000000018d49000 nid=0x17b8 waiting for monitor entry [0x0000000019d7f000] /* 线程状态:阻塞(在对象同步上)
代码位置:at
com.leo.interview.SimpleDeadLock$B.run(SimpleDeadLock.java:56)
等待锁:0x00000000d629b4d8
已 经 获 得 锁 :0x00000000d629b4e8*/ java.lang.Thread.State: BLOCKED (on object monitor) at
com.leo.interview.SimpleDeadLock$B.run(SimpleDeadLock.java:56)
- waiting to lock <0x00000000d629b4d8> (a java.lang.Object)
- locked <0x00000000d629b4e8> (a java.lang.Object)
"Thread-0" #11 prio=5 os_prio=0 tid=0x0000000018d44000 nid=0x1ebc waiting for monitor entry [0x000000001907f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at
com.leo.interview.SimpleDeadLock$A.run(SimpleDeadLock.java:34)
- waiting to lock <0x00000000d629b4e8> (a java.lang.Object)
- locked <0x00000000d629b4d8> (a java.lang.Object)
"Service Thread" #10 daemon prio=9 os_prio=0 tid=0x0000000018ca5000 nid=0x1264 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread2" #9 daemon prio=9 os_prio=2 tid=0x0000000018c46000 nid=0xb8c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" #8 daemon prio=9 os_prio=2 tid=0x0000000018be4800 nid=0x1db4 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #7 daemon prio=9 os_prio=2 tid=0x0000000018be3800 nid=0x810 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Monitor Ctrl-Break" #6 daemon prio=5 os_prio=0 tid=0x0000000018bcc800 nid=0x1c24 runnable [0x00000000193ce000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at
java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java: 78)
- locked <0x00000000d632b928> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x00000000d632b928> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at
com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:6
4)
"Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x0000000017781800 nid=0x524 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x000000001778f800 nid=0x1b08 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001776a800 nid=0xdac in Object.wait() [0x0000000018b6f000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d6108ec8> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143) - locked <0x00000000d6108ec8> (a
java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
"Reference Handler" #2 daemon prio=10 os_prio= tid=0x0000000017723800 nid=0x1670 in Object.wait() [0x00000000189ef000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000000d6106b68> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x00000000d6106b68> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Thread" os prio=2 tid=0x000000001771b800 nid=0x604 runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000000001c9d800 nid=0x9f0 runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000000001c9f000 nid=0x154c runnable
"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x0000000001ca0800 nid=0xcd0 runnable
"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x0000000001ca2000 nid=0x1e58 runnable
"VM Periodic Task Thread" os_prio=2 tid=0x0000000018c5a0 nid=0x1b58 waiting on condition
JNI global references: 33
/* 此处可以看待死锁的相关信息! */
Found one Java-level deadlock:
============================= "Thread-1":
waiting to lock monitor 0x0000000017729fc8 (object 0x00000000d629b4d8, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x0000000017727738 (object 0x00000000d629b4e8, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above: ============================================== =====
"Thread-1":
at
com.leo.interview.SimpleDeadLock$B.run(SimpleDeadLock.java:56)
- waiting to lock <0x00000000d629b4d8> (a java.lang.Object)
- locked <0x00000000d629b4e8> (a java.lang.Object)
"Thread-0":
at
com.leo.interview.SimpleDeadLock$A.run(SimpleDeadLock.java:34)
- waiting to lock <0x00000000d629b4e8> (a java.lang.Object)
- locked <0x00000000d629b4d8> (a java.lang.Object)
Found 1 deadlock.
/* 内存使用状况,详情得看 JVM 方面的书 */ Heap
PSYoungGen total 37888K, used 4590K [0x00000000d6100000, 0x00000000d8b00000, 0x0000000100000000)
eden space 32768K, 14% used
[0x00000000d6100000,0x00000000d657b968,0x00000000d8100000)
from space 5120K, 0% used
[0x00000000d8600000,0x00000000d8600000,0x00000000d8b00000)
to space 5120K, 0% used
[0x00000000d8100000,0x00000000d8100000,0x00000000d8600000)
ParOldGen total 86016K, used 0K [0x0000000082200000, 0x0000000087600000, 0x00000000d6100000)
object space 86016K, 0% used
[0x0000000082200000,0x0000000082200000,0x0000000087600000)
Metaspace used 3474K, capacity 4500K, committed 4864K,
reserved 1056768K
class space used 382K, capacity 388K, committed 512K, reserved
1048576K
为什么我们调用 start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
当你调用 start()方法时你将创建新的线程, 并且执行在 run()方法里的代码。 但是如果你直接调用 run()方法,它不会创建新的线程也不会执行调用线程的代码,只会把 run 方法当作普通方法去执行。
Java 中你怎样唤醒一个阻塞的线程?
在 Java 发展史上曾经使用 suspend()、resume()方法对于线程进行阻塞唤醒,但随之出现很多问题, 比较典型的还是死锁问题。 解决方案可以使用以对象为目标的阻塞, 即利用 Object 类的 wait()和 notify()方法实现线程阻塞。 首先,wait、notify 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞,阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对象阻塞的线程, 但它需要重新获取改对象的锁, 直到获取成功才能往下执行; 其次,wait、notify 方法必须在 synchronized 块或方法中被调用, 并且要保证同步块或方法的锁对象与调用 wait、notify 方法的对象是同一个, 如此一来在调用 wait 之前当前线程就已经成功获取某对象的锁,执行wait 阻塞后当前线程就将之前获取的对象锁释放。
在 Java 中CycliBarriar 和CountdownLatch 有什么区别?
CyclicBarrier 可以重复使用, 而 CountdownLatch 不能重复使用。 Java 的 concurrent 包里面的 CountDownLatch 其实可以把它看作一个计数器, 只不过这个计数器的操作是原子操作, 同时只能有一个线程去操作这个计数器, 也就是同时只能有一个线程去减这个计数器里面的值。 你可以向 CountDownLatch 对象设置一个初始的数字作为计数值,任何调用这个对象上 的 await()方法都会阻塞,直到这个计数器的计数值被其他的线程减为 0 为止。 所以在当前计数到达零之前, await 方法会一直受阻塞。之后, 会释放所有等待的线程,await 的所有后续调用都将立即返回。这种现象只出现一次— — 计数无法被重置。如果需要重置计数, 请考虑使用 CyclicBarrier。 CountDownLatch 的一个非常典型的应用场景是: 有一个任务想要往下执行, 但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个 CountDownLatch 对象的 await() 方法, 其他的任务执行完自己的任务后调用同一个 CountDownLatch 对象上的 countDown()方法, 这个调用 await()方法的任务将一直阻塞等待, 直到这个 CountDownLatch 对象的计数值减到 0 为止。 CyclicBarrier 一个同步辅助类, 它允许一组线程互相等待, 直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待, 此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用, 所以称它为循环 的 barrier。
什么是不可变对象,它对写并发应用有什么帮助?
不可变对象(Immutable Objects)即对象一旦被创建它的状态( 对象的数据,也即对象属性值) 就不能改变, 反之即为可变对象(Mutable Objects)。 不可变对象的类即为不可变类(Immutable Class)。Java 平台类库中包含许多不可变类, 如 String、基本类型的包装类、BigInteger 和 BigDecimal 等。 不可变对象天生是线程安全的。它们的常量( 域) 是在构造函数中创建的。既然它们的状态无法修改, 这些常量永远不会变。 不可变对象永远是线程安全的。 只有满足如下状态, 一个对象才是不可变的; 它的状态不能在创建后再被修改; 所有域都是 final 类型; 并且, 它被正确创建( 创建期间没有发生 this 引用的逸出)。
什么是多线程中的上下文切换?
在上下文切换过程中, CPU 会停止处理当前运行的程序, 并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看, 上下文切换有点像我们同时阅读几本书, 在来回切换书本的同时我们需要记住每本书当前读到的页码。在程序中, 上下文切换过程中的“ 页码” 信息是保存在进程控制块( PCB) 中的。PCB 还经常被称作“ 切换桢” ( switchframe)。“ 页码” 信息会一直保存到 CPU 的内存中, 直到他们被再次使用。上下文切换是存储和恢复 CPU 状态的过程, 它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。
Java 中用到的线程调度算法是什么?
计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令.所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务.在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU 的使用权. 有两种调度模型: 分时调度模型和抢占式调度模型。 分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。 java 虚拟机采用抢占式调度模型, 是指优先让可运行池中优先级高的线程占用CPU, 如果可运行池中的线程优先级相同, 那么就随机选择一个线程, 使其占用CPU。处于运行状态的线程会一直运行, 直至它不得不放弃 CPU。
什么是线程组,为什么在 Java 中不推荐使用?
线程组和线程池是两个不同的概念, 他们的作用完全不同, 前者是为了方便线程的管理,后者是为了管理线程的生命周期, 复用线程, 减少创建销毁线程的开销。
为什么使用 Executor 框架比使用应用创建和管理线程好?
为什么要使用 Executor 线程池框架 1、每次执行任务创建线程 new Thread()比较消耗性能, 创建一个线程是比较耗时、耗资源的。 2、调用 new Thread()创建的线程缺乏管理, 被称为野线程, 而且可以无限制的创建, 线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪, 还有线程之间的频繁交替也会消耗很多系统资源。 3、直接使用 new Thread() 启动的线程不利于扩展, 比如定时执行、定期执行、定时定期执行、线程中断等都不便实现。 使用 Executor 线程池框架的优点 1、能复用已存在并空闲的线程从而减少线程对象的创建从而减少了消亡线程的开 销。 2、可有效控制最大并发线程数, 提高系统资源使用率, 同时避免过多资源竞争。 3、框 中已经有定时、定期、单线程、并发数控制等功能。 综上所述使用线程池框架 Executor 能更好的管理线程、提供系统资源使用率。
java 中有几种方法可以实现一个线程?
继 承 Thread 类 实现 Runnable 接口 实现 Callable 接口, 需要实现的是 call() 方法
如何停止一个正在运行的线程?
使用共享变量的方式 在这种方式中, 之所以引入共享变量, 是因为该变量可以被多个执行相同任务的线程用来作为是否中断的信号, 通知中断线程的执行。 使用 interrupt 方法终止线程 如果一个线程由于等待某些事件的发生而被阻塞, 又该怎样停止该线程呢? 这种情况经 常会发生, 比如当一个线程由于需要等候键盘输入而被阻塞, 或者调用Thread.join()方 法, 或者 Thread.sleep()方法, 在网络中调用 ServerSocket.accept()方法,或者调用了 DatagramSocket.receive()方法时,都有可能 导致线程阻塞, 使线程处于处于不可运行状态时, 即使主程序中将该线程的共享变量设 置为 true, 但该线程此时根本无法检查循环标志, 当然也就无法立即中断。这里我们给 出的建议是,不要使用 stop()方法,而是使用 Thread 提供的interrupt()方法,因为该方法 虽然不会中断一个正在运行的线程,但是它可以使一 个被阻塞的线程抛出一个中断异常, 从而使线程提前结束阻塞状态, 退出堵塞代码。
notify()和 notifyAll()有什么区别?
当一个线程进入 wait 之后,就必须等其他线程 notify/notifyall,使用 notifyall,可以唤醒所有处于 wait 状态的线程,使其重新进入锁的争夺队列中,而 notify 只能唤醒一个。 如果没把握, 建议 notifyAll, 防止 notigy 因为信号丢失而造成程序异常。
什么是 Daemon 线程?它有什么意义?
所谓后台(daemon)线程, 是指在程序运行的时候在后台提供一种通用服务的线程, 并且这个线程并不属于程序中不可或缺的部分。因此, 当所有的非后台线程结束时, 程序也就终止了, 同时会杀死进程中的所有后台线程。反过来说, 只要有任何非后台线程还在运行, 程序就不会终止。必须在线程启动之前调用setDaemon()方法, 才能把它设置为后台线程。注意: 后台进程在不执行 finally 子句的情况下就会终止其 run()方法。 比如: JVM 的垃圾回收线程就是 Daemon 线程, Finalizer 也是守护线程。
java 如何实现多线程之间的通讯和协作?
中断 和 共享变量
什么是可重入锁(ReentrantLock)?
举例来说明锁的可重入性
public class UnReentrant{
Lock lock = new Lock();
public void outer(){
lock.lock();
inner();
lock.unlock();
}
public void inner(){
lock.lock();
//do something
lock.unlock();
}
}
outer 中调用了 inner,outer 先锁住了 lock,这样 inner 就不能再获取 lock。其实调用 outer 的线程已经获取了 lock 锁, 但是不能在 inner 中重复利用已经获取的锁资源,这种锁即称之为 不可重入可重入就意味着:线程可以进入任何一个它已经拥有的锁所同步着的代码块。 synchronized、ReentrantLock 都是可重入的锁, 可重入锁相对来说简化了并发编程的开发。
当一个线程进入某个对象的一个 synchronized 的实例方法后,其它线程是否可进入此对象的其它方法?
如果其他方法没有 synchronized 的话, 其他线程是可以进入的。 所以要开放一个线程安全的对象时, 得保证每个方法都是线程安全的。
乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
悲观锁: 总是假设最坏的情况, 每次去拿数据的时候都认为别人会修改, 所以每次在拿数据的时候都会上锁, 这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制, 比如行锁, 表锁等, 读锁, 写锁等, 都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁。 乐观锁: 顾名思义, 就是很乐观, 每次去拿数据的时候都认为别人不会修改, 所以不会上锁, 但是在更新的时候会判断一下在此期间别人有没有去更新这个数据, 可以使用版本号等机制。乐观锁适用于多读的应用类型, 这样可以提高吞吐量, 像数据库提供的类似于 write_condition 机制, 其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。 乐观锁的实现方式: 1、使用版本标识来确定读到的数据与提交时的数据是否一致。提交后修改版本标 识, 不一致时可以采取丢弃和再次尝试的策略。 2、 java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 CAS 操 作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A) 和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。 CAS 缺点: 1、ABA 问题: 比如说一个线程 one 从内存位置 V 中取出 A,这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成 A,这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。尽管线 程 one 的 CAS 操作成功,但可能存在潜藏的问题。从 Java1.5 开始 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。 2、循环时间长开销大: 对于资源竞争严重( 线程冲突严重) 的情况, CAS 自旋的概率会比较大, 从而浪费更多的 CPU 资源, 效率低于 synchronized。 3、只能保证一个共享变量的原子操作: 当对一个共享变量执行操作时, 我们可以使用循环 CAS 的方式来保证原子操作, 但是对多个共享变量操作时, 循环 CAS 就无法保证操作的原子性, 这个时候就可以用锁。
SynchronizedMap 和 ConcurrentHashMap 有什么区别?
SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为map。 ConcurrentHashMap 使用分段锁来保证在多线程下的性能。 ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将 hash 表分为 16 个桶, 诸如 get,put,remove 等常用操作只锁当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。 另外 ConcurrentHashMap 使用了一种不同的迭代方式。在这种迭代方式中, 当 iterator 被创建后集合再发生改变就不再是抛出 ConcurrentModificationException , 取而代之的是在改变时 new 新的数据从而 不影响原有的数据 , iterator 完成后再将头指针替换为新的数据 ,这样 iterator 线程可以使用原来老的数据, 而写线程也可以并发的完成改变。
CopyOnWriteArrayList 可以用于什么应用场景?
CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时, 不会抛出 ConcurrentModificationException。 在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地, 使得复制的数组在被修改时, 读取操作可以安全地执行。
- 由于写操作的时候, 需要拷贝数组, 会消耗内存, 如果原数组的内容比较多的情况下, 可能导致 young gc 或者 full gc;
- 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然 CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;
CopyOnWriteArrayList 透露的思想
- 读写分离, 读和写分开
- 最终一致性
- 使用另外开辟空间的思路, 来解决并发冲突
什么叫线程安全?servlet 是线程安全吗?
线程安全是编程中的术语, 指某个函数、函数库在多线程环境中被调用时, 能够正确地处理多个线程之间的共享变量, 使程序功能正确完成。 Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。 Struts2 的 action 是多实例多线程的, 是线程安全的, 每个请求过来都会 new 一个新的 action 分配给这个请求, 请求完成后销毁。 SpringMVC 的 Controller 是线程安全的吗? 不是的,和 Servlet 类似的处理流程。 Struts2 好处是不用考虑线程安全问题; Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。
volatile 有什么用?能否用一句话说明下volatile 的应用场景?
volatile 保证内存可见性和禁止指令重排。 volatile 用于多线程环境下的单次操作(单次读或者单次写)。
为什么代码会重排序?
在执行程序时, 为了提供性能, 处理器和编译器常常会对指令进行重排序, 但是不能随意重排序, 不是你想怎么排序就怎么排序, 它需要满足以下两个条件: 在单线程环境下不能改变程序运行的结果; 存在数据依赖关系的不允许重排序 需要注意的是: 重排序不会影响单线程环境的执行结果, 但是会破坏多线程的执行语义。
在 java 中wait 和sleep 方法的不同?
最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。Wait 通常被用于线程间交互, sleep 通常被用于暂停执行。 直接了解的深入一点吧:
在 Java 中线程的状态一共被分成 6 种:
- 初始态: NEW
创建一个 Thread 对象, 但还未调用 start()启动线程时, 线程处于初始态。
- 运行态: RUNNABLE
在 Java 中, 运行态包括就绪态 和 运行态。
就绪态
该状态下的线程已经获得执行所需的所有资源, 只要 CPU 分配执行权就能运行。所有就绪态的线程存放在就绪队列中。
运行态
获得 CPU 执行权, 正在执行的线程。由于一个 CPU 同一时刻只能执行一条线程, 因此每个 CPU 每个时刻只有一条运行态的线程。
- 阻塞态
当一条正在执行的线程请求某一资源失败时, 就会进入阻塞态。而在 Java 中, 阻塞态专指请求锁失败时进入的状态。由一个阻塞队列存放所有阻塞态的线程。处于阻塞态的线程会不断请求资源, 一旦请求成功, 就会进入就绪队列, 等待执行。PS: 锁、IO、Socket等都资源。
- 等待态
当前线程中调用 wait、join、park 函数时, 当前线程就会进入等待态。也有一个等待队列存放所有等待态的线程。线程处于等待态表示它需要等待其他线程的指示才能继续运行。进入等待态的线程会释放 CPU 执行权, 并释放资源( 如: 锁)
- 超时等待态
当运行中的线程调用 sleep(time)、wait、join、parkNanos、parkUntil 时, 就会进入该状态; 它和等待态一样, 并不是因为请求不到资源, 而是主动进入, 并且进入后需要其他线程唤醒;进入该状态后释放 CPU 执行权 和 占有的资源。与等待态的区别: 到了超时时间后自动进入阻塞队列, 开始竞争锁。
- 终止态
线程执行结束后的状态。
注意:
wait()方法会释放 CPU 执行权 和 占有的锁。
sleep(long)方法仅释放 CPU 使用权,锁仍然占用;线程被放入超时等待队列,与yield相比, 它会使线程较长时间得不到运行。
yield()方法仅释放 CPU 执行权, 锁仍然占用, 线程会被放入就绪队列, 会在短时间内再次执行。
wait 和 notify 必须配套使用, 即必须使用同一把锁调用。
wait 和 notify 必须放在一个同步块中调用 wait 和 notify 的对象必须是他们所处同步块的锁对象。
用 Java 实现阻塞队列
参考 java 中的阻塞队列的内容吧, 直接实现有点烦:
一个线程运行时发生异常会怎样?
如果异常没有被捕获该线程将会停止执行。Thread.UncaughtExceptionHandler 是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候 JVM 会使用Thread.getUncaughtExceptionHandler() 来查询线程的 UncaughtExceptionHandler 并将线程和异常作为参数传递给handler 的 uncaughtException()方法进行处理。
如何在两个线程间共享数据?
在两个线程间共享变量即可实现共享。 一般来说, 共享变量要求变量本身是线程安全的, 然后在线程内使用的时候, 如果有对共享变量的复合操作, 那么也得保证复合操作的线程安全性。
Java 中 notify 和 notifyAll 有什么区别?
notify() 方法不能唤醒某个具体的线程, 所以只有一个线程在等待的时候它才有用武之地。而 notifyAll()唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行。
为什么 wait, notify 和 notifyAll 这些方法不在 thread 类里面?
一个很明显的原因是 JAVA 提供的锁是对象级的而不是线程级的, 每个对象都有锁,通过线程获得。由于 wait,notify 和 notifyAll 都是锁级别的操作,所以把他们定义在 Object 类中因为锁属于对象。
什么是 ThreadLocal 变量?
ThreadLocal 是 Java 里一种特殊的变量。每个线程都有一个 ThreadLocal 就是每个线程都拥有了自己独立的一个变量, 竞争条件被彻底消除了。它是为创建代价高昂的对象获取线程安全的好方法, 比如你可以用 ThreadLocal 让 SimpleDateFormat 变成线程安全的, 因为那个类创建代价高昂且每次调用都需要创建不同的实例所以不值得在局部范围使用它, 如果为每个线程提供一个自己
独有的变量拷贝, 将大大提高效率。首先, 通过复用减少了代价高昂的对象的创建个数。 其次, 你在没有使用高代价的同步或者不变性的情况下获得了线程安全。
Java 中 interrupted 和 isInterrupted 方法的区别?
interrupt interrupt 方法用于中断线程。调用该方法的线程的状态为将被置为”中断”状态。注意:线程中断仅仅是置线程的中断状态位, 不会停止线程。需要用户自己去监视线程的状态为并做处理。支持线程中断的方法( 也就是线程中断后会抛出 interruptedException 的方法) 就是在监视线程的中断状态, 一旦线程的中断状态被置为“ 中断状态” , 就会抛出中断异常。 interrupted 查询当前线程的中断状态, 并且清除原状态。如果一个线程被中断了, 第一次调用 interrupted 则返回 true, 第二次和后面的就返回 false 了。 isInterrupted 仅仅是查询当前线程的中断状态
为什么 wait 和notify 方法要在同步块中调用?
Java API 强制要求这样做, 如果你不这么做, 你的代码会抛出 IllegalMonitorStateException 异常。还有一个原因是为了避免 wait 和 notify 之间产生竞态条件。
为什么你应该在循环中检查等待条件?
处于等待状态的线程可能会收到错误警报和伪唤醒, 如果不在循环中检查等待条件, 程序就会在没有满足结束条件的情况下退出。
Java 中的同步集合与并发集合有什么区别?
同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合, 不过并发集合的可扩展性更高。在 Java1.5 之前程序员们只有同步集合来用且在多线程并发的时候会导致争用, 阻碍了系统的扩展性。Java5 介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高 了可扩展性。
什么是线程池? 为什么要使用它?
创建线程要花费昂贵的资源和时间, 如果任务来了才创建线程那么响应时间会变长, 而且一个进程能创建的线程数有限。为了避免这些问题, 在程序启动的时候就创建若干线程来响应处理, 它们被称为线程池, 里面的线程叫工作线程。从 JDK1.5 开始, Java API 提供了 Executor 框架让你可以创建不同的线程池 。
怎么检测一个线程是否拥有锁?
在 java.lang.Thread 中有一个方法叫 holdsLock(), 它返回 true 如果当且仅当当前线程拥有某个具体对象的锁。
你如何在 Java 中获取线程堆栈?
kill -3 [java pid] 不会在当前终端输出, 它会输出到代码执行的或指定的地方去。比如, kill -3 tomcat pid, 输出堆栈到 log 目录下。 Jstack [java pid] 这个比较简单, 在当前终端显示, 也可以重定向到指定文件中。 -JvisualVM: Thread Dump 不做说明, 打开 JvisualVM 后, 都是界面操作, 过程还是很简单的。55、 JVM 中哪个参数是用来控制线程的栈堆栈小的? -Xss 每个线程的栈大小
Thread 类中的yield 方法有什么作用?
使当前线程从执行状态( 运行状态) 变为可执行态( 就绪状态)。 当前线程到了就绪状态, 那么接下来哪个线程会从就绪状态变成执行状态呢? 可能是当前线程, 也可能是其他线程, 看系统的分配了。
Java 中 ConcurrentHashMap 的并发度是什么?
ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的, 它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16, 这样在多线程情况下就能避免争用。 在 JDK8 后, 它摒弃了 Segment( 锁段) 的概念, 而是启用了一种全新的方式实现,利用 CAS 算法。同时加入了更多的辅助变量来提高并发度,具体内容还是查看源码吧。
Java 中 Semaphore 是什么?
Java 中的 Semaphore 是一种新的同步类, 它是一个计数信号。从概念上讲, 从概念上讲, 信号量维护了一个许可集合。如有必要, 在许可可用前会阻塞每一个acquire(),然后再获取该许可。每个 release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数, 并采取相应的行动。信号量常常用于多线程的代码中, 比如数据库连接池。
Java 线程池中submit() 和 execute()方法有什么区别?
两个方法都可以向线程池提交任务,execute()方法的返回类型是 void,它定义在 Executor 接口中。 而 submit()方法可以返回持有计算结果的 Future 对象, 它定义在 ExecutorService 接口中, 它扩展了Executor 接口, 其它线程池类像 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 都有这些方法。
什么是阻塞式方法?
阻塞式方法是指程序会一直等待该方法完成期间不做其他事情, ServerSocket 的accept()方法就是一直等待客户端连接。这里的阻塞是指调用结果返回之前,当前 线程会被挂起,直到得到结果之后才会返回。此外, 还有异步和非阻塞式方法在任务完成前就返回。
Java 中的 ReadWriteLock 是什么?
读写锁是用来提升并发程序性能的锁分离技术的成果。
volatile 变量和 atomic 变量有什么不同?
Volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量那么 count++ 操作就不是原子性的。 而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一, 其它数据类型和引用变量也可以进行相似操作。
可以直接调用 Thread 类的run ()方法么?
当然可以。但是如果我们调用了 Thread 的 run()方法, 它的行为就会和普通的方法一样, 会在当前线程中执行。为了在新的线程中执行我们的代码, 必须使用Thread.start()方法。
如何让正在运行的线程暂停一段时间?
我们可以使用 Thread 类的 Sleep()方法让线程暂停一段时间。需要注意的是, 这并不会让线程终止,一旦从休眠中唤醒线程,线程的状态将会被改变为 Runnable, 并且根据线程调度, 它将得到执行。
你对线程优先级的理解是什么?
每一个线程都是有优先级的, 一般来说, 高优先级的线程在运行时会具有优先权, 但这依赖于线程调度的实现, 这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级, 但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程优先级是一个 int 变量(从 1-10), 1 代表最低优先级, 10 代表最高优先级。 java 的线程优先级调度会委托给操作系统去处理, 所以与具体的操作系统优先级有关,如非特别需要, 一般无需设置线程优先级。
什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing )?
线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它, 它的执行便依赖于线程调度器的实现。 同上一个问题, 线程调度并不受到 Java 虚拟机控制, 所以由应用程序来控制它是更好的选择( 也就是说不要让你的程序依赖于线程的优先级)。 时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。
你如何确保 main()方法所在的线程是Java 程序最后结束的线程?
我们可以使用 Thread 类的 join()方法来确保所有程序创建的线程在 main()方法退出前结束。
线程之间是如何通信的?
当线程间是可以共享资源时, 线程间通信是协调它们的重要的手段。 Object 类中 wait()\notify()\notifyAll() 方法可以用于线程间通信关于资源的锁的状态。
为什么线程通信的方法 wait(), notify()和notifyAll()被定义在Object 类里?
Java 的每个对象中都有一个锁(monitor,也可以成为监视器) 并且 wait(),notify()等方法用于等待对象的锁或者通知其他线程对象的监视器可用。在 Java 的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是 Object 类的一部分, 这样 Java 的每一个类都有用于线程间通信的基本方法。
为什么 wait(), notify()和notifyAll ()必须在同步方法或者同步块中被调用?
当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify() 方法。同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁, 以便其他在等待的线程就可以得到这个对象锁。由于所有的这些方法都需要线程持有对象的锁, 这样就只能通过同步来实现, 所以他们只能在同步方法或者同步块中被调用。
为什么 Thread 类的sleep()和 yield ()方法是静态的?
Thread 类的 sleep()和 yield()方法将在当前正在执行的线程上运行。所以在其他处于等待状态的线程上调用这些方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作, 并避免程序员错误的认为可以在其他非运行线程调用这些方法。
如何确保线程安全?
在 Java 中可以有很多方法来保证线程安全— — 同步, 使用原子类(atomic concurrent classes), 实现并发锁, 使用 volatile 关键字, 使用不变类和线程安全类。
同步方法和同步块,哪个是更好的选择?
同步块是更好的选择, 因为它不会锁住整个对象( 当然你也可以让它锁住整个对象)。同步方法会锁住整个对象, 哪怕这个类中有多个不相关联的同步块, 这通常会导致他们停止执行并需要等待获得这个对象上的锁。 同步块更要符合开放调用的原则, 只在需要锁住的代码块锁住相应的对象, 这样从侧面来说也可以避免死锁。
如何创建守护线程?
使用 Thread 类的 setDaemon(true)方法可以将线程设置为守护线程, 需要注意的是,需要在调用 start()方法前调用这个方法, 否则会抛出 IllegalThreadStateException 异常。
什么是 Java Timer 类?如何创建一个有特定时间间隔的任务?
java.util.Timer 是一个工具类, 可以用于安排一个线程在未来的某个特定时间执行。 Timer 类可以用安排一次性任务或者周期任务。 java.util.TimerTask 是一个实现了 Runnable 接口的抽象类, 我们需要去继承这个类来创建我们自己的定时任务并使用 Timer 去安排它的执行。
并发编程三要素?
原子性 原子性指的是一个或者多个操作, 要么全部执行并且在执行的过程中不被其他操作打断,要么就全部都不执行。
可见性 可见性指多个线程操作一个共享变量时, 其中一个线程对变量进行修改后, 其他线程可以立即看到修改的结果。
有序性 有序性, 即程序的执行顺序按照代码的先后顺序来执行。
实现可见性的方法有哪些?
synchronized 或者 Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存, 实现可见性。
多线程的价值?
- 发挥多核 CPU 的优势
多线程,可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的,采用多线程的方式去同时完成几件事情而不互相干扰。
- 防止阻塞
从程序运行效率的角度来看, 单核 CPU 不但不会发挥出多线程的优势, 反而会因为在单核 CPU 上运行多线程导致线程上下文的切换, 而降低程序整体的效率。但是单核 CPU 我们还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU 使用单线程, 那么只要这个线程阻塞了, 比方说远程读取某个数据吧, 对端迟迟未返回又没有设置超时时间, 那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题, 多条线程同时运行, 哪怕一条线程的代码执行读取数据阻塞, 也不会影响其它任务的执行。
- 便于建模
这是另外一个没有这么明显的优点了。假设有一个大的任务 A,单线程编程,那么就要考虑很多, 建立整个程序模型比较麻烦。但是如果把这个大的任务 A 分解成几个小任务,任务 B、任务 C、任务 D,分别建立程序模型,并通过多线程分别运行这几个任务, 那就简单很多了。
创建线程的有哪些方式?
- 继承 Thread 类创建线程类
- 通过 Runnable 接口创建线程类
- 通过 Callable 和 Future 创建线程
- 通过线程池创建
创建线程的三种方式的对比?
采用实现 Runnable、Callable 接口的方式创建多线程。优势是: 线程类只是实现了 Runnable 接口或 Callable 接口, 还可以继承其他类。 在这种方式下, 多个线程可以共享同一个 target 对象, 所以非常适合多个相同线程来处理同一份资源的情况, 从而可以将 CPU、代码和数据分开, 形成清晰的模型, 较好地体现了面向对象的思想。 劣势是: 编程稍微复杂,如果要访问当前线程,则必须使用 Thread.currentThread() 方法。
使用继承 Thread 类的方式创建多线程优 势是: 编写简单, 如果需要访问当前线程, 则无需使用 Thread.currentThread()方法, 直接使用 this 即可获得当前线程。 劣势是: 线程类已经继承了 Thread 类, 所以不能再继承其他父类。
Runnable 和 Callable 的区别
1、Callable 规定(重写)的方法是 call(),Runnable 规定( 重写)的方法是 run()。 2、Callable 的任务执行后可返回值, 而 Runnable 的任务是不能返回值的。 3、Call 方法可以抛出异常, run 方法不可以。 4、运行 Callable 任务可以拿到一个 Future 对象, 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过 Future 对象可以了解任务执行情况, 可取消任务的执行, 还可获取执行结果。
Java 线程具有五中基本状态
在 Java 中,线程的生命周期包含六种基本状态,而非五种。这六种状态定义在 Thread.State 枚举类中,分别对应线程从创建到销毁的完整过程。以下是详细说明: 线程的六种状态及转换关系
- 新建状态(New)
- 当线程对象被创建(如 new Thread())但尚未调用 start() 方法时,线程处于此状态。
- 此时线程未分配系统资源,仅存在于内存中。
- 就绪状态(Runnable)
- 调用 start() 方法后,线程进入就绪状态。
- 此时线程已获取除 CPU 外的所有资源,等待操作系统调度 CPU 时间片。
- 注意:Java 中 Runnable 状态包含两种情况:一是正在等待 CPU 调度,二是正在运行(实际执行代码),二者统一归为 “就绪状态”(逻辑上的可运行状态)。
- 阻塞状态(Blocked)
- 线程试图获取 synchronized 同步锁时,若锁被其他线程占用,则进入阻塞状态。
- 当锁被释放且当前线程获得锁后,从阻塞状态转为就绪状态。
- 等待状态(Waiting)
- 线程调用无超时参数的等待方法后进入此状态,需被其他线程显式唤醒:
- Object.wait()(需被 Object.notify()/notifyAll() 唤醒)
- Thread.join()(等待目标线程执行完毕)
- LockSupport.park()(需被 LockSupport.unpark() 唤醒)
- 处于等待状态的线程不会主动唤醒,必须依赖其他线程的操作。
- 超时等待状态(Timed Waiting)
- 线程调用带超时参数的等待方法后进入此状态,超时后自动唤醒,或被提前唤醒:
- Thread.sleep(long)(超时后自动唤醒)
- Object.wait(long)(超时自动唤醒或被 notify 唤醒)
- Thread.join(long)(超时自动唤醒或目标线程执行完毕)
- LockSupport.parkNanos()/parkUntil()(超时自动唤醒或被 unpark 唤醒)
- 终止状态(Terminated)
- 线程执行完 run() 方法,或因异常退出 run() 方法后,进入终止状态。
- 此状态下线程无法再被启动(调用 start() 会抛出 IllegalThreadStateException)。 状态转换图(简化)
新建(New)→ 就绪(Runnable)→ 终止(Terminated)
↑ ↓
阻塞(Blocked)←→ 等待(Waiting)/ 超时等待(Timed Waiting)
常见误区:为何容易认为是 “五种状态”? 部分资料可能将 “就绪状态” 和 “运行中” 视为两种状态,或合并 “等待” 和 “超时等待”,从而产生 “五种状态” 的说法。但根据 Java 官方定义(Thread.State 枚举),标准分类为上述六种,其中:
- Runnable 包含 “就绪待调度” 和 “正在运行” 两种情况(逻辑上统一为可运行状态)。
- “等待” 和 “超时等待” 因唤醒机制不同(主动唤醒 vs 超时自动唤醒)被明确分为两种状态。
1、新建状态( New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread(); 2、就绪状态( Runnable) : 当调用线程对象的 start() 方法( t.start();), 线程即进入就绪状态。处于就绪状态的线程, 只是说明此线程已经做好了准备, 随时等待 CPU 调度执行, 并不是说执行了 t.start()此线程立即就会执行; 3、运行状态( Running) : 当 CPU 开始调度处于就绪状态的线程时, 此时线程才得以真正执行, 即进入到运行状态。注: 就 绪状态是进入到运行状态的唯一入口,也就是说, 线程要想进入运行状态执行, 首先必须处于就绪状态中; 4、阻塞状态( Blocked):处于运行状态中的线程由于某种原因,暂时放弃对 CPU 的使用权, 停止执行, 此时进入阻塞状态, 直到其进入到就绪状态, 才 有机会再次被CPU 调用以进入到运行状态。
根据阻塞产生的原因不同, 阻塞状态又可以分为三种: 1、等待阻塞:运行状态中的线程执行 wait()方法,使本线程进入到等待阻塞状态;2、同步阻塞:线程在获取synchronized 同步锁失败(因为锁被其它线程所占用), 它会进入同步阻塞状态; 3、其他阻塞:通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时, 线程重新转入就绪状态。 5、死亡状态( Dead):线程执行完了或者因异常退出了 run() 方法,该线程结束生命周期。
什么是线程池?有哪几种创建方式?
线程池就是提前创建若干个线程, 如果有任务需要处理, 线程池里的线程就会处理任务, 处理完之后线程并不会被销毁, 而是等待下一个任务。由于创建和销毁线程都是消耗系统资源的, 所以当你想要频繁的创建和销毁线程的时候就可以考虑使用线程池来提升系统的性能。 java 提供了一个 java.util.concurrent.Executor 接口的实现用于创建线程池。
四种线程池的创建:
- newCachedThreadPool 创建一个可缓存线程池
- newFixedThreadPool 创建一个定长线程池, 可控制线程最大并发数。
- newScheduledThreadPool 创建一个定长线程池, 支持定时及周期性任务执行。
- newSingleThreadExecutor 创建一个单线程化的线程池, 它只会用唯一的工作线程来执行任务。
线程池的优点?
- 重用存在的线程, 减少对象创建销毁的开销。
- 可有效的控制最大并发线程数, 提高系统资源的使用率,同时避免过多资源竞争, 避免堵塞。
- 提供定时执行、定期执行、单线程、并发数控制等功能。
常用的并发工具类有哪些?
- CountDownLatch
- CyclicBarrier
- Semaphore
- Exchanger
CyclicBarrier 和 CountDownLatch 的区别
- CountDownLatch 简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用 countDown()方法发出通知后, 当前线程才可以继续执行。
- CyclicBarrier 是所有线程都进行等待, 直到所有线程都准备好进入 await()方法之后, 所有线程同时开始执行!
- CountDownLatch 的计数器只能使用一次 。而 CyclicBarrier 的计数器可以使用 reset() 方法重置。所以CyclicBarrier 能处理更为复杂的业务场景, 比如如果计算发生错误, 可以重置计数器, 并让线程们重新执行一次。
- CyclicBarrier 还提供其他有用的方法, 比如 getNumberWaiting 方法可以获得 CyclicBarrier 阻塞的线程数量。isBroken 方法用来知道阻塞的线程是否被中断。如果被中断返回 true, 否则返回 false。
synchronized 的作用?
在 Java 中, synchronized 关键字是用来控制线程同步的, 就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。 synchronized 既可以加在一段代码上, 也可以加在方法上。
volatile 关键字的作用
对于可见性, Java 提供了 volatile 关键字来保证可见性。 当一个共享变量被 volatile 修饰时, 它会保证修改的值会立即被更新到主存, 当有其他线程需要读取时, 它会去内存中读取新值。 从实践角度而言, volatile 的一个重要作用就是和 CAS 结合, 保证了原子性, 详细的可以参见 java.util.concurrent.atomic 包下的类, 比如 AtomicInteger。
什么是 CAS
CAS 是 compare and swap 的缩写, 即我们所说的比较交换。 cas 是一种基于锁的操作, 而且是乐观锁。在 java 中锁分为乐观锁和悲观锁。悲观锁是将资源锁住, 等一个之前获得锁的线程释放锁之后, 下一个线程才可以访
问。而乐观锁采取了一种宽泛的态度, 通过某种方式不加锁来处理资源, 比如通过给记录加 version 来获取数据, 性能较悲观锁有很大的提高。 CAS 操作包含三个操作数 — — 内存位置( V)、预期原值( A) 和新值(B)。如果内存地址里面的值和 A 的值是一样的, 那么就将内存里面的值更新成 B。 CAS 是通过无限循环来获取数据的,若果在第一轮循环中, a 线程获取地址里面的值被 b 线程修改了, 那么 a 线程需要自旋, 到下次循环才有可能机会执行。 java.util.concurrent.atomic 包下的类大多是使用 CAS 操作来实现的 ( AtomicInteger,AtomicBoolean,AtomicLong)。
CAS 的问题
1、CAS 容易造成 ABA 问题 一个线程 a 将数值改成了 b, 接着又改成了 a, 此时 CAS 认为是没有变化, 其实是已经变化过了, 而这个问题的解决方案可以使用版本号标识, 每操作一次 version 加 1。在 java5 中,已经提供了 AtomicStampedReference 来解决问题。2、不能保证代码块的原子性 CAS 机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证 3 个变量共同进行原子性的更新, 就不得不使用 synchronized 了。3、CAS 造成 CPU 利用率增加 之前说过了 CAS 里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。
什么是 Future?
在并发编程中, 我们经常用到非阻塞的模型, 在之前的多线程的三种实现中, 不管是继承 thread 类还是实现 runnable 接口,都无法保证获取到之前的执行结果。通过实现 Callback 接口, 并用 Future 可以来接收多线程的执行结果。 Future 表示一个可能还没有完成的异步任务的结果, 针对这个结果可以添加 Callback 以便在任务执行成功或失败后作出相应的操作。
什么是 AQS
AQS 是 AbustactQueuedSynchronizer 的简称, 它是一个 Java 提高的底层同步工具类, 用一个 int 类型的变量表示同步状态, 并提供了一系列的 CAS 操作来管理这个同步状态。 AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器, 比如我们提到的 ReentrantLock, Semaphore, 其他的诸如ReentrantReadWriteLock, SynchronousQueue, FutureTask 等等皆是基于AQS的。
AQS 支持两种同步方式:
1、独占式 2、共享式
这样方便使用者实现不同类型的同步组件, 独占式如 ReentrantLock, 共享式如Semaphore,CountDownLatch,组合式的如 ReentrantReadWriteLock。总之, AQS 为使用提供了底层支撑, 如何组装实现, 使用者可以自由发挥。
ReadWriteLock 是什么
首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时候有局限。如果使用 ReentrantLock, 可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致, 但这样, 如果线程 C 在读数据、线程 D 也在读数据, 读数据是不会改变数据的, 没有必要加锁, 但是还是加锁了, 降低了程序的性能。因为这个,才诞生了读写锁 ReadWriteLock。ReadWriteLock 是一个读写锁接口, ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写
的分离, 读锁是共享的, 写锁是独占的, 读和读之间不会互斥, 读和写、写和读、写和写之间才会互斥, 提升了读写的性能。
FutureTask 是什么
这个其实前面有提到过,FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类, 可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然, 由于 FutureTask 也是 Runnable 接口的实现类, 所以 FutureTask 也可以放入线程池中。
synchronized 和ReentrantLock 的区别
synchronized 是和 if、else、for、while 一样的关键字, ReentrantLock 是类, 这是二者的本质区别。既然 ReentrantLock 是类, 那么它就提供了比 synchronized 更多更灵活的特性, 可以被继承、可以有方法、可以有各种各样的类变量, ReentrantLock 比 synchronized 的扩展性体现在几点上:
- ReentrantLock 可以对获取锁的等待时间进行设置, 这样就避免了死锁
- ReentrantLock 可以获取各种锁的信息
- ReentrantLock 可以灵活地实现多路通知 另外,二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park方法加锁, synchronized 操作的应该是对象头中 mark word, 这点我不能确定。
什么是乐观锁和悲观锁
乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态, 乐观锁认为竞争不总是会发生, 因此它不需要持有锁, 将比较-替换这两个动作作 为一个原子操作尝试去修改内存中的变量, 如果失败则表示发生冲突, 那么就应该有相应的重试逻辑。
悲观锁:还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状 态, 悲观锁认为竞争总是会发生, 因此每次对某资源进行操作时, 都会持有一个独占的锁, 就像 synchronized, 不管三七二十一, 直接上了锁就操作资源了。
线程 B 怎么知道线程A 修改了变量
- volatile 修饰变量
- synchronized 修饰修改变量的方法
- wait/notify
- while 轮询
synchronized、volatile、CAS 比较
- synchronized 是悲观锁, 属于抢占式, 会引起其他线程阻塞。
- volatile 提供多线程共享变量可见性和禁止指令重排序优化。
- CAS 是基于冲突检测的乐观锁( 非阻塞)
sleep 方法和 wait 方法有什么区别?
这个问题常问,sleep 方法和 wait 方法都可以用来放弃 CPU 一定的时间,不同点在于如果线程持有某个对象的监视器, sleep 方法不会放弃这个对象的监视器, wait 方法会放弃这个对象的监视器
ThreadLocal 是什么?有什么用?
ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射, 各个线程之间的变量互不干扰, 在高并发场景下, 可以实现无状态的调用, 特别适用于各个线程依赖不通的变量值完成操作的场景。简单说 ThreadLocal 就是一种以空间换时间的做法, 在每个 Thread 里面维护了一个以开地址法实现的ThreadLocal.ThreadLocalMap, 把数据进行隔离, 数据不共享, 自然就没有线程安全方面的问题了。
为什么 wait()方法和notify()/notifyAll()方法要在同步块中被调用
这是 JDK 强制的,wait()方法和 notify()/notifyAll()方法在调用前都必须先获得对象的锁
多线程同步有哪几种方法?
Synchronized 关键字, Lock 锁实现, 分布式锁等。
线程的调度策略
线程调度器选择优先级最高的线程运行, 但是, 如果发生以下情况, 就会终止线程的运行:
- 线程体中调用了 yield 方法让出了对 cpu 的占用权利
- 线程体中调用了 sleep 方法使线程进入睡眠状态
- 线程由于 IO 操作受到阻塞
- 另外一个更高优先级线程出现
- 在支持时间片的系统中, 该线程的时间片用完
ConcurrentHashMap 的并发度是什么
ConcurrentHashMap 的并发度就是 segment 的大小, 默认为 16, 这意味着最多同时可以有 16 条线程操作 ConcurrentHashMap, 这也是 ConcurrentHashMap 对 Hashtable 的最大优势, 任何情况下, Hashtable 能同时有两条线程获取 Hashtable 中的数据吗?
Linux 环境下如何查找哪个线程使用CPU 最长
1、获取项目的 pid, jps 或者 ps -ef | grep java, 这个前面有讲过 、 top -H -p pid, 顺序不能改变
Java 死锁以及如何避免?
Java 中的死锁是一种编程情况,其中两个或多个线程被永久阻塞,Java 死锁情况出现至少两个线程和两个或更多资源。 Java 发生死锁的根本原因是: 在申请锁时发生了交叉闭环申请。
锁的原因
1、是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖 的闭环。例如: 线程在获得了锁 A 并且没有释放的情况下去申请锁 B, 这时, 另一个线程已经获得了锁 B,在释放锁 B 之前又要先获得锁 A,因此闭环发生,陷入死锁循环。2、默认的锁申请操作是阻塞的。
所以要避免死锁, 就要在一遇到多个对象锁交叉的情况, 就要仔细审查这几个对象的类中的所有方法, 是否存在着导致锁依赖的环路的可能性。总之是尽量避免在一个同步方法中调用其它对象的延时方法和同步方法。
怎么唤醒一个阻塞的线程
如果线程是因为调用了 wait()、sleep()或者 join()方法而导致的阻塞,可以中断线程, 并且通过抛出 InterruptedException 来唤醒它; 如果线程遇到了 IO 阻塞, 无能为力,因为 IO 是操作系统实现的,Java 代码并没有办法直接接触到操作系统。
不可变对象对多线程有什么帮助
前面有提到过的一个问题, 不可变对象保证了对象的内存可见性, 对不可变对象的读取不需要进行额外的同步手段, 提升了代码执行效率。
什么是多线程的上下文切换
多线程的上下文切换是指 CPU 控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取 CPU 执行权的线程的过程。
如果你提交任务时,线程池队列已满,这时会发生什么
这里区分一下:
如果使用的是无界队列 LinkedBlockingQueue, 也就是无界队列的话, 没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列, 可以无限存放任务
如果使用的是有界队列比如 ArrayBlockingQueue, 任务首先会被添加到 ArrayBlockingQueue 中,ArrayBlockingQueue 满了, 会根据 maximumPoolSize 的值增加线程数量, 如果增加了线程数量还是处理不过来, ArrayBlockingQueue 继续满, 那么则会使用拒绝策略 RejectedExecutionHandler 处理满了的任务, 默认是 AbortPolicy
Java 中用到的线程调度算法是什么
抢占式。一个线程用完 CPU 之后, 操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)?
线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它, 它的执行便依赖于线程调度器的实现。时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配CPU 时间可以基于线程优先级或者线程等待的时间。线程调度并不受到 Java 虚拟机控制, 所以由应用程序来控制它是更好的选择( 也就是说不要让你的程序依赖于线程的优先级)。
什么是自旋
很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作, 因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线 程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁, 再阻塞, 这样可能是一种更好的策略。
Java Concurrency API 中的Lock 接口(Lock interface) 是什么?对比同步它有什么优势?
Lock 接口比同步方法和同步块提供了更具扩展性的锁操作。他们允许更灵活的结构, 可以具有完全不同的性质, 并且可以支持多个相关类的条件对象。它的优势有:
- 可以使锁更公平
- 可以使线程在等待锁的时候响应中断
- 可以让线程尝试获取锁, 并在无法获取锁的时候立即返回或者等待一段时间4、可以在不同的范围, 以不同的顺序获取和释放锁
单例模式的线程安全性
老生常谈的问题了, 首先要说的是单例模式的线程安全意味着: 某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法, 我总结一下:
- 饿汉式单例模式的写法: 线程安全
- 懒汉式单例模式的写法: 非线程安全
- 双检锁单例模式的写法: 线程安全
Semaphore 有什么作用
Semaphore 就是一个信号量,它的作用是限制某段代码块的并发数。Semaphore 有一个构造函数,可以传入一个 int 型整数 n,表示某段代码最多只有 n 个线程可以访问,如果超出了 n,那么请等待,等到某个线程执行完毕这段代码块,下一个
线程再进入。由此可以看出如果 Semaphore 构造函数中传入的 int 型整数 n=1, 相当于变成了一个 synchronized 了。
Executors 类是什么?
Executors 为 Executor, ExecutorService, ScheduledExecutorService, ThreadFactory 和 Callable 类提供了一些工具方法。 Executors 可以用于方便的创建线程池
线程类的构造方法、静态块是被哪个线程调用的
这是一个非常刁钻和狡猾的问题。请记住: 线程类的构造方法、静态块是被 new 这个线程类所在的线程所调用的, 而 run 方法里面的代码才是被线程自身所调用的。 如果说上面的说法让你感到困惑, 那么我举个例子, 假设 Thread2 中 new 了Thread1, main 函数中 new 了 Thread2, 那么
- Thread2 的构造方法、静态块是 main 线程调用的, Thread2 的 run()方法是 Thread2 自己调用的
- Thread1 的构造方法、静态块是 Thread2 调用的, Thread1 的 run()方法是 Thread1 自己调用的
同步方法和同步块,哪个是更好的选择?
同步块, 这意味着同步块之外的代码是异步执行的, 这比同步整个方法更提升代码的效率。请知道一条原则: 同步的范围越小越好。
Java 线程数过多会造成什么异常?
- 线程的生命周期开销非常高
- 消耗过多的 CPU 资源 如果可运行的线程数量多于可用处理器的数量, 那么有线程将会被闲置。大量空闲的线程会占用许多内存, 给垃圾回收器带来压力, 而且大量的线程在竞争 CPU 资源时还将产生其他性能的开销。
- 降低稳定性 JVM 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约, 包括 JVM 的启动参数、 Thread 构造函数中请求栈的大小, 以及底层操作系统对线程的限制等。如果破坏了这些限制, 那么可能抛出 OutOfMemoryError 异常。
在我 Java 程序中,我有三个 socket,我需要多少个线程来处理?
答案:在 Java 程序中处理 socket 所需的线程数取决于应用场景:
- 单线程处理(不推荐):所有 socket 的读写操作在同一个线程中顺序执行。若任一 socket 阻塞,会影响其他 socket 的处理效率。
- 每个 socket 一个线程:每个 socket 的读写操作分配独立线程,实现并发处理。需 3 个线程,适用于连接数少且读写操作可能阻塞的场景。
- 线程池管理:使用线程池(如
Executors.newCachedThreadPool())管理线程,动态分配线程资源,避免创建过多线程导致资源耗尽。 - NIO 非阻塞模式:使用 Java NIO(New IO),单线程通过
Selector管理多个 socket 通道,实现高效并发。此时仅需 1 个线程(或结合少量工作线程)。
解释Java中的线程创建方式
Java中创建线程主要有两种方式:继承Thread类和实现Runnable接口。
- 继承Thread类:创建一个继承自
Thread的子类,并重写其run()方法。在run()方法中定义线程执行的任务。然后创建该子类的对象,并通过start()方法启动线程。
public class MyThread extends Thread {
public void run() {
// 线程执行的代码
}
}
MyThread thread = new MyThread();
thread.start();
- 实现Runnable接口:创建一个实现
Runnable接口的类,并实现其run()方法。然后将该类的实例作为参数传递给Thread类的构造函数,并通过start()方法启动线程。
public class MyRunnable implements Runnable {
public void run() {
// 线程执行的代码
}
}
Thread thread = new Thread(new MyRunnable());
thread.start();
实现Runnable接口的方式更为常见,因为它遵循了单继承的原则,并且可以避免由于Java不支持多重继承而带来的问题。
死锁的四个必要条件
必要条件
- 互斥条件:资源在任意时刻只能被一个进程使用。
- 占有且等待条件:已经占有至少一个资源的进程还在等待其他资源。
- 非抢占条件:已经分配给进程的资源不能被抢占,只能由该进程显式释放。
- 循环等待条件:存在一个进程等待链,链中的每一个进程都在等待下一个进程所占有的资源。
示例
假设我们有两个进程 P1 和 P2,以及两个资源 R1 和 R2。如果 P1 已经持有 R1 并试图获取 R2,同时 P2 已经持有 R2 并试图获取 R1,那么两个进程都会陷入等待状态,从而形成死锁。
怎么避免死锁
方法
- 破坏互斥条件:不太现实,因为很多资源本质上就是独占的。
- 破坏占有且等待条件:使用一次性分配所有需要的资源,或者在请求新资源前释放已拥有的资源。
- 破坏非抢占条件:允许进程抢占资源,例如使用银行家算法等资源分配策略。
- 破坏循环等待条件:
- 资源顺序分配:为资源分配一个全局唯一的顺序号,进程按顺序请求资源。
- 锁顺序:为资源或锁分配一个顺序号,按照顺序获取锁。
示例
如果资源 R1 和 R2 分别被赋予顺序号 1 和 2,那么所有进程在请求资源时必须先请求顺序号小的资源,再请求顺序号大的资源,这样就可以避免循环等待。
对象锁和类锁是否会互相影响
影响
- 对象锁:作用于对象实例上,确保同一时刻只有一个线程可以访问被同步的代码块或方法。
- 类锁:作用于类级别,确保同一时刻只有一个线程可以访问被同步的静态代码块或静态方法。
互相影响
- 如果一个线程获得了某个对象的实例锁,那么其他线程在尝试获取同一对象的实例锁时将会被阻塞。
- 如果一个线程获得了某个类的类锁,那么其他线程在尝试获取同一类的类锁时也将被阻塞。
- 对象锁和类锁是独立的,它们之间不会互相影响。
示例
public class MyClass {
public synchronized void instanceMethod() {
// 同步实例方法
}
public static synchronized void staticMethod() {
// 同步静态方法
}
}
// 线程1
new Thread(() -> {
MyClass instance = new MyClass();
instance.instanceMethod();
}).start();
// 线程2
new Thread(() -> {
MyClass.staticMethod();
}).start();
在这个例子中,线程1尝试获取 MyClass 实例的锁,而线程2尝试获取 MyClass 类的锁。它们之间不会互相影响,因为它们锁定的是不同的资源。
什么是线程池,如何使用?
概述
线程池是一个管理线程的工具,它维护了一组预先创建好的线程,这些线程可以复用,以提高应用程序的响应性和资源利用率。
主要组成部分
- 核心线程数:线程池中始终维持的最小线程数。
- 最大线程数:线程池允许创建的最大线程数。
- 工作队列:当核心线程数不足以处理所有任务时,额外的任务会被放入队列等待执行。
- 拒绝策略:当线程池和工作队列都无法接受更多任务时的处理策略。
使用方法
- 创建线程池:
ExecutorService executor = Executors.newFixedThreadPool(5);
- 提交任务:
executor.submit(() -> {
// 任务执行代码
});
示例
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executor.submit(() -> {
System.out.println("Task executed by " + Thread.currentThread().getName());
});
}
executor.shutdown();
}
}
Java的并发、多线程、线程模型
并发
- 概念:指多个任务在同一时间段内交替执行,但不是真正的并行执行。
- 实现:通过多线程、多进程等方式实现。
多线程
- 概念:指在一个进程中同时执行多个线程。
- 优点:提高了程序的响应性和资源利用率。
- 实现:通过创建线程对象并调用
start()方法来启动线程。
线程模型
- 模型:描述线程如何管理和调度的方式。
- Java 的线程模型:基于线程池的模型,线程池维护了一组预创建的线程,这些线程可以复用。
- 线程池的好处:减少了线程创建和销毁的开销,提高了资源利用率。
示例
public class ThreadModelExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Thread started: " + Thread.currentThread().getName());
});
thread.start();
}
}
谈谈对多线程的理解
理解
- 概念:多线程是指在一个进程中同时执行多个线程的技术。
- 优点:提高了程序的响应性和资源利用率,使得程序能够更好地利用多核处理器。
- 应用场景:
- I/O 密集型应用:在网络通信、文件读写等场景中,多线程可以提高应用程序的响应性。
- 计算密集型应用:在大规模科学计算、图像处理等场景中,多线程可以提高计算效率。
- 挑战:
- 线程安全:多线程环境下需要确保数据的一致性和安全性。
- 死锁和活锁:需要避免线程间因资源竞争而导致的死锁和活锁问题。
- 性能开销:过多的线程会增加调度开销,降低程序性能。
- 最佳实践:
- 合理设计线程模型:根据应用特点选择合适的线程模型,如线程池。
- 同步机制:使用适当的同步机制,如
synchronized、ReentrantLock等,确保线程安全。 - 避免过度同步:尽量减少不必要的同步操作,提高程序性能。
- 线程池:使用线程池管理线程,减少线程创建和销毁的开销。
- 监控和调试:使用工具和框架帮助监控和调试多线程程序。
多线程有什么要注意的问题?
在开发多线程应用时,需要注意以下几个关键问题:
1. 线程安全
- 共享资源保护:确保多个线程访问共享资源时不会产生数据不一致的问题。
- 同步机制:使用
synchronized关键字、ReentrantLock等机制来保证线程安全。
2. 死锁
- 死锁条件:确保不会出现死锁的四个必要条件:互斥条件、占有且等待条件、非抢占条件和循环等待条件。
- 避免策略:采用资源顺序分配、锁顺序等策略来避免死锁。
3. 活锁
- 活锁现象:多个线程在不断尝试执行某种操作,但由于彼此干扰而始终无法完成。
- 解决方案:使用更高级的同步机制,如
Condition来协调线程间的协作。
4. 饥饿
- 资源竞争:部分线程长期得不到执行的机会。
- 公平性:确保所有线程都有机会执行,使用公平锁或轮询策略。
5. 性能问题
- 上下文切换:频繁的线程切换会导致性能下降。
- 线程数量:合理设置线程池中的线程数量,避免过度创建线程。
6. 异常处理
- 捕获异常:确保线程在执行过程中发生的异常能够被妥善处理。
- 守护线程:使用守护线程来处理异常,避免主线程意外终止。
7. 线程生命周期管理
- 线程创建与销毁:合理管理线程的生命周期,避免不必要的线程创建和销毁。
- 线程池:使用线程池来管理线程的创建和销毁,提高性能。
8. 线程通信
- 同步机制:使用条件变量、信号量等机制来协调线程间的通信。
- 共享变量:使用
volatile关键字来确保线程间变量的可见性。
9. 资源泄露
- 资源管理:确保线程使用的资源能够在不再需要时被及时释放。
- 对象生命周期:注意对象的生命周期管理,避免内存泄漏。
10. 测试
- 单元测试:编写针对多线程的单元测试来验证线程安全。
- 压力测试:模拟高并发场景来测试系统的稳定性和性能。
谈谈你对并发编程的理解并举例说明
理解
并发编程是一种编程范式,允许程序在多个执行流(线程、进程)中同时运行,以提高程序的响应性和资源利用率。并发编程的目标是在有限的系统资源下实现最大化的工作负载。
特点
- 并发性:多个任务同时执行。
- 异步性:任务可以不按顺序完成。
- 资源共享:多个执行流共享相同的资源。
示例
假设有一个Web服务器需要处理来自客户端的HTTP请求。这个服务器可以使用并发编程技术来处理这些请求:
- 多线程模型:每当接收到一个新的客户端连接时,服务器创建一个新的线程来处理该连接。
- 线程池模型:服务器预先创建一个固定大小的线程池,每当有新的请求到来时,从线程池中取出一个空闲线程来处理该请求。
谈谈你对多线程同步机制的理解?
概念
多线程同步机制是用来确保多个线程之间正确且安全地共享资源的一组技术和工具。
类型
- 互斥锁:确保同一时刻只有一个线程可以访问共享资源。
- 条件变量:用来协调线程间的活动,允许线程等待特定条件成立。
- 信号量:用于控制对资源的访问次数。
- 原子变量:提供线程安全的变量操作。
- ThreadLocal:为每个线程提供独立的变量副本。
如何保证多线程读写文件的安全?
在多线程环境中读写文件时,需要确保数据的一致性和安全性。以下是一些常用的方法:
1. 文件锁
- 使用
FileLock来确保同一时刻只有一个线程可以写入文件。 - 通过
FileChannel获取FileLock。
2. 同步机制
- 使用
synchronized关键字或ReentrantLock来同步文件的读写操作。
3. 原子操作
- 对于简单的文件操作(如追加一行文本),可以考虑将操作封装成原子操作。
4. 文件复制
- 读取文件时,可以先将文件复制到内存或其他临时位置,然后在内存中进行操作,最后写回到文件。
5. 缓存
- 使用缓存机制来减少对文件的直接操作,比如使用
BufferedInputStream和BufferedOutputStream。
什么是Java中的死锁以及如何避免?
死锁(Deadlock)是多线程编程中的一种情况,当两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的状态,若无外力作用,它们都将无法继续执行。
死锁的条件:
避免死锁的方法:
死锁的避免和解决需要在设计和实现多线程程序时进行仔细的考虑和规划。通过合理的资源管理和线程调度策略,可以有效地避免死锁的发生。
- 内省是指在运行时检查对象、类、方法和属性等的详细信息。内省不涉及创建对象
- 或修改对象的状态,它只是获取信息。
- 内省可以通过
java.lang.Class类的方法,如getMethods()、getFields()等,来获取类的成员信息。 - 内省主要用于监控和调试,可以帮助开发者理解程序的运行状态。
- 互斥条件:资源不能被多个线程共享,必须一次只能被一个线程使用。
- 请求和保持条件:一个线程持有至少一个资源,并请求新的资源,而新资源被其他线程占有。
- 不剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程强行剥夺,只能由获得该资源的线程自行释放。
- 循环等待条件:存在一种线程资源的循环等待关系,每个线程都在等待下一个线程所持有的资源。
- 有序分配资源:为资源分配一个唯一的标识,并按照标识的顺序来请求资源。
- 资源一次性分配:让线程在开始前一次性地请求所有需要的资源,避免在持有部分资源的情况下再次请求。
- 资源定时分配:为资源请求设置超时时间,如果超时未获得资源,则释放已持有的资源,并在一段时间后重新尝试。
- 检测并恢复:通过工具检测死锁,并在检测到死锁时采取措施,如终止或回滚部分线程。
4.2 并发关键字
Synchronized可以作用在哪里?
- 对象锁
- 方法锁
- 类锁
Synchronized本质上是通过什么保证线程安全的?
- 加锁和释放锁的原理
深入JVM看字节码,创建如下的代码:
public class SynchronizedDemo2 {
Object object = new Object();
public void method1() {
synchronized (object) {
}
}
}
使用javac命令进行编译生成.class文件
>javac SynchronizedDemo2.java
使用javap命令反编译查看.class文件的信息
>javap -verbose SynchronizedDemo2.class
得到如下的信息:

关注红色方框里的monitorenter和monitorexit即可。
Monitorenter和Monitorexit指令,会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:
- monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
- 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加
- 这把锁已经被别的线程获取了,等待锁释放
monitorexit指令:释放对于monitor的所有权,释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。
下图表现了对象,对象监视器,同步队列以及执行线程状态之间的关系:

该图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。
- 可重入原理:加锁次数计数器
看如下的例子:
public class SynchronizedDemo {
public static void main(String[] args) {
synchronized (SynchronizedDemo.class) {
}
method2();
}
private synchronized static void method2() {
}
}
对应的字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class tech/pdai/test/synchronized/SynchronizedDemo
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
15: invokestatic #3 // Method method2:()V
Exception table:
from to target type
5 7 10 any
10 13 10 any
上面的SynchronizedDemo中在执行完同步代码块之后紧接着再会去执行一个静态同步方法,而这个方法锁的对象依然就这个类对象,那么这个正在执行的线程还需要获取该锁吗? 答案是不必的,从上图中就可以看出来,执行静态同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是锁的重入性,即在同一锁程中,线程不需要再次获取同一把锁。
Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
- 保证可见性的原理:内存模型和happens-before规则
Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。继续来看代码:
public class MonitorDemo {
private int a = 0;
public synchronized void writer() { // 1
a++; // 2
} // 3
public synchronized void reader() { // 4
int i = a; // 5
} // 6
}
该代码的happens-before关系如图所示:

在图中每一个箭头连接的两个节点就代表之间的happens-before关系,黑色的是通过程序顺序规则推导出来,红色的为监视器锁规则推导而出:线程A释放锁happens-before线程B加锁,蓝色的则是通过程序顺序规则和监视器锁规则推测出来happens-befor关系,通过传递性规则进一步推导的happens-before关系。现在我们来重点关注2 happens-before 5,通过这个关系我们可以得出什么?
根据happens-before的定义中的一条:如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B。线程A先对共享变量A进行加一,由2 happens-before 5关系可知线程A的执行结果对线程B可见即线程B所读取到的a的值为1。
Synchronized使得同时只有一个线程可以执行,性能比较差,有什么提升的方法?
简单来说在JVM中monitorenter和monitorexit字节码依赖于底层的操作系统的Mutex Lock来实现的,但是由于使用Mutex Lock需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的;然而在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境)如果每次都调用Mutex Lock那么将严重的影响程序的性能。不过在jdk1.6中对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销。
- 锁粗化(Lock Coarsening):也就是减少不必要的紧连在一起的unlock,lock操作,将多个连续的锁扩展成一个范围更大的锁。
- 锁消除(Lock Elimination):通过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁保护,通过逃逸分析也可以在线程本地Stack上进行对象空间的分配(同时还可以减少Heap上的垃圾收集开销)。
- 轻量级锁(Lightweight Locking):这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁,取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行CAS指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒。
- 偏向锁(Biased Locking):是为了在无锁竞争的情况下避免在锁获取过程中执行不必要的CAS原子指令,因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟。
- 适应性自旋(Adaptive Spinning):当线程在获取轻量级锁的过程中执行CAS操作失败时,在进入与monitor相关联的操作系统重量级锁(mutex semaphore)前会进入忙等待(Spinning)然后再次尝试,当尝试一定的次数后如果仍然没有成功则调用与该monitor关联的semaphore(即互斥锁)进入到阻塞状态。
Synchronized由什么样的缺陷? Java Lock是怎么弥补这些缺陷的?
- synchronized的缺陷
- 效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时
- 不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活
- 无法知道是否成功获得锁,相对而言,Lock可以拿到状态
- Lock解决相应问题
Lock类这里不做过多解释,主要看里面的4个方法:
lock(): 加锁unlock(): 解锁tryLock(): 尝试获取锁,返回一个boolean值tryLock(long,TimeUtil): 尝试获取锁,可以设置超时
Synchronized只有锁只与一个条件(是否获取锁)相关联,不灵活,后来Condition与Lock的结合解决了这个问题。
多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。ReentrantLock的lockInterruptibly()方法可以优先考虑响应中断。 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待。有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了。
Synchronized和Lock的对比,和选择?
- 存在层次上
synchronized: Java的关键字,在jvm层面上
Lock: 是一个接口
- 锁的释放
synchronized: 1、以获取锁的线程执行完同步代码,释放锁 2、线程执行发生异常,jvm会让线程释放锁
Lock: 在finally中必须释放锁,不然容易造成线程死锁
- 锁的获取
synchronized: 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待
Lock: 分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待(可以通过tryLock判断有没有锁)
- 锁的释放(死锁产生)
synchronized: 在发生异常时候会自动释放占有的锁,因此不会出现死锁
Lock: 发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生
- 锁的状态
synchronized: 无法判断
Lock: 可以判断
- 锁的类型
synchronized: 可重入 不可中断 非公平
Lock: 可重入 可判断 可公平(两者皆可)
- 性能
synchronized: 少量同步
Lock: 大量同步
Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离) 在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。
- 调度
synchronized: 使用Object对象本身的wait 、notify、notifyAll调度机制
Lock: 可以使用Condition进行线程之间的调度
- 用法
synchronized: 在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
Lock: 一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。
- 底层实现
synchronized: 底层使用指令码方式来控制锁的,映射成字节码指令就是增加来两个指令:monitorenter和monitorexit。当线程执行遇到monitorenter指令时会尝试获取内置锁,如果获取锁则锁计数器+1,如果没有获取锁则阻塞;当遇到monitorexit指令时锁计数器-1,如果计数器为0则释放锁。
Lock: 底层是CAS乐观锁,依赖AbstractQueuedSynchronizer类,把所有的请求线程构成一个CLH队列。而对该队列的操作均通过Lock-Free(CAS)操作。
Synchronized在使用时有何注意事项?
- 锁对象不能为空,因为锁的信息都保存在对象头里
- 作用域不宜过大,影响程序执行的速度,控制范围过大,编写代码也容易出错
- 避免死锁
- 在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键,因为代码量少,避免出错
Synchronized修饰的方法在抛出异常时,会释放锁吗?
会
多个线程等待同一个Synchronized锁的时候,JVM如何选择下一个获取锁的线程?
非公平锁,即抢占式。
synchronized是公平锁吗?
synchronized实际上是非公平的,新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待,这样有利于提高性能,但是也可能会导致饥饿现象。
volatile关键字的作用是什么?
- 防重排序 我们从一个最经典的例子来分析重排序问题。大家应该都很熟悉单例模式的实现,而在并发环境下的单例实现方式,我们通常可以采用双重检查加锁(DCL)的方式来实现。其源码如下:
public class Singleton {
public static volatile Singleton singleton;
/**
* 构造函数私有,禁止外部实例化
*/
private Singleton() {};
public static Singleton getInstance() {
if (singleton == null) {
synchronized (singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
现在我们分析一下为什么要在变量singleton之间加上volatile关键字。要理解这个问题,先要了解对象的构造过程,实例化一个对象其实可以分为三个步骤:
- 分配内存空间。
- 初始化对象。
- 将内存空间的地址赋值给对应的引用。
但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:
- 分配内存空间。
- 将内存空间的地址赋值给对应的引用。
- 初始化对象
如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量。
- 实现可见性
可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存。volatile关键字能有效的解决这个问题,我们看下下面的例子,就可以知道其作用:
public class TestVolatile {
private static boolean stop = false;
public static void main(String[] args) {
// Thread-A
new Thread("Thread A") {
@Override
public void run() {
while (!stop) {
}
System.out.println(Thread.currentThread() + " stopped");
}
}.start();
// Thread-main
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread() + " after 1 seconds");
} catch (InterruptedException e) {
e.printStackTrace();
}
stop = true;
}
}
执行输出如下
Thread[main,5,main] after 1 seconds
// Thread A一直在loop, 因为Thread A 由于可见性原因看不到Thread Main 已经修改stop的值
可以看到 Thread-main 休眠1秒之后,设置 stop = ture,但是Thread A根本没停下来,这就是可见性问题。如果通过在stop变量前面加上volatile关键字则会真正stop:
Thread[main,5,main] after 1 seconds
Thread[Thread A,5,main] stopped
Process finished with exit code 0
- 保证原子性:单次读/写
volatile不能保证完全的原子性,只能保证单次的读/写操作具有原子性。
volatile能保证原子性吗?
不能完全保证,只能保证单次的读/写操作具有原子性。
32位机器上共享的long和double变量的为什么要用volatile?
因为long和double两种数据类型的操作可分为高32位和低32位两部分,因此普通的long或double类型读/写可能不是原子的。因此,鼓励大家将共享的long和double变量设置为volatile类型,这样能保证任何情况下对long和double的单次读/写操作都具有原子性。
如下是JLS中的解释:
17.7 Non-Atomic Treatment of double and long
- For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write.
- Writes and reads of volatile long and double values are always atomic.
- Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values.
- Some implementations may find it convenient to divide a single write action on a 64-bit long or double value into two write actions on adjacent 32-bit values. For efficiency’s sake, this behavior is implementation-specific; an implementation of the Java Virtual Machine is free to perform writes to long and double values atomically or in two parts.
- Implementations of the Java Virtual Machine are encouraged to avoid splitting 64-bit values where possible. Programmers are encouraged to declare shared 64-bit values as volatile or synchronize their programs correctly to avoid possible complications.
目前各种平台下的商用虚拟机都选择把 64 位数据的读写操作作为原子操作来对待,因此我们在编写代码时一般不把long 和 double 变量专门声明为 volatile多数情况下也是不会错的。
volatile是如何实现可见性的?
内存屏障。
volatile是如何实现有序性的?
happens-before等
说下volatile的应用场景?
使用 volatile 必须具备的条件
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
- 只有在状态真正独立于程序内其他内容时才能使用 volatile。
- 例子 1: 单例模式
单例模式的一种实现方式,但很多人会忽略 volatile 关键字,因为没有该关键字,程序也可以很好的运行,只不过代码的稳定性总不是 100%,说不定在未来的某个时刻,隐藏的 bug 就出来了。
class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- 例子2: volatile bean
在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。
@ThreadSafe
public class Person {
private volatile String firstName;
private volatile String lastName;
private volatile int age;
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setAge(int age) {
this.age = age;
}
}
所有的final修饰的字段都是编译期常量吗?
不是
如何理解private所修饰的方法是隐式的final?
类中所有private方法都隐式地指定为final的,由于无法取用private方法,所以也就不能覆盖它。可以对private方法增添final关键字,但这样做并没有什么好处。看下下面的例子:
public class Base {
private void test() {
}
}
public class Son extends Base{
public void test() {
}
public static void main(String[] args) {
Son son = new Son();
Base father = son;
//father.test();
}
}
Base和Son都有方法test(),但是这并不是一种覆盖,因为private所修饰的方法是隐式的final,也就是无法被继承,所以更不用说是覆盖了,在Son中的test()方法不过是属于Son的新成员罢了,Son进行向上转型得到father,但是father.test()是不可执行的,因为Base中的test方法是private的,无法被访问到。
说说final类型的类如何拓展?
比如String是final类型,我们想写个MyString复用所有String中方法,同时增加一个新的toMyString()的方法,应该如何做?
外观模式:
/**
*
*/
class MyString{
private String innerString;
// ...init & other methods
// 支持老的方法
public int length(){
return innerString.length(); // 通过innerString调用老的方法
}
// 添加新方法
public String toMyString(){
//...
}
}
final方法可以被重载吗?
我们知道父类的final方法是不能够被子类重写的,那么final方法可以被重载吗? 答案是可以的,下面代码是正确的。
public class FinalExampleParent {
public final void test() {
}
public final void test(String str) {
}
}
父类的final方法能不能够被子类重写?
不可以
说说基本类型的final域重排序规则?
先看一段示例性的代码:
public class FinalDemo {
private int a; //普通域
private final int b; //final域
private static FinalDemo finalDemo;
public FinalDemo() {
a = 1; // 1. 写普通域
b = 2; // 2. 写final域
}
public static void writer() {
finalDemo = new FinalDemo();
}
public static void reader() {
FinalDemo demo = finalDemo; // 3.读对象引用
int a = demo.a; //4.读普通域
int b = demo.b; //5.读final域
}
}
假设线程A在执行writer()方法,线程B执行reader()方法。
- 写final域重排序规则
写final域的重排序规则禁止对final域的写重排序到构造函数之外,这个规则的实现主要包含了两个方面:
- JMM禁止编译器把final域的写重排序到构造函数之外;
- 编译器会在final域写之后,构造函数return之前,插入一个storestore屏障。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。
我们再来分析writer方法,虽然只有一行代码,但实际上做了两件事情:
- 构造了一个FinalDemo对象;
- 把这个对象赋值给成员变量finalDemo。
我们来画下存在的一种可能执行时序图,如下:

由于a,b之间没有数据依赖性,普通域(普通变量)a可能会被重排序到构造函数之外,线程B就有可能读到的是普通变量a初始化之前的值(零值),这样就可能出现错误。而final域变量b,根据重排序规则,会禁止final修饰的变量b重排序到构造函数之外,从而b能够正确赋值,线程B就能够读到final变量初始化后的值。
因此,写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域就不具有这个保障。比如在上例,线程B有可能就是一个未正确初始化的对象finalDemo。
- 读final域重排序规则
读final域重排序规则为:在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM会禁止这两个操作的重排序。(注意,这个规则仅仅是针对处理器),处理器会在读final域操作的前面插入一个LoadLoad屏障。实际上,读对象的引用和读该对象的final域存在间接依赖性,一般处理器不会重排序这两个操作。但是有一些处理器会重排序,因此,这条禁止重排序规则就是针对这些处理器而设定的。
read()方法主要包含了三个操作:
- 初次读引用变量finalDemo;
- 初次读引用变量finalDemo的普通域a;
- 初次读引用变量finalDemo的final域b;
假设线程A写过程没有重排序,那么线程A和线程B有一种的可能执行时序为下图:

读对象的普通域被重排序到了读对象引用的前面就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。而final域的读操作就“限定”了在读final域变量前已经读到了该对象的引用,从而就可以避免这种情况。
读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用。
说说final的原理?
- 写final域会要求编译器在final域写之后,构造函数返回前插入一个StoreStore屏障。
- 读final域的重排序规则会要求编译器在读final域的操作前插入一个LoadLoad屏障。
PS:很有意思的是,如果以X86处理为例,X86不会对写-写重排序,所以StoreStore屏障可以省略。由于不会对有间接依赖性的操作重排序,所以在X86处理器中,读final域需要的LoadLoad屏障也会被省略掉。也就是说,以X86为例的话,对final域的读/写的内存屏障都会被省略!具体是否插入还是得看是什么处理器。
4.3 JUC全局观
JUC框架包含几个部分?
五个部分:

主要包含: (注意: 上图是网上找的图,无法表述一些继承关系,同时少了部分类;但是主体上可以看出其分类关系也够了)
- Lock框架和Tools类(把图中这两个放到一起理解)
- Collections: 并发集合
- Atomic: 原子类
- Executors: 线程池
Lock框架和Tools哪些核心的类?

- 接口: Condition, Condition为接口类型,它将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set (wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。可以通过await(),signal()来休眠/唤醒线程。
- 接口: Lock,Lock为接口类型,Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的Condition对象。
- 接口ReadWriteLock ReadWriteLock为接口类型, 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。
- 抽象类: AbstractOwnableSynchonizer AbstractOwnableSynchonizer为抽象类,可以由线程以独占方式拥有的同步器。此类为创建锁和相关同步器(伴随着所有权的概念)提供了基础。AbstractOwnableSynchronizer 类本身不管理或使用此信息。但是,子类和工具可以使用适当维护的值帮助控制和监视访问以及提供诊断。
- 抽象类(long): AbstractQueuedLongSynchronizer AbstractQueuedLongSynchronizer为抽象类,以 long 形式维护同步状态的一个 AbstractQueuedSynchronizer 版本。此类具有的结构、属性和方法与 AbstractQueuedSynchronizer 完全相同,但所有与状态相关的参数和结果都定义为 long 而不是 int。当创建需要 64 位状态的多级别锁和屏障等同步器时,此类很有用。
- 核心抽象类(int): AbstractQueuedSynchronizer AbstractQueuedSynchronizer为抽象类,其为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。
- 锁常用类: LockSupport LockSupport为常用类,用来创建锁和其他同步类的基本线程阻塞原语。LockSupport的功能和"Thread中的 Thread.suspend()和Thread.resume()有点类似",LockSupport中的park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程。但是park()和unpark()不会遇到“Thread.suspend 和 Thread.resume所可能引发的死锁”问题。
- 锁常用类: ReentrantLock ReentrantLock为常用类,它是一个可重入的互斥锁 Lock,它具有与使用 synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
- 锁常用类: ReentrantReadWriteLock ReentrantReadWriteLock是读写锁接口ReadWriteLock的实现类,它包括Lock子类ReadLock和WriteLock。ReadLock是共享锁,WriteLock是独占锁。
- 锁常用类: StampedLock 它是java8在java.util.concurrent.locks新增的一个API。StampedLock控制锁有三种模式(写,读,乐观读),一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为票据stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问。在读锁上分为悲观锁和乐观锁。
- 工具常用类: CountDownLatch CountDownLatch为常用类,它是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
- 工具常用类: CyclicBarrier CyclicBarrier为常用类,其是一个同步辅助类,它允许一组线程互相等待,直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用,所以称它为循环 的 barrier。
- 工具常用类: Phaser Phaser是JDK 7新增的一个同步辅助类,它可以实现CyclicBarrier和CountDownLatch类似的功能,而且它支持对任务的动态调整,并支持分层结构来达到更高的吞吐量。
- 工具常用类: Semaphore Semaphore为常用类,其是一个计数信号量,从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。
- 工具常用类: Exchanger Exchanger是用于线程协作的工具类, 主要用于两个线程之间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange()方法交换数据,当一个线程先执行exchange()方法后,它会一直等待第二个线程也执行exchange()方法,当这两个线程到达同步点时,这两个线程就可以交换数据了。
JUC并发集合哪些核心的类?

- Queue: ArrayBlockingQueue 一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部 是在队列中存在时间最长的元素。队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。
- Queue: LinkedBlockingQueue 一个基于已链接节点的、范围任意的 blocking queue。此队列按 FIFO(先进先出)排序元素。队列的头部 是在队列中时间最长的元素。队列的尾部 是在队列中时间最短的元素。新元素插入到队列的尾部,并且队列获取操作会获得位于队列头部的元素。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。
- Queue: LinkedBlockingDeque 一个基于已链接节点的、任选范围的阻塞双端队列。
- Queue: ConcurrentLinkedQueue 一个基于链接节点的无界线程安全队列。此队列按照 FIFO(先进先出)原则对元素进行排序。队列的头部 是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。新的元素插入到队列的尾部,队列获取操作从队列头部获得元素。当多个线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择。此队列不允许使用 null 元素。
- Queue: ConcurrentLinkedDeque 是双向链表实现的无界队列,该队列同时支持FIFO和FILO两种操作方式。
- Queue: DelayQueue 延时无界阻塞队列,使用Lock机制实现并发访问。队列里只允许放可以“延期”的元素,队列中的head是最先“到期”的元素。如果队里中没有元素到“到期”,那么就算队列中有元素也不能获取到。
- Queue: PriorityBlockingQueue 无界优先级阻塞队列,使用Lock机制实现并发访问。priorityQueue的线程安全版,不允许存放null值,依赖于comparable的排序,不允许存放不可比较的对象类型。
- Queue: SynchronousQueue 没有容量的同步队列,通过CAS实现并发访问,支持FIFO和FILO。
- Queue: LinkedTransferQueue JDK 7新增,单向链表实现的无界阻塞队列,通过CAS实现并发访问,队列元素使用 FIFO(先进先出)方式。LinkedTransferQueue可以说是ConcurrentLinkedQueue、SynchronousQueue(公平模式)和LinkedBlockingQueue的超集, 它不仅仅综合了这几个类的功能,同时也提供了更高效的实现。
- List: CopyOnWriteArrayList ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。
- Set: CopyOnWriteArraySet 对其所有操作使用内部CopyOnWriteArrayList的Set。即将所有操作转发至CopyOnWriteArayList来进行操作,能够保证线程安全。在add时,会调用addIfAbsent,由于每次add时都要进行数组遍历,因此性能会略低于CopyOnWriteArrayList。
- Set: ConcurrentSkipListSet 一个基于ConcurrentSkipListMap 的可缩放并发 NavigableSet 实现。set 的元素可以根据它们的自然顺序进行排序,也可以根据创建 set 时所提供的 Comparator 进行排序,具体取决于使用的构造方法。
- Map: ConcurrentHashMap 是线程安全HashMap的。ConcurrentHashMap在JDK 7之前是通过Lock和segment(分段锁)实现,JDK 8 之后改为CAS+synchronized来保证并发安全。
- Map: ConcurrentSkipListMap 线程安全的有序的哈希表(相当于线程安全的TreeMap);映射可以根据键的自然顺序进行排序,也可以根据创建映射时所提供的 Comparator 进行排序,具体取决于使用的构造方法。
JUC原子类哪些核心的类?
其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,不会阻塞线程(或者说只是在硬件级别上阻塞了)。
- 原子更新基本类型
- AtomicBoolean: 原子更新布尔类型。
- AtomicInteger: 原子更新整型。
- AtomicLong: 原子更新长整型。
- 原子更新数组
- AtomicIntegerArray: 原子更新整型数组里的元素。
- AtomicLongArray: 原子更新长整型数组里的元素。
- AtomicReferenceArray: 原子更新引用类型数组里的元素。
- 原子更新引用类型
- AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
- AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
- AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
- AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述
- 原子更新字段类
- AtomicReference: 原子更新引用类型。
- AtomicStampedReference: 原子更新引用类型, 内部使用Pair来存储元素值及其版本号。
- AtomicMarkableReferce: 原子更新带有标记位的引用类型。
JUC线程池哪些核心的类?

- 接口: Executor Executor接口提供一种将任务提交与每个任务将如何运行的机制(包括线程使用的细节、调度等)分离开来的方法。通常使用 Executor 而不是显式地创建线程。
- ExecutorService ExecutorService继承自Executor接口,ExecutorService提供了管理终止的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 可以关闭 ExecutorService,这将导致其停止接受新任务。关闭后,执行程序将最后终止,这时没有任务在执行,也没有任务在等待执行,并且无法提交新任务。
- ScheduledExecutorService ScheduledExecutorService继承自ExecutorService接口,可安排在给定的延迟后运行或定期执行的命令。
- AbstractExecutorService AbstractExecutorService继承自ExecutorService接口,其提供 ExecutorService 执行方法的默认实现。此类使用 newTaskFor 返回的 RunnableFuture 实现 submit、invokeAny 和 invokeAll 方法,默认情况下,RunnableFuture 是此包中提供的 FutureTask 类。
- FutureTask FutureTask 为 Future 提供了基础实现,如获取任务执行结果(get)和取消任务(cancel)等。如果任务尚未完成,获取任务执行结果时将会阻塞。一旦执行结束,任务就不能被重启或取消(除非使用runAndReset执行计算)。FutureTask 常用来封装 Callable 和 Runnable,也可以作为一个任务提交到线程池中执行。除了作为一个独立的类之外,此类也提供了一些功能性函数供我们创建自定义 task 类使用。FutureTask 的线程安全由CAS来保证。
- 核心: ThreadPoolExecutor ThreadPoolExecutor实现了AbstractExecutorService接口,也是一个 ExecutorService,它使用可能的几个池线程之一执行每个提交的任务,通常使用 Executors 工厂方法配置。 线程池可以解决两个不同问题: 由于减少了每个任务调用的开销,它们通常可以在执行大量异步任务时提供增强的性能,并且还可以提供绑定和管理资源(包括执行任务集时使用的线程)的方法。每个 ThreadPoolExecutor 还维护着一些基本的统计数据,如完成的任务数。
- 核心: ScheduledThreadExecutor ScheduledThreadPoolExecutor实现ScheduledExecutorService接口,可安排在给定的延迟后运行命令,或者定期执行命令。需要多个辅助线程时,或者要求 ThreadPoolExecutor 具有额外的灵活性或功能时,此类要优于 Timer。
- 核心: Fork/Join框架 ForkJoinPool 是JDK 7加入的一个线程池类。Fork/Join 技术是分治算法(Divide-and-Conquer)的并行实现,它是一项可以获得良好的并行性能的简单且高效的设计技术。目的是为了帮助我们更好地利用多处理器带来的好处,使用所有可用的运算能力来提升应用的性能。
- 工具类: Executors Executors是一个工具类,用其可以创建ExecutorService、ScheduledExecutorService、ThreadFactory、Callable等对象。它的使用融入到了ThreadPoolExecutor, ScheduledThreadExecutor和ForkJoinPool中。
4.4 JUC原子类
线程安全的实现方法有哪些?
线程安全的实现方法包含:
- 互斥同步: synchronized 和 ReentrantLock
- 非阻塞同步: CAS, AtomicXXXX
- 无同步方案: 栈封闭,Thread Local,可重入代码
什么是CAS?
CAS的全称为Compare-And-Swap,直译就是对比交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。 简单解释:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。
CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁。JDK中大量使用了CAS来更新数据而防止加锁(synchronized 重量级锁)来保持原子更新。
相信sql大家都熟悉,类似sql中的条件更新一样:update set id=3 from table where id=2。因为单条sql执行具有原子性,如果有多个线程同时执行此sql语句,只有一条能更新成功。
CAS使用示例,结合AtomicInteger给出示例?
如果不使用CAS,在高并发下,多线程同时修改一个变量的值我们需要synchronized加锁(可能有人说可以用Lock加锁,Lock底层的AQS也是基于CAS进行获取锁的)。
public class Test {
private int i=0;
public synchronized int add(){
return i++;
}
}
java中为我们提供了AtomicInteger 原子类(底层基于CAS进行更新数据的),不需要加锁就在多线程并发场景下实现数据的一致性。
public class Test {
private AtomicInteger i = new AtomicInteger(0);
public int add(){
return i.addAndGet(1);
}
}
CAS会有哪些问题?
CAS 方式为乐观锁,synchronized 为悲观锁。因此使用 CAS 解决并发问题通常情况下性能更优。
但使用 CAS 方式也会有几个问题:
- ABA问题
因为CAS需要在操作值的时候,检查值有没有发生变化,比如没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时则会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。
从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
- 循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行命令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率。
- 只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i = 2,j = a,合并一下ij = 2a,然后用CAS来操作ij。
从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
AtomicInteger底层实现?
- CAS+volatile
- volatile保证线程的可见性,多线程并发时,一个线程修改数据,可以保证其它线程立马看到修改后的值CAS 保证数据更新的原子性。
请阐述你对Unsafe类的理解?
UnSafe类总体功能:

如上图所示,Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类,下面将对其相关方法和应用场景进行详细介绍。
说说你对Java原子类的理解?
包含13个,4组分类,说说作用和使用场景。
- 原子更新基本类型
- AtomicBoolean: 原子更新布尔类型。
- AtomicInteger: 原子更新整型。
- AtomicLong: 原子更新长整型。
- 原子更新数组
- AtomicIntegerArray: 原子更新整型数组里的元素。
- AtomicLongArray: 原子更新长整型数组里的元素。
- AtomicReferenceArray: 原子更新引用类型数组里的元素。
- 原子更新引用类型
- AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
- AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
- AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
- AtomicReferenceFieldUpdater: 上面已经说过此处不在赘述
- 原子更新字段类
- AtomicReference: 原子更新引用类型。
- AtomicStampedReference: 原子更新引用类型, 内部使用Pair来存储元素值及其版本号。
- AtomicMarkableReferce: 原子更新带有标记位的引用类型。
AtomicStampedReference是怎么解决ABA的?
AtomicStampedReference主要维护包含一个对象引用以及一个可以自动更新的整数"stamp"的pair对象来解决ABA问题。
4.5 JUC锁
为什么LockSupport也是核心基础类?
AQS框架借助于两个类:Unsafe(提供CAS操作)和LockSupport(提供park/unpark操作)
通过wait/notify实现同步?
class MyThread extends Thread {
public void run() {
synchronized (this) {
System.out.println("before notify");
notify();
System.out.println("after notify");
}
}
}
public class WaitAndNotifyDemo {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
synchronized (myThread) {
try {
myThread.start();
// 主线程睡眠3s
Thread.sleep(3000);
System.out.println("before wait");
// 阻塞主线程
myThread.wait();
System.out.println("after wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果
before wait
before notify
after notify
after wait
说明: 具体的流程图如下

使用wait/notify实现同步时,必须先调用wait,后调用notify,如果先调用notify,再调用wait,将起不了作用。具体代码如下
class MyThread extends Thread {
public void run() {
synchronized (this) {
System.out.println("before notify");
notify();
System.out.println("after notify");
}
}
}
public class WaitAndNotifyDemo {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.start();
// 主线程睡眠3s
Thread.sleep(3000);
synchronized (myThread) {
try {
System.out.println("before wait");
// 阻塞主线程
myThread.wait();
System.out.println("after wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
before notify
after notify
before wait
说明: 由于先调用了notify,再调用的wait,此时主线程还是会一直阻塞。
通过LockSupport的park/unpark实现同步?
import java.util.concurrent.locks.LockSupport;
class MyThread extends Thread {
private Object object;
public MyThread(Object object) {
this.object = object;
}
public void run() {
System.out.println("before unpark");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取blocker
System.out.println("Blocker info " + LockSupport.getBlocker((Thread) object));
// 释放许可
LockSupport.unpark((Thread) object);
// 休眠500ms,保证先执行park中的setBlocker(t, null);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再次获取blocker
System.out.println("Blocker info " + LockSupport.getBlocker((Thread) object));
System.out.println("after unpark");
}
}
public class test {
public static void main(String[] args) {
MyThread myThread = new MyThread(Thread.currentThread());
myThread.start();
System.out.println("before park");
// 获取许可
LockSupport.park("ParkAndUnparkDemo");
System.out.println("after park");
}
}
运行结果:
before park
before unpark
Blocker info ParkAndUnparkDemo
after park
Blocker info null
after unpark
说明: 本程序先执行park,然后在执行unpark,进行同步,并且在unpark的前后都调用了getBlocker,可以看到两次的结果不一样,并且第二次调用的结果为null,这是因为在调用unpark之后,执行了Lock.park(Object blocker)函数中的setBlocker(t, null)函数,所以第二次调用getBlocker时为null。
上例是先调用park,然后调用unpark,现在修改程序,先调用unpark,然后调用park,看能不能正确同步。具体代码如下
import java.util.concurrent.locks.LockSupport;
class MyThread extends Thread {
private Object object;
public MyThread(Object object) {
this.object = object;
}
public void run() {
System.out.println("before unpark");
// 释放许可
LockSupport.unpark((Thread) object);
System.out.println("after unpark");
}
}
public class ParkAndUnparkDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread(Thread.currentThread());
myThread.start();
try {
// 主线程睡眠3s
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("before park");
// 获取许可
LockSupport.park("ParkAndUnparkDemo");
System.out.println("after park");
}
}
运行结果:
before unpark
after unpark
before park
after park
说明: 可以看到,在先调用unpark,再调用park时,仍能够正确实现同步,不会造成由wait/notify调用顺序不当所引起的阻塞。因此park/unpark相比wait/notify更加的灵活。
Thread.sleep()、Object.wait()、Condition.await()、LockSupport.park()的区别? 重点
- Thread.sleep()和Object.wait()的区别
首先,我们先来看看Thread.sleep()和Object.wait()的区别,这是一个烂大街的题目了,大家应该都能说上来两点。
- Thread.sleep()不会释放占有的锁,Object.wait()会释放占有的锁;
- Thread.sleep()必须传入时间,Object.wait()可传可不传,不传表示一直阻塞下去;
- Thread.sleep()到时间了会自动唤醒,然后继续执行;
- Object.wait()不带时间的,需要另一个线程使用Object.notify()唤醒;
- Object.wait()带时间的,假如没有被notify,到时间了会自动唤醒,这时又分好两种情况,一是立即获取到了锁,线程自然会继续执行;二是没有立即获取锁,线程进入同步队列等待获取锁;
其实,他们俩最大的区别就是Thread.sleep()不会释放锁资源,Object.wait()会释放锁资源。
- Object.wait()和Condition.await()的区别
Object.wait()和Condition.await()的原理是基本一致的,不同的是Condition.await()底层是调用LockSupport.park()来实现阻塞当前线程的。
实际上,它在阻塞当前线程之前还干了两件事,一是把当前线程添加到条件队列中,二是“完全”释放锁,也就是让state状态变量变为0,然后才是调用LockSupport.park()阻塞当前线程。
- Thread.sleep()和LockSupport.park()的区别 LockSupport.park()还有几个兄弟方法——parkNanos()、parkUtil()等,我们这里说的park()方法统称这一类方法。
- 从功能上来说,Thread.sleep()和LockSupport.park()方法类似,都是阻塞当前线程的执行,且都不会释放当前线程占有的锁资源;
- Thread.sleep()没法从外部唤醒,只能自己醒过来;
- LockSupport.park()方法可以被另一个线程调用LockSupport.unpark()方法唤醒;
- Thread.sleep()方法声明上抛出了InterruptedException中断异常,所以调用者需要捕获这个异常或者再抛出;
- LockSupport.park()方法不需要捕获中断异常;
- Thread.sleep()本身就是一个native方法;
- LockSupport.park()底层是调用的Unsafe的native方法;
- Object.wait()和LockSupport.park()的区别
二者都会阻塞当前线程的运行,他们有什么区别呢? 经过上面的分析相信你一定很清楚了,真的吗? 往下看!
- Object.wait()方法需要在synchronized块中执行;
- LockSupport.park()可以在任意地方执行;
- Object.wait()方法声明抛出了中断异常,调用者需要捕获或者再抛出;
- LockSupport.park()不需要捕获中断异常;
- Object.wait()不带超时的,需要另一个线程执行notify()来唤醒,但不一定继续执行后续内容;
- LockSupport.park()不带超时的,需要另一个线程执行unpark()来唤醒,一定会继续执行后续内容;
park()/unpark()底层的原理是“二元信号量”,你可以把它相像成只有一个许可证的Semaphore,只不过这个信号量在重复执行unpark()的时候也不会再增加许可证,最多只有一个许可证。
如果在wait()之前执行了notify()会怎样?
如果当前的线程不是此对象锁的所有者,却调用该对象的notify()或wait()方法时抛出IllegalMonitorStateException异常;
如果当前线程是此对象锁的所有者,wait()将一直阻塞,因为后续将没有其它notify()唤醒它。
如果在park()之前执行了unpark()会怎样?
线程不会被阻塞,直接跳过park(),继续执行后续内容
什么是AQS? 为什么它是核心?
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AbstractQueuedSynchronizer类底层的数据结构是使用CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。其中Sync queue,即同步队列,是双向链表,包括head结点和tail结点,head结点主要用作后续的调度。而Condition queue不是必须的,其是一个单向链表,只有当使用Condition时,才会存在此单向链表。并且可能会有多个Condition queue。

AQS的核心思想是什么?
底层数据结构: AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS有哪些核心的方法?
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
AQS定义什么样的资源获取方式?
AQS定义了两种资源获取方式:
- 独占(只有一个线程能访问执行,又根据是否按队列的顺序分为公平锁和非公平锁,如
ReentrantLock) - 共享(多个线程可同时访问执行,如
Semaphore、CountDownLatch、CyclicBarrier)。ReentrantReadWriteLock可以看成是组合式,允许多个线程同时对某一资源进行读。
AQS底层使用了什么样的设计模式?
模板, 共享锁和独占锁在一个接口类中。
什么是可重入,什么是可重入锁? 它用来解决什么问题?
可重入:(来源于维基百科)若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的。
可重入锁:又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。
ReentrantLock的核心是AQS,那么它怎么来实现的,继承吗?
ReentrantLock总共有三个内部类,并且三个内部类是紧密相关的,下面先看三个类的关系。

说明: ReentrantLock类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。下面逐个进行分析。
ReentrantLock是如何实现公平锁的?
FairSync
ReentrantLock是如何实现非公平锁的?
UnFairSync
ReentrantLock默认实现的是公平还是非公平锁?
非公平锁
为了有了ReentrantLock还需要ReentrantReadWriteLock?
读锁和写锁分离:ReentrantReadWriteLock表示可重入读写锁,ReentrantReadWriteLock中包含了两种锁,读锁ReadLock和写锁WriteLock,可以通过这两种锁实现线程间的同步。
ReentrantReadWriteLock底层实现原理?
ReentrantReadWriteLock有五个内部类,五个内部类之间也是相互关联的。内部类的关系如下图所示。

说明: 如上图所示,Sync继承自AQS、NonfairSync继承自Sync类、FairSync继承自Sync类;ReadLock实现了Lock接口、WriteLock也实现了Lock接口。
ReentrantReadWriteLock底层读写状态如何设计的?
高16位为读锁,低16位为写锁
读锁和写锁的最大数量是多少?
2的16次方-1
本地线程计数器ThreadLocalHoldCounter是用来做什么的?
本地线程计数器,与对象绑定(线程-》线程重入的次数)
写锁的获取与释放是怎么实现的?
tryAcquire/tryRelease
读锁的获取与释放是怎么实现的?
tryAcquireShared/tryReleaseShared
什么是锁的升降级?
RentrantReadWriteLock为什么不支持锁升级? RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。
分析 Java 中的 AQS(AbstractQueuedSynchronizer)框架的原理和应用
AbstractQueuedSynchronizer(简称 AQS)是 Java 并发包中的一个抽象框架,它为实现依赖于先进先出等待队列的阻塞锁和相关同步器提供了一个骨架实现。
- 核心状态:AQS 维护一个整型的同步状态,通过内部类
Node来构建 FIFO 队列。 - 独占模式:只有一个线程可以获取资源。
- 共享模式:允许多个线程同时获取资源。
应用
- ReentrantLock:可重入的互斥锁。
- Semaphore:信号量,控制同时访问特定资源的线程数量。
- CountDownLatch:倒计时锁存器,等待一个倒计时事件完成。
总结
特性描述核心状态一个整型的同步状态,用于同步操作。等待队列一个 FIFO 队列,用于管理等待获取锁的线程。独占模式只有一个线程可以获得锁。共享模式多个线程可以同时获得锁。
深入探讨 Java 中的锁优化策略,如自旋锁、适应性自旋锁等
Java中的锁优化策略包括自旋锁和适应性自旋锁等技术:
- 自旋锁:当一个线程试图获取锁时,如果锁已经被其他线程持有,则该线程将循环等待,而不是放弃CPU时间片,直到锁被释放。
- 适应性自旋锁:自旋锁的时间长度不是固定的,而是根据前一次自旋锁等待时间以及锁的竞争程度来调整。
总结
锁优化策略描述自旋锁线程循环等待,直到锁被释放。适应性自旋锁自旋锁的时间长度不是固定的,而是动态调整的。
谈谈 Java 中的并发工具类,如 CountDownLatch、CyclicBarrier 等的实现原理
Java并发工具类如 CountDownLatch 和 CyclicBarrier 提供了高级的同步功能:
- CountDownLatch:允许一个或多个线程等待其他线程完成操作。
- CyclicBarrier:允许一组线程相互等待,直到到达某个公共屏障点。
实现原理
- CountDownLatch:通过一个计数器来控制线程的等待和释放。
- CyclicBarrier:通过内部维护的
AQS实现线程间的同步,当线程到达屏障点时,它们会被阻塞,直到所有线程都到达。
总结
工具类描述CountDownLatch通过一个计数器来控制线程的等待和释放。CyclicBarrier通过内部维护的 AQS 实现线程间的同步。
分析 Java 中的线程本地存储(ThreadLocal)的实现机制和应用场景
ThreadLocal 提供了一种线程本地存储的方式,使得每个线程都有自己独立的变量副本。
- 实现机制:
ThreadLocal类中的每个实例都维护了一个ThreadLocalMap,该 map 存储了每个线程的变量副本。 - 应用场景:在多线程环境下,当需要每个线程拥有独立的变量副本时,使用
ThreadLocal。
总结
特性描述实现机制每个 ThreadLocal 实例维护了一个 ThreadLocalMap。应用场景需要每个线程拥有独立变量副本的场景。
描述Java中的并发工具类,如CountDownLatch和CyclicBarrier。
Java提供了一系列的并发工具类,以支持多线程编程中的同步和协作。CountDownLatch和CyclicBarrier是其中两个非常有用的工具类,它们可以帮助程序员处理线程间的协调问题。
CountDownLatch是一个同步辅助类,它允许一个或多个线程等待一组其他线程完成操作。CountDownLatch的计数器在初始化时被设置为一个特定的值,每当一个线程完成了它的任务后,计数器的值就会减少1。当计数器的值变为0时,等待的线程就会被释放,可以继续执行。
例如,如果有10个线程同时开始执行任务,而主线程需要等待这10个线程都完成任务后才能继续执行,可以使用CountDownLatch:
int numberOfThreads = 10;
CountDownLatch latch = new CountDownLatch(numberOfThreads);
for (int i = 0; i < numberOfThreads; i++) {
new Thread(() -> {
// 执行任务 latch.countDown();
// 任务完成后减少计数器
}).start();
}
// 等待所有线程完成任务
latch.await();
// 继续执行主线程
CyclicBarrier也是一个同步辅助类,它允许一组线程互相等待,直到所有线程都到达了某个公共屏障点(Barrier Point),然后这些线程才会继续执行。与CountDownLatch不同的是,CyclicBarrier可以重用,它允许线程在释放后再次阻塞和释放。
例如,如果有多个线程需要分阶段执行任务,每个阶段都需要等待其他线程完成,可以使用CyclicBarrier:
int numberOfThreads = 4;
CyclicBarrier barrier = new CyclicBarrier(
numberOfThreads, () -> {
// 所有线程到达屏障点后执行的代码
System.out.println("All threads have reached the barrier.");
}
);
for (int i = 0; i < numberOfThreads; i++) {
new Thread(() -> {
// 执行第一个阶段的任务
// ... barrier.await();
// 到达屏障点,等待其他线程
// 执行第二个阶段的任务 // ...
}).start();
}
CountDownLatch和CyclicBarrier都是处理并发问题的重要工具,它们可以帮助程序员编写更加健壮和高效的多线程程序。
谈谈线程池的拒绝策略有哪些,以及它们的适用场景
拒绝策略
线程池的拒绝策略是在线程池无法接受更多任务时采取的行为。主要的拒绝策略包括:
- AbortPolicy:默认策略,抛出
RejectedExecutionException异常。 - CallerRunsPolicy:调用者的线程执行该任务,否则阻塞。
- DiscardPolicy:不处理该任务(即丢弃),也不抛出异常。
- DiscardOldestPolicy:丢弃队列中最老的任务,然后重试提交新任务。
- Custom Policy:用户可以实现
RejectedExecutionHandler接口来自定义策略。
适用场景
- AbortPolicy:适用于任务必须执行的场景,当线程池满时抛出异常通知调用者。
- CallerRunsPolicy:适用于任务不能丢失但线程池资源紧张的情况。
- DiscardPolicy:适用于任务可以丢弃的场景,不希望因为失败而产生异常。
- DiscardOldestPolicy:适用于需要优先处理最新任务的场景。
解释线程池中的核心线程和非核心线程的区别和作用
核心线程
- 定义:线程池创建时指定的数量,这些线程始终存活。
- 作用:即使没有任务执行,核心线程也会等待任务的到来。
- 空闲超时:核心线程没有超时限制,除非设置为允许核心线程超时。
非核心线程
- 定义:超出核心线程数目的线程。
- 作用:非核心线程在空闲一段时间后会被销毁。
- 空闲超时:非核心线程有超时限制,超过空闲时间后会被销毁。
描述线程池的工作队列(Work Queue)的类型和特点
工作队列类型
- ArrayBlockingQueue:基于数组结构的有界阻塞队列。
- LinkedBlockingQueue:基于链表结构的阻塞队列,可以选择是否为有界队列。
- SynchronousQueue:不存储元素的阻塞队列,每个插入操作必须等待另一个线程调用移除操作。
- PriorityBlockingQueue:具有优先级的无界阻塞队列。
特点
- ArrayBlockingQueue:固定容量,支持公平性和非公平性插入。
- LinkedBlockingQueue:可选容量,高吞吐量。
- SynchronousQueue:不存储元素,适合传递任务。
- PriorityBlockingQueue:优先级队列,无界,按照优先级顺序取出。
谈谈在多线程环境下,如何实现线程间的通信和协作
方法
- Wait/Notify:使用
wait()和notify()方法进行同步。 - CountDownLatch:允许一个或多个线程等待其他线程完成操作。
- CyclicBarrier:允许一组线程互相等待,直到到达某个公共屏障点。
- Semaphore:控制同时访问特定资源的线程数量。
- Exchanger:使两个线程能够交换数据。
示例
- CountDownLatch:在启动一系列任务后,等待所有任务完成。
- CyclicBarrier:多个线程需要同时开始执行某些操作。
- Semaphore:限制并发访问的数量,例如模拟并发用户登录。
- Exchanger:两个线程需要交换计算结果。
解释线程池的参数配置对性能的影响
参数及其影响
- corePoolSize:核心线程数,影响响应时间和资源利用率。
- maximumPoolSize:最大线程数,影响并发能力和资源消耗。
- keepAliveTime:非核心线程空闲超时时间,影响资源回收速度。
- workQueue:工作队列类型,影响任务执行顺序和性能。
- threadFactory:线程工厂,影响线程的创建和命名。
- handler:拒绝策略,影响任务的处理方式。
影响总结
- 核心线程数:过低会导致任务堆积,过高会导致CPU过度切换。
- 最大线程数:过高可能导致资源耗尽,过低可能限制并发能力。
- 工作队列:不同队列类型对任务执行顺序有不同的影响。
- 拒绝策略:选择合适的策略可以避免资源浪费和系统崩溃。
描述线程池的线程复用机制
复用机制
- 线程创建:当提交的任务超过当前活跃线程数时,线程池会创建新的线程来执行任务。
- 线程重用:当任务完成后,线程不会立即销毁,而是等待新的任务到来。
- 核心线程:核心线程始终存在,即使没有任务也会保持空闲状态。
- 非核心线程:非核心线程在超过空闲时间后会被销毁。
- 工作队列:任务提交到线程池后先进入工作队列等待执行。
优势
- 减少创建和销毁线程的成本。
- 提高响应速度。
- 更好地控制资源利用。
谈谈如何监控线程池的运行状态和性能指标
监控指标
- poolSize:当前线程池中的线程数量。
- activeCount:当前正在执行任务的线程数量。
- completedTaskCount:已完成的任务数量。
- taskCount:提交的任务总数。
- queue:工作队列的状态,如队列长度和内容。
方法
- JMX:使用 Java Management Extensions 监控线程池状态。
- ThreadMXBean:获取线程详情。
- ThreadPoolExecutor:提供多种方法获取运行时信息。
- 日志记录:记录关键事件和异常。
示例
- 使用
ThreadPoolExecutor.getActiveCount()获取当前活跃线程数。 - 使用
ThreadPoolExecutor.getQueue().size()获取工作队列的大小。
解释线程池的饱和策略(Handler)的实现原理
饱和策略原理
- 线程池饱和:当线程池无法接受更多的任务时触发。
- 策略选择:根据配置的拒绝策略处理无法执行的任务。
- 处理方式:丢弃任务、等待、交给调用者执行或自定义处理。
实现
- AbortPolicy:抛出
RejectedExecutionException异常。 - CallerRunsPolicy:交给调用者执行。
- DiscardPolicy:简单地丢弃任务。
- DiscardOldestPolicy:丢弃队列中最旧的任务并重新尝试提交。
- 自定义策略:实现
RejectedExecutionHandler接口。
选择依据
- 任务重要性:决定任务是否可以丢弃。
- 资源限制:确定是否可以继续等待或增加资源。
- 系统稳定性:确保系统不会因过多的任务而导致崩溃。
- 用户体验:考虑任务被丢弃对最终用户的影响。
- 性能影响:评估不同策略对系统整体性能的影响。
- 业务需求:根据具体的业务场景选择合适的策略。
描述在多线程编程中,如何避免活锁(LiveLock)和饥饿(Starvation)现象
活锁
定义: 活锁发生在两个或多个进程不断地重复尝试执行某些动作,但都未能成功,导致它们一直在尝试而没有任何进展。
避免方法:
- 随机化延迟:当检测到冲突时,让进程以随机的时间间隔重试。
- 优先级机制:为线程分配优先级,确保优先级高的线程能够优先获得资源。
表格 还在加载中,请等待加载完成后再尝试复制
饥饿
定义: 饥饿是指某些线程由于某种原因一直得不到执行的机会,即使系统有足够的资源,也无法执行。
避免方法:
- 公平调度:确保线程调度的公平性,比如使用公平锁。
- 老化机制:随着时间推移增加线程的优先级,使其有机会获得资源。
表格 还在加载中,请等待加载完成后再尝试复制
谈谈如何在线程池中处理异常情况
异常处理策略
- 捕获异常:在线程中捕获异常,防止线程异常导致线程池异常。
- 记录日志:记录异常信息以便于调试。
- 重试机制:对于可恢复的异常,可以设置重试机制。
- 自定义异常处理器:实现
Thread.UncaughtExceptionHandler接口来处理未捕获的异常。
表格 还在加载中,请等待加载完成后再尝试复制
解释线程池的预热(Warm-Up)机制及其作用
定义
预热是指在系统启动初期或负载较低时预先启动一定数量的线程,准备好线程池,以减少用户请求的响应时间。
作用
- 减少初始化延迟:避免在高负载下首次创建线程的开销。
- 平滑系统启动:确保系统启动时有足够的线程处理请求。
- 提高响应速度:减少线程创建等待时间,提高用户体验。
实现
- 配置预热策略:设置预热线程数量和预热时间。
- 自定义线程工厂:在创建线程时执行预热逻辑。
- 监控和调整:监控系统负载并在必要时动态调整预热策略。
表格 还在加载中,请等待加载完成后再尝试复制
描述在分布式环境下,如何实现线程池的管理和调度
分布式线程池管理
- 集中式调度:通过中心节点协调线程池的分配和任务的分发。
- 分散式调度:每个节点维护自己的线程池,通过消息中间件协同工作。
- 负载均衡:使用负载均衡器来分发任务,确保各个节点的线程池负载均衡。
分布式线程池特点
- 资源共享:多个节点共享资源池,提高资源利用率。
- 故障恢复:能够快速恢复故障节点上的任务。
- 可扩展性:通过添加节点轻松扩展处理能力。
表格 还在加载中,请等待加载完成后再尝试复制
谈谈如何优化线程池的任务提交和执行效率
优化策略
- 核心线程数:合理设置核心线程数,充分利用 CPU 核心。
- 工作队列:选择合适的工作队列类型,如
ArrayBlockingQueue或LinkedBlockingQueue。 - 任务优先级:为任务设置优先级,确保重要任务优先执行。
- 异步处理:采用异步处理模式,提高任务处理速度。
具体措施
- 核心线程数:设置为核心 CPU 数量,避免过多线程导致上下文切换开销。
- 工作队列:使用无界队列提高吞吐量,使用有界队列限制内存占用。
- 任务优先级:使用优先级队列,确保高优先级任务优先执行。
- 异步处理:将任务拆分为细粒度任务,利用异步机制并行处理。
表格 还在加载中,请等待加载完成后再尝试复制
解释线程池的扩展机制,如何根据业务需求自定义线程池
扩展机制
- 自定义线程工厂:通过实现
ThreadFactory接口来创建线程。 - 自定义拒绝策略:实现
RejectedExecutionHandler接口来处理超出线程池容量的任务。 - 动态调整线程池大小:根据负载自动调整线程池大小。
自定义线程池
- 创建线程池:使用
Executors工具类或直接构造ThreadPoolExecutor。 - 配置参数:设置核心线程数、最大线程数、空闲线程存活时间等。
- 扩展功能:通过实现接口或继承类来扩展线程池的功能。
表格 还在加载中,请等待加载完成后再尝试复制
描述在多线程环境下,如何处理资源竞争和死锁问题
资源竞争
- 互斥锁:使用
synchronized关键字或ReentrantLock来保护共享资源。 - 读写锁:使用
ReadWriteLock来区分读操作和写操作。
死锁预防
- 避免嵌套锁:尽量避免在同一个类中使用多个锁。
- 锁顺序:总是按照相同的顺序获取锁。
- 超时机制:为锁的获取设置超时时间,避免无限期等待。
表格 还在加载中,请等待加载完成后再尝试复制
谈谈如何对线程池进行单元测试和性能测试
单元测试
- Mockito:使用 Mockito 框架模拟线程池的行为。
- JUnit:编写 JUnit 测试用例验证线程池的正确性。
性能测试
- JMeter:使用 JMeter 进行压力测试,评估线程池的性能。
- Gatling:使用 Gatling 进行性能测试,模拟大量并发请求。
表格 还在加载中,请等待加载完成后再尝试复制
解释线程池的线程创建和销毁策略
线程创建
- 核心线程:创建时即存在的线程,始终保持存活状态。
- 非核心线程:根据需要创建,在空闲时销毁。
线程销毁
- 非核心线程:在空闲一段时间后自动销毁。
- 核心线程:通常不会销毁,除非设置了允许核心线程超时。
表格 还在加载中,请等待加载完成后再尝试复制
描述在多线程编程中,如何保证数据的一致性和完整性
一致性保证
- 同步机制:使用
synchronized关键字或显式锁来保证数据访问的原子性。 - 不变性约束:定义对象的状态不变性约束,确保对象状态的正确性。
完整性保证
- 事务管理:使用事务来保证操作的原子性、一致性、隔离性和持久性。
- 版本控制:为数据项添加版本号,确保数据更新的正确性。
表格 还在加载中,请等待加载完成后再尝试复制
谈谈如何在线程池中处理定时任务和周期性任务
定时任务
定时任务是指在指定的时间点执行一次的任务。在Java中,可以通过以下几种方式实现定时任务:
- ScheduledExecutorService
- 使用
ScheduledExecutorService接口提供的方法schedule(Runnable command, long delay, TimeUnit unit)来安排一个任务在未来某个时间点执行。
- 使用
- Timer 类
- 使用
java.util.Timer类的schedule(TimerTask task, long delay)方法来安排一个任务在未来某个时间点执行。
- 使用
周期性任务
周期性任务是指每隔一段时间就执行一次的任务。同样地,可以通过以下方式实现:
- ScheduledExecutorService
- 使用
ScheduledExecutorService的scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)或scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)方法来定期执行任务。
- 使用
- Timer 类
- 使用
java.util.Timer的schedule(TimerTask task, long delay, long period)方法来定期执行任务。
- 使用
示例
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledTasksExample {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(1);
// 定时任务
scheduledExecutor.schedule(() -> System.out.println("定时任务执行"), 5, TimeUnit.SECONDS);
// 周期性任务
scheduledExecutor.scheduleAtFixedRate(() -> System.out.println("周期性任务执行"), 0, 5, TimeUnit.SECONDS);
// 停止线程池
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
scheduledExecutor.shutdown();
}
}
解释线程池的线程优先级(Thread Priority)在任务执行中的作用
线程优先级的作用
在Java中,线程有一个优先级属性,范围从1(最低优先级)到10(最高优先级)。默认情况下,新创建的线程将继承其父线程的优先级。线程优先级的作用在于影响线程调度器决定哪个线程应该先执行。然而,这并不意味着高优先级的线程一定会先执行。
线程优先级对线程池的影响
- 线程池中的线程优先级
- 线程池通常会为它创建的线程设置一个固定的优先级,这个优先级可以在创建线程池时通过
ThreadFactory来设定。
- 线程池通常会为它创建的线程设置一个固定的优先级,这个优先级可以在创建线程池时通过
- 线程优先级与任务执行
- 线程池中的线程优先级对任务执行的影响较小,因为线程池通常会尽可能均匀地分配任务给线程,而不是基于线程优先级来调度任务。
- 实际应用中的考虑
- 如果需要特别关注某些任务的执行顺序,可以通过创建具有不同优先级的线程池来间接影响任务的执行顺序。
- 注意事项
- 线程优先级只是一种提示性的建议,操作系统可能会忽略这种建议,特别是在资源紧张的情况下。
ReentrantLock的内部实现
概述
ReentrantLock 是一个实现了 Lock 接口的可重入锁,它的内部实现依赖于 AbstractQueuedSynchronizer (AQS) 类。
AQS
AQS是一个抽象类,提供了锁的实现框架。- 它定义了一个
volatile int state字段,用于表示锁的状态。 - 它还定义了一些模板方法,如
tryAcquire()和tryRelease(),用于获取和释放锁。
ReentrantLock的实现
- 非公平锁:默认情况下,
ReentrantLock使用非公平锁的实现。 - 公平锁:可以通过构造函数传入
true参数来启用公平锁模式。 - 获取锁:
ReentrantLock在获取锁时会调用AQS的tryAcquire()方法尝试获取锁。 - 释放锁:在释放锁时会调用
AQS的tryRelease()方法。 - 等待队列:如果无法获取锁,线程会被加入到等待队列中,并通过条件变量等待被唤醒。
lock原理
概述
lock 通常指的是 java.util.concurrent.locks.Lock 接口中定义的锁接口,它提供了一种更高级的锁控制机制。
核心方法
lock():获取锁,如果锁已经被持有,则当前线程会阻塞等待。unlock():释放锁。tryLock():尝试获取锁,如果锁已经被持有,则返回false而不会阻塞当前线程。tryLock(long timeout, TimeUnit unit):尝试获取锁,在指定时间内等待锁,超时未获取到锁则返回false。
实现细节
Lock接口的具体实现(如ReentrantLock)通常会使用AbstractQueuedSynchronizer(AQS) 来实现锁的核心功能。AQS使用volatile int state字段来表示锁的状态,并提供了一系列模板方法来控制锁的获取和释放。AQS还维护了一个双向链表结构来保存等待线程的信息,称为 CLH (Craig-Landin-Hagersten) 队列。
示例
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
private final Lock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// 执行代码
} finally {
lock.unlock();
}
}
}
4.6 JUC集合类
- JUC集合: ConcurrentHashMap详解
- JUC集合: CopyOnWriteArrayList详解
- JUC集合: ConcurrentLinkedQueue详解
- JUC集合: BlockingQueue详解
为什么HashTable慢? 它的并发度是什么? 那么ConcurrentHashMap并发度是什么?
Hashtable之所以效率低下主要是因为其实现使用了synchronized关键字对put等操作进行加锁,而synchronized关键字加锁是对整个对象进行加锁,也就是说在进行put等修改Hash表的操作时,锁住了整个Hash表,从而使得其表现的效率低下。
ConcurrentHashMap在JDK1.7和JDK1.8中实现有什么差别? JDK1.8解決了JDK1.7中什么问题
HashTable: 使用了synchronized关键字对put等操作进行加锁;ConcurrentHashMap JDK1.7: 使用分段锁机制实现;ConcurrentHashMap JDK1.8: 则使用数组+链表+红黑树数据结构和CAS原子操作实现;
ConcurrentHashMap JDK1.7实现的原理是什么?
在JDK1.5~1.7版本,Java使用了分段锁机制实现ConcurrentHashMap.
简而言之,ConcurrentHashMap在对象中保存了一个Segment数组,即将整个Hash表划分为多个分段;而每个Segment元素,它通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全;这样,在执行put操作时首先根据hash算法定位到元素属于哪个Segment,然后对该Segment加锁即可。因此,ConcurrentHashMap在多线程并发编程中可是实现多线程put操作。

concurrencyLevel: Segment 数(并行级别、并发数)。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。
ConcurrentHashMap JDK1.7中Segment数(concurrencyLevel)默认值是多少? 为何一旦初始化就不可再扩容?
默认是 16
ConcurrentHashMap JDK1.7说说其put的机制?
整体流程还是比较简单的,由于有独占锁的保护,所以 segment 内部的操作并不复杂
- 计算 key 的 hash 值
- 根据 hash 值找到 Segment 数组中的位置 j; ensureSegment(j) 对 segment[j] 进行初始化(Segment 内部是由 数组+链表 组成的)
- 插入新值到 槽 s 中
ConcurrentHashMap JDK1.7是如何扩容的?
rehash(注:segment 数组不能扩容,扩容是 segment 数组某个位置内部的数组 HashEntry<K,V>[] 进行扩容)
ConcurrentHashMap JDK1.8实现的原理是什么?
在JDK1.7之前,ConcurrentHashMap是通过分段锁机制来实现的,所以其最大并发度受Segment的个数限制。因此,在JDK1.8中,ConcurrentHashMap的实现原理摒弃了这种设计,而是选择了与HashMap类似的数组+链表+红黑树的方式实现,而加锁则采用CAS和synchronized实现。
简而言之:数组+链表+红黑树,CAS
ConcurrentHashMap JDK1.8是如何扩容的?
tryPresize, 扩容也是做翻倍扩容的,扩容后数组容量为原来的 2 倍
ConcurrentHashMap JDK1.8链表转红黑树的时机是什么? 临界值为什么是8?
size = 8, log(N)
ConcurrentHashMap JDK1.8是如何进行数据迁移的?
transfer, 将原来的 tab 数组的元素迁移到新的 nextTab 数组中
先说说非并发集合中Fail-fast机制?
快速失败
CopyOnWriteArrayList的实现原理?
COW基于拷贝
// 将toCopyIn转化为Object[]类型数组,然后设置当前数组
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
属性中有一个可重入锁,用来保证线程安全访问,还有一个Object类型的数组,用来存放具体的元素。当然,也使用到了反射机制和CAS来保证原子性的修改lock域。
// 可重入锁
final transient ReentrantLock lock = new ReentrantLock();
// 对象数组,用于存放元素
private transient volatile Object[] array;
// 反射机制
private static final sun.misc.Unsafe UNSAFE;
// lock域的内存偏移量
private static final long lockOffset;
弱一致性的迭代器原理是怎么样的?
COWIterator<E>
COWIterator表示迭代器,其也有一个Object类型的数组作为CopyOnWriteArrayList数组的快照,这种快照风格的迭代器方法在创建迭代器时使用了对当时数组状态的引用。此数组在迭代器的生存期内不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。在迭代器上进行的元素更改操作(remove、set 和 add)不受支持。这些方法将抛出 UnsupportedOperationException。
CopyOnWriteArrayList为什么并发安全且性能比Vector好?
Vector对单独的add,remove等方法都是在方法上加了synchronized; 并且如果一个线程A调用size时,另一个线程B 执行了remove,然后size的值就不是最新的,然后线程A调用remove就会越界(这时就需要再加一个Synchronized)。这样就导致有了双重锁,效率大大降低,何必呢。于是vector废弃了,要用就用CopyOnWriteArrayList 吧。
CopyOnWriteArrayList有何缺陷,说说其应用场景?
CopyOnWriteArrayList 有几个缺点:
- 由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc
- 不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;
CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用
因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。
要想用线程安全的队列有哪些选择?
Vector,Collections.synchronizedList( List<T> list), ConcurrentLinkedQueue等
ConcurrentLinkedQueue实现的数据结构?
ConcurrentLinkedQueue的数据结构与LinkedBlockingQueue的数据结构相同,都是使用的链表结构。ConcurrentLinkedQueue的数据结构如下:

说明: ConcurrentLinkedQueue采用的链表结构,并且包含有一个头节点和一个尾结点。
ConcurrentLinkedQueue底层原理?
// 反射机制
private static final sun.misc.Unsafe UNSAFE;
// head域的偏移量
private static final long headOffset;
// tail域的偏移量
private static final long tailOffset;
说明: 属性中包含了head域和tail域,表示链表的头节点和尾结点,同时,ConcurrentLinkedQueue也使用了反射机制和CAS机制来更新头节点和尾结点,保证原子性。
ConcurrentLinkedQueue的核心方法有哪些?
offer(),poll(),peek(),isEmpty()等队列常用方法
说说ConcurrentLinkedQueue的HOPS(延迟更新的策略)的设计?
通过上面对offer和poll方法的分析,我们发现tail和head是延迟更新的,两者更新触发时机为:
- tail更新触发时机:当tail指向的节点的下一个节点不为null的时候,会执行定位队列真正的队尾节点的操作,找到队尾节点后完成插入之后才会通过casTail进行tail更新;当tail指向的节点的下一个节点为null的时候,只插入节点不更新tail。
- head更新触发时机:当head指向的节点的item域为null的时候,会执行定位队列真正的队头节点的操作,找到队头节点后完成删除之后才会通过updateHead进行head更新;当head指向的节点的item域不为null的时候,只删除节点不更新head。
并且在更新操作时,源码中会有注释为:hop two nodes at a time。所以这种延迟更新的策略就被叫做HOPS的大概原因是这个(猜的 😃),从上面更新时的状态图可以看出,head和tail的更新是“跳着的”即中间总是间隔了一个。那么这样设计的意图是什么呢?
如果让tail永远作为队列的队尾节点,实现的代码量会更少,而且逻辑更易懂。但是,这样做有一个缺点,如果大量的入队操作,每次都要执行CAS进行tail的更新,汇总起来对性能也会是大大的损耗。如果能减少CAS更新的操作,无疑可以大大提升入队的操作效率,所以doug lea大师每间隔1次(tail和队尾节点的距离为1)进行才利用CAS更新tail。对head的更新也是同样的道理,虽然,这样设计会多出在循环中定位队尾节点,但总体来说读的操作效率要远远高于写的性能,因此,多出来的在循环中定位尾节点的操作的性能损耗相对而言是很小的。
ConcurrentLinkedQueue适合什么样的使用场景?
ConcurrentLinkedQueue通过无锁来做到了更高的并发量,是个高性能的队列,但是使用场景相对不如阻塞队列常见,毕竟取数据也要不停的去循环,不如阻塞的逻辑好设计,但是在并发量特别大的情况下,是个不错的选择,性能上好很多,而且这个队列的设计也是特别费力,尤其的使用的改良算法和对哨兵的处理。整体的思路都是比较严谨的,这个也是使用了无锁造成的,我们自己使用无锁的条件的话,这个队列是个不错的参考。
什么是BlockingDeque? 适合用在什么样的场景?
BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。下图是对这个原理的阐述:

一个线程往里边放,另外一个线程从里边取的一个 BlockingQueue。
一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,直到负责消费的线程从队列中拿走一个对象。 负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。
BlockingQueue大家族有哪些?
ArrayBlockingQueue, DelayQueue, LinkedBlockingQueue, SynchronousQueue...
BlockingQueue常用的方法?
BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到立即执行的话,每个方法的表现也不同。这些方法如下:
| 抛异常 | 特定值 | 阻塞 | 超时 | |
|---|---|---|---|---|
| 插入 | add(o) | offer(o) | put(o) | offer(o, timeout, timeunit) |
| 移除 | remove() | poll() | take() | poll(timeout, timeunit) |
| 检查 | element() | peek() |
四组不同的行为方式解释:
- 抛异常: 如果试图的操作无法立即执行,抛一个异常。
- 特定值: 如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)。
- 阻塞: 如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
- 超时: 如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功(典型的是 true / false)。
BlockingQueue 实现例子?
这里是一个 Java 中使用 BlockingQueue 的示例。本示例使用的是 BlockingQueue 接口的 ArrayBlockingQueue 实现。 首先,BlockingQueueExample 类分别在两个独立的线程中启动了一个 Producer 和 一个 Consumer。Producer 向一个共享的 BlockingQueue 中注入字符串,而 Consumer 则会从中把它们拿出来。
public class BlockingQueueExample {
public static void main(String[] args) throws Exception {
BlockingQueue queue = new ArrayBlockingQueue(1024);
Producer producer = new Producer(queue);
Consumer consumer = new Consumer(queue);
new Thread(producer).start();
new Thread(consumer).start();
Thread.sleep(4000);
}
}
以下是 Producer 类。注意它在每次 put() 调用时是如何休眠一秒钟的。这将导致 Consumer 在等待队列中对象的时候发生阻塞。
public class Producer implements Runnable{
protected BlockingQueue queue = null;
public Producer(BlockingQueue queue) {
this.queue = queue;
}
public void run() {
try {
queue.put("1");
Thread.sleep(1000);
queue.put("2");
Thread.sleep(1000);
queue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
以下是 Consumer 类。它只是把对象从队列中抽取出来,然后将它们打印到 System.out。
public class Consumer implements Runnable{
protected BlockingQueue queue = null;
public Consumer(BlockingQueue queue) {
this.queue = queue;
}
public void run() {
try {
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
什么是BlockingDeque? 适合用在什么样的场景?
java.util.concurrent 包里的 BlockingDeque 接口表示一个线程安放入和提取实例的双端队列。
BlockingDeque 类是一个双端队列,在不能够插入元素时,它将阻塞住试图插入元素的线程;在不能够抽取元素时,它将阻塞住试图抽取的线程。 deque(双端队列) 是 "Double Ended Queue" 的缩写。因此,双端队列是一个你可以从任意一端插入或者抽取元素的队列。
在线程既是一个队列的生产者又是这个队列的消费者的时候可以使用到 BlockingDeque。如果生产者线程需要在队列的两端都可以插入数据,消费者线程需要在队列的两端都可以移除数据,这个时候也可以使用 BlockingDeque。BlockingDeque 图解:

BlockingDeque 与BlockingQueue有何关系,请对比下它们的方法?
BlockingDeque 接口继承自 BlockingQueue 接口。这就意味着你可以像使用一个 BlockingQueue 那样使用 BlockingDeque。如果你这么干的话,各种插入方法将会把新元素添加到双端队列的尾端,而移除方法将会把双端队列的首端的元素移除。正如 BlockingQueue 接口的插入和移除方法一样。
以下是 BlockingDeque 对 BlockingQueue 接口的方法的具体内部实现:
| BlockingQueue | BlockingDeque |
|---|---|
| add() | addLast() |
| offer() x 2 | offerLast() x 2 |
| put() | putLast() |
| remove() | removeFirst() |
| poll() x 2 | pollFirst() |
| take() | takeFirst() |
| element() | getFirst() |
| peek() | peekFirst() |
BlockingDeque大家族有哪些?
LinkedBlockingDeque 是一个双端队列,在它为空的时候,一个试图从中抽取数据的线程将会阻塞,无论该线程是试图从哪一端抽取数据。
BlockingDeque 实现例子?
既然 BlockingDeque 是一个接口,那么你想要使用它的话就得使用它的众多的实现类的其中一个。java.util.concurrent 包提供了以下 BlockingDeque 接口的实现类: LinkedBlockingDeque。
以下是如何使用 BlockingDeque 方法的一个简短代码示例:
BlockingDeque<String> deque = new LinkedBlockingDeque<String>();
deque.addFirst("1");
deque.addLast("2");
String two = deque.takeLast();
String one = deque.takeFirst();
4.7 JUC线程池
FutureTask用来解决什么问题的? 为什么会出现?
FutureTask 为 Future 提供了基础实现,如获取任务执行结果(get)和取消任务(cancel)等。如果任务尚未完成,获取任务执行结果时将会阻塞。一旦执行结束,任务就不能被重启或取消(除非使用runAndReset执行计算)。FutureTask 常用来封装 Callable 和 Runnable,也可以作为一个任务提交到线程池中执行。除了作为一个独立的类之外,此类也提供了一些功能性函数供我们创建自定义 task 类使用。FutureTask 的线程安全由CAS来保证。
FutureTask类结构关系怎么样的?

可以看到,FutureTask实现了RunnableFuture接口,则RunnableFuture接口继承了Runnable接口和Future接口,所以FutureTask既能当做一个Runnable直接被Thread执行,也能作为Future用来得到Callable的计算结果。
FutureTask的线程安全是由什么保证的?
FutureTask 的线程安全由CAS来保证。
FutureTask通常会怎么用? 举例说明。
import java.util.concurrent.*;
public class CallDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/**
* 第一种方式:Future + ExecutorService
* Task task = new Task();
* ExecutorService service = Executors.newCachedThreadPool();
* Future<Integer> future = service.submit(task1);
* service.shutdown();
*/
/**
* 第二种方式: FutureTask + ExecutorService
* ExecutorService executor = Executors.newCachedThreadPool();
* Task task = new Task();
* FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
* executor.submit(futureTask);
* executor.shutdown();
*/
/**
* 第三种方式:FutureTask + Thread
*/
// 2. 新建FutureTask,需要一个实现了Callable接口的类的实例作为构造函数参数
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Task());
// 3. 新建Thread对象并启动
Thread thread = new Thread(futureTask);
thread.setName("Task thread");
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread [" + Thread.currentThread().getName() + "] is running");
// 4. 调用isDone()判断任务是否结束
if(!futureTask.isDone()) {
System.out.println("Task is not done");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int result = 0;
try {
// 5. 调用get()方法获取任务结果,如果任务没有执行完成则阻塞等待
result = futureTask.get();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("result is " + result);
}
// 1. 继承Callable接口,实现call()方法,泛型参数为要返回的类型
static class Task implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("Thread [" + Thread.currentThread().getName() + "] is running");
int result = 0;
for(int i = 0; i < 100;++i) {
result += i;
}
Thread.sleep(3000);
return result;
}
}
}
为什么要有线程池?
线程池能够对线程进行统一分配,调优和监控:
- 降低资源消耗(线程无限制地创建,然后使用完毕后销毁)
- 提高响应速度(无须创建线程)
- 提高线程的可管理性
Java是实现和管理线程池有哪些方式? 请简单举例如何使用。
从JDK 5开始,把工作单元与执行机制分离开来,工作单元包括Runnable和Callable,而执行机制由Executor框架提供。
- WorkerThread
public class WorkerThread implements Runnable {
private String command;
public WorkerThread(String s){
this.command=s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" Start. Command = "+command);
processCommand();
System.out.println(Thread.currentThread().getName()+" End.");
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString(){
return this.command;
}
}
- SimpleThreadPool
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SimpleThreadPool {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
Runnable worker = new WorkerThread("" + i);
executor.execute(worker);
}
executor.shutdown(); // This will make the executor accept no new threads and finish all existing threads in the queue
while (!executor.isTerminated()) { // Wait until all threads are finish,and also you can use "executor.awaitTermination();" to wait
}
System.out.println("Finished all threads");
}
}
程序中我们创建了固定大小为五个工作线程的线程池。然后分配给线程池十个工作,因为线程池大小为五,它将启动五个工作线程先处理五个工作,其他的工作则处于等待状态,一旦有工作完成,空闲下来工作线程就会捡取等待队列里的其他工作进行执行。
这里是以上程序的输出。
pool-1-thread-2 Start. Command = 1
pool-1-thread-4 Start. Command = 3
pool-1-thread-1 Start. Command = 0
pool-1-thread-3 Start. Command = 2
pool-1-thread-5 Start. Command = 4
pool-1-thread-4 End.
pool-1-thread-5 End.
pool-1-thread-1 End.
pool-1-thread-3 End.
pool-1-thread-3 Start. Command = 8
pool-1-thread-2 End.
pool-1-thread-2 Start. Command = 9
pool-1-thread-1 Start. Command = 7
pool-1-thread-5 Start. Command = 6
pool-1-thread-4 Start. Command = 5
pool-1-thread-2 End.
pool-1-thread-4 End.
pool-1-thread-3 End.
pool-1-thread-5 End.
pool-1-thread-1 End.
Finished all threads
输出表明线程池中至始至终只有五个名为 "pool-1-thread-1" 到 "pool-1-thread-5" 的五个线程,这五个线程不随着工作的完成而消亡,会一直存在,并负责执行分配给线程池的任务,直到线程池消亡。
Executors 类提供了使用了 ThreadPoolExecutor 的简单的 ExecutorService 实现,但是 ThreadPoolExecutor 提供的功能远不止于此。我们可以在创建 ThreadPoolExecutor 实例时指定活动线程的数量,我们也可以限制线程池的大小并且创建我们自己的 RejectedExecutionHandler 实现来处理不能适应工作队列的工作。
这里是我们自定义的 RejectedExecutionHandler 接口的实现。
- RejectedExecutionHandlerImpl.java
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
public class RejectedExecutionHandlerImpl implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println(r.toString() + " is rejected");
}
}
ThreadPoolExecutor 提供了一些方法,我们可以使用这些方法来查询 executor 的当前状态,线程池大小,活动线程数量以及任务数量。因此我是用来一个监控线程在特定的时间间隔内打印 executor 信息。
- MyMonitorThread.java
import java.util.concurrent.ThreadPoolExecutor;
public class MyMonitorThread implements Runnable
{
private ThreadPoolExecutor executor;
private int seconds;
private boolean run=true;
public MyMonitorThread(ThreadPoolExecutor executor, int delay)
{
this.executor = executor;
this.seconds=delay;
}
public void shutdown(){
this.run=false;
}
@Override
public void run()
{
while(run){
System.out.println(
String.format("[monitor] [%d/%d] Active: %d, Completed: %d, Task: %d, isShutdown: %s, isTerminated: %s",
this.executor.getPoolSize(),
this.executor.getCorePoolSize(),
this.executor.getActiveCount(),
this.executor.getCompletedTaskCount(),
this.executor.getTaskCount(),
this.executor.isShutdown(),
this.executor.isTerminated()));
try {
Thread.sleep(seconds*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
这里是使用 ThreadPoolExecutor 的线程池实现例子。
- WorkerPool.java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class WorkerPool {
public static void main(String args[]) throws InterruptedException{
//RejectedExecutionHandler implementation
RejectedExecutionHandlerImpl rejectionHandler = new RejectedExecutionHandlerImpl();
//Get the ThreadFactory implementation to use
ThreadFactory threadFactory = Executors.defaultThreadFactory();
//creating the ThreadPoolExecutor
ThreadPoolExecutor executorPool = new ThreadPoolExecutor(2, 4, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2), threadFactory, rejectionHandler);
//start the monitoring thread
MyMonitorThread monitor = new MyMonitorThread(executorPool, 3);
Thread monitorThread = new Thread(monitor);
monitorThread.start();
//submit work to the thread pool
for(int i=0; i<10; i++){
executorPool.execute(new WorkerThread("cmd"+i));
}
Thread.sleep(30000);
//shut down the pool
executorPool.shutdown();
//shut down the monitor thread
Thread.sleep(5000);
monitor.shutdown();
}
}
注意在初始化 ThreadPoolExecutor 时,我们保持初始池大小为 2,最大池大小为 4 而工作队列大小为 2。因此如果已经有四个正在执行的任务而此时分配来更多任务的话,工作队列将仅仅保留他们(新任务)中的两个,其他的将会被 RejectedExecutionHandlerImpl 处理。
上面程序的输出可以证实以上观点。
pool-1-thread-1 Start. Command = cmd0
pool-1-thread-4 Start. Command = cmd5
cmd6 is rejected
pool-1-thread-3 Start. Command = cmd4
pool-1-thread-2 Start. Command = cmd1
cmd7 is rejected
cmd8 is rejected
cmd9 is rejected
[monitor] [0/2] Active: 4, Completed: 0, Task: 6, isShutdown: false, isTerminated: false
[monitor] [4/2] Active: 4, Completed: 0, Task: 6, isShutdown: false, isTerminated: false
pool-1-thread-4 End.
pool-1-thread-1 End.
pool-1-thread-2 End.
pool-1-thread-3 End.
pool-1-thread-1 Start. Command = cmd3
pool-1-thread-4 Start. Command = cmd2
[monitor] [4/2] Active: 2, Completed: 4, Task: 6, isShutdown: false, isTerminated: false
[monitor] [4/2] Active: 2, Completed: 4, Task: 6, isShutdown: false, isTerminated: false
pool-1-thread-1 End.
pool-1-thread-4 End.
[monitor] [4/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [2/2] Active: 0, Completed: 6, Task: 6, isShutdown: false, isTerminated: false
[monitor] [0/2] Active: 0, Completed: 6, Task: 6, isShutdown: true, isTerminated: true
[monitor] [0/2] Active: 0, Completed: 6, Task: 6, isShutdown: true, isTerminated: true
注意 executor 的活动任务、完成任务以及所有完成任务,这些数量上的变化。我们可以调用 shutdown() 方法来结束所有提交的任务并终止线程池。
ThreadPoolExecutor的原理?
其实java线程池的实现原理很简单,说白了就是一个线程集合workerSet和一个阻塞队列workQueue。当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入workQueue中。workerSet中的线程会不断的从workQueue中获取线程然后执行。当workQueue中没有任务的时候,worker就会阻塞,直到队列中有任务了就取出来继续执行。

当一个任务提交至线程池之后:
- 线程池首先当前运行的线程数量是否少于corePoolSize。如果是,则创建一个新的工作线程来执行任务。如果都在执行任务,则进入2.
- 判断BlockingQueue是否已经满了,倘若还没有满,则将线程放入BlockingQueue。否则进入3.
- 如果创建一个新的工作线程将使当前运行的线程数量超过maximumPoolSize,则交给RejectedExecutionHandler来处理任务。
当ThreadPoolExecutor创建新线程时,通过CAS来更新线程池的状态ctl.
ThreadPoolExecutor有哪些核心的配置参数? 请简要说明
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
corePoolSize线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize, 即使有其他空闲线程能够执行新来的任务, 也会继续创建线程;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。workQueue用来保存等待被执行的任务的阻塞队列. 在JDK中提供了如下阻塞队列: 具体可以参考JUC集合: BlockingQueue详解ArrayBlockingQueue: 基于数组结构的有界阻塞队列,按FIFO排序任务;LinkedBlockingQueue: 基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQueue;SynchronousQueue: 一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue;PriorityBlockingQueue: 具有优先级的无界阻塞队列;
LinkedBlockingQueue比ArrayBlockingQueue在插入删除节点性能方面更优,但是二者在put(), take()任务的时均需要加锁,SynchronousQueue使用无锁算法,根据节点的状态判断执行,而不需要用到锁,其核心是Transfer.transfer().
maximumPoolSize线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;当阻塞队列是无界队列, 则maximumPoolSize则不起作用, 因为无法提交至核心线程池的线程会一直持续地放入workQueue.keepAliveTime线程空闲时的存活时间,即当线程没有任务执行时,该线程继续存活的时间;默认情况下,该参数只在线程数大于corePoolSize时才有用, 超过这个时间的空闲线程将被终止;unitkeepAliveTime的单位threadFactory创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名。默认为DefaultThreadFactoryhandler线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:AbortPolicy: 直接抛出异常,默认策略;CallerRunsPolicy: 用调用者所在的线程来执行任务;DiscardOldestPolicy: 丢弃阻塞队列中靠最前的任务,并执行当前任务;DiscardPolicy: 直接丢弃任务;
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
ThreadPoolExecutor可以创建哪是哪三种线程池呢?
- newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
线程池的线程数量达corePoolSize后,即使线程池没有可执行任务时,也不会释放线程。
FixedThreadPool的工作队列为无界队列LinkedBlockingQueue(队列容量为Integer.MAX_VALUE), 这会导致以下问题:
线程池里的线程数量不超过corePoolSize,这导致了maximumPoolSize和keepAliveTime将会是个无用参数
由于使用了无界队列, 所以FixedThreadPool永远不会拒绝, 即饱和策略失效
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
初始化的线程池中只有一个线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行.
由于使用了无界队列, 所以SingleThreadPool永远不会拒绝, 即饱和策略失效
- newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列; 和newFixedThreadPool创建的线程池不同,newCachedThreadPool在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销; 执行过程与前两种稍微不同:
- 主线程调用SynchronousQueue的offer()方法放入task, 倘若此时线程池中有空闲的线程尝试读取 SynchronousQueue的task, 即调用了SynchronousQueue的poll(), 那么主线程将该task交给空闲线程. 否则执行(2)
- 当线程池为空或者没有空闲的线程, 则创建新的线程执行任务.
- 执行完任务的线程倘若在60s内仍空闲, 则会被终止. 因此长时间空闲的CachedThreadPool不会持有任何线程资源.
当队列满了并且worker的数量达到maxSize的时候,会怎么样?
当队列满了并且worker的数量达到maxSize的时候,执行具体的拒绝策略
private volatile RejectedExecutionHandler handler;
说说ThreadPoolExecutor有哪些RejectedExecutionHandler策略? 默认是什么策略?
- AbortPolicy, 默认
该策略是线程池的默认策略。使用该策略时,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。 源码如下:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
//不做任何处理,直接抛出异常
throw new RejectedExecutionException("xxx");
}
- DiscardPolicy
这个策略和AbortPolicy的slient版本,如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常。 源码如下:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
//就是一个空的方法
}
- DiscardOldestPolicy
这个策略从字面上也很好理解,丢弃最老的。也就是说如果队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列。 因为队列是队尾进,队头出,所以队头元素是最老的,因此每次都是移除对头元素后再尝试入队。 源码如下:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
//移除队头元素
e.getQueue().poll();
//再尝试入队
e.execute(r);
}
}
- CallerRunsPolicy
使用此策略,如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。就像是个急脾气的人,我等不到别人来做这件事就干脆自己干。 源码如下:
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
//直接执行run方法
r.run();
}
}
简要说下线程池的任务执行机制?
execute –> addWorker –>runworker (getTask)
- 线程池的工作线程通过Woker类实现,在ReentrantLock锁的保证下,把Woker实例插入到HashSet后,并启动Woker中的线程。
- 从Woker类的构造方法实现可以发现: 线程工厂在创建线程thread时,将Woker实例本身this作为参数传入,当执行start方法启动线程thread时,本质是执行了Worker的runWorker方法。
- firstTask执行完成之后,通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源;
线程池中任务是如何提交的?

- submit任务,等待线程池execute
- 执行FutureTask类的get方法时,会把主线程封装成WaitNode节点并保存在waiters链表中, 并阻塞等待运行结果;
- FutureTask任务执行完成后,通过UNSAFE设置waiters相应的waitNode为null,并通过LockSupport类unpark方法唤醒主线程;
public class Test{
public static void main(String[] args) {
ExecutorService es = Executors.newCachedThreadPool();
Future<String> future = es.submit(new Callable<String>() {
@Override
public String call() throws Exception {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "future result";
}
});
try {
String result = future.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
在实际业务场景中,Future和Callable基本是成对出现的,Callable负责产生结果,Future负责获取结果。
- Callable接口类似于Runnable,只是Runnable没有返回值。
- Callable任务除了返回正常结果之外,如果发生异常,该异常也会被返回,即Future可以拿到异步执行任务各种结果;
- Future.get方法会导致主线程阻塞,直到Callable任务执行完成;
线程池中任务是如何关闭的?
- shutdown
将线程池里的线程状态设置成SHUTDOWN状态, 然后中断所有没有正在执行任务的线程.
- shutdownNow
将线程池里的线程状态设置成STOP状态, 然后停止所有正在执行或暂停任务的线程. 只要调用这两个关闭方法中的任意一个, isShutDown() 返回true. 当所有任务都成功关闭了, isTerminated()返回true.
在配置线程池的时候需要考虑哪些配置因素?
从任务的优先级,任务的执行时间长短,任务的性质(CPU密集/ IO密集),任务的依赖关系这四个角度来分析。并且近可能地使用有界的工作队列。
性质不同的任务可用使用不同规模的线程池分开处理:
- CPU密集型: 尽可能少的线程,Ncpu+1
- IO密集型: 尽可能多的线程, Ncpu*2,比如数据库连接池
- 混合型: CPU密集型的任务与IO密集型任务的执行时间差别较小,拆分为两个线程池;否则没有必要拆分。
如何监控线程池的状态?
可以使用ThreadPoolExecutor以下方法:
getTaskCount()Returns the approximate total number of tasks that have ever been scheduled for execution.getCompletedTaskCount()Returns the approximate total number of tasks that have completed execution. 返回结果少于getTaskCount()。getLargestPoolSize()Returns the largest number of threads that have ever simultaneously been in the pool. 返回结果小于等于maximumPoolSizegetPoolSize()Returns the current number of threads in the pool.getActiveCount()Returns the approximate number of threads that are actively executing tasks.
为什么很多公司不允许使用Executors去创建线程池? 那么推荐怎么使用呢?
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors各个方法的弊端:
- newFixedThreadPool和newSingleThreadExecutor: 主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
- newCachedThreadPool和newScheduledThreadPool: 主要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。
- 推荐方式 1 首先引入:commons-lang3包
ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(1,
new BasicThreadFactory.Builder().namingPattern("example-schedule-pool-%d").daemon(true).build());
- 推荐方式 2 首先引入:com.google.guava包
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
//Common Thread Pool
ExecutorService pool = new ThreadPoolExecutor(5, 200, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
// excute
pool.execute(()-> System.out.println(Thread.currentThread().getName()));
//gracefully shutdown
pool.shutdown();
- 推荐方式 3 spring配置线程池方式:自定义线程工厂bean需要实现ThreadFactory,可参考该接口的其它默认实现类,使用方式直接注入bean调用execute(Runnable task)方法即可
<bean id="userThreadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="10" />
<property name="maxPoolSize" value="100" />
<property name="queueCapacity" value="2000" />
<property name="threadFactory" value= threadFactory />
<property name="rejectedExecutionHandler">
<ref local="rejectedExecutionHandler" />
</property>
</bean>
//in code
userThreadPool.execute(thread);
ScheduledThreadPoolExecutor要解决什么样的问题?
在很多业务场景中,我们可能需要周期性的运行某项任务来获取结果,比如周期数据统计,定时发送数据等。在并发包出现之前,Java 早在1.3就提供了 Timer 类(只需要了解,目前已渐渐被 ScheduledThreadPoolExecutor 代替)来适应这些业务场景。随着业务量的不断增大,我们可能需要多个工作线程运行任务来尽可能的增加产品性能,或者是需要更高的灵活性来控制和监控这些周期业务。这些都是 ScheduledThreadPoolExecutor 诞生的必然性。
ScheduledThreadPoolExecutor相比ThreadPoolExecutor有哪些特性?
ScheduledThreadPoolExecutor继承自 ThreadPoolExecutor,为任务提供延迟或周期执行,属于线程池的一种。和 ThreadPoolExecutor 相比,它还具有以下几种特性:
- 使用专门的任务类型—ScheduledFutureTask 来执行周期任务,也可以接收不需要时间调度的任务(这些任务通过 ExecutorService 来执行)。
- 使用专门的存储队列—DelayedWorkQueue 来存储任务,DelayedWorkQueue 是无界延迟队列DelayQueue 的一种。相比ThreadPoolExecutor也简化了执行机制(delayedExecute方法,后面单独分析)。
- 支持可选的run-after-shutdown参数,在池被关闭(shutdown)之后支持可选的逻辑来决定是否继续运行周期或延迟任务。并且当任务(重新)提交操作与 shutdown 操作重叠时,复查逻辑也不相同。
ScheduledThreadPoolExecutor有什么样的数据结构,核心内部类和抽象类?

ScheduledThreadPoolExecutor继承自 ThreadPoolExecutor:
- 详情请参考: JUC线程池: ThreadPoolExecutor详解
ScheduledThreadPoolExecutor 内部构造了两个内部类 ScheduledFutureTask 和 DelayedWorkQueue:
ScheduledFutureTask: 继承了FutureTask,说明是一个异步运算任务;最上层分别实现了Runnable、Future、Delayed接口,说明它是一个可以延迟执行的异步运算任务。DelayedWorkQueue: 这是 ScheduledThreadPoolExecutor 为存储周期或延迟任务专门定义的一个延迟队列,继承了 AbstractQueue,为了契合 ThreadPoolExecutor 也实现了 BlockingQueue 接口。它内部只允许存储 RunnableScheduledFuture 类型的任务。与 DelayQueue 的不同之处就是它只允许存放 RunnableScheduledFuture 对象,并且自己实现了二叉堆(DelayQueue 是利用了 PriorityQueue 的二叉堆结构)。
ScheduledThreadPoolExecutor有哪两个关闭策略? 区别是什么?
shutdown: 在shutdown方法中调用的关闭钩子onShutdown方法,它的主要作用是在关闭线程池后取消并清除由于关闭策略不应该运行的所有任务,这里主要是根据 run-after-shutdown 参数(continueExistingPeriodicTasksAfterShutdown和executeExistingDelayedTasksAfterShutdown)来决定线程池关闭后是否关闭已经存在的任务。
showDownNow: 立即关闭
ScheduledThreadPoolExecutor中scheduleAtFixedRate 和 scheduleWithFixedDelay区别是什么?
注意scheduleAtFixedRate和scheduleWithFixedDelay的区别: 乍一看两个方法一模一样,其实,在unit.toNanos这一行代码中还是有区别的。没错,scheduleAtFixedRate传的是正值,而scheduleWithFixedDelay传的则是负值,这个值就是 ScheduledFutureTask 的period属性。
为什么ThreadPoolExecutor 的调整策略却不适用于 ScheduledThreadPoolExecutor?
例如: 由于 ScheduledThreadPoolExecutor 是一个固定核心线程数大小的线程池,并且使用了一个无界队列,所以调整maximumPoolSize对其没有任何影响(所以 ScheduledThreadPoolExecutor 没有提供可以调整最大线程数的构造函数,默认最大线程数固定为Integer.MAX_VALUE)。此外,设置corePoolSize为0或者设置核心线程空闲后清除(allowCoreThreadTimeOut)同样也不是一个好的策略,因为一旦周期任务到达某一次运行周期时,可能导致线程池内没有线程去处理这些任务。
Executors 提供了几种方法来构造 ScheduledThreadPoolExecutor?
- newScheduledThreadPool: 可指定核心线程数的线程池。
- newSingleThreadScheduledExecutor: 只有一个工作线程的线程池。如果内部工作线程由于执行周期任务异常而被终止,则会新建一个线程替代它的位置。
Fork/Join主要用来解决什么样的问题?
ForkJoinPool 是JDK 7加入的一个线程池类。Fork/Join 技术是分治算法(Divide-and-Conquer)的并行实现,它是一项可以获得良好的并行性能的简单且高效的设计技术。目的是为了帮助我们更好地利用多处理器带来的好处,使用所有可用的运算能力来提升应用的性能。
Fork/Join框架是在哪个JDK版本中引入的?
JDK 7
Fork/Join框架主要包含哪三个模块? 模块之间的关系是怎么样的?
Fork/Join框架主要包含三个模块:
- 任务对象:
ForkJoinTask(包括RecursiveTask、RecursiveAction和CountedCompleter) - 执行Fork/Join任务的线程:
ForkJoinWorkerThread - 线程池:
ForkJoinPool
这三者的关系是: ForkJoinPool可以通过池中的ForkJoinWorkerThread来处理ForkJoinTask任务。
ForkJoinPool类继承关系?

内部类介绍:
- ForkJoinWorkerThreadFactory: 内部线程工厂接口,用于创建工作线程ForkJoinWorkerThread
- DefaultForkJoinWorkerThreadFactory: ForkJoinWorkerThreadFactory 的默认实现类
- InnocuousForkJoinWorkerThreadFactory: 实现了 ForkJoinWorkerThreadFactory,无许可线程工厂,当系统变量中有系统安全管理相关属性时,默认使用这个工厂创建工作线程。
- EmptyTask: 内部占位类,用于替换队列中 join 的任务。
- ManagedBlocker: 为 ForkJoinPool 中的任务提供扩展管理并行数的接口,一般用在可能会阻塞的任务(如在 Phaser 中用于等待 phase 到下一个generation)。
- WorkQueue: ForkJoinPool 的核心数据结构,本质上是work-stealing 模式的双端任务队列,内部存放 ForkJoinTask 对象任务,使用 @Contented 注解修饰防止伪共享。
- 工作线程在运行中产生新的任务(通常是因为调用了 fork())时,此时可以把 WorkQueue 的数据结构视为一个栈,新的任务会放入栈顶(top 位);工作线程在处理自己工作队列的任务时,按照 LIFO 的顺序。
- 工作线程在处理自己的工作队列同时,会尝试窃取一个任务(可能是来自于刚刚提交到 pool 的任务,或是来自于其他工作线程的队列任务),此时可以把 WorkQueue 的数据结构视为一个 FIFO 的队列,窃取的任务位于其他线程的工作队列的队首(base位)。
- 伪共享状态: 缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
ForkJoinTask抽象类继承关系?

ForkJoinTask 实现了 Future 接口,说明它也是一个可取消的异步运算任务,实际上ForkJoinTask 是 Future 的轻量级实现,主要用在纯粹是计算的函数式任务或者操作完全独立的对象计算任务。fork 是主运行方法,用于异步执行;而 join 方法在任务结果计算完毕之后才会运行,用来合并或返回计算结果。 其内部类都比较简单,ExceptionNode 是用于存储任务执行期间的异常信息的单向链表;其余四个类是为 Runnable/Callable 任务提供的适配器类,用于把 Runnable/Callable 转化为 ForkJoinTask 类型的任务(因为 ForkJoinPool 只可以运行 ForkJoinTask 类型的任务)。
整个Fork/Join 框架的执行流程/运行机制是怎么样的?
- 首先介绍任务的提交流程 - 外部任务(external/submissions task)提交
- 然后介绍任务的提交流程 - 子任务(Worker task)提交
- 再分析任务的执行过程(ForkJoinWorkerThread.run()到ForkJoinTask.doExec()这一部分);
- 最后介绍任务的结果获取(ForkJoinTask.join()和ForkJoinTask.invoke())
具体阐述Fork/Join的分治思想和work-stealing 实现方式?
- 分治算法(Divide-and-Conquer)
分治算法(Divide-and-Conquer)把任务递归的拆分为各个子任务,这样可以更好的利用系统资源,尽可能的使用所有可用的计算能力来提升应用性能。首先看一下 Fork/Join 框架的任务运行机制:

- work-stealing(工作窃取)算法
work-stealing(工作窃取)算法: 线程池内的所有工作线程都尝试找到并执行已经提交的任务,或者是被其他活动任务创建的子任务(如果不存在就阻塞等待)。这种特性使得 ForkJoinPool 在运行多个可以产生子任务的任务,或者是提交的许多小任务时效率更高。尤其是构建异步模型的 ForkJoinPool 时,对不需要合并(join)的事件类型任务也非常适用。
在 ForkJoinPool 中,线程池中每个工作线程(ForkJoinWorkerThread)都对应一个任务队列(WorkQueue),工作线程优先处理来自自身队列的任务(LIFO或FIFO顺序,参数 mode 决定),然后以FIFO的顺序随机窃取其他队列中的任务。
具体思路如下:
- 每个线程都有自己的一个WorkQueue,该工作队列是一个双端队列。
- 队列支持三个功能push、pop、poll
- push/pop只能被队列的所有者线程调用,而poll可以被其他线程调用。
- 划分的子任务调用fork时,都会被push到自己的队列中。
- 默认情况下,工作线程从自己的双端队列获出任务并执行。
- 当自己的队列为空时,线程随机从另一个线程的队列末尾调用poll方法窃取任务。

有哪些JDK源码中使用了Fork/Join思想?
我们常用的数组工具类 Arrays 在JDK 8之后新增的并行排序方法(parallelSort)就运用了 ForkJoinPool 的特性,还有 ConcurrentHashMap 在JDK 8之后添加的函数式方法(如forEach等)也有运用。
如何使用Executors工具类创建ForkJoinPool?
Java8在Executors工具类中新增了两个工厂方法:
// parallelism定义并行级别
public static ExecutorService newWorkStealingPool(int parallelism);
// 默认并行级别为JVM可用的处理器个数
// Runtime.getRuntime().availableProcessors()
public static ExecutorService newWorkStealingPool();
写一个例子: 用ForkJoin方式实现1+2+3+...+100000?
public class Test {
static final class SumTask extends RecursiveTask<Integer> {
private static final long serialVersionUID = 1L;
final int start; //开始计算的数
final int end; //最后计算的数
SumTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
//如果计算量小于1000,那么分配一个线程执行if中的代码块,并返回执行结果
if(end - start < 1000) {
System.out.println(Thread.currentThread().getName() + " 开始执行: " + start + "-" + end);
int sum = 0;
for(int i = start; i <= end; i++)
sum += i;
return sum;
}
//如果计算量大于1000,那么拆分为两个任务
SumTask task1 = new SumTask(start, (start + end) / 2);
SumTask task2 = new SumTask((start + end) / 2 + 1, end);
//执行任务
task1.fork();
task2.fork();
//获取任务执行的结果
return task1.join() + task2.join();
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Integer> task = new SumTask(1, 10000);
pool.submit(task);
System.out.println(task.get());
}
}
- 执行结果
ForkJoinPool-1-worker-1 开始执行: 1-625
ForkJoinPool-1-worker-7 开始执行: 6251-6875
ForkJoinPool-1-worker-6 开始执行: 5626-6250
ForkJoinPool-1-worker-10 开始执行: 3751-4375
ForkJoinPool-1-worker-13 开始执行: 2501-3125
ForkJoinPool-1-worker-8 开始执行: 626-1250
ForkJoinPool-1-worker-11 开始执行: 5001-5625
ForkJoinPool-1-worker-3 开始执行: 7501-8125
ForkJoinPool-1-worker-14 开始执行: 1251-1875
ForkJoinPool-1-worker-4 开始执行: 9376-10000
ForkJoinPool-1-worker-8 开始执行: 8126-8750
ForkJoinPool-1-worker-0 开始执行: 1876-2500
ForkJoinPool-1-worker-12 开始执行: 4376-5000
ForkJoinPool-1-worker-5 开始执行: 8751-9375
ForkJoinPool-1-worker-7 开始执行: 6876-7500
ForkJoinPool-1-worker-1 开始执行: 3126-3750
50005000
Fork/Join在使用时有哪些注意事项? 结合JDK中的斐波那契数列实例具体说明。
斐波那契数列: 1、1、2、3、5、8、13、21、34、…… 公式 : F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)
public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool(4); // 最大并发数4
Fibonacci fibonacci = new Fibonacci(20);
long startTime = System.currentTimeMillis();
Integer result = forkJoinPool.invoke(fibonacci);
long endTime = System.currentTimeMillis();
System.out.println("Fork/join sum: " + result + " in " + (endTime - startTime) + " ms.");
}
//以下为官方API文档示例
static class Fibonacci extends RecursiveTask<Integer> {
final int n;
Fibonacci(int n) {
this.n = n;
}
@Override
protected Integer compute() {
if (n <= 1) {
return n;
}
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2);
return f2.compute() + f1.join();
}
}
当然你也可以两个任务都fork,要注意的是两个任务都fork的情况,必须按照f1.fork(),f2.fork(), f2.join(),f1.join()这样的顺序,不然有性能问题,详见上面注意事项中的说明。
官方API文档是这样写到的,所以平日用invokeAll就好了。invokeAll会把传入的任务的第一个交给当前线程来执行,其他的任务都fork加入工作队列,这样等于利用当前线程也执行任务了。
{
// ...
Fibonacci f1 = new Fibonacci(n - 1);
Fibonacci f2 = new Fibonacci(n - 2);
invokeAll(f1,f2);
return f2.join() + f1.join();
}
public static void invokeAll(ForkJoinTask<?>... tasks) {
Throwable ex = null;
int last = tasks.length - 1;
for (int i = last; i >= 0; --i) {
ForkJoinTask<?> t = tasks[i];
if (t == null) {
if (ex == null)
ex = new NullPointerException();
}
else if (i != 0) //除了第一个都fork
t.fork();
else if (t.doInvoke() < NORMAL && ex == null) //留一个自己执行
ex = t.getException();
}
for (int i = 1; i <= last; ++i) {
ForkJoinTask<?> t = tasks[i];
if (t != null) {
if (ex != null)
t.cancel(false);
else if (t.doJoin() < NORMAL)
ex = t.getException();
}
}
if (ex != null)
rethrow(ex);
}
4.8 JUC工具类
- JUC工具类: CountDownLatch详解
- JUC工具类: CyclicBarrier详解
- JUC工具类: Semaphore详解
- JUC工具类: Phaser详解
- JUC工具类: Exchanger详解
- Java 并发 - ThreadLocal详解
什么是CountDownLatch?
CountDownLatch底层也是由AQS,用来同步一个或多个任务的常用并发工具类,强制它们等待由其他任务执行的一组操作完成。
CountDownLatch底层实现原理?
其底层是由AQS提供支持,所以其数据结构可以参考AQS的数据结构,而AQS的数据结构核心就是两个虚拟队列: 同步队列sync queue 和条件队列condition queue,不同的条件会有不同的条件队列。CountDownLatch典型的用法是将一个程序分为n个互相独立的可解决任务,并创建值为n的CountDownLatch。当每一个任务完成时,都会在这个锁存器上调用countDown,等待问题被解决的任务调用这个锁存器的await,将他们自己拦住,直至锁存器计数结束。
CountDownLatch一次可以唤醒几个任务?
多个
CountDownLatch有哪些主要方法?
await(), 此函数将会使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。
countDown(), 此函数将递减锁存器的计数,如果计数到达零,则释放所有等待的线程
写道题:实现一个容器,提供两个方法,add,size 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束?
说出使用CountDownLatch 代替wait notify 好处?
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* 使用CountDownLatch 代替wait notify 好处是通讯方式简单,不涉及锁定 Count 值为0时当前线程继续执行,
*/
public class T3 {
volatile List list = new ArrayList();
public void add(int i){
list.add(i);
}
public int getSize(){
return list.size();
}
public static void main(String[] args) {
T3 t = new T3();
CountDownLatch countDownLatch = new CountDownLatch(1);
new Thread(() -> {
System.out.println("t2 start");
if(t.getSize() != 5){
try {
countDownLatch.await();
System.out.println("t2 end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t2").start();
new Thread(()->{
System.out.println("t1 start");
for (int i = 0;i<9;i++){
t.add(i);
System.out.println("add"+ i);
if(t.getSize() == 5){
System.out.println("countdown is open");
countDownLatch.countDown();
}
}
System.out.println("t1 end");
},"t1").start();
}
}
什么是CyclicBarrier?
- 对于CountDownLatch,其他线程为游戏玩家,比如英雄联盟,主线程为控制游戏开始的线程。在所有的玩家都准备好之前,主线程是处于等待状态的,也就是游戏不能开始。当所有的玩家准备好之后,下一步的动作实施者为主线程,即开始游戏。
- 对于CyclicBarrier,假设有一家公司要全体员工进行团建活动,活动内容为翻越三个障碍物,每一个人翻越障碍物所用的时间是不一样的。但是公司要求所有人在翻越当前障碍物之后再开始翻越下一个障碍物,也就是所有人翻越第一个障碍物之后,才开始翻越第二个,以此类推。类比地,每一个员工都是一个“其他线程”。当所有人都翻越的所有的障碍物之后,程序才结束。而主线程可能早就结束了,这里我们不用管主线程。
CountDownLatch和CyclicBarrier对比?
- CountDownLatch减计数,CyclicBarrier加计数。
- CountDownLatch是一次性的,CyclicBarrier可以重用。
- CountDownLatch和CyclicBarrier都有让多个线程等待同步然后再开始下一步动作的意思,但是CountDownLatch的下一步的动作实施者是主线程,具有不可重复性;而CyclicBarrier的下一步动作实施者还是“其他线程”本身,具有往复多次实施动作的特点。
什么是Semaphore?
Semaphore底层是基于AbstractQueuedSynchronizer来实现的。Semaphore称为计数信号量,它允许n个任务同时访问某个资源,可以将信号量看做是在向外分发使用资源的许可证,只有成功获取许可证,才能使用资源
Semaphore内部原理?
Semaphore总共有三个内部类,并且三个内部类是紧密相关的,下面先看三个类的关系。

说明: Semaphore与ReentrantLock的内部类的结构相同,类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类。下面逐个进行分析。
Semaphore常用方法有哪些? 如何实现线程同步和互斥的?
单独使用Semaphore是不会使用到AQS的条件队列?
不同于CyclicBarrier和ReentrantLock,单独使用Semaphore是不会使用到AQS的条件队列的,其实,只有进行await操作才会进入条件队列,其他的都是在同步队列中,只是当前线程会被park。
Semaphore初始化有10个令牌,11个线程同时各调用1次acquire方法,会发生什么?
拿不到令牌的线程阻塞,不会继续往下运行。
Semaphore初始化有10个令牌,一个线程重复调用11次acquire方法,会发生什么?
线程阻塞,不会继续往下运行。可能你会考虑类似于锁的重入的问题,很好,但是,令牌没有重入的概念。你只要调用一次acquire方法,就需要有一个令牌才能继续运行。
Semaphore初始化有1个令牌,1个线程调用一次acquire方法,然后调用两次release方法,之后另外一个线程调用acquire(2)方法,此线程能够获取到足够的令牌并继续运行吗?
能,原因是release方法会添加令牌,并不会以初始化的大小为准。
Semaphore初始化有2个令牌,一个线程调用1次release方法,然后一次性获取3个令牌,会获取到吗?
能,原因是release会添加令牌,并不会以初始化的大小为准。Semaphore中release方法的调用并没有限制要在acquire后调用。
具体示例如下,如果不相信的话,可以运行一下下面的demo,在做实验之前,笔者也认为应该是不允许的。。
public class TestSemaphore2 {
public static void main(String[] args) {
int permitsNum = 2;
final Semaphore semaphore = new Semaphore(permitsNum);
try {
System.out.println("availablePermits:"+semaphore.availablePermits()+",semaphore.tryAcquire(3,1, TimeUnit.SECONDS):"+semaphore.tryAcquire(3,1, TimeUnit.SECONDS));
semaphore.release();
System.out.println("availablePermits:"+semaphore.availablePermits()+",semaphore.tryAcquire(3,1, TimeUnit.SECONDS):"+semaphore.tryAcquire(3,1, TimeUnit.SECONDS));
}catch (Exception e) {
}
}
}
Phaser主要用来解决什么问题?
Phaser是JDK 7新增的一个同步辅助类,它可以实现CyclicBarrier和CountDownLatch类似的功能,而且它支持对任务的动态调整,并支持分层结构来达到更高的吞吐量。
Phaser与CyclicBarrier和CountDownLatch的区别是什么?
Phaser 和 CountDownLatch、CyclicBarrier 都有很相似的地方。
Phaser 顾名思义,就是可以分阶段的进行线程同步。
- CountDownLatch 只能在创建实例时,通过构造方法指定同步数量;
- Phaser 支持线程动态地向它注册。
利用这个动态注册的特性,可以达到分阶段同步控制的目的:
注册一批操作,等待它们执行结束;再注册一批操作,等它们结束...
Phaser运行机制是什么样的?

- Registration(注册)
跟其他barrier不同,在phaser上注册的parties会随着时间的变化而变化。任务可以随时注册(使用方法register,bulkRegister注册,或者由构造器确定初始parties),并且在任何抵达点可以随意地撤销注册(方法arriveAndDeregister)。就像大多数基本的同步结构一样,注册和撤销只影响内部count;不会创建更深的内部记录,所以任务不能查询他们是否已经注册。(不过,可以通过继承来实现类似的记录)
- Synchronization(同步机制)
和CyclicBarrier一样,Phaser也可以重复await。方法arriveAndAwaitAdvance的效果类似CyclicBarrier.await。phaser的每一代都有一个相关的phase number,初始值为0,当所有注册的任务都到达phaser时phase+1,到达最大值(Integer.MAX_VALUE)之后清零。使用phase number可以独立控制 到达phaser 和 等待其他线程 的动作,通过下面两种类型的方法:
- Arrival(到达机制) arrive和arriveAndDeregister方法记录到达状态。这些方法不会阻塞,但是会返回一个相关的arrival phase number;也就是说,phase number用来确定到达状态。当所有任务都到达给定phase时,可以执行一个可选的函数,这个函数通过重写onAdvance方法实现,通常可以用来控制终止状态。重写此方法类似于为CyclicBarrier提供一个barrierAction,但比它更灵活。
- Waiting(等待机制) awaitAdvance方法需要一个表示arrival phase number的参数,并且在phaser前进到与给定phase不同的phase时返回。和CyclicBarrier不同,即使等待线程已经被中断,awaitAdvance方法也会一直等待。中断状态和超时时间同样可用,但是当任务等待中断或超时后未改变phaser的状态时会遭遇异常。如果有必要,在方法forceTermination之后可以执行这些异常的相关的handler进行恢复操作,Phaser也可能被ForkJoinPool中的任务使用,这样在其他任务阻塞等待一个phase时可以保证足够的并行度来执行任务。
- Termination(终止机制) :
可以用isTerminated方法检查phaser的终止状态。在终止时,所有同步方法立刻返回一个负值。在终止时尝试注册也没有效果。当调用onAdvance返回true时Termination被触发。当deregistration操作使已注册的parties变为0时,onAdvance的默认实现就会返回true。也可以重写onAdvance方法来定义终止动作。forceTermination方法也可以释放等待线程并且允许它们终止。
- Tiering(分层结构) :
Phaser支持分层结构(树状构造)来减少竞争。注册了大量parties的Phaser可能会因为同步竞争消耗很高的成本, 因此可以设置一些子Phaser来共享一个通用的parent。这样的话即使每个操作消耗了更多的开销,但是会提高整体吞吐量。 在一个分层结构的phaser里,子节点phaser的注册和取消注册都通过父节点管理。子节点phaser通过构造或方法register、bulkRegister进行首次注册时,在其父节点上注册。子节点phaser通过调用arriveAndDeregister进行最后一次取消注册时,也在其父节点上取消注册。
- Monitoring(状态监控) :
由于同步方法可能只被已注册的parties调用,所以phaser的当前状态也可能被任何调用者监控。在任何时候,可以通过getRegisteredParties获取parties数,其中getArrivedParties方法返回已经到达当前phase的parties数。当剩余的parties(通过方法getUnarrivedParties获取)到达时,phase进入下一代。这些方法返回的值可能只表示短暂的状态,所以一般来说在同步结构里并没有啥卵用。
给一个Phaser使用的示例?
模拟了100米赛跑,10名选手,只等裁判一声令下。当所有人都到达终点时,比赛结束。
public class Match {
// 模拟了100米赛跑,10名选手,只等裁判一声令下。当所有人都到达终点时,比赛结束。
public static void main(String[] args) throws InterruptedException {
final Phaser phaser=new Phaser(1) ;
// 十名选手
for (int index = 0; index < 10; index++) {
phaser.register();
new Thread(new player(phaser),"player"+index).start();
}
System.out.println("Game Start");
//注销当前线程,比赛开始
phaser.arriveAndDeregister();
//是否非终止态一直等待
while(!phaser.isTerminated()){
}
System.out.println("Game Over");
}
}
class player implements Runnable{
private final Phaser phaser ;
player(Phaser phaser){
this.phaser=phaser;
}
@Override
public void run() {
try {
// 第一阶段——等待创建好所有线程再开始
phaser.arriveAndAwaitAdvance();
// 第二阶段——等待所有选手准备好再开始
Thread.sleep((long) (Math.random() * 10000));
System.out.println(Thread.currentThread().getName() + " ready");
phaser.arriveAndAwaitAdvance();
// 第三阶段——等待所有选手准备好到达,到达后,该线程从phaser中注销,不在进行下面的阶段。
Thread.sleep((long) (Math.random() * 10000));
System.out.println(Thread.currentThread().getName() + " arrived");
phaser.arriveAndDeregister();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Exchanger主要解决什么问题?
Exchanger用于进行两个线程之间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange()方法交换数据,当一个线程先执行exchange()方法后,它会一直等待第二个线程也执行exchange()方法,当这两个线程到达同步点时,这两个线程就可以交换数据了。
对比SynchronousQueue,为什么说Exchanger可被视为 SynchronousQueue 的双向形式?
Exchanger是一种线程间安全交换数据的机制。可以和之前分析过的SynchronousQueue对比一下:线程A通过SynchronousQueue将数据a交给线程B;线程A通过Exchanger和线程B交换数据,线程A把数据a交给线程B,同时线程B把数据b交给线程A。可见,SynchronousQueue是交给一个数据,Exchanger是交换两个数据。
Exchanger在不同的JDK版本中实现有什么差别?
- 在JDK5中Exchanger被设计成一个容量为1的容器,存放一个等待线程,直到有另外线程到来就会发生数据交换,然后清空容器,等到下一个到来的线程。
- 从JDK6开始,Exchanger用了类似ConcurrentMap的分段思想,提供了多个slot,增加了并发执行时的吞吐量。
Exchanger实现举例
来一个非常经典的并发问题:你有相同的数据buffer,一个或多个数据生产者,和一个或多个数据消费者。只是Exchange类只能同步2个线程,所以你只能在你的生产者和消费者问题中只有一个生产者和一个消费者时使用这个类。
public class Test {
static class Producer extends Thread {
private Exchanger<Integer> exchanger;
private static int data = 0;
Producer(String name, Exchanger<Integer> exchanger) {
super("Producer-" + name);
this.exchanger = exchanger;
}
@Override
public void run() {
for (int i=1; i<5; i++) {
try {
TimeUnit.SECONDS.sleep(1);
data = i;
System.out.println(getName()+" 交换前:" + data);
data = exchanger.exchange(data);
System.out.println(getName()+" 交换后:" + data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
static class Consumer extends Thread {
private Exchanger<Integer> exchanger;
private static int data = 0;
Consumer(String name, Exchanger<Integer> exchanger) {
super("Consumer-" + name);
this.exchanger = exchanger;
}
@Override
public void run() {
while (true) {
data = 0;
System.out.println(getName()+" 交换前:" + data);
try {
TimeUnit.SECONDS.sleep(1);
data = exchanger.exchange(data);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName()+" 交换后:" + data);
}
}
}
public static void main(String[] args) throws InterruptedException {
Exchanger<Integer> exchanger = new Exchanger<Integer>();
new Producer("", exchanger).start();
new Consumer("", exchanger).start();
TimeUnit.SECONDS.sleep(7);
System.exit(-1);
}
}
可以看到,其结果可能如下:
Consumer- 交换前:0
Producer- 交换前:1
Consumer- 交换后:1
Consumer- 交换前:0
Producer- 交换后:0
Producer- 交换前:2
Producer- 交换后:0
Consumer- 交换后:2
Consumer- 交换前:0
Producer- 交换前:3
Producer- 交换后:0
Consumer- 交换后:3
Consumer- 交换前:0
Producer- 交换前:4
Producer- 交换后:0
Consumer- 交换后:4
Consumer- 交换前:0
什么是ThreadLocal? 用来解决什么问题的?
我们在Java 并发 - 理论基础总结过线程安全(是指广义上的共享资源访问安全性,因为线程隔离是通过副本保证本线程访问资源安全性,它不保证线程之间还存在共享关系的狭义上的安全性)的解决思路:
- 互斥同步: synchronized 和 ReentrantLock
- 非阻塞同步: CAS, AtomicXXXX
- 无同步方案: 栈封闭,本地存储(Thread Local),可重入代码
ThreadLocal是通过线程隔离的方式防止任务在共享资源上产生冲突, 线程本地存储是一种自动化机制,可以为使用相同变量的每个不同线程都创建不同的存储。
ThreadLocal是一个将在多线程中为每一个线程创建单独的变量副本的类; 当使用ThreadLocal来维护变量时, ThreadLocal会为每个线程创建单独的变量副本, 避免因多线程操作共享变量而导致的数据不一致的情况。
说说你对ThreadLocal的理解
提到ThreadLocal被提到应用最多的是session管理和数据库链接管理,这里以数据访问为例帮助你理解ThreadLocal:
- 如下数据库管理类在单线程使用是没有任何问题的
class ConnectionManager {
private static Connection connect = null;
public static Connection openConnection() {
if (connect == null) {
connect = DriverManager.getConnection();
}
return connect;
}
public static void closeConnection() {
if (connect != null)
connect.close();
}
}
很显然,在多线程中使用会存在线程安全问题:第一,这里面的2个方法都没有进行同步,很可能在openConnection方法中会多次创建connect;第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭链接。
- 为了解决上述线程安全的问题,第一考虑:互斥同步
你可能会说,将这段代码的两个方法进行同步处理,并且在调用connect的地方需要进行同步处理,比如用Synchronized或者ReentrantLock互斥锁。
- 这里再抛出一个问题:这地方到底需不需要将connect变量进行共享?
事实上,是不需要的。假如每个线程中都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改的。即改后的代码可以这样:
class ConnectionManager {
private Connection connect = null;
public Connection openConnection() {
if (connect == null) {
connect = DriverManager.getConnection();
}
return connect;
}
public void closeConnection() {
if (connect != null)
connect.close();
}
}
class Dao {
public void insert() {
ConnectionManager connectionManager = new ConnectionManager();
Connection connection = connectionManager.openConnection();
// 使用connection进行操作
connectionManager.closeConnection();
}
}
这样处理确实也没有任何问题,由于每次都是在方法内部创建的连接,那么线程之间自然不存在线程安全问题。但是这样会有一个致命的影响:导致服务器压力非常大,并且严重影响程序执行性能。由于在方法中需要频繁地开启和关闭数据库连接,这样不仅严重影响程序执行效率,还可能导致服务器压力巨大。
- 这时候ThreadLocal登场了
那么这种情况下使用ThreadLocal是再适合不过的了,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。下面就是网上出现最多的例子:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class ConnectionManager {
private static final ThreadLocal<Connection> dbConnectionLocal = new ThreadLocal<Connection>() {
@Override
protected Connection initialValue() {
try {
return DriverManager.getConnection("", "", "");
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
};
public Connection getConnection() {
return dbConnectionLocal.get();
}
}
ThreadLocal是如何实现线程隔离的?
ThreadLocalMap
为什么ThreadLocal会造成内存泄露? 如何解决
网上有这样一个例子:
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadLocalDemo {
static class LocalVariable {
private Long[] a = new Long[1024 * 1024];
}
// (1)
final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
new LinkedBlockingQueue<>());
// (2)
final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();
public static void main(String[] args) throws InterruptedException {
// (3)
Thread.sleep(5000 * 4);
for (int i = 0; i < 50; ++i) {
poolExecutor.execute(new Runnable() {
public void run() {
// (4)
localVariable.set(new LocalVariable());
// (5)
System.out.println("use local varaible" + localVariable.get());
localVariable.remove();
}
});
}
// (6)
System.out.println("pool execute over");
}
}
如果用线程池来操作ThreadLocal 对象确实会造成内存泄露, 因为对于线程池里面不会销毁的线程, 里面总会存在着<ThreadLocal, LocalVariable>的强引用, 因为final static 修饰的 ThreadLocal 并不会释放, 而ThreadLocalMap 对于 Key 虽然是弱引用, 但是强引用不会释放, 弱引用当然也会一直有值, 同时创建的LocalVariable对象也不会释放, 就造成了内存泄露; 如果LocalVariable对象不是一个大对象的话, 其实泄露的并不严重, 泄露的内存 = 核心线程数 * LocalVariable对象的大小;
所以, 为了避免出现内存泄露的情况, ThreadLocal提供了一个清除线程中对象的方法, 即 remove, 其实内部实现就是调用 ThreadLocalMap 的remove方法:
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
找到Key对应的Entry, 并且清除Entry的Key(ThreadLocal)置空, 随后清除过期的Entry即可避免内存泄露。
还有哪些使用ThreadLocal的应用场景?
- 每个线程维护了一个“序列号”
public class SerialNum {
// The next serial number to be assigned
private static int nextSerialNum = 0;
private static ThreadLocal serialNum = new ThreadLocal() {
protected synchronized Object initialValue() {
return new Integer(nextSerialNum++);
}
};
public static int get() {
return ((Integer) (serialNum.get())).intValue();
}
}
+ 经典的另外一个例子:
```Java
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
- 看看阿里巴巴 java 开发手册中推荐的 ThreadLocal 的用法:
import java.text.DateFormat;
import java.text.SimpleDateFormat;
public class DateUtils {
public static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
}
然后我们再要用到 DateFormat 对象的地方,这样调用:
DateUtils.df.get().format(new Date());
5 JVM和调优
JVM虚拟机和调优相关。
5.1 类加载机制
类加载的生命周期?
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)*。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

- 类的加载: 查找并加载类的二进制数据
- 连接
- 验证: 确保被加载的类的正确性
- 准备: 为类的静态变量分配内存,并将其初始化为默认值
- 解析: 把类中的符号引用转换为直接引用
- 初始化:为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。
- 使用: 类访问方法区内的数据结构的接口, 对象是Heap区的数据
- 卸载: 结束生命周期
类加载器的层次?

- 启动类加载器: Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
- 扩展类加载器: Extension ClassLoader,该加载器由
sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。 - 应用程序类加载器: Application ClassLoader,该类加载器由
sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。 - 自定义类加载器: 因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
- 在执行非置信代码之前,自动验证数字签名。
- 动态地创建符合用户特定需要的定制化构建类。
- 从特定的场所取得java class,例如数据库中和网络中。
Class.forName()和ClassLoader.loadClass()区别?
Class.forName(): 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;ClassLoader.loadClass(): 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。
JVM有哪些类加载机制?
- JVM类加载机制有哪些?
- 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
- 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
- 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
- 双亲委派机制, 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
- 双亲委派机制过程?
- 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
- 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
JVM预定义的三种类型类加载器:
- 启动(Bootstrap)类加载器:是用本地代码实现的类装入器,它负责将
<Java_Runtime_Home>/lib下面的类库加载到内存中(比如rt.jar)。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。 - 标准扩展(Extension)类加载器:是由 Sun 的
ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量java.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。 - 系统(System)类加载器:是由 Sun 的
AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。
除了以上列举的三种类加载器,还有一种比较特殊的类型 — 线程上下文类加载器。
双亲委派机制描述
某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
委托机制几点思考
Java虚拟机的第一个类加载器是Bootstrap,这个加载器很特殊,它不是Java类,因此它不需要被别人加载,它嵌套在Java虚拟机内核里面,也就是JVM启动的时候Bootstrap就已经启动,它是用C++写的二进制代码(不是字节码),它可以去加载别的类。
这也是我们在测试时为什么发现
System.class.getClassLoader()结果为null的原因,这并不表示System这个类没有类加载器,而是它的加载器比较特殊,是BootstrapClassLoader,由于它不是Java类,因此获得它的引用肯定返回null。委托机制具体含义 当Java虚拟机要加载一个类时,到底派出哪个类加载器去加载呢?
- 首先当前线程的类加载器去加载线程中的第一个类(假设为类A)。 注:当前线程的类加载器可以通过Thread类的getContextClassLoader()获得,也可以通过setContextClassLoader()自己设置类加载器。
- 如果类A中引用了类B,Java虚拟机将使用加载类A的类加载器去加载类B。
- 还可以直接调用
ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。
委托机制的意义 — 防止内存中出现多份同样的字节码 比如两个类A和类B都要加载System类:
- 如果不用委托而是自己加载自己的,那么类A就会加载一份System字节码,然后类B又会加载一份System字节码,这样内存中就出现了两份System字节码。
- 如果使用委托机制,会递归的向父类查找,也就是首选用Bootstrap尝试加载,如果找不到再向下。这里的System就能在Bootstrap中找到然后加载,如果此时类B也要加载System,也从Bootstrap开始,此时Bootstrap发现已经加载过了System那么直接返回内存中的System即可而不需要重新加载,这样内存中就只有一份System的字节码了。
描述Java中的类加载机制
Java中的类加载机制是Java运行时系统的一部分,负责动态加载Java类文件到JVM中。类加载过程主要分为三个阶段:加载(Loading)、链接(Linking)和初始化(Initialization)。
- 加载:在这个阶段,类加载器(ClassLoader)读取二进制的.class文件,并为之创建一个java.lang.Class对象。这个过程包括检查文件是否为有效的类文件,以及为类变量分配内存并设置默认初始值。
- 链接:链接阶段进一步准备类的加载,分为验证、准备和解析三个步骤。
- 验证:确保加载的类符合JVM规范,没有安全问题。
- 准备:为类变量分配内存,并设置默认初始值。
- 解析:将类、接口、字段和方法的符号引用转换为直接引用。
- 初始化:在这个阶段,JVM负责执行类构造器
<clinit>()方法的过程。这个方法由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生。初始化阶段是执行静态初始化代码和为静态变量赋予正确初始值的过程。
类加载器本身也有一个层次关系,遵循双亲委派模型,即类加载器在尝试加载类时,会先委托给父加载器加载,如果父加载器无法完成加载任务,子加载器才会尝试自己加载。
解释 Java 中的类加载机制,包括双亲委派模型
Java的类加载机制负责将类从文件系统或其他来源加载到 JVM 中。类加载机制包括以下几个步骤:
- 加载:读取并构造类的二进制数据。
- 验证:确保类的二进制数据符合规范。
- 准备:为类的静态变量分配内存并设置默认初始值。
- 解析:将符号引用替换为直接引用。
- 初始化:执行类构造器
<clinit>方法。
双亲委派模型
- 定义:每个类加载器都有一个父类加载器,如果一个类加载器收到了类加载请求,首先不会自己去尝试加载这个类,而是把请求委托给父类加载器完成,只有当父类加载器无法完成加载时,才会尝试自己加载。
总结
步骤描述加载读取并构造类的二进制数据。验证确保类的二进制数据符合规范。准备为类的静态变量分配内存并设置默认初始值。解析将符号引用替换为直接引用。初始化执行类构造器 <clinit> 方法。双亲委派类加载器委托给父类加载器完成类加载,若父类无法完成再自行加载。
5.2 内存结构
解释 Java 中的对象内存布局,包括对象头、实例数据和对齐填充
Java对象在堆内存中的布局主要包括对象头、实例数据和对齐填充:
- 对象头:包含对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
- 实例数据:真正存储的有效数据,即我们在程序代码里面所定义的各种类型的字段内容。
- 对齐填充:JVM要求对象起始地址必须是8字节的整数倍,填充是为了让对象符合这个要求。
总结
内存布局描述对象头包含对象的运行时数据。实例数据存储对象的有效数据。对齐填充让对象的起始地址符合8字节对齐的规则。
描述一下 JVM 加载 class 文件的原理机制?
JVM 中类的装载是由类加载器( ClassLoader) 和它的子类来实现的, Java 中的类加载器是一个重要的 Java 运行时系统组件,它负责在运行时查找和装入类文件中的类。 由于 Java 的跨平台性, 经过编译的 Java 源程序并不是一个可执行程序, 而是一个或多个类文件。当 Java 程序需要使用某个类时,JVM 会确保这个类已经被加载、连接( 验证、准备和解析)和初始化。类的加载是指把类的.class 文件中的数据读入到内存中,通常是创建一个字节数组读入.class 文件,然后产生与所加载类对应的 Class 对象。加载完成后, Class 对象还不完整, 所以此时的类还不可用。当类被加载后就进入连接阶段, 这一阶段包括验证、准备( 为静态变量分配内存并设置默认的初始值) 和解析( 将符号引用替换为直接引用) 三个步骤。最后 JVM 对类进行初始化,包括:1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类; 2)如果类中存在初始化语句, 就依次执行这些初始化语句。 类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器( Extension)、系统加载器( System) 和用户自定义类加载器 ( java.lang.ClassLoader 的子类) 。从 Java 2( JDK 1.2) 开始, 类加载过程采取了父亲委托机制( PDM )。 PDM 更好的保证了 Java 平台的安全性, 在该机制中, JVM 自带的 Bootstrap 是根加载器, 其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载, 父类加载器无能为力时才由其子类加载 器自行加载。JVM 不会向 Java 程序提供对 Bootstrap 的引用。下面是关于几个类加载器的说明:
Bootstrap:一般用本地代码实现,负责加载 JVM 基础核心类库(rt.jar); Extension:从 java.ext.dirs 系统属性所指定的目录中加载类库,它的父 加载器是 Bootstrap; System:又叫应用类加载器,其父类是 Extension。它是应用最广泛的类加载器。它从环境变量 classpath 或者系统属性 java.class.path 所指定的目录中记载类,是用户自定义加载器的默认父加载器。
解释 Java 内存模型(JMM)中的可见性、原子性和有序性
Java内存模型(JMM)定义了线程和主内存之间交互的规则,主要包括可见性、原子性和有序性:
- 可见性:指当一个线程修改共享变量的值时,其他线程能够立即看到这个修改。
- 原子性:指一个操作不能被中断,要么全部完成,要么全部不完成。
- 有序性:指程序执行的顺序按照代码的先后顺序执行,不允许指令重排序。
总结
特性描述可见性确保一个线程对共享变量的修改能够被其他线程看到。原子性保证一个操作不会被中断,确保操作的完整性。有序性保证程序执行的顺序符合代码的顺序,防止指令重排序导致的问题。
描述Java内存模型和垃圾回收机制
Java内存模型(JMM)是Java虚拟机(JVM)的一个核心组成部分,它定义了线程如何访问共享数据,以及在不同线程之间传递数据的规则。JMM的目的是保证在多线程环境下,对共享变量的读写操作能够正确地同步,从而避免内存模型不一致的问题。
JMM定义了几种不同的内存区域:
- 堆(Heap):存储对象实例和数组,是所有线程共享的内存区域。
- 方法区(Method Area):存储类信息、常量、静态变量等。
- 栈(Stack):每个线程都有自己的栈,用于存储局部变量和方法调用的信息。
- 程序计数器(Program Counter Register):记录当前线程执行的字节码指令位置。
- 本地方法栈(Native Method Stacks)**:支持本地方法执行(如C/C++编写的方法)。
- 垃圾回收(Garbage Collection,GC)是JVM自动内存管理的一部分,负责回收不再使用的对象,释放内存。垃圾回收器会跟踪对象的引用情况,当一个对象没有任何引用指向它时,该对象就成为了垃圾,可以被回收。 垃圾回收器有多种算法,如标记-清除(Mark-Sweep)、标记-整理(Mark-Compact)、复制(Copying)等。现代JVM通常使用分代收集策略,将对象分为新生代和老年代,根据不同年代的特点采用不同的回收策略。
垃圾回收机制提高了Java程序的性能和可靠性,但也意味着程序员需要了解其工作原理,以避免内存泄漏和性能瓶颈。
解释内存中的栈(stack)、堆(heap)和方法区(method area) 的用法。
通常我们定义一个基本数据类型的变量, 一个对象的引用, 还有就是函数调用的现场保存都使用 JVM 中的栈空间;而通过 new 关键字和构造器创建的对象则放在堆空间, 堆是垃圾收集器管理的主要区域, 由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代, 再具体一点可以分为 Eden、Survivor ( 又可分为 From Survivor 和 To Survivor)、 Tenured; 方法区和堆都是各个线程共享的内存区域, 用于存储已经被 JVM 加载的类信息、常量、静态变量、JIT 编译器编译后的代码等数据;程序中的字面量( literal)如直接书写的 100 、”hello” 和常量都是放在常量池中, 常量池是方法区的一部分,。栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过 JVM 的启动参数来进行调整, 栈空间用光了会引发 StackOverflowError, 而堆和常量池空间不足则会引发OutOfMemoryError。
String str = new String("hello"); 上面的语句中变量 str 放在栈上, 用 new 创建出来的字符串对象放在堆上, 而” hello” 这个字面量是放在方法区的。 补充 1: 较新版本的 Java( 从 Java 6 的某个更新开始) 中, 由于 JIT 编译器的发展和” 逃逸分析” 技术的逐渐成熟, 栈上分配、标量替换等优化技术使得对象一定分配在堆上这件事情已经变得不那么绝对了。 补充 2: 运行时常量池相当于 Class 文件常量池具有动态性, Java 语言并不要求常量一定只有编译期间才能产生, 运行期间也可以将新的常量放入池中, String 类的 intern() 方法就是这样的。
看看下面代码的执行结果是什么并且比较一下 Java 7 以前和以后的运行结果是否一致。 String s1 = new StringBuilder("go") .append("od").toString(); System.out.println(s1.intern() == s1); String s2 = new StringBuilder("ja") .append("va").toString(); System.out.println(s2.intern() == s2);
说说JVM内存整体的结构?线程私有还是共享的?
JVM 整体架构,中间部分就是 Java 虚拟机定义的各种运行时数据区域。

Java 虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。另外一些则是与线程一一对应的,这些与线程一一对应的数据区域会随着线程开始和结束而创建和销毁。
- 线程私有:程序计数器、虚拟机栈、本地方法区
- 线程共享:堆、方法区, 堆外内存(Java7的永久代或JDK8的元空间、代码缓存)
什么是程序计数器(线程私有)?
PC 寄存器用来存储指向下一条指令的地址,即将要执行的指令代码。由执行引擎读取下一条指令。
- PC寄存器为什么会被设定为线程私有的?
多线程在一个特定的时间段内只会执行其中某一个线程方法,CPU会不停的做任务切换,这样必然会导致经常中断或恢复。为了能够准确的记录各个线程正在执行的当前字节码指令地址,所以为每个线程都分配了一个PC寄存器,每个线程都独立计算,不会互相影响。
什么是虚拟机栈(线程私有)?
主管 Java 程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次 Java 方法调用,是线程私有的,生命周期和线程一致。
- 特点?
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
- JVM 直接对虚拟机栈的操作只有两个:每个方法执行,伴随着入栈(进栈/压栈),方法执行结束出栈
- 栈不存在垃圾回收问题
- 可以通过参数
-Xss来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
- 该区域有哪些异常?
- 如果采用固定大小的 Java 虚拟机栈,那每个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个 StackOverflowError 异常
- 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个OutOfMemoryError异常
- 栈帧的内部结构?
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)(或称为表达式栈)
- 动态链接(Dynamic Linking):指向运行时常量池的方法引用
- 方法返回地址(Return Address):方法正常退出或异常退出的地址
- 一些附加信息

Java虚拟机栈如何进行方法计算的?
以如下代码为例:
private static int add(int a, int b) {
int c = 0;
c = a + b;
return c;
}
可以通过jsclass 等工具查看bytecode

压栈的步骤如下:
0: iconst_0 // 0压栈
1: istore_2 // 弹出int,存放于局部变量2
2: iload_0 // 把局部变量0压栈
3: iload_1 // 局部变量1压栈
4: iadd //弹出2个变量,求和,结果压栈
5: istore_2 //弹出结果,放于局部变量2
6: iload_2 //局部变量2压栈
7: ireturn //返回
如果计算100+98的值,那么操作数栈的变化如下图

什么是本地方法栈(线程私有)?
- 本地方法接口
一个 Native Method 就是一个 Java 调用非 Java 代码的接口。我们知道的 Unsafe 类就有很多本地方法。
- 本地方法栈(Native Method Stack)
Java 虚拟机栈用于管理 Java 方法的调用,而本地方法栈用于管理本地方法的调用
什么是方法区(线程共享)?
方法区(method area)只是 JVM 规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,并没有规定如何去实现它,不同的厂商有不同的实现。而永久代(PermGen)**是 **Hotspot** 虚拟机特有的概念, Java8 的时候又被**元空间取代了,永久代和元空间都可以理解为方法区的落地实现。
JDK1.8之前调节方法区大小:
-XX:PermSize=N //方法区(永久代)初始大小
-XX:MaxPermSize=N //方法区(永久代)最大大小,超出这个值将会抛出OutOfMemoryError
JDK1.8开始方法区(HotSpot的永久代)被彻底删除了,取而代之的是元空间,元空间直接使用的是本机内存。参数设置:
-XX:MetaspaceSize=N //设置Metaspace的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置Metaspace的最大大小
栈、堆、方法区的交互关系

永久代和元空间内存使用上的差异?
Java虚拟机规范中只定义了方法区用于存储已被虚拟机加载的类信息、常量、静态变量和即时编译后的代码等数据
- jdk1.7开始符号引用存储在native heap中,字符串常量和静态类型变量存储在普通的堆区中,但分离的并不彻底,此时永久代中还保存另一些与类的元数据无关的杂项
- jdk8后HotSpot 原永久代中存储的类的元数据将存储在metaspace中,而类的静态变量和字符串常量将放在Java堆中,metaspace是方法区的一种实现,只不过它使用的不是虚拟机内的内存,而是本地内存。在元空间中保存的数据比永久代中纯粹很多,就只是类的元数据,这些信息只对编译期或JVM的运行时有用。
- 永久代有一个JVM本身设置固定大小上线,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到java.lang.OutOfMemoryError。
- 符号引用没有存在元空间中,而是存在native heap中,这是两个方式和位置,不过都可以算作是本地内存,在虚拟机之外进行划分,没有设置限制参数时只受物理内存大小限制,即只有占满了操作系统可用内存后才OOM。
堆区内存是怎么细分的?
对于大多数应用,Java 堆是 Java 虚拟机管理的内存中最大的一块,被所有线程共享。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数据都在这里分配内存。
为了进行高效的垃圾回收,虚拟机把堆内存逻辑上划分成三块区域(分代的唯一理由就是优化 GC 性能):
- 新生带(年轻代):新对象和没达到一定年龄的对象都在新生代
- 老年代(养老区):被长时间使用的对象,老年代的内存空间应该要比年轻代更大

Java 虚拟机规范规定,Java 堆可以是处于物理上不连续的内存空间中,只要逻辑上是连续的即可,像磁盘空间一样。实现时,既可以是固定大小,也可以是可扩展的,主流虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制),如果堆中没有完成实例分配,并且堆无法再扩展时,就会抛出 OutOfMemoryError 异常。
- 年轻代 (Young Generation)
年轻代是所有新对象创建的地方。当填充年轻代时,执行垃圾收集。这种垃圾收集称为 Minor GC。年轻一代被分为三个部分——伊甸园(Eden Memory)和两个幸存区(Survivor Memory,被称为from/to或s0/s1),默认比例是8:1:1
- 大多数新创建的对象都位于 Eden 内存空间中
- 当 Eden 空间被对象填充时,执行Minor GC,并将所有幸存者对象移动到一个幸存者空间中
- Minor GC 检查幸存者对象,并将它们移动到另一个幸存者空间。所以每次,一个幸存者空间总是空的
- 经过多次 GC 循环后存活下来的对象被移动到老年代。通常,这是通过设置年轻一代对象的年龄阈值来实现的,然后他们才有资格提升到老一代
- 老年代(Old Generation)
旧的一代内存包含那些经过许多轮小型 GC 后仍然存活的对象。通常,垃圾收集是在老年代内存满时执行的。老年代垃圾收集称为 主GC(Major GC),通常需要更长的时间。
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在 Eden 区和两个Survivor 区之间发生大量的内存拷贝

JVM中对象在堆中的生命周期?
- 在 JVM 内存模型的堆中,堆被划分为新生代和老年代
- 新生代又被进一步划分为 Eden区 和 Survivor区,Survivor 区由 From Survivor 和 To Survivor 组成
- 当创建一个对象时,对象会被优先分配到新生代的 Eden 区
- 此时 JVM 会给对象定义一个对象年轻计数器(
-XX:MaxTenuringThreshold)
- 此时 JVM 会给对象定义一个对象年轻计数器(
- 当 Eden 空间不足时,JVM 将执行新生代的垃圾回收(Minor GC)
- JVM 会把存活的对象转移到 Survivor 中,并且对象年龄 +1
- 对象在 Survivor 中同样也会经历 Minor GC,每经历一次 Minor GC,对象年龄都会+1
- 如果分配的对象超过了
-XX:PetenureSizeThreshold,对象会直接被分配到老年代
JVM中对象的分配过程?
为对象分配内存是一件非常严谨和复杂的任务,JVM 的设计者们不仅需要考虑内存如何分配、在哪里分配等问题,并且由于内存分配算法和内存回收算法密切相关,所以还需要考虑 GC 执行完内存回收后是否会在内存空间中产生内存碎片。
- new 的对象先放在伊甸园区,此区有大小限制
- 当伊甸园的空间填满时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。再加载新的对象放到伊甸园区
- 然后将伊甸园中的剩余对象移动到幸存者 0 区
- 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者 0 区,如果没有回收,就会放到幸存者 1 区
- 如果再次经历垃圾回收,此时会重新放回幸存者 0 区,接着再去幸存者 1 区
- 什么时候才会去养老区呢? 默认是 15 次回收标记
- 在养老区,相对悠闲。当养老区内存不足时,再次触发 Major GC,进行养老区的内存清理
- 若养老区执行了 Major GC 之后发现依然无法进行对象的保存,就会产生 OOM 异常
什么是 TLAB (Thread Local Allocation Buffer)?
- 从内存模型而不是垃圾回收的角度,对 Eden 区域继续进行划分,JVM 为每个线程分配了一个私有缓存区域,它包含在 Eden 空间内
- 多线程同时分配内存时,使用 TLAB 可以避免一系列的非线程安全问题,同时还能提升内存分配的吞吐量,因此我们可以将这种内存分配方式称为快速分配策略
- OpenJDK 衍生出来的 JVM 大都提供了 TLAB 设计
为什么要有 TLAB ?
- 堆区是线程共享的,任何线程都可以访问到堆区中的共享数据
- 由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
- 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
尽管不是所有的对象实例都能够在 TLAB 中成功分配内存,但 JVM 确实是将 TLAB 作为内存分配的首选。
在程序中,可以通过 -XX:UseTLAB 设置是否开启 TLAB 空间。
默认情况下,TLAB 空间的内存非常小,仅占有整个 Eden 空间的 1%,我们可以通过 -XX:TLABWasteTargetPercent 设置 TLAB 空间所占用 Eden 空间的百分比大小。
一旦对象在 TLAB 空间分配内存失败时,JVM 就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在 Eden 空间中分配内存。
谈谈 Java 中的方法区(Method Area)的演变和优化
演变
- JDK 1.6:
- 方法区位于永久代中。
- 永久代的大小固定,容易造成内存溢出。
- JDK 1.7:
- 与 JDK 1.6 相似,但引入了
-XX:+UseCompressedClassPointers和-XX:+UseCompressedOops选项来减少方法区的内存消耗。
- 与 JDK 1.6 相似,但引入了
- JDK 1.8:
- 方法区被 Metaspace 替代。
- Metaspace 不位于 JVM 堆内,而是使用本地内存。
优化
- Metaspace:可以动态扩展,解决了永久代的固定大小限制。
- 压缩指针:通过
-XX:+UseCompressedClassPointers和-XX:+UseCompressedOops选项减少内存占用。 - 去重和压缩:字符串常量池和类信息的去重和压缩。
总结
特性JDK 1.6JDK 1.7JDK 1.8位置永久代永久代Metaspace大小限制固定固定动态扩展内存溢出有有无
解释 Java 中的 HotSpot 虚拟机的优化技术,如即时编译(JIT)
即时编译(JIT)
- 概念:将字节码编译成本地机器码,以提高程序执行速度。
- 优化:通过分析热点代码,将其编译为本地代码,减少解释执行的开销。
其他优化技术
- 内联缓存:避免方法调用时的虚方法表查询。
- 分支预测:预测分支执行路径,提前执行以减少延迟。
- 逃逸分析:分析对象是否逃逸到其他线程,优化锁的使用。
总结
技术描述JIT将热点代码编译为本地代码,提高执行效率。内联缓存避免虚方法表查询,加速方法调用。分支预测预测分支执行路径,减少延迟。逃逸分析分析对象是否逃逸,优化锁使用。
深入探讨 Java 中的逃逸分析(Escape Analysis)及其优化效果
逃逸分析
- 定义:分析对象是否逃逸到其他线程或栈帧之外。
- 目的:优化锁的使用,减少同步开销。
- 效果:如果对象没有逃逸,可以使用栈分配或标量替换。
优化效果
- 栈分配:将对象分配在栈上,减少垃圾回收的压力。
- 标量替换:用原始类型替代对象,减少内存消耗。
- 锁优化:减少不必要的锁操作。
总结
表格 还在加载中,请等待加载完成后再尝试复制
谈谈 Java 中的堆外内存(Off-Heap Memory)的使用和管理
堆外内存
- 定义:位于 JVM 堆之外的内存,主要用于存储非 Java 对象的数据。
- 用途:提高应用程序性能,减少垃圾回收的影响。
使用场景
- NIO 缓冲区:通过
DirectByteBuffer管理堆外内存。 - 高性能应用:如高速缓存、数据库连接池等。
管理
- 分配:通过
Unsafe类或DirectByteBuffer进行分配。 - 释放:手动释放或通过内存池管理。
总结
表格 还在加载中,请等待加载完成后再尝试复制
分析 Java 中的 Monitor(监视器)的实现原理
实现原理
- 对象监视器:每个 Java 对象都有一个内置的监视器锁。
- 锁状态:记录锁的持有者、等待队列和条件队列。
- 互斥性:同一时间只能有一个线程拥有锁。
作用
- 同步:确保线程互斥地访问共享资源。
- 等待通知:线程可以通过
wait和notify方法实现等待和唤醒机制。
总结
表格 还在加载中,请等待加载完成后再尝试复制
解释 Java 中的偏向锁、轻量级锁和重量级锁的升级过程
锁升级
- 偏向锁:无竞争情况下,偏向于第一个获得锁的线程。
- 轻量级锁:使用 CAS 操作尝试获取锁。
- 重量级锁:使用操作系统级别的互斥锁。
升级过程
- 偏向锁:线程第一次访问同步块时,如果同步块未被锁定,线程会尝试获取偏向锁。
- 轻量级锁:如果同步块已经被锁定,线程会尝试使用 CAS 获取轻量级锁。
- 重量级锁:如果 CAS 失败,或者有多个线程竞争锁,会升级为重量级锁。
总结
表格 还在加载中,请等待加载完成后再尝试复制
深入探讨 Java 中的 Metaspace(元空间)的特点和优势
特点
- 动态扩展:Metaspace 可以根据需要动态扩展。
- 本地内存:Metaspace 使用本地内存而非堆内存。
- 垃圾回收:不会对 Metaspace 进行垃圾回收。
优势
- 解决永久代溢出:Metaspace 没有固定大小限制,解决了永久代溢出的问题。
- 动态调整:可以根据实际需求调整大小。
- 提高性能:减少了永久代的管理开销。
总结
表格 还在加载中,请等待加载完成后再尝试复制
谈谈 Java 中的安全点(Safepoint)和安全区域(Safe Region)的概念
安全点
- 定义:JVM 用来暂停所有线程的特定代码执行点。
- 用途:执行全局性的操作,如垃圾回收。
安全区域
- 定义:代码块内,线程可以安全地执行,不受外部影响。
- 用途:在安全区域内执行长时间的操作,无需担心被中断。
总结
表格 还在加载中,请等待加载完成后再尝试复制
分析 Java 中的对象分配和回收的过程
分配过程
- 新对象创建:在新生代的 Eden 区分配空间。
- 对象增长:如果 Eden 区空间不足,触发 Minor GC。
- 对象晋升:Eden 区存活的对象晋升到 Survivor 区或直接晋升到老年代。
回收过程
- Minor GC:清理新生代。
- Major GC:清理整个堆,包括老年代。
- Full GC:清理整个堆和方法区。
总结
表格 还在加载中,请等待加载完成后再尝试复制
5.3 GC垃圾回收
GC 是什么?为什么要有GC?
GC 是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃, Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的, Java 语言没有提供释放已分配内存的显示操作方法。Java 程序员不用担心内存管理, 因为垃圾收集器会自动进行管理。要请求垃圾收集, 可以调用下面的方法之一: System.gc() 或Runtime.getRuntime().gc() , 但 JVM 可以屏蔽掉显示的垃圾回收调用。 垃圾回收可以有效的防止内存泄露, 有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优先级的线程运行, 不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收, 程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。 在 Java 诞生初期, 垃圾回收是 Java 最大的亮点之一, 因为服务器端的编程需要有效的防止内存泄露问题, 然而时过境迁, 如 今 Java 的垃圾回收机制已经成为被诟病的东西。移动智能终端用户通常觉得 iOS 的系统比 Android 系统有更好的用户体验, 其中一个深层次的原因就在于 Android 系统中垃圾回收的不可预知性。 补充: 垃圾回收机制有很多种, 包括: 分代复制垃圾回收、标记垃圾回收、增量垃圾回收等方式。标准的 Java 进程既有栈又有堆。栈保存了原始型局部变量, 堆保存了要创建的对象。Java 平台对堆内存回收和再利用的基本算法被称为标记和清除,但是 Java 对其进行了改进,采用“ 分代式垃圾收集”。这种方法会跟 Java 对象的生命周期将堆内存划分为不同的区域, 在垃圾收集过程中, 可能会将对象移动到不同区域: 伊甸园(Eden):这是对象最初诞生的区域,并且对大多数对象来说, 这里是它们唯一存在过的区域。 幸存者乐园(Survivor):从伊甸园幸存下来的对象会被挪到这里。 终身颐养园(Tenured):这是足够老的幸存对象的归宿。年轻代收集(Minor-GC)过程是不会触及这个地方的。当年轻代收集不能把对象放进终身颐养园时,就会触发一次完全收集(Major-GC),这里可能还会牵扯到压缩,以便为大对象腾出足够的空间。
与垃圾回收相关的 JVM 参数:
-Xms / -Xmx — 堆的初始大小 / 堆的最大大小 -Xmn — 堆中年轻代的大小 -XX:-DisableExplicitGC — 让 System.gc()不产生任何作用 -XX:+PrintGCDetails — 打印 GC 的细节 -XX:+PrintGCDateStamps — 打印 GC 操作的时间戳 -XX:NewSize / XX:MaxNewSize — 设置新生代大小/新生代最大大小 -XX:NewRatio — 可以设置老生代和新生代的比例 -XX:PrintTenuringDistribution — 设置每次新生代 GC 后输出幸存者 乐园中对象年龄的分布 -XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:设置老 年代阀值的初始值和最大值 -XX:TargetSurvivorRatio:设置幸存区的目标使用率
哪些情况下的对象会被垃圾回收机制处理掉?
Java的垃圾回收机制会自动回收不再使用的对象所占用的内存。以下是一些可能导致对象被垃圾回收的情况:
- 对象没有强引用:当一个对象不再被任何强引用指向时,它就可以被垃圾回收。
- 对象所在的引用链断裂:如果一个对象仅通过弱引用、软引用或虚引用来保持引用,那么当相应的引用类型不再需要时,对象就可能被回收。
- 对象池中的对象不再使用:对于实现了对象池模式的类,当对象不再需要时,可能会被回收。
- 长生命周期对象的替换:对于长时间存在的对象,如果它们不再使用,也可能被垃圾回收机制处理。
分析 Java 中的垃圾回收算法,如标记-清除、标记-整理、复制算法等
标记-清除算法
- 标记阶段:标记出活动对象。
- 清除阶段:回收未被标记的对象。
标记-整理算法
- 标记阶段:标记活动对象。
- 整理阶段:将存活的对象移动到内存的一端,然后清理边界之外的内存。
复制算法
- 分代假设:新生代对象大多很快死亡,老年代对象存活率较高。
- 复制过程:将内存分为两个相等的部分,每次只使用其中一部分,回收时将存活对象复制到另一部分。
总结
算法描述标记-清除标记活动对象,然后清除未被标记的对象。标记-整理标记活动对象,然后整理内存空间。复制算法将内存分为两部分,每次只使用一部分,回收时复制存活对象。
64 位 JVM 中,int 的长度是多数?
Java 中,int 类型变量的长度是一个固定值,与平台无关,都是 32 位。意思就是说, 在 32 位 和 64 位 的 Java 虚拟机中, int 类型的长度是相同的。
Serial 与 Parallel GC 之间的不同之处?
Serial 与 Parallel 在 GC 执行的时候都会引起 stop-the-world。它们之间主要不同 serial 收集器是默认的复制收集器, 执行 GC 的时候只有一个线程, 而parallel 收集器使用多个 GC 线程来执行。
32 位和 64 位的 JVM,int 类型变量的长度是多数?
32 位和 64 位的 JVM 中, int 类型变量的长度是相同的, 都是 32 位或者 4 个字节。
Java 中 WeakReference 与 SoftReference 的区别?
虽然 WeakReference 与 SoftReference 都有利于提高 GC 和 内存的效率, 但是 WeakReference ,一旦失去最后一个强引用,就会被 GC 回收,而软引用虽然不能阻止被回收, 但是可以延迟到 JVM 内存不足的时候。
WeakHashMap 是怎么工作的?
WeakHashMap 的工作与正常的 HashMap 类似, 但是使用弱引用作为 key, 意思就是当 key 对象没有任何引用时, key/value 将会被回收。
JVM 选项 -XX:+UseCompressedOops 有什么作用?
为什么要使用?
当你将你的应用从 32 位的 JVM 迁移到 64 位的 JVM 时,由于对象的指针从 32 位增加到了 64 位, 因此堆内存会突然增加, 差不多要翻倍。这也会对 CPU 缓存 ( 容量比内存小很多)的数据产生不利的影响。因为,迁移到 64 位的 JVM 主要动机在 于可以指定最大堆大小, 通过压缩 OOP 可以节省一定的内存。通过 -XX:+UseCompressedOops 选项, JVM 会使用 32 位的 OOP ,而不是 64 位的 OOP 。
怎样通过 Java 程序来判断 JVM 是 32 位 还是 64 位?
你可以检查某些系统属性如 sun.arch.data.model 或 os.arch 来获取该信息。
32 位 JVM 和 64 位 JVM 的最大堆内存分别是多数?
理论上说上 32 位的 JVM 堆内存可以到达 2^32,即 4GB,但实际上会比这个小很多。不同操作系统之间不同,如 Windows 系统大约 1.5 GB,Solaris 大约3GB。64 位 JVM 允许指定最大的堆内存, 理论上可以达到 2^64, 这是一个非常大的数字,实际上你可以指定堆内存大小到 100GB。甚至有的 JVM,如 Azul, 堆内存到 1000G 都是可能的。
JRE、JDK、JVM 及 JIT 之间有什么不同?
JRE 代表 Java 运行时( Java run-time),是运行 Java 引用所必须的。JDK 代表 Java 开发工具( Java development kit),是 Java 程序的开发工具,如 Java 编译器,它也包含 JRE。JVM 代表 Java 虚拟机( Java virtual machine), 它的责任是运行 Java 应用。JIT 代表即时编译( Just In Time compilation), 当代码执行的次数超过一定的阈值时, 会将 Java 字节码转换为本地代码, 如, 主要的热点代码会被准换为本地代码, 这样有利大幅度提高 Java 应用的性能。
解释 Java 堆空间及 GC?
当通过 Java 命令启动 Java 进程的时候, 会为它分配内存。内存的一部分用于创建堆空间,当程序中创建对象的时候,就从对空间中分配内存。GC 是 JVM 内部的一个进程,回收无效对象的内存用于将来的分配。
你能保证 GC 执行吗?
不能,虽然你可以调用 System.gc() 或者 Runtime.gc() ,但是没有办法保证 GC 的执行。
怎么获取 Java 程序使用的内存?堆使用的百分比?
可以通过 java.lang.Runtime 类中与内存相关方法来获取剩余的内存,总内存及最大堆内存。通过这些方法你也可以获取到堆使用的百分比及堆内存的剩余空间。 Runtime.freeMemory() 方法返回剩余空间的字节数, Runtime.totalMemory() 方法总内存的字节数, Runtime.maxMemory() 返回最大内存的字节数。
Java 中堆和栈有什么区别?
JVM 中堆和栈属于不同的内存区域,使用目的也不同。栈常用于保存方法帧和局部变量,而对象总是在堆上分配。栈通常都比堆小, 也不会在多个线程之间共享, 而堆被整个 JVM 的所有线程共享。
如何判断一个对象是否可以回收?
- 引用计数算法
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。
正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
- 可达性分析算法
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。

Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:
- 虚拟机栈中引用的对象
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
对象有哪些引用类型?
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
Java 具有四种强度不同的引用类型。
- 强引用
被强引用关联的对象不会被回收。
使用 new 一个新对象的方式来创建强引用。
Object obj = new Object();
- 软引用
被软引用关联的对象只有在内存不够的情况下才会被回收。
使用 SoftReference 类来创建软引用。
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联
- 弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
使用 WeakReference 类来实现弱引用。
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
- 虚引用
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。
为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。
使用 PhantomReference 来实现虚引用。
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;
谈谈 Java 中的弱引用(WeakReference)、软引用(SoftReference)和强引用(StrongReference)的区别及使用场景
Java中有不同类型的引用,它们有不同的强度,适用于不同的场景:
- 强引用:最常用的引用类型,只要引用存在,垃圾收集器就不会回收该对象。
- 软引用:用于描述还有用但并非必需的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中清理掉。
- 弱引用:更弱一些的引用关系,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。
使用场景
- 强引用:用于创建对象的普通引用,是最常见的引用类型。
- 软引用:通常用于实现内存敏感的缓存机制。
- 弱引用:通常用于实现缓存机制,如
WeakHashMap。
总结
引用类型描述强引用最常见的引用类型,对象不会被回收,直到引用被显式地置为 null。软引用在系统内存不足时会被回收,适用于实现内存敏感的缓存。弱引用对象会在下一次垃圾回收时被回收,适用于实现缓存。
有哪些基本的垃圾回收算法?
- 标记 - 清除

将存活的对象进行标记,然后清理掉未被标记的对象。
不足:
标记和清除过程效率都不高;
会产生大量不连续的内存碎片,导致无法给大对象分配内存。
标记 - 整理

让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
- 复制

将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。
现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。
HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。
- 分代收集
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将堆分为新生代和老年代。
- 新生代使用: 复制算法
- 老年代使用: 标记 - 清除 或者 标记 - 整理 算法
分代收集算法和分区收集算法区别?

- 分代收集算法
当前主流 VM 垃圾收集都采用”分代收集”(Generational Collection)算法, 这种算法会根据 对象存活周期的不同将内存划分为几块, 如 JVM 中的 新生代、老年代、永久代,这样就可以根据 各年代特点分别采用最适当的 GC 算法
在新生代-复制算法:
每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出少量 存活对象的复制成本就可以完成收集
在老年代-标记整理算法:
因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标 记—整理”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存.
- ParNew: 一款多线程的收集器,采用复制算法,主要工作在 Young 区,可以通过
-XX:ParallelGCThreads参数来控制收集的线程数,整个过程都是 STW 的,常与 CMS 组合使用。 - CMS: 以获取最短回收停顿时间为目标,采用“标记-清除”算法,分 4 大步进行垃圾收集,其中初始标记和重新标记会 STW ,多数应用于互联网站或者 B/S 系统的服务器端上,JDK9 被标记弃用,JDK14 被删除。
- 分区收集算法
分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的 好处是可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是 整个堆), 从而减少一次 GC 所产生的停顿。
- G1: 一种服务器端的垃圾收集器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能地满足垃圾收集暂停时间的要求。
- ZGC: JDK11 中推出的一款低延迟垃圾回收器,适用于大内存低延迟服务的内存管理和回收,SPECjbb 2015 基准测试,在 128G 的大堆下,最大停顿时间才 1.68 ms,停顿时间远胜于 G1 和 CMS。
什么是Minor GC、Major GC、Full GC?
JVM 在进行 GC 时,并非每次都对堆内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。
针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类:部分收集(Partial GC),整堆收集(Full GC)
- 部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为:
- 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
- 老年代收集(Major GC/Old GC):只是老年代的垃圾收集
- 目前,只有 CMS GC 会有单独收集老年代的行为
- 很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收
- 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集
- 目前只有 G1 GC 会有这种行为
- 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾
说说JVM内存分配策略?
- 对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
- 大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。
- 长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
- 动态对象年龄判定
虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
- 空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。
什么情况下会触发Full GC?
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
- 调用 System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
- 老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
- 空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
- JDK 1.7 及以前的永久代空间不足
在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。
当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。
- Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
Hotspot中有哪些垃圾回收器?

以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
- 单线程与多线程: 单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程;
- 串行与并行: 串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并形指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
- Serial 收集器

Serial 翻译为串行,也就是说它以串行的方式执行。
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。
- ParNew 收集器

它是 Serial 收集器的多线程版本。
是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。
- Parallel Scavenge 收集器
与 ParNew 一样是多线程收集器。
其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的: 新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手动指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
- Serial Old 收集器

是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:
- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
- Parallel Old 收集器

是 Parallel Scavenge 收集器的老年代版本。
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
- CMS 收集器

CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
分为以下四个流程:
- 初始标记: 仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
- 并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
- 重新标记: 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
- 并发清除: 不需要停顿。
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
具有以下缺点:
- 吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
- G1 收集器
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。

G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。

如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
- 初始标记
- 并发标记
- 最终标记: 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收: 首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
具备如下特点:
- 空间整合: 整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿: 能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
5.4 问题排查
常见的Linux定位问题的工具?
- 文本操作
- 文本查找 - grep
- 文本分析 - awk
- 文本处理 - sed
- 文件操作
- 文件监听 - tail
- 文件查找 - find
- 网络和进程
- 网络接口 - ifconfig
- 防火墙 - iptables -L
- 路由表 - route -n
- netstat
- 其它常用
- 进程 ps -ef | grep java
- 分区大小 df -h
- 内存 free -m
- 硬盘大小 fdisk -l |grep Disk
- top
- 环境变量 env
JDK自带的定位问题的工具?
- jps jps是jdk提供的一个查看当前java进程的小工具, 可以看做是JavaVirtual Machine Process Status Tool的缩写。
jps –l # 输出输出完全的包名,应用主类名,jar的完全路径名
- jstack jstack是jdk自带的线程堆栈分析工具,使用该命令可以查看或导出 Java 应用程序中线程堆栈信息。
# 基本
jstack 2815
jstack -m 2815 # java和native c/c++框架的所有栈信息
jstack -l 2815 # 额外的锁信息列表,查看是否死锁
- jinfo jinfo 是 JDK 自带的命令,可以用来查看正在运行的 java 应用程序的扩展参数,包括Java System属性和JVM命令行参数;也可以动态的修改正在运行的 JVM 一些参数。当系统崩溃时,jinfo可以从core文件里面知道崩溃的Java应用程序的配置信息
jinfo 2815 # 输出当前 jvm 进程的全部参数和系统属性
- jmap 命令jmap是一个多功能的命令。它可以生成 java 程序的 dump 文件, 也可以查看堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列。
# 查看堆的情况
jmap -heap 2815
# dump
jmap -dump:live,format=b,file=/tmp/heap2.bin 2815
- jstat jstat参数众多,但是使用一个就够了
jstat -gcutil 2815 1000
如何使用在线调试工具Arthas?
举几个例子
- 查看最繁忙的线程,以及是否有阻塞情况发生?
场景:我想看下查看最繁忙的线程,以及是否有阻塞情况发生? 常规查看线程,一般我们可以通过 top 等系统命令进行查看,但是那毕竟要很多个步骤,很麻烦。
thread -n 3 # 查看最繁忙的三个线程栈信息
thread # 以直观的方式展现所有的线程情况
thread -b #找出当前阻塞其他线程的线程
- 确认某个类是否已被系统加载?
场景:我新写了一个类或者一个方法,我想知道新写的代码是否被部署了?
# 即可以找到需要的类全路径,如果存在的话
sc *MyServlet
# 查看这个某个类所有的方法
sm pdai.tech.servlet.TestMyServlet *
# 查看某个方法的信息,如果存在的话
sm pdai.tech.servlet.TestMyServlet testMethod
- 如何查看一个class类的源码信息?
场景:我新修改的内容在方法内部,而上一个步骤只能看到方法,这时候可以反编译看下源码
# 直接反编译出java 源代码,包含一此额外信息的
jad pdai.tech.servlet.TestMyServlet
- 如何跟踪某个方法的返回值、入参?
场景:我想看下我新加的方法在线运行的参数和返回值?
# 同时监控入参,返回值,及异常
watch pdai.tech.servlet.TestMyServlet testMethod "{params, returnObj, throwExp}" -e -x 2
- 如何看方法调用栈的信息?
场景:我想看下某个方法的调用栈的信息?
stack pdai.tech.servlet.TestMyServlet testMethod
运行此命令之后需要即时触发方法才会有响应的信息打印在控制台上
- 找到最耗时的方法调用?
场景:testMethod这个方法入口响应很慢,如何找到最耗时的子调用?
# 执行的时候每个子调用的运行时长,可以找到最耗时的子调用。
stack pdai.tech.servlet.TestMyServlet testMethod
运行此命令之后需要即时触发方法才会有响应的信息打印在控制台上,然后一层一层看子调用。
- 如何临时更改代码运行?
场景:我找到了问题所在,能否线上直接修改测试,而不需要在本地改了代码后,重新打包部署,然后重启观察效果?
# 先反编译出class源码
jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java
# 然后使用外部工具编辑内容
mc /tmp/UserController.java -d /tmp # 再编译成class
# 最后,重新载入定义的类,就可以实时验证你的猜测了
redefine /tmp/com/example/demo/arthas/user/UserController.class
如上,是直接更改线上代码的方式,但是一般好像是编译不成功的。所以,最好是本地ide编译成 class文件后,再上传替换为好!
总之,已经完全不用重启和发布了!这个功能真的很方便,比起重启带来的代价,真的是不可比的。比如,重启时可能导致负载重分配,选主等等问题,就不是你能控制的了。
- 我如何测试某个方法的性能问题?
monitor -c 5 demo.MathGame primeFactors
如何使用Idea的远程调试?
要让远程服务器运行的代码支持远程调试,则启动的时候必须加上特定的JVM参数,这些参数是:
-Xdebug -Xrunjdwp:transport=dt_socket,suspend=n,server=y,address=127.0.0.1:5555
复杂综合类型问题的定位思路?

6 Java 新版本
Java 8版本特性,及Java8+版本特性。
6.1 Java 8 特性
什么是函数式编程?Lambda表达式?
- 函数式编程
面向对象编程是对数据进行抽象;函数式编程是对行为进行抽象。
核心思想: 使用不可变值和函数,函数对一个值进行处理,映射成另一个值。
- Lambda表达式
lambda表达式仅能放入如下代码: 预定义使用了 @Functional 注释的函数式接口,自带一个抽象函数的方法,或者SAM(Single Abstract Method 单个抽象方法)类型。这些称为lambda表达式的目标类型,可以用作返回类型,或lambda目标代码的参数。例如,若一个方法接收Runnable、Comparable或者 Callable 接口,都有单个抽象方法,可以传入lambda表达式。类似的,如果一个方法接受声明于 java.util.function 包内的接口,例如 Predicate、Function、Consumer 或 Supplier,那么可以向其传lambda表达式
Stream中常用方法?
stream(),parallelStream()filter()findAny()findFirst()sortforEachvoidmap(), reduce()flatMap()- 将多个Stream连接成一个Streamcollect(Collectors.toList())distinct,limitcountmin,max,summaryStatistics
什么是Java中的Lambda表达式和Stream API?
Java 8引入了Lambda表达式和Stream API,这两个特性极大地简化了集合的处理和并行编程。
Lambda表达式是一种简洁的表示匿名函数的方式,它允许将表达式作为方法参数,或者将代码作为数据。Lambda表达式的基本语法是:(parameters) -> expression 或 (parameters) -> { statements; }。Lambda表达式可以用于任何函数式接口,即只定义了一个抽象方法的接口。
例如,使用Lambda表达式来排序一个列表:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Collections.sort(names, (s1, s2) -> s1.compareTo(s2));
Stream API提供了一种高级抽象,可以让你以声明式方式处理数据集合。Stream API支持过滤、映射、聚合等操作,并且可以很容易地进行并行处理。
使用Stream API对列表进行操作的例子:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
long count = names.stream() .filter(name -> name.length() > 4) .count();
Stream API和Lambda表达式结合使用,可以编写出更加简洁、灵活和高效的代码。它们使得函数式编程风格在Java中变得可能,并且大大提升了处理集合和并行计算的能力。
什么是FunctionalInterface?
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface{}
- interface做注解的注解类型,被定义成java语言规
- 一个被它注解的接口只能有一个抽象方法,有两种例外
- 第一是接口允许有实现的方法,这种实现的方法是用default关键字来标记的(java反射中java.lang.reflect.Method#isDefault()方法用来判断是否是default方法)
- 第二如果声明的方法和java.lang.Object中的某个方法一样,它可以不当做未实现的方法,不违背这个原则: 一个被它注解的接口只能有一个抽象方法, 比如:
java public interface Comparator<T> { int compare(T o1, T o2); boolean equals(Object obj); } - 如果一个类型被这个注解修饰,那么编译器会要求这个类型必须满足如下条件:
- 这个类型必须是一个interface,而不是其他的注解类型、枚举enum或者类class
- 这个类型必须满足function interface的所有要求,如你个包含两个抽象方法的接口增加这个注解,会有编译错误。
- 编译器会自动把满足function interface要求的接口自动识别为function interface。
如何自定义函数接口?
@FunctionalInterface
public interface IMyInterface {
void study();
}
public class TestIMyInterface {
public static void main(String[] args) {
IMyInterface iMyInterface = () -> System.out.println("I like study");
iMyInterface.study();
}
}
内置的四大函数接口及使用?
- 消费型接口: Consumer< T> void accept(T t)有参数,无返回值的抽象方法;
Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.firstName);
greeter.accept(new Person("Luke", "Skywalker"));
- 供给型接口: Supplier < T> T get() 无参有返回值的抽象方法;
以stream().collect(Collector<? super T, A, R> collector)为例:
比如:
Supplier<Person> personSupplier = Person::new;
personSupplier.get(); // new Person
- 断定型接口:
Predicate<T> boolean test(T t):有参,但是返回值类型是固定的boolean
比如: steam().filter()中参数就是Predicate
Predicate<String> predicate = (s) -> s.length() > 0;
predicate.test("foo"); // true
predicate.negate().test("foo"); // false
Predicate<Boolean> nonNull = Objects::nonNull;
Predicate<Boolean> isNull = Objects::isNull;
Predicate<String> isEmpty = String::isEmpty;
Predicate<String> isNotEmpty = isEmpty.negate();
- 函数型接口: Function<T,R> R apply(T t)有参有返回值的抽象方法;
比如: steam().map() 中参数就是Function<? super T, ? extends R>;reduce()中参数BinaryOperator<T> (ps: BinaryOperator<T> extends BiFunction<T,T,T>)
Function<String, Integer> toInteger = Integer::valueOf;
Function<String, String> backToString = toInteger.andThen(String::valueOf);
backToString.apply("123"); // "123"
Optional要解决什么问题?
在调用一个方法得到了返回值却不能直接将返回值作为参数去调用别的方法,我们首先要判断这个返回值是否为null,只有在非空的前提下才能将其作为其他方法的参数。Java 8引入了一个新的Optional类:这是一个可以为null的容器对象,如果值存在则isPresent()方法会返回true,调用get()方法会返回该对象。
如何使用Optional来解决嵌套对象的判空问题?
假设我们有一个像这样的类层次结构:
class Outer {
Nested nested;
Nested getNested() {
return nested;
}
}
class Nested {
Inner inner;
Inner getInner() {
return inner;
}
}
class Inner {
String foo;
String getFoo() {
return foo;
}
}
解决这种结构的深层嵌套路径是有点麻烦的。我们必须编写一堆 null 检查来确保不会导致一个 NullPointerException:
Outer outer = new Outer();
if (outer != null && outer.nested != null && outer.nested.inner != null) {
System.out.println(outer.nested.inner.foo);
}
我们可以通过利用 Java 8 的 Optional 类型来摆脱所有这些 null 检查。map 方法接收一个 Function 类型的 lambda 表达式,并自动将每个 function 的结果包装成一个 Optional 对象。这使我们能够在一行中进行多个 map 操作。Null 检查是在底层自动处理的。
Optional.of(new Outer())
.map(Outer::getNested)
.map(Nested::getInner)
.map(Inner::getFoo)
.ifPresent(System.out::println);
还有一种实现相同作用的方式就是通过利用一个 supplier 函数来解决嵌套路径的问题:
Outer obj = new Outer();
resolve(() -> obj.getNested().getInner().getFoo())
.ifPresent(System.out::println);
什么是默认方法,为什么要有默认方法?
就是接口可以有实现方法,而且不需要实现类去实现其方法。只需在方法名前面加个default关键字即可。
public interface A {
default void foo(){
System.out.println("Calling A.foo()");
}
}
public class Clazz implements A {
public static void main(String[] args){
Clazz clazz = new Clazz();
clazz.foo();//调用A.foo()
}
}
- 为什么出现默认方法?
首先,之前的接口是个双刃剑,好处是面向抽象而不是面向具体编程,缺陷是,当需要修改接口时候,需要修改全部实现该接口的类,目前的java 8之前的集合框架没有foreach方法,通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而,对于已经发布的版本,是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法。他们的目的是为了解决接口的修改与现有的实现不兼容的问题。
什么是类型注解?
类型注解被用来支持在Java的程序中做强类型检查。配合插件式的check framework,可以在编译的时候检测出runtime error,以提高代码质量。这就是类型注解的作用了。
- 在java 8之前,注解只能是在声明的地方所使用,比如类,方法,属性;
- java 8里面,注解可以应用在任何地方,比如:
创建类实例
new @Interned MyObject();
类型映射
myString = (@NonNull String) str;
implements 语句中
class UnmodifiableList<T> implements @Readonly List<@Readonly T> { … }
throw exception声明
void monitorTemperature() throws @Critical TemperatureException { … }
需要注意的是,类型注解只是语法而不是语义,并不会影响java的编译时间,加载时间,以及运行时间,也就是说,编译成class文件的时候并不包含类型注解。
什么是重复注解?
允许在同一申明类型(类,属性,或方法)的多次使用同一个注解
- JDK8之前
java 8之前也有重复使用注解的解决方案,但可读性不是很好,比如下面的代码:
public @interface Authority {
String role();
}
public @interface Authorities {
Authority[] value();
}
public class RepeatAnnotationUseOldVersion {
@Authorities({@Authority(role="Admin"),@Authority(role="Manager")})
public void doSomeThing(){
}
}
由另一个注解来存储重复注解,在使用时候,用存储注解Authorities来扩展重复注解。
- Jdk8重复注解
我们再来看看java 8里面的做法:
@Repeatable(Authorities.class)
public @interface Authority {
String role();
}
public @interface Authorities {
Authority[] value();
}
public class RepeatAnnotationUseNewVersion {
@Authority(role="Admin")
@Authority(role="Manager")
public void doSomeThing(){ }
}
不同的地方是,创建重复注解Authority时,加上@Repeatable,指向存储注解Authorities,在使用时候,直接可以重复使用Authority注解。从上面例子看出,java 8里面做法更适合常规的思维,可读性强一点
说出 JDK 1.7 中的三个新特性?
虽然 JDK 1.7 不像 JDK 5 和 8 一样的大版本, 但是, 还是有很多新的特性, 如 try-with-resource 语句, 这样你在使用流或者资源的时候, 就不需要手动关闭, Java 会自动关闭。Fork-Join 池某种程度上实现 Java 版的 Map-reduce。允许 Switch 中有 String 变量和文本。菱形操作符(<>)用于类型推断, 不再需要在变量声明的右边申明泛型, 因此可以写出可读写更强、更简洁的代码。另一个值得一提的特性是改善异常处理,如允许在同一个 catch 块中捕获多个异常。
说出 5 个 JDK 1.8 引入的新特性?
Java 8 在 Java 历史上是一个开创新的版本, 下面 JDK 8 中 5 个主要的特性: Lambda 表达式, 允许像对象一样传递匿名函数 Stream API, 充分利用现代多核 CPU, 可以写出很简洁的代码 Date 与 Time API, 最终, 有一个稳定、简单的日期和时间库可供你使用扩展方法, 现在, 接口中可以有静态、默认方法。 重复注解, 现在你可以将相同的注解在同一类型上使用多次。
6.2 Java 9+ 特性
Java 9后续版本发布是按照什么样的发布策略呢?
Java现在发布的版本很快,每年两个,但是真正会被大规模使用的是三年一个的TLS版本。
- 每3年发布一个TLS,长期维护版本。意味着Java 8 ,Java 11, Java 17 才可能被大规模使用。
- 每年发布两个正式版本,分别是3月份和9月份。
Java 9后续新版本中你知道哪些?
能够举几个即可:
- Java10 - 并行全垃圾回收器 G1
大家如果接触过 Java 性能调优工作,应该会知道,调优的最终目标是通过参数设置来达到快速、低延时的内存垃圾回收以提高应用吞吐量,尽可能的避免因内存回收不及时而触发的完整 GC(Full GC 会带来应用出现卡顿)。
G1 垃圾回收器是 Java 9 中 Hotspot 的默认垃圾回收器,是以一种低延时的垃圾回收器来设计的,旨在避免进行 Full GC,但是当并发收集无法快速回收内存时,会触发垃圾回收器回退进行 Full GC。之前 Java 版本中的 G1 垃圾回收器执行 GC 时采用的是基于单线程标记扫描压缩算法(mark-sweep-compact)。为了最大限度地减少 Full GC 造成的应用停顿的影响,Java 10 中将为 G1 引入多线程并行 GC,同时会使用与年轻代回收和混合回收相同的并行工作线程数量,从而减少了 Full GC 的发生,以带来更好的性能提升、更大的吞吐量。
Java 10 中将采用并行化 mark-sweep-compact 算法,并使用与年轻代回收和混合回收相同数量的线程。具体并行 GC 线程数量可以通过: -XX:ParallelGCThreads 参数来调节,但这也会影响用于年轻代和混合收集的工作线程数。
- Java11 - ZGC:可伸缩低延迟垃圾收集器
ZGC 即 Z Garbage Collector(垃圾收集器或垃圾回收器),这应该是 Java 11 中最为瞩目的特性,没有之一。ZGC 是一个可伸缩的、低延迟的垃圾收集器,主要为了满足如下目标进行设计:
- GC 停顿时间不超过 10ms
- 即能处理几百 MB 的小堆,也能处理几个 TB 的大堆
- 应用吞吐能力不会下降超过 15%(与 G1 回收算法相比)
- 方便在此基础上引入新的 GC 特性和利用 colord
- 针以及 Load barriers 优化奠定基础
- 当前只支持 Linux/x64 位平台
停顿时间在 10ms 以下,10ms 其实是一个很保守的数据,即便是 10ms 这个数据,也是 GC 调优几乎达不到的极值。根据 SPECjbb 2015 的基准测试,128G 的大堆下最大停顿时间才 1.68ms,远低于 10ms,和 G1 算法相比,改进非常明显。

- Java 14 - Switch 表达式(正式版)
switch 表达式在之前的 Java 12 和 Java 13 中都是处于预览阶段,而在这次更新的 Java 14 中,终于成为稳定版本,能够正式可用。
switch 表达式带来的不仅仅是编码上的简洁、流畅,也精简了 switch 语句的使用方式,同时也兼容之前的 switch 语句的使用;之前使用 switch 语句时,在每个分支结束之前,往往都需要加上 break 关键字进行分支跳出,以防 switch 语句一直往后执行到整个 switch 语句结束,由此造成一些意想不到的问题。switch 语句一般使用冒号 :来作为语句分支代码的开始,而 switch 表达式则提供了新的分支切换方式,即 -> 符号右则表达式方法体在执行完分支方法之后,自动结束 switch 分支,同时 -> 右则方法块中可以是表达式、代码块或者是手动抛出的异常。以往的 switch 语句写法如下:
int dayOfWeek;
switch (day) {
case MONDAY:
case FRIDAY:
case SUNDAY:
dayOfWeek = 6;
break;
case TUESDAY:
dayOfWeek = 7;
break;
case THURSDAY:
case SATURDAY:
dayOfWeek = 8;
break;
case WEDNESDAY:
dayOfWeek = 9;
break;
default:
dayOfWeek = 0;
break;
}
而现在 Java 14 可以使用 switch 表达式正式版之后,上面语句可以转换为下列写法:
int dayOfWeek = switch (day) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
default -> 0;
};
很明显,switch 表达式将之前 switch 语句从编码方式上简化了不少,但是还是需要注意下面几点:
- 需要保持与之前 switch 语句同样的 case 分支情况。
- 之前需要用变量来接收返回值,而现在直接使用 yield 关键字来返回 case 分支需要返回的结果。
- 现在的 switch 表达式中不再需要显式地使用 return、break 或者 continue 来跳出当前分支。
- 现在不需要像之前一样,在每个分支结束之前加上 break 关键字来结束当前分支,如果不加,则会默认往后执行,直到遇到 break 关键字或者整个 switch 语句结束,在 Java 14 表达式中,表达式默认执行完之后自动跳出,不会继续往后执行。
- 对于多个相同的 case 方法块,可以将 case 条件并列,而不需要像之前一样,通过每个 case 后面故意不加 break 关键字来使用相同方法块。
使用 switch 表达式来替换之前的 switch 语句,确实精简了不少代码,提高了编码效率,同时也可以规避一些可能由于不太经意而出现的意想不到的情况,可见 Java 在提高使用者编码效率、编码体验和简化使用方面一直在不停的努力中,同时也期待未来有更多的类似 lambda、switch 表达式这样的新特性出来。
- Java 14 - Records
在 Java 14 中引入了 Record 类型,其效果有些类似 Lombok 的 @Data 注解、Kotlin 中的 data class,但是又不尽完全相同,它们的共同点都是类的部分或者全部可以直接在类头中定义、描述,并且这个类只用于存储数据而已。对于 Record 类型,具体可以用下面代码来说明:
public record Person(String name, int age) {
public static String address;
public String getName() {
return name;
}
}
对上述代码进行编译,然后反编译之后可以看到如下结果:
public final class Person extends java.lang.Record {
private final java.lang.String name;
private final java.lang.String age;
public Person(java.lang.String name, java.lang.String age) { /* compiled code */ }
public java.lang.String getName() { /* compiled code */ }
public java.lang.String toString() { /* compiled code */ }
public final int hashCode() { /* compiled code */ }
public final boolean equals(java.lang.Object o) { /* compiled code */ }
public java.lang.String name() { /* compiled code */ }
public java.lang.String age() { /* compiled code */ }
}