1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-25 02:27:10 +08:00
This commit is contained in:
ygf 2020-09-17 15:25:26 +08:00
commit 057a82a981
15 changed files with 901 additions and 381 deletions

View File

@ -1,3 +1,5 @@
> 准备面试的小伙伴可以考虑面试专版:[《Java面试进阶指南》](https://xiaozhuanlan.com/javainterview?rel=javaguide) ,欢迎加入[我的星球](https://wx.zsxq.com/dweb2/index/group/48418884588288)获取更多实用干货。
如果 Github 访问速度比较慢或者图片无法刷新出来的话,可以转移到[码云](https://gitee.com/SnailClimb/JavaGuide )查看,或者[在线阅读](https://snailclimb.gitee.io/javaguide )。**如果你要提交 issue 或者 pr 的话请到 [Github](https://github.com/Snailclimb/JavaGuide) 提交。** 如果 Github 访问速度比较慢或者图片无法刷新出来的话,可以转移到[码云](https://gitee.com/SnailClimb/JavaGuide )查看,或者[在线阅读](https://snailclimb.gitee.io/javaguide )。**如果你要提交 issue 或者 pr 的话请到 [Github](https://github.com/Snailclimb/JavaGuide) 提交。**
《JavaGuide面试突击版》PDF版本+3本PDF Java 学习手册,在公众号 **[JavaGuide](#公众号)** 后台回复“**面试突击**”即可获取。 《JavaGuide面试突击版》PDF版本+3本PDF Java 学习手册,在公众号 **[JavaGuide](#公众号)** 后台回复“**面试突击**”即可获取。
@ -43,7 +45,7 @@
<tr> <tr>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://w.url.cn/s/AS6JeXA"> <a href="https://w.url.cn/s/AS6JeXA">
<img src="http://javaguide.cn/991599477417_.pic_hd.jpg" style="margin: 0 auto;width:450px" /></a> <img src="https://img-service.csdnimg.cn/img_convert/3556d24d84c4880abaf280ed833faccc.png#pic_center" style="margin: 0 auto;width:450px" /></a>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@ -108,8 +110,6 @@
* [待办](#待办) * [待办](#待办)
* [说明](#说明) * [说明](#说明)
准备面试的小伙伴可以考虑面试专版:[《Java面试进阶指南》](https://xiaozhuanlan.com/javainterview?rel=javaguide) ,欢迎加入[我的星球](https://wx.zsxq.com/dweb2/index/group/48418884588288)获取更多实用干货。
## Java ## Java

View File

@ -39,7 +39,7 @@
![布隆过滤器hash计算](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/布隆过滤器-hash运算.png) ![布隆过滤器hash计算](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/布隆过滤器-hash运算.png)
如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后在对应的位数组的下表的元素设置为 1当位数组初始化时 所有位置均为0。当第二次存储相同字符串时因为先前的对应位置已设置为1所以很容易知道此值已经存在去重非常方便 如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后在对应的位数组的下表的元素设置为 1当位数组初始化时 所有位置均为0。当第二次存储相同字符串时因为先前的对应位置已设置为 1所以很容易知道此值已经存在去重非常方便
如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1如果值都为 1那么说明这个值在布隆过滤器中如果存在一个值不为 1说明该元素不在布隆过滤器中。 如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1如果值都为 1那么说明这个值在布隆过滤器中如果存在一个值不为 1说明该元素不在布隆过滤器中。
@ -49,7 +49,7 @@
### 3.布隆过滤器使用场景 ### 3.布隆过滤器使用场景
1. 判断给定数据是否存在比如判断一个数字是否在于包含大量数字的数字集中数字集很大5亿以上、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤、黑名单功能等等。 1. 判断给定数据是否存在:比如判断一个数字是否在于包含大量数字的数字集中数字集很大5亿以上、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤、黑名单功能等等。
2. 去重:比如爬给定网址的时候对已经爬取过的 URL 去重。 2. 去重:比如爬给定网址的时候对已经爬取过的 URL 去重。
### 4.通过 Java 编程手动实现布隆过滤器 ### 4.通过 Java 编程手动实现布隆过滤器

View File

@ -98,7 +98,7 @@ MyISAM是MySQL的默认数据库引擎5.5版之前)。虽然性能极佳,
**两者的对比:** **两者的对比:**
1. **是否支持行级锁** : MyISAM 只有表级锁(table-level locking)而InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。 1. **是否支持行级锁** : MyISAM 只有表级锁(table-level locking)而InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。
2. **是否支持事务和崩溃后的安全恢复: MyISAM** 强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快但是不提供事务支持。但是**InnoDB** 提供事务支持事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。 2. **是否支持事务和崩溃后的安全恢复: MyISAM** 强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快但是不提供事务支持。但是**InnoDB** 提供事务支持,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。
3. **是否支持外键:** MyISAM不支持而InnoDB支持。 3. **是否支持外键:** MyISAM不支持而InnoDB支持。
4. **是否支持MVCC** :仅 InnoDB 支持。应对高并发事务, MVCC比单纯的加锁更高效;MVCC只在 `READ COMMITTED``REPEATABLE READ` 两个隔离级别下工作;MVCC可以使用 乐观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统一。推荐阅读[MySQL-InnoDB-MVCC多版本并发控制](https://segmentfault.com/a/1190000012650596) 4. **是否支持MVCC** :仅 InnoDB 支持。应对高并发事务, MVCC比单纯的加锁更高效;MVCC只在 `READ COMMITTED``REPEATABLE READ` 两个隔离级别下工作;MVCC可以使用 乐观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统一。推荐阅读[MySQL-InnoDB-MVCC多版本并发控制](https://segmentfault.com/a/1190000012650596)
5. ...... 5. ......
@ -148,7 +148,7 @@ set global query_cache_size=600000;
缓存建立之后MySQL的查询缓存系统会跟踪查询中涉及的每张表如果这些表数据或结构发生变化那么和这张表相关的所有缓存数据都将失效。 缓存建立之后MySQL的查询缓存系统会跟踪查询中涉及的每张表如果这些表数据或结构发生变化那么和这张表相关的所有缓存数据都将失效。
**缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。** 因此,开启缓存查询要谨慎尤其对于写密集的应用来说更是如此。如果开启要注意合理控制缓存空间大小一般来说其大小设置为几十MB比较合适。此外**还可以通过sql_cache和sql_no_cache来控制某个查询语句是否需要缓存** **缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。** 因此,开启查询缓存要谨慎尤其对于写密集的应用来说更是如此。如果开启要注意合理控制缓存空间大小一般来说其大小设置为几十MB比较合适。此外**还可以通过sql_cache和sql_no_cache来控制某个查询语句是否需要缓存**
```sql ```sql
select sql_no_cache count(*) from usr; select sql_no_cache count(*) from usr;
``` ```
@ -164,7 +164,7 @@ select sql_no_cache count(*) from usr;
![事物的特性](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/事务特性.png) ![事物的特性](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/事务特性.png)
1. **原子性Atomicity** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; 1. **原子性Atomicity** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
2. **一致性Consistency** 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的 2. **一致性Consistency** 执行事务后,数据库从一个正确的状态变化到另一个正确的状态
3. **隔离性Isolation** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; 3. **隔离性Isolation** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
4. **持久性Durability** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。 4. **持久性Durability** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

View File

@ -462,7 +462,7 @@ typedef struct redisDb {
常用的过期数据的删除策略就两个(重要!自己造缓存轮子的时候需要格外考虑的东西): 常用的过期数据的删除策略就两个(重要!自己造缓存轮子的时候需要格外考虑的东西):
1. **惰性删除** 只会在取出key的时候才对数据进行过期检查。这样对CPU最友好但是可能会造成太多过期 key 没有被删除。 1. **惰性删除** 只会在取出key的时候才对数据进行过期检查。这样对CPU最友好但是可能会造成太多过期 key 没有被删除。
2. **定期删除** 每隔一段时间抽取一批 key 执行删除过期key操作。并且Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。 2. **定期删除** 每隔一段时间抽取一批 key 执行删除过期key操作。并且Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
定期删除对内存更加友好惰性删除对CPU更加友好。两者各有千秋所以Redis 采用的是 **定期删除+惰性/懒汉式删除** 定期删除对内存更加友好惰性删除对CPU更加友好。两者各有千秋所以Redis 采用的是 **定期删除+惰性/懒汉式删除**

View File

@ -130,7 +130,7 @@ Java 虚拟机JVM是运行 Java 字节码的虚拟机。JVM 有针对不
##### 1.1.2.2. JDK 和 JRE ##### 1.1.2.2. JDK 和 JRE
JDK 是 Java Development Kit它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切还有编译器javac和工具如 javadoc 和 jdb。它能够创建和编译程序。 JDK 是 Java Development Kit 缩写,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切还有编译器javac和工具如 javadoc 和 jdb。它能够创建和编译程序。
JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机JVMJava 类库java 命令和其他的一些基础构件。但是,它不能用于创建新程序。 JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机JVMJava 类库java 命令和其他的一些基础构件。但是,它不能用于创建新程序。
@ -162,12 +162,12 @@ JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有
- 都是面向对象的语言,都支持封装、继承和多态 - 都是面向对象的语言,都支持封装、继承和多态
- Java 不提供指针来直接访问内存,程序内存更加安全 - Java 不提供指针来直接访问内存,程序内存更加安全
- Java 的类是单继承的C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。 - Java 的类是单继承的C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。
- Java 有自动内存管理机制,不需要程序员手动释放无用内存 - Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存
- **在 C 语言中,字符串或字符数组最后都会有一个额外的字符\0来表示结束。但是Java 语言中没有结束符这一概念。** 这是一个值得深度思考的问题,具体原因推荐看这篇文章: [https://blog.csdn.net/sszgg2006/article/details/49148189](https://blog.csdn.net/sszgg2006/article/details/49148189) - **在 C 语言中,字符串或字符数组最后都会有一个额外的字符`'\0'`来表示结束。但是Java 语言中没有结束符这一概念。** 这是一个值得深度思考的问题,具体原因推荐看这篇文章: [https://blog.csdn.net/sszgg2006/article/details/49148189](https://blog.csdn.net/sszgg2006/article/details/49148189)
#### 1.1.5. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同? #### 1.1.5. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同?
一个程序中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 main方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public 类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。 一个程序中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 `main()` 方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public 类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。
#### 1.1.6. Java 应用程序与小程序之间有哪些差别? #### 1.1.6. Java 应用程序与小程序之间有哪些差别?
@ -190,9 +190,10 @@ Java 语言既具有编译型语言的特征,也具有解释型语言的特征
#### 1.2.1. 字符型常量和字符串常量的区别? #### 1.2.1. 字符型常量和字符串常量的区别?
1. 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符 1. 形式上: 字符常量是单引号引起的一个字符; 字符串常量是双引号引起的0个或若干个字符
2. 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置) 2. 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)
3. 占内存大小 字符常量只占 2 个字节; 字符串常量占若干个字节 (**注意: char 在 Java 中占两个字节**) 3. 占内存大小 字符常量只占 2 个字节; 字符串常量占若干个字节 (**注意: char 在 Java 中占两个字节**),
> 字符封装类 `Character` 有一个成员常量 `Character.SIZE` 值为16,单位是`bits`,该值除以8(`1byte=8bits`)后就可以得到2个字节
> java 编程思想第四版2.2.2 节 > java 编程思想第四版2.2.2 节
> ![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-15/86735519.jpg) > ![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-15/86735519.jpg)
@ -207,7 +208,7 @@ Java 中的注释有三种:
3. 文档注释。 3. 文档注释。
在我们编写代码的时候,如果代码量比较少,我们自己或者团队其他成员还可以很轻易地看懂代码,但是当项目结构一旦复杂起来,我们就需要用到注释了。注释并不会执行,是我们程序员写给自己看的,注释是你的代码说明书,能够帮助看代码的人快速地理清代码之间的逻辑关系。因此,在写程序的时候随手加上注释是一个非常好的习惯。 在我们编写代码的时候,如果代码量比较少,我们自己或者团队其他成员还可以很轻易地看懂代码,但是当项目结构一旦复杂起来,我们就需要用到注释了。注释并不会执行(编译器在编译代码之前会把代码中的所有注释抹掉,字节码中不保留注释),是我们程序员写给自己看的,注释是你的代码说明书,能够帮助看代码的人快速地理清代码之间的逻辑关系。因此,在写程序的时候随手加上注释是一个非常好的习惯。
《Clean Code》这本书明确指出 《Clean Code》这本书明确指出
@ -253,7 +254,7 @@ Java 中的注释有三种:
在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1Java 提供了一种特殊的运算符,用于这种表达式,叫做自增运算符(++)和自减运算符(--)。 在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1Java 提供了一种特殊的运算符,用于这种表达式,叫做自增运算符(++)和自减运算符(--)。
++和--运算符可以放在操作数之前,也可以放在操作数之后,当运算符放在操作数之前时,先自增/减,再赋值;当运算符放在操作数之后时,先赋值,再自增/减。例如当“b=++a”时先自增自己增加 1再赋值赋值给 b当“b=a++”时,先赋值(赋值给 b),再自增(自己增加 1。也就是++a 输出的是 a+1 的值a++输出的是 a 值。用一句口诀就是:“符号在前就先加/减,符号在后就后加/减”。 ++和--运算符可以放在变量之前,也可以放在变量之后,当运算符放在变量之前时(前缀),先自增/减,再赋值;当运算符放在变量之后时(后缀),先赋值,再自增/减。例如,当 `b = ++a` 时,先自增(自己增加 1再赋值赋值给 b`b = a++` 时,先赋值(赋值给 b),再自增(自己增加 1。也就是++a 输出的是 a+1 的值a++输出的是 a 值。用一句口诀就是:“符号在前就先加/减,符号在后就后加/减”。
#### 1.2.6. continue、break、和return的区别是什么 #### 1.2.6. continue、break、和return的区别是什么
@ -582,7 +583,7 @@ private static class CharacterCache {
```java ```java
Integer i1 = 40; Integer i1 = 40;
Integer i2 = new Integer(40); Integer i2 = new Integer(40);
System.out.println(i1==i2);//输出 false System.out.println(i1 == i2);//输出 false
``` ```
**Integer 比较更丰富的一个例子:** **Integer 比较更丰富的一个例子:**
@ -863,9 +864,9 @@ public int f4(int a, int b) {
```java ```java
// return在无返回值方法的特殊使用 // return在无返回值方法的特殊使用
public void f5(int a) { public void f5(int a) {
if (a>10) { if (a > 10) {
return;//表示结束所在方法 f5方法的执行,下方的输出语句不会执行 return;//表示结束所在方法 f5方法的执行,下方的输出语句不会执行
} }
System.out.println(a); System.out.println(a);
} }
``` ```
@ -895,7 +896,7 @@ Java 程序在执行子类的构造方法之前,如果没有用 `super()`来
#### 2.1.4. 成员变量与局部变量的区别有哪些? #### 2.1.4. 成员变量与局部变量的区别有哪些?
1. 从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。 1. 从语法形式上看:成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
2. 从变量在内存中的存储方式来看:如果成员变量是使用`static`修饰的,那么这个成员变量是属于类的,如果没有使用`static`修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 2. 从变量在内存中的存储方式来看:如果成员变量是使用`static`修饰的,那么这个成员变量是属于类的,如果没有使用`static`修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。 3. 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。 4. 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

View File

@ -52,9 +52,9 @@ Objects.equals(null,"SnailClimb");// false
我们看一下`java.util.Objects#equals`的源码就知道原因了。 我们看一下`java.util.Objects#equals`的源码就知道原因了。
```java ```java
public static boolean equals(Object a, Object b) { public static boolean equals(Object a, Object b) {
// 可以避免空指针异常。如果a==null的话此时a.equals(b)就不会得到执行,避免出现空指针异常。 // 可以避免空指针异常。如果a==null的话此时a.equals(b)就不会得到执行,避免出现空指针异常。
return (a == b) || (a != null && a.equals(b)); return (a == b) || (a != null && a.equals(b));
} }
``` ```
**注意:** **注意:**
@ -104,14 +104,18 @@ System.out.println(a == b);// false
BigDecimal a = new BigDecimal("1.0"); BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9"); BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8"); BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);// 0.1
BigDecimal y = b.subtract(c);// 0.1 BigDecimal x = a.subtract(b);
System.out.println(x.equals(y));// true BigDecimal y = b.subtract(c);
System.out.println(x); /* 0.1 */
System.out.println(y); /* 0.1 */
System.out.println(Objects.equals(x, y)); /* true */
``` ```
### 1.3.2. BigDecimal 的大小比较 ### 1.3.2. BigDecimal 的大小比较
`a.compareTo(b)` : 返回 -1 表示小于0 表示 等于, 1表示 大于。 `a.compareTo(b)` : 返回 -1 表示 `a` 小于 `b`0 表示 `a` 等于 `b` 1表示 `a` 大于 `b`
```java ```java
BigDecimal a = new BigDecimal("1.0"); BigDecimal a = new BigDecimal("1.0");
@ -167,7 +171,7 @@ Reference:《阿里巴巴Java开发手册》
`Arrays.asList()`在平时开发中还是比较常见的我们可以使用它将一个数组转换为一个List集合。 `Arrays.asList()`在平时开发中还是比较常见的我们可以使用它将一个数组转换为一个List集合。
```java ```java
String[] myArray = { "Apple", "Banana", "Orange" } String[] myArray = {"Apple", "Banana", "Orange"};
List<String> myList = Arrays.asList(myArray); List<String> myList = Arrays.asList(myArray);
//上面两个语句等价于下面一条语句 //上面两个语句等价于下面一条语句
List<String> myList = Arrays.asList("Apple","Banana", "Orange"); List<String> myList = Arrays.asList("Apple","Banana", "Orange");
@ -177,8 +181,9 @@ JDK 源码对于这个方法的说明:
```java ```java
/** /**
*返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁与 Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。 *返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的API之间的桥梁
*/ * 与 Collection.toArray()结合使用。返回的List是可序列化并实现RandomAccess接口。
*/
public static <T> List<T> asList(T... a) { public static <T> List<T> asList(T... a) {
return new ArrayList<>(a); return new ArrayList<>(a);
} }
@ -197,12 +202,12 @@ public static <T> List<T> asList(T... a) {
`Arrays.asList()`是泛型方法,传入的对象必须是对象数组。 `Arrays.asList()`是泛型方法,传入的对象必须是对象数组。
```java ```java
int[] myArray = { 1, 2, 3 }; int[] myArray = {1, 2, 3};
List myList = Arrays.asList(myArray); List myList = Arrays.asList(myArray);
System.out.println(myList.size());//1 System.out.println(myList.size());//1
System.out.println(myList.get(0));//数组地址值 System.out.println(myList.get(0));//数组地址值
System.out.println(myList.get(1));//报错ArrayIndexOutOfBoundsException System.out.println(myList.get(1));//报错ArrayIndexOutOfBoundsException
int [] array=(int[]) myList.get(0); int[] array = (int[]) myList.get(0);
System.out.println(array[0]);//1 System.out.println(array[0]);//1
``` ```
当传入一个原生数据类型数组时,`Arrays.asList()` 的真正得到的参数就不是数组中的元素而是数组对象本身此时List 的唯一元素就是这个数组,这也就解释了上面的代码。 当传入一个原生数据类型数组时,`Arrays.asList()` 的真正得到的参数就不是数组中的元素而是数组对象本身此时List 的唯一元素就是这个数组,这也就解释了上面的代码。
@ -210,7 +215,7 @@ System.out.println(array[0]);//1
我们使用包装类型数组就可以解决这个问题。 我们使用包装类型数组就可以解决这个问题。
```java ```java
Integer[] myArray = { 1, 2, 3 }; Integer[] myArray = {1, 2, 3};
``` ```
**使用集合的修改方法:`add()``remove()``clear()`会抛出异常。** **使用集合的修改方法:`add()``remove()``clear()`会抛出异常。**
@ -296,7 +301,7 @@ static <T> List<T> arrayToList(final T[] array) {
for (final T s : array) { for (final T s : array) {
l.add(s); l.add(s);
} }
return (l); return l;
} }
``` ```
@ -344,6 +349,14 @@ List<String> list = new ArrayList<String>();
CollectionUtils.addAll(list, str); CollectionUtils.addAll(list, str);
``` ```
**6. 使用 Java9 的 `List.of()`方法**
``` java
Integer[] array = {1, 2, 3};
List<Integer> list = List.of(array);
System.out.println(list); /* [1, 2, 3] */
/* 不支持基本数据类型 */
```
## 2.2. Collection.toArray()方法使用的坑&如何反转数组 ## 2.2. Collection.toArray()方法使用的坑&如何反转数组
该方法是一个泛型方法:`<T> T[] toArray(T[] a);` 如果`toArray`方法中没有传递任何参数的话返回的是`Object`类型数组。 该方法是一个泛型方法:`<T> T[] toArray(T[] a);` 如果`toArray`方法中没有传递任何参数的话返回的是`Object`类型数组。
@ -365,6 +378,16 @@ s=list.toArray(new String[0]);//没有指定类型的话会报错
> **fail-fast 机制** :多个线程对 fail-fast 集合进行修改的时可能会抛出ConcurrentModificationException单线程下也会出现这种情况上面已经提到过。 > **fail-fast 机制** :多个线程对 fail-fast 集合进行修改的时可能会抛出ConcurrentModificationException单线程下也会出现这种情况上面已经提到过。
Java8开始可以使用`Collection#removeIf()`方法删除满足特定条件的元素,如
``` java
List<Integer> list = new ArrayList<>();
for (int i = 1; i <= 10; ++i) {
list.add(i);
}
list.removeIf(filter -> filter % 2 == 0); /* 删除list中的所有偶数 */
System.out.println(list); /* [1, 3, 5, 7, 9] */
```
`java.util`包下面的所有的集合类都是fail-fast的`java.util.concurrent`包下面的所有的类都是fail-safe的。 `java.util`包下面的所有的集合类都是fail-fast的`java.util.concurrent`包下面的所有的类都是fail-safe的。
![不要在 foreach 循环里进行元素的 remove/add 操作](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/foreach-remove:add.png) ![不要在 foreach 循环里进行元素的 remove/add 操作](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019/7/foreach-remove:add.png)

View File

@ -1,65 +1,118 @@
点击关注[公众号](#公众号)及时获取笔主最新更新文章并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。 点击关注[公众号](#公众号)及时获取笔主最新更新文章并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。
<!-- TOC -->
<!-- @import "[TOC]" {cmd="toc" depthFrom=1 depthTo=6 orderedList=false} -->
<!-- code_chunk_output -->
- [Java 并发进阶常见面试题总结](#java-并发进阶常见面试题总结) - [Java 并发进阶常见面试题总结](#java-并发进阶常见面试题总结)
- [1. synchronized 关键字](#1-synchronized-关键字) - [1.synchronized 关键字](#1synchronized-关键字)
- [1.1. 说一说自己对于 synchronized 关键字的了解](#11-说一说自己对于-synchronized-关键字的了解) - [1.1.说一说自己对于 synchronized 关键字的了解](#11说一说自己对于-synchronized-关键字的了解)
- [1.2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗](#12-说说自己是怎么使用-synchronized-关键字在项目中用到了吗) - [1.2. 说说自己是怎么使用 synchronized 关键字](#12-说说自己是怎么使用-synchronized-关键字)
- [1.3. 讲一下 synchronized 关键字的底层原理](#13-讲一下-synchronized-关键字的底层原理) - [1.3. 构造方法可以使用 synchronized 关键字修饰么?](#13-构造方法可以使用-synchronized-关键字修饰么)
- [1.4. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗](#14-说说-jdk16-之后的synchronized-关键字底层做了哪些优化可以详细介绍一下这些优化吗) - [1.3. 讲一下 synchronized 关键字的底层原理](#13-讲一下-synchronized-关键字的底层原理)
- [1.5. 谈谈 synchronized和ReentrantLock 的区别](#15-谈谈-synchronized和reentrantlock-的区别) - [1.3.1. synchronized 同步语句块的情况](#131-synchronized-同步语句块的情况)
- [2. volatile关键字](#2-volatile关键字) - [1.3.2. `synchronized` 修饰方法的的情况](#132-synchronized-修饰方法的的情况)
- [2.1. 讲一下Java内存模型](#21-讲一下java内存模型) - [1.3.3.总结](#133总结)
- [2.2. 说说 synchronized 关键字和 volatile 关键字的区别](#22-说说-synchronized-关键字和-volatile-关键字的区别) - [1.4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗](#14-说说-jdk16-之后的-synchronized-关键字底层做了哪些优化可以详细介绍一下这些优化吗)
- [3. ThreadLocal](#3-threadlocal) - [1.5. 谈谈 synchronized 和 ReentrantLock 的区别](#15-谈谈-synchronized-和-reentrantlock-的区别)
- [3.1. ThreadLocal简介](#31-threadlocal简介) - [1.5.1. 两者都是可重入锁](#151-两者都是可重入锁)
- [3.2. ThreadLocal示例](#32-threadlocal示例) - [1.5.2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API](#152synchronized-依赖于-jvm-而-reentrantlock-依赖于-api)
- [3.3. ThreadLocal原理](#33-threadlocal原理) - [1.5.3.ReentrantLock 比 synchronized 增加了一些高级功能](#153reentrantlock-比-synchronized-增加了一些高级功能)
- [3.4. ThreadLocal 内存泄露问题](#34-threadlocal-内存泄露问题) - [2. volatile 关键字](#2-volatile-关键字)
- [4. 线程池](#4-线程池) - [2.1. CPU 缓存模型](#21-cpu-缓存模型)
- [4.1. 为什么要用线程池?](#41-为什么要用线程池) - [2.2. 讲一下 JMM(Java 内存模型)](#22-讲一下-jmmjava-内存模型)
- [4.2. 实现Runnable接口和Callable接口的区别](#42-实现runnable接口和callable接口的区别) - [2.3. 并发编程的三个重要特性](#23-并发编程的三个重要特性)
- [4.3. 执行execute()方法和submit()方法的区别是什么呢?](#43-执行execute方法和submit方法的区别是什么呢) - [2.4. 说说 synchronized 关键字和 volatile 关键字的区别](#24-说说-synchronized-关键字和-volatile-关键字的区别)
- [4.4. 如何创建线程池](#44-如何创建线程池) - [3. ThreadLocal](#3-threadlocal)
- [5. Atomic 原子类](#5-atomic-原子类) - [3.1. ThreadLocal 简介](#31-threadlocal-简介)
- [5.1. 介绍一下Atomic 原子类](#51-介绍一下atomic-原子类) - [3.2. ThreadLocal 示例](#32-threadlocal-示例)
- [5.2. JUC 包中的原子类是哪4类?](#52-juc-包中的原子类是哪4类) - [3.3. ThreadLocal 原理](#33-threadlocal-原理)
- [5.3. 讲讲 AtomicInteger 的使用](#53-讲讲-atomicinteger-的使用) - [3.4. ThreadLocal 内存泄露问题](#34-threadlocal-内存泄露问题)
- [5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理](#54-能不能给我简单介绍一下-atomicinteger-类的原理) - [4. 线程池](#4-线程池)
- [6. AQS](#6-aqs) - [4.1. 为什么要用线程池?](#41-为什么要用线程池)
- [6.1. AQS 介绍](#61-aqs-介绍) - [4.2. 实现 Runnable 接口和 Callable 接口的区别](#42-实现-runnable-接口和-callable-接口的区别)
- [6.2. AQS 原理分析](#62-aqs-原理分析) - [4.3. 执行 execute()方法和 submit()方法的区别是什么呢?](#43-执行-execute方法和-submit方法的区别是什么呢)
- [6.2.1. AQS 原理概览](#621-aqs-原理概览) - [4.4. 如何创建线程池](#44-如何创建线程池)
- [6.2.2. AQS 对资源的共享方式](#622-aqs-对资源的共享方式) - [4.5 ThreadPoolExecutor 类分析](#45-threadpoolexecutor-类分析)
- [6.2.3. AQS底层使用了模板方法模式](#623-aqs底层使用了模板方法模式) - [4.5.1 `ThreadPoolExecutor`构造函数重要参数分析](#451-threadpoolexecutor构造函数重要参数分析)
- [6.3. AQS 组件总结](#63-aqs-组件总结) - [4.5.2 `ThreadPoolExecutor` 饱和策略](#452-threadpoolexecutor-饱和策略)
- [7 Reference](#7-reference) - [4.6 一个简单的线程池 Demo](#46-一个简单的线程池-demo)
- [4.7 线程池原理分析](#47-线程池原理分析)
- [5. Atomic 原子类](#5-atomic-原子类)
- [5.1. 介绍一下 Atomic 原子类](#51-介绍一下-atomic-原子类)
- [5.2. JUC 包中的原子类是哪 4 类?](#52-juc-包中的原子类是哪-4-类)
- [5.3. 讲讲 AtomicInteger 的使用](#53-讲讲-atomicinteger-的使用)
- [5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理](#54-能不能给我简单介绍一下-atomicinteger-类的原理)
- [6. AQS](#6-aqs)
- [6.1. AQS 介绍](#61-aqs-介绍)
- [6.2. AQS 原理分析](#62-aqs-原理分析)
- [6.2.1. AQS 原理概览](#621-aqs-原理概览)
- [6.2.2. AQS 对资源的共享方式](#622-aqs-对资源的共享方式)
- [6.2.3. AQS 底层使用了模板方法模式](#623-aqs-底层使用了模板方法模式)
- [6.3. AQS 组件总结](#63-aqs-组件总结)
- [6.4. 用过 CountDownLatch 么?什么场景下用的?](#64-用过-countdownlatch-么什么场景下用的)
- [7 Reference](#7-reference)
- [公众号](#公众号)
<!-- /code_chunk_output -->
<!-- /TOC -->
# Java 并发进阶常见面试题总结 # Java 并发进阶常见面试题总结
## 1. synchronized 关键字 ## 1.synchronized 关键字
### 1.1. 说一说自己对于 synchronized 关键字的了解 ![](images/interview-questions/synchronized关键字.png)
synchronized关键字解决的是多个线程之间访问资源的同步性synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。 ### 1.1.说一说自己对于 synchronized 关键字的了解
另外,在 Java 早期版本中synchronized属于重量级锁效率低下因为监视器锁monitor是依赖于底层的操作系统的 Mutex Lock 来实现的Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。 **`synchronized` 关键字解决的是多个线程之间访问资源的同步性,`synchronized`关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。**
另外,在 Java 早期版本中,`synchronized` 属于 **重量级锁**,效率低下。
### 1.2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗 **为什么呢?**
**synchronized关键字最主要的三种使用方式** 因为监视器锁monitor是依赖于底层的操作系统的 `Mutex Lock` 来实现的Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
- **修饰实例方法:** 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁 庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
- **修饰静态方法:** 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源不管new了多少个对象只有一份。所以如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**。
- **修饰代码块:** 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
**总结:** synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中字符串常量池具有缓存功能 所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字。
下面我以一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。 ### 1.2. 说说自己是怎么使用 synchronized 关键字
**synchronized 关键字最主要的三种使用方式:**
**1.修饰实例方法:** 作用于当前对象实例加锁,进入同步代码前要获得 **当前对象实例的锁**
```java
synchronized void method() {
//业务代码
}
```
**2.修饰静态方法:** 也就是给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 **当前 class 的锁**。因为静态成员不属于任何一个实例对象,是类成员( _static 表明这是该类的一个静态资源,不管 new 了多少个对象只有一份_)。所以,如果一个线程 A 调用一个实例对象的非静态 `synchronized` 方法,而线程 B 需要调用这个实例对象所属类的静态 `synchronized` 方法,是允许的,不会发生互斥现象,**因为访问静态 `synchronized` 方法占用的锁是当前类的锁,而访问非静态 `synchronized` 方法占用的锁是当前实例对象锁**。
```java
synchronized void staic method() {
//业务代码
}
```
**3.修饰代码块** :指定加锁对象,对给定对象/类加锁。`synchronized(this|object)` 表示进入同步代码库前要获得**给定对象的锁**。`synchronized(类.class)` 表示进入同步代码前要获得 **当前 class 的锁**
```java
synchronized(this) {
//业务代码
}
```
**总结:**
- `synchronized` 关键字加到 `static` 静态方法和 `synchronized(class)` 代码块上都是是给 Class 类上锁。
- `synchronized` 关键字加到实例方法上是给对象实例上锁。
- 尽量不要使用 `synchronized(String a)` 因为 JVM 中,字符串常量池具有缓存功能!
下面我以一个常见的面试题为例讲解一下 `synchronized` 关键字的具体使用。
面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!” 面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!”
@ -87,23 +140,30 @@ public class Singleton {
} }
} }
``` ```
另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行: 另外,需要注意 `uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要。
1. 为 uniqueInstance 分配内存空间 `uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要的, `uniqueInstance = new Singleton();` 这段代码其实是分为三步执行:
2. 初始化 uniqueInstance
3. 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如线程 T1 执行了 1 和 3此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance但此时 uniqueInstance 还未被初始化。 1. 为 `uniqueInstance` 分配内存空间
2. 初始化 `uniqueInstance`
3. 将 `uniqueInstance` 指向分配的内存地址
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如线程 T1 执行了 1 和 3此时 T2 调用 `getUniqueInstance`() 后发现 `uniqueInstance` 不为空,因此返回 `uniqueInstance`,但此时 `uniqueInstance` 还未被初始化。
使用 `volatile` 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
### 1.3. 构造方法可以使用 synchronized 关键字修饰么?
先说结论:**构造方法不能使用 synchronized 关键字修饰。**
构造方法本身就属于线程安全的,不存在同步的构造方法一说。
### 1.3. 讲一下 synchronized 关键字的底层原理 ### 1.3. 讲一下 synchronized 关键字的底层原理
**synchronized 关键字底层原理属于 JVM 层面。** **synchronized 关键字底层原理属于 JVM 层面。**
**① synchronized 同步语句块的情况** #### 1.3.1. synchronized 同步语句块的情况
```java ```java
public class SynchronizedDemo { public class SynchronizedDemo {
@ -116,15 +176,25 @@ public class SynchronizedDemo {
``` ```
通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class` 通过 JDK 自带的 `javap` 命令查看 `SynchronizedDemo` 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`
![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理.png) ![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理.png)
从上面我们可以看出: 从上面我们可以看出:
**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置monitorexit 指令则指明同步代码块的结束位置。** 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中synchronized 锁便是通过这种方式获取锁的也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后将锁计数器设为0表明锁被释放。如果获取对象锁失败那当前线程就要阻塞等待直到锁被另外一个线程释放为止。 **`synchronized` 同步语句块的实现使用的是 `monitorenter``monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。**
**② synchronized 修饰方法的的情况** 当执行 `monitorenter` 指令时,线程试图获取锁也就是获取 **对象监视器 `monitor`** 的持有权。
> 在 Java 虚拟机(HotSpot)中Monitor 是基于 C++实现的,由[ObjectMonitor](https://github.com/openjdk-mirror/jdk7u-hotspot/blob/50bdefc3afe944ca74c3093e7448d6b889cd20d1/src/share/vm/runtime/objectMonitor.cpp)实现的。每个对象中都内置了一个 `ObjectMonitor`对象。
>
> 另外,**`wait/notify`等方法也依赖于`monitor`对象,这就是为什么只有在同步的块或者方法中才能调用`wait/notify`等方法,否则会抛出`java.lang.IllegalMonitorStateException`的异常的原因。**
在执行`monitorenter`时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
在执行 `monitorexit` 指令后,将锁计数器设为 0表明锁被释放。如果获取对象锁失败那当前线程就要阻塞等待直到锁被另外一个线程释放为止。
#### 1.3.2. `synchronized` 修饰方法的的情况
```java ```java
public class SynchronizedDemo2 { public class SynchronizedDemo2 {
@ -137,83 +207,112 @@ public class SynchronizedDemo2 {
![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理2.png) ![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理2.png)
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 `synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
#### 1.3.3.总结
### 1.4. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗 `synchronized` 同步语句块的实现使用的是 `monitorenter``monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。
`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。
**不过两者的本质都是对对象监视器 monitor 的获取。**
### 1.4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。 JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
关于这几种优化的详细信息可以查看笔主的这篇文章:<https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/Multithread/synchronized.md> 关于这几种优化的详细信息可以查看下面这几篇文章:
### 1.5. 谈谈 synchronized和ReentrantLock 的区别 - [Java 性能 -- synchronized 锁升级优化](https://blog.csdn.net/qq_34337272/article/details/108498442)
- [Java6 及以上版本对 synchronized 的优化](https://www.cnblogs.com/wuqinglong/p/9945618.html)
### 1.5. 谈谈 synchronized 和 ReentrantLock 的区别
**① 两者都是可重入锁** #### 1.5.1. 两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁此时这个对象锁还没有释放当其再次想要获取这个对象的锁的时候还是可以获取的如果不可锁重入的话就会造成死锁。同一个线程每次获取锁锁的计数器都自增1所以要等到锁的计数器下降为0时才能释放锁。 **“可重入锁”** 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1所以要等到锁的计数器下降为 0 时才能释放锁。
**② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API** #### 1.5.2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化但是这些优化都是在虚拟机层面实现的并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 `synchronized` 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 `synchronized` 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。`ReentrantLock` 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
**③ ReentrantLock 比 synchronized 增加了一些高级功能** #### 1.5.3.ReentrantLock 比 synchronized 增加了一些高级功能
相比synchronizedReentrantLock增加了一些高级功能。主要来说主要有三点**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)** 相比`synchronized``ReentrantLock`增加了一些高级功能。主要来说主要有三点:
- **ReentrantLock提供了一种能够中断等待锁的线程的机制**通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 - **等待可中断** : `ReentrantLock`提供了一种能够中断等待锁的线程的机制,通过 `lock.lockInterruptibly()` 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
- **ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReentrantLock默认情况是非公平的可以通过 ReentrantLock类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。 - **可实现公平锁** : `ReentrantLock`可以指定是公平锁还是非公平锁。而`synchronized`只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。`ReentrantLock`默认情况是非公平的,可以通过 `ReentrantLock`类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。
- synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制ReentrantLock类当然也可以实现但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的它具有很好的灵活性比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例即对象监视器**线程对象可以注册在指定的Condition中从而可以有选择性的进行线程通知在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的用ReentrantLock类结合Condition实例可以实现“选择性通知”** 这个功能非常重要而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程 - **可实现选择性通知(锁可以绑定多个条件)**: `synchronized`关键字与`wait()``notify()`/`notifyAll()`方法相结合可以实现等待/通知机制。`ReentrantLock`类当然也可以实现,但是需要借助于`Condition`接口与`newCondition()`方法
如果你想使用上述功能那么选择ReentrantLock是一个不错的选择 > `Condition`是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个`Lock`对象中可以创建多个`Condition`实例(即对象监视器),**线程对象可以注册在指定的`Condition`中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用`notify()/notifyAll()`方法进行通知时,被通知的线程是由 JVM 选择的,用`ReentrantLock`类结合`Condition`实例可以实现“选择性通知”** ,这个功能非常重要,而且是 Condition 接口默认提供的。而`synchronized`关键字就相当于整个 Lock 对象中只有一个`Condition`实例,所有的线程都注册在它一个身上。如果执行`notifyAll()`方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而`Condition`实例的`signalAll()`方法 只会唤醒注册在该`Condition`实例中的所有等待线程
**性能已不是选择标准** **如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。性能已不是选择标准**
## 2. volatile关键字 ## 2. volatile 关键字
### 2.1. 讲一下Java内存模型 我们先要从 **CPU 缓存模型** 说起!
### 2.1. CPU 缓存模型
在 JDK1.2 之前Java的内存模型实现总是从**主存**(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存**本地内存**(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成**数据的不一致**。 **为什么要弄一个 CPU 高速缓存呢?**
![数据不一致](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/数据不一致.png) 类比我们开发网站后台系统使用的缓存(比如 Redis是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。 **CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。**
要解决这个问题,就需要把变量声明为**volatile**,这就指示 JVM这个变量是不稳定的每次使用它都到主存中进行读取 我们甚至可以把 **内存可以看作外存的高速缓存**,程序运行的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样提高了处理速度
说白了, **volatile** 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。 总结:**CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。**
![volatile关键字的可见性](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/volatile关键字的可见性.png) 为了更好地理解,我画了一个简单的 CPU Cache 示意图如下(实际上,现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache:
### 2.2 并发编程的三个重要特性 ![CPU Cache](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/303a300f-70dd-4ee1-9974-3f33affc6574.png)
1. **原子性** : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。`synchronized ` 可以保证代码片段的原子性。 **CPU Cache 的工作方式:**
2. **可见性** :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。`volatile` 关键字可以保证共享变量的可见性。
先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 **内存缓存不一致性的问题** !比如我执行一个 i++操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1两个线程做了 1++运算完之后再写回 Main Memory 之后 i=2而正确结果应该是 i=3。
**CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议或者其他手段来解决。**
### 2.2. 讲一下 JMM(Java 内存模型)
在 JDK1.2 之前Java 的内存模型实现总是从**主存**(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存**本地内存**(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成**数据的不一致**。
![JMM(Java内存模型)](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/0ac7e663-7db8-4b95-8d8e-7d2b179f67e8.png)
要解决这个问题,就需要把变量声明为**`volatile`**,这就指示 JVM这个变量是共享且不稳定的每次使用它都到主存中进行读取。
所以,**`volatile` 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。**
![volatile关键字的可见性](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-8/d49c5557-140b-4abf-adad-8aac3c9036cf.png)
### 2.3. 并发编程的三个重要特性
1. **原子性** : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。`synchronized` 可以保证代码片段的原子性。
2. **可见性** :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。`volatile` 关键字可以保证共享变量的可见性。
3. **有序性** 代码在执行的过程中的先后顺序Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。`volatile` 关键字可以禁止指令进行重排序优化。 3. **有序性** 代码在执行的过程中的先后顺序Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。`volatile` 关键字可以禁止指令进行重排序优化。
### 2.3. 说说 synchronized 关键字和 volatile 关键字的区别 ### 2.4. 说说 synchronized 关键字和 volatile 关键字的区别
`synchronized` 关键字和 `volatile` 关键字是两个互补的存在,而不是对立的存在: `synchronized` 关键字和 `volatile` 关键字是两个互补的存在,而不是对立的存在
- **volatile关键字**是线程同步的**轻量级实现**,所以**volatile性能肯定比synchronized关键字要好**。但是**volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块**。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,**实际开发中使用 synchronized 关键字的场景还是更多一些**。 - **volatile 关键字**是线程同步的**轻量级实现**,所以**volatile 性能肯定比 synchronized 关键字要好**。但是**volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块**。
- **多线程访问volatile关键字不会发生阻塞而synchronized关键字可能会发生阻塞** - **volatile 关键字能保证数据的可见性但不能保证数据的原子性。synchronized 关键字两者都能保证。**
- **volatile关键字能保证数据的可见性但不能保证数据的原子性。synchronized关键字两者都能保证。** - **volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。**
- **volatile关键字主要用于解决变量在多个线程之间的可见性而 synchronized关键字解决的是多个线程之间访问资源的同步性。**
## 3. ThreadLocal ## 3. ThreadLocal
### 3.1. ThreadLocal简介 ### 3.1. ThreadLocal 简介
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** 通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK 中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。**
**如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get``set` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。** **如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get``set` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。**
再举个简单的例子: 再举个简单的例子:
比如有两个人去宝屋收集宝物这两个共用一个袋子的话肯定会产生争执但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话那么ThreadLocal就是用来避免这两个线程竞争的。 比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。
### 3.2. ThreadLocal示例 ### 3.2. ThreadLocal 示例
相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。 相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。
@ -278,9 +377,9 @@ Thread Name= 8 formatter = yy-M-d ah:mm
Thread Name= 9 formatter = yy-M-d ah:mm Thread Name= 9 formatter = yy-M-d ah:mm
``` ```
从输出中可以看出Thread-0已经改变了formatter的值但仍然是thread-2默认格式化程序与初始化值相同其他线程也一样。 从输出中可以看出Thread-0 已经改变了 formatter 的值,但仍然是 thread-2 默认格式化程序与初始化值相同,其他线程也一样。
上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识它等于下面这段代码如果你写了下面这段代码的话IDEA会提示你转换为Java8的格式(IDEA真的不错)。因为ThreadLocal类在Java 8中扩展使用一个新的方法`withInitial()`将Supplier功能接口作为参数。 上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识它等于下面这段代码如果你写了下面这段代码的话IDEA 会提示你转换为 Java8 的格式(IDEA 真的不错!)。因为 ThreadLocal 类在 Java 8 中扩展,使用一个新的方法`withInitial()`,将 Supplier 功能接口作为参数。
```java ```java
private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){ private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
@ -292,7 +391,7 @@ Thread Name= 9 formatter = yy-M-d ah:mm
}; };
``` ```
### 3.3. ThreadLocal原理 ### 3.3. ThreadLocal 原理
`Thread`类源代码入手。 `Thread`类源代码入手。
@ -308,7 +407,7 @@ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
} }
``` ```
从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是null只有当前线程调用 `ThreadLocal` 类的 `set``get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()``set() `方法。 从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是 null只有当前线程调用 `ThreadLocal` 类的 `set``get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()``set()`方法。
`ThreadLocal`类的`set()`方法 `ThreadLocal`类的`set()`方法
@ -328,7 +427,7 @@ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,`ThreadLocal` 可以理解为只是`ThreadLocalMap`的封装,传递了变量值。** `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。 通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,`ThreadLocal` 可以理解为只是`ThreadLocalMap`的封装,传递了变量值。** `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。
**每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为key Object 对象为 value的键值对。** **每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal` key Object 对象为 value 的键值对。**
```java ```java
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
@ -346,7 +445,7 @@ ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
### 3.4. ThreadLocal 内存泄露问题 ### 3.4. ThreadLocal 内存泄露问题
`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下在垃圾回收的时候key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现key为null的Entry。假如我们不做任何措施的话value 永远无法被GC 回收这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况在调用 `set()``get()``remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法 `ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下在垃圾回收的时候key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现 key null Entry。假如我们不做任何措施的话value 永远无法被 GC 回收这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 `set()``get()``remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法
```java ```java
static class Entry extends WeakReference<ThreadLocal<?>> { static class Entry extends WeakReference<ThreadLocal<?>> {
@ -364,7 +463,7 @@ ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 > 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
> >
> 弱引用可以和一个引用队列ReferenceQueue联合使用如果弱引用所引用的对象被垃圾回收Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。 > 弱引用可以和一个引用队列ReferenceQueue联合使用如果弱引用所引用的对象被垃圾回收Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
## 4. 线程池 ## 4. 线程池
@ -380,9 +479,9 @@ ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
- **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。 - **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 - **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
### 4.2. 实现Runnable接口和Callable接口的区别 ### 4.2. 实现 Runnable 接口和 Callable 接口的区别
`Runnable`自Java 1.0以来一直存在,但`Callable`仅在Java 1.5中引入,目的就是为了来处理`Runnable`不支持的用例。**`Runnable` 接口**不会返回结果或抛出检查异常,但是**`Callable` 接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **`Runnable` 接口**,这样代码看起来会更加简洁。 `Runnable` Java 1.0 以来一直存在,但`Callable`仅在 Java 1.5 中引入,目的就是为了来处理`Runnable`不支持的用例。**`Runnable` 接口**不会返回结果或抛出检查异常,但是**`Callable` 接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **`Runnable` 接口**,这样代码看起来会更加简洁。
工具类 `Executors` 可以实现 `Runnable` 对象和 `Callable` 对象之间的相互转换。(`Executors.callableRunnable task`)或 `Executors.callableRunnable taskObject resule`)。 工具类 `Executors` 可以实现 `Runnable` 对象和 `Callable` 对象之间的相互转换。(`Executors.callableRunnable task`)或 `Executors.callableRunnable taskObject resule`)。
@ -412,7 +511,7 @@ public interface Callable<V> {
} }
``` ```
### 4.3. 执行execute()方法和submit()方法的区别是什么呢? ### 4.3. 执行 execute()方法和 submit()方法的区别是什么呢?
1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;** 1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;**
2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future``get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `getlong timeoutTimeUnit unit`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。 2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future``get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `getlong timeoutTimeUnit unit`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
@ -446,23 +545,23 @@ public interface Callable<V> {
### 4.4. 如何创建线程池 ### 4.4. 如何创建线程池
《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
> Executors 返回线程池对象的弊端如下: > Executors 返回线程池对象的弊端如下:
> >
> - **FixedThreadPool 和 SingleThreadExecutor** 允许请求的队列长度为 Integer.MAX_VALUE 可能堆积大量的请求从而导致OOM。 > - **FixedThreadPool 和 SingleThreadExecutor** 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
> - **CachedThreadPool 和 ScheduledThreadPool** 允许创建的线程数量为 Integer.MAX_VALUE 可能会创建大量线程从而导致OOM。 > - **CachedThreadPool 和 ScheduledThreadPool** 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
**方式一:通过构造方法实现** **方式一:通过构造方法实现**
![ThreadPoolExecutor构造方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ThreadPoolExecutor构造方法.png) ![ThreadPoolExecutor构造方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ThreadPoolExecutor构造方法.png)
**方式二通过Executor 框架的工具类Executors来实现** **方式二:通过 Executor 框架的工具类 Executors 来实现**
我们可以创建三种类型的ThreadPoolExecutor 我们可以创建三种类型的 ThreadPoolExecutor
- **FixedThreadPool** 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 - **FixedThreadPool** 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
- **SingleThreadExecutor** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 - **SingleThreadExecutor** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
- **CachedThreadPool** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 - **CachedThreadPool** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
对应Executors工具类中的方法如图所示 对应 Executors 工具类中的方法如图所示:
![Executor框架的工具类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/Executor框架的工具类.png) ![Executor框架的工具类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/Executor框架的工具类.png)
### 4.5 ThreadPoolExecutor 类分析 ### 4.5 ThreadPoolExecutor 类分析
@ -526,7 +625,7 @@ public interface Callable<V> {
举个例子: Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了) 举个例子: Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了)
### 4.6 一个简单的线程池Demo:`Runnable`+`ThreadPoolExecutor` ### 4.6 一个简单的线程池 Demo
为了让大家更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。 为了让大家更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。
@ -713,18 +812,17 @@ pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019
## 5. Atomic 原子类 ## 5. Atomic 原子类
### 5.1. 介绍一下Atomic 原子类 ### 5.1. 介绍一下 Atomic 原子类
Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。 Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
所以,所谓原子类说简单点就是具有原子/原子操作特征的类。 所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。 并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。
![JUC原子类概览](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JUC原子类概览.png) ![JUC原子类概览](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JUC原子类概览.png)
### 5.2. JUC 包中的原子类是哪4类? ### 5.2. JUC 包中的原子类是哪 4 类?
**基本类型** **基本类型**
@ -738,7 +836,6 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是
使用原子的方式更新数组里的某个元素 使用原子的方式更新数组里的某个元素
- AtomicIntegerArray整形数组原子类 - AtomicIntegerArray整形数组原子类
- AtomicLongArray长整形数组原子类 - AtomicLongArray长整形数组原子类
- AtomicReferenceArray引用类型数组原子类 - AtomicReferenceArray引用类型数组原子类
@ -755,10 +852,9 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是
- AtomicLongFieldUpdater原子更新长整形字段的更新器 - AtomicLongFieldUpdater原子更新长整形字段的更新器
- AtomicStampedReference原子更新带有版本号的引用类型。该类将整数值与引用关联起来可用于解决原子的更新数据和数据的版本号可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。 - AtomicStampedReference原子更新带有版本号的引用类型。该类将整数值与引用关联起来可用于解决原子的更新数据和数据的版本号可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
### 5.3. 讲讲 AtomicInteger 的使用 ### 5.3. 讲讲 AtomicInteger 的使用
**AtomicInteger 类常用方法** **AtomicInteger 类常用方法**
```java ```java
public final int get() //获取当前的值 public final int get() //获取当前的值
@ -770,9 +866,10 @@ boolean compareAndSet(int expect, int update) //如果输入的数值等于预
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
``` ```
**AtomicInteger 类的使用示例** **AtomicInteger 类的使用示例**
使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。 使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。
```java ```java
class AtomicIntegerTest { class AtomicIntegerTest {
private AtomicInteger count = new AtomicInteger(); private AtomicInteger count = new AtomicInteger();
@ -811,7 +908,7 @@ AtomicInteger 类的部分源码:
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。 AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS的原理是拿期望的值和原本的一个值作比较如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量在内存中可见因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。 CAS 的原理是拿期望的值和原本的一个值作比较如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
关于 Atomic 原子类这部分更多内容可以查看我的这篇文章:并发编程面试必备:[JUC 中的 Atomic 原子类总结](https://mp.weixin.qq.com/s/joa-yOiTrYF67bElj8xqvg) 关于 Atomic 原子类这部分更多内容可以查看我的这篇文章:并发编程面试必备:[JUC 中的 Atomic 原子类总结](https://mp.weixin.qq.com/s/joa-yOiTrYF67bElj8xqvg)
@ -819,38 +916,37 @@ CAS的原理是拿期望的值和原本的一个值作比较如果相同则
### 6.1. AQS 介绍 ### 6.1. AQS 介绍
AQS的全称为AbstractQueuedSynchronizer这个类在java.util.concurrent.locks包下面。 AQS 的全称为AbstractQueuedSynchronizer这个类在 java.util.concurrent.locks 包下面。
![AQS类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS类.png) ![AQS类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS类.png)
AQS是一个用来构建锁和同步器的框架使用AQS能简单且高效地构造出应用广泛的大量的同步器比如我们提到的ReentrantLockSemaphore其他的诸如ReentrantReadWriteLockSynchronousQueueFutureTask等等皆是基于AQS的。当然我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。 AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLockSemaphore其他的诸如 ReentrantReadWriteLockSynchronousQueueFutureTask 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。
### 6.2. AQS 原理分析 ### 6.2. AQS 原理分析
AQS 原理这部分参考了部分博客在5.2节末尾放了链接。 AQS 原理这部分参考了部分博客,在 5.2 节末尾放了链接。
> 在面试中被问到并发知识的时候大多都会被问到“请你说一下自己对于AQS原理的理解”。下面给大家一个示例供大家参加面试不是背题大家一定要加入自己的思想即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。 > 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于 AQS 原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。
下面大部分内容其实在AQS类注释上已经给出了不过是英语看着比较吃力一点感兴趣的话可以看看源码。 下面大部分内容其实在 AQS 类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。
#### 6.2.1. AQS 原理概览 #### 6.2.1. AQS 原理概览
**AQS核心思想是如果被请求的共享资源空闲则将当前请求资源的线程设置为有效的工作线程并且将共享资源设置为锁定状态。如果被请求的共享资源被占用那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制这个机制AQS是用CLH队列锁实现的即将暂时获取不到锁的线程加入到队列中。** **AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。**
> CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列虚拟的双向队列即不存在队列实例仅存在结点之间的关联关系。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点Node来实现锁的分配。 > CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列虚拟的双向队列即不存在队列实例仅存在结点之间的关联关系。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点Node来实现锁的分配。
看个AQS(AbstractQueuedSynchronizer)原理图:
看个 AQS(AbstractQueuedSynchronizer)原理图:
![AQS原理图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS原理图.png) ![AQS原理图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS原理图.png)
AQS使用一个int成员变量来表示同步状态通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。 AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
```java ```java
private volatile int state;//共享变量使用volatile修饰保证线程可见性 private volatile int state;//共享变量使用volatile修饰保证线程可见性
``` ```
状态信息通过protected类型的getStatesetStatecompareAndSetState进行操作 状态信息通过 protected 类型的 getStatesetStatecompareAndSetState 进行操作
```java ```java
@ -870,27 +966,27 @@ protected final boolean compareAndSetState(int expect, int update) {
#### 6.2.2. AQS 对资源的共享方式 #### 6.2.2. AQS 对资源的共享方式
**AQS定义两种资源共享方式** **AQS 定义两种资源共享方式**
- **Exclusive**独占只有一个线程能执行如ReentrantLock。又可分为公平锁和非公平锁 - **Exclusive**(独占):只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁 - 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的 - 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
- **Share**共享多个线程可同时执行如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。 - **Share**(共享):多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。 ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等AQS已经在顶层实现好了。 不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等AQS 已经在顶层实现好了。
#### 6.2.3. AQS底层使用了模板方法模式 #### 6.2.3. AQS 底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用): 同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。这些重写方法很简单无非是对于共享资源state的获取和释放 1. 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)
2. 将AQS组合在自定义同步组件的实现中并调用其模板方法而这些模板方法会调用使用者重写的方法。 2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。 这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
**AQS使用了模板方法模式自定义同步器时需要重写下面几个AQS提供的模板方法** **AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:**
```java ```java
isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
@ -901,13 +997,13 @@ tryReleaseShared(int)//共享方式。尝试释放资源成功则返回true
``` ```
默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。 默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的并且通常应该简短而不是阻塞。AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。
以ReentrantLock为例state初始化为0表示未锁定状态。A线程lock()时会调用tryAcquire()独占该锁并将state+1。此后其他线程再tryAcquire()时就会失败直到A线程unlock()到state=0即释放锁为止其它线程才有机会获取该锁。当然释放锁之前A线程自己是可以重复获取此锁的state会累加这就是可重入的概念。但要注意获取多少次就要释放多么次这样才能保证state是能回到零态的。 ReentrantLock 为例state 初始化为 0表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0即释放锁为止其它线程才有机会获取该锁。当然释放锁之前A 线程自己是可以重复获取此锁的state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。
再以CountDownLatch以例任务分为N个子线程去执行state也初始化为N注意N要与线程个数一致。这N个子线程是并行执行的每个子线程执行完后countDown()一次state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0)会unpark()主调用线程然后主调用线程就会从await()函数返回,继续后余动作。 再以 CountDownLatch 以例,任务分为 N 个子线程去执行state 也初始化为 N注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次state CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease``tryAcquireShared-tryReleaseShared`中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式`ReentrantReadWriteLock` 一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease``tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`
推荐两篇 AQS 原理和相关源码分析的文章: 推荐两篇 AQS 原理和相关源码分析的文章:
@ -917,14 +1013,97 @@ tryReleaseShared(int)//共享方式。尝试释放资源成功则返回true
### 6.3. AQS 组件总结 ### 6.3. AQS 组件总结
- **Semaphore(信号量)-允许多个线程同时访问:** synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源Semaphore(信号量)可以指定多个线程同时访问某个资源。 - **Semaphore(信号量)-允许多个线程同时访问:** synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源Semaphore(信号量)可以指定多个线程同时访问某个资源。
- **CountDownLatch (倒计时器):** CountDownLatch是一个同步工具类用来协调多个线程之间的同步。这个工具通常用来控制线程等待它可以让某一个线程等待直到倒计时结束再开始执行。 - **CountDownLatch (倒计时器):** CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
- **CyclicBarrier(循环栅栏)** CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用Cyclic的屏障Barrier。它要做的事情是让一组线程到达一个屏障也可以叫同步点时被阻塞直到最后一个线程到达屏障时屏障才会开门所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties)其参数表示屏障拦截的线程数量每个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。 - **CyclicBarrier(循环栅栏)** CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用Cyclic的屏障Barrier。它要做的事情是让一组线程到达一个屏障也可以叫同步点时被阻塞直到最后一个线程到达屏障时屏障才会开门所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
### 6.4. 用过 CountDownLatch 么?什么场景下用的?
`CountDownLatch` 的作用就是 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。之前在项目中,有一个使用多线程读取多个文件处理的场景,我用到了 `CountDownLatch` 。具体场景是下面这样的:
我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。
为此我们定义了一个线程池和 count 为 6 的`CountDownLatch`对象 。使用线程池处理读取任务,每一个线程处理完之后就将 count-1调用`CountDownLatch`对象的 `await()`方法,直到所有文件读取完之后,才会接着执行后面的逻辑。
伪代码是下面这样的:
```java
public class CountDownLatchExample1 {
// 处理文件的数量
private static final int threadCount = 6;
public static void main(String[] args) throws InterruptedException {
// 创建一个具有固定线程数量的线程池对象(推荐使用构造方法创建)
ExecutorService threadPool = Executors.newFixedThreadPool(10);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {
try {
//处理文件的业务操作
......
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//表示一个文件已经被完成
countDownLatch.countDown();
}
});
}
countDownLatch.await();
threadPool.shutdown();
System.out.println("finish");
}
}
```
**有没有可以改进的地方呢?**
可以使用 `CompletableFuture` 类来改进Java8 的 `CompletableFuture` 提供了很多对多线程友好的方法,使用它可以很方便地为我们编写多线程程序,什么异步、串行、并行或者等待所有线程执行完任务什么的都非常方便。
```java
CompletableFuture<Void> task1 =
CompletableFuture.supplyAsync(()->{
//自定义业务操作
});
......
CompletableFuture<Void> task6 =
CompletableFuture.supplyAsync(()->{
//自定义业务操作
});
......
CompletableFuture<Void> headerFuture=CompletableFuture.allOf(task1,.....,task6);
try {
headerFuture.join();
} catch (Exception ex) {
......
}
System.out.println("all done. ");
```
上面的代码还可以接续优化,当任务过多的时候,把每一个 task 都列出来不太现实,可以考虑通过循环来添加任务。
```java
//文件夹位置
List<String> filePaths = Arrays.asList(...)
// 异步处理所有文件
List<CompletableFuture<String>> fileFutures = filePaths.stream()
.map(filePath -> doSomeThing(filePath))
.collect(Collectors.toList());
// 将他们合并起来
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
fileFutures.toArray(new CompletableFuture[fileFutures.size()])
);
```
## 7 Reference ## 7 Reference
- 《深入理解 Java 虚拟机》 - 《深入理解 Java 虚拟机》
- 《实战 Java 高并发程序设计》 - 《实战 Java 高并发程序设计》
- 《Java并发编程的艺术》 - 《Java 并发编程的艺术》
- http://www.cnblogs.com/waterystone/p/4920797.html - http://www.cnblogs.com/waterystone/p/4920797.html
- https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html - https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
- <https://www.journaldev.com/1076/java-threadlocal-example> - <https://www.journaldev.com/1076/java-threadlocal-example>
@ -933,8 +1112,8 @@ tryReleaseShared(int)//共享方式。尝试释放资源成功则返回true
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
**《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取! **《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取!
**Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 **Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) ![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png)

View File

@ -84,7 +84,7 @@ public class MultiThread {
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈** 从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**
**总结:** 线程 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反 **总结:** **线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。**
下面是该知识点的扩展内容! 下面是该知识点的扩展内容!

View File

@ -0,0 +1,422 @@
## synchronized / Lock
1. JDK 1.5之前
Java通过
synchronized
关键字来实现
功能
- synchronized是JVM实现的**内置锁**锁的获取和释放都是由JVM**隐式**实现的
2. JDK 1.5
,并发包中新增了
Lock接口
来实现锁功能
- 提供了与synchronized类似的同步功能但需要**显式**获取和释放锁
3. Lock同步锁是基于
Java
实现的而synchronized是基于底层操作系统的
Mutex Lock
实现的
- 每次获取和释放锁都会带来**用户态和内核态的切换**,从而增加系统的**性能开销**
- 在锁竞争激烈的情况下synchronized同步锁的性能很糟糕
- 在**JDK 1.5**,在**单线程重复申请锁**的情况下synchronized锁性能要比Lock的性能**差很多**
4. **JDK 1.6**Java对synchronized同步锁做了**充分的优化**甚至在某些场景下它的性能已经超越了Lock同步锁
## 实现原理
复制
```
public class SyncTest {
public synchronized void method1() {
}
public void method2() {
Object o = new Object();
synchronized (o) {
}
}
}
```
复制
```
$ javac -encoding UTF-8 SyncTest.java
$ javap -v SyncTest
```
### 修饰方法
复制
```
public synchronized void method1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
```
1. JVM使用**ACC_SYNCHRONIZED**访问标识来区分一个方法是否为**同步方法**
2. 在方法调用时,会检查方法是否被设置了
ACC_SYNCHRONIZED
访问标识
- 如果是,执行线程会将先尝试**持有Monitor对象**,再执行方法,方法执行完成后,最后**释放Monitor对象**
### 修饰代码块
复制
```
public void method2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: aload_1
9: dup
10: astore_2
11: monitorenter
12: aload_2
13: monitorexit
14: goto 22
17: astore_3
18: aload_2
19: monitorexit
20: aload_3
21: athrow
22: return
```
1. synchronized修饰同步代码块时由**monitorenter**和**monitorexit**指令来实现同步
2. 进入**monitorenter**指令后,线程将**持有**该**Monitor对象**,进入**monitorexit**指令,线程将**释放**该**Monitor对象**
### 管程模型
1. JVM中的**同步**是基于进入和退出**管程****Monitor**)对象实现的
2. **每个Java对象实例都会有一个Monitor**Monitor可以和Java对象实例一起被创建和销毁
3. Monitor是由**ObjectMonitor**实现的,对应[ObjectMonitor.hpp](https://github.com/JetBrains/jdk8u_hotspot/blob/master/src/share/vm/runtime/objectMonitor.hpp)
4. 当多个线程同时访问一段同步代码时,会先被放在**EntryList**中
5. 当线程获取到Java对象的Monitor时Monitor是依靠
底层操作系统
Mutex Lock
来实现
互斥
的)
- 线程申请Mutex成功则持有该Mutex其它线程将无法获取到该Mutex
6. 进入
WaitSet
- 竞争锁**失败**的线程会进入**WaitSet**
- 竞争锁**成功**的线程如果调用**wait**方法,就会**释放当前持有的Mutex**,并且该线程会进入**WaitSet**
- 进入**WaitSet**的进程会等待下一次唤醒然后进入EntryList**重新排队**
7. 如果当前线程顺利执行完方法也会释放Mutex
8. Monitor依赖于**底层操作系统**的实现,存在**用户态**和**内核态之间**的**切换**,所以增加了**性能开销**
[![img](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-monitor.png)](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-monitor.png)
复制
```
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; // 持有该Monitor的线程
_WaitSet = NULL; // 处于wait状态的线程会被加入 _WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 多个线程访问同步块或同步方法,会首先被加入 _EntryList
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
```
## 锁升级优化
1. 为了提升性能,在**JDK 1.6**引入**偏向锁、轻量级锁、重量级锁**,用来**减少锁竞争带来的上下文切换**
2. 借助JDK 1.6新增的**Java对象头**,实现了**锁升级**功能
### Java对象头
1. 在**JDK 1.6**的JVM中对象实例在**堆内存**中被分为三部分:**对象头**、**实例数据**、**对齐填充**
2. 对象头的组成部分:**Mark Word**、**指向类的指针**、**数组长度**(可选,数组类型时才有)
3. Mark Word记录了**对象**和**锁**有关的信息在64位的JVM中Mark Word为**64 bit**
4. 锁升级功能主要依赖于Mark Word中**锁标志位**和**是否偏向锁标志位**
5. synchronized同步锁的升级优化路径***偏向锁** -> **轻量级锁** -> **重量级锁***
[![img](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-mark-word.jpg)](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-mark-word.jpg)
### 偏向锁
1. 偏向锁主要用来优化**同一线程多次申请同一个锁**的竞争,在某些情况下,大部分时间都是同一个线程竞争锁资源
2. 偏向锁的作用
- 当一个线程再次访问同一个同步代码时,该线程只需对该对象头的**Mark Word**中去判断是否有偏向锁指向它
- **无需再进入Monitor去竞争对象**(避免用户态和内核态的**切换**
3. 当对象被当做同步锁,并有一个线程抢到锁时
- 锁标志位还是**01**,是否偏向锁标志位设置为**1**,并且记录抢到锁的**线程ID**,进入***偏向锁状态***
4. 偏向锁
**不会主动释放锁**
- 当线程1再次获取锁时会比较**当前线程的ID**与**锁对象头部的线程ID**是否一致如果一致无需CAS来抢占锁
- 如果不一致,需要查看
锁对象头部记录的线程
是否存活
- 如果**没有存活**,那么锁对象被重置为**无锁**状态也是一种撤销然后重新偏向线程2
- 如果
存活
查找线程1的栈帧信息
- 如果线程1还是需要继续持有该锁对象那么暂停线程1**STW****撤销偏向锁****升级为轻量级锁**
- 如果线程1不再使用该锁对象那么将该锁对象设为**无锁**状态也是一种撤销然后重新偏向线程2
5. 一旦出现其他线程竞争锁资源时,偏向锁就会被
撤销
- 偏向锁的撤销**可能需要**等待**全局安全点**,暂停持有该锁的线程,同时检查该线程**是否还在执行该方法**
- 如果还没有执行完,说明此刻有**多个线程**竞争,升级为**轻量级锁**;如果已经执行完毕,唤醒其他线程继续**CAS**抢占
6. 在
高并发
场景下,当
大量线程
同时竞争同一个锁资源时,偏向锁会被
撤销
,发生
STW
,加大了
性能开销
- 默认配置
- `-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=4000`
- 默认开启偏向锁,并且**延迟生效**因为JVM刚启动时竞争非常激烈
- 关闭偏向锁
- `-XX:-UseBiasedLocking`
- 直接
设置为重量级锁
- `-XX:+UseHeavyMonitors`
红线流程部分:偏向锁的**获取**和**撤销**
[![img](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-1.png)](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-1.png)
### 轻量级锁
1. 当有另外一个线程竞争锁时,由于该锁处于**偏向锁**状态
2. 发现对象头Mark Word中的线程ID不是自己的线程ID该线程就会执行
CAS
操作获取锁
- 如果获取**成功**直接替换Mark Word中的线程ID为自己的线程ID该锁会***保持偏向锁状态***
- 如果获取**失败**,说明当前锁有一定的竞争,将偏向锁**升级**为轻量级锁
3. 线程获取轻量级锁时会有两步
- 先把**锁对象的Mark Word**复制一份到线程的**栈帧**中(**DisplacedMarkWord**),主要为了**保留现场**!!
- 然后使用**CAS**,把对象头中的内容替换为**线程栈帧中DisplacedMarkWord的地址**
4. 场景
- 在线程1复制对象头Mark Word的同时CAS之前线程2也准备获取锁也复制了对象头Mark Word
- 在线程2进行CAS时发现线程1已经把对象头换了线程2的CAS失败线程2会尝试使用**自旋锁**来等待线程1释放锁
5. 轻量级锁的适用场景:线程**交替执行**同步块,***绝大部分的锁在整个同步周期内都不存在长时间的竞争***
红线流程部分:升级轻量级锁
[![img](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-2.png)](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-2.png)
### 自旋锁 / 重量级锁
1. 轻量级锁
CAS
抢占失败,线程将会被挂起进入
阻塞
状态
- 如果正在持有锁的线程在**很短的时间**内释放锁资源,那么进入**阻塞**状态的线程被**唤醒**后又要**重新抢占**锁资源
2. JVM提供了**自旋锁**,可以通过**自旋**的方式**不断尝试获取锁**,从而***避免线程被挂起阻塞***
3. 从
JDK 1.7
开始,
自旋锁默认启用
,自旋次数
不建议设置过大
(意味着
长时间占用CPU
- `-XX:+UseSpinning -XX:PreBlockSpin=10`
4. 自旋锁重试之后如果依然抢锁失败,同步锁会升级至
重量级锁
,锁标志位为
10
- 在这个状态下,未抢到锁的线程都会**进入Monitor**,之后会被阻塞在**WaitSet**中
5. 在
锁竞争不激烈
锁占用时间非常短
的场景下,自旋锁可以提高系统性能
- 一旦锁竞争激烈或者锁占用的时间过长,自旋锁将会导致大量的线程一直处于**CAS重试状态****占用CPU资源**
6. 在
高并发
的场景下,可以通过
关闭自旋锁
来优化系统性能
- ```
-XX:-UseSpinning
```
- 关闭自旋锁优化
- ```
-XX:PreBlockSpin
```
- 默认的自旋次数,在**JDK 1.7**后,**由JVM控制**
[![img](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-3.png)](https://java-performance-1253868755.cos.ap-guangzhou.myqcloud.com/java-performance-synchronized-lock-upgrade-3.png)
## 小结
1. JVM在**JDK 1.6**中引入了**分级锁**机制来优化synchronized
2. 当一个线程获取锁时,首先对象锁成为一个
偏向锁
- 这是为了避免在**同一线程重复获取同一把锁**时,**用户态和内核态频繁切换**
3. 如果有多个线程竞争锁资源,锁将会升级为
轻量级锁
- 这适用于在**短时间**内持有锁,且分锁**交替切换**的场景
- 轻量级锁还结合了**自旋锁**来**避免线程用户态与内核态的频繁切换**
4. 如果锁竞争太激烈(自旋锁失败),同步锁会升级为重量级锁
5. 优化synchronized同步锁的关键
减少锁竞争
- 应该尽量使synchronized同步锁处于**轻量级锁**或**偏向锁**这样才能提高synchronized同步锁的性能
- 常用手段
- **减少锁粒度**:降低锁竞争
- **减少锁的持有时间**提高synchronized同步锁在自旋时获取锁资源的成功率**避免升级为重量级锁**
6. 在**锁竞争激烈**时,可以考虑**禁用偏向锁**和**禁用自旋锁**

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

View File

@ -1,171 +0,0 @@
![Synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/%E4%BA%8C%20%20Synchronized%20%E5%85%B3%E9%94%AE%E5%AD%97%E4%BD%BF%E7%94%A8%E3%80%81%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E3%80%81JDK1.6%20%E4%B9%8B%E5%90%8E%E7%9A%84%E5%BA%95%E5%B1%82%E4%BC%98%E5%8C%96%E4%BB%A5%E5%8F%8A%20%E5%92%8CReenTrantLock%20%E7%9A%84%E5%AF%B9%E6%AF%94.png)
### synchronized关键字最主要的三种使用方式的总结
- **修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁**
- **修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁** 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源不管new了多少个对象只有一份所以对该类的所有对象都加了锁。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**。
- **修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。** 和 synchronized 方法一样synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中字符串常量池具有缓冲功能
下面我已一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。
面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!”
**双重校验锁实现对象单例(线程安全)**
```java
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
```
另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。
uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
1. 为 uniqueInstance 分配内存空间
2. 初始化 uniqueInstance
3. 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如线程 T1 执行了 1 和 3此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance但此时 uniqueInstance 还未被初始化。
使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
###synchronized 关键字底层原理总结
**synchronized 关键字底层原理属于 JVM 层面。**
**① synchronized 同步语句块的情况**
```java
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
```
通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`
![synchronized 关键字原理](https://images.gitbook.cn/abc37c80-d21d-11e8-aab3-09d30029e0d5)
从上面我们可以看出:
**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置monitorexit 指令则指明同步代码块的结束位置。** 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中synchronized 锁便是通过这种方式获取锁的也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后将锁计数器设为0表明锁被释放。如果获取对象锁失败那当前线程就要阻塞等待直到锁被另外一个线程释放为止。
**② synchronized 修饰方法的的情况**
```java
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
```
![synchronized 关键字原理](https://images.gitbook.cn/7d407bf0-d21e-11e8-b2d6-1188c7e0dd7e)
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识该标识指明了该方法是一个同步方法JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
在 Java 早期版本中synchronized 属于重量级锁效率低下因为监视器锁monitor是依赖于底层的操作系统的 Mutex Lock 来实现的Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
### JDK1.6 之后的底层优化
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
**①偏向锁**
**引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉**。
偏向锁的“偏”就是偏心的偏它的意思是会偏向于第一个获得它的线程如果在接下来的执行中该锁没有被其他线程获取那么持有偏向锁的线程就不需要进行同步关于偏向锁的原理可以查看《深入理解Java虚拟机JVM高级特性与最佳实践》第二版的13章第三节锁优化。
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
**② 轻量级锁**
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。**轻量级锁不是为了代替重量级锁它的本意是在没有多线程竞争的前提下减少传统的重量级锁使用操作系统互斥量产生的性能消耗因为使用轻量级锁时不需要申请互斥量。另外轻量级锁的加锁和解锁都用到了CAS操作。** 关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机JVM高级特性与最佳实践》第二版的13章第三节锁优化。
**轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争除了互斥量开销外还会额外发生CAS操作因此在有锁竞争的情况下轻量级锁比传统的重量级锁更慢如果锁竞争激烈那么轻量级将很快膨胀为重量级锁**
**③ 自旋锁和自适应自旋**
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。
**一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。** 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。**为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋**。
百度百科对自旋锁的解释:
> 何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过`--XX:+UseSpinning`参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。**自旋次数的默认值是10次用户可以修改`--XX:PreBlockSpin`来更改**。
另外,**在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了**。
**④ 锁消除**
锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
**⑤ 锁粗化**
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,——直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。
大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。
### Synchronized 和 ReenTrantLock 的对比
**① 两者都是可重入锁**
两者都是可重入锁。“可重入锁”概念是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁此时这个对象锁还没有释放当其再次想要获取这个对象的锁的时候还是可以获取的如果不可锁重入的话就会造成死锁。同一个线程每次获取锁锁的计数器都自增1所以要等到锁的计数器下降为0时才能释放锁。
**② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API**
synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化但是这些优化都是在虚拟机层面实现的并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
**③ ReenTrantLock 比 synchronized 增加了一些高级功能**
相比synchronizedReenTrantLock增加了一些高级功能。主要来说主要有三点**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)**
- **ReenTrantLock提供了一种能够中断等待锁的线程的机制**通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
- **ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReenTrantLock默认情况是非公平的可以通过 ReenTrantLock类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。
- synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制ReentrantLock类当然也可以实现但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的它具有很好的灵活性比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例即对象监视器**线程对象可以注册在指定的Condition中从而可以有选择性的进行线程通知在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的用ReentrantLock类结合Condition实例可以实现“选择性通知”** 这个功能非常重要而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
如果你想使用上述功能那么选择ReenTrantLock是一个不错的选择。
**synchronized 异常就会释放锁,而 ReenTrantLock 异常需要在 finally 里 unlock**
**④ 性能已不是选择标准**
在JDK1.6之前synchronized 的性能是比 ReenTrantLock 差很多。具体表示为synchronized 关键字吞吐量随线程数的增加下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。**JDK1.6 之后synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的JDK1.6之后性能已经不是选择synchronized和ReenTrantLock的影响因素了而且虚拟机在未来的性能改进中会更偏向于原生的synchronized所以还是提倡在synchronized能满足你的需求的情况下优先考虑使用synchronized关键字来进行同步优化后的synchronized和ReenTrantLock一样在很多地方都是用到了CAS操作**。

View File

@ -0,0 +1,62 @@
JDK1.6 对锁的实现引入了大量的优化来减少锁操作的开销,如: **偏向锁**、**轻量级锁**、**自旋锁**、**适应性自旋锁**、**锁消除**、**锁粗化** 等等技术。
锁主要存在四中状态,依次是:
1. 无锁状态
2. 偏向锁状态
3. 轻量级锁状态
4. 重量级锁状态
锁🔐会随着竞争的激烈而逐渐升级。
另外,需要注意:**锁可以升级不可降级,即 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁是单向的。** 这种策略是为了提高获得锁和释放锁的效率。
### 偏向锁
**引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉**。
偏向锁的“偏”就是偏心的偏它的意思是会偏向于第一个获得它的线程如果在接下来的执行中该锁没有被其他线程获取那么持有偏向锁的线程就不需要进行同步关于偏向锁的原理可以查看《深入理解Java虚拟机JVM高级特性与最佳实践》第二版的13章第三节锁优化。
#### 偏向锁的加锁
当一个线程访问同步块并获取锁时, 会在锁对象的对象头和栈帧中的锁记录里存储锁偏向的线程ID, 以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁, 只需要简单的测试一下锁对象的对象头的MarkWord里是否存储着指向当前线程的偏向锁(线程ID是当前线程), 如果测试成功, 表示线程已经获得了锁; 如果测试失败, 则需要再测试一下MarkWord中偏向锁的标识是否设置成1(表示当前是偏向锁), 如果没有设置, 则使用CAS竞争锁, 如果设置了, 则尝试使用CAS将锁对象的对象头的偏向锁指向当前线程.
#### 偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制, 所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁. 偏向锁的撤销需要等到全局安全点(在这个时间点上没有正在执行的字节码). 首先会暂停持有偏向锁的线程, 然后检查持有偏向锁的线程是否存活, 如果线程不处于活动状态, 则将锁对象的对象头设置为无锁状态; 如果线程仍然活着, 则锁对象的对象头中的MarkWord和栈中的锁记录要么重新偏向于其它线程要么恢复到无锁状态, 最后唤醒暂停的线程(释放偏向锁的线程).
但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
### 轻量级锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。**轻量级锁不是为了代替重量级锁它的本意是在没有多线程竞争的前提下减少传统的重量级锁使用操作系统互斥量产生的性能消耗因为使用轻量级锁时不需要申请互斥量。另外轻量级锁的加锁和解锁都用到了CAS操作。** 关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机JVM高级特性与最佳实践》第二版的13章第三节锁优化。
**轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争除了互斥量开销外还会额外发生CAS操作因此在有锁竞争的情况下轻量级锁比传统的重量级锁更慢如果锁竞争激烈那么轻量级将很快膨胀为重量级锁**
### 自旋锁和自适应自旋
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。
**一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。** 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。**为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋**。
百度百科对自旋锁的解释:
> 何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过`--XX:+UseSpinning`参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。**自旋次数的默认值是10次用户可以修改`--XX:PreBlockSpin`来更改**。
另外,**在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了**。
### 锁消除
锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
### 锁粗化
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,——直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。
大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。

View File

@ -92,7 +92,7 @@ public class Sub extends Super {
**使用 this 和 super 要注意的问题:** **使用 this 和 super 要注意的问题:**
- 在构造器中使用 `super` 调用父类中的其他构造方法时该语句必须处于构造器的首行否则编译器会报错。另外this 调用本类中的其他构造方法时,也要放在首行。 - 在构造器中使用 `super()` 调用父类中的其他构造方法时该语句必须处于构造器的首行否则编译器会报错。另外this 调用本类中的其他构造方法时,也要放在首行。
- this、super不能用在static方法中。 - this、super不能用在static方法中。
**简单解释一下:** **简单解释一下:**
@ -141,7 +141,7 @@ public class StaticBean {
this.name = name; this.name = name;
} }
//静态方法 //静态方法
static void SayHello() { static void sayHello() {
System.out.println("Hello i am java"); System.out.println("Hello i am java");
} }
@Override @Override
@ -164,7 +164,7 @@ public class StaticDemo {
StaticBean.age = 33; StaticBean.age = 33;
System.out.println(staticBean + " " + staticBean2 + " " + staticBean3 + " " + staticBean4); System.out.println(staticBean + " " + staticBean2 + " " + staticBean3 + " " + staticBean4);
//StaticBean{name=1,age=33} StaticBean{name=2,age=33} StaticBean{name=3,age=33} StaticBean{name=4,age=33} //StaticBean{name=1,age=33} StaticBean{name=2,age=33} StaticBean{name=3,age=33} StaticBean{name=4,age=33}
StaticBean.SayHello();//Hello i am java StaticBean.sayHello();//Hello i am java
} }
} }
@ -173,7 +173,7 @@ public class StaticDemo {
### 静态代码块 ### 静态代码块
静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。 该类不管创建多少对象,静态代码块只执行一次. 静态代码块定义在类中方法外, 静态代码块在非静态代码块之前执行(静态代码块 > 非静态代码块 > 构造方法)。 该类不管创建多少对象,静态代码块只执行一次.
静态代码块的格式是 静态代码块的格式是
@ -274,7 +274,11 @@ class Foo {
} }
``` ```
你可以像这样调用静态方法:`Foo.method1()`。 如果您尝试使用这种方法调用 method2 将失败。 但这样可行:`Foo bar = new Foo(1);bar.method2();` 你可以像这样调用静态方法:`Foo.method1()`。 如果您尝试使用这种方法调用 method2 将失败。 但这样可行
``` java
Foo bar = new Foo(1);
bar.method2();
```
总结: 总结:
@ -285,7 +289,7 @@ class Foo {
相同点: 都是在JVM加载类时且在构造方法执行之前执行在类中都可以定义多个定义多个时按定义的顺序执行一般在代码块中对一些static变量进行赋值。 相同点: 都是在JVM加载类时且在构造方法执行之前执行在类中都可以定义多个定义多个时按定义的顺序执行一般在代码块中对一些static变量进行赋值。
不同点: 静态代码块在非静态代码块之前执行(静态代码块—非静态代码块—构造方法)。静态代码块只在第一次new执行一次之后不再执行而非静态代码块在每new一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。 不同点: 静态代码块在非静态代码块之前执行(静态代码块 -> 非静态代码块 -> 构造方法)。静态代码块只在第一次new执行一次之后不再执行而非静态代码块在每new一次就执行一次。 非静态代码块可在普通方法中定义(不过作用不大);而静态代码块不行。
> 修正 [issue #677](https://github.com/Snailclimb/JavaGuide/issues/677)静态代码块可能在第一次new的时候执行但不一定只在第一次new的时候执行。比如通过 `Class.forName("ClassDemo")`创建 Class 对象的时候也会执行。 > 修正 [issue #677](https://github.com/Snailclimb/JavaGuide/issues/677)静态代码块可能在第一次new的时候执行但不一定只在第一次new的时候执行。比如通过 `Class.forName("ClassDemo")`创建 Class 对象的时候也会执行。
@ -347,6 +351,6 @@ public class Test {
### 参考 ### 参考
- httpsblog.csdn.netchen13579867831articledetails78995480 - https://blog.csdn.net/chen13579867831/article/details/78995480
- httpwww.cnblogs.comchenssyp3388487.html - https://www.cnblogs.com/chenssy/p/3388487.html
- httpwww.cnblogs.comQian123p5713440.html - https://www.cnblogs.com/Qian123/p/5713440.html

View File

@ -51,10 +51,7 @@ public class Pizza {
} }
public boolean isDeliverable() { public boolean isDeliverable() {
if (getStatus() == PizzaStatus.READY) { return getStatus() == PizzaStatus.READY;
return true;
}
return false;
} }
// Methods that set and get the status variable. // Methods that set and get the status variable.
@ -63,9 +60,9 @@ public class Pizza {
## 3.使用 == 比较枚举类型 ## 3.使用 == 比较枚举类型
由于枚举类型确保JVM中仅存在一个常量实例因此我们可以安全地使用“ ==”运算符比较两个变量,如上例所示;此外,“ ==”运算符可提供编译时和运行时的安全性。 由于枚举类型确保JVM中仅存在一个常量实例因此我们可以安全地使用 `==` 运算符比较两个变量,如上例所示;此外,`==` 运算符可提供编译时和运行时的安全性。
首先,让我们看一下以下代码段中的运行时安全性,其中“ ==”运算符用于比较状态并且如果两个值均为null 都不会引发 NullPointerException。相反如果使用equals方法将抛出 NullPointerException 首先,让我们看一下以下代码段中的运行时安全性,其中 `==` 运算符用于比较状态并且如果两个值均为null 都不会引发 NullPointerException。相反如果使用equals方法将抛出 NullPointerException
```java ```java
if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED)); if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED));
@ -84,9 +81,12 @@ if(testPz.getStatus() == TestColor.GREEN);
```java ```java
public int getDeliveryTimeInDays() { public int getDeliveryTimeInDays() {
switch (status) { switch (status) {
case ORDERED: return 5; case ORDERED:
case READY: return 2; return 5;
case DELIVERED: return 0; case READY:
return 2;
case DELIVERED:
return 0;
} }
return 0; return 0;
} }

View File

@ -75,7 +75,7 @@
![](http://img.francisqiang.top/img/垃圾例子.jpg) ![](http://img.francisqiang.top/img/垃圾例子.jpg)
而上述前者就是 `Eureka` 的处理方式它保证了AP可用性后者就是我们今天所要`ZooKeeper` 的处理方式它保证了CP数据一致性 而上述前者就是 `Eureka` 的处理方式它保证了AP可用性后者就是我们今天所要`ZooKeeper` 的处理方式它保证了CP数据一致性
## 4. 一致性协议和算法 ## 4. 一致性协议和算法