diff --git a/.gitignore b/.gitignore index 7ee001fcc8..6d26a3347a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,44 @@ -*.DS_Store -*.iml -*.xml -*.txt -*.dio +# Created by .ignore support plugin (hsz.mobi) +### Java template +# Compiled class file +*.class + +# Log file *.log +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +.DS_Store +.idea/JavaEdge.iml +.idea/compiler.xml .idea/inspectionProfiles/ +.idea/misc.xml +.idea/modules.xml +.idea/vcs.xml +.idea/workspace.xml +Python/.DS_Store +大数据/.DS_Store +操作系统/.DS_Store +数据存储/.DS_Store +面试题系列/.DS_Store + .metals/metals.h2.db +.metals/metals.log .vscode/settings.json -Spring/feign.md -TODO/MySQL会丢数据吗?.md -TODO/【阿里最新数据库面试题】MySQL主从一致性.md -TODO/【阿里数据库面试题解】MySQL高可用原理.md -TODO/MySQL执行更新语句时做了什么?.md -TODO/为何阿里不推荐MySQL使用join?.md -TODO/【阿里MySQL面试题】内部临时表.md -TODO/MySQL数据查询太多会OOM吗?.md -TODO/有了InnoDB,Memory存储引擎还有意义吗?.md -TODO/MySQL执行insert会如何加锁?.md -TODO/MySQL的自增id竟然用到头了怎么办?.md -TODO/MySQL全局锁和表锁.md -TODO/MySQL事务是怎么实现隔离的?.md - diff --git "a/Git/Git\345\267\245\344\275\234:\351\235\242\350\257\225\345\277\205\347\237\245\345\277\205\344\274\232.md" "b/Git/Git\345\267\245\344\275\234:\351\235\242\350\257\225\345\277\205\347\237\245\345\277\205\344\274\232.md" index 3abed05a64..ba9faa1c77 100644 --- "a/Git/Git\345\267\245\344\275\234:\351\235\242\350\257\225\345\277\205\347\237\245\345\277\205\344\274\232.md" +++ "b/Git/Git\345\267\245\344\275\234:\351\235\242\350\257\225\345\277\205\347\237\245\345\277\205\344\274\232.md" @@ -234,8 +234,8 @@ git://协议较为快速和有效,但是有时必须使用http协议,比如你 在默认情况下,Git会把"Git URL"里最后一级目录名的'.git'的后辍去掉,做为新克隆(clone)项目的目录名: (例如. git clone [http://git.kernel.org/linux/kernel/git/torvalds/linux-2.6.git](https://link.jianshu.com?t=http://git.kernel.org/linux/kernel/git/torvalds/linux-2.6.git) 会建立一个目录叫'linux-2.6') 另外,如果访问一个Git URL需要用法名和密码,可以在Git URL前加上用户名,并在它们之间加上@符合以表示分割,然后执行git clone命令,git会提示你输入密码。 示例 -git clone [v_JavaEdge@http://www.kernel.org/pub/scm/git/git.git](https://link.jianshu.com?t=http://v_JavaEdge@http://www.kernel.org/pub/scm/git/git.git) -这样将以作为[v_JavaEdge](https://link.jianshu.com?t=http://v_JavaEdge)用户名访问[http://www.kernel.org/pub/scm/git/git.git](https://link.jianshu.com?t=http://www.kernel.org/pub/scm/git/git.git),然后按回车键执行git clone命令,git会提示你输入密码。 +git clone [v_shishusheng@http://www.kernel.org/pub/scm/git/git.git](https://link.jianshu.com?t=http://v_shishusheng@http://www.kernel.org/pub/scm/git/git.git) +这样将以作为[v_shishusheng](https://link.jianshu.com?t=http://v_shishusheng)用户名访问[http://www.kernel.org/pub/scm/git/git.git](https://link.jianshu.com?t=http://www.kernel.org/pub/scm/git/git.git),然后按回车键执行git clone命令,git会提示你输入密码。 另外,我们可以通过-b 来指定要克隆的分支名,比如 $ git clone -b master2 ../server . 表示克隆名为master2的这个分支,如果省略-b 表示克隆master分支。 diff --git "a/Git/Git\347\211\210\346\234\254\345\233\236\351\200\200\346\226\271\346\263\225\350\256\272(\345\217\257\350\203\275\350\247\243\345\206\263\344\275\240101%\351\201\207\345\210\260\347\232\204Git\347\211\210\346\234\254\351\227\256\351\242\230).md" "b/Git/Git\347\211\210\346\234\254\345\233\236\351\200\200\346\226\271\346\263\225\350\256\272(\345\217\257\350\203\275\350\247\243\345\206\263\344\275\240101%\351\201\207\345\210\260\347\232\204Git\347\211\210\346\234\254\351\227\256\351\242\230).md" deleted file mode 100644 index 8d5f27d5c5..0000000000 --- "a/Git/Git\347\211\210\346\234\254\345\233\236\351\200\200\346\226\271\346\263\225\350\256\272(\345\217\257\350\203\275\350\247\243\345\206\263\344\275\240101%\351\201\207\345\210\260\347\232\204Git\347\211\210\346\234\254\351\227\256\351\242\230).md" +++ /dev/null @@ -1,120 +0,0 @@ -# 1 本地回退 -你在本地做了错误的 commit,先找到要回退的版本的`commit id`: - -```bash -git reflog -``` -![](https://img-blog.csdnimg.cn/20200414142250436.png) -接着回退版本: - -```bash -git reset --hard cac0 -``` -> cac0就是你要回退的版本的`commit id`的前面几位 - -回退到某次提交。回退到的指定提交以后的提交都会从提交日志上消失。 - -> 工作区和暂存区的内容都会被重置到指定提交的时候,如果不加`--hard`则只移动`HEAD`指针,不影响工作区和暂存区的内容。 - -结合`git reflog`找回提交日志上看不到的版本历史,撤回某次操作前的状态 -这个方法可以对你的回退操作进行回退,因为这时候`git log`已经找不到历史提交的hash值了。 - -# 2 远程回退 -## 2.1 回退自己的远程分支 -你的错误commit已经推送到远程分支,就需要回滚远程分支。 -- 首先要回退本地分支: - -```bash -git reflog -git reset --hard cac0 -``` -![](https://img-blog.csdnimg.cn/20200414142459436.png) -- 由于本地分支回滚后,版本将落后远程分支,必须使用强制推送覆盖远程分支,否则后面将无法推送到远程分支。 - -```bash -git push -f -``` -![](https://img-blog.csdnimg.cn/20200414142539953.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -- 注意修正为`git push -f origin branch_name` -![](https://img-blog.csdnimg.cn/20200414142624784.png) - -## 2.2 回退公共远程分支 -如果你回退公共远程分支,把别人的提交给丢掉了怎么办? -> 本人毕业时在前东家 hw 经常干的蠢事。 - -### 分析 -假如你的远程master分支情况是这样的: - -```bash -A1–A2–B1 -``` -> A、B分别代表两个人 -> A1、A2、B1代表各自的提交 -> 所有人的本地分支都已经更新到最新版本,和远程分支一致 - -这时发现A2这次commit有误,你用reset回滚远程分支master到A1,那么理想状态是你的同事一拉代码git pull,他们的master分支也回滚了 -然而现实却是,你的同事会看到下面的提示: - -```bash -$ git status -On branch master -Your branch is ahead of 'origin/master' by 2 commits. - (use "git push" to publish your local commits) -nothing to commit, working directory clean -``` -也就是说,你的同事的分支并没有主动回退,而是比远程分支超前了两次提交,因为远程分支回退了。 -不幸的是,现实中,我们经常遇到的都是猪一样的队友,他们一看到下面提示: - -```bash -$ git status -On branch master -Your branch is ahead of 'origin/master' by 2 commits. - (use "git push" to publish your local commits) -nothing to commit, working directory clean -``` -就习惯性的git push一下,或者他们直接用的SourceTree这样的图形界面工具,一看到界面上显示的是推送的提示就直接点了推送按钮,卧槽,辛辛苦苦回滚的版本就这样轻松的被你猪一样的队友给还原了,所以,只要有一个队友push之后,远程master又变成了: - -```bash -A1 – A2 – B1 -``` - -这就是分布式,每个人都有副本。 - -用另外一种方法来回退版本。 - -# 3 公共远程回退 -使用git reset回退公共远程分支的版本后,需要其他所有人手动用远程master分支覆盖本地master分支,显然,这不是优雅的回退方法。 - -```bash -git revert HEAD //撤销最近一次提交 -git revert HEAD~1 //撤销上上次的提交,注意:数字从0开始 -git revert 0ffaacc //撤销0ffaacc这次提交 -``` -git revert 命令意思是撤销某次提交。它会产生一个新的提交,虽然代码回退了,但是版本依然是向前的,所以,当你用revert回退之后,所有人pull之后,他们的代码也自动的回退了。但是,要注意以下几点: - -- revert 是撤销一次提交,所以后面的commit id是你需要回滚到的版本的前一次提交 -- 使用revert HEAD是撤销最近的一次提交,如果你最近一次提交是用revert命令产生的,那么你再执行一次,就相当于撤销了上次的撤销操作,换句话说,你连续执行两次revert HEAD命令,就跟没执行是一样的 -- 使用revert HEAD~1 表示撤销最近2次提交,这个数字是从0开始的,如果你之前撤销过产生了commi id,那么也会计算在内的 -- 如果使用 revert 撤销的不是最近一次提交,那么一定会有代码冲突,需要你合并代码,合并代码只需要把当前的代码全部去掉,保留之前版本的代码就可以了 -- git revert 命令的好处就是不会丢掉别人的提交,即使你撤销后覆盖了别人的提交,他更新代码后,可以在本地用 reset 向前回滚,找到自己的代码,然后拉一下分支,再回来合并上去就可以找回被你覆盖的提交了。 - -# 4 revert 合并代码,解决冲突 -使用revert命令,如果不是撤销的最近一次提交,那么一定会有冲突,如下所示: - -```bash -<<<<<<< HEAD -全部清空 -第一次提交 -======= -全部清空 ->>>>>>> parent of c24cde7... 全部清空 -``` -解决冲突很简单,因为我们只想回到某次提交,因此需要把当前最新的代码去掉即可,也就是HEAD标记的代码: - -```bash -<<<<<<< HEAD -全部清空 -第一次提交 -======= -``` -把上面部分代码去掉就可以了,然后再提交一次代码就可以解决冲突了。 \ No newline at end of file diff --git "a/Go/Go-\350\257\255\350\250\200\345\256\236\346\210\230(\344\270\200)-\344\273\213\347\273\215.md" "b/Go/Go-\350\257\255\350\250\200\345\256\236\346\210\230(\344\270\200)-\344\273\213\347\273\215.md" new file mode 100644 index 0000000000..753a7bc490 --- /dev/null +++ "b/Go/Go-\350\257\255\350\250\200\345\256\236\346\210\230(\344\270\200)-\344\273\213\347\273\215.md" @@ -0,0 +1,39 @@ +# 1 导读 +![](https://upload-images.jianshu.io/upload_images/4685968-8067e131b74cf883.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-05849479768d9a59.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-a94edab973aa9a90.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-5646f82218592cee.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-a9a6d876127e4602.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-5e4078206028a1ba.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-4d073cfef6322d14.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-a7b692cf53c4b34a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-830265d690b8bc04.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-1b2ed4e4ee24d0be.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-f6ce781b6a033ef9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-e64d8a1c7a7105d1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-0cd7ec2892ae20b2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-31a8ec633fce04f5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-06d96554e4264a78.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + ![](https://upload-images.jianshu.io/upload_images/4685968-a4183e137d911e18.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-da6ffcadddb67c63.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 2 安装与环境 +- 官网 +https://golang.org/ +![](https://upload-images.jianshu.io/upload_images/4685968-ba6f181f533c7958.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 中文官网 +https://studygolang.com/ +![](https://upload-images.jianshu.io/upload_images/4685968-a749c9fa21a7d644.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-48407483cfcb14f9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-18d11b08613794a0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +# X 交流学习 +![](https://upload-images.jianshu.io/upload_images/16782311-8d7acde57fdce062.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## [Java交流群](https://jq.qq.com/?_wv=1027&k=5UB4P1T) + + +## [博客](https://blog.csdn.net/qq_33589510) + + + +## [Github](https://github.com/Wasabi1234) diff --git "a/Go/Go\345\256\236\346\210\230\344\272\221\345\255\230\345\202\250(\344\270\211) - \346\214\201\344\271\205\345\214\226\344\272\221\346\226\207\344\273\266\344\277\241\346\201\257.md" "b/Go/Go\345\256\236\346\210\230\344\272\221\345\255\230\345\202\250(\344\270\211) - \346\214\201\344\271\205\345\214\226\344\272\221\346\226\207\344\273\266\344\277\241\346\201\257.md" new file mode 100644 index 0000000000..f664fcc153 --- /dev/null +++ "b/Go/Go\345\256\236\346\210\230\344\272\221\345\255\230\345\202\250(\344\270\211) - \346\214\201\344\271\205\345\214\226\344\272\221\346\226\207\344\273\266\344\277\241\346\201\257.md" @@ -0,0 +1,71 @@ +# 0 [相关源码](https://github.com/Wasabi1234/Go-Cloud-Store) + +# 1 MySQL基础 +## 简介 +为避免每次重启系统从零开始的问题,需要将数据持久化. +![](https://upload-images.jianshu.io/upload_images/16782311-d59a5f2a87c4a342.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## 服务架构变迁 +![](https://upload-images.jianshu.io/upload_images/16782311-7888a1be823355ef.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## 安装配置 +### 安装模式 +- 单点 +- 主从 + +![](https://upload-images.jianshu.io/upload_images/16782311-a588e4e352f3b543.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 多主 + +# 2 文件表设计 +- 新建数据库 +![](https://upload-images.jianshu.io/upload_images/16782311-93d994cc266e86d3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 新建文件表 +![](https://upload-images.jianshu.io/upload_images/16782311-03abc93755e61f19.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## 分库分表 +### 水平分表 +![](https://upload-images.jianshu.io/upload_images/16782311-1cd30450d73cbafa.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/16782311-1dcc2a59e1813009.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## Golang 操作MySQL +![](https://upload-images.jianshu.io/upload_images/16782311-e96ba8777c17421c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +# 4 持久化元数据到文件表 +![](https://upload-images.jianshu.io/upload_images/16782311-c495ccb60d234cd8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +![](https://upload-images.jianshu.io/upload_images/16782311-86b0de25058fd99a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +# 5 从文件表中获取元数据 +- file.go +![](https://upload-images.jianshu.io/upload_images/16782311-df37fab7de1abb14.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- handler.go +![](https://upload-images.jianshu.io/upload_images/16782311-f716d1c342913724.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- filemeta.go +![](https://upload-images.jianshu.io/upload_images/16782311-a58f2e525809ead1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- test over! +![](https://upload-images.jianshu.io/upload_images/16782311-2dd492a053782a40.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +# 6 Go - MySQL使用小结 +## 通过sqI.DB来管理数据库连接对象 +## 通过sql.Open来创建协程安全的sql.DB对象 +## 优先使用Prepared Statement + +### 1 MySQL特点与应用场景 +### 2 主从架构与文件表设计逻辑 +### 3 Golang与MySQL的亲密接触 + +# X 交流学习 +![](https://upload-images.jianshu.io/upload_images/16782311-8d7acde57fdce062.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## [Java交流群](https://jq.qq.com/?_wv=1027&k=5UB4P1T) + + +## [博客](https://blog.csdn.net/qq_33589510) + + + +## [Github](https://github.com/Wasabi1234) + + diff --git "a/Go/Go\345\256\236\346\210\230\344\272\221\345\255\230\345\202\250(\345\233\233) - \350\264\246\345\217\267\347\263\273\347\273\237\344\270\216\351\211\264\346\235\203.md" "b/Go/Go\345\256\236\346\210\230\344\272\221\345\255\230\345\202\250(\345\233\233) - \350\264\246\345\217\267\347\263\273\347\273\237\344\270\216\351\211\264\346\235\203.md" new file mode 100644 index 0000000000..b55cb016fc --- /dev/null +++ "b/Go/Go\345\256\236\346\210\230\344\272\221\345\255\230\345\202\250(\345\233\233) - \350\264\246\345\217\267\347\263\273\347\273\237\344\270\216\351\211\264\346\235\203.md" @@ -0,0 +1,63 @@ +# 0 [相关源码](https://github.com/Wasabi1234/Go-Cloud-Store) +修改代码部分直接查看github commit历史即可! + +加入用户系统后架构升级说明; 快速实现用户注册/登录/信息查询功能; 快速实现用户资源隔离存储及安全鉴权功能。 + +# 1 帐号系统介绍与用户表设计 + +## 1.1 账号系统的功能 +支持用户注册/登录 +支持用户Session鉴权 +用户数据资源隔离 + +![在系统架构中的地位](https://upload-images.jianshu.io/upload_images/16782311-2392dcd62cfa1819.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## 1.2 MySQL用户表的设计 +![](https://upload-images.jianshu.io/upload_images/16782311-228a0c7eca1e1093.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + + +# 2 实现用户注册接口 +- 注册页面 +![](https://upload-images.jianshu.io/upload_images/16782311-cdd0eba9d8ff9d87.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 成功入库 +![](https://upload-images.jianshu.io/upload_images/16782311-4c0e6e59e6c0c98e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +# 3 实现用户登录接口 +![](https://upload-images.jianshu.io/upload_images/16782311-bbe121950f743239.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 添加路由规则,注意静态路由 +![](https://upload-images.jianshu.io/upload_images/16782311-40dc32688dcdb765.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +- 登录界面 +![](https://upload-images.jianshu.io/upload_images/16782311-29e162e74da73cde.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +# 4 实现用户信息查询接口 +除了注册登录,其他接口都需要验证token信息 + +- 登录后的重定向界面 +![](https://upload-images.jianshu.io/upload_images/16782311-838a12d892b7c542.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +# 5 接口梳理小结 +## 用户注册 +![](https://upload-images.jianshu.io/upload_images/16782311-71e82a1ae3a10986.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## 用户登录 +![](https://upload-images.jianshu.io/upload_images/16782311-c473e82fd21b0783.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## 用户信息查询 +![](https://upload-images.jianshu.io/upload_images/16782311-53f80ac192de5dc7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## 拦截验证token +业务接口需要验证请求合法性 +![](https://upload-images.jianshu.io/upload_images/16782311-b2a446b19d8fae0c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +# 6 编码实战:快速实现访问鉴权接口+小结 + +## 6.2 小结 +### MySQL 用户表的设计 +### 注册/登录/查询接口的实现 +### 验证Token的拦截器 + diff --git "a/Go/Go\350\257\255\350\250\200\345\256\236\346\210\230(\344\270\211)---\345\206\205\345\273\272\345\256\271\345\231\250.md" "b/Go/Go\350\257\255\350\250\200\345\256\236\346\210\230(\344\270\211)---\345\206\205\345\273\272\345\256\271\345\231\250.md" new file mode 100644 index 0000000000..e0b5d9e31f --- /dev/null +++ "b/Go/Go\350\257\255\350\250\200\345\256\236\346\210\230(\344\270\211)---\345\206\205\345\273\272\345\256\271\345\231\250.md" @@ -0,0 +1,90 @@ +# 1 数组 +数组是单一类型元素的编号序列,该单一类型称为元素类型。元素的数量称为长度且为非负数。 + +>数组类型 = "[" 数组长度 "]" 元素类型 . +数组长度 = 表达式 . +元素类型 = 类型 . + +长度是数组类型的一部分,其求值结果必须可表示为 `int` 类型的非负 +数组 `a` 的长度可使用内建函数 [`len`]获取, 其元素可通过整数[下标]0 到 `len(a)-1` 寻址 + 数组类型总是一维的,但可组合构成多维的类型。 +``` +[32]byte +[2*N] struct { x, y int32 } +[1000]*float64 +[3][5]int +[2][2][2]float64 // 等价于[2]([2]([2]float64)) +``` +## 构造数组 +![](https://upload-images.jianshu.io/upload_images/4685968-21b6511cb0771db4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![运行结果](https://upload-images.jianshu.io/upload_images/4685968-c93e33633eec5e22.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## 遍历 +- 首先想到这样 +![](https://upload-images.jianshu.io/upload_images/4685968-c59b16f93675b8a1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 但是通常使用 range关键字遍历 +![](https://upload-images.jianshu.io/upload_images/4685968-30c512bd06035fb5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-621aaf1e5fa8cdb3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 为什么要用 range +![](https://upload-images.jianshu.io/upload_images/4685968-8eae12986ac725c9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## 数组是值类型 +![](https://upload-images.jianshu.io/upload_images/4685968-2befddde7ce6ce8c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-f8b18c079a3e1e52.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![运行结果](https://upload-images.jianshu.io/upload_images/4685968-a2a3ac6f37c0b7b4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-f137f07f12479533.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +要想传递引用可修改数组,可传指针参数 +![](https://upload-images.jianshu.io/upload_images/4685968-3130ca70a71a94d7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 2 切片 (Slice) 的概念 +切片是数组连续段的引用及包含此数组的元素的编号序列。 切片类型表示元素类型为数组的所有切片的集。未初始化切片的值为 nil。 + +>切片类型 = "[" "]" 元素类型 . + +类似于数组,切片是可索引的且拥有一个长度 +切片 `s` 的长度可通过内建函数 [`len`]获取;不同于数组的是,切片可`在执行过程中被改变`, 其元素可通过整数(§[下标]) 0 到 `len(s)-1` 寻址。 给定元素的切片下标可能小于它在其基本数组中的下标。 + +切片一旦初始化,就总是伴随着一个包含其元素的基本数组。 +因此,切片与其数组及其它本数组的切片共享存储 + 与此相反,不同的数组总是表示其不同的存储 + +切片的基本数组可扩展其切片的结尾 +- **容量** 是该扩展的量度 +它是切片的长度和切片往后数组的长度之和;长度达到其容量的切片可通过从原切片 (§[Slices](http://docscn.studygolang.com/ref/spec.old#Slices))‘切下’一个新的来创建。 切片 `a` 的容量可使用内建函数 [`cap(a)`](http://docscn.studygolang.com/ref/spec.old#%E9%95%BF%E5%BA%A6%E4%B8%8E%E5%AE%B9%E9%87%8F) 获取。 + +给定元素类型 `T` 的一个新的,已初始化的切片值使用内建函数 [`make`](http://docscn.studygolang.com/ref/spec.old#%E5%88%9B%E5%BB%BA%E5%88%87%E7%89%87%E3%80%81%E6%98%A0%E5%B0%84%E4%B8%8E%E4%BF%A1%E9%81%93)创建, 它需要一个切片类型和指定其长度与可选容量的形参: +``` +make([]T, length) +make([]T, length, capacity) +``` +调用 make将分配一个被返回的切片值所引用的,新的、隐藏的数组。即,执行 +``` +make([]T, length, capacity) +``` +产生切片与分配数组后再对其进行切片相同,因此这两个例子的结果为相同的切片: +``` +make([]int, 50, 100) +new([100]int)[0:50] +``` +类似于数组,切片总是一维的,但可组合构造更高维的对象 +元素为数组的数组,根据其构造,其内部数组的长度始终相同 +然而元素为切片的切片(或元素为数组的切片),其长度会动态地改变 +此外,其内部的切片必须单独地(通过 make)分配 + + +## 基本定义 +![](https://upload-images.jianshu.io/upload_images/4685968-ead1ce9561545a4a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-e41c5f608c014cc9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![运行结果](https://upload-images.jianshu.io/upload_images/4685968-12c9801aca625402.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## 运行可修改 +![](https://upload-images.jianshu.io/upload_images/4685968-2c896e39b6aea368.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +# X 交流学习 +![](https://upload-images.jianshu.io/upload_images/16782311-8d7acde57fdce062.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## [Java交流群](https://jq.qq.com/?_wv=1027&k=5UB4P1T) + + +## [博客](https://blog.csdn.net/qq_33589510) + + + +## [Github](https://github.com/Wasabi1234) + diff --git "a/Go/Go\350\257\255\350\250\200\345\256\236\346\210\230(\344\272\214)---\345\237\272\347\241\200\350\257\255\346\263\225.md" "b/Go/Go\350\257\255\350\250\200\345\256\236\346\210\230(\344\272\214)---\345\237\272\347\241\200\350\257\255\346\263\225.md" new file mode 100644 index 0000000000..be174d2255 --- /dev/null +++ "b/Go/Go\350\257\255\350\250\200\345\256\236\346\210\230(\344\272\214)---\345\237\272\347\241\200\350\257\255\346\263\225.md" @@ -0,0 +1,130 @@ +# 1 变量定义 +![](https://upload-images.jianshu.io/upload_images/4685968-dcc545a0c25312cd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +var 语句定义了一个变量的列表;跟函数的参数列表一样,类型在后面。 +就像在这个例子中看到的一样,`var` 语句可以定义在包或函数级别。 + +![](https://upload-images.jianshu.io/upload_images/4685968-c761b0b8887cb33b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-24cbfe3978b0a256.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +变量一旦定义了,就必须使用到,否则报错 +![](https://upload-images.jianshu.io/upload_images/4685968-d9849076af8bd39e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +根据变量内容即可判断变量类型,无须再显式声明 +![](https://upload-images.jianshu.io/upload_images/4685968-c033e5c0f088b243.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-49d94f156dd5b46e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-399b556331d40c82.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 总结 +![](https://upload-images.jianshu.io/upload_images/4685968-159e6852758c709a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-d1adadf8cea48117.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 2 内置变量类型 +![](https://upload-images.jianshu.io/upload_images/4685968-ab2098c4512d04c3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## 复数机制 +![](https://upload-images.jianshu.io/upload_images/4685968-a3af538c25f3a461.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +验证欧拉公式 +![](https://upload-images.jianshu.io/upload_images/4685968-e316fa9f439a6702.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## 强制类型转换 +![sqrt源码](https://upload-images.jianshu.io/upload_images/4685968-eb4bae006a5dee01.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +传参 int 直接报错 +![](https://upload-images.jianshu.io/upload_images/4685968-01e1a1da01217af9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +因为没有隐式转换,只有显式的强制类型转换 +![](https://upload-images.jianshu.io/upload_images/4685968-c22fda2c24ab0e0d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-82cd955ce6fe0838.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 3 常量与枚举 +![](https://upload-images.jianshu.io/upload_images/4685968-8de62a5eb0bf7618.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![常量的定义](https://upload-images.jianshu.io/upload_images/4685968-793b802cb1aeb54d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-f5bd95e7786dbed5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +使用 iota 在 const 块定义中实现自增值 +![iota = 0](https://upload-images.jianshu.io/upload_images/4685968-727bc25d5d409b51.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-26d00599bd2cab78.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![内存进制大小数值](https://upload-images.jianshu.io/upload_images/4685968-d052ac5984efae3f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![使用常量定义枚举类型](https://upload-images.jianshu.io/upload_images/4685968-61754fdce22e95a2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## 常量定义要点回顾 +![](https://upload-images.jianshu.io/upload_images/4685968-38af7e8d959d8bcf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 4 条件语句 +## if +"If"语句根据一个布尔表达式的值指定两个分支的条件来执行。 若该表达式求值为true,则执行"if"分支,否则执行"else"分支 +``` +If语句 = "if" [ 简单语句 ";" ] 表达式 块 [ "else" ( If语句 | 块 ) ] . +``` +``` +if x > max { + x = max +} +``` +![](https://upload-images.jianshu.io/upload_images/4685968-1030048192aa8c60.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +工具方法,该方法有两个返回值哦! +![](https://upload-images.jianshu.io/upload_images/4685968-d455cae29784ba11.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![文件位置](https://upload-images.jianshu.io/upload_images/4685968-0f10aeb7c16461b1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-b3966cbf6a39a7dc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-023df8ffbbbdd0a6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## switch +"Switch"语句提供多路执行。表达式或类型说明符与"switch"中的"cases"相比较从而决定执行哪一分支。 +``` +Switch语句 = 表达式选择语句 | 类型选择语句 +``` +![](https://upload-images.jianshu.io/upload_images/4685968-43a05a5d91b99c5b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-c1f3b76493c2ba18.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-aa6666c6a42f0616.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-84520d3418ef0700.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-34463d922ce507b7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## 循环 +![](https://upload-images.jianshu.io/upload_images/4685968-afd40ac5b52b975d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-3994aac2770e4490.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-0a56457977716208.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-ead89acd1f82240d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-12979092e66a0dc5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-4c16122e1ce2a270.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-db220303bd7260ea.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-52430de72c205b31.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-28865bac63f52d6b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 5 函数 +- 参数 参数类型, +![](https://upload-images.jianshu.io/upload_images/4685968-7ede8751dc54c5fc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-341698877c01a08c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- Go的函数可以返回多个值 +![多返回值](https://upload-images.jianshu.io/upload_images/4685968-822efa5637e6f0b3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 若参数过多,这样并不是一种清晰的写法 +![](https://upload-images.jianshu.io/upload_images/4685968-fdd54a3889db81c3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +建议如下最佳实践! +![](https://upload-images.jianshu.io/upload_images/4685968-704fe54da0cece27.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 多返回值函数,只接收一个返回值 +![](https://upload-images.jianshu.io/upload_images/4685968-c582a9c2b3fb2933.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 对于多返回值参数,一般可用于返回`值 + error` +![](https://upload-images.jianshu.io/upload_images/4685968-c850ba0c797cd2cd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +比如这样,控制台输出就很难看,因为error直接程序中断了 +![](https://upload-images.jianshu.io/upload_images/4685968-88ec2aed632194da.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-bf666abd6bf34d57.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-d3e2f0d88b15b07a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-c5d32e7246e69988.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +- 亦可写成匿名函数 +![](https://upload-images.jianshu.io/upload_images/4685968-212c829f3b098030.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-50f02de0b6e82784.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +无默认参数,有可变参数,可变参数类型其实是[] type 类型 +![](https://upload-images.jianshu.io/upload_images/4685968-98976a75ee0cbe25.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-91c8fb4da3fff9f0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-f516bfbbed050cf0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +# 6 指针 +![](https://upload-images.jianshu.io/upload_images/4685968-48f45af0ade89920.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +## 6.1 参数传递 +![](https://upload-images.jianshu.io/upload_images/4685968-fae13000a0e0e46d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-cbdaefbf665f5318.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-433a6d23da7d5e90.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![1](https://upload-images.jianshu.io/upload_images/4685968-878f29183b7cc5f2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![2](https://upload-images.jianshu.io/upload_images/4685968-37024340d43a58eb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![3](https://upload-images.jianshu.io/upload_images/4685968-e03a16755081e3f5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-26869ed2467509bd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![](https://upload-images.jianshu.io/upload_images/4685968-c7b7a157d43769e0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +当然啦,还有最简单的 +![](https://upload-images.jianshu.io/upload_images/4685968-b0283ef0cae97b30.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + + +# X 交流学习 +![](https://upload-images.jianshu.io/upload_images/16782311-8d7acde57fdce062.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +## [Java交流群](https://jq.qq.com/?_wv=1027&k=5UB4P1T) + + +## [博客](https://blog.csdn.net/qq_33589510) + + + +## [Github](https://github.com/Wasabi1234) + diff --git "a/Go/Go\350\257\255\350\250\200\347\232\204\351\235\242\345\220\221\345\257\271\350\261\241.md" "b/Go/Go\350\257\255\350\250\200\347\232\204\351\235\242\345\220\221\345\257\271\350\261\241.md" new file mode 100644 index 0000000000..05a4e56b40 --- /dev/null +++ "b/Go/Go\350\257\255\350\250\200\347\232\204\351\235\242\345\220\221\345\257\271\350\261\241.md" @@ -0,0 +1,33 @@ +go语言仅支持封装,不支持继承和多态。 +go语言没有class,只有struct。 + +# 结构的定义 +![](https://img-blog.csdnimg.cn/20201223155119118.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +- go 语言即使是指针,不像 C语言使用`->`也可一直使用`.`引用下去![](https://img-blog.csdnimg.cn/20201223192039227.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +- go 没有构造器,但可使用工厂函数 +![](https://img-blog.csdnimg.cn/20201223193155572.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +那么这到底创建在了堆还是栈呢? +不需要知道。如果返回了没人用就在栈上 +如果返回了有人用,就在堆,并参与到 GC。 +所以没必要知道,编译器自己都知道。 + +# 参数前后区别 +- 参数定义在函数名前后有啥区别呢? +![](https://img-blog.csdnimg.cn/20201223194005570.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +- 使用区别 +![](https://img-blog.csdnimg.cn/20201223194043874.png) +Go都是值传递,记住了,和 Java 一样。 + +# 使用指针作为方法接收者 +![](https://img-blog.csdnimg.cn/20201223200025882.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) +- 只有使用指针才可以改变结构内容 +- nil 指针也可调用方法(Go 很特殊) + +# 值接收者 V.S 指针接收者 +- 要改变内容必须使用指针接收者 +- 结构过大也考虑使用指针接收者 +- 一致性:如有指针接收者,最好都是指针接收者 + + +值接收者才是go语言特有的! +值/指针接收者均可接收值/指针 \ No newline at end of file diff --git "a/Go/\360\237\247\247\347\263\273\347\273\237/Go\345\256\236\346\210\230\360\237\247\247\347\263\273\347\273\237(\344\270\211) - \346\236\266\346\236\204\350\256\276\350\256\241.md" "b/Go/\360\237\247\247\347\263\273\347\273\237/Go\345\256\236\346\210\230\360\237\247\247\347\263\273\347\273\237(\344\270\211) - \346\236\266\346\236\204\350\256\276\350\256\241.md" new file mode 100644 index 0000000000..32bb0cbda8 --- /dev/null +++ "b/Go/\360\237\247\247\347\263\273\347\273\237/Go\345\256\236\346\210\230\360\237\247\247\347\263\273\347\273\237(\344\270\211) - \346\236\266\346\236\204\350\256\276\350\256\241.md" @@ -0,0 +1,226 @@ +# 1 代码架构的意义 +![](https://img-blog.csdnimg.cn/20190621180825370.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +代码架构就是详细设计中的核心内容! + +## 1.1 代码架构承上启下,决定软件质量 +◆ 承上 +说明业务逻辑和业务领域模型 +◆ 本身 +保证代码有更好的可读性和可维护性、可扩展性 +◆ 启下 +承载代码运行的硬件部署架构 + +# 2 代码架构的操作 +## 2.1 业务逻辑表达 +向上沟通,提供交互入口 +## 2.2 自身业务逻辑及技术实现 +向下沟通,保存运行状态 + +# 3 代码架构的设计 + +先看一下DDD和分层架构的相关知识。 +## 3.1 DDD +DDD(Domain Driven Design,领域驱动设计)作为一种软件开发方法,它可以帮助我们设计高质量的软件模型。在正确实现的情况下,我们通过DDD完成的设计恰恰就是软件的工作方式。 + +UL(Ubiquitous Language,通用语言)是团队共享的语言,是DDD中最具威力的特性之一。不管你在团队中的角色如何,只要你是团队的一员,你都将使用UL。由于UL的重要性,所以需要让每个概念在各自的上下文中是清晰无歧义的,于是DDD在战略设计上提出了模式BC(Bounded Context,限界上下文)。UL和BC同时构成了DDD的两大支柱,并且它们是相辅相成的,即UL都有其确定的上下文含义,而BC中的每个概念都有唯一的含义。 + +一个业务领域划分成若干个BC,它们之间通过Context Map进行集成。BC是一个显式的边界,领域模型便存在于这个边界之内。领域模型是关于某个特定业务领域的软件模型。通常,领域模型通过对象模型来实现,这些对象同时包含了数据和行为,并且表达了准确的业务含义。 + +从广义上来讲,领域即是一个组织所做的事情以及其中所包含的一切,表示整个业务系统。 +由于“领域模型”包含了“领域”这个词,我们可能会认为应该为整个业务系统创建一个单一的、内聚的和全功能式的模型。然而,这并不是我们使用DDD的目标。正好相反,领域模型存在于BC内。 +在微服务架构实践中,人们大量地使用了DDD中的概念和技术: + +微服务中应该首先建立UL,然后再讨论领域模型。 +一个微服务最大不要超过一个BC,否则微服务内会存在有歧义的领域概念。 +一个微服务最小不要小于一个聚合,否则会引入分布式事务的复杂度。 +微服务的划分过程类似于BC的划分过程,每个微服务都有一个领域模型。 +微服务间的集成可以通过Context Map来完成,比如ACL(Anticorruption Layer,防腐层)。 +微服务间最好采用Domain Event(领域事件)来进行交互,使得微服务可以保持松耦合。 +. + +下面介绍最为流行的分层代码架构 +## 3.1 分层架构简介 + + +分层架构的一个重要原则是每层只能与位于其下方的层发生耦合。 +分层架构可以简单分为两种 +- 严格分层架构 +某层只能与位于其直接下方的层发生耦合 +- 松散分层架构 +松散分层架构中,则允许某层与它的任意下方层发生耦合。 + +分层架构的好处是显而易见的。首先,由于层间松散的耦合关系,使得我们可以专注于本层的设计,而不必关心其他层的设计,也不必担心自己的设计会影响其它层,对提高软件质量大有裨益。其次,分层架构使得程序结构清晰,升级和维护都变得十分容易,更改某层的具体实现代码,只要本层的接口保持稳定,其他层可以不必修改。即使本层的接口发生变化,也只影响相邻的上层,修改工作量小且错误可以控制,不会带来意外的风险。 +要保持程序分层架构的优点,就必须坚持层间的松散耦合关系。设计程序时,应先划分出可能的层次,以及此层次提供的接口和需要的接口。设计某层时,应尽量保持层间的隔离,仅使用下层提供的接口。 + +## 3.2 分层架构的优点 +单一职责 +高内聚低耦合 +提高复用性 + +开发人员可以只关注整个结构中的某一层。 +可以很容易的用新的实现来替换原有层次的实现。 +可以降低层与层之间的依赖。 +有利于标准化。 +利于各层逻辑的复用。 + + +## 3.3 分层架构的缺陷 +“金无足赤,人无完人”,分层架构也不可避免具有一些缺陷: + +降低了系统的性能。这是显然的,因为增加了中间层,不过可以通过缓存机制来改善。 +可能会导致级联的修改。这种修改尤其体现在自上而下的方向,不过可以通过依赖倒置来改善。 + +在每个BC中为了凸显领域模型,DDD中提出了分层架构模式 + +最基本的为三层架构 +## 3.4 三层架构 +表现层 +业务逻辑层 +数据持久层 + +但是职责定义并不明确,耦合度高 + +所以我们项目使用四层逻辑分层架构 +## 3.5 逻辑分层架构 +![](https://img-blog.csdnimg.cn/20190621235019157.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +- User Interface - 用户接口 +人机交互,为用户界面层(或表示层),负责向用户显示信息和解释用户命令。这里指的用户可以是另一个计算机系统,不一定是使用用户界面的人。 + +- Application - 应用层 +定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其它系统的应用层进行交互的必要渠道。应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使它们互相协作。它没有反映业务情况的状态,但是却可以具有另外一种状态,为用户或程序显示某个任务的进度。 + +- Domain - 领域层(或模型层) +表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节是由基础设施层实现的,但是反映业务情况的状态是由本层控制并且使用的。领域层是业务软件的核心,领域模型位于这一层。 + +- Infrastructure - 基础实施层 +向其他层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件,等等。基础设施层还能够通过架构框架来支持四个层次间的交互模式。 + + +传统的四层架构都是限定型松散分层架构,即Infrastructure层的任意上层都可以访问该层(“L”型),而其它层遵守严格分层架构 + +在四层架构模式的实践中,对于分层的本地化定义主要为: +- User Interface层主要是Restful消息处理,配置文件解析,等等。 +- Application层主要是多进程管理及调度,多线程管理及调度,多协程调度和状态机管理,等等。 +- Domain层主要是领域模型的实现,包括领域对象的确立,这些对象的生命周期管理及关系,领域服务的定义,领域事件的发布,等等。 +- Infrastructure层主要是业务平台,编程框架,第三方库的封装,基础算法,等等。 + +> 严格意义上来说,User Interface指的是用户界面,Restful消息和配置文件解析等处理应该放在Application层,User Interface层没有的话就空缺。但User Interface也可以理解为用户接口,所以将Restful消息和配置文件解析等处理放在User Interface层也行。 + +## 3.6 物理分层 +- 魔改四层的六层架构 +![](https://img-blog.csdnimg.cn/2019062201340683.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +◆ 用户接口 +◆ 应用服务层接口: +◆ 核心层 +- 应用服务实现层 +- 领域层 +- 数据访问层 + +◆ 基础设施层 + + +- 架构设计图 +![](https://img-blog.csdnimg.cn/20190622013814347.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +# 4 Go 语言规范 +## 4.1 包名 +◆ 完整包名组成:引入路径+包名 +◆ 源代码中的包名称 +- 可以和文件夹名称不一致,建议尽量一致 +- 同一文件夹中所有源文件中的包名必须一致 +◆ 代码引用时使用包名,而非文件夹名称 +◆ 源代码导入的是文件夹路径名称 +- 非包名 +- 非文件名 +## 4.2 源代码文件名 +◆ 文件名称只是约定描述性的,并无任何编程含义 + + +# 5 🧧系统 - 代码结构 +![](https://img-blog.csdnimg.cn/20190622101718233.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +![](https://img-blog.csdnimg.cn/20190622102431607.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +# 6 🧧系统 - 包结构 +![](https://img-blog.csdnimg.cn/20190622103131713.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +# 7 包设计规范 +## 7.1 apis包 - 用户接口层 +◆ 文件名称可以描述其业务含义的单词 +◆ 定义外部交互逻辑和交互形式: UI、RESTful接口 +◆ 不涉及任何业务,随时可以替换为其他形式的交互方式 +◆ services构造和初始化 + +## 7.2 services包 - 应用层接口 +◆ 文件名称使用可以描述其业务含义 +◆ 需要对外暴露 +- DTO、 service interface +- 枚举、常数等 + +## 7.3 core包 - 应用层/领域层/数据访问层 +◆ 文件名称使用可以描述 +业务含义 +分层名称 +◆ Service实现 +Domain、Dao、 PO + +# 8 Go的包管理 +## 8.1 历史 +go get => vendor => go modules +◆ go get无版本概念 +◆ vendor曲线救国,但仍未版本化 +◆ go1.11 modules开启版本依赖新大门 + +> 详细过程推荐阅读 +[Go 包管理的前世今生](https://www.infoq.cn/article/history-go-package-management) + +## 8.2 Go modules +通过GO 1.11 MODULE环境变量来开启或者关闭,默认是auto + +◆ off/on/auto +关闭,开启,自动识别 + +◆ 使用module后,GOPATH失去了部分意义 + +◆ 要用module ,第一步将项目从GOPATH中移出去 + +## 8.3 go.mod 文件 +go.mod文件来管理依赖,定义模块依赖 +◆ go.mod文件放在项目根目录 +◆ go.mod文件面向行,由指令+参数组成 +◆ 注释使用// + +### 8.3.1 go.mod 主要指令 +◆ module:定义当前模块和包路径 +◆ require: 定义依赖的模块和版本 +◆ exclude: 排除特定模块和版本的使用 +◆ replace:模块源的替换 + +### 8.3.2 go.mod 命令 +go.mod文件用go mod命令来创建和维护 + +◆ 命令格式 +``` +go mod <命令> [可选参数] +``` +◆ 8个子命令 +- init ,tidy,vendor,verify +- download,edit,graph ,why + +### 8.3.3 实战演示 +#### 8.3.3.1 使用go mod init 创建和初始化go.mod文件 +``` + go mod init javaedge.com/GoDemo +``` +![](https://img-blog.csdnimg.cn/20190622113300232.png) +- 生成文件 +![](https://img-blog.csdnimg.cn/20190622113357360.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) + +#### 8.3.3.2 go get引入依赖 +![](https://img-blog.csdnimg.cn/20190622200239750.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) +#### 8.3.3.3 tidy子命令更新模块依赖 +对于已存在项目进行module化,即可使用该命令 +会自动添加依赖的包,使用go build更新依赖 +# 参考 +[DDD分层架构的三种模式](https://www.jianshu.com/p/a775836c7e25) diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/AbstractQueuedSynchronizer\345\216\237\347\220\206\350\247\243\346\236\220.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/AbstractQueuedSynchronizer\345\216\237\347\220\206\350\247\243\346\236\220.md" deleted file mode 100644 index 75c4862c41..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/AbstractQueuedSynchronizer\345\216\237\347\220\206\350\247\243\346\236\220.md" +++ /dev/null @@ -1,448 +0,0 @@ -AbstractQueuedSynchronizer 抽象同步队列简称 AQS ,它是实现同步器的基础组件, -并发包中锁的底层就是使用 AQS 实现的. -大多数开发者可能永远不会直接使用AQS ,但是知道其原理对于架构设计还是很有帮助的,而且要理解ReentrantLock、CountDownLatch等高级锁我们必须搞懂 AQS. - -# 1 整体感知 -## 1.1 架构图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMjA3XzIwMjAwMjA5MTk1MDI3NjQ4LnBuZw?x-oss-process=image/format,png) -AQS框架大致分为五层,自上而下由浅入深,从AQS对外暴露的API到底层基础数据. - - -当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。当自定义同步器进行加锁或者解锁操作时,先经过第一层的API进入AQS内部方法,然后经过第二层进行锁的获取,接着对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层。 - -AQS 本身就是一套锁的框架,它定义了获得锁和释放锁的代码结构,所以如果要新建锁,只要继承 AQS,并实现相应方法即可。 - -## 1.2 类设计 -该类提供了一种框架,用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关的同步器(信号量,事件等)。此类的设计旨在为大多数依赖单个原子int值表示 state 的同步器提供切实有用的基础。子类必须定义更改此 state 的 protected 方法,并定义该 state 对于 acquired 或 released 此对象而言意味着什么。鉴于这些,此类中的其他方法将执行全局的排队和阻塞机制。子类可以维护其他状态字段,但是就同步而言,仅跟踪使用方法 *getState*,*setState* 和 *compareAndSetState* 操作的原子更新的int值。 -子类应定义为用于实现其所在类的同步属性的非公共内部帮助器类。 - -子类应定义为用于实现其所在类的同步属性的非 public 内部辅助类。类AbstractQueuedSynchronizer不实现任何同步接口。 相反,它定义了诸如*acquireInterruptible*之类的方法,可以通过具体的锁和相关的同步器适当地调用这些方法来实现其 public 方法。 - -此类支持默认的排他模式和共享模式: -- 当以独占方式进行获取时,其他线程尝试进行的获取将无法成功 -- 由多个线程获取的共享模式可能(但不一定)成功 - -该类不理解这些差异,只是从机制的意义上说,当共享模式获取成功时,下一个等待线程(如果存在)也必须确定它是否也可以获取。在不同模式下等待的线程们共享相同的FIFO队列。 通常,实现的子类仅支持这些模式之一,但也可以同时出现,比如在ReadWriteLock.仅支持排他模式或共享模式的子类无需定义支持未使用模式的方法. - -此类定义了一个内嵌的 **ConditionObject** 类,可由支持独占模式的子类用作Condition 的实现,该子类的 *isHeldExclusively* 方法报告相对于当前线程是否独占同步,使用当前 *getState* 值调用的方法 *release* 会完全释放此对象 ,并获得给定的此保存状态值,最终将该对象恢复为其先前的获取状态。否则,没有AbstractQueuedSynchronizer方***创建这样的条件,因此,如果无法满足此约束,请不要使用它。ConditionObject的行为当然取决于其同步器实现的语义。 - -此类提供了内部队列的检查,检测和监视方法,以及条件对象的类似方法。 可以根据需要使用 AQS 将它们导出到类中以实现其同步机制。 - -此类的序列化仅存储基础原子整数维护状态,因此反序列化的对象具有空线程队列。 需要序列化性的典型子类将定义一个readObject方法,该方法在反序列化时将其恢复为已知的初始状态。 - -# 2 用法 -要将此类用作同步器的基础,使用*getState* *setState*和/或*compareAndSetState*检查和/或修改同步状态,以重新定义以下方法(如适用) -- tryAcquire -- tryRelease -- tryAcquireShared -- tryReleaseShared -- isHeldExclusively - -默认情况下,这些方法中的每一个都会抛 *UnsupportedOperationException*。 -这些方法的实现必须在内部是线程安全的,并且通常应简短且不阻塞。 定义这些方法是使用此类的**唯一**受支持的方法。 所有其他方法都被声明为final,因为它们不能独立变化。 - -从 AQS 继承的方法对跟踪拥有排他同步器的线程很有用。 鼓励使用它们-这将启用监视和诊断工具,以帮助用户确定哪些线程持有锁。 - -虽然此类基于内部的FIFO队列,它也不会自动执行FIFO获取策略。 独占同步的核心采用以下形式: -- Acquire -```java -while (!tryAcquire(arg)) { - 如果线程尚未入队,则将其加入队列; - 可能阻塞当前线程; -} -``` -- Release - -```java -if (tryRelease(arg)) - 取消阻塞第一个入队的线程; -``` -共享模式与此相似,但可能涉及级联的signal。 - -acquire 中的检查是在入队前被调用,所以新获取的线程可能会在被阻塞和排队的其他线程之前插入。但若需要,可以定义tryAcquire、tryAcquireShared以通过内部调用一或多种检查方法来禁用插入,从而提供公平的FIFO获取顺序。 - -特别是,若 hasQueuedPredecessors()(公平同步器专门设计的一种方法)返回true,则大多数公平同步器都可以定义tryAcquire返回false. - -- 公平与否取决于如下一行代码: -```java -if (c == 0) { - if (!hasQueuedPredecessors() && - compareAndSetState(0, acquires)) { - setExclusiveOwnerThread(current); - return true; - } -} -``` -### hasQueuedPredecessors -```java -public final boolean hasQueuedPredecessors() { - // The correctness of this depends on head being initialized - // before tail and on head.next being accurate if the current - // thread is first in queue. - Node t = tail; // Read fields in reverse initialization order - Node h = head; - // s代表等待队列的第一个节点 - Node s; - // (s = h.next) == null 说明此时有另一个线程正在尝试成为头节点,详见AQS的acquireQueued方法 - // s.thread != Thread.currentThread():此线程不是等待的头节点 - return h != t && - ((s = h.next) == null || s.thread != Thread.currentThread()); -} -``` - - - -对于默认的插入(也称为贪婪,放弃和convoey-avoidance)策略,吞吐量和可伸缩性通常最高。 尽管不能保证这是公平的或避免饥饿,但允许较早排队的线程在较晚排队的线程之前进行重新竞争,并且每个重新争用都有一次机会可以毫无偏向地成功竞争过进入的线程。 -同样,尽管获取通常无需自旋,但在阻塞前,它们可能会执行tryAcquire的多次调用,并插入其他任务。 如果仅短暂地保持排他同步,则这将带来自旋的大部分好处,而如果不进行排他同步,则不会带来很多负担。 如果需要的话,可以通过在调用之前使用“fast-path”检查来获取方法来增强此功能,并可能预先检查*hasContended*()和/或*hasQueuedThreads()*,以便仅在同步器可能不存在争用的情况下这样做。 - -此类为同步提供了有效且可扩展的基础,部分是通过将其使用范围规范化到可以依赖于int状态,acquire 和 release 参数以及内部的FIFO等待队列的同步器。 当这还不够时,可以使用原子类、自定义队列类和锁支持阻塞支持从较低级别构建同步器。 - -# 3 使用案例 -这里是一个不可重入的排他锁,它使用值0表示解锁状态,使用值1表示锁定状态。虽然不可重入锁并不严格要求记录当前所有者线程,但是这个类这样做是为了更容易监视使用情况。它还支持条件,并暴露其中一个检测方法: - -```java -class Mutex implements Lock, java.io.Serializable { - - // 我们内部的辅助类 - private static class Sync extends AbstractQueuedSynchronizer { - // 报告是否处于锁定状态 - protected boolean isHeldExclusively() { - return getState() == 1; - } - - // 如果 state 是 0,获取锁 - public boolean tryAcquire(int acquires) { - assert acquires == 1; // Otherwise unused - if (compareAndSetState(0, 1)) { - setExclusiveOwnerThread(Thread.currentThread()); - return true; - } - return false; - } - - // 通过将 state 置 0 来释放锁 - protected boolean tryRelease(int releases) { - assert releases == 1; // Otherwise unused - if (getState() == 0) throw new IllegalMonitorStateException(); - setExclusiveOwnerThread(null); - setState(0); - return true; - } - - // 提供一个 Condition - Condition newCondition() { return new ConditionObject(); } - - // 反序列化属性 - private void readObject(ObjectInputStream s) - throws IOException, ClassNotFoundException { - s.defaultReadObject(); - setState(0); // 重置到解锁状态 - } - } - - // 同步对象完成所有的工作。我们只是期待它. - private final Sync sync = new Sync(); - - public void lock() { sync.acquire(1); } - public boolean tryLock() { return sync.tryAcquire(1); } - public void unlock() { sync.release(1); } - public Condition newCondition() { return sync.newCondition(); } - public boolean isLocked() { return sync.isHeldExclusively(); } - public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); } - public void lockInterruptibly() throws InterruptedException { - sync.acquireInterruptibly(1); - } - public boolean tryLock(long timeout, TimeUnit unit) - throws InterruptedException { - return sync.tryAcquireNanos(1, unit.toNanos(timeout)); - } -} -``` - -这是一个闩锁类,它类似于*CountDownLatch*,只是它只需要一个单信号就可以触发。因为锁存器是非独占的,所以它使用共享的获取和释放方法。 - -```java - class BooleanLatch { - - private static class Sync extends AbstractQueuedSynchronizer { - boolean isSignalled() { return getState() != 0; } - - protected int tryAcquireShared(int ignore) { - return isSignalled() ? 1 : -1; - } - - protected boolean tryReleaseShared(int ignore) { - setState(1); - return true; - } - } - - private final Sync sync = new Sync(); - public boolean isSignalled() { return sync.isSignalled(); } - public void signal() { sync.releaseShared(1); } - public void await() throws InterruptedException { - sync.acquireSharedInterruptibly(1); - } - } -``` - -# 4 基本属性与框架 -## 4.1 继承体系图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyOTU4XzIwMjAwMjEwMjMyNTQwMzUwLnBuZw?x-oss-process=image/format,png) -## 4.2 定义 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDk2XzIwMjAwMjEwMjMxMTIwMTMzLnBuZw?x-oss-process=image/format,png) - -可知 AQS 是一个抽象类,生来就是被各种子类锁继承的。继承自AbstractOwnableSynchronizer,其作用就是为了知道当前是哪个线程获得了锁,便于后续的监控 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDYyXzIwMjAwMjEwMjMyMzE4MzcyLnBuZw?x-oss-process=image/format,png) - - -## 4.3 属性 -### 4.3.1 状态信息 -- volatile 修饰,对于可重入锁,每次获得锁 +1,释放锁 -1 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMTEwXzIwMjAwMjEwMjMyOTI0MTczLnBuZw?x-oss-process=image/format,png) -- 可以通过 *getState* 得到同步状态的当前值。该操作具有 volatile 读的内存语义。 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyOTM5XzIwMjAwMjEwMjMzMjM0OTE0LnBuZw?x-oss-process=image/format,png) -- setState 设置同步状态的值。该操作具有 volatile 写的内存语义 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDc4XzIwMjAwMjEwMjMzOTI2NjQ3LnBuZw?x-oss-process=image/format,png) -- compareAndSetState 如果当前状态值等于期望值,则以原子方式将同步状态设置为给定的更新值。此操作具有 volatile 读和写的内存语义 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDMyXzIwMjAwMjEwMjM1MjM1NDAzLnBuZw?x-oss-process=image/format,png) -- 自旋比使用定时挂起更快。粗略估计足以在非常短的超时时间内提高响应能力,当设置等待时间时才会用到这个属性 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDY0XzIwMjAwMjExMDAzOTIzNjQxLnBuZw?x-oss-process=image/format,png) - -这写方法都是Final的,子类无法重写。 -- 独占模式 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyODIzXzIwMjAwMjExMDIyNTI0NjM5LnBuZw?x-oss-process=image/format,png) -- 共享模式 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDQzXzIwMjAwMjExMDIyNTUxODMwLnBuZw?x-oss-process=image/format,png) -### 4.3.2 同步队列 -- CLH 队列( FIFO) -![](https://img-blog.csdnimg.cn/2020100800492945.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center)![在这里插入图片描述](https://img-blog.csdnimg.cn/img_convert/22686302fc050911b9dc9cdaf672934b.png) - -- 作用 -阻塞获取不到锁(独占锁)的线程,并在适当时机从队首释放这些线程。 - -同步队列底层数据结构是个双向链表。 - -- 等待队列的头,延迟初始化。 除初始化外,只能通过 *setHead* 方法修改 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyODc4XzIwMjAwMjExMDIzMjU4OTU4LnBuZw?x-oss-process=image/format,png) -注意:如果head存在,则其waitStatus保证不会是 *CANCELLED* - -- 等待队列的尾部,延迟初始化。 仅通过方法 *enq* 修改以添加新的等待节点 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDk0XzIwMjAwMjExMDIzNTU3MTkyLnBuZw?x-oss-process=image/format,png) -### 4.3.4 条件队列 -#### 为什么需要条件队列? -同步队列并非所有场景都能cover,遇到锁 + 队列结合的场景时,就需要 Lock + Condition,先使用 Lock 决定: -- 哪些线程可以获得锁 -- 哪些线程需要到同步队列里排队阻塞 - -获得锁的多个线程在碰到队列满或空时,可使用 Condition 来管理这些线程,让这些线程阻塞等待,然后在合适的时机后,被正常唤醒。 - -**同步队列 + 条件队列的协作多被用在锁 + 队列场景。** -#### 作用 -AQS 的内部类,结合锁实现线程同步。存放调用条件变量的 await 方法后被阻塞的线程 - -- 实现了 Condition 接口,而 Condition 接口就相当于 Object 的各种监控方法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMTEwXzIwMjAwMjExMDI0MDUzNi5wbmc?x-oss-process=image/format,png) -需要使用时,直接 new ConditionObject()。 - -### 4.3.5 Node -同步队列和条件队列的共用节点。 -入队时,用 Node 把线程包装一下,然后把 Node 放入两个队列中,我们看下 Node 的数据结构,如下: -#### 4.3.5.1 模式 -- 共享模式 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDUyXzIwMjAwMjExMDI1NTQxMTYyLnBuZw?x-oss-process=image/format,png) -- 独占模式 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyOTAzXzIwMjAwMjExMDI1NjE2NzkyLnBuZw?x-oss-process=image/format,png) - -#### 4.3.5.2 waitstatus - 等待状态 -```java -volatile int waitStatus; -``` -仅能为如下值: -##### SIGNAL -- 同步队列中的节点在自旋获取锁时,如果前一个节点的状态是 `SIGNAL`,那么自己就直接被阻塞,否则一直自旋 -- 该节点的后继节点会被(或很快)阻塞(通过park),因此当前节点释放或取消时必须unpark其后继节点。为避免竞争,acquire方法必须首先指示它们需要一个 signal,然后重试原子获取,然后在失败时阻塞。 -```java -static final int SIGNAL = -1; -``` - -##### CANCELLED -表示线程获取锁的请求已被取消了: -```java -static final int CANCELLED = 1; -``` -可能由于超时或中断,该节点被取消。 - -节点永远不会离开此状态,此为一种终极状态。具有 cancelled 节点的线程永远不会再次阻塞。 -##### CONDITION -该节点当前在条件队列,当节点从同步队列被转移到条件队列,状态就会被更改该态: -```java -static final int CONDITION = -2; -``` -在被转移之前,它不会用作同步队列的节点,此时状态将置0(该值的使用与该字段的其他用途无关,仅是简化了机制)。 - -##### PROPAGATE -线程处在 `SHARED` 情景下,该字段才会启用。 - -指示下一个**acquireShared**应该无条件传播,共享模式下,该状态的线程处Runnable态 -```java -static final int PROPAGATE = -3; -``` -*releaseShared* 应该传播到其他节点。 在*doReleaseShared*中对此进行了设置(仅适用于头节点),以确保传播继续进行,即使此后进行了其他操作也是如此。 -##### 0 -初始化时的默认值。 -##### 小结 -这些值是以数字方式排列,极大方便了开发者的使用。我们在平时开发也可以定义一些有特殊意义的常量值。 - -非负值表示节点不需要 signal。 因此,大多数代码并不需要检查特定值,检查符号即可。 - -- 对于普通的同步节点,该字段初始化为0 -- 对于条件节点,该字段初始化为`CONDITION` - -使用CAS(或在可能的情况下进行无条件的 volatile 写)对其进行修改。 - -注意两个状态的区别 -- state 是锁的状态,int 型,子类继承 AQS 时,都是要根据 state 字段来判断有无得到锁 -- waitStatus 是节点(Node)的状态 - -#### 4.3.5.3 数据结构 -##### 前驱节点 -- 链接到当前节点/线程所依赖的用来检查 *waitStatus* 的前驱节点 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyODY0XzIwMjAwMjEyMDMyNjI1NjYxLnBuZw?x-oss-process=image/format,png) - -在入队期间赋值,并且仅在出队时将其清空(为了GC)。 - -此外,在取消一个前驱结点后,在找到一个未取消的节点后会短路,这将始终存在,因为头节点永远不会被取消:只有成功 acquire 后,一个节点才会变为头。 - -取消的线程永远不会成功获取,并且线程只会取消自身,不会取消任何其他节点。 - -##### 后继节点 -链接到后继节点,当前节点/线程在释放时将其unpark。 在入队时赋值,在绕过已取消的前驱节点时进行调整,在出队时置null(为了GC)。 -入队操作直到附加后才赋值前驱节点的`next`字段,因此看到`next`字段为 null,并不一定意味该节点位于队尾(有时间间隙)。 - -但若`next == null`,则可从队尾开始扫描`prev`以进行再次检查。 -```java -// 若节点通过从tail向前搜索发现在在同步队列上,则返回 true -// 仅在调用了 isOnSyncQueue 且有需要时才调用 -private boolean findNodeFromTail(Node node) { - Node t = tail; - for (;;) { - if (t == node) - return true; - if (t == null) - return false; - t = t.prev; - } -} -``` -```java -final boolean isOnSyncQueue(Node node) { - if (node.waitStatus == Node.CONDITION || node.prev == null) - return false; - if (node.next != null) // If has successor, it must be on queue - return true; - /** - * node.prev 可以非null,但还没有在队列中,因为将它放在队列中的 CAS 可能会失败。 - * 所以必须从队尾向前遍历以确保它确实成功了。 - * 在调用此方法时,它将始终靠近tail,并且除非 CAS 失败(这不太可能) - * 否则它会在那里,因此几乎不会遍历太多 - */ - return findNodeFromTail(node); -} -``` -已取消节点的`next`字段设置为指向节点本身而不是null,以使isOnSyncQueue更轻松。 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDg3XzIwMjAwMjEyMDM1NTAzNjAyLnBuZw?x-oss-process=image/format,png) -- 使该节点入队的线程。 在构造时初始化,使用后消亡。![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyODcwXzIwMjAwMjEyMjAwMTI3OTkwLnBuZw?x-oss-process=image/format,png) - -在同步队列中,nextWaiter 表示当前节点是独占模式还是共享模式 -在条件队列中,nextWaiter 表示下一个节点元素 - -链接到在条件队列等待的下一个节点,或者链接到特殊值`SHARED`。 由于条件队列仅在以独占模式保存时才被访问,因此我们只需要一个简单的链接队列即可在节点等待条件时保存节点。 然后将它们转移到队列中以重新获取。 并且由于条件只能是独占的,因此我们使用特殊值来表示共享模式来保存字段。![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDUxXzIwMjAwMjEyMjAxMjMyODMucG5n?x-oss-process=image/format,png) -# 5 Condition 接口 -JDK5 时提供。 -- 条件队列 ConditionObject 实现了 Condition 接口 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDc1XzIwMjAwMjEyMjA0NjMxMTMzLnBuZw?x-oss-process=image/format,png) -- 本节就让我们一起来研究之 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDgwXzIwMjAwMjEyMjA1MzQ2NzIyLnBuZw?x-oss-process=image/format,png) - -Condition 将对象监视方法(wait,notify和notifyAll)分解为不同的对象,从而通过与任意Lock实现结合使用,从而使每个对象具有多个wait-sets。 当 Lock 替换了 synchronized 方法和语句的使用,Condition 就可以替换了Object监视器方法的使用。 - -Condition 的实现可以提供与 Object 监视方法不同的行为和语义,例如保证通知的顺序,或者在执行通知时不需要保持锁定。 如果实现提供了这种专门的语义,则实现必须记录这些语义。 - -Condition实例只是普通对象,它们本身可以用作 synchronized 语句中的目标,并且可以调用自己的监视器 wait 和 notification 方法。 获取 Condition 实例的监视器锁或使用其监视器方法与获取与该条件相关联的锁或使用其 await 和 signal 方法没有特定的关系。 建议避免混淆,除非可能在自己的实现中,否则不要以这种方式使用 Condition 实例。 - -```java - class BoundedBuffer { - final Lock lock = new ReentrantLock(); - final Condition notFull = lock.newCondition(); - final Condition notEmpty = lock.newCondition(); - - final Object[] items = new Object[100]; - int putptr, takeptr, count; - - public void put(Object x) throws InterruptedException { - lock.lock(); - try { - while (count == items.length) - notFull.await(); - items[putptr] = x; - if (++putptr == items.length) putptr = 0; - ++count; - notEmpty.signal(); - } finally { - lock.unlock(); - } - } - - public Object take() throws InterruptedException { - lock.lock(); - try { - while (count == 0) - notEmpty.await(); - Object x = items[takeptr]; - if (++takeptr == items.length) takeptr = 0; - --count; - notFull.signal(); - return x; - } finally { - lock.unlock(); - } - } - } -``` -(ArrayBlockingQueue类提供了此功能,因此没有理由实现此示例用法类。) -定义出一些方法,这些方法奠定了条件队列的基础 -## API -### await -- 使当前线程等待,直到被 signalled 或被中断 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyOTkwXzIwMjAwMjEyMjExNzU3ODYyLnBuZw?x-oss-process=image/format,png) - -与此 Condition 相关联的锁被原子释放,并且出于线程调度目的,当前线程被禁用,并且处于休眠状态,直到发生以下四种情况之一: -- 其它线程为此 Condition 调用了 signal 方法,并且当前线程恰好被选择为要唤醒的线程 -- 其它线程为此 Condition 调用了 signalAll 方法 -- 其它线程中断了当前线程,并且当前线程支持被中断 -- 发生“虚假唤醒”。 - -在所有情况下,在此方法可以返回之前,必须重新获取与此 Condition 关联的锁,才能真正被唤醒。当线程返回时,可以保证保持此锁。 -### await 超时时间 -- 使当前线程等待,直到被 signal 或中断,或经过指定的等待时间 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDczXzIwMjAwMjEyMjIxMjU3NTcyLnBuZw?x-oss-process=image/format,png) - -此方法在行为上等效于: - - -```java -awaitNanos(unit.toNanos(time)) > 0 -``` -所以,虽然入参可以是任意单位的时间,但其实仍会转化成纳秒 -### awaitNanos -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyOTg0XzIwMjAwMjEyMjIxOTMxNTU5LnBuZw?x-oss-process=image/format,png) -注意这里选择纳秒是为了避免计算剩余等待时间时的截断误差 - - -### signal() -- 唤醒条件队列中的一个线程,在被唤醒前必须先获得锁 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDYzXzIwMjAwMjEyMjMwNTQ3NjMzLnBuZw?x-oss-process=image/format,png) -### signalAll() -- 唤醒条件队列中的所有线程 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyODkyXzIwMjAwMjEyMjMwNjUzNTM5LnBuZw?x-oss-process=image/format,png) \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/FurureTask.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/FurureTask.md" index 149a2a9585..3c1bbd6ce7 100644 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/FurureTask.md" +++ "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/FurureTask.md" @@ -1,26 +1,32 @@ -# 1 简介 -- 使用继承方式的好处是方便传参,可以在子类里面添加成员变量,通过 set 方法设置参数或者通过构造函数进行传递 +# 1 引导语 +研究源码,一般我们都从整体以及实例先入手,再研究细节,不至于一开始就“深陷其中而"当局者迷". +本文,我们来看最后一种有返回值的线程创建方式。 + +- 使用继承方式的好处是方便传参,可以在子类里面添加成员变量,通过 set 方法设置参数或者通过构造函数进行传递 - 使用 Runnable 方式,则只能使用主线程里面被声明为 final 变量 -不好的地方是 Java 不支持多继承,如果继承了 Thread 类,那么子类不能再继承其他 ,而 Runable接口则没有这个限制 。而且 Thread 类和 Runnable 接口都不允许声明检查型异常,也不能定义返回值。没有返回值这点稍微有点麻烦。前两种方式都没办法拿到任务的返回结果,但今天的主角 FutureTask 却可以。 +不好的地方是 Java 不支持多继承,如果继承了 Thread 类,那么子类不能再继承其他 ,而 Runable接口则没有这个限制 。而且 Thread 类和 Runnable 接口都不允许声明检查型异常,也不能定义返回值。没有返回值这点稍微有点麻烦。前两种方式都没办法拿到任务的返回结果,但今天的主角 FutureTask 却可以. 不能声明抛出检查型异常则更麻烦一些。run()方法意味着必须捕获并处理检查型异常。即使小心地保存了异常信息(在捕获异常时)以便稍后检查,但也不能保证这个 Runnable 对象的所有使用者都读取异常信息。你也可以修改Runnable实现的getter,让它们都能抛出任务执行中的异常。但这种方法除了繁琐也不是十分安全可靠,你不能强迫使用者调用这些方法,程序员很可能会调用join()方法等待线程结束然后就不管了。 但是现在不用担心了,以上的问题终于在1.5中解决了。Callable接口和Future接口的引入以及他们对线程池的支持优雅地解决了这两个问题。 # 2 案例 先看一个demo,了解 FutureTask 相关组件是如何使用的 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY3MjUwXzIwMjAwMjA3MjIxNjM4OTk2LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177167250_20200207221638996.png) +CallerTask 类实现了 Callable 接口的 call() 方法 。在 main 函数内首先创建FutrueTask对 象(构造函数为 CallerTask 实例), 然后使用创建的 FutureTask 作为任务创建了一个线程并且启动它, 最后通过 futureTask.get()等待任务执行完毕并返回结果. + + # 3 Callable Callable函数式接口定义了唯一方法 - call(). 我们可以在Callable的实现中声明强类型的返回值,甚至是抛出异常。同时,利用call()方法直接返回结果的能力,省去读取值时的类型转换。 - 源码定义 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NjQ1XzIwMjAwMjAyMjA0MjA0MjIyLnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166645_20200202204204222.png) 注意到返回值是一个泛型,使用的时候,不会直接使用 Callable,而是和 FutureTask 协同. # 4 Future - Callable 可以返回线程的执行结果,在获取结果时,就需要用到 Future 接口. -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2OTIwXzIwMjAwMjA0MDQwMDA3MzMucG5n?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166920_2020020404000733.png) Future是 Java5 中引入的接口,当提交一个Callable对象给线程池时,将得到一个Future对象,并且它和传入的Callable有相同的结果类型声明。 @@ -34,7 +40,7 @@ Future表示异步计算的结果。提供了一些方法来检查计算是否 ## 4.1 Future API ### 4.1.1 cancel - 尝试取消执行任务 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2Njc4XzIwMjAwMjA0MDIxOTEwMTI1LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166678_20200204021910125.png) 一个比较复杂的方法,当任务处于不同状态时,该方法有不同响应: - 任务 已经完成 / 已经取消 / 由于某些其他原因无法被取消,该尝试会直接失败 - 尝试成功,且此时任务尚未开始,调用后是可以取消成功的 @@ -45,42 +51,34 @@ Future表示异步计算的结果。提供了一些方法来检查计算是否 如果此方法返回 true,则随后对 *isCancelled* 的调用将始终返回 true. ### 4.1.2 isCancelled - 是否被取消 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2ODM0XzIwMjAwMjA0MDMwMzU2OTM1LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166834_20200204030356935.png) 如果此任务在正常完成之前被取消,则返回true. ### 4.1.3 isDone - 是否完成 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NTc0XzIwMjAwMjA0MDMxMDA1NDg4LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166574_20200204031005488.png) 如果此任务完成,则返回true. 完成可能是由于正常终止,异常或取消引起的,在所有这些情况下,此方法都将返回true. ### 4.1.4 get - 获取结果 -等待任务完成,然后获取其结果。 -![](https://img-blog.csdnimg.cn/20210615233538777.png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166906_20200204031206355.png) +等待任务完成,然后获取其结果. -若: -- 任务被取消,抛 *CancellationException* -- 当前线程在等待时被中断,抛 *InterruptedException* -- 任务抛出了异常,抛 *ExecutionException* +- 如果任务被取消,抛 *CancellationException* +- 如果当前线程在等待时被中断,抛 *InterruptedException* +- 如果任务抛出了异常,抛 *ExecutionException* ### 4.1.5 timed get - 超时获取 -- 必要时最多等待给定时间以完成任务,然后获取其结果(如果有的话)。 -![](https://img-blog.csdnimg.cn/20210615233634165.png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166633_20200204033827757.png) +必要时最多等待给定时间以完成任务,然后获取其结果(如果有的话)。 - 抛CancellationException 如果任务被取消 - 抛 ExecutionException 如果任务抛了异常 - 抛InterruptedException 如果当前线程在等待时被中断 - 抛TimeoutException 如果等待超时了 -两个get()方法都是阻塞的,若被调用时,任务还没有执行完,则调用get()方法的线程会阻塞,直到任务执行完才会被唤醒。所以future.get()是会阻塞当前调用线程。 -- 阻塞异步线程 -![](https://img-blog.csdnimg.cn/20210420165206559.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -![](https://img-blog.csdnimg.cn/2021042016492740.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 阻塞主线程 -![](https://img-blog.csdnimg.cn/20210420165240137.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +需要注意:这两个get()方法都是阻塞式的,如果被调用的时候,任务还没有执行完,那么调用get()方法的线程会阻塞,直到任务执行完才会被唤醒。 -![](https://img-blog.csdnimg.cn/20210420165122388.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -Future 接口定义了许多对任务进行管理的 API,极大地方便了我们的开发调控。 +Future 接口定义了许多对任务进行管理的 API,极大地方便了我们的开发调控. # 5 RunnableFuture Java6 时提供的持有 Runnable 性质的 Future. @@ -88,7 +86,7 @@ Java6 时提供的持有 Runnable 性质的 Future. 成功执行run方法导致Future的完成,并允许访问其结果. RunnableFuture接口比较简单,就是继承了 Runnable 和 Future 接口。只提供一个*run*方法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2OTE2XzIwMjAwMjA0MDQwMjM0NzM1LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166916_20200204040234735.png) 现在,我们应该都知道,创建任务有两种方式 - 无返回值的 Runnable @@ -99,8 +97,8 @@ RunnableFuture接口比较简单,就是继承了 Runnable 和 Future 接口。 所以铺垫了这么多,本文的主角 FutureTask 来了! # 6 FutureTask -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NTk5XzIwMjAwMjAyMjA1MzM1MzA3LnBuZw?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2OTQ3XzIwMjAwMjA4MjI0MjEzNDYucG5n?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166599_20200202205335307.png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166947_2020020822421346.png) 前面的Future是一个接口,而 FutureTask 才是一个实实在在的工具类,是线程运行的具体任务. - 实现了 RunnableFuture 接口 - 也就是实现了 Runnnable 接口,即FutureTask 本身就是个 Runnnable @@ -114,7 +112,7 @@ RunnableFuture接口比较简单,就是继承了 Runnable 和 Future 接口。 - 注意这些常量字段的定义方式,遵循避免魔鬼数字的编程规约. -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2ODg4XzIwMjAwMjA4MTM0OTU2MTQ1LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166888_20200208134956145.png) - NEW 线程任务创建,开始状态 @@ -139,28 +137,28 @@ RunnableFuture接口比较简单,就是继承了 Runnable 和 Future 接口。 ### 6.1.2 其他属性 - 组合的 callable,这样就具备了转化 Callable 和 Runnable 的功能 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2ODQ3XzIwMjAwMjA4MjI0MDM0NzU0LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166847_20200208224034754.png) - 从ge()返回或抛出异常的结果,非volatile,受状态读/写保护 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NTYzXzIwMjAwMjA4MjI0MzQzNjQ2LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166563_20200208224343646.png) - 运行 callable 的线程; 在run()期间进行CAS -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NzkyXzIwMjAwMjA4MjI1NDU3Mzc2LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166792_20200208225457376.png) - 记录调用 get 方法时被等待的线程 - 栈形式 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NjI5XzIwMjAwMjA4MjI1NzM1NzE5LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166629_20200208225735719.png) 从属性上我们明显看到 Callable 是作为 FutureTask 的属性之一,这也就让 FutureTask 接着我们看下 FutureTask 的构造器,看看两者是如何转化的。 ## 6.2 构造方法 ### 6.2.1 Callable 参数 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY3MTI4XzIwMjAwMjA4MjMwMjIyNDg2LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177167128_20200208230222486.png) ### 6.2.2 Runnable 参数 为协调 callable 属性,辅助result 参数 Runnable 是没有返回值的,所以 result 一般没有用,置为 null 即可,正如 JDK 所推荐写法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2ODcwXzIwMjAwMjA4MjMxNTEwMzEwLnBuZw?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2OTcwXzIwMjAwMjA4MjMwMzM2MjIxLnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166870_20200208231510310.png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166970_20200208230336221.png) - Executors.callable 方法负责将 runnable 适配成 callable. - ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NzE0XzIwMjAwMjA4MjMyMDUxMjQ2LnBuZw?x-oss-process=image/format,png) + ![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166714_20200208232051246.png) - 通过转化类 RunnableAdapter进行适配 - ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY3MzE0XzIwMjAwMjA4MjMyMjExNjE2LnBuZw?x-oss-process=image/format,png) + ![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177167314_20200208232211616.png) ### 6.2.3 小结 我们可以学习这里的适配器模式,目标是要把 Runnable 适配成 Callable,那么我们首先要实现 Callable 接口,并且在 Callable 的 call 方法里面调用被适配对象即 Runnable的方法即可. @@ -236,12 +234,12 @@ private int awaitDone(boolean timed, long nanos) } ``` -get 是一种阻塞式方法,当发现任务还在进行中,没有完成时,就会阻塞当前进程,等待任务完成后再返回结果值。 - -阻塞底层使用的是 LockSupport.park 方法,使当前线程进入 `WAITING` 或 `TIMED_WAITING` 态。 +get 是一种阻塞式方法,当发现任务还在进行中,没有完成时,就会阻塞当前进程,等待任务完成后再返回结果值. +阻塞底层使用的是 LockSupport.park 方法,使当前线程进入 `WAITING` 或 `TIMED_WAITING` 态. ## 6.4 run -该方法可被直接调用,也可由线程池调用 +该方法可被直接调用,也可由线程池调用 + ```java public void run() { // 状态非 NEW 或当前任务已有线程在执行,直接返回 @@ -277,9 +275,12 @@ public void run() { } ``` -run 方法没有返回值,通过给 outcome 属性赋值(set(result)),get 时就能从 outcome 属性中拿到返回值。 +run 方法我们再说明几点: + +run 方法是没有返回值的,通过给 outcome 属性赋值(set(result)),get 时就能从 outcome 属性中拿到返回值; FutureTask 两种构造器,最终都转化成了 Callable,所以在 run 方法执行的时候,只需要执行 Callable 的 call 方法即可,在执行 c.call() 代码时,如果入参是 Runnable 的话, 调用路径为 c.call() -> RunnableAdapter.call() -> Runnable.run(),如果入参是 Callable 的话,直接调用。 ## 6.5 cancel + ```java // 取消任务,如果正在运行,尝试去打断 public boolean cancel(boolean mayInterruptIfRunning) { @@ -308,4 +309,4 @@ public boolean cancel(boolean mayInterruptIfRunning) { ``` # 7 总结 -FutureTask 统一了 Runnnable 和 Callable,方便了我们后续对线程池的使用。 \ No newline at end of file +FutureTask 统一了 Runnnable 和 Callable,方便了我们后续对线程池的使用. \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/CountDownLatch.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/JDK\346\272\220\347\240\201\350\247\243\346\236\220\345\256\236\346\210\230 - CountDownLatch.md" similarity index 100% rename from "JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/CountDownLatch.md" rename to "JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/JDK\346\272\220\347\240\201\350\247\243\346\236\220\345\256\236\346\210\230 - CountDownLatch.md" diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/Java\351\233\206\345\220\210\344\271\213HashMap\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/Java\351\233\206\345\220\210\344\271\213HashMap\346\272\220\347\240\201\350\247\243\346\236\220.md" deleted file mode 100644 index 59050eb978..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/Java\351\233\206\345\220\210\344\271\213HashMap\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ /dev/null @@ -1,715 +0,0 @@ -# 1 概述 -HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长. - -HashMap是非线程安全的,只适用于单线程环境,多线程环境可以采用并发包下的`concurrentHashMap` - -HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆 - -HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射操作,并允许使用null值和null键.此类不保证映射的顺序,特别是它不保证该顺序恒久不变. - -Java8中又对此类底层实现进行了优化,比如引入了红黑树的结构以解决哈希碰撞 -  -# 2 HashMap的数据结构 -在Java中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造,HashMap也不例外. -HashMap实际上是一个"链表散列"的数据结构,即数组和链表的结合体. - -![HashMap的结构](https://img-blog.csdnimg.cn/img_convert/c5ac3aacefa3b745d9d2fa48c577b11d.png) -HashMap的主结构类似于一个数组,添加值时通过`key`确定储存位置. -每个位置是一个Entry的数据结构,该结构可组成链表. -当发生冲突时,相同hash值的键值对会组成链表. -这种`数组+链表`的组合形式大部分情况下都能有不错的性能效果,Java6、7就是这样设计的. -然而,在极端情况下,一组(比如经过精心设计的)键值对都发生了冲突,这时的哈希结构就会退化成一个链表,使HashMap性能急剧下降. - -所以在Java8中,HashMap的结构实现变为数组+链表+红黑树 -![Java8 HashMap的结构](https://img-blog.csdnimg.cn/img_convert/7668e49d6bb167520dcf09ab09537378.png) -可以看出,HashMap底层就是一个数组结构 -数组中的每一项又是一个链表 -当新建一个HashMap时,就会初始化一个数组. - -# 3 三大集合与迭代子 -HashMap使用三大集合和三种迭代子来轮询其Key、Value和Entry对象 -```java -public class HashMapExam { - public static void main(String[] args) { - Map map = new HashMap<>(16); - for (int i = 0; i < 15; i++) { - map.put(i, new String(new char[]{(char) ('A'+ i)})); - } - - System.out.println("======keySet======="); - Set set = map.keySet(); - Iterator iterator = set.iterator(); - while (iterator.hasNext()) { - System.out.println(iterator.next()); - } - - System.out.println("======values======="); - Collection values = map.values(); - Iterator stringIterator=values.iterator(); - while (stringIterator.hasNext()) { - System.out.println(stringIterator.next()); - } - - System.out.println("======entrySet======="); - for (Map.Entry entry : map.entrySet()) { - System.out.println(entry); - } - } -} -``` - -# 4 源码分析 -```java - //默认的初始容量16,且实际容量是2的整数幂 - static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; - - //最大容量(传入容量过大将被这个值替换) - static final int MAXIMUM_CAPACITY = 1 << 30; - - // 默认加载因子为0.75(当表达到3/4满时,才会再散列),这个因子在时间和空间代价之间达到了平衡.更高的因子可以降低表所需的空间,但是会增加查找代价,而查找是最频繁操作 - static final float DEFAULT_LOAD_FACTOR = 0.75f; - - //桶的树化阈值:即 链表转成红黑树的阈值,在存储数据时,当链表长度 >= 8时,则将链表转换成红黑树 - static final int TREEIFY_THRESHOLD = 8; - // 桶的链表还原阈值:即 红黑树转为链表的阈值,当在扩容(resize())时(HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 <= 6时,则将 红黑树转换成链表 - static final int UNTREEIFY_THRESHOLD = 6; - //最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树) -``` -因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要 -链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短 - -还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换 -假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。 -``` - // 为了避免扩容/树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD - // 小于该值时使用的是扩容哦!!! - static final int MIN_TREEIFY_CAPACITY = 64; - - // 存储数据的Node数组,长度是2的幂. - // HashMap采用链表法解决冲突,每一个Node本质上是一个单向链表 - //HashMap底层存储的数据结构,是一个Node数组.上面得知Node类为元素维护了一个单向链表.至此,HashMap存储的数据结构也就很清晰了:维护了一个数组,每个数组又维护了一个单向链表.之所以这么设计,考虑到遇到哈希冲突的时候,同index的value值就用单向链表来维护 - //与 JDK 1.7 的对比(Entry类),仅仅只是换了名字 - transient Node[] table; - - // HashMap的底层数组中已用槽的数量 - transient int size; - // HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子) - int threshold; - - // 负载因子实际大小 - final float loadFactor; - - // HashMap被改变的次数 - transient int modCount; - - // 指定“容量大小”和“加载因子”的构造函数,是最基础的构造函数 - public HashMap(int initialCapacity, float loadFactor) { - if (initialCapacity < 0) - throw new IllegalArgumentException("Illegal initial capacity: " + - initialCapacity); - // HashMap的最大容量只能是MAXIMUM_CAPACITY - if (initialCapacity > MAXIMUM_CAPACITY) - initialCapacity = MAXIMUM_CAPACITY; - //负载因子须大于0 - if (loadFactor <= 0 || Float.isNaN(loadFactor)) - throw new IllegalArgumentException("Illegal load factor: " + - loadFactor); - // 设置"负载因子" - this.loadFactor = loadFactor; - // 设置"HashMap阈值",当HashMap中存储数据的数量达到threshold时,就需将HashMap的容量加倍 - this.threshold = tableSizeFor(initialCapacity); - } -``` - -- 上面的tableSizeFor有何用? -tableSizeFor方法保证函数返回值是大于等于给定参数initialCapacity最小的2的幂次方的数值 -``` - static final int tableSizeFor(int cap) { - int n = cap - 1; - n |= n >>> 1; - n |= n >>> 2; - n |= n >>> 4; - n |= n >>> 8; - n |= n >>> 16; - return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; - } -``` - 可以看出该方法是一系列的二进制位操作 - ->a |= b 等同于 a = a|b - -逐行分析 -- `int n = cap - 1` -给定的cap 减 1,为了避免参数cap本来就是2的幂次方,这样一来,经过后续操作,cap将会变成2 * cap,是不符合我们预期的 - -- `n |= n >>> 1` -n >>> 1 : n无符号右移1位,即n二进制最高位的1右移一位 -n | (n >>> 1) 导致 n二进制的高2位值为1 -目前n的高1~2位均为1 -- `n |= n >>> 2` -n继续无符号右移2位 -n | (n >>> 2) 导致n二进制表示的高3~4位经过运算值均为1 -目前n的高1~4位均为1 -- `n |= n >>> 4` -n继续无符号右移4位 -n | (n >>> 4) 导致n二进制表示的高5~8位经过运算值均为1 -目前n的高1~8位均为1 -- `n |= n >>> 8` -n继续无符号右移8位 -n | (n >>> 8) 导致n二进制表示的高9~16位经过运算值均为1 -目前n的高1~16位均为1 - -可以看出,无论给定cap(cap < MAXIMUM_CAPACITY )的值是多少,经过以上运算,其值的二进制所有位都会是1.再将其加1,这时候这个值一定是2的幂次方. -当然如果经过运算值大于MAXIMUM_CAPACITY,直接选用MAXIMUM_CAPACITY. -![例子](https://img-blog.csdnimg.cn/img_convert/e58fef4c158d1c978777f5aa40ebee5e.png) -至此tableSizeFor如何保证cap为2的幂次方已经显而易见了,那么问题来了 - -## 4.1 **为什么cap要保持为2的幂次方?** -主要与HashMap中的数据存储有关. - -在Java8中,HashMap中key的Hash值由Hash(key)方法计得 -![](https://img-blog.csdnimg.cn/img_convert/0882f5d36b225a33c5e17666f5fb6695.png) - -HashMap中存储数据table的index是由key的Hash值决定的. -在HashMap存储数据时,我们期望数据能均匀分布,以防止哈希冲突. -自然而然我们就会想到去用`%`取余操作来实现我们这一构想 - ->取余(%)操作 : 如果除数是2的幂次则等价于与其除数减一的与(&)操作. - -这也就解释了为什么一定要求cap要为2的幂次方.再来看看table的index的计算规则: -![](https://img-blog.csdnimg.cn/img_convert/d8df9e11f3a143218a08f515ba6e805e.png) - 等价于: -``` - index = e.hash % newCap -``` -采用二进制位操作&,相对于%,能够提高运算效率,这就是cap的值被要求为2幂次的原因 -![](https://img-blog.csdnimg.cn/img_convert/6fcd40c3f37371a2a9ebb2a209f3be58.png) -![数据结构 & 参数与 JDK 7 / 8](https://img-blog.csdnimg.cn/img_convert/3d946e0e1bd86c8ae41c1d6857377445.png) -## 4.2 **Node类** - -``` -static class Node implements Map.Entry { - final int hash; - final K key; - V value; - Node next; - - Node(int hash, K key, V value, Node next) { - this.hash = hash; - this.key = key; - this.value = value; - this.next = next; - } - - public final K getKey() { return key; } - public final V getValue() { return value; } - public final String toString() { return key + "=" + value; } - - public final int hashCode() { - return Objects.hashCode(key) ^ Objects.hashCode(value); - } - - public final V setValue(V newValue) { - V oldValue = value; - value = newValue; - return oldValue; - } - - public final boolean equals(Object o) { - if (o == this) - return true; - if (o instanceof Map.Entry) { - Map.Entry e = (Map.Entry)o; - if (Objects.equals(key, e.getKey()) && - Objects.equals(value, e.getValue())) - return true; - } - return false; - } - } -``` -Node 类是HashMap中的静态内部类,实现Map.Entry接口.定义了key键、value值、next节点,也就是说元素之间构成了单向链表. - -## 4.3 TreeNode -``` -static final class TreeNode extends LinkedHashMap.Entry { - TreeNode parent; // red-black tree links - TreeNode left; - TreeNode right; - TreeNode prev; // needed to unlink next upon deletion - boolean red; - TreeNode(int hash, K key, V val, Node next) {} - - // 返回当前节点的根节点 - final TreeNode root() { - for (TreeNode r = this, p;;) { - if ((p = r.parent) == null) - return r; - r = p; - } - } - } -``` -红黑树结构包含前、后、左、右节点,以及标志是否为红黑树的字段 -此结构是Java8新加的 - -## 4.4 hash方法 -Java 8中的散列值优化函数 -![](https://img-blog.csdnimg.cn/img_convert/5a02fb83605c5435e8585b2c37175e69.png) -只做一次16位右位移异或 -key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值 - -理论上散列值是一个int型,如果直接拿散列值作为下标访问HashMap主数组的话,考虑到2进制32位带符号的int范围大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。 -但问题是一个40亿长度的数组,内存是放不下的.HashMap扩容之前的数组初始大小才16,所以这个散列值是不能直接拿来用的. -用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组下标 -源码中模运算就是把散列值和数组长度做一个"与"操作, -![](https://img-blog.csdnimg.cn/img_convert/d92c8b227a759d2c17736bc2b6403d57.png) -这也正好解释了为什么HashMap的数组长度要取2的整次幂 -因为这样(数组长度-1)正好相当于一个“低位掩码” -“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问 - -以初始长度16为例,16-1=15 -2进制表示是00000000 00000000 00001111 -和某散列值做“与”操作如下,结果就是截取了最低的四位值 -![](https://img-blog.csdnimg.cn/img_convert/553e83380e5587fd4e40724b373d79e4.png) -但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重 - -这时候“扰动函数”的价值就体现出来了 -![](https://img-blog.csdnimg.cn/img_convert/b2051fb82b033621ca8d154861ff5c15.png) -右位移16位,正好是32位一半,自己的高半区和低半区做异或,就是为了混合原始hashCode的高位和低位,以此来加大低位的随机性 -而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。 - -index的运算规则是 -``` -e.hash & (newCap - 1) -``` -newCap是2的幂,所以newCap - 1的高位全0 - -若e.hash值只用自身的hashcode,index只会和e.hash的低位做&操作.这样一来,index的值就只有低位参与运算,高位毫无存在感,从而会带来哈希冲突的风险 -所以在计算key的hashCode时,用其自身hashCode与其低16位做异或操作 -这也就让高位参与到index的计算中来了,即降低了哈希冲突的风险又不会带来太大的性能问题 - -## 4.5 Put方法 -![](https://img-blog.csdnimg.cn/img_convert/da29710b339ad70260610e2ed358305f.png) - -![](https://img-blog.csdnimg.cn/img_convert/977877dab11a1fcd23dffadecbe6b2cd.png) - -![HashMap-put(k,v)](https://img-blog.csdnimg.cn/img_convert/0df25cf27e264797c7f1451e11c927f1.png) -①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容 - -②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③ - -③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals - -④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤ - -⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可 - -⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,执行resize()扩容 -``` - public V put(K key, V value) { - // 对key的hashCode()做hash - return putVal(hash(key), key, value, false, true); - } - -final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { - Node[] tab; Node p; int n, i; - // 步骤① tab为空则调用resize()初始化创建 - if ((tab = table) == null || (n = tab.length) == 0) - n = (tab = resize()).length; - // 步骤② 计算index,并对null做处理 - //tab[i = (n - 1) & hash对应下标的第一个节点 - if ((p = tab[i = (n - 1) & hash]) == null) - // 无哈希冲突的情况下,将value直接封装为Node并赋值 - tab[i] = newNode(hash, key, value, null); - else { - Node e; K k; - // 步骤③ 节点的key相同,直接覆盖节点 - if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) - e = p; - // 步骤④ 判断该链为红黑树 - else if (p instanceof TreeNode) - // p是红黑树类型,则调用putTreeVal方式赋值 - e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); - // 步骤⑤ p非红黑树类型,该链为链表 - else { - // index 相同的情况下 - for (int binCount = 0; ; ++binCount) { - if ((e = p.next) == null) { - // 如果p的next为空,将新的value值添加至链表后面 - p.next = newNode(hash, key, value, null); - if (binCount >= TREEIFY_THRESHOLD - 1) - // 如果链表长度大于8,链表转化为红黑树,执行插入 - treeifyBin(tab, hash); - break; - } - // key相同则跳出循环 - if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) - break; - //就是移动指针方便继续取 p.next - - p = e; - } - } - if (e != null) { // existing mapping for key - V oldValue = e.value; - //根据规则选择是否覆盖value - if (!onlyIfAbsent || oldValue == null) - e.value = value; - afterNodeAccess(e); - return oldValue; - } - } - ++modCount; - // 步骤⑥:超过最大容量,就扩容 - if (++size > threshold) - // size大于加载因子,扩容 - resize(); - afterNodeInsertion(evict); - return null; - } -``` -在构造函数中最多也只是设置了initialCapacity、loadFactor的值,并没有初始化table,table的初始化工作是在put方法中进行的. -## 4.6 resize -![](https://img-blog.csdnimg.cn/img_convert/24a22ebcda082c0c13df1c2193ee18d6.png) -扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,内部的数组无法装载更多的元素时,就需要扩大数组的长度. -当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组 -``` - /** - * 该函数有2种使用情况:1.初始化哈希表 2.当前数组容量过小,需扩容 - */ -final Node[] resize() { - Node[] oldTab = table; - int oldCap = (oldTab == null) ? 0 : oldTab.length; - int oldThr = threshold; - int newCap, newThr = 0; - - // 针对情况2:若扩容前的数组容量超过最大值,则不再扩充 - if (oldCap > 0) { - if (oldCap >= MAXIMUM_CAPACITY) { - threshold = Integer.MAX_VALUE; - return oldTab; - } - // 针对情况2:若无超过最大值,就扩充为原来的2倍 - else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && - oldCap >= DEFAULT_INITIAL_CAPACITY) - //newCap设置为oldCap的2倍并小于MAXIMUM_CAPACITY,且大于默认值, 新的threshold增加为原来的2倍 - newThr = oldThr << 1; // double threshold - } - - // 针对情况1:初始化哈希表(采用指定 or 默认值) - else if (oldThr > 0) // initial capacity was placed in threshold - // threshold>0, 将threshold设置为newCap,所以要用tableSizeFor方法保证threshold是2的幂次方 - newCap = oldThr; - else { // zero initial threshold signifies using defaults - // 默认初始化 - newCap = DEFAULT_INITIAL_CAPACITY; - newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); - } - - // 计算新的resize上限 - if (newThr == 0) { - // newThr为0,newThr = newCap * 0.75 - float ft = (float)newCap * loadFactor; - newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? - (int)ft : Integer.MAX_VALUE); - } - threshold = newThr; - @SuppressWarnings({"rawtypes","unchecked"}) - // 新生成一个table数组 - Node[] newTab = (Node[])new Node[newCap]; - table = newTab; - if (oldTab != null) { - // oldTab 复制到 newTab - for (int j = 0; j < oldCap; ++j) { - Node e; - if ((e = oldTab[j]) != null) { - oldTab[j] = null; - if (e.next == null) - // 链表只有一个节点,直接赋值 - //为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。 - newTab[e.hash & (newCap - 1)] = e; - else if (e instanceof TreeNode) - // e为红黑树的情况 - ((TreeNode)e).split(this, newTab, j, oldCap); - else { // preserve order链表优化重hash的代码块 - Node loHead = null, loTail = null; - Node hiHead = null, hiTail = null; - Node next; - do { - next = e.next; - // 原索引 - if ((e.hash & oldCap) == 0) { - if (loTail == null) - loHead = e; - else - loTail.next = e; - loTail = e; - } - // 原索引 + oldCap - else { - if (hiTail == null) - hiHead = e; - else - hiTail.next = e; - hiTail = e; - } - } while ((e = next) != null); - // 原索引放到bucket里 - if (loTail != null) { - loTail.next = null; - newTab[j] = loHead; - } - // 原索引+oldCap放到bucket里 - if (hiTail != null) { - hiTail.next = null; - newTab[j + oldCap] = hiHead; - } - } - } - } - } - return newTab; - } -``` -![图片发自简书App](https://img-blog.csdnimg.cn/img_convert/dd74408150f2d30938f08296a6501d71.png) - - - -## 4.7 remove方法 -remove(key) 方法 和 remove(key, value) 方法都是通过调用removeNode的方法来实现删除元素的 -```java - final Node removeNode(int hash, Object key, Object value, - boolean matchValue, boolean movable) { - Node[] tab; Node p; int n, index; - if ((tab = table) != null && (n = tab.length) > 0 && - (p = tab[index = (n - 1) & hash]) != null) { - Node node = null, e; K k; V v; - if (p.hash == hash && - ((k = p.key) == key || (key != null && key.equals(k)))) - // index 元素只有一个元素 - node = p; - else if ((e = p.next) != null) { - if (p instanceof TreeNode) - // index处是一个红黑树 - node = ((TreeNode)p).getTreeNode(hash, key); - else { - // index处是一个链表,遍历链表返回node - do { - if (e.hash == hash && - ((k = e.key) == key || - (key != null && key.equals(k)))) { - node = e; - break; - } - p = e; - } while ((e = e.next) != null); - } - } - // 分不同情形删除节点 - if (node != null && (!matchValue || (v = node.value) == value || - (value != null && value.equals(v)))) { - if (node instanceof TreeNode) - ((TreeNode)node).removeTreeNode(this, tab, movable); - else if (node == p) - tab[index] = node.next; - else - p.next = node.next; - ++modCount; - --size; - afterNodeRemoval(node); - return node; - } - } - return null; - } -``` -## 4.8 get -```java -/** - * 函数原型 - * 作用:根据键key,向HashMap获取对应的值 - */ - map.get(key); - - - /** - * 源码分析 - */ - public V get(Object key) { - Node e; - // 1. 计算需获取数据的hash值 - // 2. 通过getNode()获取所查询的数据 ->>分析1 - // 3. 获取后,判断数据是否为空 - return (e = getNode(hash(key), key)) == null ? null : e.value; -} - -/** - * 分析1:getNode(hash(key), key)) - */ -final Node getNode(int hash, Object key) { - Node[] tab; Node first, e; int n; K k; - - // 1. 计算存放在数组table中的位置 - if ((tab = table) != null && (n = tab.length) > 0 && - (first = tab[(n - 1) & hash]) != null) { - - // 4. 通过该函数,依次在数组、红黑树、链表中查找(通过equals()判断) - // a. 先在数组中找,若存在,则直接返回 - if (first.hash == hash && // always check first node - ((k = first.key) == key || (key != null && key.equals(k)))) - return first; - - // b. 若数组中没有,则到红黑树中寻找 - if ((e = first.next) != null) { - // 在树中get - if (first instanceof TreeNode) - return ((TreeNode)first).getTreeNode(hash, key); - - // c. 若红黑树中也没有,则通过遍历,到链表中寻找 - do { - if (e.hash == hash && - ((k = e.key) == key || (key != null && key.equals(k)))) - return e; - } while ((e = e.next) != null); - } - } - return null; -} -``` -> 在JDK1.7及以前的版本中,HashMap里是没有红黑树的实现的,在JDK1.8中加入了红黑树是为了防止哈希表碰撞攻击,当链表链长度为8时,及时转成红黑树,提高map的效率 - -如果某个桶中的记录过大的话(当前是TREEIFY_THRESHOLD = 8),HashMap会动态的使用一个专门的treemap实现来替换掉它。这样做的结果会更好,是O(logn),而不是糟糕的O(n)。它是如何工作的? -前面产生冲突的那些KEY对应的记录只是简单的追加到一个链表后面,这些记录只能通过遍历来进行查找。但是超过这个阈值后HashMap开始将列表升级成一个二叉树,使用哈希值作为树的分支变量,如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。如果哈希值相等,HashMap希望key值最好是实现了Comparable接口的,这样它可以按照顺序来进行插入。这对HashMap的key来说并不是必须的,不过如果实现了当然最好。如果没有实现这个接口,在出现严重的哈希碰撞的时候,你就并别指望能获得性能提升了。 - -这个性能提升有什么用处?比方说恶意的程序,如果它知道我们用的是哈希算法,它可能会发送大量的请求,导致产生严重的哈希碰撞。然后不停的访问这些key就能显著的影响服务器的性能,这样就形成了一次拒绝服务攻击(DoS)。JDK 8中从O(n)到O(logn)的飞跃,可以有效地防止类似的攻击,同时也让HashMap性能的可预测性稍微增强了一些 -```java -/** - * 源码分析:resize(2 * table.length) - * 作用:当容量不足时(容量 > 阈值),则扩容(扩到2倍) - */ - void resize(int newCapacity) { - - // 1. 保存旧数组(old table) - Entry[] oldTable = table; - - // 2. 保存旧容量(old capacity ),即数组长度 - int oldCapacity = oldTable.length; - - // 3. 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出 - if (oldCapacity == MAXIMUM_CAPACITY) { - threshold = Integer.MAX_VALUE; - return; - } - - // 4. 根据新容量(2倍容量)新建1个数组,即新table - Entry[] newTable = new Entry[newCapacity]; - - // 5. (重点分析)将旧数组上的数据(键值对)转移到新table中,从而完成扩容 ->>分析1.1 - transfer(newTable); - - // 6. 新数组table引用到HashMap的table属性上 - table = newTable; - - // 7. 重新设置阈值 - threshold = (int)(newCapacity * loadFactor); -} - - /** - * 分析1.1:transfer(newTable); - * 作用:将旧数组上的数据(键值对)转移到新table中,从而完成扩容 - * 过程:按旧链表的正序遍历链表、在新链表的头部依次插入 - */ -void transfer(Entry[] newTable) { - // 1. src引用了旧数组 - Entry[] src = table; - - // 2. 获取新数组的大小 = 获取新容量大小 - int newCapacity = newTable.length; - - // 3. 通过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中 - for (int j = 0; j < src.length; j++) { - // 3.1 取得旧数组的每个元素 - Entry e = src[j]; - if (e != null) { - // 3.2 释放旧数组的对象引用(for循环后,旧数组不再引用任何对象) - src[j] = null; - - do { - // 3.3 遍历 以该数组元素为首 的链表 - // 注:转移链表时,因是单链表,故要保存下1个结点,否则转移后链表会断开 - Entry next = e.next; - // 3.3 重新计算每个元素的存储位置 - int i = indexFor(e.hash, newCapacity); - // 3.4 将元素放在数组上:采用单链表的头插入方式 = 在链表头上存放数据 = 将数组位置的原有数据放在后1个指针、将需放入的数据放到数组位置中 - // 即 扩容后,可能出现逆序:按旧链表的正序遍历链表、在新链表的头部依次插入 - e.next = newTable[i]; - newTable[i] = e; - // 访问下1个Entry链上的元素,如此不断循环,直到遍历完该链表上的所有节点 - e = next; - } while (e != null); - // 如此不断循环,直到遍历完数组上的所有数据元素 - } - } - } -``` -从上面可看出:在扩容resize()过程中,在将旧数组上的数据 转移到 新数组上时,转移数据操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况 - ->`设重新计算存储位置后不变,即扩容前 = 1->2->3,扩容后 = 3->2->1` - -此时若并发执行 put 操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环(Infinite Loop),即死锁 -![](https://img-blog.csdnimg.cn/img_convert/cd3bbd816bcefb360280b591d5cf41cf.png) -![image.png](https://img-blog.csdnimg.cn/img_convert/cb6354c50e5af1ef24c1ab36b802217e.png) -![](https://img-blog.csdnimg.cn/img_convert/5e35431f5f3c179e389f5528e1b03d42.png) -![为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键](https://img-blog.csdnimg.cn/img_convert/38321f907a385a91616ff972204237d4.png) -## 4.9 getOrDefault -getOrDefault() 方法获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值。 -![](https://img-blog.csdnimg.cn/55e1bdebe6cd4fc9aaa22d6662c501e4.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -# 5 单线程rehash -单线程情况下,rehash无问题 -[![HashMap rehash single thread](https://img-blog.csdnimg.cn/img_convert/50437495a5eda75989d04de58316b1f7.png)](http://www.jasongj.com/img/java/concurrenthashmap/single_thread_rehash.png) -# 6 多线程并发下的rehash - -这里假设有两个线程同时执行了put操作并引发了rehash,执行了transfer方法,并假设线程一进入transfer方法并执行完next = e.next后,因为线程调度所分配时间片用完而“暂停”,此时线程二完成了transfer方法的执行。此时状态如下。 - -[![HashMap rehash multi thread step 1](https://img-blog.csdnimg.cn/img_convert/a5aaefdb773217a54d29cbe9809e2aa9.png)](http://www.jasongj.com/img/java/concurrenthashmap/multi_thread_rehash_1.png) -接着线程1被唤醒,继续执行第一轮循环的剩余部分 -``` -e.next = newTable[1] = null -newTable[1] = e = key(5) -e = next = key(9) -``` -结果如下图所示 -[![HashMap rehash multi thread step 2](https://img-blog.csdnimg.cn/img_convert/a261863e3e27077ba7b3223a3914f0de.png)](http://www.jasongj.com/img/java/concurrenthashmap/multi_thread_rehash_2.png) - -接着执行下一轮循环,结果状态图如下所示 -[![HashMap rehash multi thread step 3](https://img-blog.csdnimg.cn/img_convert/03cea3cdb5a9477ca25e98ee6f37cf43.png)](http://www.jasongj.com/img/java/concurrenthashmap/multi_thread_rehash_3.png) - -继续下一轮循环,结果状态图如下所示 -[![HashMap rehash multi thread step 4](https://img-blog.csdnimg.cn/img_convert/22e59112a7635a44dff4fa42a7e6a840.png)](http://www.jasongj.com/img/java/concurrenthashmap/multi_thread_rehash_4.png) - -此时循环链表形成,并且key(11)无法加入到线程1的新数组。在下一次访问该链表时会出现死循环。 -# 7 Fast-fail -## 产生原因 - -在使用迭代器的过程中如果HashMap被修改,那么`ConcurrentModificationException`将被抛出,也即Fast-fail策略。 - -当HashMap的iterator()方法被调用时,会构造并返回一个新的EntryIterator对象,并将EntryIterator的expectedModCount设置为HashMap的modCount(该变量记录了HashMap被修改的次数)。 -``` -HashIterator() { - expectedModCount = modCount; - if (size > 0) { // advance to first entry - Entry[] t = table; - while (index < t.length && (next = t[index++]) == null) - ; - } -} -``` - - -在通过该Iterator的next方法访问下一个Entry时,它会先检查自己的expectedModCount与HashMap的modCount是否相等,如果不相等,说明HashMap被修改,直接抛出`ConcurrentModificationException`。该Iterator的remove方法也会做类似的检查。该异常的抛出意在提醒用户及早意识到线程安全问题。 - -## 线程安全解决方案 -单线程条件下,为避免出现`ConcurrentModificationException`,需要保证只通过HashMap本身或者只通过Iterator去修改数据,不能在Iterator使用结束之前使用HashMap本身的方法修改数据。因为通过Iterator删除数据时,HashMap的modCount和Iterator的expectedModCount都会自增,不影响二者的相等性。如果是增加数据,只能通过HashMap本身的方法完成,此时如果要继续遍历数据,需要重新调用iterator()方法从而重新构造出一个新的Iterator,使得新Iterator的expectedModCount与更新后的HashMap的modCount相等。 - -多线程条件下,可使用`Collections.synchronizedMap`方法构造出一个同步Map,或者直接使用线程安全的ConcurrentHashMap。 \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ReentrantLock\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ReentrantLock\346\272\220\347\240\201\350\247\243\346\236\220.md" index ad32141823..a1b5860985 100644 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ReentrantLock\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ReentrantLock\346\272\220\347\240\201\350\247\243\346\236\220.md" @@ -74,79 +74,90 @@ ReentrantLock 就负责实现这些接口,使用时,直接调用的也是这 ## 4.2 FairSync - 公平锁 只实现 *lock* 和 *tryAcquire* 两个方法 ### 4.2.1 lock -lock 方法加锁成功,直接返回,所以可以继续执行业务逻辑。 - 公平模式的 lock ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzJhNGEyZmViNzY?x-oss-process=image/format,png) + 直接调用 acquire,而没有像非公平模式先试图获取,因为这样可能导致违反“公平”的语义:在已等待在队列中的线程之前获取了锁。 -*acquire* 是 AQS 的方法,表示先尝试获得锁,失败之后进入同步队列阻塞等待。 +*acquire* 是 AQS 的方法,表示先尝试获得锁,失败之后进入同步队列阻塞等待,详情见本专栏的上一文 ### 4.2.2 tryAcquire +公平模式的 *tryAcquire*。不要授予访问权限,除非递归调用或没有等待线程或是第一个调用的。 - 该方法是 AQS 在 acquire 方法中留给子类去具体实现的 -![](https://img-blog.csdnimg.cn/20210705225313380.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -#### 公平模式 -不要授予访问权限,除非递归调用或没有等待线程或是第一个调用的。 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzJhMjJmYmVkZWM?x-oss-process=image/format,png) + +话不多说,看源码: ```java protected final boolean tryAcquire(int acquires) { - // 获取当前的线程 + // 获取当前的线程 final Thread current = Thread.currentThread(); - // 获取 state 锁的状态(volatile 读语义) + // 获取 state 锁的状态 int c = getState(); // state == 0 => 尚无线程获取锁 if (c == 0) { - // 判断 AQS 的同步对列里是否有线程等待 + // 判断 AQS 的同步对列里是否有线程等待,若没有则直接 CAS 获取锁 if (!hasQueuedPredecessors() && - // 若没有则直接 CAS(保证原子性,线程安全) 获取锁 compareAndSetState(0, acquires)) { // 获取锁成功,设置独占线程 setExclusiveOwnerThread(current); return true; } } - // 已经获取锁的是否为当前的线程? + // 判断已经获取锁是否为当前的线程 else if (current == getExclusiveOwnerThread()) { // 锁的重入, 即 state 加 1 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); - // 已经获取 lock,所以这里不考虑并发 setState(nextc); return true; } return false; } ``` -和Sync#nonfairTryAcquire类似,唯一不同的是当发现锁未被占用时,使用 **hasQueuedPredecessors** 确保了公平性。 +和 Sync 的 *nonfairTryAcquire* 方法实现类似,唯一不同的是当发现锁未被占用时,使用 *hasQueuedPredecessors* 确保了公平性。 #### hasQueuedPredecessors -判断当前线程是不是属于同步队列的头节点的下一个节点(头节点是释放锁的节点): -- 是(返回false),符合FIFO,可以获得锁 -- 不是(返回true),则继续等待 +会判断当前线程是不是属于同步队列的头节点的下一个节点(头节点是释放锁的节点) +- 如果是(返回false),符合FIFO,可以获得锁 +- 如果不是(返回true),则继续等待 ```java -public final boolean hasQueuedPredecessors() { - // 这种方法的正确性取决于头在尾之前初始化和头初始化。如果当前线程是队列中的第一个线程,则next是精确的 - Node t = tail; // 按反初始化顺序读取字段 - Node h = head; - Node s; - return h != t && - ((s = h.next) == null || s.thread != Thread.currentThread()); -} + public final boolean hasQueuedPredecessors() { + // 这种方法的正确性取决于头在尾之前初始化和头初始化。如果当前线程是队列中的第一个线程,则next是精确的 + Node t = tail; // 按反初始化顺序读取字段 + Node h = head; + Node s; + return h != t && + ((s = h.next) == null || s.thread != Thread.currentThread()); + } ``` + + + # 5 nonfairTryAcquire -执行非公平的 *tryLock*。 *tryAcquire* 是在子类中实现的,但是都需要对*trylock* 方法进行非公平的尝试。 +执行非公平的 *tryLock*。 +*tryAcquire* 是在子类中实现的,但是都需要对*trylock* 方法进行非公平的尝试。 + ```java final boolean nonfairTryAcquire(int acquires) { + // 获取当前的线程 final Thread current = Thread.currentThread(); + // 获取 AQS 中的 state 字段 int c = getState(); + // state 为 0,表示同步器的锁尚未被持有 if (c == 0) { - // 这里可能有竞争,所以可能失败 + // CAS state 获取锁(这里可能有竞争,所以可能失败) if (compareAndSetState(0, acquires)) { // 获取锁成功, 设置获取独占锁的线程 setExclusiveOwnerThread(current); + // 直接返回 true return true; } } + // 判断现在获取独占锁的线程是否为当前线程(可重入锁的体现) else if (current == getExclusiveOwnerThread()) { + // state 计数加1(重入获取锁) int nextc = c + acquires; - if (nextc < 0) - throw new Error("Maximum lock count exceeded"); + if (nextc < 0) // 整型溢出 + throw new Error("Maximum lock count exceeded"); + // 已经获取 lock,所以这里不考虑并发 setState(nextc); return true; } @@ -154,7 +165,6 @@ final boolean nonfairTryAcquire(int acquires) { } ``` 无参的 *tryLock* 调用的就是此方法 - # 6 tryLock ## 6.1 无参 Lock 接口中定义的方法。 @@ -166,6 +176,19 @@ Lock 接口中定义的方法。 如果当前线程已经持有该锁,那么持有计数将增加1,方法返回true。 如果锁被另一个线程持有,那么这个方法将立即返回值false。 +- 典型的使用方法 +```java + Lock lock = ...; + if (lock.tryLock()) { + try { + // manipulate protected state + } finally { + lock.unlock(); + } + } else { + // 执行可选的操作 + } +``` ## 6.2 有参 - 提供了超时时间的入参,在时间内,仍没有得到锁,会返回 false ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzJhMzZhNTAwMzQ?x-oss-process=image/format,png) @@ -191,4 +214,6 @@ protected final boolean tryRelease(int releases) { setState(c); return free; } -``` \ No newline at end of file +``` +# 8 总结 +AQS 搭建了整个锁架构,子类锁的实现只需要根据场景,实现 AQS 对应的方法即可。 \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ThreadLocal.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ThreadLocal.md" deleted file mode 100644 index 5f5a4c37b5..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ThreadLocal.md" +++ /dev/null @@ -1,246 +0,0 @@ - -# 1 前言 - -此类提供线程本地变量,与普通变量不同,因为每个访问一个变量(通过其get或set方法)的线程都有其自己的,独立初始化的变量副本。 -ThreadLocal 实例通常是期望将状态与线程(例如,用户ID或事务ID)关联的类中的 private static 字段。 - -例如,下面的类生成每个线程本地的唯一标识符。线程的ID是在第一次调用ThreadId.get() 时赋值的,并且在以后的调用中保持不变。 - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzcvMTcxZWZhYzdiMDdiMDMzNw?x-oss-process=image/format,png) - -只要线程是活跃的并且 ThreadLocal 实例是可访问的,则每个线程都对其线程本地变量的副本持有隐式的引用。线程消失后,线程本地实例的所有副本都会被 GC(除非存在对这些副本的其他引用)。 - -# 2 继续体系 -- 继承?不存在的,这其实也是 java.lang 包下的工具类,但是 ThreadLocal 定义带有泛型,说明可以储存任意格式的数据。 -![](https://img-blog.csdnimg.cn/20210615235535658.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - -# 3 属性 -ThreadLocal 依赖于附加到每个线程(Thread.threadLocals和InheritableThreadLocals)的线程线性探测哈希表。 - -## threadLocalHashCode -ThreadLocal 对象充当key,通过 threadLocalHashCode 进行搜索。这是一个自定义哈希码(仅在ThreadLocalMaps 中有用),它消除了在相同线程使用连续构造的threadlocal的常见情况下的冲突,而在不太常见的情况下仍然表现良好。 - -ThreadLocal 通过这样的 hashCode,计算当前 ThreadLocal 在 ThreadLocalMap 中的索引 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzcvMTcxZWZhYzdiNDcxM2Q2Mw?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzcvMTcxZWZhYzdhZjZjMDQwMg?x-oss-process=image/format,png) - -- 连续生成的哈希码之间的差值,该值的设定参考文章[ThreadLocal的hash算法(关于 0x61c88647)](https://juejin.im/post/5cced289f265da03804380f2) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzcvMTcxZWZhYzdiNmUwNjA0NQ?x-oss-process=image/format,png) - -- 注意 static 修饰。ThreadLocalMap 会被 set 多个 ThreadLocal ,而多个 ThreadLocal 就根据 threadLocalHashCode 区分 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzcvMTcxZWZhYzdkMTkzOWUwMw?x-oss-process=image/format,png) -# 4 ThreadLocalMap -自定义的哈希表,仅适用于维护线程本地的值。没有操作导出到ThreadLocal类之外。 -该类包私有,允许在 Thread 类中的字段声明。为帮助处理非常长的使用寿命,哈希表节点使用 WeakReferences 作为key。 -但由于不使用引用队列,因此仅在表空间不足时,才保证删除过时的节点。 -```java -static class ThreadLocalMap { - - /** - * 此哈希表中的节点使用其主引用字段作为key(始终是一个 ThreadLocal 对象),继承了 WeakReference。 - * 空键(即entry.get()== null)意味着不再引用该键,因此可以从表中删除该节点。 - * 在下面的代码中,此类节点称为 "stale entries" - */ - static class Entry extends WeakReference> { - /** 与此 ThreadLocal 关联的值 */ - Object value; - - Entry(ThreadLocal k, Object v) { - super(k); - value = v; - } - } - - private static final int INITIAL_CAPACITY = 16; - - private Entry[] table; - - private int size = 0; - - private int threshold; // 默认为 0 -``` -## 特点 -- key 是 ThreadLocal 的引用 -- value 是 ThreadLocal 保存的值 -- 数组的数据结构 -# 5 set -## 5.1 ThreadLocal#set -将此线程本地变量的当前线程副本设置为指定值。子类无需重写此方法,而仅依靠initialValue方法设置线程本地变量的值。 -![](https://img-blog.csdnimg.cn/20210616000550930.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 执行流程 -1. 获取当前线程 -2. 获取线程所对应的ThreadLocalMap。每个线程都是独立的,所以该方法天然线程安全 -3. 判断 map 是否为 null - - 否,K.V 对赋值,k 为this(即当前的 ThreaLocal 对象) - - 是,初始化一个 ThreadLocalMap 来维护 K.V 对 - -来具体看看ThreadLocalMap中的 set - -## 5.2 ThreadLocalMap#set -```java -private void set(ThreadLocal key, Object value) { - // 新引用指向 table - Entry[] tab = table; - int len = tab.length; - // 获取对应 ThreadLocal 在table 中的索引 - int i = key.threadLocalHashCode & (len-1); - - /** - * 从该下标开始循环遍历 - * 1、如遇相同key,则直接替换value - * 2、如果该key已经被回收失效,则替换该失效的key - */ - for (Entry e = tab[i]; - e != null; - e = tab[i = nextIndex(i, len)]) { - ThreadLocal k = e.get(); - // 找到内存地址一样的 ThreadLocal,直接替换 - if (k == key) { - e.value = value; - return; - } - // 若 k 为 null,说明 ThreadLocal 被清理了,则替换当前失效的 k - if (k == null) { - replaceStaleEntry(key, value, i); - return; - } - } - // 找到空位,创建节点并插入 - tab[i] = new Entry(key, value); - // table内元素size自增 - int sz = ++size; - // 达到阈值(数组大小的三分之二)时,执行扩容 - if (!cleanSomeSlots(i, sz) && sz >= threshold) - rehash(); -} -``` -注意通过 hashCode 计算的索引位置 i 处如果已经有值了,会从 i 开始,通过 +1 不断的往后寻找,直到找到索引位置为空的地方,把当前 ThreadLocal 作为 key 放进去。 - -# 6 get -```java -public T get() { - // 获取当前线程 - Thread t = Thread.currentThread(); - // 获取当前线程对应的ThreadLocalMap - ThreadLocalMap map = getMap(t); - - // 如果map不为空 - if (map != null) { - // 取得当前ThreadLocal对象对应的Entry - ThreadLocalMap.Entry e = map.getEntry(this); - // 如果不为空,读取当前 ThreadLocal 中保存的值 - if (e != null) { - @SuppressWarnings("unchecked") - T result = (T)e.value; - return result; - } - } - // 否则都执行 setInitialValue - return setInitialValue(); -} -``` -### setInitialValue -```java -private T setInitialValue() { - // 获取初始值,一般是子类重写 - T value = initialValue(); - - // 获取当前线程 - Thread t = Thread.currentThread(); - - // 获取当前线程对应的ThreadLocalMap - ThreadLocalMap map = getMap(t); - - // 如果map不为null - if (map != null) - - // 调用ThreadLocalMap的set方法进行赋值 - map.set(this, value); - - // 否则创建个ThreadLocalMap进行赋值 - else - createMap(t, value); - return value; -} -``` - -接着我们来看下 -## ThreadLocalMap#getEntry -```java -// 得到当前 thradLocal 对应的值,值的类型是由 thradLocal 的泛型决定的 -// 由于 thradLocalMap set 时解决数组索引位置冲突的逻辑,导致 thradLocalMap get 时的逻辑也是对应的 -// 首先尝试根据 hashcode 取模数组大小-1 = 索引位置 i 寻找,找不到的话,自旋把 i+1,直到找到索引位置不为空为止 -private Entry getEntry(ThreadLocal key) { - // 计算索引位置:ThreadLocal 的 hashCode 取模数组大小-1 - int i = key.threadLocalHashCode & (table.length - 1); - Entry e = table[i]; - // e 不为空,并且 e 的 ThreadLocal 的内存地址和 key 相同,直接返回,否则就是没有找到,继续通过 getEntryAfterMiss 方法找 - if (e != null && e.get() == key) - return e; - else - // 这个取数据的逻辑,是因为 set 时数组索引位置冲突造成的 - return getEntryAfterMiss(key, i, e); -} -// 自旋 i+1,直到找到为止 -private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { - Entry[] tab = table; - int len = tab.length; - // 在大量使用不同 key 的 ThreadLocal 时,其实还蛮耗性能的 - while (e != null) { - ThreadLocal k = e.get(); - // 内存地址一样,表示找到了 - if (k == key) - return e; - // 删除没用的 key - if (k == null) - expungeStaleEntry(i); - // 继续使索引位置 + 1 - else - i = nextIndex(i, len); - e = tab[i]; - } - return null; -} -``` -# 6 扩容 -ThreadLocalMap 中的 ThreadLocal 的个数超过阈值时,ThreadLocalMap 就要开始扩容了,我们一起来看下扩容的逻辑: -```java -private void resize() { - // 拿出旧的数组 - Entry[] oldTab = table; - int oldLen = oldTab.length; - // 新数组的大小为老数组的两倍 - int newLen = oldLen * 2; - // 初始化新数组 - Entry[] newTab = new Entry[newLen]; - int count = 0; - // 老数组的值拷贝到新数组上 - for (int j = 0; j < oldLen; ++j) { - Entry e = oldTab[j]; - if (e != null) { - ThreadLocal k = e.get(); - if (k == null) { - e.value = null; // Help the GC - } else { - // 计算 ThreadLocal 在新数组中的位置 - int h = k.threadLocalHashCode & (newLen - 1); - // 如果索引 h 的位置值不为空,往后+1,直到找到值为空的索引位置 - while (newTab[h] != null) - h = nextIndex(h, newLen); - // 给新数组赋值 - newTab[h] = e; - count++; - } - } - } - // 给新数组初始化下次扩容阈值,为数组长度的三分之二 - setThreshold(newLen); - size = count; - table = newTab; -} -``` -扩容时是绝对没有线程安全问题的,因为 ThreadLocalMap 是线程的一个属性,一个线程同一时刻只能对 ThreadLocalMap 进行操作,因为同一个线程执行业务逻辑必然是串行的,那么操作 ThreadLocalMap 必然也是串行的。 -# 7 总结 -我们在写中间件的时候经常会用到,比如说流程引擎中上下文的传递,调用链ID的传递等。 \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/\343\200\220\346\255\273\347\243\225JDK\346\272\220\347\240\201\343\200\221ThreadPoolExecutor\346\272\220\347\240\201\344\277\235\345\247\206\347\272\247\350\257\246\350\247\243.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/\343\200\220\346\255\273\347\243\225JDK\346\272\220\347\240\201\343\200\221ThreadPoolExecutor\346\272\220\347\240\201\344\277\235\345\247\206\347\272\247\350\257\246\350\247\243.md" deleted file mode 100644 index 6968385400..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/\343\200\220\346\255\273\347\243\225JDK\346\272\220\347\240\201\343\200\221ThreadPoolExecutor\346\272\220\347\240\201\344\277\235\345\247\206\347\272\247\350\257\246\350\247\243.md" +++ /dev/null @@ -1,686 +0,0 @@ -位运算表示线程池状态,因为位运算是改变当前值的一种高效手段。 - -# 属性 -## 线程池状态 -Integer 有32位: -- 最左边3位表示线程池状态,可表示从0至7的8个不同数值 -- 最右边29位表工作线程数 -```java -private static final int COUNT_BITS = Integer.SIZE - 3; -``` - - -线程池的状态用高3位表示,其中包括了符号位。五种状态的十进制值按从小到大依次排序为: - -```bash -RUNNING < SHUTDOWN < STOP < TIDYING =核心线程数 或线程创建失败,则将当前任务放到工作队列中 -// 只有线程池处于 RUNNING 态,才执行后半句 : 置入队列 -if (isRunning(c) && workQueue.offer(command)) { - int recheck = ctl.get(); - - // 只有线程池处于 RUNNING 态,才执行后半句 : 置入队列 - if (! isRunning(recheck) && remove(command)) - reject(command); - // 若之前的线程已被消费完,新建一个线程 - else if (workerCountOf(recheck) == 0) - addWorker(null, false); -// 核心线程和队列都已满,尝试创建一个新线程 -} -else if (!addWorker(command, false)) - // 抛出RejectedExecutionException异常 - // 若 addWorker 返回是 false,即创建失败,则唤醒拒绝策略. - reject(command); -} -``` -发生拒绝的理由有两个 -( 1 )线程池状态为非RUNNING状态 -(2)等待队列已满。 - -下面继续分析`addWorker` - -## addWorker 源码解析 -原子性地检查 runState 和 workerCount,通过返回 false 来防止在不应该添加线程时出现误报。 - -根据当前线程池状态,检查是否可以添加新的线程: -- 若可 -则创建并启动任务;若一切正常则返回true; -- 返回false的可能原因: -1. 线程池没有处`RUNNING`态 -2. 线程工厂创建新的任务线程失败 -### 参数 -- firstTask -外部启动线程池时需要构造的第一个线程,它是线程的母体 -- core -新增工作线程时的判断指标 - - true -需要判断当前`RUNNING`态的线程是否少于`corePoolsize` - - false -需要判断当前`RUNNING`态的线程是否少于`maximumPoolsize` -### JDK8源码 -```java -private boolean addWorker(Runnable firstTask, boolean core) { - // 1. 不需要任务预定义的语法标签,响应下文的continue retry - // 快速退出多层嵌套循环 - retry: - // 外自旋,判断线程池的运行状态 - for (;;) { - int c = ctl.get(); - int rs = runStateOf(c); - // 2. 若RUNNING态,则条件为false,不执行后面判断 - // 若STOP及以上状态,或firstTask初始线程非空,或队列为空 - // 都会直接返回创建失败 - // Check if queue empty only if necessary. - if (rs >= SHUTDOWN && - ! (rs == SHUTDOWN && - firstTask == null && - ! workQueue.isEmpty())) - return false; - - for (;;) { - int wc = workerCountOf(c); - // 若超过最大允许线程数,则不能再添加新线程 - if (wc >= CAPACITY || - wc >= (core ? corePoolSize : maximumPoolSize)) - return false; - // 3. 将当前活动线程数+1 - if (compareAndIncrementWorkerCount(c)) - break retry; - // 线程池状态和工作线程数是可变化的,需经常读取最新值 - c = ctl.get(); // Re-read ctl - // 若已关闭,则再次从retry 标签处进入,在第2处再做判断(第4处) - if (runStateOf(c) != rs) - continue retry; - //如果线程池还是RUNNING态,说明仅仅是第3处失败 -//继续循环执行(第5外) - // else CAS failed due to workerCount change; retry inner loop - } - } - - // 开始创建工作线程 - boolean workerStarted = false; - boolean workerAdded = false; - Worker w = null; - try { - // 利用Worker 构造方法中的线程池工厂创建线程,并封装成工作线程Worker对象 - // 和 AQS 有关!!! - w = new Worker(firstTask); - // 6. 注意这是Worker中的属性对象thread - final Thread t = w.thread; - if (t != null) { - // 在进行ThreadpoolExecutor的敏感操作时 - // 都需要持有主锁,避免在添加和启动线程时被干扰 - final ReentrantLock mainLock = this.mainLock; - mainLock.lock(); - try { - // Recheck while holding lock. - // Back out on ThreadFactory failure or if - // shut down before lock acquired. - int rs = runStateOf(ctl.get()); - // 当线程池状态为RUNNING 或SHUTDOWN - // 且firstTask 初始线程为空时 - if (rs < SHUTDOWN || - (rs == SHUTDOWN && firstTask == null)) { - if (t.isAlive()) // precheck that t is startable - throw new IllegalThreadStateException(); - workers.add(w); - int s = workers.size(); - // 整个线程池在运行期间的最大并发任务个数 - if (s > largestPoolSize) - // 更新为工作线程的个数 - largestPoolSize = s; - // 新增工作线程成功 - workerAdded = true; - } - } finally { - mainLock.unlock(); - } - if (workerAdded) { - // 看到亲切迷人的start方法了! - // 这并非线程池的execute 的command 参数指向的线程 - t.start(); - workerStarted = true; - } - } - } finally { - // 线程启动失败,把刚才第3处加,上的工作线程计数再减-回去 - if (! workerStarted) - addWorkerFailed(w); - } - return workerStarted; -} -``` -#### 第1处 -配合循环语句出现的标签,类似于goto语法作用。label 定义时,必须把标签和冒号的组合语句紧紧相邻定义在循环体之前,否则编译报错。目的是在实现多重循环时能够快速退出到任何一层。出发点似乎非常贴心,但在大型软件项目中,滥用标签行跳转的后果将是无法维护的! - - -在 **workerCount** 加1成功后,直接退出两层循环。 - -#### 第2处,这样的表达式不利于阅读,应如是 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzg1XzQ2ODU5NjgtMDg2ZTlkNWY5ZGEyYWZkNC5wbmc?x-oss-process=image/format,png) - -#### 第3处 -与第1处的标签呼应,`AtomicInteger`对象的加1操作是原子性的。`break retry`表 直接跳出与`retry` 相邻的这个循环体 - -#### 第4处 -此`continue`跳转至标签处,继续执行循环. -如果条件为false,则说明线程池还处于运行状态,即继续在`for(;)`循环内执行. - -#### 第5处 -`compareAndIncrementWorkerCount `方法执行失败的概率非常低. -即使失败,再次执行时成功的概率也是极高的,类似于自旋原理. -这里是先加1,创建失败再减1,这是轻量处理并发创建线程的方式; -如果先创建线程,成功再加1,当发现超出限制后再销毁线程,那么这样的处理方式明显比前者代价要大. - -#### 第6处 -`Worker `对象是工作线程的核心类实现。它实现了`Runnable`接口,并把本对象作为参数输入给`run()`中的`runWorker (this)`。所以内部属性线程`thread`在`start`的时候,即会调用`runWorker`。 - -```java -private final class Worker - extends AbstractQueuedSynchronizer - implements Runnable -{ - /** - * This class will never be serialized, but we provide a - * serialVersionUID to suppress a javac warning. - */ - private static final long serialVersionUID = 6138294804551838833L; - - /** Thread this worker is running in. Null if factory fails. */ - final Thread thread; - /** Initial task to run. Possibly null. */ - Runnable firstTask; - /** Per-thread task counter */ - volatile long completedTasks; - - /** - * Creates with given first task and thread from ThreadFactory. - * @param firstTask the first task (null if none) - */ - Worker(Runnable firstTask) { - setState(-1); // 直到调用runWorker前,禁止被中断 - this.firstTask = firstTask; - this.thread = getThreadFactory().newThread(this); - } - - /** 将主线程的 run 循环委托给外部的 runWorker 执行 */ - public void run() { - runWorker(this); - } - - // Lock methods - // - // The value 0 represents the unlocked state. - // The value 1 represents the locked state. - - protected boolean isHeldExclusively() { - return getState() != 0; - } - - protected boolean tryAcquire(int unused) { - if (compareAndSetState(0, 1)) { - setExclusiveOwnerThread(Thread.currentThread()); - return true; - } - return false; - } - - protected boolean tryRelease(int unused) { - setExclusiveOwnerThread(null); - setState(0); - return true; - } - - public void lock() { acquire(1); } - public boolean tryLock() { return tryAcquire(1); } - public void unlock() { release(1); } - public boolean isLocked() { return isHeldExclusively(); } - - void interruptIfStarted() { - Thread t; - if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) { - try { - t.interrupt(); - } catch (SecurityException ignore) { - } - } - } -} -``` -#### setState(-1)是为何 -设置个简单的状态,检查状态以防止中断。在调用停止线程池时会判断state 字段,决定是否中断之。 -![](https://img-blog.csdnimg.cn/20210713174701301.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70)![](https://img-blog.csdnimg.cn/20210713175625198.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70)![](https://img-blog.csdnimg.cn/20210713175645371.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70)![](https://img-blog.csdnimg.cn/20210713175718745.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -#### t 到底是谁? -![](https://img-blog.csdnimg.cn/20210713180707150.png) -# 源码分析 -```java - /** - * 检查是否可以根据当前池状态和给定的边界(核心或最大) - * 添加新工作线程。如果是这样,工作线程数量会相应调整,如果可能的话,一个新的工作线程创建并启动 - * 将firstTask作为其运行的第一项任务。 - * 如果池已停止此方法返回false - * 如果线程工厂在被访问时未能创建线程,也返回false - * 如果线程创建失败,或者是由于线程工厂返回null,或者由于异常(通常是在调用Thread.start()后的OOM)),我们干净地回滚。 - */ - private boolean addWorker(Runnable firstTask, boolean core) { - /** - * Check if queue empty only if necessary. - * - * 如果线程池已关闭,并满足以下条件之一,那么不创建新的 worker: - * 1. 线程池状态大于 SHUTDOWN,也就是 STOP, TIDYING, 或 TERMINATED - * 2. firstTask != null - * 3. workQueue.isEmpty() - * 简单分析下: - * 状态控制的问题,当线程池处于 SHUTDOWN ,不允许提交任务,但是已有任务继续执行 - * 当状态大于 SHUTDOWN ,不允许提交任务,且中断正在执行任务 - * 多说一句:若线程池处于 SHUTDOWN,但 firstTask 为 null,且 workQueue 非空,是允许创建 worker 的 - * - */ - if (rs >= SHUTDOWN && - ! (rs == SHUTDOWN && - firstTask == null && - ! workQueue.isEmpty())) - return false; - - for (;;) { - int wc = workerCountOf(c); - if (wc >= CAPACITY || - wc >= (core ? corePoolSize : maximumPoolSize)) - return false; - // 如果成功,那么就是所有创建线程前的条件校验都满足了,准备创建线程执行任务 - // 这里失败的话,说明有其他线程也在尝试往线程池中创建线程 - if (compareAndIncrementWorkerCount(c)) - break retry; - // 由于有并发,重新再读取一下 ctl - c = ctl.get(); // Re-read ctl - // 正常如果是 CAS 失败的话,进到下一个里层的for循环就可以了 - // 可如果是因为其他线程的操作,导致线程池的状态发生了变更,如有其他线程关闭了这个线程池 - // 那么需要回到外层的for循环 - if (runStateOf(c) != rs) - continue retry; - // else CAS failed due to workerCount change; retry inner loop - } - } - - /* * - * 到这里,我们认为在当前这个时刻,可以开始创建线程来执行任务 - */ - - // worker 是否已经启动 - boolean workerStarted = false; - // 是否已将这个 worker 添加到 workers 这个 HashSet 中 - boolean workerAdded = false; - Worker w = null; - try { - // 把 firstTask 传给 worker 的构造方法 - w = new Worker(firstTask); - // 取 worker 中的线程对象,Worker的构造方法会调用 ThreadFactory 来创建一个新的线程 - final Thread t = w.thread; - if (t != null) { - //先加锁 - final ReentrantLock mainLock = this.mainLock; - // 这个是整个类的全局锁,持有这个锁才能让下面的操作“顺理成章”, - // 因为关闭一个线程池需要这个锁,至少我持有锁的期间,线程池不会被关闭 - mainLock.lock(); - try { - // Recheck while holding lock. - // Back out on ThreadFactory failure or if - // shut down before lock acquired. - int rs = runStateOf(ctl.get()); - - // 小于 SHUTTDOWN 即 RUNNING - // 如果等于 SHUTDOWN,不接受新的任务,但是会继续执行等待队列中的任务 - if (rs < SHUTDOWN || - (rs == SHUTDOWN && firstTask == null)) { - // worker 里面的 thread 不能是已启动的 - if (t.isAlive()) // precheck that t is startable - throw new IllegalThreadStateException(); - // 加到 workers 这个 HashSet 中 - workers.add(w); - int s = workers.size(); - if (s > largestPoolSize) - largestPoolSize = s; - workerAdded = true; - } - } finally { - mainLock.unlock(); - } - // 若添加成功 - if (workerAdded) { - // 启动线程 - t.start(); - workerStarted = true; - } - } - } finally { - // 若线程没有启动,做一些清理工作,若前面 workCount 加了 1,将其减掉 - if (! workerStarted) - addWorkerFailed(w); - } - // 返回线程是否启动成功 - return workerStarted; - } -``` -看下 `addWorkFailed` -![](https://img-blog.csdnimg.cn/20210714141244398.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - -![记录 workers 中的个数的最大值,因为 workers 是不断增加减少的,通过这个值可以知道线程池的大小曾经达到的最大值](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzA4XzQ2ODU5NjgtMDc4NDcyYjY4MmZjYzljZC5wbmc?x-oss-process=image/format,png) -继续看 -### runWorker -```java -// worker 线程启动后调用,while 循环(即自旋!)不断从等待队列获取任务并执行 -// worker 初始化时,可指定 firstTask,那么第一个任务也就可以不需要从队列中获取 -final void runWorker(Worker w) { - Thread wt = Thread.currentThread(); - // 该线程的第一个任务(若有) - Runnable task = w.firstTask; - w.firstTask = null; - // 允许中断 - w.unlock(); - - boolean completedAbruptly = true; - try { - // 循环调用 getTask 获取任务 - while (task != null || (task = getTask()) != null) { - w.lock(); - // 若线程池状态大于等于 STOP,那么意味着该线程也要中断 - /** - * 若线程池STOP,请确保线程 已被中断 - * 如果没有,请确保线程未被中断 - * 这需要在第二种情况下进行重新检查,以便在关中断时处理shutdownNow竞争 - */ - if ((runStateAtLeast(ctl.get(), STOP) || - (Thread.interrupted() && - runStateAtLeast(ctl.get(), STOP))) && - !wt.isInterrupted()) - wt.interrupt(); - try { - // 这是一个钩子方法,留给需要的子类实现 - beforeExecute(wt, task); - Throwable thrown = null; - try { - // 到这里终于可以执行任务了 - task.run(); - } catch (RuntimeException x) { - thrown = x; throw x; - } catch (Error x) { - thrown = x; throw x; - } catch (Throwable x) { - // 这里不允许抛出 Throwable,所以转换为 Error - thrown = x; throw new Error(x); - } finally { - // 也是一个钩子方法,将 task 和异常作为参数,留给需要的子类实现 - afterExecute(task, thrown); - } - } finally { - // 置空 task,准备 getTask 下一个任务 - task = null; - // 累加完成的任务数 - w.completedTasks++; - // 释放掉 worker 的独占锁 - w.unlock(); - } - } - completedAbruptly = false; - } finally { - // 到这里,需要执行线程关闭 - // 1. 说明 getTask 返回 null,也就是说,这个 worker 的使命结束了,执行关闭 - // 2. 任务执行过程中发生了异常 - // 第一种情况,已经在代码处理了将 workCount 减 1,这个在 getTask 方法分析中说 - // 第二种情况,workCount 没有进行处理,所以需要在 processWorkerExit 中处理 - processWorkerExit(w, completedAbruptly); - } -} -``` -看看 -### getTask() -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzgwXzQ2ODU5NjgtNWU5NDc3MzE5M2Q5Y2Y0OS5wbmc?x-oss-process=image/format,png) -```java -// 此方法有三种可能 -// 1. 阻塞直到获取到任务返回。默认 corePoolSize 之内的线程是不会被回收的,它们会一直等待任务 -// 2. 超时退出。keepAliveTime 起作用的时候,也就是如果这么多时间内都没有任务,那么应该执行关闭 -// 3. 如果发生了以下条件,须返回 null -// 池中有大于 maximumPoolSize 个 workers 存在(通过调用 setMaximumPoolSize 进行设置) -// 线程池处于 SHUTDOWN,而且 workQueue 是空的,前面说了,这种不再接受新的任务 -// 线程池处于 STOP,不仅不接受新的线程,连 workQueue 中的线程也不再执行 -private Runnable getTask() { - boolean timedOut = false; // Did the last poll() time out? - - for (;;) { - // 允许核心线程数内的线程回收,或当前线程数超过了核心线程数,那么有可能发生超时关闭 - - // 这里 break,是为了不往下执行后一个 if (compareAndDecrementWorkerCount(c)) - // 两个 if 一起看:如果当前线程数 wc > maximumPoolSize,或者超时,都返回 null - // 那这里的问题来了,wc > maximumPoolSize 的情况,为什么要返回 null? - // 换句话说,返回 null 意味着关闭线程。 - // 那是因为有可能开发者调用了 setMaximumPoolSize 将线程池的 maximumPoolSize 调小了 - - // 如果此 worker 发生了中断,采取的方案是重试 - // 解释下为什么会发生中断,这个读者要去看 setMaximumPoolSize 方法, - // 如果开发者将 maximumPoolSize 调小了,导致其小于当前的 workers 数量, - // 那么意味着超出的部分线程要被关闭。重新进入 for 循环,自然会有部分线程会返回 null - int c = ctl.get(); - int rs = runStateOf(c); - - // Check if queue empty only if necessary. - if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { - // CAS 操作,减少工作线程数 - decrementWorkerCount(); - return null; - } - - int wc = workerCountOf(c); - - // Are workers subject to culling? - boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; - - if ((wc > maximumPoolSize || (timed && timedOut)) - && (wc > 1 || workQueue.isEmpty())) { - if (compareAndDecrementWorkerCount(c)) - return null; - continue; - } - - try { - Runnable r = timed ? - workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : - workQueue.take(); - if (r != null) - return r; - timedOut = true; - } catch (InterruptedException retry) { - // 如果此 worker 发生了中断,采取的方案是重试 - // 解释下为什么会发生中断,这个读者要去看 setMaximumPoolSize 方法, - // 如果开发者将 maximumPoolSize 调小了,导致其小于当前的 workers 数量, - // 那么意味着超出的部分线程要被关闭。重新进入 for 循环,自然会有部分线程会返回 null - timedOut = false; - } - } -} -``` -到这里,基本上也说完了整个流程,回到 execute(Runnable command) 方法,看看各个分支,我把代码贴过来一下: -```java - public void execute(Runnable command) { - if (command == null) - throw new NullPointerException(); - //表示 “线程池状态” 和 “线程数” 的整数 - int c = ctl.get(); - // 如果当前线程数少于核心线程数,直接添加一个 worker 执行任务, - // 创建一个新的线程,并把当前任务 command 作为这个线程的第一个任务(firstTask) - if (workerCountOf(c) < corePoolSize) { - // 添加任务成功,即结束 - // 执行的结果,会包装到 FutureTask - // 返回 false 代表线程池不允许提交任务 - if (addWorker(command, true)) - return; - - c = ctl.get(); - } - // 到这说明,要么当前线程数大于等于核心线程数,要么刚刚 addWorker 失败 - - // 如果线程池处于 RUNNING ,把这个任务添加到任务队列 workQueue 中 - if (isRunning(c) && workQueue.offer(command)) { - /* 若任务进入 workQueue,我们是否需要开启新的线程 - * 线程数在 [0, corePoolSize) 是无条件开启新线程的 - * 若线程数已经大于等于 corePoolSize,则将任务添加到队列中,然后进到这里 - */ - int recheck = ctl.get(); - // 若线程池不处于 RUNNING ,则移除已经入队的这个任务,并且执行拒绝策略 - if (! isRunning(recheck) && remove(command)) - reject(command); - // 若线程池还是 RUNNING ,且线程数为 0,则开启新的线程 - // 这块代码的真正意图:担心任务提交到队列中了,但是线程都关闭了 - else if (workerCountOf(recheck) == 0) - addWorker(null, false); - } - // 若 workQueue 满,到该分支 - // 以 maximumPoolSize 为界创建新 worker, - // 若失败,说明当前线程数已经达到 maximumPoolSize,执行拒绝策略 - else if (!addWorker(command, false)) - reject(command); - } -``` -**工作线程**:线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会循环获取工作队列里的任务来执行.我们可以从Worker类的run()方法里看到这点 - -```java - public void run() { - try { - Runnable task = firstTask; - firstTask = null; - while (task != null || (task = getTask()) != null) { - runTask(task); - task = null; - } - } finally { - workerDone(this); - } - } - boolean workerStarted = false; - boolean workerAdded = false; - Worker w = null; - try { - w = new Worker(firstTask); - - final Thread t = w.thread; - if (t != null) { - //先加锁 - final ReentrantLock mainLock = this.mainLock; - mainLock.lock(); - try { - // Recheck while holding lock. - // Back out on ThreadFactory failure or if - // shut down before lock acquired. - int rs = runStateOf(ctl.get()); - - if (rs < SHUTDOWN || - (rs == SHUTDOWN && firstTask == null)) { - if (t.isAlive()) // precheck that t is startable - throw new IllegalThreadStateException(); - workers.add(w); - int s = workers.size(); - if (s > largestPoolSize) - largestPoolSize = s; - workerAdded = true; - } - } finally { - mainLock.unlock(); - } - if (workerAdded) { - t.start(); - workerStarted = true; - } - } - } finally { - if (! workerStarted) - addWorkerFailed(w); - } - return workerStarted; - } -``` -线程池中的线程执行任务分两种情况 - - 在execute()方法中创建一个线程时,会让这个线程执行当前任务 - - 这个线程执行完上图中 1 的任务后,会反复从BlockingQueue获取任务来执行 \ No newline at end of file diff --git "a/JDK/JVM/JVM\345\206\205\345\255\230\346\250\241\345\236\213.md" "b/JDK/JVM/JVM\345\256\236\346\210\230---\345\206\205\345\255\230\346\250\241\345\236\213.md" similarity index 98% rename from "JDK/JVM/JVM\345\206\205\345\255\230\346\250\241\345\236\213.md" rename to "JDK/JVM/JVM\345\256\236\346\210\230---\345\206\205\345\255\230\346\250\241\345\236\213.md" index 949dedcae2..0768ba81f8 100644 --- "a/JDK/JVM/JVM\345\206\205\345\255\230\346\250\241\345\236\213.md" +++ "b/JDK/JVM/JVM\345\256\236\346\210\230---\345\206\205\345\255\230\346\250\241\345\236\213.md" @@ -352,6 +352,17 @@ Runtime类封装java应用运行时的环境,每个java应用程序都有一 对于在jvm中注册的多个关闭钩子,他们会并发执行,jvm并不能保证他们的执行顺序。 +# 参考 +《码出高效》 + +# X 交流学习 +![](https://img-blog.csdnimg.cn/20190504005601174.jpg) +## [Java交流群](https://jq.qq.com/?_wv=1027&k=5UB4P1T) +## [博客](https://blog.csdn.net/qq_33589510) + +## [Github](https://github.com/Wasabi1234) + + diff --git "a/JDK/JVM/JVM\345\236\203\345\234\276\346\224\266\351\233\206GC\347\256\227\346\263\225.md" "b/JDK/JVM/JVM\345\256\236\346\210\230---\345\236\203\345\234\276\346\224\266\351\233\206\347\256\227\346\263\225.md" similarity index 100% rename from "JDK/JVM/JVM\345\236\203\345\234\276\346\224\266\351\233\206GC\347\256\227\346\263\225.md" rename to "JDK/JVM/JVM\345\256\236\346\210\230---\345\236\203\345\234\276\346\224\266\351\233\206\347\256\227\346\263\225.md" diff --git "a/JDK/JVM/JVM\347\232\204\346\240\210\344\270\212\345\210\206\351\205\215\344\270\216\351\200\203\351\200\270\345\210\206\346\236\220\357\274\210Escape Analysis\357\274\211.md" "b/JDK/JVM/JVM\347\232\204\346\240\210\344\270\212\345\210\206\351\205\215\344\270\216\351\200\203\351\200\270\345\210\206\346\236\220\357\274\210Escape Analysis\357\274\211.md" new file mode 100644 index 0000000000..99703f884f --- /dev/null +++ "b/JDK/JVM/JVM\347\232\204\346\240\210\344\270\212\345\210\206\351\205\215\344\270\216\351\200\203\351\200\270\345\210\206\346\236\220\357\274\210Escape Analysis\357\274\211.md" @@ -0,0 +1,90 @@ +# 1 逃逸分析 +JVM中较前沿的优化技术,它与类型继承关系分析一样,并非直接优化代码,而是为其他优化措施提供依据的分析技术。 + +## 1.1 基本原理 +分析对象动态作用域,当一个对象在方法里面被定义后,它可能 +- 被外部方法所引用 +例如作为调用参数传递给其他方法,称为方法逃逸 +- 被外部线程访问 +譬如赋值给可以在其他线程中访问的实例变量,称为线程逃逸 + +从不逃逸 =》方法逃逸 =》线程逃逸,称为对象由低到高的不同逃逸程度。 + +如果能证明一个对象不会逃逸到方法或线程外(即别的方法或线程无法通过任何途径访问到该对象),或逃逸程度较低(只逃逸出方法而不逃逸出线程),则可能为这个对象实例采取不同程度的优化,如: + +# 2 栈上分配(Stack Allocations) +> 由于复杂度等原因,HotSpot中目前暂时还没有做这项优化,但一些其他的虚拟机(如Excelsior JET)使用了该优化。 + +JVM中,Java堆上分配创建对象的内存空间是常识,Java堆中的对象对各线程共享可见,只要持有该对象的引用,就可访问到堆中存储的对象数据。 +虚拟机的GC子系统会回收堆中不再使用的对象,但回收动作无论是标记筛选出可回收对象,还是回收和整理内存,都需耗费大量资源。 +如果确定一个对象不会逃逸出线程,那让该对象在栈上分配内存是个不错主意,对象所占用内存空间就可随栈帧出栈而销毁。 +在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例很大,如果能使用栈上分配,那大量对象就会随方法结束而自动销毁,GC子系统压力会下降很多。栈上分配可支持方法逃逸,但不能支持线程逃逸。 + +# 3 标量替换(Scalar Replacement) +若一个数据已经无法再分解成更小数据来表示,JVM中基础数据类型都不能再进一步分解,这些数据可被称为标量。 +相对的,如果一个数据可以继续分解,那它就被称为聚合量(Aggregate),Java 中的对象就是典型的聚合量。如果把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始类型来访问,该过程就称为标量替换。 +假如逃逸分析能够证明一个对象不会被方法外部访问,并且该对象可被分解,那么程序真正执行时将可能不去创建该对象,而改为直接创建它的若干个被这方法使用的成员变量代替。 +将对象拆分后,除可让对象的成员变量在栈上 (栈上存储的数据,很大机会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写外,还可为后续进步优化创建条件。 +标量替换可视作栈上分配一种特例,实现更简单(不用考虑对象完整结构的分配),但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内。 + + +# 4 同步消除(Synchronization Elimination) +线程同步本身是一个相对耗时过程,如果逃逸分析能确定一个变量不会逃逸出线程,无法被其他线程访问,那么该变量读写肯定不会有竞争, 对该变量实施的同步措施也可安全消除。 +关于逃逸分析的研究论文早在1999年就已经发表,但到JDK 6,HotSpot才开始支持初步逃逸分析,到现在这优化技术尚未足够成熟。 +不成熟的原因主要是逃逸分析的计算成本非常高,甚至不能保证逃逸分析带来的性能收益会高于它的消耗。要百分之百准确地判断一个对象是否会逃逸,需要进行一系列复杂的数据流敏感的过程间分析,才能确定程序各个分支执行时对此对象的影响。 +前面介绍即时编译、提前编译优劣势时提到了过程间分析这种大压力的分析算法正是即时编译的弱项。可以试想一下,如果逃逸分析完毕后发现几乎找不到几个不逃逸的对象, 那这些运行期耗用的时间就白白浪费了,所以目前虚拟机只能采用不那么准确,但时间压力相对较小的算法来完成分析。 +C和C++原生支持栈上分配(不使用new即可),而C#也支持值类型,可以自然做到标量替换(但并不会对引用类型做这种优化)。 +在灵活运用栈内存方面,确实是Java的弱项。 +在现在仍处于实验阶段的Valhalla项目里,设计了新的inline关键字用于定义Java的内联类型, 目的是实现与C#中值类型相对标的功能。有了这个标识与约束,以后逃逸分析做起来就会简单很多。 + +下面通过一系列Java伪代码的变化过程来模拟逃逸分析是如何工作的,向读者展示逃逸分析能够实现的效果。 +初始代码如下所示: + +```sql +// 完全未优化代码 +public int test(int x) { + int xx = x + 2; + Point p = new Point(xx, 42); + return p.getX(); +} +``` +此处省略了Point类的代码。第一步,将Point的构造函数和getX()方法进行内联优化: + +```sql + // 步骤1:构造函数内联后的样子 +public int test(int x) { + int xx = x + 2; + Point p = point_memory_alloc(); // 在堆中分配P对象的示意方法 + p.x = xx; // Point构造函数被内联后的样子 + p.y = 42 + return p.x; // Point::getX()被内联后的样子 +} +``` + +第二步,经过逃逸分析,发现在整个test()方法的范围内Point对象实例不会发生任何程度的逃逸, 这样可以对它进行标量替换优化,把其内部的x和y直接置换出来,分解为test()方法内的局部变量,从 而避免Point对象实例被实际创建,优化后的结果如下所示: + +```java + // 步骤2:标量替换后的样子 + public int test(int x) { + int xx = x + 2; + int px = xx; + int py = 42 + return px; + } +``` + +第三步,通过数据流分析,发现py的值其实对方法不会造成任何影响,那就可以放心地去做无效 代码消除得到最终优化结果,如下所示: + +```java +// 步骤3:做无效代码消除后的样子 +public int test(int x) { + return x + 2; +} +``` + +从测试结果来看,实施逃逸分析后的程序在MicroBenchmarks中往往能得到不错的成绩,但是在实际的应用程序中,尤其是大型程序中反而发现实施逃逸分析可能出现效果不稳定的情况,或分析过程耗时但却无法有效判别出非逃逸对象而导致性能(即时编译的收益)下降,所以曾经在很长的一段时 间里,即使是服务端编译器,也默认不开启逃逸分析(从JDK 6 Update 23开始,服务端编译器中开始才默认开启逃逸分析。 +),甚至在某些版本(如JDK 6 Update 18)中还曾完全禁止这项优化,一直到JDK 7时这项优化才成为服务端编译器默认开启的选项。如果有需要,或者确认对程序运行有益,用户也可以使用参数-XX:+DoEscapeAnalysis来手动开启逃逸分析, 开启之后可以通过参数-XX:+PrintEscapeAnalysis来查看分析结果。有了逃逸分析支持之后,用户可使用参数-XX:+EliminateAllocations来开启标量替换,使用+XX:+EliminateLocks来开启同步消 除,使用参数-XX:+PrintEliminateAllocations查看标量的替换情况。 +尽管目前逃逸分析技术仍在发展之中,未完全成熟,但它是即时编译器优化技术的一个重要前进 方向,在日后的Java虚拟机中,逃逸分析技术肯定会支撑起一系列更实用、有效的优化技术。 + +# 参考 +《深入理解 Java 虚拟机》 \ No newline at end of file diff --git "a/JDK/JVM/JVM\351\200\203\351\200\270\345\210\206\346\236\220.md" "b/JDK/JVM/JVM\351\200\203\351\200\270\345\210\206\346\236\220.md" deleted file mode 100644 index e65e59102a..0000000000 --- "a/JDK/JVM/JVM\351\200\203\351\200\270\345\210\206\346\236\220.md" +++ /dev/null @@ -1,215 +0,0 @@ -1 逃逸分析 - - -JVM中高深的优化技术,如同类继承关系分析,该技术并非直接去优化代码,而是一种为其他优化措施提供依据的分析技术。 - -分析对象的动态作用域,当某对象在方法里被定义后,它可能 - -* **方法**逃逸 - - 被外部方法引用,例如作为参数传递给其他方法 - -* **线程**逃逸 - - 被外部线程访问,例如赋值给可以在其他线程中访问的实例变量 - - - - -所以 Java 对象由低到高的逃逸程度即为: - -* **不逃逸 =》** - -* **方法逃逸 =》** - -* **线程逃逸** - - - - -若能确定一个对象 - -* 不会逃逸到方法或线程外(即其它方法、线程无法访问到该对象) - -* 或逃逸程度较低(只逃逸出方法而不逃逸出线程) - - -则可为该对象实例采取不同程度的优化方案。 - -2 优化方案 - - - - - - - - - -2.1  栈上分配(Stack Allocations) - - - - -> 由于复杂度等原因,HotSpot中目前暂时还没有做这项优化,但一些其他的虚拟机(如Excelsior JET)使用了该优化。 - -JVM的GC模块会回收堆中不再使用的对象,但如下回收动作 - -* 标记筛选出可回收对象 - -* 回收和整理内存 - - -都需耗费大量资源。 -若确定一个对象不会逃逸出线程,那让该对象在栈上分配内存就是个不错主意,对象所占用内存空间就可随栈帧出栈而销毁。 - - -在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所占比例很大,若能使用栈上分配,则大量对象就会随方法结束而自动销毁,GC系统压力会下降很多。 - -**栈上分配可支持方法逃逸,但不能支持线程逃逸。** - - - - -2.2 标量替换(Scalar Replacement) - - -2.2.1 标量 - - - - -若一个数据已经无法再分解成更小数据,JVM中的原始数据类型(如 int、long 等数值类型及 reference 类型)都不能再进一步分解,这些数据即为标量。 - - -2.2.2 聚合量 - - - - -若一个数据可继续分解,则称为聚合量(Aggregate),比如 Java 对象就是聚合量。 - - -2.2.3 标量替换 - - - - -把一个Java对象拆散,根据程序访问情况,将其用到的成员变量恢复为原始类型来访问。 - - -假如逃逸分析能证明一个对象不会被方法外部访问,并且该对象可被分解,那么程序真正执行时将可能不去创建该对象,而改为直接创建它的若干个被这方法使用的成员变量。 -将对象拆分后: - -* 可让对象的成员变量在栈上 (栈上存储的数据,很大概率会被JVM分配至物理机器的高速寄存器中存储)分配和读写 - -* 为后续进步优化创建条件 - - -2.2.4 适用场景 - - - -标量替换可视为栈上分配一种特例,实现更简单(不用考虑对象完整结构的分配),但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内。 - - - - - -2.3 同步消除(Synchronization Elimination) - - - - -线程同步是个相对耗时的过程,若逃逸分析能确定一个变量不会逃逸出线程,即不会被其他线程访问,则该变量的读写肯定不会有线程竞争, 也可安全消除对该变量实施的同步措施。 - -> 逃逸分析的论文在1999年就已发表,但到JDK 6,HotSpot才开始初步支持逃逸分析,至今该也尚未成熟,主要因为逃逸分析的计算成本高到无法保证带来的性能收益会高于它的消耗。要百分百准确判断一个对象是否会逃逸,需进行一系列复杂数据流敏感的过程间分析,才能确定程序各个分支执行时对此对象的影响。过程间分析这种大压力的分析算法正是即时编译的弱项。试想,若逃逸分析完毕后发现几乎找不到几个不逃逸的对象, 那这些运行期耗用的时间就白费了,所以目前JVM只能采用不那么准确,但时间压力相对较小的算法来完成分析。 - -C和C++原生支持栈上分配(不使用new即可),灵活运用栈内存方面,Java的确是弱势群体。 -在现在仍处于实验阶段的Valhalla项目,设计了新的inline关键字用于定义Java的内联类型, 对标C#的值类型。有了该标识与约束,以后逃逸分析做起来就会简单很多。 - -3 代码实战验证 - - - - - - - - - -3.1 全无优化的代码  - - - -``` -public int test(int x) {   int xx = x + 2;   Point p = new Point(xx, 42);   return p.getX(); } -``` - - - - - - - -3.2 优化step1:内联构造器和getX()方法 - - - -``` -public int test(int x) {   int xx = x + 2;  // 在堆中分配P对象   Point p = point\_memory\_alloc();  // Point构造器被内联后    p.x = xx;   p.y = 42;  // Point::getX()被内联后   return p.x;} -``` - - - - - - - -优化step2:标量替换 - - - - -逃逸分析后,发现在整个test()方法的范围内Point对象实例不会发生任何程度逃逸, 便可对它进行标量替换:把其内部的x和y直接置换出来,分解为test()方法内的局部变量,从而避免了Point对象实例的创建 - -``` -public int test(int x) {    int xx = x + 2;    int px = xx;    int py = 42    return px; } -``` - - -step3:无效代码消除 - - -数据流分析,发现py的值其实对方法不会造成任何影响,那就可以放心地去做无效代码消除得到最终优化结果,如下所示: - -``` -public int test(int x) {   return x + 2; } -``` - -观察测试结果,实施逃逸分析后的程序在MicroBenchmarks中往往能得到不错的成绩,但在实际应用程序中,尤其是大型程序中反而发现实施逃逸分析可能出现效果不稳定,或分析过程耗时但却无法有效判别出非逃逸对象而导致性能(即时编译的收益)下降,所以曾经在很长的一段时间,即使是服务端编译器,也默认不开启逃逸分析(从JDK 6 Update 23开始,服务端编译器中开始才默认开启逃逸分析。),甚至在某些版本(如JDK 6 Update 18)中还曾完全禁止这项优化,一直到JDK 7时这项优化才成为服务端编译器默认开启的选项。 - -若有需要或确认对程序有益,可使用参数: - -* **-XX:+DoEscapeAnalysis **手动开启逃逸分析 - - -开启后可通过参数: - -* **-XX:+PrintEscapeAnalysis **查看分析结果 - - -有逃逸分析支持后,用户可使用如下参数: - -* **-XX:+EliminateAllocations **开启标量替换 - -* **+XX:+EliminateLocks **开启同步消除 - -* **-XX:+PrintEliminateAllocations **查看标量的替换情况 - - - 让我们一起期待该JIT优化技术之逃逸分析的发展。 - -> 参考 -> -> 《深入理解 Java 虚拟机》 \ No newline at end of file diff --git "a/JDK/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213\346\267\261\345\205\245\350\257\246\350\247\243(JMM).md" "b/JDK/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213\346\267\261\345\205\245\350\257\246\350\247\243(JMM).md" deleted file mode 100644 index 1dbb544b41..0000000000 --- "a/JDK/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213\346\267\261\345\205\245\350\257\246\350\247\243(JMM).md" +++ /dev/null @@ -1,128 +0,0 @@ -# 前言 -定义俩共享变量及俩方法: -- 第一个方法, -- 第二个方法 -- (r1,r2)的可能值有哪些? -![](https://img-blog.csdnimg.cn/05139ccfbb40447a869632ff35959841.png) - -在单线程环境下,可先调用第一个方法,最终(r1,r2)为(1,0) -也可以先调用第二个方法,最终为(0,2)。 - -![](https://img-blog.csdnimg.cn/20200404214401993.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -# 1 Java内存模型的意义 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTkyYTFmZGY0OGJlMTllMDYucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTIzZTVlOWE0OWFkZWI1YTEucG5n?x-oss-process=image/format,png) -JMM 与硬件内存架构对应关系![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTVlMTM3NGEwYWJmOWM5MjkucG5n?x-oss-process=image/format,png) -JMM抽象结构图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQ0ZWE4ODQzYTg4YTk0MGQucG5n?x-oss-process=image/format,png) -内存模型描述程序的可能行为。 - -Java虚拟机规范中试图定义一种Java内存模型,`来屏蔽掉各种硬件和os的内存访问差异`,规定: -- 线程如何、何时能看到其他线程修改过的共享变量的值 -- 必要时,如何同步地访问共享变量 - -以实现让Java程序在各种平台下都能达到一致性的内存访问效果。 - -JMM通过检查执行跟踪中的每个读操作,并根据某些规则检查该读操作观察到的写操作是否有效来工作。 - -只要程序的所有执行产生的结果都可由JMM预测。具体实现者任意实现,包括操作的重新排序和删除不必要的同步。 - -JMM决定了在程序的每个点上可以读取什么值。 -## 1.1 共享变量(Shared Variables) -可在线程之间共享的内存称为`共享内存或堆内存`。所有实例字段、静态字段和数组元素都存储在堆内存。 -不包括局部变量与方法参数,因为这些是线程私有的,不存在共享。 - -对同一变量的两次访问(读或写),若有一个是写请求,则是冲突的! -# 2 主内存与工作内存 -工作内存缓存 -![](https://img-blog.csdnimg.cn/20191014024209488.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -JMM的主要是定义了`各个变量的访问规则`,在JVM中的如下底层细节: -- 将变量存储到内存 -- 从内存中取出变量值 - -为获得较好执行效率,JMM并未限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器调整代码执行顺序这类权限。 - -JMM规定: -- 所有变量都存储在主内存(Main Memory) -- 每条线程有自己的工作内存(Working Memory) -保存了该线程使用到的`变量的主内存副本拷贝`(线程所访问对象的引用或者对象中某个在线程访问到的字段,不会是整个对象的拷贝) -线程对变量的所有操作(读,赋值等)都必须在工作内存进行,不能直接读写主内存中的变量 -volatile变量依然有工作内存的拷贝,,是他特殊的操作顺序性规定,看起来如同直接在主内存读写 -不同线程间,无法直接访问对方工作内存中的变量,线程间变量值的传递均要通过主内存 - -线程、主内存、工作内存三者的交互关系: -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTEyMjA5YjEyZDU3OGEyZWQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWJiM2QzN2MxNTVjZDgyZDgucG5n?x-oss-process=image/format,png) - -JVM模型与JMM不是同一层次的内存划分,基本毫无关系的,硬要对应起来,从变量,内存,工作内存的定义来看 -- 主内存 《=》Java堆中的对象实例数据部分 -- 工作内存 《=》虚拟机栈中的部分区域 - -从更底层的层次来看: -- 主内存直接对应物理硬件的内存 -- 为更好的运行速度,虚拟机(甚至硬件系统的本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存器,因为程序运行时主要访问读写的是工作内存 -# 3 内存间同步操作 -## 3.1 线程操作的定义 -### 操作定义 -write要写的变量以及要写的值。 -read要读的变量以及可见的写入值(由此,我们可以确定可见的值)。 -lock要锁定的管程(监视器monitor)。 -unlock要解锁的管程。 -外部操作(socket等等..) -启动和终止 -### 程序顺序 -如果一个程序没有数据竞争,那么程序的所有执行看起来都是顺序一致的 - -本规范只涉及线程间的操作; -一个变量如何从主内存拷贝到工作内存,从工作内存同步回主内存的实现细节 - -JMM 本身已经定义实现了以下8种操作来完成,且都具备`原子性` -- lock(锁定) -作用于主内存变量,把一个变量标识为一条线程独占的状态 -- unlock(解锁) -作用于主内存变量,把一个处于锁定状态的变量释放,释放后的变量才可以被其它线程锁定 -unlock之前必须将变量值同步回主内存 -- read(读取) -作用于主内存变量,把一个变量的值从主内存传输到工作内存,以便随后的load -- load(载入) -作用于工作内存变量,把read从主内存中得到的变量值放入工作内存的变量副本 -- use(使用) -作用于工作内存变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到的变量的值得字节码指令时将会执行这个操作 -- assign(赋值) -作用于工作内存变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 -- store(存储) -作用于工作内存变量,把工作内存中一个变量的值传送到主内存,以便随后的write操作使用 -- write(写入) -作用于主内存变量,把store操作从工作内存中得到的值放入主内存的变量中 - -- 把一个变量从主内存`复制`到工作内存 -就要顺序执行read和load - -- 把变量从工作内存`同步`回主内存 -就要顺序地执行store和write操作 - -JMM只要求上述两个操作必须`按序执行`,而没有保证连续执行 -也就是说read/load之间、store/write之间可以插入其它指令 -如对主内存中的变量a,b访问时,一种可能出现的顺序是read a->readb->loadb->load a - -JMM规定执行上述八种基础操作时必须满足如下 -## 3.1 同步规则 -◆ 对于监视器 m 的解锁与所有后续操作对于 m 的加锁 `同步`(之前的操作保持可见) -◆对 volatile变量v的写入,与所有其他线程后续对v的读同步 - -◆ `启动` 线程的操作与线程中的第一个操作同步 -◆ 对于每个属性写入默认值(0, false, null)与每个线程对其进行的操作同步 -◆ 线程 T1的最后操作与线程T2发现线程T1已经结束同步。( isAlive ,join可以判断线程是否终结) -◆ 如果线程 T1中断了T2,那么线程T1的中断操作与其他所有线程发现T2被中断了同步通过抛出*InterruptedException*异常,或者调用*Thread.interrupted*或*Thread.isInterrupted* - -- 不允许read/load、store/write操作之一单独出现 -不允许一个变量从主内存读取了但工作内存不接收,或从工作内存发起回写但主内存不接收 -- 不允许一个线程丢弃它的最近的assign -即变量在工作内存中改变(为工作内存变量赋值)后必须把该变化同步回主内存 -- 新变量只能在主内存“诞生”,不允许在工作内存直接使用一个未被初始化(load或assign)的变量 -换话说就是一个变量在实施use,store之前,必须先执行过assign和load -- 如果一个变量事先没有被load锁定,则不允许对它执行unlock,也不允许去unlock一个被其它线程锁定的变量 -- 对一个变量执行unloack前,必须把此变量同步回主内存中(执行store,write) - -> 参考 -> - https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.1 \ No newline at end of file diff --git "a/JDK/JVM/Java\346\200\247\350\203\275\350\260\203\344\274\230\345\267\245\345\205\267\344\271\213JDK\345\221\275\344\273\244\350\241\214.md" "b/JDK/JVM/Java\346\200\247\350\203\275\350\260\203\344\274\230\345\267\245\345\205\267\344\271\213JDK\345\221\275\344\273\244\350\241\214.md" index 59c68099d5..fd762cc410 100644 --- "a/JDK/JVM/Java\346\200\247\350\203\275\350\260\203\344\274\230\345\267\245\345\205\267\344\271\213JDK\345\221\275\344\273\244\350\241\214.md" +++ "b/JDK/JVM/Java\346\200\247\350\203\275\350\260\203\344\274\230\345\267\245\345\205\267\344\271\213JDK\345\221\275\344\273\244\350\241\214.md" @@ -1,30 +1,21 @@ ## 1.1 jps -类似Linux的ps,但jps只列出Java的进程。可方便查看Java进程的启动类、传入参数和JVM参数。直接运行,不加参数,列出Java程序的进程ID及Main函数名称。 +类似Linux的ps,但是jps只用于列出Java的进程 +可以方便查看Java进程的启动类,传入参数和JVM参数等 +直接运行,不加参数,列出Java程序的进程ID以及Main函数等名称 ![jps命令本质也是Java程序](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTljMjE4OWRlZDljYmQ1M2UucG5n?x-oss-process=image/format,png) -- -m 输出传递给Java进程的参数![](https://img-blog.csdnimg.cn/20210117135731422.png) - -- -l 输出主函数的完整路径![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWJkOGY1NzU2NWQzNDY1NDMucG5n?x-oss-process=image/format,png) -- -q 只输出进程ID![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWUzODhhY2U5MmYzMDNkYWYucG5n?x-oss-process=image/format,png) -- -v 显示传递给jvm的参数![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTlhM2JhYjkzZjk0Y2U2YzgucG5n?x-oss-process=image/format,png) +![-m 输出传递给Java进程的参数](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTE1ZDAzZGJkNWY4MDIxMDkucG5n?x-oss-process=image/format,png) +![-l 输出主函数的完整路径](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWJkOGY1NzU2NWQzNDY1NDMucG5n?x-oss-process=image/format,png) +![-q 只输出进程ID](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWUzODhhY2U5MmYzMDNkYWYucG5n?x-oss-process=image/format,png) +![-v 显示传递给jvm的参数](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTlhM2JhYjkzZjk0Y2U2YzgucG5n?x-oss-process=image/format,png) ## 1.2 jstat -观察Java应用程序运行时信息的工具,详细查看堆使用情况以及GC情况 -- jstat -options -![](https://img-blog.csdnimg.cn/20210117142257563.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - +用于观察Java应用程序运行时信息的工具,详细查看堆使用情况以及GC情况 +![jstat -options](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWZiYzg1NWU5NDk3MTljNzcucG5n?x-oss-process=image/format,png) ### 1.2.1 jstat -class pid 显示加载class的数量及所占空间等信息 -- -compiler -t:显示JIT编译的信息 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTcyM2Y5ZjA4MjMyMjcyMDQucG5n?x-oss-process=image/format,png) +![-compiler -t:显示JIT编译的信息](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTcyM2Y5ZjA4MjMyMjcyMDQucG5n?x-oss-process=image/format,png) ### 1.2.2 -gc pid -显示gc信息,查看gc的次数及时间 -![](https://img-blog.csdnimg.cn/20210117144320641.png) - -```shell -➜ ~ jstat -gc 87552 - S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT -25088.0 20992.0 0.0 20992.0 500224.0 56227.0 363008.0 35238.1 76672.0 72902.5 10368.0 9590.5 9 0.078 3 0.162 - - 0.239 -``` - +可以显示gc的信息,查看gc的次数及时间 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTZiODFlOTZjOWNlNGExMjIucG5n?x-oss-process=image/format,png) ### 1.2.3 -gccapacity 比-gc多了各个代的最大值和最小值 ![jstat -gccapacity 3661](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LThmMGMwNTY4YTgzM2M5MzkucG5n?x-oss-process=image/format,png) @@ -44,28 +35,25 @@ ![-gcoldcapacity展现老年代的容量信息](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTc0NjhlMDA4YTUwZGMyMDQucG5n?x-oss-process=image/format,png) ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTA0OWRjZjBhYzRkNjM0YjYucG5n?x-oss-process=image/format,png) ### -gcutil -相比于-gc 参数,只显示使用率而非使用量了。 显示GC回收相关信息 -![](https://img-blog.csdnimg.cn/20210117144845233.png) - +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTNhYmVkNDJmOTdiOWZmYzQucG5n?x-oss-process=image/format,png) ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWY5MDZjZTFhZDA2MDA3YTAucG5n?x-oss-process=image/format,png) ### -printcompilation 当前VM执行的信息 ![jstat -printcompilation 3661](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWMxMzk3NmMzZjE4OTViYWIucG5n?x-oss-process=image/format,png) ### 还可以同时加两个数 -- 输出进程4798的ClassLoader信息,每1秒统计一次,共输出2次 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTBjMjcyNGI5MzIwOWU4ZjIucG5n?x-oss-process=image/format,png) +![输出进程4798的ClassLoader信息,每1秒统计一次,共输出2次](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTBjMjcyNGI5MzIwOWU4ZjIucG5n?x-oss-process=image/format,png) ## 1.3 jinfo `jinfo