mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-25 02:27:10 +08:00
Merge branch 'master' of https://gitee.com/xiaofeixiaygf/JavaGuide
This commit is contained in:
commit
057a82a981
@ -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
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
### 3.布隆过滤器使用场景
|
### 3.布隆过滤器使用场景
|
||||||
|
|
||||||
1. 判断给定数据是否存在:比如判断一个数字是否在于包含大量数字的数字集中(数字集很大,5亿以上!)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤、黑名单功能等等。
|
1. 判断给定数据是否存在:比如判断一个数字是否存在于包含大量数字的数字集中(数字集很大,5亿以上!)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤、黑名单功能等等。
|
||||||
2. 去重:比如爬给定网址的时候对已经爬取过的 URL 去重。
|
2. 去重:比如爬给定网址的时候对已经爬取过的 URL 去重。
|
||||||
|
|
||||||
### 4.通过 Java 编程手动实现布隆过滤器
|
### 4.通过 Java 编程手动实现布隆过滤器
|
||||||
|
@ -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;
|
|||||||

|

|
||||||
|
|
||||||
1. **原子性(Atomicity):** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
|
1. **原子性(Atomicity):** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
|
||||||
2. **一致性(Consistency):** 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
|
2. **一致性(Consistency):** 执行事务后,数据库从一个正确的状态变化到另一个正确的状态;
|
||||||
3. **隔离性(Isolation):** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
|
3. **隔离性(Isolation):** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
|
||||||
4. **持久性(Durability):** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
|
4. **持久性(Durability):** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
|
||||||
|
|
||||||
|
@ -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 采用的是 **定期删除+惰性/懒汉式删除** 。
|
||||||
|
|
||||||
|
@ -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 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。
|
JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,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 节
|
||||||
> 
|
> 
|
||||||
@ -207,7 +208,7 @@ Java 中的注释有三种:
|
|||||||
|
|
||||||
3. 文档注释。
|
3. 文档注释。
|
||||||
|
|
||||||
在我们编写代码的时候,如果代码量比较少,我们自己或者团队其他成员还可以很轻易地看懂代码,但是当项目结构一旦复杂起来,我们就需要用到注释了。注释并不会执行,是我们程序员写给自己看的,注释是你的代码说明书,能够帮助看代码的人快速地理清代码之间的逻辑关系。因此,在写程序的时候随手加上注释是一个非常好的习惯。
|
在我们编写代码的时候,如果代码量比较少,我们自己或者团队其他成员还可以很轻易地看懂代码,但是当项目结构一旦复杂起来,我们就需要用到注释了。注释并不会执行(编译器在编译代码之前会把代码中的所有注释抹掉,字节码中不保留注释),是我们程序员写给自己看的,注释是你的代码说明书,能够帮助看代码的人快速地理清代码之间的逻辑关系。因此,在写程序的时候随手加上注释是一个非常好的习惯。
|
||||||
|
|
||||||
《Clean Code》这本书明确指出:
|
《Clean Code》这本书明确指出:
|
||||||
|
|
||||||
@ -253,7 +254,7 @@ Java 中的注释有三种:
|
|||||||
|
|
||||||
在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1,Java 提供了一种特殊的运算符,用于这种表达式,叫做自增运算符(++)和自减运算符(--)。
|
在写代码的过程中,常见的一种情况是需要某个整数类型变量增加 1 或减少 1,Java 提供了一种特殊的运算符,用于这种表达式,叫做自增运算符(++)和自减运算符(--)。
|
||||||
|
|
||||||
++和--运算符可以放在操作数之前,也可以放在操作数之后,当运算符放在操作数之前时,先自增/减,再赋值;当运算符放在操作数之后时,先赋值,再自增/减。例如,当“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的区别是什么?
|
||||||
|
|
||||||
@ -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 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
|
||||||
|
@ -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,7 +181,8 @@ 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);
|
||||||
@ -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的。
|
||||||
|
|
||||||

|

|
||||||
|
@ -1,30 +1,47 @@
|
|||||||
点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《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.3. 讲一下 synchronized 关键字的底层原理](#13-讲一下-synchronized-关键字的底层原理)
|
||||||
- [1.4. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗](#14-说说-jdk16-之后的synchronized-关键字底层做了哪些优化可以详细介绍一下这些优化吗)
|
- [1.3.1. synchronized 同步语句块的情况](#131-synchronized-同步语句块的情况)
|
||||||
- [1.5. 谈谈 synchronized和ReentrantLock 的区别](#15-谈谈-synchronized和reentrantlock-的区别)
|
- [1.3.2. `synchronized` 修饰方法的的情况](#132-synchronized-修饰方法的的情况)
|
||||||
- [2. volatile关键字](#2-volatile关键字)
|
- [1.3.3.总结](#133总结)
|
||||||
- [2.1. 讲一下Java内存模型](#21-讲一下java内存模型)
|
- [1.4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗](#14-说说-jdk16-之后的-synchronized-关键字底层做了哪些优化可以详细介绍一下这些优化吗)
|
||||||
- [2.2. 说说 synchronized 关键字和 volatile 关键字的区别](#22-说说-synchronized-关键字和-volatile-关键字的区别)
|
- [1.5. 谈谈 synchronized 和 ReentrantLock 的区别](#15-谈谈-synchronized-和-reentrantlock-的区别)
|
||||||
|
- [1.5.1. 两者都是可重入锁](#151-两者都是可重入锁)
|
||||||
|
- [1.5.2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API](#152synchronized-依赖于-jvm-而-reentrantlock-依赖于-api)
|
||||||
|
- [1.5.3.ReentrantLock 比 synchronized 增加了一些高级功能](#153reentrantlock-比-synchronized-增加了一些高级功能)
|
||||||
|
- [2. volatile 关键字](#2-volatile-关键字)
|
||||||
|
- [2.1. CPU 缓存模型](#21-cpu-缓存模型)
|
||||||
|
- [2.2. 讲一下 JMM(Java 内存模型)](#22-讲一下-jmmjava-内存模型)
|
||||||
|
- [2.3. 并发编程的三个重要特性](#23-并发编程的三个重要特性)
|
||||||
|
- [2.4. 说说 synchronized 关键字和 volatile 关键字的区别](#24-说说-synchronized-关键字和-volatile-关键字的区别)
|
||||||
- [3. ThreadLocal](#3-threadlocal)
|
- [3. ThreadLocal](#3-threadlocal)
|
||||||
- [3.1. ThreadLocal简介](#31-threadlocal简介)
|
- [3.1. ThreadLocal 简介](#31-threadlocal-简介)
|
||||||
- [3.2. ThreadLocal示例](#32-threadlocal示例)
|
- [3.2. ThreadLocal 示例](#32-threadlocal-示例)
|
||||||
- [3.3. ThreadLocal原理](#33-threadlocal原理)
|
- [3.3. ThreadLocal 原理](#33-threadlocal-原理)
|
||||||
- [3.4. ThreadLocal 内存泄露问题](#34-threadlocal-内存泄露问题)
|
- [3.4. ThreadLocal 内存泄露问题](#34-threadlocal-内存泄露问题)
|
||||||
- [4. 线程池](#4-线程池)
|
- [4. 线程池](#4-线程池)
|
||||||
- [4.1. 为什么要用线程池?](#41-为什么要用线程池)
|
- [4.1. 为什么要用线程池?](#41-为什么要用线程池)
|
||||||
- [4.2. 实现Runnable接口和Callable接口的区别](#42-实现runnable接口和callable接口的区别)
|
- [4.2. 实现 Runnable 接口和 Callable 接口的区别](#42-实现-runnable-接口和-callable-接口的区别)
|
||||||
- [4.3. 执行execute()方法和submit()方法的区别是什么呢?](#43-执行execute方法和submit方法的区别是什么呢)
|
- [4.3. 执行 execute()方法和 submit()方法的区别是什么呢?](#43-执行-execute方法和-submit方法的区别是什么呢)
|
||||||
- [4.4. 如何创建线程池](#44-如何创建线程池)
|
- [4.4. 如何创建线程池](#44-如何创建线程池)
|
||||||
|
- [4.5 ThreadPoolExecutor 类分析](#45-threadpoolexecutor-类分析)
|
||||||
|
- [4.5.1 `ThreadPoolExecutor`构造函数重要参数分析](#451-threadpoolexecutor构造函数重要参数分析)
|
||||||
|
- [4.5.2 `ThreadPoolExecutor` 饱和策略](#452-threadpoolexecutor-饱和策略)
|
||||||
|
- [4.6 一个简单的线程池 Demo](#46-一个简单的线程池-demo)
|
||||||
|
- [4.7 线程池原理分析](#47-线程池原理分析)
|
||||||
- [5. Atomic 原子类](#5-atomic-原子类)
|
- [5. Atomic 原子类](#5-atomic-原子类)
|
||||||
- [5.1. 介绍一下Atomic 原子类](#51-介绍一下atomic-原子类)
|
- [5.1. 介绍一下 Atomic 原子类](#51-介绍一下-atomic-原子类)
|
||||||
- [5.2. JUC 包中的原子类是哪4类?](#52-juc-包中的原子类是哪4类)
|
- [5.2. JUC 包中的原子类是哪 4 类?](#52-juc-包中的原子类是哪-4-类)
|
||||||
- [5.3. 讲讲 AtomicInteger 的使用](#53-讲讲-atomicinteger-的使用)
|
- [5.3. 讲讲 AtomicInteger 的使用](#53-讲讲-atomicinteger-的使用)
|
||||||
- [5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理](#54-能不能给我简单介绍一下-atomicinteger-类的原理)
|
- [5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理](#54-能不能给我简单介绍一下-atomicinteger-类的原理)
|
||||||
- [6. AQS](#6-aqs)
|
- [6. AQS](#6-aqs)
|
||||||
@ -32,34 +49,70 @@
|
|||||||
- [6.2. AQS 原理分析](#62-aqs-原理分析)
|
- [6.2. AQS 原理分析](#62-aqs-原理分析)
|
||||||
- [6.2.1. AQS 原理概览](#621-aqs-原理概览)
|
- [6.2.1. AQS 原理概览](#621-aqs-原理概览)
|
||||||
- [6.2.2. AQS 对资源的共享方式](#622-aqs-对资源的共享方式)
|
- [6.2.2. AQS 对资源的共享方式](#622-aqs-对资源的共享方式)
|
||||||
- [6.2.3. AQS底层使用了模板方法模式](#623-aqs底层使用了模板方法模式)
|
- [6.2.3. AQS 底层使用了模板方法模式](#623-aqs-底层使用了模板方法模式)
|
||||||
- [6.3. AQS 组件总结](#63-aqs-组件总结)
|
- [6.3. AQS 组件总结](#63-aqs-组件总结)
|
||||||
|
- [6.4. 用过 CountDownLatch 么?什么场景下用的?](#64-用过-countdownlatch-么什么场景下用的)
|
||||||
- [7 Reference](#7-reference)
|
- [7 Reference](#7-reference)
|
||||||
|
- [公众号](#公众号)
|
||||||
|
|
||||||
|
<!-- /code_chunk_output -->
|
||||||
|
|
||||||
<!-- /TOC -->
|
|
||||||
|
|
||||||
# Java 并发进阶常见面试题总结
|
# Java 并发进阶常见面试题总结
|
||||||
|
|
||||||
## 1.synchronized 关键字
|
## 1.synchronized 关键字
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### 1.1.说一说自己对于 synchronized 关键字的了解
|
### 1.1.说一说自己对于 synchronized 关键字的了解
|
||||||
|
|
||||||
synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
|
**`synchronized` 关键字解决的是多个线程之间访问资源的同步性,`synchronized`关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。**
|
||||||
|
|
||||||
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
|
另外,在 Java 早期版本中,`synchronized` 属于 **重量级锁**,效率低下。
|
||||||
|
|
||||||
|
**为什么呢?**
|
||||||
|
|
||||||
### 1.2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗
|
因为监视器锁(monitor)是依赖于底层的操作系统的 `Mutex Lock` 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。
|
||||||
|
|
||||||
|
庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对 synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6 对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
|
||||||
|
|
||||||
|
所以,你会发现目前的话,不论是各种开源框架还是 JDK 源码都大量使用了 synchronized 关键字。
|
||||||
|
|
||||||
|
### 1.2. 说说自己是怎么使用 synchronized 关键字
|
||||||
|
|
||||||
**synchronized 关键字最主要的三种使用方式:**
|
**synchronized 关键字最主要的三种使用方式:**
|
||||||
|
|
||||||
- **修饰实例方法:** 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
|
**1.修饰实例方法:** 作用于当前对象实例加锁,进入同步代码前要获得 **当前对象实例的锁**
|
||||||
- **修饰静态方法:** 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**。
|
|
||||||
- **修饰代码块:** 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
|
|
||||||
|
|
||||||
**总结:** synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
|
```java
|
||||||
|
synchronized void method() {
|
||||||
|
//业务代码
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
下面我以一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。
|
**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 同步语句块的实现使用的是 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,8 +207,15 @@ public class SynchronizedDemo2 {
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
|
`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
|
||||||
|
|
||||||
|
#### 1.3.3.总结
|
||||||
|
|
||||||
|
`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。
|
||||||
|
|
||||||
|
`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。
|
||||||
|
|
||||||
|
**不过两者的本质都是对对象监视器 monitor 的获取。**
|
||||||
|
|
||||||
### 1.4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗
|
### 1.4. 说说 JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗
|
||||||
|
|
||||||
@ -146,58 +223,80 @@ JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、
|
|||||||
|
|
||||||
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
|
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
|
||||||
|
|
||||||
关于这几种优化的详细信息可以查看笔主的这篇文章:<https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/Multithread/synchronized.md>
|
关于这几种优化的详细信息可以查看下面这几篇文章:
|
||||||
|
|
||||||
|
- [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. 谈谈 synchronized 和 ReentrantLock 的区别
|
||||||
|
|
||||||
|
#### 1.5.1. 两者都是可重入锁
|
||||||
|
|
||||||
**① 两者都是可重入锁**
|
**“可重入锁”** 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
|
||||||
|
|
||||||
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
|
#### 1.5.2.synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
|
||||||
|
|
||||||
**② 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 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
|
#### 1.5.3.ReentrantLock 比 synchronized 增加了一些高级功能
|
||||||
|
|
||||||
**③ ReentrantLock 比 synchronized 增加了一些高级功能**
|
相比`synchronized`,`ReentrantLock`增加了一些高级功能。主要来说主要有三点:
|
||||||
|
|
||||||
相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)**
|
- **等待可中断** : `ReentrantLock`提供了一种能够中断等待锁的线程的机制,通过 `lock.lockInterruptibly()` 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
|
||||||
|
- **可实现公平锁** : `ReentrantLock`可以指定是公平锁还是非公平锁。而`synchronized`只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。`ReentrantLock`默认情况是非公平的,可以通过 `ReentrantLock`类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。
|
||||||
|
- **可实现选择性通知(锁可以绑定多个条件)**: `synchronized`关键字与`wait()`和`notify()`/`notifyAll()`方法相结合可以实现等待/通知机制。`ReentrantLock`类当然也可以实现,但是需要借助于`Condition`接口与`newCondition()`方法。
|
||||||
|
|
||||||
- **ReentrantLock提供了一种能够中断等待锁的线程的机制**,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
|
> `Condition`是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个`Lock`对象中可以创建多个`Condition`实例(即对象监视器),**线程对象可以注册在指定的`Condition`中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用`notify()/notifyAll()`方法进行通知时,被通知的线程是由 JVM 选择的,用`ReentrantLock`类结合`Condition`实例可以实现“选择性通知”** ,这个功能非常重要,而且是 Condition 接口默认提供的。而`synchronized`关键字就相当于整个 Lock 对象中只有一个`Condition`实例,所有的线程都注册在它一个身上。如果执行`notifyAll()`方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而`Condition`实例的`signalAll()`方法 只会唤醒注册在该`Condition`实例中的所有等待线程。
|
||||||
- **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是一个不错的选择。
|
**如果你想使用上述功能,那么选择 ReentrantLock 是一个不错的选择。性能已不是选择标准**
|
||||||
|
|
||||||
**④ 性能已不是选择标准**
|
|
||||||
|
|
||||||
## 2. volatile 关键字
|
## 2. volatile 关键字
|
||||||
|
|
||||||
### 2.1. 讲一下Java内存模型
|
我们先要从 **CPU 缓存模型** 说起!
|
||||||
|
|
||||||
|
### 2.1. CPU 缓存模型
|
||||||
|
|
||||||
|
**为什么要弄一个 CPU 高速缓存呢?**
|
||||||
|
|
||||||
|
类比我们开发网站后台系统使用的缓存(比如 Redis)是为了解决程序处理速度和访问常规关系型数据库速度不对等的问题。 **CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。**
|
||||||
|
|
||||||
|
我们甚至可以把 **内存可以看作外存的高速缓存**,程序运行的时候我们把外存的数据复制到内存,由于内存的处理速度远远高于外存,这样提高了处理速度。
|
||||||
|
|
||||||
|
总结:**CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。**
|
||||||
|
|
||||||
|
为了更好地理解,我画了一个简单的 CPU Cache 示意图如下(实际上,现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**CPU Cache 的工作方式:**
|
||||||
|
|
||||||
|
先复制一份数据到 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 内存模型下,线程可以把变量保存**本地内存**(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成**数据的不一致**。
|
在 JDK1.2 之前,Java 的内存模型实现总是从**主存**(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存**本地内存**(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成**数据的不一致**。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
要解决这个问题,就需要把变量声明为**volatile**,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。
|
要解决这个问题,就需要把变量声明为**`volatile`**,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
|
||||||
|
|
||||||
说白了, **volatile** 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。
|
所以,**`volatile` 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 2.2 并发编程的三个重要特性
|
### 2.3. 并发编程的三个重要特性
|
||||||
|
|
||||||
1. **原子性** : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。`synchronized` 可以保证代码片段的原子性。
|
1. **原子性** : 一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。`synchronized` 可以保证代码片段的原子性。
|
||||||
2. **可见性** :当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。`volatile` 关键字可以保证共享变量的可见性。
|
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 关键字解决的是多个线程之间访问资源的同步性。**
|
||||||
|
|
||||||
@ -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。
|
||||||
|
|
||||||
@ -719,7 +818,6 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是
|
|||||||
|
|
||||||
所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
|
所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
|
||||||
|
|
||||||
|
|
||||||
并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。
|
并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。
|
||||||
|
|
||||||

|

|
||||||
@ -738,7 +836,6 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是
|
|||||||
|
|
||||||
使用原子的方式更新数组里的某个元素
|
使用原子的方式更新数组里的某个元素
|
||||||
|
|
||||||
|
|
||||||
- AtomicIntegerArray:整形数组原子类
|
- AtomicIntegerArray:整形数组原子类
|
||||||
- AtomicLongArray:长整形数组原子类
|
- AtomicLongArray:长整形数组原子类
|
||||||
- AtomicReferenceArray:引用类型数组原子类
|
- AtomicReferenceArray:引用类型数组原子类
|
||||||
@ -755,7 +852,6 @@ Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是
|
|||||||
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
|
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
|
||||||
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
|
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
|
||||||
|
|
||||||
|
|
||||||
### 5.3. 讲讲 AtomicInteger 的使用
|
### 5.3. 讲讲 AtomicInteger 的使用
|
||||||
|
|
||||||
**AtomicInteger 类常用方法**
|
**AtomicInteger 类常用方法**
|
||||||
@ -773,6 +869,7 @@ 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();
|
||||||
@ -841,7 +938,6 @@ AQS 原理这部分参考了部分博客,在5.2节末尾放了链接。
|
|||||||
|
|
||||||
看个 AQS(AbstractQueuedSynchronizer)原理图:
|
看个 AQS(AbstractQueuedSynchronizer)原理图:
|
||||||
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
|
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
|
||||||
@ -920,6 +1016,89 @@ tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true
|
|||||||
- **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 虚拟机》
|
||||||
|
@ -84,7 +84,7 @@ public class MultiThread {
|
|||||||
|
|
||||||
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。
|
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。
|
||||||
|
|
||||||
**总结:** 线程 是 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反
|
**总结:** **线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。**
|
||||||
|
|
||||||
下面是该知识点的扩展内容!
|
下面是该知识点的扩展内容!
|
||||||
|
|
||||||
|
422
docs/java/Multithread/Untitled.md
Normal file
422
docs/java/Multithread/Untitled.md
Normal 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依赖于**底层操作系统**的实现,存在**用户态**和**内核态之间**的**切换**,所以增加了**性能开销**
|
||||||
|
|
||||||
|
[](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同步锁的升级优化路径:***偏向锁** -> **轻量级锁** -> **重量级锁***
|
||||||
|
|
||||||
|
[](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`
|
||||||
|
|
||||||
|
红线流程部分:偏向锁的**获取**和**撤销**
|
||||||
|
[](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. 轻量级锁的适用场景:线程**交替执行**同步块,***绝大部分的锁在整个同步周期内都不存在长时间的竞争***
|
||||||
|
|
||||||
|
红线流程部分:升级轻量级锁
|
||||||
|
[](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控制**
|
||||||
|
|
||||||
|
[](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 |
@ -1,171 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 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 同步语句块的实现使用的是 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 修饰的方法并没有 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 增加了一些高级功能**
|
|
||||||
|
|
||||||
相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)**
|
|
||||||
|
|
||||||
- **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操作**。
|
|
62
docs/java/Multithread/synchronized在JDK1.6之后的底层优化.md
Normal file
62
docs/java/Multithread/synchronized在JDK1.6之后的底层优化.md
Normal 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 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了**。
|
||||||
|
|
||||||
|
### 锁消除
|
||||||
|
|
||||||
|
锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
|
||||||
|
|
||||||
|
### 锁粗化
|
||||||
|
|
||||||
|
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,——直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。
|
||||||
|
|
||||||
|
大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
而上述前者就是 `Eureka` 的处理方式,它保证了AP(可用性),后者就是我们今天所要将的 `ZooKeeper` 的处理方式,它保证了CP(数据一致性)。
|
而上述前者就是 `Eureka` 的处理方式,它保证了AP(可用性),后者就是我们今天所要讲的 `ZooKeeper` 的处理方式,它保证了CP(数据一致性)。
|
||||||
|
|
||||||
## 4. 一致性协议和算法
|
## 4. 一致性协议和算法
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user