diff --git a/.gitignore b/.gitignore index 7ee001fcc8..9a016ff1bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,26 @@ -*.DS_Store -*.iml -*.xml -*.txt -*.dio +# Created by .ignore support plugin (hsz.mobi) +### Java template +# Compiled class file +*.class + +# Log file *.log -.idea/inspectionProfiles/ -.metals/metals.h2.db -.vscode/settings.json +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar -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 +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index a55e7a179b..0000000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git "a/Git/Git\344\273\223\345\272\223\345\210\235\345\247\213\345\214\226.md" "b/Git/Git\344\273\223\345\272\223\345\210\235\345\247\213\345\214\226.md" deleted file mode 100644 index 01e47e4ac6..0000000000 --- "a/Git/Git\344\273\223\345\272\223\345\210\235\345\247\213\345\214\226.md" +++ /dev/null @@ -1,11 +0,0 @@ -git仓库的建立 -![这里写图片描述](http://upload-images.jianshu.io/upload_images/4685968-946cf05120484d9a?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -ssh公钥配置 -![这里写图片描述](http://upload-images.jianshu.io/upload_images/4685968-ec3c7dd8307210db?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -本地仓库建立成功 -![这里写图片描述](http://upload-images.jianshu.io/upload_images/4685968-a08b8ae97290f41d?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -切换分支并配置相关目录 - -> 不在master上开发,作为一个只读的分支开放给开发者,而开发时会切分支来开发 - -![这里写图片描述](http://upload-images.jianshu.io/upload_images/4685968-a07fb89fd2831ead?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) diff --git "a/Git/Git\344\277\256\346\224\271\345\267\262\346\217\220\344\272\244\347\232\204commit.md" "b/Git/Git\344\277\256\346\224\271\345\267\262\346\217\220\344\272\244\347\232\204commit.md" deleted file mode 100644 index ef35dd30cb..0000000000 --- "a/Git/Git\344\277\256\346\224\271\345\267\262\346\217\220\344\272\244\347\232\204commit.md" +++ /dev/null @@ -1,124 +0,0 @@ -# 1 本地修改 -由于以下修改本身是对版本历史的修改,在需要push到远程仓库时,往往是不成功的,只能强行push,这样会出现的一个问题就是,如果你是push到多人协作的远程仓库中,会对其他人的远程操作构成影响。通常情况下,建议与项目远程仓库的管理员进行沟通,在完成你强制push操作后,通知其他人同步。 -## 1.1 修改最近一次的commit -- 修改提交的描述 -``` -git commit --amend -``` -然后会进入一个文本编辑器界面,修改commit的描述内容,即可完成操作。 -- 修改提交的文件 -``` -git add # 或者 git rm -git commit --amend # 将缓存区的内容做为最近一次提交 -``` -## 1.2 修改任意提交历史位置的commit - -可以通过变基命令,修改最近一次提交以前的某次提交。不过修改的提交到当前提交之间的所有提交的hash值都会改变。 -变基操作需要非常小心,一定要多用git status命令来查看你是否还处于变基操作,可能某次误操作的会对后面的提交历史造成很大影响。 - -首先查看提交日志,以便变基后,确认提交历史的修改 -``` -git log -``` -变基操作。 可以用commit~n或commit^^这种形式替代:前者表示当前提交到n次以前的提交,后者`^`符号越多表示的范围越大,commit可以是HEAD或者某次提交的hash值;-i参数表示进入交互模式。 -``` -git rebase -i -``` -以上变基命令会进入文本编辑器,其中每一行就是某次提交,把pick修改为edit,保存退出该文本编辑器。 - -**注意:**变基命令打开的文本编辑器中的commit顺序跟git log查看的顺序是相反的,也就是最近的提交在下面,老旧的提交在上面 - -**注意:**变基命令其实可以同时对多个提交进行修改,只需要修改将对应行前的pick都修改为edit,保存退出后会根据你修改的数目多次打开修改某次commit的文本编辑器界面。但是这个范围内的最终祖先commit不能修改,也就是如果有5行commit信息,你只能修改下面4行的,这不仅限于commit修改,重排、删除以及合并都如此。 -``` -git commit --amend -``` -接下来修改提交描述内容或者文件内容,跟最近一次的commit的操作相同,不赘述。 - -然后完成变基操作 -``` -git rebase --continue -``` -有时候会完成变基失败,需要`git add --all`才能解决,一般git会给出提示。 - -再次查看提交日志,对比变基前后的修改,可以看到的内的所有提交的hash值都被修改了 -``` -git log -``` -如果过了一段时间后,你发现这次历史修改有误,想退回去怎么办?请往下继续阅读 - -## 1.3 重排或删除某些提交 - -变基命令非常强大,还可以将提交历史重新手动排序或者删除某次提交。这为某些误操作,导致不希望公开信息的提交,提供了补救措施 - -git rebase -i -如前面描述,这会进入文本编辑器,对某行提交进行排序或者删除,保存退出。可以是多行修改。 - -后续操作同上。 - -## 1.4 合并多次提交 - -非关键性的提交太多会让版本历史很难看、冗余,所以合并多次提交也是挺有必要的。同样是使用以上的变基命令,不同的是变基命令打开的文本编辑器里的内容的修改。 - -将pick修改为squash,可以是多行修改,然后保存退出。这个操作会将标记为squash的所有提交,都合并到最近的一个祖先提交上。 - -**注意:**不能对的第一行commit进行修改,至少保证第一行是接受合并的祖先提交。 - -后续操作同上。 - -## 1.5 分离某次提交 - -变基命令还能分离提交,这里不描述,详情查看后面的参考链接 - -终极手段 - -git还提供了修改版本历史的“大杀器”——filter-branch,可以对整个版本历史中的每次提交进行修改,可用于删除误操作提交的密码等敏感信息。 - -删除所有提交中的某个文件 -``` -git filter-branch --treefilter 'rm -f password.txt' HEAD -``` -将新建的主目录作为所有提交的根目录 -``` -git filter-branch --subdirectory-filter trunk HEAD -``` -本地回退 -回退操作也是对过往提交的一剂“后悔药”,常用的回退方式有三种:checkout、reset和revert - -checkout - -对单个文件进行回退。不会修改当前的HEAD指针的位置,也就是提交并未回退 - -可以是某次提交的hash值,或者HEAD(缺省即默认) -``` -git checkout -- -reset -``` -回退到某次提交。回退到的指定提交以后的提交都会从提交日志上消失 -**注意:**工作区和暂存区的内容都会被重置到指定提交的时候,如果不加--hard则只移动HEAD的指针,不影响工作区和暂存区的内容。 -``` -git reset --hard -``` -结合git reflog找回提交日志上看不到的版本历史,撤回某次操作前的状态 - -git reflog # 找到某次操作前的提交hash值 -git reset -这个方法可以对你的回退操作进行回退,因为这时候git log命令已经找不到历史提交的hash值了。 - -revert - -这个方法是最温和,最受推荐的,因为本质上不是修改过去的版本历史,而是将回退版本历史作为一次新的提交,所以不会改变版本历史,在push到远程仓库的时候也不会影响到团队其他人。 - -git revert -远程修改 -对远程仓库的版本历史修改,都是在本地修改的基础上进行的:本地修改完成后,再push到远程仓库。 - -但是除了git revert可以直接push,其他都会对原有的版本历史修改,只能使用强制push - -git push -f -总结 -git commit --amend改写单次commit -git rebase -i 删改排以及合并多个commit -git checkout -- 获取历史版本的某个文件 -git reset [--hard] 移动HEAD指针 -git revert 创建一个回退提交 -git push -f 强制push,覆盖原有远程仓库 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" deleted file mode 100644 index 3abed05a64..0000000000 --- "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" +++ /dev/null @@ -1,433 +0,0 @@ - -# 0 前言 -全是干货的技术殿堂 - -> `文章收录在我的 GitHub 仓库,欢迎Star/fork:` -> [Java-Interview-Tutorial](https://github.com/Wasabi1234/Java-Interview-Tutorial) -> https://github.com/Wasabi1234/Java-Interview-Tutorial - -# 下载安装及基本配置 -[Git官网下载](https://git-scm.com/download/win) -[Git GUI下载](https://desktop.github.com/) - -安装成功后,打开,右击选择options进行个性化设置: -- 外观 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA4MDcvNTA4ODc1NV8xNTY1MTQzNjI0OTg2XzIwMTkwODA1MTUxNDM0MTYwLnBuZw?x-oss-process=image/format,png) - -- 字体 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA4MDcvNTA4ODc1NV8xNTY1MTQzNjI0OTkyXzIwMTkwODA1MTUxNjE1ODQ3LnBuZw?x-oss-process=image/format,png) - - -- 版本 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA4MDcvNTA4ODc1NV8xNTY1MTQzNjI0OTUwXzIwMTkwODA1MTUxNzU2MjQzLnBuZw?x-oss-process=image/format,png) -# 1 版本控制 -## 1.1 关于版本控制 -版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。开发中,我们仅对保存着软件源代码的文本文件作版本控制管理,但实际上,可以对任何类型的文件进行版本控制。 - -采用版本控制系统就可以将某个文件回溯到之前的状态,甚至将整个项目都回退到过去某个时间点的状态。 -可以比较文件的变化细节,查出最后是谁修改了哪个地方,从而找出导致怪异问题出现的原因,又是谁在何时报告了某个功能缺陷等等。 -使用版本控制系统通常还意味着,就算你乱来一气把整个项目中的文件改的改删的删,你也照样可以轻松恢复到原先的样子。但额外增加的工作量却微乎其微。 - -### 1.1.1 本地版本控制系统 -许多人习惯用复制整个项目目录的方式来保存不同的版本,或许还会改名加上备份时间以示区别。这么做唯一的好处就是简单。不过坏处也不少:有时候会混淆所在的工作目录,一旦弄错文件丢了数据就没法撤销恢复。 - -为了解决这个问题,人们很久以前就开发了许多种本地版本控制系统,大多都是采用某种简单的数据库来记录文件的历次更新差异 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA4MDcvNTA4ODc1NV8xNTY1MTQzNjI0OTI5XzIwMTkwODA3MDk1NTMxNzk2LnBuZw?x-oss-process=image/format,png) -其中最流行的一种叫做 rcs,现今许多计算机系统上都还看得到它的踪影。甚至在流行的 Mac OS X 系统上安装了开发者工具包之后,也可以使用 rcs 命令。它的工作原理基本上就是保存并管理文件补丁(patch)。文件补丁是一种特定格式的文本文件,记录着对应文件修订前后的内容变化。所以,根据每次修订后的补丁,rcs 可以通过不断打补丁,计算出各个版本的文件内容,像WPS也有类似功能。 - -### 1.1.2 集中化的版本控制系统 -如何让在不同系统上的开发者协同工作? -于是,集中化的版本控制系统( Centralized Version Control Systems,CVCS )应运而生。 -诸如 CVS,Subversion 以及 Perforce 等,都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。多年以来,这已成为版本控制系统的标准做法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA4MDcvNTA4ODc1NV8xNTY1MTQzNjI1MDQxXzIwMTkwODA3MDk1NzUyNDgucG5n?x-oss-process=image/format,png) - -每个人都可以在一定程度上看到项目中的其他人正在做些什么。 -管理员也可以轻松掌控每个开发者的权限,并且管理一个 CVCS 要远比在各个客户端上维护本地数据库来得轻松容易。 - -#### 缺陷 -中央服务器的单点故障。如果宕机一小时,那么在这一小时内,谁都无法提交更新,也就无法协同工作。 -要是中央服务器的磁盘发生故障,碰巧没做备份,或者备份不够及时,就会有丢失数据的风险。 -最坏的情况是彻底丢失整个项目的所有历史更改记录,而被客户端偶然提取出来的保存在本地的某些快照数据就成了恢复数据的希望。但这样的话依然是个问题,你不能保证所有的数据都已经有人事先完整提取出来过。 -本地版本控制系统也存在类似问题,只要整个项目的历史记录被保存在单一位置,就有丢失所有历史更新记录的风险。 - -于是分布式版本控制系统( Distributed Version Control System,简称 DVCS )面世! - -### 1.1.3 分布式版本控制系统 -像 Git,Mercurial,Bazaar 以及 Darcs 等,客户端并不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来。 - -#### 优势 -任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。因为每一次的提取操作,实际上都是一次对代码仓库的完整备份 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA4MDcvNTA4ODc1NV8xNTY1MTQzNjI0OTU3XzIwMTkwODA3MTAwMTA3OTU0LnBuZw?x-oss-process=image/format,png) - -许多这类系统都可以指定和若干不同的远端代码仓库进行交互。籍此,你就可以在同一个项目中,分别和不同工作小组的人相互协作。你可以根据需要设定不同的协作流程,比如层次模型式的工作流,而这在以前的集中式系统中是无法实现的。 - -# 2 Git 发展史 -Linux 内核开源项目有着为数众广的参与者。绝大多数的 Linux 内核维护工作都花在了提交补丁和保存归档的繁琐事务上(1991-2002年间)。到 2002 年,整个项目组开始启用分布式版本控制系统 BitKeeper 来管理和维护代码。 - -到了 2005 年,开发 BitKeeper 的商业公司同 Linux 内核开源社区的合作关系结束,他们收回了免费使用 BitKeeper 的权力。这就迫使 Linux 开源社区(特别是 Linux 的缔造者 Linus Torvalds )不得不吸取教训,只有开发一套属于自己的版本控制系统才不至于重蹈覆辙。他们对新的系统制订了若干目标: - -速度 -简单的设计 -对非线性开发模式的强力支持(允许上千个并行开发的分支) -完全分布式 -有能力高效管理类似 Linux 内核一样的超大规模项目(速度和数据量) -自诞生于 2005 年以来,Git 日臻成熟完善,在高度易用的同时,仍然保留着初期设定的目标。它的速度飞快,极其适合管理大项目,它还有着令人难以置信的非线性分支管理系统(见第三章),可以应付各种复杂的项目开发需求。 - -# 2 Git 命令 -## 2.1 Git配置 -```bash -$ git config --global user.name "Your Name" -$ git config --global user.email "email@example.com" -``` -`git config`命令的`--global`参数,表明这台机器上的所有Git仓库都会使用这个配置,也可以对某个仓库指定不同的用户名和邮箱地址。 - -## 2.2 版本库 -#### 初始化一个Git仓库 -```bash -$ git init -``` -#### 添加文件到Git仓库 -包括两步: -```bash -$ git add -$ git commit -m "description" -``` -`git add`可以反复多次使用,添加多个文件,`git commit`可以一次提交很多文件,`-m`后面输入的是本次提交的说明,可以输入任意内容。 - -### 查看工作区状态 -```bash -$ git status -``` -### 查看修改内容 -```bash -$ git diff -``` -```bash -$ git diff --cached -``` -```bash -$ git diff HEAD -- -``` -- `git diff` 可以查看工作区(work dict)和暂存区(stage)的区别 -- `git diff --cached` 可以查看暂存区(stage)和分支(master)的区别 -- `git diff HEAD -- ` 可以查看工作区和版本库里面最新版本的区别 -### 查看提交日志 -```bash -$ git log -``` -简化日志输出信息 -```bash -$ git log --pretty=oneline -``` -### 查看命令历史 -```bash -$ git reflog -``` -### 版本回退 -```bash -$ git reset --hard HEAD^ -``` -以上命令是返回上一个版本,在Git中,用`HEAD`表示当前版本,上一个版本就是`HEAD^`,上上一个版本是`HEAD^^`,往上100个版本写成`HEAD~100`。 -### 回退指定版本号 -```bash -$ git reset --hard commit_id -``` -commit_id是版本号,是一个用SHA1计算出的序列 - -### 工作区、暂存区和版本库 -工作区:在电脑里能看到的目录; -版本库:在工作区有一个隐藏目录`.git`,是Git的版本库。 -Git的版本库中存了很多东西,其中最重要的就是称为stage(或者称为index)的暂存区,还有Git自动创建的`master`,以及指向`master`的指针`HEAD`。 - -![理解](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA4MDcvNTA4ODc1NV8xNTY1MTQzNjI1MTIyX2FIUjBjSE02THk5MWNHeHZZV1JtYVd4bGN5NXViM2RqYjJSbGNpNWpiMjB2Wm1sc1pYTXZNakF4T1RBME1UVXZOVEE0T0RjMU5WOHhOVFUxTXpNd01USXlOVFU1WHpRMk9EVTVOamd0TXpBM1pEa3pZakUyT1RKa04yWTFaZw?x-oss-process=image/format,png) - -进一步解释一些命令: -- `git add`实际上是把文件添加到暂存区 -- `git commit`实际上是把暂存区的所有内容提交到当前分支 -### 撤销修改 -#### 丢弃工作区的修改 -```bash -$ git checkout -- -``` -该命令是指将文件在工作区的修改全部撤销,这里有两种情况: -1. 一种是file自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态; -2. 一种是file已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。 - -总之,就是让这个文件回到最近一次git commit或git add时的状态。 - -#### 丢弃暂存区的修改 -分两步: -第一步,把暂存区的修改撤销掉(unstage),重新放回工作区: -```bash -$ git reset HEAD -``` -第二步,撤销工作区的修改 -```bash -$ git checkout -- -``` -小结: -1. 当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令`git checkout -- `。 -2. 当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步,第一步用命令`git reset HEAD `,就回到了第一步,第二步按第一步操作。 - -3. 已经提交了不合适的修改到版本库时,想要撤销本次提交,进行版本回退,前提是没有推送到远程库。 - -### 删除文件 -```bash -$ git rm -``` -`git rm `相当于执行 -```bash -$ rm -$ git add -``` -#### 进一步的解释 -Q:比如执行了`rm text.txt` 误删了怎么恢复? -A:执行`git checkout -- text.txt` 把版本库的东西重新写回工作区就行了 -Q:如果执行了`git rm text.txt`我们会发现工作区的text.txt也删除了,怎么恢复? -A:先撤销暂存区修改,重新放回工作区,然后再从版本库写回到工作区 -```bash -$ git reset head text.txt -$ git checkout -- text.txt -``` -Q:如果真的想从版本库里面删除文件怎么做? -A:执行`git commit -m "delete text.txt"`,提交后最新的版本库将不包含这个文件 - -## git rm 与 git rm --cached -当我们需要删除暂存区或分支上的文件, 同时工作区也不需要这个文件了, 可以使用 - -```bash -git rm file_path -``` - -当我们需要删除暂存区或分支上的文件, 但本地又需要使用, 只是不希望这个文件被版本控制, 可以使用 - -```bash -git rm --cached file_path -``` - - -## 2.3 远程仓库 -#### 创建SSH Key -```bash -$ ssh-keygen -t rsa -C "youremail@example.com" -``` -### 关联远程仓库 -```bash -$ git remote add origin https://github.com/username/repositoryname.git -``` -### 推送到远程仓库 -```bash -$ git push -u origin master -``` -`-u` 表示第一次推送master分支的所有内容,此后,每次本地提交后,只要有必要,就可以使用命令`git push origin master`推送最新修改。 - -### 从远程克隆 -在使用git来进行版本控制时,为了得一个项目的拷贝(copy),我们需要知道这个项目仓库的地址(Git URL). -Git能在许多协议下使用,所以Git URL可能以ssh://, http(s)://, git://,或是只是以一个用户名(git 会认为这是一个ssh 地址)为前辍. - -有些仓库可以通过不只一种协议来访问 -例如,Git本身的源代码你既可以用 git:// 协议来访问: -git clone [git://git.kernel.org/pub/scm/git/git.git](https://link.jianshu.com?t=git://git.kernel.org/pub/scm/git/git.git) -也可以通过http 协议来访问: -git clone [http://www.kernel.org/pub/scm/git/git.git](https://link.jianshu.com?t=http://www.kernel.org/pub/scm/git/git.git) -git://协议较为快速和有效,但是有时必须使用http协议,比如你公司的防火墙阻止了你的非http访问请求.如果你执行了上面两行命令中的任意一个,你会看到一个新目录: 'git',它包含有所的Git源代码和历史记录. -在默认情况下,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会提示你输入密码。 -另外,我们可以通过-b 来指定要克隆的分支名,比如 -$ git clone -b master2 ../server . -表示克隆名为master2的这个分支,如果省略-b 表示克隆master分支。 -```bash -$ git clone https://github.com/usern/repositoryname.git -``` - -### 删除远程仓库文件 -可能某些不需要的目录上传到远程仓库去了,下面开始操作 -- 预览将要删除的文件 - -```bash -git rm -r -n --cached 文件/文件夹名称 -``` -![](https://img-blog.csdnimg.cn/2020030909581747.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -> 加上 -n 参数,执行命令时,不会删除任何文件,而是展示此命令要删除的文件列表预览 - -- 确认后删除 - -```bash -git rm -r --cached 文件/文件夹名称 -``` -![](https://img-blog.csdnimg.cn/20200309100251449.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 提交到本地并推送到远程服务器 - -```bash -git commit -m "提交说明" -``` -![](https://img-blog.csdnimg.cn/20200309100358480.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -```bash -git push origin master -``` -![](https://img-blog.csdnimg.cn/2020030910043664.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -## 2.4 分支 -### 2.4.1 创建分支 -```bash -$ git branch -``` -### 2.4.2 查看分支 -```bash -$ git branch -``` -`git branch`命令会列出所有分支,当前分支前面会标一个*号。 - -### 2.4.3 切换分支 -```bash -$ git checkout -``` - -### 2.4.4 创建+切换分支 -```bash -$ git checkout -b -``` - -### 2.4.5 合并某分支到当前分支 -```bash -$ git merge -``` - -### 2.4.6 删除分支 -```bash -$ git branch -d -``` -### 2.4.7 查看分支合并图 -```bash -$ git log --graph -``` -当Git无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。用`git log --graph`命令可以看到分支合并图。 - -#### 普通模式合并分支 -```bash -$ git merge --no-ff -m "description" -``` -因为本次合并要创建一个新的commit,所以加上`-m`参数,把commit描述写进去。合并分支时,加上`--no-ff`参数就可以用普通模式合并,能看出来曾经做过合并,包含作者和时间戳等信息,而fast forward合并就看不出来曾经做过合并。 - -#### 保存工作现场 -```bash -$ git stash -``` -#### 查看工作现场 -```bash -$ git stash list -``` -#### 恢复工作现场 -```bash -$ git stash pop -``` -#### 丢弃一个没有合并过的分支 -```bash -$ git branch -D -``` - -#### 查看远程库信息 -```bash -$ git remote -v -``` - -#### 在本地创建和远程分支对应的分支 -```bash -$ git checkout -b branch-name origin/branch-name, -``` -本地和远程分支的名称最好一致; - -#### 建立本地分支和远程分支的关联 -```bash -$ git branch --set-upstream branch-name origin/branch-name; -``` - - -#### 从本地推送分支 (将本地项目与远程仓库项目关联) -```bash -$ git push origin branch-name -``` -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA4MDcvNTA4ODc1NV8xNTY1MTQzNjI1MDg1X2FIUjBjSE02THk5MWNHeHZZV1JtYVd4bGN5NXViM2RqYjJSbGNpNWpiMjB2Wm1sc1pYTXZNakF4T1RBME1UVXZOVEE0T0RjMU5WOHhOVFUxTXpNd01USXlOemd5WHpFMk56Z3lNekV4TFdFME9XSmtZV000WW1RNVpUSXpNRE11Y0c1bg?x-oss-process=image/format,png) -如果推送失败,先用git pull抓取远程的新提交; - -#### 从远程抓取分支 -```bash -$ git pull -``` -如果有冲突,要先处理冲突。 - -### 标签 -tag就是一个让人容易记住的有意义的名字,它跟某个commit绑在一起。 -#### 新建一个标签 -```bash -$ git tag -``` -命令`git tag `用于新建一个标签,默认为HEAD,也可以指定一个commit id。 -#### 指定标签信息 -```bash -$ git tag -a -m or commit_id -``` -`git tag -a -m "blablabla..."`可以指定标签信息。 -#### PGP签名标签 -```bash -$ git tag -s -m or commit_id -``` -`git tag -s -m "blablabla..."`可以用PGP签名标签。 -#### 查看所有标签 -```bash -$ git tag -``` -#### 推送一个本地标签 -```bash -$ git push origin -``` -#### 推送全部未推送过的本地标签 -```bash -$ git push origin --tags -``` -#### 删除一个本地标签 -```bash -$ git tag -d -``` -#### 删除一个远程标签 -```bash -$ git push origin :refs/tags/ -``` - -#### 调整commit之间的顺序 -- 首先看一下当前的提交历史,代码如下: -``` -$ git log --oneline -``` -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA4MDcvNTA4ODc1NV8xNTY1MTQzNjI1MTM0X2FIUjBjSE02THk5MWNHeHZZV1JtYVd4bGN5NXViM2RqYjJSbGNpNWpiMjB2Wm1sc1pYTXZNakF4T1RBME1UVXZOVEE0T0RjMU5WOHhOVFUxTXpNd01USXlPREExWHpRMk9EVTVOamd0TW1RM1lqVmxOREkwTXpFeU1HRTFNaTV3Ym1j?x-oss-process=image/format,png) -下面将add N提交挪到c2提交之前,下面开始操作: -``` -$ git rebase -i b0aa963 -``` -特别说明:b0aa963用来确定commit范围,表示从此提交开始到当前的提交(不包括b0aa963提交)。 - -运行此命令后,弹出VIM编辑器 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA4MDcvNTA4ODc1NV8xNTY1MTQzNjI1MDY4X2FIUjBjSE02THk5MWNHeHZZV1JtYVd4bGN5NXViM2RqYjJSbGNpNWpiMjB2Wm1sc1pYTXZNakF4T1RBME1UVXZOVEE0T0RjMU5WOHhOVFUxTXpNd01USXlOekV3WHpRMk9EVTVOamd0TjJNMFkyWTVOemc0TkRneE16bGlOUzV3Ym1j?x-oss-process=image/format,png) -截图说明: - -(1).顶部的commit提交排列顺序与git log排列相反,最先提交的在最上面。 - -(2).前面的pick表示保留此次commit提交不做修改。 - -(3).底部给出所有可用的命令。 - -只要手动调整一下对应提交的位置即可: -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA4MDcvNTA4ODc1NV8xNTY1MTQzNjI1MDgwX2FIUjBjSE02THk5MWNHeHZZV1JtYVd4bGN5NXViM2RqYjJSbGNpNWpiMjB2Wm1sc1pYTXZNakF4T1RBME1UVXZOVEE0T0RjMU5WOHhOVFUxTXpNd01USXlOakkyWHpRMk9EVTVOamd0TjJGa016SXdORFkyTXpOaFlqUXpNaTV3Ym1j?x-oss-process=image/format,png) -最后保存离开就可以自动完成,再来看一下提交历史记录: -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA4MDcvNTA4ODc1NV8xNTY1MTQzNjI1MTY1X2FIUjBjSE02THk5MWNHeHZZV1JtYVd4bGN5NXViM2RqYjJSbGNpNWpiMjB2Wm1sc1pYTXZNakF4T1RBME1UVXZOVEE0T0RjMU5WOHhOVFUxTXpNd01USXlOalkwWHpRMk9EVTVOamd0TUdOaFpqQmtNRE5sTnpCaU1UVXhOQzV3Ym1j?x-oss-process=image/format,png) -.调整影响: - -无论是调整commit顺序或者删除commit,都有可能产生冲突或者错误。 - -比如,后面的提交对前面的他比较有依赖性,而删除前面的提交,则势必会出现问题,就好比穿越时空来到父母恋爱之时,这时候如果热恋中的父母分手,那自己又会从哪里来呢。 - -# 参考 -[Git Book](https://git-scm.com/book/en/v2) \ No newline at end of file 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/Git/git-rebase.md b/Git/git-rebase.md deleted file mode 100644 index 572f609df0..0000000000 --- a/Git/git-rebase.md +++ /dev/null @@ -1,39 +0,0 @@ -假设你现在基于远程分支"origin",创建一个叫"mywork"的分支 -``` -$ git checkout -b mywork origin -``` -![](https://upload-images.jianshu.io/upload_images/4685968-72ba2f21b986cb6e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -现在我们在这个分支做一些修改,然后生成两个提交(commit). -``` -$ vi file.txt -$ git commit -$ vi otherfile.txt -$ git commit -... -``` -但是与此同时,有些人也在"origin"分支上做了一些修改并且做了提交了. 这就意味着"origin"和"mywork"这两个分支各自"前进"了,它们之间"分叉"了 -![](https://upload-images.jianshu.io/upload_images/4685968-ceff145996f677c9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -在这里,你可以用"pull"命令把"origin"分支上的修改拉下来并且和你的修改合并; -结果看起来就像一个新的"合并的提交"(merge commit): -![](https://upload-images.jianshu.io/upload_images/4685968-66103d52203ffe21.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -但是,如果你想让"mywork"分支历史看起来像没有经过任何合并一样,你也许可以用 [git rebase](http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html): -``` -$ git checkout mywork -$ git rebase origin -``` -这些命令会把你的"mywork"分支里的每个提交(commit)取消掉,并且把它们临时 保存为补丁(patch)(这些补丁放到".git/rebase"目录中),然后把"mywork"分支更新 到最新的"origin"分支,最后把保存的这些补丁应用到"mywork"分支上。 -![](https://upload-images.jianshu.io/upload_images/4685968-8f14953da4483f36.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -当'mywork'分支更新之后,它会指向这些新创建的提交(commit),而那些老的提交会被丢弃。 如果运行垃圾收集命令(pruning garbage collection), 这些被丢弃的提交就会删除. (请查看 [git gc](http://www.kernel.org/pub/software/scm/git/docs/git-gc.html)) -![](https://upload-images.jianshu.io/upload_images/4685968-1b8d4cfd4051dc9e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -现在我们可以看一下用合并(merge)和用rebase所产生的历史的区别: -![](https://upload-images.jianshu.io/upload_images/4685968-69ffac8fdbb0d5b3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -在rebase的过程中,也许会出现冲突(conflict). 在这种情况,Git会停止rebase并会让你去解决 冲突;在解决完冲突后,用"git-add"命令去更新这些内容的索引(index), 然后,你无需执行 git-commit,只要执行: -``` -$ git rebase --continue -``` -这样git会继续应用(apply)余下的补丁。 - -在任何时候,你可以用--abort参数来终止rebase的行动,并且"mywork" 分支会回到rebase开始前的状态。 -``` -$ git rebase --abort -``` diff --git "a/Git/gitlab-\350\264\246\345\217\267\346\263\250\345\206\214\345\217\212\344\277\256\346\224\271\350\265\204\346\226\231.md" "b/Git/gitlab-\350\264\246\345\217\267\346\263\250\345\206\214\345\217\212\344\277\256\346\224\271\350\265\204\346\226\231.md" deleted file mode 100644 index 92c95120ad..0000000000 --- "a/Git/gitlab-\350\264\246\345\217\267\346\263\250\345\206\214\345\217\212\344\277\256\346\224\271\350\265\204\346\226\231.md" +++ /dev/null @@ -1,18 +0,0 @@ -# 填写注册信息 - -* 点击注册链接[奇迹 GitLab](http://www.lawyer5.cn/users/sign_in)后,可以看到以下界面,输入用户名、邮箱等信息,点击 SIGN UP 进行注册: -![](https://upload-images.jianshu.io/upload_images/4685968-db95d2f9c9e47f7e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 确认邮件 - -注册后邮箱会收到一封确认邮件,如果没有收到邮件,可能是被误判为垃圾邮件,请进入邮箱的垃圾箱进行查找。(目前无法收到邮件,请加群后,在群内@Tinker 进行激活) -![](https://upload-images.jianshu.io/upload_images/4685968-65749c2e04bc3489.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -- 点击邮件中的链接,确认刚刚注册的账户,将跳转到 GitLab 的主页。 -![](https://upload-images.jianshu.io/upload_images/4685968-1cd494de12080ac9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -按照下图的设置修改Gitlab账号的姓名为自己的真实姓名。 -![](https://upload-images.jianshu.io/upload_images/4685968-e025a2a60e1fd9e3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -- 同时本地gitlab的信息要设置为自己真实姓名。设置方法为在本地终端(Windows下为cmd命令行)中输入如下命令: -- git config --global user.name "王二麻子" -- git config --global user.email "wangermazi@example.com" diff --git "a/Git/git\351\205\215\347\275\256.md" "b/Git/git\351\205\215\347\275\256.md" deleted file mode 100644 index 1834a63263..0000000000 --- "a/Git/git\351\205\215\347\275\256.md" +++ /dev/null @@ -1,8 +0,0 @@ -首先确认设置了user.name和user.email -#0 配置 用户信息 -将user.name和user.email设置正确,为了保护知识产权,你必须要保证它们的正确性 -![这里写图片描述](http://upload-images.jianshu.io/upload_images/4685968-5465cb33d0241966?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -#1 配置 行尾和颜色 -![这里写图片描述](http://upload-images.jianshu.io/upload_images/4685968-ca81b45d92e54edc?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -#2 有用的设置 -local global system优先级降序排列 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/ArrayBlockingQueue \346\240\270\345\277\203\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ArrayBlockingQueue \346\240\270\345\277\203\346\272\220\347\240\201\345\210\206\346\236\220.md" deleted file mode 100644 index b2e6be4019..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ArrayBlockingQueue \346\240\270\345\277\203\346\272\220\347\240\201\345\210\206\346\236\220.md" +++ /dev/null @@ -1,140 +0,0 @@ -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e62f4154c7c?w=998&h=690&f=png&s=1582218 "图片标题") - -> 最聪明的人是最不愿浪费时间的人。 -> ——但丁 - -# 0 前言 -由数组支持的有界阻塞队列。此队列对元素按 FIFO(先进先出)进行排序。队首是已在队列中最长时间的元素。队尾是最短时间出现在队列中的元素。新元素插入到队列的尾部,并且队列检索操作在队列的开头获取元素。 -这是经典的“有界缓冲区”,其中固定大小的数组包含由生产者插入并由消费者提取的元素。一旦创建,容量将无法更改。试图将一个元素放入一个完整的队列将导致操作阻塞;从空队列中取出一个元素的尝试也会类似地阻塞。 - -此类支持可选的公平性策略,用于排序正在等待的生产者和使用者线程。默认情况下,不保证此排序。但是,将公平性设置为true构造的队列将按FIFO顺序授予线程访问权限。公平通常会降低吞吐量,但会减少可变性并避免饥饿。 - -此类及其迭代器实现了Collection和Iterator接口的所有可选方法。 - -此类是Java Collections Framework的成员。 - -# 1 继承体系 -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e62f8c9ecc4?w=3182&h=436&f=png&s=169197 "图片标题") - -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e63099ec3f9?w=980&h=796&f=png&s=53926 "图片标题") - - -- Java中的阻塞队列接口BlockingQueue继承自Queue接口。 - -# 2 属性 -- 存储队列元素的数组,是个循环数组 -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e63508bf515?w=1366&h=330&f=png&s=38372 "图片标题") - -- 下次take, poll, peek or remove 时的数据索引 -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e63513c3512?w=1028&h=332&f=png&s=30668 "图片标题") - -- 下次 put, offer, or add 时的数据索引 -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e638c78e2d1?w=970&h=320&f=png&s=27625 "图片标题") - -有了上面两个关键字段,在存数据和取数据时,无需计算,就能知道应该新增到什么位置,应该从什么位置取数据。 - -- 队列中的元素数 -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e639c4bd07e?w=854&h=292&f=png&s=23214 "图片标题") - -## 并发控制采用经典的双条件(notEmpty + notFull)算法 -- Lock 锁 -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e638cc82954?w=1598&h=332&f=png&s=44939 "图片标题") - -- 等待take的条件,在 put 成功时使用 -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e63e73d8679?w=1972&h=330&f=png&s=59297 "图片标题") - -- 等待put的条件,在 take 成功时使用 -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e63ec36708b?w=1922&h=328&f=png&s=56379 "图片标题") - -*ArrayBlockingQueue is a State-Dependent class*,该类只有一些先决条件才能执行操作. - - -如果前提条件(notFull)为 false ,写线程将只能等待. -如果队列满,写需要等待. -原子式释放锁,并等待信号(读线程发起的 *notFull.signal()*) -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e63e0d14864?w=2800&h=732&f=png&s=119381 "图片标题") - -对于读,概念是相同的,但使用 notEmpty 条件: -如果队列为空,则读线程需要等待. -原子地释放锁,并等待信号(由写线程触发的 *notEmpty.signal()*) -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e641e4fec4e?w=2800&h=732&f=png&s=121706 "图片标题") - -当一个线程被唤醒,那么你需要做2件主要的事情: - 1. 获取锁 - 2. 重测条件 - -这种设计,它支持只唤醒对刚刚发生的事情感兴趣的线程. -例如,一个试图从空队列中取数据的线程,只对队列是否为空(有一些数据要取出)感兴趣,而并不关心队列是否满。确实经典的设计! - -# 3 构造方法 -## 3.1 无参 -注意这是没有无参构造方法的哦!必须设置容量! - -## 3.2 有参 -- 创建具有给定(固定)容量和默认访问策略(非公平)的ArrayBlockingQueue -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e64608b4245?w=2372&h=414&f=png&s=110699 "图片标题") - -- 创建具有给定(固定)容量和指定访问策略的ArrayBlockingQueue -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e6418d91a35?w=3052&h=1086&f=png&s=293400 "图片标题") - - - -- 创建一个具有给定(固定)容量,指定访问策略并最初包含给定集合的元素的ArrayBlockingQueue,该元素以集合的迭代器的遍历顺序添加. -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e642038dee6?w=2720&h=2604&f=png&s=420673 "图片标题") - -### fair 参数 -指定读写锁是否公平 -- 公平锁,锁竞争按先来先到顺序 -- 非公平锁,锁竞争随机 - -# 3 新增数据 -ArrayBlockingQueue有不同的几个数据添加方法,add、offer、put方法,数据都会按照 putIndex 的位置新增. - -## 3.1 add -- 如果可以在不超过队列容量的情况下立即将指定元素插入此队列的尾部,则在成功插入时返回true,如果此队列已满则抛出IllegalStateException. -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e64c4b23236?w=1618&h=434&f=png&s=79648 "图片标题") -调用的是抽象父类 AbstractQueue的 add 方法 -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e648c83edaf?w=3032&h=848&f=png&s=170809 "图片标题") - -### offer -- 之后又是调用的自身实现的 offer 方法. -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e6530cde717?w=2720&h=2588&f=png&s=373860 "图片标题") - -### enqueue -在当前放置位置插入元素,更新并发出信号. -仅在持有锁时可以调用 -- 内部继续调用入队方法 -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e64d535aea6?w=2060&h=1824&f=png&s=346941 "图片标题") - -类似的看 put 方法. -## 3.2 put -- 将指定的元素插入此队列的末尾,如果队列已满,则等待空间变为可用. -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e651c50ded0?w=2436&h=1728&f=png&s=291757 "图片标题") -实现类似 add,不再赘述. - -# 4 取数据 -从队首取数据,我们以 poll 为例看源码. - -## 4.1 poll -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e651d4af1ff?w=2348&h=1524&f=png&s=223031 "图片标题") - -### dequeue -- 提取当前位置的元素,更新并发出信号.仅在持有锁时可调用. -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e6564306374?w=1964&h=2336&f=png&s=436581 "图片标题") - -# 5 删除数据 -![](https://user-gold-cdn.xitu.io/2020/4/23/171a6e6574553879?w=2364&h=4680&f=png&s=798516 "图片标题") - -从源码可以看出删除有两种情景: -1. 删除位置等于takeIndex,直接将该位元素置 null ,并重新计算 takeIndex - -2. 找到要删除元素的下一个,计算删除元素和 putIndex 的关系,若下一个元素 - - 是 putIndex,将 putIndex 的值修改成删除位 - - - - 非 putIndex,将下一个元素往前移动一位 - - - -# 6 总结 -ArrayBlockingQueue 是一种循环队列,通过维护队首、队尾的指针,来优化插入、删除,从而使时间复杂度为O(1). \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/CountDownLatch \346\240\270\345\277\203\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/CountDownLatch \346\240\270\345\277\203\346\272\220\347\240\201\350\247\243\346\236\220.md" deleted file mode 100644 index 18635b7b5e..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/CountDownLatch \346\240\270\345\277\203\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ /dev/null @@ -1,143 +0,0 @@ -# 1 基本设计 -一种同步辅助,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。 -CountDownLatch 是用给定的 *count* 初始化的。由于调用了*countDown*()方法,*await* 方法阻塞,直到当前计数为零,之后释放所有等待线程,并立即返回任何后续的 await 调用。这是一种一次性现象——计数无法重置。如果需要重置计数的版本,可以考虑使用*CyclicBarrier*。 - -CountDownLatch 是一种通用的同步工具,可以用于多种用途。count为1时初始化的CountDownLatch用作简单的 on/off 的 latch或gate:所有调用wait的线程都在gate处等待,直到调用*countDown*()的线程打开它。一个初始化为N的CountDownLatch可以用来让一个线程等待,直到N个线程完成某个动作,或者某个动作已经完成N次。 - -CountDownLatch的一个有用的特性是,它不需要调用倒计时的线程等待计数达到0才继续,它只是防止任何线程继续等待,直到所有线程都通过。 - -# 2 类架构 -## 2.1 UML 图 -![](https://uploadfiles.nowcoder.com/files/20200216/5088755_1581785266972_20200215203021768.png) - -## 2.2 继承关系 - -![](https://uploadfiles.nowcoder.com/files/20200216/5088755_1581785267331_20200215211511903.png) -可以看出,CountDownLatch并无显式地继承什么接口或类。 - -## 2.3 构造函数细节 -- 构造一个用给定计数初始化的CountDownLatch。 -![](https://uploadfiles.nowcoder.com/files/20200216/5088755_1581785266976_20200215214925155.png) -- 参数 count -在线程通过*await*()之前必须调用countDown()的次数 - -CountDownLatch 的 state 并不是 AQS 的默认值 0,而是可赋值的,就是在 CountDownLatch 初始化时,count 就代表了 state 的初始化值 - -- new Sync(count) 其实就是调用了内部类 Sync 的如下构造函数 -![](https://uploadfiles.nowcoder.com/files/20200216/5088755_1581785267076_20200216000150446.png) - -count 表示我们希望等待的线程数,可能是 -- 等待一组线程全部启动完成,或者 -- 等待一组线程全部执行完成 - -## 2.4 内部类 -和 ReentrantLock 一样,CountDownLatch类也存在一个内部同步器 Sync,继承了 AbstractQueuedSynchronizer - -- 这也是唯一的属性 -![](https://uploadfiles.nowcoder.com/files/20200216/5088755_1581785266834_20200215215958610.png) -```java -private static final class Sync extends AbstractQueuedSynchronizer { - private static final long serialVersionUID = 4982264981922014374L; - - // 构造方法 - Sync(int count) { - setState(count); - } - - // 返回当前计数 - int getCount() { - return getState(); - } - - // 在共享模式下获取锁 - protected int tryAcquireShared(int acquires) { - return (getState() == 0) ? 1 : -1; - } - - // 共享模式下的锁释放 - protected boolean tryReleaseShared(int releases) { - // 降低计数器; 至 0 时发出信号 - for (;;) { - // 获取锁状态 - int c = getState(); - // 锁未被任何线程持有 - if (c == 0) - return false; - int nextc = c-1; - if (compareAndSetState(c, nextc)) - return nextc == 0; - } - } -} -``` - -# 3 await -可以叫做等待,也可以称之为加锁。 -## 3.1 无参 -![](https://uploadfiles.nowcoder.com/files/20200216/5088755_1581785267172_2020021522023292.png) -造成当前线程等待,直到锁存器计数到零,除非线程被中断。 -如果当前计数为零,则此方法立即返回。 - -如果当前线程数大于0,则当前线程将出于线程调度的目的而禁用,并处于睡眠状态,直到发生以下两种情况之一: -- 由于调用了*countDown*()方法,计数为零 -- 其他线程中断了当前线程 - -如果当前线程: -- 在进入此方法时已设置其中断状态;或者 -- 在等待时被中断 - -就会抛 *InterruptedException*,并清除当前线程的中断状态。 - -无参版 await 内部使用的是 acquireSharedInterruptibly 方法,实现在 AQS 中的 final 方法 -![](https://uploadfiles.nowcoder.com/files/20200216/5088755_1581785267111_20200215232024447.png) -1. 使用CountDownLatch 的内部类 Sync 重写的*tryAcquireShared* 方法尝试获得锁,如果获取了锁直接返回,获取不到锁走 2 -2. 获取不到锁,用 Node 封装一下当前线程,追加到同步队列的尾部,等待在合适的时机去获得锁,本步已完全实现在 AQS 中 - -### tryAcquireShared -![](https://uploadfiles.nowcoder.com/files/20200216/5088755_1581785267130_20200215232259170.png) - -## 3.2 超时参数 -- 最终都会转化成毫秒 -![](https://uploadfiles.nowcoder.com/files/20200216/5088755_1581785267072_20200215225934784.png) -造成当前线程等待,直到锁存器计数到零,除非线程被中断,或者指定的等待时间已过。 -如果当前计数为零,则此方法立即返回值 true。 - -如果当前线程数大于0,则当前线程将出于线程调度的目的而禁用,并处于休眠状态,直到发生以下三种情况之一: -- 由于调用了countDown()方法,计数为零;或 -- 其他一些线程中断当前线程;或 -- 指定的等待时间已经过了 - -如果计数为零,则该方法返回值true。 - -如果当前线程: -- 在进入此方法时已设置其中断状态;或 -- 在等待时中断, - -就会抛出InterruptedException,并清除当前线程的中断状态。 -如果指定的等待时间过期,则返回false值。如果时间小于或等于0,则该方法根本不会等待。 - - -- 使用的是 AQS 的 *tryAcquireSharedNanos* 方法 -![](https://uploadfiles.nowcoder.com/files/20200216/5088755_1581785267296_20200215232132482.png) - -获得锁时,state 的值不会发生变化,像 ReentrantLock 在获得锁时,会把 state + 1,但 CountDownLatch 不会 - - -# 4 countDown -降低锁存器的计数,如果计数为 0,则释放所有等待的线程。 -如果当前计数大于零,则递减。如果新计数为零,那么所有等待的线程都将重新启用,以便进行线程调度。 - -如果当前计数等于0,则什么也不会发生。 -![](https://uploadfiles.nowcoder.com/files/20200216/5088755_1581785266936_20200216002459132.png) -releaseShared 已经完全实现在 AQS -![](https://uploadfiles.nowcoder.com/files/20200216/5088755_1581785267223_20200216004436507.png) -主要分成两步: -1. 尝试释放锁(tryReleaseShared),锁释放失败直接返回,释放成功走 2,本步由 Sync 实现 -2. 释放当前节点的后置等待节点,该步 AQS 已经完全实现 -### tryReleaseShared -![](https://uploadfiles.nowcoder.com/files/20200216/5088755_1581785267244_2020021600383397.png) - -对 state 进行递减,直到 state 变成 0;当 state 递减为 0 时,才返回 true。 - -# 总结 -研究完 CountDownLatch 的源码,可知其底层结构仍然依赖了 AQS,对其线程所封装的结点是采用共享模式,而 ReentrantLock 是采用独占模式。可以仔细对比差异,深入理解研究。 \ 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/CountDownLatch.md" deleted file mode 100644 index 2c09b622e9..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/CountDownLatch.md" +++ /dev/null @@ -1,143 +0,0 @@ -# 1 基本设计 -一种同步辅助,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。 -CountDownLatch 是用给定的 *count* 初始化的。由于调用了*countDown*()方法,*await* 方法阻塞,直到当前计数为零,之后释放所有等待线程,并立即返回任何后续的 await 调用。这是一种一次性现象——计数无法重置。如果需要重置计数的版本,可以考虑使用*CyclicBarrier*。 - -CountDownLatch 是一种通用的同步工具,可以用于多种用途。count为1时初始化的CountDownLatch用作简单的 on/off 的 latch或gate:所有调用wait的线程都在gate处等待,直到调用*countDown*()的线程打开它。一个初始化为N的CountDownLatch可以用来让一个线程等待,直到N个线程完成某个动作,或者某个动作已经完成N次。 - -CountDownLatch的一个有用的特性是,它不需要调用倒计时的线程等待计数达到0才继续,它只是防止任何线程继续等待,直到所有线程都通过。 - -# 2 类架构 -## 2.1 UML 图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTYvNTA4ODc1NV8xNTgxNzg1MjY2OTcyXzIwMjAwMjE1MjAzMDIxNzY4LnBuZw?x-oss-process=image/format,png) - -## 2.2 继承关系 - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTYvNTA4ODc1NV8xNTgxNzg1MjY3MzMxXzIwMjAwMjE1MjExNTExOTAzLnBuZw?x-oss-process=image/format,png) -可以看出,CountDownLatch并无显式地继承什么接口或类。 - -## 2.3 构造函数细节 -- 构造一个用给定计数初始化的CountDownLatch。 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTYvNTA4ODc1NV8xNTgxNzg1MjY2OTc2XzIwMjAwMjE1MjE0OTI1MTU1LnBuZw?x-oss-process=image/format,png) -- 参数 count -在线程通过*await*()之前必须调用countDown()的次数 - -CountDownLatch 的 state 并不是 AQS 的默认值 0,而是可赋值的,就是在 CountDownLatch 初始化时,count 就代表了 state 的初始化值 - -- new Sync(count) 其实就是调用了内部类 Sync 的如下构造函数 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTYvNTA4ODc1NV8xNTgxNzg1MjY3MDc2XzIwMjAwMjE2MDAwMTUwNDQ2LnBuZw?x-oss-process=image/format,png) - -count 表示我们希望等待的线程数,可能是 -- 等待一组线程全部启动完成,或者 -- 等待一组线程全部执行完成 - -## 2.4 内部类 -和 ReentrantLock 一样,CountDownLatch类也存在一个内部同步器 Sync,继承了 AbstractQueuedSynchronizer - -- 这也是唯一的属性 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTYvNTA4ODc1NV8xNTgxNzg1MjY2ODM0XzIwMjAwMjE1MjE1OTU4NjEwLnBuZw?x-oss-process=image/format,png) -```java -private static final class Sync extends AbstractQueuedSynchronizer { - private static final long serialVersionUID = 4982264981922014374L; - - // 构造方法 - Sync(int count) { - setState(count); - } - - // 返回当前计数 - int getCount() { - return getState(); - } - - // 在共享模式下获取锁 - protected int tryAcquireShared(int acquires) { - return (getState() == 0) ? 1 : -1; - } - - // 共享模式下的锁释放 - protected boolean tryReleaseShared(int releases) { - // 降低计数器; 至 0 时发出信号 - for (;;) { - // 获取锁状态 - int c = getState(); - // 锁未被任何线程持有 - if (c == 0) - return false; - int nextc = c-1; - if (compareAndSetState(c, nextc)) - return nextc == 0; - } - } -} -``` - -# 3 await -可以叫做等待,也可以称之为加锁。 -## 3.1 无参 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTYvNTA4ODc1NV8xNTgxNzg1MjY3MTcyXzIwMjAwMjE1MjIwMjMyOTIucG5n?x-oss-process=image/format,png) -造成当前线程等待,直到锁存器计数到零,除非线程被中断。 -如果当前计数为零,则此方法立即返回。 - -如果当前线程数大于0,则当前线程将出于线程调度的目的而禁用,并处于睡眠状态,直到发生以下两种情况之一: -- 由于调用了*countDown*()方法,计数为零 -- 其他线程中断了当前线程 - -如果当前线程: -- 在进入此方法时已设置其中断状态;或者 -- 在等待时被中断 - -就会抛 *InterruptedException*,并清除当前线程的中断状态。 - -无参版 await 内部使用的是 acquireSharedInterruptibly 方法,实现在 AQS 中的 final 方法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTYvNTA4ODc1NV8xNTgxNzg1MjY3MTExXzIwMjAwMjE1MjMyMDI0NDQ3LnBuZw?x-oss-process=image/format,png) -1. 使用CountDownLatch 的内部类 Sync 重写的*tryAcquireShared* 方法尝试获得锁,如果获取了锁直接返回,获取不到锁走 2 -2. 获取不到锁,用 Node 封装一下当前线程,追加到同步队列的尾部,等待在合适的时机去获得锁,本步已完全实现在 AQS 中 - -### tryAcquireShared -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTYvNTA4ODc1NV8xNTgxNzg1MjY3MTMwXzIwMjAwMjE1MjMyMjU5MTcwLnBuZw?x-oss-process=image/format,png) - -## 3.2 超时参数 -- 最终都会转化成毫秒 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTYvNTA4ODc1NV8xNTgxNzg1MjY3MDcyXzIwMjAwMjE1MjI1OTM0Nzg0LnBuZw?x-oss-process=image/format,png) -造成当前线程等待,直到锁存器计数到零,除非线程被中断,或者指定的等待时间已过。 -如果当前计数为零,则此方法立即返回值 true。 - -如果当前线程数大于0,则当前线程将出于线程调度的目的而禁用,并处于休眠状态,直到发生以下三种情况之一: -- 由于调用了countDown()方法,计数为零;或 -- 其他一些线程中断当前线程;或 -- 指定的等待时间已经过了 - -如果计数为零,则该方法返回值true。 - -如果当前线程: -- 在进入此方法时已设置其中断状态;或 -- 在等待时中断, - -就会抛出InterruptedException,并清除当前线程的中断状态。 -如果指定的等待时间过期,则返回false值。如果时间小于或等于0,则该方法根本不会等待。 - - -- 使用的是 AQS 的 *tryAcquireSharedNanos* 方法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTYvNTA4ODc1NV8xNTgxNzg1MjY3Mjk2XzIwMjAwMjE1MjMyMTMyNDgyLnBuZw?x-oss-process=image/format,png) - -获得锁时,state 的值不会发生变化,像 ReentrantLock 在获得锁时,会把 state + 1,但 CountDownLatch 不会 - - -# 4 countDown -降低锁存器的计数,如果计数为 0,则释放所有等待的线程。 -如果当前计数大于零,则递减。如果新计数为零,那么所有等待的线程都将重新启用,以便进行线程调度。 - -如果当前计数等于0,则什么也不会发生。 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTYvNTA4ODc1NV8xNTgxNzg1MjY2OTM2XzIwMjAwMjE2MDAyNDU5MTMyLnBuZw?x-oss-process=image/format,png) -releaseShared 已经完全实现在 AQS -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTYvNTA4ODc1NV8xNTgxNzg1MjY3MjIzXzIwMjAwMjE2MDA0NDM2NTA3LnBuZw?x-oss-process=image/format,png) -主要分成两步: -1. 尝试释放锁(tryReleaseShared),锁释放失败直接返回,释放成功走 2,本步由 Sync 实现 -2. 释放当前节点的后置等待节点,该步 AQS 已经完全实现 -### tryReleaseShared -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTYvNTA4ODc1NV8xNTgxNzg1MjY3MjQ0XzIwMjAwMjE2MDAzODMzOTcucG5n?x-oss-process=image/format,png) - -对 state 进行递减,直到 state 变成 0;当 state 递减为 0 时,才返回 true。 - -# 总结 -研究完 CountDownLatch 的源码,可知其底层结构仍然依赖了 AQS,对其线程所封装的结点是采用共享模式,而 ReentrantLock 是采用独占模式。可以仔细对比差异,深入理解研究。 \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/CurrentHashMap\345\216\237\347\220\206\344\270\216\345\272\224\347\224\250\350\257\246\350\247\243 - \344\270\212\347\257\207 (JDK7 && JDK8).md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/CurrentHashMap\345\216\237\347\220\206\344\270\216\345\272\224\347\224\250\350\257\246\350\247\243 - \344\270\212\347\257\207 (JDK7 && JDK8).md" deleted file mode 100644 index d446dfb5cc..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/CurrentHashMap\345\216\237\347\220\206\344\270\216\345\272\224\347\224\250\350\257\246\350\247\243 - \344\270\212\347\257\207 (JDK7 && JDK8).md" +++ /dev/null @@ -1,343 +0,0 @@ -# 1 为什么要使用ConcurrentHashMap(全靠同行衬托) -## 1.1 HashMap : 线程不安全! -最常用的Map类,性能好、速度快,但不能保证线程安全! - -在多线程环境下,使用HashMap进行put操作会引起死循环!因为多线程会导致HashMap的Entry链表成环,一旦成环,Entry的next节点永远不为空! - -## 1.2 HashTable : 效率低下! -百分百线程安全的Map类,对外方法全部使用synchronize修饰 - -这意味着在多线程下,每个线程操作都会锁住整个map,操作完成后才释放锁。这会导致竞争愈发的激烈,效率自然很低! - -## 是时候表演高端操作 : 锁分段, 可有效提升并发访问率 -HashTable在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁 -假如容器里有多把锁,每一把锁用于锁容器其中一部分的数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术 - - 首先将数据分成一段一段地存储 - - 然后给每一段数据配一把锁 - - 当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问 - -# 2 ConcurrentHashMap的结构 -通过ConcurrentHashMap的类图来分析ConcurrentHashMap的结构 -ConcurrentHashMap 以下简称 CHM -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtZTAxNjJkZWQ2NGY5MjU0Zg) -由Segment数组和HashEntry数组组成. - -Segment是一种可重入锁,在CHM里扮演锁的角色; -HashEntry则用于存储键值对数据. - -一个CHM里包含一个Segment数组. -Segment的结构和HashMap类似,是一种数组和链表结构. - -一个Segment元素里包含一个HashEntry数组 -每个HashEntry是一个链表结构的元素 -每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时, -必须首先获得与它对应的Segment锁 - -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtOGRkNTJjY2Q3NWZhN2RlMQ) - -# 3 ConcurrentHashMap的初始化 -## 3.1 Segment详解 -### Segment的索引与读取 -ConcurrentHashMap类中包含三个与Segment相关的成员变量: - -```java -/** - * Mask value for indexing into segments. The upper bits of a - * key's hash code are used to choose the segment. - */ final int segmentMask; - -/** - * Shift value for indexing within segments. - */ final int segmentShift; - -/** - * The segments, each of which is a specialized hash table. - */ final Segment[] segments; -``` - -其中segments是Segment的原生数组,此数组的长度可以在CHM的构造函数中使用并发度参数指定,默认值为 -DEFAULT_CONCURRENCY_LEVEL=16 - -- segmentShift : 计算segments数组索引的位移量 -- segmentMask : 计算索引的掩码值 - -例如并发度为16时(即segments数组长度为16),segmentShift为32-4=28(因为2的4次幂为16),而segmentMask则为1111(二进制),索引的计算式如下: -```java -int j = (hash >>> segmentShift) & segmentMask; -``` - -### 多线程访问共享变量的解决方案 -为保证数据的正确,可以采用以下方法 -| 方案| 性能 |性质 | -|--|--|--| -| 加锁 |最低 | 能保证原子性、可见性,防止指令重排 -|volatile | 中等 | 保证可见性,防止指令重排 -|getObjectVolatile | 最好 |防止指令重排 - -因此ConcurrentHashMap选择了使用Unsafe的getObjectVolatile来读取segments中的元素,相关代码如下 - -```java -// Unsafe mechanics private static final sun.misc.Unsafe UNSAFE; -private static final long SBASE; -private static final int SSHIFT; -private static final long TBASE; -private static final int TSHIFT; -private static final long HASHSEED_OFFSET; -private static final long SEGSHIFT_OFFSET; -private static final long SEGMASK_OFFSET; -private static final long SEGMENTS_OFFSET; - -static { - int ss, ts; - try { - UNSAFE = sun.misc.Unsafe.getUnsafe(); - Class tc = HashEntry[].class; - Class sc = Segment[].class; - TBASE = UNSAFE.arrayBaseOffset(tc); - SBASE = UNSAFE.arrayBaseOffset(sc); - ts = UNSAFE.arrayIndexScale(tc); - ss = UNSAFE.arrayIndexScale(sc); - HASHSEED_OFFSET = UNSAFE.objectFieldOffset( - ConcurrentHashMap.class.getDeclaredField("hashSeed")); - SEGSHIFT_OFFSET = UNSAFE.objectFieldOffset( - ConcurrentHashMap.class.getDeclaredField("segmentShift")); - SEGMASK_OFFSET = UNSAFE.objectFieldOffset( - ConcurrentHashMap.class.getDeclaredField("segmentMask")); - SEGMENTS_OFFSET = UNSAFE.objectFieldOffset( - ConcurrentHashMap.class.getDeclaredField("segments")); - } catch (Exception e) { - throw new Error(e); - } - if ((ss & (ss-1)) != 0 || (ts & (ts-1)) != 0) - throw new Error("data type scale not a power of two"); - SSHIFT = 31 - Integer.numberOfLeadingZeros(ss); - TSHIFT = 31 - Integer.numberOfLeadingZeros(ts); -} - - -private Segment segmentForHash(int h) { - long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; - return (Segment) UNSAFE.getObjectVolatile(segments, u); -} -``` - -观察segmentForHash(int h)方法可知 -- 首先使用`(h >>> segmentShift) & segmentMask` -计算出该h对应的segments索引值(假设为x) -- 然后使用索引值`(x< node = tryLock() ? null : - scanAndLockForPut(key, hash, value); - V oldValue; - try { -//实际代码…… - } - } finally { - unlock(); - } - return oldValue; -} -``` -首先调用tryLock,如果加锁失败,则进入`scanAndLockForPut(key, hash, value)` -该方法实际上是先自旋等待其他线程解锁,直至指定的次数`MAX_SCAN_RETRIES` -若自旋过程中,其他线程释放了锁,导致本线程直接获得了锁,就避免了本线程进入等待锁的场景,提高了效率 -若自旋一定次数后,仍未获取锁,则调用lock方法进入等待锁的场景 - -采用这种自旋锁和独占锁结合的方法,在很多场景下能够提高Segment并发操作数据的效率。 - - - - -初始化方法是通过initialCapacity、loadFactor和concurrencyLevel等几个 -参数来初始化segment数组、段偏移量segmentShift、段掩码segmentMask和每个segment里的HashEntry数组来实现的. - - - 初始化segments数组 - - -``` - if (concurrencyLevel > MAX_SEGMENTS) - concurrencyLevel = MAX_SEGMENTS; - int sshift = 0; - int ssize = 1; - while (ssize < concurrencyLevel) { - ++sshift; - ssize <<= 1; - } - segmentShift = 32 - sshift; - segmentMask = ssize - 1; - this.segments = Segment.newArray(ssize); -``` -segments数组的长度`ssize`是通过`concurrencyLevel`计算得出的 -为了能通过按位与的散列算法来定位segments数组的索引,必须保证segments数组的长度是2的N次方,所以必须计算出一个大于或等于concurrencyLevel的最小的2的N次方值来作为segments数组的长度 - -> concurrencyLevel的最大值是65535,这意味着segments数组的长度最大为65536,对应的二进制是16位 - -- 初始化segmentShift和segmentMask -这两个全局变量需要在定位segment时的散列算法里使用 -sshift等于ssize从1向左移位的次数,默认concurrencyLevel等于16,1需要向左移位移动4次,所以sshift为4. - - segmentShift用于定位参与散列运算的位数,segmentShift等于32减sshift,所以等于28,这里之所以用32是因为ConcurrentHashMap里的hash()方法输出的最大数是32位,后面的测试中我们可以看到这点 - - segmentMask是散列运算的掩码,等于ssize减1,即15,掩码的二进制各个位的值都是1.因为ssize的最大长度是65536,所以segmentShift最大值是16,segmentMask最大值是65535,对应的二进制是16位,每个位都是1 -- 初始化每个segment -输入参数initialCapacity是ConcurrentHashMap的初始化容量,loadfactor是每个segment的负载因子,在构造方法里需要通过这两个参数来初始化数组中的每个segment. -``` - if (initialCapacity > MAXIMUM_CAPACITY) - initialCapacity = MAXIMUM_CAPACITY; - int c = initialCapacity / ssize; - if (c * ssize < initialCapacity) - ++c; - int cap = 1; - while (cap < c) - cap <<= 1; - for (int i = 0; i < this.segments.length; ++i) - this.segments[i] = new Segment(cap, loadFactor); -``` -上面代码中的变量cap就是segment里HashEntry数组的长度,它等于initialCapacity除以ssize的倍数c,如果c大于1,就会取大于等于c的2的N次方值,所以cap不是1,就是2的N次方. -segment的容量threshold=(int)cap*loadFactor,默认initialCapacity等于16,loadfactor等于0.75,通过运算cap等于1,threshold等于零. - -- 定位Segment -既然ConcurrentHashMap使用分段锁Segment来保护不同段的数据,那么在插入和获取元素时,必须先通过散列算法定位到Segment.可以看到ConcurrentHashMap会首先使用Wang/Jenkins hash的变种算法对元素的hashCode进行一次再散列. -``` - private static int hash(int h) { - h += (h << 15) ^ 0xffffcd7d; - h ^= (h >>> 10); - h += (h << 3); - h ^= (h >>> 6); - h += (h << 2) + (h << 14); - return h ^ (h >>> 16); - } -``` -进行再散列,是为了减少散列冲突,使元素能够均匀地分布在不同的Segment上,从而提高容器的存取效率. -假如散列的质量差到极点,那么所有的元素都在一个Segment中,不仅存取元素缓慢,分段锁也会失去意义. - -ConcurrentHashMap通过以下散列算法定位segment - -``` -final Segment segmentFor(int hash) { - return segments[(hash >>> segmentShift) & segmentMask]; -} -``` -默认情况下segmentShift为28,segmentMask为15,再散列后的数最大是32位二进制数据,向右无符号移动28位,即让高4位参与到散列运算中,(hash>>>segmentShift)&segmentMask的运算结果分别是4、15、7和8,可以看到散列值没有发生冲突. - -### HashEntry -如果说ConcurrentHashMap中的segments数组是第一层hash表,则每个Segment中的HashEntry数组(transient volatile -HashEntry[] table)是第二层hash表。每个HashEntry有一个next属性,因此它们能够组成一个单向链表。HashEntry相关代码如下: - -``` -static final class HashEntry { - final int hash; - final K key; - volatile V value; - volatile HashEntry next; - - HashEntry(int hash, K key, V value, HashEntry next) { - this.hash = hash; - this.key = key; - this.value = value; - this.next = next; - } - - /** - * Sets next field with volatile write semantics. (See above - * about use of putOrderedObject.) - */ final void setNext(HashEntry n) { - UNSAFE.putOrderedObject(this, nextOffset, n); - } - - // Unsafe mechanics static final sun.misc.Unsafe UNSAFE; - static final long nextOffset; - static { - try { - UNSAFE = sun.misc.Unsafe.getUnsafe(); - Class k = HashEntry.class; - nextOffset = UNSAFE.objectFieldOffset - (k.getDeclaredField("next")); - } catch (Exception e) { - throw new Error(e); - } - } -} - -/** - * Gets the ith element of given table (if nonnull) with volatile - * read semantics. Note: This is manually integrated into a few - * performance-sensitive methods to reduce call overhead. - */ @SuppressWarnings("unchecked") -static final HashEntry entryAt(HashEntry[] tab, int i) { - return (tab == null) ? null : - (HashEntry) UNSAFE.getObjectVolatile - (tab, ((long)i << TSHIFT) + TBASE); -} - -/** - * Sets the ith element of given table, with volatile write - * semantics. (See above about use of putOrderedObject.) - */ static final void setEntryAt(HashEntry[] tab, int i, - HashEntry e) { - UNSAFE.putOrderedObject(tab, ((long)i << TSHIFT) + TBASE, e); -} -``` -与Segment类似,HashEntry使用UNSAFE.putOrderedObject来设置它的next成员变量,这样既可以提高性能,又能保持并发可见性。同时,entryAt方法和setEntryAt方法也使用了UNSAFE.getObjectVolatile和UNSAFE.putOrderedObject来读取和写入指定索引的HashEntry。 - -总之,Segment数组和HashEntry数组的读取写入一般都是使用UNSAFE。 - - -#5 ConcurrentHashMap的操作 -主要研究ConcurrentHashMap的3种操作——get操作、put操作和size操作. - -## 5.1 get操作 - Segment的get操作实现非常简单和高效. - - 先经过一次再散列 - - 然后使用这个散列值通过散列运算定位到Segment - - 再通过散列算法定位到元素. - - -``` -public V get(Object key) { - Segment s; - HashEntry[] tab; - int h = hash(key); -//找到segment的地址 long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; -//取出segment,并找到其hashtable if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null && - (tab = s.table) != null) { -//遍历此链表,直到找到对应的值 for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile - (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); - e != null; e = e.next) { - K k; - if ((k = e.key) == key || (e.hash == h && key.equals(k))) - return e.value; - } - } - return null; -} -``` -整个get方法不需要加锁,只需要计算两次hash值,然后遍历一个单向链表(此链表长度平均小于2),因此get性能很高。 -高效之处在于整个过程不需要加锁,除非读到的值是空才会加锁重读. -HashTable容器的get方法是需要加锁的,那ConcurrentHashMap的get操作是如何做到不加锁的呢? -原因是它的get方法将要使用的**共享变量都定义成了volatile类型**, -如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value.**定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写**(有一种情况可以被多线程写,就是写入的值不依赖于原值), -在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁. -之所以不会读到过期的值,是因为根据Java内存模型的happen before原则,对volatile字段的写操作先于读操作,即使两个线程同时修改和获取 -volatile变量,get操作也能拿到最新的值, -这是用volatile替换锁的经典应用场景. - -``` -transient volatile int count; -volatile V value; -``` -在定位元素的代码里可以发现,定位HashEntry和定位Segment的散列算法虽然一样,都与数组的长度减去1再相“与”,但是相“与”的值不一样 - - - 定位Segment使用的是元素的hashcode再散列后得到的值的高位 - - 定位HashEntry直接使用再散列后的值. - -其目的是避免两次散列后的值一样,虽然元素在Segment里散列开了,但是却没有在HashEntry里散列开. - -``` -hash >>> segmentShift & segmentMask   // 定位Segment所使用的hash算法 -int index = hash & (tab.length - 1);   // 定位HashEntry所使用的hash算法 -``` - diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/DelayQueue\346\240\270\345\277\203\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/DelayQueue\346\240\270\345\277\203\346\272\220\347\240\201\350\247\243\346\236\220.md" deleted file mode 100644 index 0fca7178a9..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/DelayQueue\346\240\270\345\277\203\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ /dev/null @@ -1,163 +0,0 @@ -# 0 前言 -延迟元素的无边界阻塞队列,在该队列中,仅当元素的延迟到期时才可以使用它. -队首是该 Delayed 元素,其延迟在过去最远过期. -如果没有延迟已经过期,就没有head, poll将返回null. -当元素的getDelay(TimeUnit.NANOSECONDS)方法返回的值小于或等于零时,就会发生过期. -即使未到期的元素无法使用take或poll删除,它们也被视为普通的元素。 例如,size方法返回过期和未过期元素的计数. -此队列不允许空元素. -该类及其迭代器实现集合和迭代器接口的所有可选方法。方法Iterator()中提供的迭代器不能保证以任何特定的顺序遍历DelayQueue中的元素. - -此类是Java Collections Framework的成员. - -# 1 继承体系 -![](https://user-gold-cdn.xitu.io/2020/5/3/171d6b90030e184d?w=3118&h=406&f=png&s=138004 "图片标题") -![](https://user-gold-cdn.xitu.io/2020/5/3/171d6b9002c21d48?w=1462&h=1010&f=png&s=71351 "图片标题") - -- 该队列里的元素必须实现Delayed接口才能入队 -![](https://user-gold-cdn.xitu.io/2020/5/3/171d6b9002bd1894?w=3218&h=542&f=png&s=178161 "图片标题") -混合式的接口,用于标记在给定延迟后应作用的对象。此接口的实现还必须定义一个compareTo方法,该方法提供与其getDelay方法一致的顺序. - -# 2 属性 -- 锁 -![](https://user-gold-cdn.xitu.io/2020/5/3/171d6b9005a8cb1f?w=3266&h=292&f=png&s=94507 "图片标题") - -- PriorityQueue队列里的元素会根据某些属性排列先后的顺序,这里正好可以利用Delayed接口里的getDelay的返回值来进行排序,delayQueue其实就是在每次往优先级队列中添加元素,然后以元素的delay/过期值作为排序的因素,以此来达到先过期的元素会拍在队首,每次从队列里取出来都是最先要过期的元素 -![](https://user-gold-cdn.xitu.io/2020/5/3/171d6b9006df95aa?w=3102&h=336&f=png&s=92567 "图片标题") - -- 指定用于等待队首元素的线程。 Leader-Follower模式的变体用于最大程度地减少不必要的定时等待.当一个线程成为leader时,它仅等待下一个延迟过去,但是其他线程将无限期地等待.leader线程必须在从take()或poll(...)返回之前向其他线程发出信号,除非其他线程成为过渡期间的leader。.每当队首被具有更早到期时间的元素替换时,leader字段都会被重置为null来无效,并且会发出一些等待线程(但不一定是当前leader)的信号。 因此,等待线程必须准备好在等待时获得并失去leader能力. -![](https://user-gold-cdn.xitu.io/2020/5/3/171d6b9031b69c43?w=1750&h=336&f=png&s=51112 "图片标题") - -- 当更新的元素在队首变得可用或新的线程可能需要成为 leader 时,会发出条件信号 -![](https://user-gold-cdn.xitu.io/2020/5/3/171d6b9040e67bd7?w=3038&h=324&f=png&s=94625 "图片标题") - -# 3 构造方法 -## 3.1 无参 -- 创建一个新的 DelayQueue,它初始是空的 -![](https://user-gold-cdn.xitu.io/2020/5/3/171d6b904212e386?w=1472&h=334&f=png&s=47174 "图片标题") - -## 3.2 有参 -- 创建一个DelayQueue,初始包含Delayed实例的给定集合的元素。 -![](https://user-gold-cdn.xitu.io/2020/5/3/171d6b907021a193?w=2618&h=416&f=png&s=105156 "图片标题") - -# 4 新增数据 - -先看看继承自 BlockingQueue 的方法 -## put -- 将指定的元素插入此延迟队列。 由于队列无界,因此此方法将永远不会阻塞. -![](https://user-gold-cdn.xitu.io/2020/5/3/171d6b90604c54ff?w=1462&h=416&f=png&s=61676 "图片标题") -可以看到 put 调用的是 offer - -## DelayQueue#offer -- 将指定的元素插入此延迟队列 -![](https://user-gold-cdn.xitu.io/2020/5/3/171d6b90713320b7?w=2266&h=1734&f=png&s=255721 "图片标题") - -### 执行流程 -1.加锁 -2.元素添加到优先级队列中 -3.检验元素是否为队首,是则设置 leader 为null, 并唤醒一个消费线程 -4.解锁 - -其内部调用的是 PriorityQueue 的 offer 方法 -### PriorityQueue#offer -将指定的元素插入此优先级队列. -```java -public boolean offer(E e) { - // 若元素为 null,抛NPE - if (e == null) - throw new NullPointerException(); - // 修改计数器加一 - modCount++; - int i = size; - // 如果队列大小 > 容量 - if (i >= queue.length) - // => 扩容 - grow(i + 1); - size = i + 1; - // 若队列空,则当前元素正好处于队首 - if (i == 0) - queue[0] = e; - else - // 若队列非空,根据优先级排序 - siftUp(i, e); - return true; -} -``` -#### 执行流程 -1. 元素判空 -2. 队列扩容判断 -3. 根据元素的 compareTo 方法进行排序,希望最终排序的结果是从小到大的,因为想让队首的都是过期的数据,需要在 compareTo 方法实现. - -# 5 取数据 - -## take -检索并删除此队列的头,如有必要,请等待直到延迟过期的元素在此队列上可用 -```java - public E take() throws InterruptedException { - final ReentrantLock lock = this.lock; - // 获取可中断锁 - lock.lockInterruptibly(); - try { - for (;;) { - // 从优先级队列中获取队首 - E first = q.peek(); - if (first == null) - // 队首为 null,说明无元素,当前线程加入等待队列,并阻塞 - available.await(); - else { - // 获取延迟时间 - long delay = first.getDelay(NANOSECONDS); - if (delay <= 0) - // 已到期,获取并删除头部元素 - return q.poll(); - first = null; // 在等待时不要保留引用 - if (leader != null) - available.await(); - else { - Thread thisThread = Thread.currentThread(); - leader = thisThread; - try { - // 线程节点进入等待队列 - available.awaitNanos(delay); - } finally { - if (leader == thisThread) - leader = null; - } - } - } - } - } finally { - // 若leader == null且还存在元素,则唤醒一个消费线程 - if (leader == null && q.peek() != null) - available.signal(); - // 解锁 - lock.unlock(); - } - } -``` -### 执行流程 -1. 加锁 -2. 取出优先级队列的队首 -3. 若队列为空,阻塞 -3. 若队首非空,获得这个元素的delay时间值,如果first的延迟delay时间值为0的话,说明该元素已经到了可以使用的时间,调用poll方法弹出该元素,跳出方法 -4. 若first的延迟delay时间值非0,释放元素first的引用,避免内存泄露 -5. 循环以上操作,直至return - ->take 方法是会无限阻塞,直到队头的过期时间到了才会返回. -> 如果不想无限阻塞,可以尝试 poll 方法,设置超时时间,在超时时间内,队头元素还没有过期的> 话,就会返回 null. - -# 6 解密 leader 元素 -leader 是一个Thread元素,表示当前获取到锁的消费者线程. - -- 以take代码段为例 -![](https://user-gold-cdn.xitu.io/2020/5/3/171d6b9094181aaf?w=2294&h=1608&f=png&s=223030 "图片标题") - -若 leader 非 null,说明已有消费者线程获取锁,直接阻塞当前线程. - -若 leader 为 null,把当前线程赋给 leader,并等待剩余的到期时间,最后释放 leader. -这里假设有多个消费者线程执行 take 取数据,若没有`leader != null` 判断,这些线程都会无限循环,直到返回第一个元素,这显然很浪费系统资源. 所以 leader 在这里相当于一个线程标识,避免消费者线程的无脑竞争. - -> - 注意这里因为first是队首的引用,阻塞时会有很多线程同时持有队首引用,可能导致内存溢出,所以需要手动释放. -![](https://user-gold-cdn.xitu.io/2020/5/3/171d6b90a9422cd2?w=3184&h=202&f=png&s=75465 "图片标题") - -# 7 总结 -DelayQueue 使用排序和超时机制即实现了延迟队列。充分利用已有的 PriorityQueue 排序功能,超时阻塞又恰当好处的利用了锁的等待,在已有机制的基础上进行封装。在实际开发中,可以多多实践这一思想,使代码架构具备高复用性。 \ 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" deleted file mode 100644 index 149a2a9585..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/FurureTask.md" +++ /dev/null @@ -1,311 +0,0 @@ -# 1 简介 -- 使用继承方式的好处是方便传参,可以在子类里面添加成员变量,通过 set 方法设置参数或者通过构造函数进行传递 -- 使用 Runnable 方式,则只能使用主线程里面被声明为 final 变量 - -不好的地方是 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) -# 3 Callable -Callable函数式接口定义了唯一方法 - call(). -我们可以在Callable的实现中声明强类型的返回值,甚至是抛出异常。同时,利用call()方法直接返回结果的能力,省去读取值时的类型转换。 -- 源码定义 - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NjQ1XzIwMjAwMjAyMjA0MjA0MjIyLnBuZw?x-oss-process=image/format,png) -注意到返回值是一个泛型,使用的时候,不会直接使用 Callable,而是和 FutureTask 协同. - -# 4 Future -- Callable 可以返回线程的执行结果,在获取结果时,就需要用到 Future 接口. -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2OTIwXzIwMjAwMjA0MDQwMDA3MzMucG5n?x-oss-process=image/format,png) - -Future是 Java5 中引入的接口,当提交一个Callable对象给线程池时,将得到一个Future对象,并且它和传入的Callable有相同的结果类型声明。 - -它取代了Java5 前直接操作 Thread 实例的做法。以前,不得不用Thread.join()或者Thread.join(long millis)等待任务完成. - - -Future表示异步计算的结果。提供了一些方法来检查计算是否完成,等待其完成以及检索计算结果。 -只有在计算完成时才可以使用get方法检索结果,必要时将其阻塞,直到准备就绪为止。取消是通过cancel方法执行的。提供了其他方法来确定任务是正常完成还是被取消。一旦计算完成,就不能取消计算。 - -如果出于可取消性的目的使用`Future`而不提供可用的结果,则可以声明`Future <?>`形式的类型,并作为基础任务的结果返回null。 - -## 4.1 Future API -### 4.1.1 cancel - 尝试取消执行任务 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2Njc4XzIwMjAwMjA0MDIxOTEwMTI1LnBuZw?x-oss-process=image/format,png) -一个比较复杂的方法,当任务处于不同状态时,该方法有不同响应: -- 任务 已经完成 / 已经取消 / 由于某些其他原因无法被取消,该尝试会直接失败 -- 尝试成功,且此时任务尚未开始,调用后是可以取消成功的 -- 任务已经开始,则 *mayInterruptIfRunning* 参数确定是否可以中断执行该任务的线程以尝试停止该任务。 - - -此方法返回后,对 *isDone* 的后续调用将始终返回 true. -如果此方法返回 true,则随后对 *isCancelled* 的调用将始终返回 true. - -### 4.1.2 isCancelled - 是否被取消 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2ODM0XzIwMjAwMjA0MDMwMzU2OTM1LnBuZw?x-oss-process=image/format,png) -如果此任务在正常完成之前被取消,则返回true. - -### 4.1.3 isDone - 是否完成 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NTc0XzIwMjAwMjA0MDMxMDA1NDg4LnBuZw?x-oss-process=image/format,png) -如果此任务完成,则返回true. - -完成可能是由于正常终止,异常或取消引起的,在所有这些情况下,此方法都将返回true. - -### 4.1.4 get - 获取结果 -等待任务完成,然后获取其结果。 -![](https://img-blog.csdnimg.cn/20210615233538777.png) - -若: -- 任务被取消,抛 *CancellationException* -- 当前线程在等待时被中断,抛 *InterruptedException* -- 任务抛出了异常,抛 *ExecutionException* - -### 4.1.5 timed get - 超时获取 -- 必要时最多等待给定时间以完成任务,然后获取其结果(如果有的话)。 -![](https://img-blog.csdnimg.cn/20210615233634165.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) - -![](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,极大地方便了我们的开发调控。 - -# 5 RunnableFuture -Java6 时提供的持有 Runnable 性质的 Future. - -成功执行run方法导致Future的完成,并允许访问其结果. - -RunnableFuture接口比较简单,就是继承了 Runnable 和 Future 接口。只提供一个*run*方法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2OTE2XzIwMjAwMjA0MDQwMjM0NzM1LnBuZw?x-oss-process=image/format,png) - -现在,我们应该都知道,创建任务有两种方式 -- 无返回值的 Runnable -- 有返回值的 Callable - -但这样的设计,对于其他 API 来说并不方便,没办法统一接口. - -所以铺垫了这么多,本文的主角 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) -前面的Future是一个接口,而 FutureTask 才是一个实实在在的工具类,是线程运行的具体任务. -- 实现了 RunnableFuture 接口 -- 也就是实现了 Runnnable 接口,即FutureTask 本身就是个 Runnnable -- 也表明了 FutureTask 实现了 Future,具备对任务进行管理的功能 - -## 6.1 属性 -### 6.1.1 运行状态 -最初为`NEW`。 运行状态仅在*set*,*setException*和*cancel*方法中转换为最终状态。 -在完成期间,状态可能会呈现`COMPLETING`(正在设置结果时)或`INTERRUPTING`(仅在中断运行任务去满足cancel(true)时)的瞬态值。 -从这些中间状态到最终状态的转换使用更加低价的有序/惰性写入,因为值是唯一的,无法进一步修改。 - - -- 注意这些常量字段的定义方式,遵循避免魔鬼数字的编程规约. -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2ODg4XzIwMjAwMjA4MTM0OTU2MTQ1LnBuZw?x-oss-process=image/format,png) - -- NEW -线程任务创建,开始状态 -- COMPLETING -任务执行中,正在运行状态 -- NORMAL -任务执行结束 -- EXCEPTIONAL -任务异常 -- CANCELLED -任务取消成功 -- INTERRUPTING -任务正在被打断中 -- INTERRUPTED = 6 -任务被打断成功 - -#### 可能的状态转换 -- NEW -> COMPLETING -> NORMAL -- NEW -> COMPLETING -> EXCEPTIONAL -- NEW -> CANCELLED -- NEW -> INTERRUPTING -> INTERRUPTED - -### 6.1.2 其他属性 -- 组合的 callable,这样就具备了转化 Callable 和 Runnable 的功能 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2ODQ3XzIwMjAwMjA4MjI0MDM0NzU0LnBuZw?x-oss-process=image/format,png) -- 从ge()返回或抛出异常的结果,非volatile,受状态读/写保护 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NTYzXzIwMjAwMjA4MjI0MzQzNjQ2LnBuZw?x-oss-process=image/format,png) -- 运行 callable 的线程; 在run()期间进行CAS -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NzkyXzIwMjAwMjA4MjI1NDU3Mzc2LnBuZw?x-oss-process=image/format,png) -- 记录调用 get 方法时被等待的线程 - 栈形式 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NjI5XzIwMjAwMjA4MjI1NzM1NzE5LnBuZw?x-oss-process=image/format,png) -从属性上我们明显看到 Callable 是作为 FutureTask 的属性之一,这也就让 FutureTask 接着我们看下 FutureTask 的构造器,看看两者是如何转化的。 - -## 6.2 构造方法 -### 6.2.1 Callable 参数 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY3MTI4XzIwMjAwMjA4MjMwMjIyNDg2LnBuZw?x-oss-process=image/format,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) -- Executors.callable 方法负责将 runnable 适配成 callable. - ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NzE0XzIwMjAwMjA4MjMyMDUxMjQ2LnBuZw?x-oss-process=image/format,png) - - 通过转化类 RunnableAdapter进行适配 - ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY3MzE0XzIwMjAwMjA4MjMyMjExNjE2LnBuZw?x-oss-process=image/format,png) - -### 6.2.3 小结 -我们可以学习这里的适配器模式,目标是要把 Runnable 适配成 Callable,那么我们首先要实现 Callable 接口,并且在 Callable 的 call 方法里面调用被适配对象即 Runnable的方法即可. - -下面,让我们看看对 Future 接口方法的具体实现. -## 6.3 get -我们来看限时阻塞的 get 方法,源码如下: - -```java -public V get(long timeout, TimeUnit unit) - throws InterruptedException, ExecutionException, TimeoutException { - if (unit == null) - throw new NullPointerException(); - int s = state; - // 任务已经在执行中了,并且等待一定时间后,仍在执行中,直接抛异常 - if (s <= COMPLETING && - (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING) - throw new TimeoutException(); - // 任务完成,返回执行结果 - return report(s); -} -``` -等待任务执行完成 -```java -private int awaitDone(boolean timed, long nanos) - throws InterruptedException { - // 计算等待终止时间,如果一直等待的话,终止时间为 0 - final long deadline = timed ? System.nanoTime() + nanos : 0L; - WaitNode q = null; - // 不排队 - boolean queued = false; - // 无限循环 - for (;;) { - // 如果线程已经被打断了,删除,抛异常 - if (Thread.interrupted()) { - removeWaiter(q); - throw new InterruptedException(); - } - // 当前任务状态 - int s = state; - // 当前任务已经执行完了,返回 - if (s > COMPLETING) { - // 当前任务的线程置空 - if (q != null) - q.thread = null; - return s; - } - // 如果正在执行,当前线程让出 cpu,重新竞争,防止 cpu 飙高 - else if (s == COMPLETING) // cannot time out yet - Thread.yield(); - // 如果第一次运行,新建 waitNode,当前线程就是 waitNode 的属性 - else if (q == null) - q = new WaitNode(); - // 默认第一次都会执行这里,执行成功之后,queued 就为 true,就不会再执行了 - // 把当前 waitNode 当做 waiters 链表的第一个 - else if (!queued) - queued = UNSAFE.compareAndSwapObject(this, waitersOffset, - q.next = waiters, q); - // 如果设置了超时时间,并过了超时时间的话,从 waiters 链表中删除当前 wait - else if (timed) { - nanos = deadline - System.nanoTime(); - if (nanos <= 0L) { - removeWaiter(q); - return state; - } - // 没有过超时时间,线程进入 TIMED_WAITING 状态 - LockSupport.parkNanos(this, nanos); - } - // 没有设置超时时间,进入 WAITING 状态 - else - LockSupport.park(this); - } -} -``` - -get 是一种阻塞式方法,当发现任务还在进行中,没有完成时,就会阻塞当前进程,等待任务完成后再返回结果值。 - -阻塞底层使用的是 LockSupport.park 方法,使当前线程进入 `WAITING` 或 `TIMED_WAITING` 态。 - -## 6.4 run -该方法可被直接调用,也可由线程池调用 -```java -public void run() { - // 状态非 NEW 或当前任务已有线程在执行,直接返回 - if (state != NEW || - !UNSAFE.compareAndSwapObject(this, runnerOffset, - null, Thread.currentThread())) - return; - try { - Callable c = callable; - // Callable 非空且已 NEW - if (c != null && state == NEW) { - V result; - boolean ran; - try { - // 真正执行业务代码的地方 - result = c.call(); - ran = true; - } catch (Throwable ex) { - result = null; - ran = false; - setException(ex); - } - // 给 outcome 赋值,这样 Future.get 方法执行时,就可以从 outCome 中取值 - if (ran) - set(result); - } - } finally { - runner = null; - int s = state; - if (s >= INTERRUPTING) - handlePossibleCancellationInterrupt(s); - } -} -``` - -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) { - if (!(state == NEW &&//任务状态不是创建 并且不能把 new 状态置为取消,直接返回 false - UNSAFE.compareAndSwapInt(this, stateOffset, NEW, - mayInterruptIfRunning ? INTERRUPTING : CANCELLED))) - return false; - // 进行取消操作,打断可能会抛出异常,选择 try finally 的结构 - try { // in case call to interrupt throws exception - if (mayInterruptIfRunning) { - try { - Thread t = runner; - if (t != null) - t.interrupt(); - } finally { // final state - //状态设置成已打断 - UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); - } - } - } finally { - // 清理线程 - finishCompletion(); - } - return true; -} -``` - -# 7 总结 -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/Future\344\270\216FutureTask.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/Future\344\270\216FutureTask.md" deleted file mode 100644 index 18faa0fe46..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/Future\344\270\216FutureTask.md" +++ /dev/null @@ -1,471 +0,0 @@ -# 1 Future -Future 表示一个任务的生命周期,是一个可取消的异步运算。提供了相应的方法来判断任务状态(完成或取消),以及获取任务的结果和取消任务等。 -适合具有可取消性和执行时间较长的异步任务。 - -并发包中许多异步任务类都继承自Future,其中最典型的就是 FutureTask - -## 1.1 介绍 -![](https://upload-images.jianshu.io/upload_images/4685968-0857aed817311722.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。 -计算完成后只能使用` get `方法来获取结果,如有必要,计算完成前可以阻塞此方法。 -取消则由 `cancel` 方法来执行。 -还提供了其他方法,以确定任务是正常完成还是被取消了。 -一旦计算完成,就不能再取消计算。 -如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future 形式类型、并返回 null 作为底层任务的结果。 - -也就是说Future具有这样的特性 -- 异步执行,可用 get 方法获取执行结果 -- 如果计算还没完成,get 方法是会阻塞的,如果完成了,是可以多次获取并立即得到结果的 -- 如果计算还没完成,是可以取消计算的 -- 可以查询计算的执行状态 - -# 2 FutureTask - -FutureTask 为 Future 提供了基础实现,如获取任务执行结果(get)和取消任务(cancel)等。如果任务尚未完成,获取任务执行结果时将会阻塞。一旦执行结束,任务就不能被重启或取消(除非使用`runAndReset`执行计算)。 - -FutureTask 常用来封装 Callable 和 Runnable,也可作为一个任务提交到线程池中执行。除了作为一个独立的类,此类也提供创建自定义 task 类使用。FutureTask 的线程安全由CAS保证。 - -FutureTask 内部维护了一个由`volatile`修饰的`int`型变量—`state`,代表当前任务的运行状态 -![](https://upload-images.jianshu.io/upload_images/4685968-97655f663f4b14db.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -* NEW:新建 -* COMPLETING:完成 -* NORMAL:正常运行 -* EXCEPTIONAL:异常退出 -* CANCELLED:任务取消 -* INTERRUPTING:线程中断中 -* INTERRUPTED:线程已中断 - -在这七种状态中,有四种任务终止状态:NORMAL、EXCEPTIONAL、CANCELLED、INTERRUPTED。各种状态的转化如下: -![](https://upload-images.jianshu.io/upload_images/4685968-f080b652b4823926.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -## 数据结构及核心参数 -![](https://upload-images.jianshu.io/upload_images/4685968-4eaea72991f9d694.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -![](https://upload-images.jianshu.io/upload_images/4685968-cc194172c03f44ea.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -```java -//内部持有的callable任务,运行完毕后置空 -private Callable callable; - -//从get()中返回的结果或抛出的异常 -private Object outcome; // non-volatile, protected by state reads/writes - -//运行callable的线程,在 run 时进行 CAS 操作 -private volatile Thread runner; - -//使用Treiber栈保存等待线程 -private volatile WaitNode waiters; -``` -FutureTask 继承了`Runnale`和`Future`,本身也作为一个线程运行,可以提交给线程池执行。 -维护了一个内部类`WaitNode`,使用简单的Treiber栈(无锁并发栈)实现,用于存储等待线程。 -FutureTask 只有一个自定义的同步器 Sync 的属性,所有的方法都是委派给此同步器来实现。这也是JUC里使用AQS的通用模式。 - - -# 源码解析 -FutureTask 的同步器 -由于Future在任务完成后,可以多次自由获取结果,因此,用于控制同步的AQS使用共享模式。 - -FutureTask 底层任务的执行状态保存在AQS的状态里。AQS是否允许线程获取(是否阻塞)是取决于任务是否执行完成,而不是具体的状态值。 -``` -private final class Sync extends AbstractQueuedSynchronizer { - // 定义表示任务执行状态的常量。由于使用了位运算进行判断,所以状态值分别是2的幂。 - - // 表示任务已经准备好了,可以执行 - private static final int READY = 0; - - // 表示任务正在执行中 - private static final int RUNNING = 1; - - // 表示任务已执行完成 - private static final int RAN = 2; - - // 表示任务已取消 - private static final int CANCELLED = 4; - - - // 底层的表示任务的可执行对象 - private final Callable callable; - - // 表示任务执行结果,用于get方法返回。 - private V result; - - // 表示任务执行中的异常,用于get方法调用时抛出。 - private Throwable exception; - - /* - * 用于执行任务的线程。在 set/cancel 方法后置为空,表示结果可获取。 - * 必须是 volatile的,用于确保完成后(result和exception)的可见性。 - * (如果runner不是volatile,则result和exception必须都是volatile的) - */ - private volatile Thread runner; - - - /** - * 已完成或已取消 时成功获取 - */ - protected int tryAcquireShared( int ignore) { - return innerIsDone() ? 1 : -1; - } - - /** - * 在设置最终完成状态后让AQS总是通知,通过设置runner线程为空。 - * 这个方法并没有更新AQS的state属性, - * 所以可见性是通过对volatile的runner的写来保证的。 - */ - protected boolean tryReleaseShared( int ignore) { - runner = null; - return true; - } - - - // 执行任务的方法 - void innerRun() { - // 用于确保任务不会重复执行 - if (!compareAndSetState(READY, RUNNING)) - return; - - // 由于Future一般是异步执行,所以runner一般是线程池里的线程。 - runner = Thread.currentThread(); - - // 设置执行线程后再次检查,在执行前检查是否被异步取消 - // 由于前面的CAS已把状态设置RUNNING, - if (getState() == RUNNING) { // recheck after setting thread - V result; - // - try { - result = callable.call(); - } catch (Throwable ex) { - // 捕获任务执行过程中抛出的所有异常 - setException(ex); - return; - } - set(result); - } else { - // 释放等待的线程 - releaseShared(0); // cancel - } - } - - // 设置结果 - void innerSet(V v) { - // 放在循环里进行是为了失败后重试。 - for (;;) { - // AQS初始化时,状态值默认是 0,对应这里也就是 READY 状态。 - int s = getState(); - - // 已完成任务不能设置结果 - if (s == RAN) - return; - - // 已取消 的任务不能设置结果 - if (s == CANCELLED) { - // releaseShared 会设置runner为空, - // 这是考虑到与其他的取消请求线程 竞争中断 runner - releaseShared(0); - return; - } - - // 先设置已完成,免得多次设置 - if (compareAndSetState(s, RAN)) { - result = v; - releaseShared(0); // 此方法会更新 runner,保证result的可见性 - done(); - return; - } - } - } - - // 获取异步计算的结果 - V innerGet() throws InterruptedException, ExecutionException { - acquireSharedInterruptibly(0);// 获取共享,如果没有完成则会阻塞。 - - // 检查是否被取消 - if (getState() == CANCELLED) - throw new CancellationException(); - - // 异步计算过程中出现异常 - if (exception != null) - throw new ExecutionException(exception); - - return result; - } - - // 取消执行任务 - boolean innerCancel( boolean mayInterruptIfRunning) { - for (;;) { - int s = getState(); - - // 已完成或已取消的任务不能再次取消 - if (ranOrCancelled(s)) - return false; - - // 任务处于 READY 或 RUNNING - if (compareAndSetState(s, CANCELLED)) - break; - } - // 任务取消后,中断执行线程 - if (mayInterruptIfRunning) { - Thread r = runner; - if (r != null) - r.interrupt(); - } - releaseShared(0); // 释放等待的访问结果的线程 - done(); - return true; - } - - /** - * 检查任务是否处于完成或取消状态 - */ - private boolean ranOrCancelled( int state) { - return (state & (RAN | CANCELLED)) != 0; - } - - // 其他方法省略 -} -``` -从 innerCancel 方法可知,取消操作只是改变了任务对象的状态并可能会中断执行线程。如果任务的逻辑代码没有响应中断,则会一直异步执行直到完成,只是最终的执行结果不会被通过get方法返回,计算资源的开销仍然是存在的。 - -总的来说,Future 是线程间协调的一种工具。 - -### AbstractExecutorService.submit(Callable task) - -FutureTask 内部实现方法都很简单,先从线程池的`submit`分析。`submit`方法默认实现在`AbstractExecutorService`,几种实现源码如下: -![](https://upload-images.jianshu.io/upload_images/4685968-68b9d862b491752d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -```java -public Future submit(Runnable task) { - if (task == null) throw new NullPointerException(); - RunnableFuture ftask = newTaskFor(task, null); - execute(ftask); - return ftask; -} -public Future submit(Runnable task, T result) { - if (task == null) throw new NullPointerException(); - RunnableFuture ftask = newTaskFor(task, result); - execute(ftask); - return ftask; -} -public Future submit(Callable task) { - if (task == null) throw new NullPointerException(); - RunnableFuture ftask = newTaskFor(task); - execute(ftask); - return ftask; -} -protected RunnableFuture newTaskFor(Runnable runnable, T value) { - return new FutureTask(runnable, value); -} -public FutureTask(Runnable runnable, V result) { - this.callable = Executors.callable(runnable, result); - this.state = NEW; // ensure visibility of callable -} - -``` -首先调用`newTaskFor`方法构造`FutureTask`,然后调用`execute`把任务放进线程池中,返回`FutureTask` - -#### FutureTask.run() -![](https://upload-images.jianshu.io/upload_images/4685968-18d51e170fe206db.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -```java -public void run() { - //新建任务,CAS替换runner为当前线程 - if (state != NEW || - !UNSAFE.compareAndSwapObject(this, runnerOffset, - null, Thread.currentThread())) - return; - try { - Callable c = callable; - if (c != null && state == NEW) { - V result; - boolean ran; - try { - result = c.call(); - ran = true; - } catch (Throwable ex) { - result = null; - ran = false; - setException(ex); - } - if (ran) - set(result);//设置执行结果 - } - } finally { - // runner must be non-null until state is settled to - // prevent concurrent calls to run() - runner = null; - // state must be re-read after nulling runner to prevent - // leaked interrupts - int s = state; - if (s >= INTERRUPTING) - handlePossibleCancellationInterrupt(s);//处理中断逻辑 - } -} - -``` -运行任务,如果任务状态为`NEW`状态,则利用`CAS`修改为当前线程。执行完毕调用`set(result)`方法设置执行结果。 -`set(result)`源码如下 -![](https://upload-images.jianshu.io/upload_images/4685968-f85add9ff47d62ac.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -首先利用`cas`修改`state`状态为![](https://upload-images.jianshu.io/upload_images/4685968-20ab2573e2b9f2ce.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)设置返回结果,然后使用 lazySet(`UNSAFE.putOrderedInt`)的方式设置`state`状态为![](https://upload-images.jianshu.io/upload_images/4685968-976ce0a1f461b825.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -结果设置完毕后,调用`finishCompletion()`唤醒等待线程 -![](https://upload-images.jianshu.io/upload_images/4685968-492c91e4c50716cf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -``` -private void finishCompletion() { - for (WaitNode q; (q = waiters) != null;) { - if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {//移除等待线程 - for (;;) {//自旋遍历等待线程 - Thread t = q.thread; - if (t != null) { - q.thread = null; - LockSupport.unpark(t);//唤醒等待线程 - } - WaitNode next = q.next; - if (next == null) - break; - q.next = null; // unlink to help gc - q = next; - } - break; - } - } - //任务完成后调用函数,自定义扩展 - done(); - callable = null; // to reduce footprint -} -``` -回到`run`方法,如果在 run 期间被中断,此时需要调用`handlePossibleCancellationInterrupt`处理中断逻辑,确保任何中断(例如`cancel(true)`)只停留在当前`run`或`runAndReset`的任务中 -![](https://upload-images.jianshu.io/upload_images/4685968-8cb34d01686bb1ae.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -``` -private void handlePossibleCancellationInterrupt(int s) { - //在中断者中断线程之前可能会延迟,所以我们只需要让出CPU时间片自旋等待 - if (s == INTERRUPTING) - while (state == INTERRUPTING) - Thread.yield(); // wait out pending interrupt -} -``` -* * * -## FutureTask.runAndReset() -![](https://upload-images.jianshu.io/upload_images/4685968-398d635ccf4d8e2c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -`runAndReset`是 FutureTask另外一个任务执行的方法,它不会返回执行结果,而且在任务执行完之后会重置`stat`的状态为`NEW`,使任务可以多次执行。 -`runAndReset`的典型应用是在 ScheduledThreadPoolExecutor 中,周期性的执行任务。 -* * * -## FutureTask.get() -![](https://upload-images.jianshu.io/upload_images/4685968-45f39b6cf4cdc132.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -FutureTask 通过`get()`获取任务执行结果。 -如果任务处于未完成的状态(`state <= COMPLETING`),就调用`awaitDone`等待任务完成。 -任务完成后,通过`report`获取执行结果或抛出执行期间的异常。 -![](https://upload-images.jianshu.io/upload_images/4685968-149b414c7a0f6334.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -## awaitDone(boolean timed, long nanos) -![](https://upload-images.jianshu.io/upload_images/4685968-0795ecddc68db095.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -``` -private int awaitDone(boolean timed, long nanos) - throws InterruptedException { - final long deadline = timed ? System.nanoTime() + nanos : 0L; - WaitNode q = null; - boolean queued = false; - for (;;) {//自旋 - if (Thread.interrupted()) {//获取并清除中断状态 - removeWaiter(q);//移除等待WaitNode - throw new InterruptedException(); - } - - int s = state; - if (s > COMPLETING) { - if (q != null) - q.thread = null;//置空等待节点的线程 - return s; - } - else if (s == COMPLETING) // cannot time out yet - Thread.yield(); - else if (q == null) - q = new WaitNode(); - else if (!queued) - //CAS修改waiter - queued = UNSAFE.compareAndSwapObject(this, waitersOffset, - q.next = waiters, q); - else if (timed) { - nanos = deadline - System.nanoTime(); - if (nanos <= 0L) { - removeWaiter(q);//超时,移除等待节点 - return state; - } - LockSupport.parkNanos(this, nanos);//阻塞当前线程 - } - else - LockSupport.park(this);//阻塞当前线程 - } -} - -``` -`awaitDone`用于等待任务完成,或任务因为中断或超时而终止。返回任务的完成状态。 -1. 如果线程被中断,首先清除中断状态,调用`removeWaiter`移除等待节点,然后抛`InterruptedException`。`removeWaiter`源码如下: -![](https://upload-images.jianshu.io/upload_images/4685968-cc9aa7802cf52d23.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -``` -private void removeWaiter(WaitNode node) { - if (node != null) { - node.thread = null;//首先置空线程 - retry: - for (;;) { // restart on removeWaiter race - //依次遍历查找 - for (WaitNode pred = null, q = waiters, s; q != null; q = s) { - s = q.next; - if (q.thread != null) - pred = q; - else if (pred != null) { - pred.next = s; - if (pred.thread == null) // check for race - continue retry; - } - else if (!UNSAFE.compareAndSwapObject(this, waitersOffset,q, s)) //cas替换 - continue retry; - } - break; - } - } -} - -``` -2. 如果当前为结束态(`state>COMPLETING`),则根据需要置空等待节点的线程,并返回 Future 状态 -3. 如果当前为正在完成(`COMPLETING`),说明此时 Future 还不能做出超时动作,为任务让出CPU执行时间片 -4. 如果`state`为`NEW`,先新建一个`WaitNode`,然后`CAS`修改当前`waiters` -5. 如果等待超时,则调用`removeWaiter`移除等待节点,返回任务状态;如果设置了超时时间但是尚未超时,则`park`阻塞当前线程 -6. 其他情况直接阻塞当前线程 -* * * - -#### FutureTask.cancel(boolean mayInterruptIfRunning) - -``` -public boolean cancel(boolean mayInterruptIfRunning) { - //如果当前Future状态为NEW,根据参数修改Future状态为INTERRUPTING或CANCELLED - if (!(state == NEW && - UNSAFE.compareAndSwapInt(this, stateOffset, NEW, - mayInterruptIfRunning ? INTERRUPTING : CANCELLED))) - return false; - try { // in case call to interrupt throws exception - if (mayInterruptIfRunning) {//可以在运行时中断 - try { - Thread t = runner; - if (t != null) - t.interrupt(); - } finally { // final state - UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); - } - } - } finally { - finishCompletion();//移除并唤醒所有等待线程 - } - return true; -} - -``` - -**说明**:尝试取消任务。如果任务已经完成或已经被取消,此操作会失败。 -如果当前Future状态为`NEW`,根据参数修改Future状态为`INTERRUPTING`或`CANCELLED`。 -如果当前状态不为`NEW`,则根据参数`mayInterruptIfRunning`决定是否在任务运行中也可以中断。中断操作完成后,调用`finishCompletion`移除并唤醒所有等待线程。 - -* * * -##示例 -![](https://upload-images.jianshu.io/upload_images/4685968-ac4c4509dd7e34b6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -![](https://upload-images.jianshu.io/upload_images/4685968-94a4fbbe88323fe8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -# 小结 - -本章重点:**FutureTask 结果返回机制,以及内部运行状态的转变** 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" deleted file mode 100644 index ad32141823..0000000000 --- "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" +++ /dev/null @@ -1,194 +0,0 @@ -学习完 AQS,本文我们就来研究第一个 AQS 的实现类:ReentrantLock。 - -# 1 基本设计 -`ReentrantLock` 可重入锁,可重入表示同一个线程可以对同一个共享资源重复的加锁或释放锁。 - -具有与使用 synchronized 方法和语句访问的隐式监视器锁相同的基本行为和语义的可重入互斥锁,但具有扩展功能。 - -`ReentrantLock` 由最后成功锁定但尚未解锁的线程所拥有。当另一个线程不拥有该锁时,调用该锁的线程将成功返回该锁。如果当前线程已经拥有该锁,则该方法将立即返回。可以使用 *isHeldByCurrentThread* 和*getHoldCount* 方法进行检查。 - -此类的构造函数接受一个可选的 fairness 参数。设置为true时,在争用下,锁倾向于授予给等待时间最长的线程。否则,此锁不能保证任何特定的访问顺序。使用多线程访问的公平锁的程序可能会比使用默认设置的程序呈现较低的总吞吐量(即较慢;通常要慢得多),但获得锁并保证没有饥饿的时间差异较小。但是请注意,锁的公平性不能保证线程调度的公平性。因此,使用公平锁的多个线程之一可能会连续多次获得它,而其他活动线程没有进行且当前未持有该锁。还要注意,未定时的 *tryLock* 方法不支持公平性设置。如果锁可用,即使其他线程正在等待,它将成功。 - -建议的做法是始终立即在调用后使用try块进行锁定,最常见的是在构造之前/之后,例如: - -```java - class X { - private final ReentrantLock lock = new ReentrantLock(); - // ... - - public void m() { - lock.lock(); // block until condition holds - try { - // ... method body - } finally { - lock.unlock() - } - } - } -``` -除了实现Lock接口之外,此类还定义了许多用于检查锁状态的 public 方法和 protected 方法。 其中一些方法仅对检测和监视有用。 - -此类的序列化与内置锁的行为相同:反序列化的锁处于解锁状态,而不管序列化时的状态如何。 - -此锁通过同一线程最多支持2147483647个递归锁。 尝试超过此限制会导致锁定方法引发错误。 -# 2 类架构 -- ReentrantLock 本身不继承 AQS,而是实现了 Lock 接口 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzJhMjI0MGZkY2I?x-oss-process=image/format,png) - -Lock 接口定义了各种加锁,释放锁的方法,比如 lock() 这种不响应中断获取锁,在ReentrantLock 中实现的 lock 方法是通过调用自定义的同步器 Sync 中的的同名抽象方法,再由两种模式的子类具体实现此抽象方法来获取锁。 - -ReentrantLock 就负责实现这些接口,使用时,直接调用的也是这些方法,这些方法的底层实现都是交给 Sync 实现。 - -# 3 构造方法 -- 无参数构造方法 -相当于 ReentrantLock(false),默认为非公平的锁 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzI5ZjY0MWI1MTc?x-oss-process=image/format,png) -- 有参构造方法,可以选择锁的公平性 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzI5ZjJhYWE5YTE?x-oss-process=image/format,png) - -可以看出 -- 公平锁依靠 FairSync 实现 -- 非公平锁依靠 NonfairSync 实现 - -# 4 Sync 同步器 -- 结构图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzJhMDgwZGVkMTc?x-oss-process=image/format,png) -- 继承体系![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzI5ZjI5MWM4YjA?x-oss-process=image/format,png) - -可见是ReentrantLock的抽象静态内部类 Sync 继承了 AbstractQueuedSynchronizer ,所以ReentrantLock依靠 Sync 就持有了锁的框架,只需要 Sync 实现 AQS 规定的非 final 方法即可,只交给子类 NonfairSync 和 FairSync 实现 lock 和 tryAcquire 方法 - -## 4.1 NonfairSync - 非公平锁 -- Sync 对象的非公平锁 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzI5ZjM5ZTNhMDQ?x-oss-process=image/format,png) -### 4.1.1 lock -- 非公平模式的 lock 方法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzJhMTNkYmExYTg?x-oss-process=image/format,png) -- 若 CAS(已经定义并实现在 AQS 中的 final 方法)state 成功,即获取锁成功并将当前线程设置为独占线程 -- 若 CAS state 失败,即获取锁失败,则进入 AQS 中已经定义并实现的 *Acquire* 方法善后 - -这里的 lock 方法并没有直接调用 AQS 提供的 acquire 方法,而是先试探地使用 CAS 获取了一下锁,CAS 操作失败再调用 acquire 方法。这样设计可以提升性能。因为可能很多时候我们能在第一次试探获取时成功,而不需要再经过 `acquire => tryAcquire => nonfairAcquire` 的调用链。 - -### 4.1.2 tryAcquire -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzJhMjAzYmVkMmQ?x-oss-process=image/format,png) -其中真正的实现 *nonfairTryAcquire* 就定义在其父类 Sync 中。下一节分析。 -## 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 的方法,表示先尝试获得锁,失败之后进入同步队列阻塞等待。 -### 4.2.2 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) -#### 公平模式 -不要授予访问权限,除非递归调用或没有等待线程或是第一个调用的。 -```java -protected final boolean tryAcquire(int acquires) { - // 获取当前的线程 - final Thread current = Thread.currentThread(); - // 获取 state 锁的状态(volatile 读语义) - int c = getState(); - // state == 0 => 尚无线程获取锁 - if (c == 0) { - // 判断 AQS 的同步对列里是否有线程等待 - 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** 确保了公平性。 -#### hasQueuedPredecessors -判断当前线程是不是属于同步队列的头节点的下一个节点(头节点是释放锁的节点): -- 是(返回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()); -} -``` -# 5 nonfairTryAcquire -执行非公平的 *tryLock*。 *tryAcquire* 是在子类中实现的,但是都需要对*trylock* 方法进行非公平的尝试。 -```java -final boolean nonfairTryAcquire(int acquires) { - final Thread current = Thread.currentThread(); - int c = getState(); - if (c == 0) { - // 这里可能有竞争,所以可能失败 - if (compareAndSetState(0, acquires)) { - // 获取锁成功, 设置获取独占锁的线程 - setExclusiveOwnerThread(current); - return true; - } - } - else if (current == getExclusiveOwnerThread()) { - int nextc = c + acquires; - if (nextc < 0) - throw new Error("Maximum lock count exceeded"); - setState(nextc); - return true; - } - return false; -} -``` -无参的 *tryLock* 调用的就是此方法 - -# 6 tryLock -## 6.1 无参 -Lock 接口中定义的方法。 - -- 仅当锁在调用时未被其他线程持有时,才获取锁 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzJhMjVmYmYyYzg?x-oss-process=image/format,png) -如果锁未被其他线程持有,则获取锁,并立即返回值 true,将锁持有计数设置为1。即使这个锁被设置为使用公平的排序策略,如果锁可用,调用 *tryLock()* 也会立即获得锁,不管其他线程是否正在等待锁。这种妥协行为在某些情况下是有用的,虽然它破坏了公平。如果想为这个锁执行公平设置,那么使用 *tryLock(0, TimeUnit.SECONDS)*,这几乎是等价的(它还可以检测到中断)。 - -如果当前线程已经持有该锁,那么持有计数将增加1,方法返回true。 -如果锁被另一个线程持有,那么这个方法将立即返回值false。 - -## 6.2 有参 -- 提供了超时时间的入参,在时间内,仍没有得到锁,会返回 false -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzJhMzZhNTAwMzQ?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzJhM2ZkMGFmZmI?x-oss-process=image/format,png) -其中的 doAcquireNanos 已经实现好在 AQS 中。 -# 7 tryRelease -释放锁,对于公平和非公平锁都适用 - -```java -protected final boolean tryRelease(int releases) { - // 释放 releases (由于可重入,这里的 c 不一定直接为 0) - int c = getState() - releases; - // 判断当前线程是否是获取独占锁的线程 - if (Thread.currentThread() != getExclusiveOwnerThread()) - throw new IllegalMonitorStateException(); - boolean free = false; - // 锁已被完全释放 - if (c == 0) { - free = true; - // 无线程持有独占锁,所以置 null - setExclusiveOwnerThread(null); - } - setState(c); - return free; -} -``` \ 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/Thread\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/Thread\346\272\220\347\240\201\350\247\243\346\236\220.md" deleted file mode 100644 index 14a15bdfd5..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/Thread\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ /dev/null @@ -1,283 +0,0 @@ -# 1 类注释 -程序中执行的线程。JVM允许应用程序拥有多个并发运行的执行线程。 - -每个线程都有一个优先级。优先级高的线程优先于优先级低的线程执行。每个线程可能被标记为守护线程,也可能不被标记为守护线程。 - -当在某个线程中运行的代码创建一个新 Thread 对象时,新线程的优先级最初设置为创建线程的优先级,并且只有在创建线程是一个守护线程时,新线程才是守护线程。 - -当JVM启动时,通常有一个非守护的线程(它通常调用某个指定类的main方法)。JVM 继续执行线程,直到发生以下任何一种情况时停止: -- *Runtime* 类的 *exit* 方法已被调用,且安全管理器已允许执行退出操作(比如调用 Thread.interrupt 方法) -- 不是守护线程的所有线程都已死亡,要么从对 *run* 方法的调用返回,要么抛出一个在 *run* 方法之外传播的异常 - -每个线程都有名字,多个线程可能具有相同的名字,Thread 有的构造器如果没有指定名字,会自动生成一个名字。 - -# 2 线程的基本概念 - -## 2.1 线程的状态 -源码中一共枚举了六种线程状态 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzQvMTcxZGJmNmM1MTkxOWQ4OA?x-oss-process=image/format,png) -- 线程的状态机 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzQvMTcxZGJmNmM1MWU0ZmY1OA?x-oss-process=image/format,png) -### 2.1.1 状态机说明 -- `NEW` 表示线程创建成功,但还没有运行,在 `new Thread` 后,没有 `start` 前,线程的状态都是 `NEW`; -- 当调用`start()`,进入`RUNNABLE `,当前线程sleep()结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入`RUNNABLE ` -- 当线程运行完成、被打断、被中止,状态都会从 `RUNNABLE` 变成 `TERMINATED` -- 如果线程正好在等待获得 monitor lock 锁,比如在等待进入 synchronized 修饰的代码块或方法时,会从 `RUNNABLE` 转至 `BLOCKED` -- `WAITING` 和 `TIMED_WAITING` 类似,都表示在遇到 *Object#wait*、*Thread#join*、*LockSupport#park* 这些方法时,线程就会等待另一个线程执行完特定的动作之后,才能结束等待,只不过 `TIMED_WAITING` 是带有等待时间的 - -## 2.2 线程的优先级 -优先级代表线程执行的机会的大小,优先级高的可能先执行,低的可能后执行,在 Java 源码中,优先级从低到高分别是 1 到 10,线程默认 new 出来的优先级都是 5,源码如下: -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzQvMTcxZGJmNmM1MzA5OTRjOA?x-oss-process=image/format,png) -分别为最低,普通(默认优先级),最大优先级 - - -## 2.3 守护线程 -创建的线程默认都是非守护线程。 -- 创建守护线程时,需要将 Thread 的 daemon 属性设置成 true -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzQvMTcxZGJmNmM1NDQwYTc1NQ?x-oss-process=image/format,png) -守护线程的优先级很低,当 JVM 退出时,是不关心有无守护线程的,即使还有很多守护线程,JVM 仍然会退出。 -在工作中,我们可能会写一些工具做一些监控的工作,这时我们都是用守护线程去做,这样即使监控抛出异常,也不会影响到业务主线程,所以 JVM 也无需关注监控是否正在运行,该退出就退出,所以对业务不会产生任何影响。 - -# 3 线程的初始化的两种方式 -无返回值的线程主要有两种初始化方式: - -## 3.1 继承 Thread -看下 start 方法的源码: -```java - public synchronized void start() { - /** - * 对于由VM创建/设置的主方法线程或“系统”组线程,不调用此方法。 - * 将来添加到此方法中的任何新功能可能也必须添加到VM中。 - * - * 零状态值对应于状态“NEW”。 - * 因此,如果没有初始化,直接抛异常 - */ - if (threadStatus != 0) - throw new IllegalThreadStateException(); - - /* - * 通知组此线程即将start,以便可以将其添加到组的线程列表中 - * 并且可以减少组的unstarted线程的计数 - */ - group.add(this); - - // started 是个标识符,在处理一系列相关操作时,经常这么设计 - // 操作执行前前标识符是 false,执行完成后变成 true - boolean started = false; - try { - // 创建一个新的线程,执行完成后,新的线程已经在运行了,即 target 的内容已在运行 - start0(); - // 这里执行的还是 main 线程 - started = true; - } finally { - try { - // 若失败,将线程从线程组中移除 - if (!started) { - group.threadStartFailed(this); - } - // Throwable 可以捕捉一些 Exception 捕捉不到的异常,比如子线程抛出的异常 - } catch (Throwable ignore) { - /* - * 什么也不做。 - * 如果start0抛出一个Throwable,那么它将被传递到调用堆栈 - */ - } - } - } - - // 开启新线程使用的是 native 方法 - private native void start0(); -``` -注意上面提到的的`threadStatus`变量 -用于工具的Java线程状态,初始化以指示线程“尚未启动” -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzQvMTcxZGJmNmM3MGE3M2MyYg?x-oss-process=image/format,png) -## 3.2 实现 Runnable 接口 -这是实现 Runnable 的接口,并作为 Thread 构造器的入参,调用时我们使用了两种方式,可以根据实际情况择一而终 -- 使用 start 会开启子线程来执行 run 里面的内容 -- 使用 run 方法执行的还是主线程。 - -我们来看下 *run* 方法的源码: - -- 不会新起线程,target 是 Runnable -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzQvMTcxZGJmNmM3ZmNiN2U3NA?x-oss-process=image/format,png) -源码中的 target 就是在 new Thread 时,赋值的 Runnable。 - -# 4 线程的初始化 -线程初始化的源码有点长,我们只看比较重要的代码 (不重要的被我删掉了),如下: - -```java -// 无参构造器,线程名字自动生成 -public Thread() { - init(null, null, "Thread-" + nextThreadNum(), 0); -} -// g 代表线程组,线程组可以对组内的线程进行批量的操作,比如批量的打断 interrupt -// target 是我们要运行的对象 -// name 我们可以自己传,如果不传默认是 "Thread-" + nextThreadNum(),nextThreadNum 方法返回的是自增的数字 -// stackSize 可以设置堆栈的大小 -private void init(ThreadGroup g, Runnable target, String name, - long stackSize, AccessControlContext acc) { - if (name == null) { - throw new NullPointerException("name cannot be null"); - } - - this.name = name.toCharArray(); - // 当前线程作为父线程 - Thread parent = currentThread(); - this.group = g; - // 子线程会继承父线程的守护属性 - this.daemon = parent.isDaemon(); - // 子线程继承父线程的优先级属性 - this.priority = parent.getPriority(); - // classLoader - if (security == null || isCCLOverridden(parent.getClass())) - this.contextClassLoader = parent.getContextClassLoader(); - else - this.contextClassLoader = parent.contextClassLoader; - this.inheritedAccessControlContext = - acc != null ? acc : AccessController.getContext(); - this.target = target; - setPriority(priority); - // 当父线程的 inheritableThreadLocals 的属性值不为空时 - // 会把 inheritableThreadLocals 里面的值全部传递给子线程 - if (parent.inheritableThreadLocals != null) - this.inheritableThreadLocals = - ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); - this.stackSize = stackSize; - /* Set thread ID */ - // 线程 id 自增 - tid = nextThreadID(); -} -``` - -从初始化源码中可以看到,很多属性,子线程都是直接继承父线程的,包括优先性、守护线程、inheritableThreadLocals 里面的值等等。 - -# 5 线程其他操作 -## 5.1 join -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzQvMTcxZGJmNmM5OTFkZmVlMQ?x-oss-process=image/format,png) -当前线程等待另一个线程执行死亡之后,才能继续操作。 -```java - public final synchronized void join(long millis) - throws InterruptedException { - long base = System.currentTimeMillis(); - long now = 0; - - if (millis < 0) { - throw new IllegalArgumentException("timeout value is negative"); - } - - if (millis == 0) { - while (isAlive()) { - wait(0); - } - } else { - while (isAlive()) { - long delay = millis - now; - if (delay <= 0) { - break; - } - wait(delay); - now = System.currentTimeMillis() - base; - } - } - } -``` -等待最多 millis 毫秒以使该线程消失。 0 超时时间意味着永远等待。 - -此实现使用以 this.isAlive 为条件的 this.wait 调用循环。当线程终止时,将调用this.notifyAll方法。 建议应用程序不要在线程实例上使用 wait,notify 或 notifyAll。 - - -## 5.2 yield -- 是个 native 方法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzQvMTcxZGJmNmM5ZTIzYWUxNA?x-oss-process=image/format,png) - -令当前线程做出让步,放弃当前 CPU,让 CPU 重新选择线程,避免线程长时占用 CPU。 - -在写 while 死循环时,预计短时间内 while 死循环可结束的话,可在其中使用 yield,防止 CPU 一直被占用。 - -> 让步不是绝不执行,即重新竞争时, CPU 可能还重新选中了自己。 - -## 5.3 sleep -根据系统计时器和调度器的精度和准确性,使当前执行的线程休眠(暂时停止执行)指定的毫秒数。但是注意,休眠期间线程并不会失去任何监视器的所有权。 - -### 毫秒的一个入参 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzQvMTcxZGJmNmNhMTIxYTA2MQ?x-oss-process=image/format,png) -native 方法 -### 毫秒和纳秒的两个入参 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzQvMTcxZGJmNmNhNTU2YjNlNw?x-oss-process=image/format,png) -表示当前线程会沉睡多久,沉睡时不会释放锁资源,所以沉睡时,其它线程是无法得到锁的。最终调用的其实还是单参数的 sleep 方法。 - -## 5.4 interrupt -中断这个线程。 - -除非当前线程是中断自身(这是始终允许的),否则将调用此线程的 checkAccess 方法,这可能导致抛 SecurityException。 - -如果这个线程被 Object 类的 wait(), wait(long), or wait(long, int) 方法或者 Thread 类的 join(), join(long), join(long, int), sleep(long), or sleep(long, int) 调用而阻塞,线程进入 `WAITING` 或 `TIMED_WAITING`状态,这时候打断这些线程,就会抛出 *InterruptedException* ,使线程的状态直接到 `TERMINATED`; - -如果这个线程在一个InterruptibleChannel的I/O操作中被阻塞,主动打断当前线程,那么这个通道将被关闭,线程的中断状态将被设置,线程将收到一个ClosedByInterruptException。 - -如果这个线程在 Selector 中被阻塞,那么这个线程的中断状态将被设置,并且它将从选择的操作立即返回,可能带有一个非零值,就像调用了选择器的 wakeup 方法一样。 - -如果前面的条件都不成立,那么这个线程的中断状态将被设置。 - -中断非活动的线程不会有任何影响。 - -```java - public void interrupt() { - if (this != Thread.currentThread()) - checkAccess(); - - synchronized (blockerLock) { - Interruptible b = blocker; - if (b != null) { - interrupt0(); // Just to set the interrupt flag - b.interrupt(this); - return; - } - } - interrupt0(); - } -``` -- 最终调用的其实是该 native 方法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzQvMTcxZGJmNmNhODIwMDQ5ZA?x-oss-process=image/format,png) - -## 5.5 interrupted -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzQvMTcxZGJmODA3OTMyMmYxNQ?x-oss-process=image/format,png) - -测试当前线程是否已被中断。 通过此方法可以清除线程的中断状态。 换句话说,如果要连续两次调用此方法,则第二个调用将返回false(除非在第一个调用清除了其中断状态之后且在第二个调用对其进行检查之前,当前线程再次被中断)。 - -由于此方法返回false,因此将反映线程中断,因为该线程在中断时尚未处于活动状态而被忽略。 - -## notifyAll -![](https://img-blog.csdnimg.cn/20200610152011147.png) -唤醒在等待该对象的监视器上的全部线程。 线程通过调用的一个对象的监视器上等待wait的方法。 -被唤醒的线程将无法继续进行,直到当前线程放弃此对象的锁。 被唤醒的线程将在与可能,积极争相此对象上进行同步的任何其他线程通常的方式竞争; 例如,唤醒的线程享受成为下一个线程锁定这个对象没有可靠的特权或劣势。 -此方法只能由一个线程,它是此对象监视器的所有者被调用。 看到notify了,其中一个线程能够成为监视器所有者的方法的描述方法。 - -## notify -唤醒在此对象监视器上等待的单个线程。 如果任何线程此对象上等待,它们中的一个被选择为被唤醒。 选择是任意的,并在执行的自由裁量权发生。 线程通过调用的一个对象的监视器上等待wait的方法。 -唤醒的线程将无法继续进行,直到当前线程放弃此对象的锁。 被唤醒的线程将在与可能,积极争相此对象上进行同步的任何其他线程通常的方式竞争; 例如,唤醒的线程在享有作为一个线程锁定这个对象没有可靠的特权或劣势。 -此方法只能由一个线程,它是此对象监视器的所有者被调用。 线程成为三种方式之一的对象监视器的所有者: -通过执行对象的同步实例方法。 -通过执行体synchronized的对象上进行同步的语句。 -对于类型的对象Class,通过执行该类的同步静态方法。 -一次只能有一个线程拥有对象的监视器 - - -## wait -导致当前线程等待,直到其他线程调用notify()方法或notifyAll()此对象的方法。 -换句话说,此方法的行为就好像它简单地执行呼叫wait(0) -当前线程必须拥有该对象的监视器。 这款显示器并等待线程释放所有权,直到另一个线程通知等候在这个对象监视器上的通过调用要么醒来的notify方法或notifyAll方法。 该线程将等到重新获得对监视器的所有权后才能继续执行。 -如在一个参数的版本,中断和杂散唤醒是可能的,而且这种方法应该总是在一个循环中使用: - -```java - synchronized (obj) { - while () - obj.wait(); - ... // Perform action appropriate to condition - } -``` - -此方法只能由一个线程,它是此对象监视器的所有者被调用。 看到notify了,其中一个线程能够成为监视器所有者的方法的描述方法 - -# 6 总结 -本文主要介绍了线程的一些常用概念、状态、初始化方式和操作,这些知识是工作及面试中必备的,也是后面理解高级并发编程的基础。 \ 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\256\236\346\210\230(\345\205\255)-class\346\226\207\344\273\266\347\273\223\346\236\204.md" "b/JDK/JVM/JVM\345\256\236\346\210\230(\345\205\255)-class\346\226\207\344\273\266\347\273\223\346\236\204.md" deleted file mode 100644 index e8af44394b..0000000000 --- "a/JDK/JVM/JVM\345\256\236\346\210\230(\345\205\255)-class\346\226\207\344\273\266\347\273\223\346\236\204.md" +++ /dev/null @@ -1,267 +0,0 @@ -# 1 JVM的“平台无关性” - -Java具有平台无关性,即任何操作系统都能运行Java代码。 -之所以能实现这一点,是因为Java运行在虚拟机之上,不同的操作系统都拥有各自的Java虚拟机,因此Java能实现"一次编写,处处运行"。 - -而JVM不仅具有平台无关性,还具有语言无关性: -- 平台无关性是指不同操作系统都有各自的JVM -- 语言无关性是指Java虚拟机能运行除Java以外的代码! - -但JVM对能运行的语言是有严格要求的。首先来了解下Java代码的运行过程: -Java源代码首先需要使用Javac编译器编译成class文件,然后启动JVM执行class文件,从而程序开始运行。 -即JVM只认识class文件,它并不管何种语言生成了class文件,只要class文件符合JVM的规范就能运行。 - -因此目前已经有Scala、JRuby、Jython等语言能够在JVM上运行。它们有各自的语法规则,不过它们的编译器都能将各自的源码编译成符合JVM规范的class文件,从而能够借助JVM运行它们。 -![](https://imgconvert.csdnimg.cn/aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTYwNTI5MTQxNTU4NDg3?x-oss-process=image/format,png) - -Class文件是JVM的输入, Java虚拟机规范中定义了Class文件的结构。Class文件是JVM实现平台无关、技术无关的基础。 - -# 2 纵观Class文件结构 -class文件包含Java程序执行的字节码,数据严格按照格式紧凑排列在class文件中的二进制流,中间无任何分隔符。 -文件开头有一个0xcafebabe(16进制)特殊的一个标志。 -- 下图展示为16进制 - -![](https://img-blog.csdnimg.cn/20190822021614774.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - - - -![](https://img-blog.csdnimg.cn/20190822021315238.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -class文件是一组以8字节为单位的二进制字节流,对于占用空间大于8字节的数据项,按照高位在前的方式分割成多个8字节进行存储。 -它的内容具有严格的规范,文件中没有任何分隔符,全是连续的0/1。 - -class文件中的所有内容被分为两种类型: -- 无符号数 -基本的数据类型,以u1、u2、u4、u8,分别代表1字节、2字节、4字节、8字节的无符号数 -- 表 -class文件中所有数据(即无符号数)要么单独存在,要么由多个无符号数组成二维表.即class文件中的数据要么是单个值,要么是二维表.通常以_info 结尾 - -## 文件格式 - javap工具生成非正式的"虚拟机汇编语言” ,格式如下: - -```java - [ [ ...]][] -``` - -- ``是指令操作码在数组中的下标,该数组以字节形式来存储当前方法的Java虚拟机代码;也可以是相对于方法起始处的字节偏移量 -- ``是指令的助记码 -- `< operand>`是操作数 -- ``是行尾的注释 - - - -## 实践 -- Demo1.java -![](https://img-blog.csdnimg.cn/20190823232245291.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -- Demo1.txt![](https://img-blog.csdnimg.cn/20190823232156855.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190823232641596.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -版本号规则: JDK5,6,7,8 -分别对应49,50,51,52 -## 2.1 魔数(Magic Number) -class文件的头4个字节称为魔数,唯一作用是确定这个文件是否为一个能被JVM接受的Class文件. -作用就相当于文件后缀名,只不过后缀名容易被修改,不安全. -是用16进制表示的"CAFEBABE". -## 2.2 版本信息 -紧接着魔数的4个字节是版本号.它表示本class中使用的是哪个版本的JDK. -在高版本的JVM上能够运行低版本的class文件,但在低版本的JVM上无法运行高版本的class文件. - -## 2.3 常量池 -### 2.3.1 什么是常量池? -紧接着版本号之后的就是常量池. -常量池中存放两种类型的常量: - -- 字面量 (Literal) -接近Java语言的常量概念,如:字符串文本、final常量值等. -- 符号引用 (Symbolic Reference) -属于编译原理方面,包括下面三类常量: - - 类和接口的全限定名 - - 字段的名称和描述符 - - 方法的名称和描述符 - -## 2.3.2 常量池的特点 -- 长度不固定 -常量池的大小不固定,因此常量池开头放置一个u2类型的无符号数,代表当前常量池的容量. -**该值从1开始,若为5表示池中有4项常量,索引值1~5** -- 常量由二维表表示 -开头有个常量池容量计数值,接下来就全是一个个常量了,只不过常量都是由一张张二维表构成,除了记录常量的值以外,还记录当前常量的相关信息 -- class文件的资源仓库 -- 与本class中其它部分关联最多的数据类型 -- 占用Class文件空间最大的部分之一 ,也是第一个出现的表类型项目 - -### 2.3.3 常量池中常量的类型 -根据常量的数据类型不同,被细分为14种常量类型,都有各自的二维表示结构 -每种常量类型的头1个字节都是tag,表示当前常量属于14种类型中的哪一个. -![这里写图片描述](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9waWMxLnpoaW1nLmNvbS92Mi0xY2E2NmU3NmFhMWEyMjYzNGUzYzQ2YjVmZDQ2M2FkY19yLnBuZw?x-oss-process=image/format,png) -以CONSTANT_Class_info常量为例,它的二维表示结构如下: -**CONSTANT_Class_info表** - -|类型|名称 |数量| -|--|--|--| -| u1 |tag |1| -| u2 |name_index |1| - -- tag 表示当前常量的类型(当前常量为CONSTANT_Class_info,因此tag的值应为7,表一个类或接口的全限定名); -- name_index 表示这个类或接口全限定名的位置.它的值表示指向常量池的第几个常量.它会指向一个**CONSTANT_Utf8_info**类型的常量 - -|类型|名称 |数量| -|--|--|--| -| u1 |tag |1| -| u2 |length |1| -| u1 |bytes |length| - -**CONSTANT_Utf8_info**表字符串常量 -- tag 表当前常量的类型,这里是1 -- length 表该字符串的长度 -- bytes为这个字符串的内容(采用缩略的UTF8编码) - -**Java中定义的类、变量名字必须小于64K** -类、接口、变量等名字都属于符号引用,它们都存储在常量池中 -而不管哪种符号引用,它们的名字都由**CONSTANT_Utf8_info**类型的常量表示,这种类型的常量使用u2存储字符串的长度 -由于2字节最多能表示65535个数,因此这些名字的最大长度最多只能是64K - -**UTF-8编码 VS 缩略UTF-8编码** -前者每个字符使用3个字节表示,而后者把128个ASCII码用1字节表示,某些字符用2字节表示,某些字符用3字节表示。 - -- Demo1.txt中的常量池部分 -![](https://img-blog.csdnimg.cn/20190823232901622.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -- 类信息包含的静态常量,编译之后就能确认 -![](https://img-blog.csdnimg.cn/20190823233205779.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -## JVM 指令 -| | | -|--|--| -invokeinterface |用以调用接口方法,在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。(Invoke interface method) -invokevirtual |指令用于调用对象的实例方法,根据对象的实际类型进行分派(Invoke instance method; dispatch based on class) -invokestatic |用以调用类方法(Invoke a class (static) method ) -invokespecial |指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。(Invoke instance method; special handling for superclass, private, and instance initialization method invocations ) - -invokedynamic JDK1.7新加入的一个虚拟机指令,相比于之前的四条指令,他们的分派逻辑都是固化在JVM内部,而invokedynamic则用于处理新的方法分派:它允许应用级别的代码来确定执行哪一个方法调用,只有在调用要执行的时候,才会进行这种判断,从而达到动态语言的支持。(Invoke dynamic method) - - -## 2.4 访问控制 -在常量池结束之后是2字节的访问控制 -表示这个class文件是类/接口、是否被public/abstract/final修饰等. - -由于这些标志都由是/否表示,因此可以用0/1表示. -访问标志为2字节,可以表示16位标志,但JVM目前只定义了8种,未定义的直接写0. -|标志名称 | 标志值 |含义 -|--|--|--| -| ACC_INTERFACE | |是一个接口,而不是一个类 -ACC_MODULE | | 声明的模块; 可能无法从其模块外部访问。 仅当ClassFile具有Module属性时才可以设置。 -ACC_STATIC | 0x0008 |声明为静态 -![](https://img-blog.csdnimg.cn/20190824003331383.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -- Demo1.txt中的构造方法 -![](https://img-blog.csdnimg.cn/20190824001922551.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -Demo1这个示例中,我们并没有写构造函数。 -由此可见,`没有定义构造函数时,会有隐式的无参构造函数` - -## 2.5 类索引、父类索引、接口索引集合 -表示当前class文件所表示类的名字、父类名字、接口们的名字. -它们按照顺序依次排列,类索引和父类索引各自使用一个u2类型的无符号常量,这个常量指向CONSTANT_Class_info类型的常量,该常量的bytes字段记录了本类、父类的全限定名. -由于一个类的接口可能有好多个,因此需要用一个集合来表示接口索引,它在类索引和父类索引之后.这个集合头两个字节表示接口索引集合的长度,接下来就是接口的名字索引. -## 2.6 字段表的集合 -### 2.6.1 什么是字段表集合? -用于存储本类所涉及到的成员变量,包括实例变量和类变量,但不包括方法中的局部变量. -每一个字段表只表示一个成员变量,本类中所有的成员变量构成了字段表集合. -### 2.6.2 字段表结构的定义 -![这里写图片描述](https://imgconvert.csdnimg.cn/aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTcwNTEwMTc1NjM4NDM2?x-oss-process=image/format,png) - -- access_flags -字段的访问标志。在Java中,每个成员变量都有一系列的修饰符,和上述class文件的访问标志的作用一样,只不过成员变量的访问标志与类的访问标志稍有区别。 -- name_index -本字段名字的索引。指向一个CONSTANT_Class_info类型的常量,这里面存储了本字段的名字等信息。 -- descriptor_index -描述符。用于描述本字段在Java中的数据类型等信息(下面详细介绍) -- attributes_count -属性表集合的长度。 -- attributes -属性表集合。到descriptor_index为止是字段表的固定信息,光有上述信息可能无法完整地描述一个字段,因此用属性表集合来存放额外的信息,比如一个字段的值。(下面会详细介绍) -### 2.6.3 什么是描述符? -成员变量(包括静态成员变量和实例变量) 和 方法都有各自的描述符。 -对于字段而言,描述符用于描述字段的数据类型; -对于方法而言,描述符用于描述字段的数据类型、参数列表、返回值。 - -在描述符中,基本数据类型用大写字母表示,对象类型用“L对象类型的全限定名”表示,数组用“[数组类型的全限定名”表示。 -描述方法时,将参数根据上述规则放在()中,()右侧按照上述方法放置返回值。而且,参数之间无需任何符号。 -### 2.6.4 字段表集合的注意点 - -- 一个class文件的字段表集合中不能出现从父类/接口继承而来字段; -- 一个class文件的字段表集合中可能会出现程序猿没有定义的字段 -如编译器会自动地在内部类的class文件的字段表集合中添加外部类对象的成员变量,供内部类访问外部类。 -- Java中只要两个字段名字相同就无法通过编译。但在JVM规范中,允许两个字段的名字相同但描述符不同的情况,并且认为它们是两个不同的字段。 - - -- Demo1.txt中的程序入口main方法 -![](https://img-blog.csdnimg.cn/20190824002128156.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/2019082400225092.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -## 2.7 方法表的集合 -在class文件中,所有的方法以二维表的形式存储,每张表来表示一个函数,一个类中的所有方法构成方法表的集合。 -方法表的结构和字段表的结构一致,只不过访问标志和属性表集合的可选项有所不同。 -![这里写图片描述](https://imgconvert.csdnimg.cn/aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTYwNjEwMTEzNzM0NTU5?x-oss-process=image/format,png) -方法表的属性表集合中有一张Code属性表,用于存储当前方法经编译器编译过后的字节码指令。 - -### **方法表集合的注意点** - -- 如果本class没有重写父类的方法,那么本class文件的方法表集合中是不会出现父类/父接口的方法表; -- 本class的方法表集合可能出现程序猿没有定义的方法 -编译器在编译时会在class文件的方法表集合中加入类构造器和实例构造器。 -- 重载一个方法需要有相同的简单名称和不同的特征签名。JVM的特征签名和Java的特征签名有所不同: - - Java特征签名:方法参数在常量池中的字段符号引用的集合 - - JVM特征签名:方法参数+返回值 - -## 2.8 属性表的集合 - -![](https://img-blog.csdnimg.cn/20190824004202215.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- 1![](https://img-blog.csdnimg.cn/20190824004232751.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -- 2 -![](https://img-blog.csdnimg.cn/2019082400425144.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -# 程序完整运行分析 -![](https://img-blog.csdnimg.cn/20190824004554877.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190824004639785.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -![](https://img-blog.csdnimg.cn/20190824004654676.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -![](https://img-blog.csdnimg.cn/20190824004709591.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -![](https://img-blog.csdnimg.cn/20190824004835459.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190824004727517.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) - -![](https://img-blog.csdnimg.cn/20190824004849121.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190824005033311.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -![](https://img-blog.csdnimg.cn/2019082400582822.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -![](https://img-blog.csdnimg.cn/20190824005917921.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -![](https://img-blog.csdnimg.cn/20190824005953346.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -![](https://img-blog.csdnimg.cn/2019082401001818.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - - - -![](https://img-blog.csdnimg.cn/2019082401003190.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - - - - - - - - - -![](https://img-blog.csdnimg.cn/20190824010045825.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20190824010233509.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -# 总结 -我们将JVM运行的核心逻辑进行了详细剖析。 - -> JVM运行原理中更底层实现,针对不同的操作系统或者处理器,会有不同的实现。 -这也是JAVA能够实现“`一处编写,处处运行`”的原因。 -开发人员理解到这个层次,就足够掌握高深的多线程 - -参考 -- 《码出高效》 \ No newline at end of file diff --git "a/JDK/JVM/JVM\345\256\236\346\210\230-\345\236\203\345\234\276\346\224\266\351\233\206\345\231\250.md" "b/JDK/JVM/JVM\345\256\236\346\210\230-\345\236\203\345\234\276\346\224\266\351\233\206\345\231\250.md" deleted file mode 100644 index 49d2bd534a..0000000000 --- "a/JDK/JVM/JVM\345\256\236\346\210\230-\345\236\203\345\234\276\346\224\266\351\233\206\345\231\250.md" +++ /dev/null @@ -1,275 +0,0 @@ -![](https://img-blog.csdnimg.cn/20200324202121830.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -HotSpot虚拟机提供了多种垃圾收集器,每种收集器都有各自的特点,没有最好的垃圾收集器,只有最适合的垃圾收集器.我们可以根据自己实际的应用需求选择最适合的垃圾收集器. - -使用分代垃圾收集器,基于以下观察事实(弱分代假设) -- 大多数分配对象的存活时间短 -- 存活时间久的对象很少引用存活时间短的对象 - -由此, HotSpot VM 将堆分为两个物理区空间,这就是分代(永久代只存储元数据, eg. 类的数据结构,保留字符串( Interned String)) - - -根据新生代和老年代各自的特点,我们应该分别为它们选择不同的收集器,以提升垃圾回收效率. -![](https://img-blog.csdnimg.cn/img_convert/58f4a15c353193eb5d92efd41ade7272.png) - -# 1 Serial -主要应用于Y-GC的垃圾回收器,采用串行单线程方式完成GC任务,其中“Stop The World"简称STW,即垃圾回收的某个阶段会暂停整个应用程序的执行 -F-GC的时间相对较长,频繁FGC会严重影响应用程序的性能 -![Serial 回收流程](https://img-blog.csdnimg.cn/img_convert/ebed677b68f68e6c61a54c214c29c984.png) -`单线程 Stop-The-World 式`,STW:工作线程全部停止。 -![](https://img-blog.csdnimg.cn/img_convert/96b8ea78d1075c693728013a76c5512e.png) -![](https://img-blog.csdnimg.cn/20200416204647729.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -## 单线程 -只会使用一个CPU或一条GC线程进行垃圾回收,并且在垃圾回收过程中暂停其他所有的工作线程,从而用户的请求或图形化界面会出现卡顿. - -## 适合Client模式 -一般客户端应用所需内存较小,不会创建太多的对象,而且堆内存不大,因此垃圾回收时间比较短,即使在这段时间停止一切用户线程,也不会感到明显停顿. - -## 简单高效 -由于Serial收集器只有一条GC线程,避免了线程切换的开销. - -## 采用"复制"算法 -# 2 ParNew垃圾收集器 -- ParNew是Serial的多线程版本。 -![](https://img-blog.csdnimg.cn/img_convert/d9765f452cebd7604ab2bf2f78486906.png) - -## 2.1 多线程并行执行 -ParNew由多条GC线程并行地进行垃圾清理.但清理过程仍然需要暂停一切其他用户线程.但由于有多条GC线程同时清理,清理速度比Serial有一定的提升. - -## 2.2 适合多CPU的服务器环境 -由于使用了多线程,因此适合CPU较多的服务器环境. - -- 与Serial性能对比 -ParNew和Serial唯一区别就是使用了多线程进行垃圾回收,在多CPU的环境下性能比Serial会有一定程度的提升;但线程切换需要额外的开销,因此在单CPU环境中表现不如Serial,双CPU环境也不一定就比Serial高效.默认开启的收集线程数与CPU数量相同. - -## 2.3 采用"复制"算法 - -## 2.4 追求"降低停顿时间" -和Serial相比,ParNew使用多线程的目的就是缩短垃圾收集时间,从而减少用户线程被停顿的时间. -# 3 Parallel Scavenge垃圾收集器 -Parallel Scavenge和ParNew一样都是并行的多线程、新生代收集器,都使用"复制"算法进行垃圾回收.但它们有个巨大不同点: - -- ParNew收集器追求降低GC时用户线程的停顿时间,适合交互式应用,良好的反应速度提升用户体验. -- Parallel Scavenge追求CPU吞吐量,能够在较短的时间内完成指定任务,因此适合不需要太多交互的后台运算. - -> 吞吐量:指用户线程运行时间占CPU总时间的比例。CPU总时间包括 : 用户线程运行时间 和 GC线程运行的时间. -> 因此,吞吐量越高表示用户线程运行时间越长,从而用户线程能够被快速处理完。 - -- 降低停顿时间的两种方式 -1.在多CPU环境中使用多条GC线程,从而垃圾回收的时间减少,从而用户线程停顿的时间也减少; -2.实现GC线程与用户线程并发执行。所谓并发,就是用户线程与GC线程交替执行,从而每次停顿的时间会减少,用户感受到的停顿感降低,但线程之间不断切换意味着需要额外的开销,从而垃圾回收和用户线程的总时间将会延长。 - -- Parallel Scavenge提供的参数 - - -XX:GCTimeRadio -直接设置吞吐量大小,GC时间占总时间比率.相当于是吞吐量的倒数. - - - -XX:MaxGCPauseMillis -设置最大GC停顿时间. -Parallel Scavenge会根据这个值的大小确定新生代的大小.如果这个值越小,新生代就会越小,从而收集器就能以较短的时间进行一次回收;但新生代变小后,回收的频率就会提高,吞吐量也降下来了,因此要合理控制这个值. - - -XX:+UseAdaptiveSizePolicy -通过命令就能开启GC **自适应的调节策略(区别于ParNew)**.我们只要设置最大堆(-Xmx)和MaxGCPauseMillis或GCTimeRadio,收集器会自动调整新生代的大小、Eden和Survior的比例、对象进入老年代的年龄,以最大程度上接近我们设置的MaxGCPauseMillis或GCTimeRadio. - -> Parallel Scavenge不能与CMS一起使用。 - -# **以下都是老年代垃圾收集器** -## 1 Serial Old垃圾收集器 -Serial Old收集器是Serial的老年代版本,它们都是单线程收集器,也就是垃圾收集时只启动一条GC线程,因此都适合客户端应用. - -它们唯一的区别就是Serial Old工作在老年代,使用"标记-整理"算法;而Serial工作在新生代,使用"复制"算法. -![](https://img-blog.csdnimg.cn/img_convert/d00b542c1fb7546f3fc7fe05dc99db63.png) - - -## 2 Parallel Old垃圾收集器 - -Parallel Old收集器是Parallel Scavenge的老年代版本,一般它们搭配使用,追求CPU吞吐量. -它们在垃圾收集时都是由多条GC线程并行执行,并暂停一切用户线程,使用"标记-整理"算法.因此,由于在GC过程中没有使垃圾收集和用户线程并行执行,因此它们是追求吞吐量的垃圾收集器. -![](https://img-blog.csdnimg.cn/img_convert/a3a8a3c96812df81cacfbbccb04d5d48.png) -![](https://img-blog.csdnimg.cn/20200416205038206.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -## 3 CMS垃圾收集器(Concurrent Mark Sweep): 低延迟为先! -回收停顿时间比较短,对许多应用来说,快速响应比端到端的吞吐量更为重要。管理新生代的方法与 parallel 和 serial 相同。在老年代则尽可能并发执行,每个 GC 周期只有2次短的停顿。 -一种追求最短停顿时间的收集器,它在垃圾收集时使得用户线程和GC线程并发执行,因此在GC过程中用户也不会感受到明显卡顿.但用户线程和GC线程之间不停地切换会有额外的开销,因此垃圾回收总时间就会被延长. - -**垃圾回收过程** -前两步需要"Stop The World" - -- 初始标记 -停止一切用户线程,仅使用一条初始标记线程对所有与GC Roots直接相关联的对象进行标记,速度很快,因为没啥根对象. -- 并发标记 -使用多条并发标记线程并行执行,并与用户线程并发执行.此过程进行可达性分析,标记出所有废弃的对象,速度很慢. 就像你麻麻在你屋子里收拾垃圾,并不影响你在屋里继续浪.这里也是新一代的收集器努力优化的地方 -- 重新标记 -显然,你麻麻再怎么努力收垃圾,你的屋子可能还是一堆被你新生的垃圾!漏标了很多垃圾!所以此时必须 STW,停止一切用户线程! -使用多条重新标记线程并行执行,将刚才并发标记过程中新出现的废弃对象标记出来.这个过程的运行时间介于初始标记和并发标记之间. -- 并发清除 -只使用一条并发清除线程,和用户线程们并发执行,清除刚才标记的对象.这个过程非常耗时. - -![](https://img-blog.csdnimg.cn/img_convert/3748f7fddc911b57cd0f4501035b9e80.png) - -- 线程角度![](https://img-blog.csdnimg.cn/20200324202758394.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -**CMS的缺点** - -- 吞吐量低 -由于CMS在垃圾收集过程使用用户线程和GC线程并行执行,从而线程切换会有额外开销,因此CPU吞吐量就不如在GC过程中停止一切用户线程的方式来的高. -- 无法处理浮动垃圾,导致频繁Full GC -由于垃圾清除过程中,用户线程和GC线程并发执行,也就是用户线程仍在执行,那么在执行过程中会产生垃圾,这些垃圾称为"浮动垃圾". -如果CMS在垃圾清理过程中,用户线程需要在老年代中分配内存时发现空间不足,就需再次发起Full GC,而此时CMS正在进行清除工作,因此此时只能由Serial Old临时对老年代进行一次Full GC. -- 使用"标记-清除"算法产生碎片空间 -由于CMS采用的是“标记-清除算法",因此戸生大量的空间碎片,不利于空间利用率。为了解决这个问题,CMS可以通过配置 -``` --XX:+UseCMSCompactAtFullCollection -``` -参数,强制JVM在FGC完成后対老年代迸行圧縮,执行一次空间碎片整理,但是空间碎片整理阶段也会引发STW。为了减少STW次数,CMS还可以通过配置 -``` --XX:+CMSFullGCsBeforeCompaction=n -``` -参数,在执行了n次FGC后, JVM再在老年代执行空间碎片整理。 - -在并发收集失败的情况下,Java虚拟机会使用其他两个压缩型垃圾回收器进行一次垃圾回收。由于G1的出现,CMS在Java 9中已被废弃。 - -# 三色标记算法 - 漏标问题引入 -没有遍历到的 - 白色 -自己标了,孩子也标了 - 黑色 -自己标了,孩子还没标 - 灰色 - -- 第一种情况 ,已经标好了 ab,还没 d,如下,此时B=>D 消失,突然A=D了,因为 A已黑了,不会再 看他的孩子,于是 D 被漏标了! -![](https://img-blog.csdnimg.cn/20200324203803767.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -![](https://img-blog.csdnimg.cn/20200324203902985.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70)![](https://img-blog.csdnimg.cn/20200324204437162.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -## 漏标的解决方案 -把 A 再标成灰色,看起来解决了?其实依然漏标! - -CMS方案: Incremental Update的非常隐蔽的问题: -并发标记,依旧产生漏标! - -![](https://img-blog.csdnimg.cn/20200324204937207.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/2020032420523942.png) - -于是产生了 G1! - - -G1(Garbage First)是一个横跨新生代和老年代的垃圾回收器。实际上,它已经打乱了前面所说的堆结构,直接将堆分成极其多个区域。每个区域都可以充当Eden区、Survivor区或者老年代中的一个。它采用的是标记-压缩算法,而且和CMS一样都能够在应用程序运行过程中并发地进行垃圾回收。 - -G1能够针对每个细分的区域来进行垃圾回收。在选择进行垃圾回收的区域时,它会优先回收死亡对象较多的区域。这也是G1名字的由来。 - -# G1收集器(Garbage-First) -Hotspot 在JDK7中推出了新一代 G1 ( Garbage-First Garbage Collector )垃圾回收,通过 -``` --XX:+UseG1GC -``` -参数启用 -和CMS相比,Gl具备压缩功能,能避免碎片向題,G1的暂停时间更加可控。性能总体还是非常不错的,G1是当今最前沿的垃圾收集器成果之一。 - -![](https://img-blog.csdnimg.cn/20200416212655781.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -当今最前沿的垃圾收集器成果之一. -## G1的特点 -- 追求停顿时间 -- 多线程GC -- 面向服务端应用 -- 整体来看基于标记-整理和局部来看基于复制算法合并 -不会产生内存空间碎片. -- 可对整个堆进行垃圾回收 -- 可预测的停顿时间 -## G1的内存模型 -没有分代概念,而是将Java堆划分为一块块独立的大小相等的Region.当要进行垃圾收集时,首先估计每个Region中的垃圾数量,每次都从垃圾回收价值最大的Region开始回收,因此可以获得最大的回收效率. - -![](https://img-blog.csdnimg.cn/img_convert/1b6aecdf894dbef83eead24c57a53400.png) - -G1将Java堆空间分割成了若干相同大小的区域,即region -包括 -- Eden -- Survivor -- Old -- Humongous - -其中,`Humongous `是特殊的Old类型,专门放置大型对象. -这样的划分方式意味着不需要一个连续的内存空间管理对象.G1将空间分为多个区域,**优先回收垃圾最多**的区域. -G1采用的是**Mark-Copy** ,有非常好的空间整合能力,不会产生大量的空间碎片 -G1的一大优势在于可预测的停顿时间,能够尽可能快地在指定时间内完成垃圾回收任务 -在JDK11中,已经将G1设为默认垃圾回收器,通过jstat命令可以查看垃圾回收情况,在YGC时S0/S1并不会交换. -## Remembered Set -一个对象和它内部所引用的对象可能不在同一个Region中,那么当垃圾回收时,是否需要扫描整个堆内存才能完整地进行一次可达性分析? -当然不是,每个Region都有一个Remembered Set,用于记录本区域中所有对象引用的对象所在的区域,从而在进行可达性分析时,只要在GC Roots中再加上Remembered Set即可防止对所有堆内存的遍历. -## G1垃圾收集过程 - -- 初始标记 -标记与GC Roots直接关联的对象,停止所有用户线程,只启动一条初始标记线程,这个过程很快. -- 并发标记 -进行全面的可达性分析,开启一条并发标记线程与用户线程并行执行.这个过程比较长. -- 最终标记 -标记出并发标记过程中用户线程新产生的垃圾.停止所有用户线程,并使用多条最终标记线程并行执行. -- 筛选回收 -回收废弃的对象.此时也需要停止一切用户线程,并使用多条筛选回收线程并行执行. - -![](https://img-blog.csdnimg.cn/img_convert/2b407cb7a63bc9c6ce67d5863d68444b.png) - -S0/S1的功能由G1中的Survivor region来承载,通过GC日志可以观察到完整的垃圾回收过程如下,其中就有Survivor regions的区域从0个到1个 -![](https://img-blog.csdnimg.cn/img_convert/ce92bb3baddb8af14e5cfa1f4737093d.png) - 红色标识的为G1中的四种region,都处于Heap中. -G1执行时使用4个worker并发执行,在初始标记时,还是会触发STW,如第一步所示的Pause -## 回收算法 -依旧前面例子: -![](https://img-blog.csdnimg.cn/20200324205602561.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70) -![](https://img-blog.csdnimg.cn/20200324205533638.png) -因此,还是能追踪到 D,如果不维护 rset,需要扫描其他所有对象!因此只需要扫描该 region 即可~ - -针对新生代的垃圾回收器共有三个:Serial,Parallel Scavenge和Parallel New。这三个采用的都是标记-复制算法。其中,Serial是一个单线程的,Parallel New可以看成Serial的多线程版本。Parallel Scavenge和Parallel New类似,但更加注重吞吐率。此外,Parallel Scavenge不能与CMS一起使用。 - -针对老年代的垃圾回收器也有三个:刚刚提到的Serial Old和Parallel Old,以及CMS。Serial Old和Parallel Old都是标记-压缩算法。同样,前者是单线程的,而后者可以看成前者的多线程版本。 - -CMS采用的是标记-清除算法,并且是并发的。除了少数几个操作需要Stop-the-world之外,它可以在应用程序运行过程中进行垃圾回收。在并发收集失败的情况下,Java虚拟机会使用其他两个压缩型垃圾回收器进行一次垃圾回收。由于G1的出现,CMS在Java 9中已被废弃[3]。 - -G1(Garbage First)是一个横跨新生代和老年代的垃圾回收器。实际上,它已经打乱了前面所说的堆结构,直接将堆分成极其多个区域。每个区域都可以充当Eden区、Survivor区或者老年代中的一个。它采用的是标记-压缩算法,而且和CMS一样都能够在应用程序运行过程中并发地进行垃圾回收。 - -G1能够针对每个细分的区域来进行垃圾回收。在选择进行垃圾回收的区域时,它会优先回收死亡对象较多的区域。这也是G1名字的由来。 - - -100g内存时,到头性能. -且G1 浪费空间,fullgc 特别慢!很多阶段都是 STW 的,所以有了 ZGC! -# ZGC -听说你是 zerpo paused GC? -Java 11引入了ZGC,宣称暂停时间不超过10ms,支持 4TB,JDK13 到了 16TB! - -和内存无关,TB 级也只停顿 1-10ms -![](https://img-blog.csdnimg.cn/20200324211051586.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- UMA -![](https://img-blog.csdnimg.cn/20200324211303399.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -- NUMA -知道NUMA存在并且能利用,哪个CPU要分配对象,优先分配离得近的内存 - -- 目前不分代(将来可能分冷热对象) -ZGC 学习 Asul 的商用C4收集器 - -## 颜色指针 -![](https://img-blog.csdnimg.cn/20200324212327248.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -原来的GC信息记录在哪里呢?对象头部 -ZGC记录在指针,跟对象无关,因此可以immediate memory reuse -低42位指向对象,2^42=4T JDK13 2^44=16T, 目前最大就 16T,还能再大吗???? -后面四位伐表对象不同状态m0 m1 remapped finalizable -18为unused - -### 灵魂问题 -内存中有个地址 -地址中装了01001000 , mov 72,到底是一个立即数,还是一条指令? -CPU->内存,通过总线连接,-> 数据总线地址总线控制总线,所以看是从啥总线来的即可 -主板地址总线最宽 48bit 48-4 颜色位,就只剩 44 位了,所以最大 16T. - -## ZGC 阶段 -1.pause mark start -2.concurrent mark -3.relocate -![](https://img-blog.csdnimg.cn/20200324214821947.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -4.remap - -对象的位置改变了,将其引用也改变过去 - 写屏障(与 JMM 的屏障不同,勿等同!) - -而 ZGC 使用的读屏障! -# GC的优势在哪里 -- 流行于现代的各大语言和平台 -- 效率和稳定性 - - 程序员不需要负责释放及销毁对象 - - 消除了不稳定性,延迟以及维护等几乎全部(普遍的)的可能 -- 保证了互操作性 - - 不需要与APIs之间交互的内存管理契约 - - 与不协调的库,框架,应用程序流畅地交互操作 \ No newline at end of file diff --git "a/JDK/JVM/JVM\345\256\236\346\210\230-\347\261\273\345\212\240\350\275\275\343\200\201\351\252\214\350\257\201\343\200\201\345\207\206\345\244\207\343\200\201\350\247\243\346\236\220\343\200\201\345\210\235\345\247\213\345\214\226\343\200\201\345\215\270\350\275\275\350\277\207\347\250\213.md" "b/JDK/JVM/JVM\345\256\236\346\210\230-\347\261\273\345\212\240\350\275\275\343\200\201\351\252\214\350\257\201\343\200\201\345\207\206\345\244\207\343\200\201\350\247\243\346\236\220\343\200\201\345\210\235\345\247\213\345\214\226\343\200\201\345\215\270\350\275\275\350\277\207\347\250\213.md" deleted file mode 100644 index 4ba99cfd6f..0000000000 --- "a/JDK/JVM/JVM\345\256\236\346\210\230-\347\261\273\345\212\240\350\275\275\343\200\201\351\252\214\350\257\201\343\200\201\345\207\206\345\244\207\343\200\201\350\247\243\346\236\220\343\200\201\345\210\235\345\247\213\345\214\226\343\200\201\345\215\270\350\275\275\350\277\207\347\250\213.md" +++ /dev/null @@ -1,212 +0,0 @@ -# 0 使用类的准备工作 - -任何程序都需要加载到内存才能与CPU进行交流,同理, 字节码.class文件同样需要加载到内存中,才可以实例化类。 -`ClassLoader`的使命就是提前加载.class 类文件到内存中,在加载类时,使用的是Parents Delegation Model(溯源委派加载模型)。 - -Java的类加载器是一个运行时核心基础设施模块,主要是在启动之初进行类的加载、链接、初始化: -- Java 类加载过程 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWJmZGRhYjUzMjk5ZTIyZTQucG5n) -## Load-加载 -由类加载器执行。 - -读取类文件(通常在 classpath 所指定的路径中查找,但classpath非必须),查找字节码,从而产生二进制流,并转为特定数据结构,初步校验cafe babe魔法数、常量池、文件长度、是否有父类等,然后创建对应类的java.lang.Class实例。 - -## Link-链接 -将已读入内存的类的二进制数据合并到 JVM 运行时环境。 -包括验证、准备、解析三步: -- 验证 -确保被加载类的正确性。验证类中的字节码,是更详细的校验,比如final是否合规、类型是否正确、静态变量是否合理 -- 准备 -为类的static字段分配内存,并设定初始默认值,解析类和方法确保类与类之间的相互引用正确性,完成内存结构布局 -- 解析 -如果需要的话,将解析这个类创建的对其他类的所有引用,将常量池的符号引用转换成直接引用 。 -## Init-初始化 -执行类构造器 方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马上解析另外一个类,在虚拟机栈中执行完毕后通过返回值进行赋值 - - 类加载是一个将.class字节码文件实例化成Class对象并进行相关初始化的过程。 -在这个过程中,JVM会初始化继承树上还没有被初始化过的所有父类,并且会执行这个链路上所有未执行过的静态代码块、静态变量赋值语句等。 -某些类在使用时,也可以按需由类加载器进行加载。 - -全小写的class是关键字,用来定义类 -而首字母大写的Class,它是所有class的类 -这句话理解起来有难度,类已经是现实世界中某种事物的抽象,为什么这个抽象还是另外一个类Class的对象? -示例代码如下: -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTM0MTk3NGNiNTllZTY4MmEucG5n) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTdlYmNlYjI0NDVmYzA3MjUucG5n) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTViZDgyODE1ODU4MDI0YWUucG5n) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWE4MmM0NzUyMThhYzQ5NzAucG5n) - ● 第1处说明: -Class类下的`newInstance()`在JDK9中已经置为过时,使用`getDeclaredConstructor().newInstance()`的方式 -着重说明一下new与newInstance的区别 -- new是强类型校验,可以调用任何构造方法,在使用new操作的时候,这个类可以没有被加载过 -- 而Class类下的newInstance是弱类型,只能调用无参构造方法 - - 如果没有默认构造方法,就拋出`InstantiationException`异常; - - 如果此构造方法没有权限访问,则拋 `IllegalAccessException`异常 - -Java 通过类加载器把类的实现与类的定义进行解耦,所以是实现面向接口编程、依赖倒置的必然选择。 - -● 第2处说明: -可以使用类似的方式获取其他声明,如注解、方法等 -![类的反射信息](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTdkNTRhODI2OTI5NDU4NzUucG5n) - -● 第3处说明: private 成员在类外是否可以修改? -通过`setccessible(true)`,即可使用Class类的set方法修改其值 -如果没有这一步,则抛出如下异常: -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTQ3YWNmNjU0ZWQyNzZjOTUucG5n) - - -- 参考 -[看完这篇JVM类加载器,再也不怕阿里面试官了!](https://javaedge.blog.csdn.net/article/details/105250625) - -# 1 加载的定位 -> “加载”是“类加载”(Class Loading)过程的第一步。 -## 1.1 加载过程 -JVM主要做如下事情: -- 通过类的全限定名(保证全局唯一)获取该类的二进制字节流(class文件) -在程序运行过程中,当要访问一个类时,若发现这个类尚未被加载,并满足类初始化的条件时,就根据要被初始化的这个类的全限定名找到该类的二进制字节流,开始加载过程。 -把类加载阶段的“通过类的全限定名来获取该类的二进制字节流”这个动作交给虚拟机之外的类加载器来完成的好处在于,可自行实现类加载器来加载其他格式的类,只要是二进制字节流就行,这就大大增强加载器的灵活性。 -- 将这个字节流的静态存储结构转化为方法区的运行时数据结构 -- 在内存中创建一个该类的`java.lang.Class`对象,作为方法区该类的各种数据的访问入口,所以所有类都可以调用 getClass 方法 - -程序在运行中所有对该类的访问都通过这个类对象,也就是这个Class对象是提供给外界访问该类的接口 -## 1.2 加载源 -JVM规范对于加载过程给予了较大的宽松度。一般二进制字节流都从已经编译好的本地class文件中读取,此外还可以从以下地方读取 -- zip包 -Jar、War、Ear等 -- 其它文件生成 -由JSP文件中生成对应的Class类 -- 数据库中 -将二进制字节流存储至数据库中,然后在加载时从数据库中读取.有些中间件会这么做,用来实现代码在集群间分发 -- 网络 -从网络中获取二进制字节流,比如Applet -- 运行时动态计算生成 -动态代理技术,用`PRoxyGenerator.generateProxyClass`为特定接口生成形式为"*$Proxy"的代理类的二进制字节流 - - -## 1.3 类和数组加载过程的区别 -数组也有类型,称为“数组类型”,如: -```java -String[] str = new String[10]; -``` -这个数组的数组类型是`Ljava.lang.String`,而String只是这个数组的元素类型。 -当程序在运行过程中遇到new关键字创建一个数组时,由JVM直接创建数组类,再由类加载器创建数组中的元素类型。 - -而普通类的加载由类加载器创建。既可以使用系统提供的引导类加载器,也可以由用户自定义的类加载器完成(即重写一个类加载器的loadClass()方法) -## 1.4 加载过程的注意点 -- JVM规范并未给出类在方法区中存放的数据结构 -类完成加载后,二进制字节流就以特定的数据结构存储在方法区中,但存储的数据结构是由虚拟机自己定义的,虚拟机规范并没有指定 -- JVM规范并没有指定Class对象存放的位置 -在二进制字节流以特定格式存储在方法区后,JVM会创建一个java.lang.Class类的对象,作为本类的外部访问接口 -既然是对象就应该存放在Java堆中,不过JVM规范并没有给出限制,不同的虚拟机根据自己的需求存放这个对象 -HotSpot将Class对象存放在方法区. -- 加载阶段和链接阶段是交叉的 -类加载的过程中每个步骤的开始顺序都有严格限制,但每个步骤的结束顺序没有限制.也就是说,类加载过程中,必须按照如下顺序开始: ->加载 -> 链接 -> 初始化 - -但结束顺序无所谓,因此由于每个步骤处理时间的长短不一就会导致有些步骤会出现交叉 -# 2 验证 -验证阶段比较耗时,它非常重要但不一定必要(因为对程序运行期没有影响),如果所运行的代码已经被反复使用和验证过,那么可以使用`-Xverify:none`参数关闭,以缩短类加载时间 -## 2.1 验证的目的 -保证二进制字节流中的信息符合虚拟机规范,并没有安全问题 -## 2.2 验证的必要性 -虽然Java语言是一门安全的语言,它能确保程序猿无法访问数组边界以外的内存、避免让一个对象转换成任意类型、避免跳转到不存在的代码行.也就是说,Java语言的安全性是通过编译器来保证的. - -但是我们知道,编译器和虚拟机是两个独立的东西,虚拟机只认二进制字节流,它不会管所获得的二进制字节流是哪来的,当然,如果是编译器给它的,那么就相对安全,但如果是从其它途径获得的,那么无法确保该二进制字节流是安全的。 - -通过上文可知,虚拟机规范中没有限制二进制字节流的来源,在字节码层面上,上述Java代码无法做到的都是可以实现的,至少语义上是可以表达出来的,为了防止字节流中有安全问题,需要验证! -## 2.3 验证的过程 -- 文件格式验证 -验证字节流是否符合Class文件格式的规范,并且能被当前的虚拟机处理. -本验证阶段是基于二进制字节流进行的,只有`通过本阶段验证,才被允许存到方法区` -后面的三个验证阶段都是基于方法区的存储结构进行,不会再直接操作字节流. - -> 通过上文可知,加载开始前,二进制字节流还没进方法区,而加载完成后,二进制字节流已经存入方法区 -> 而在文件格式验证前,二进制字节流尚未进入方法区,文件格式验证通过之后才进入方法区 -> 也就是说,加载开始后,立即启动了文件格式验证,本阶段验证通过后,二进制字节流被转换成特定数据结构存储至方法区中,继而开始下阶段的验证和创建Class对象等操作 -这个过程印证了:加载和验证是交叉进行的 - -- 元数据验证 -对字节码描述的信息进行语义分析,确保符合Java语法规范。 -- 字节码验证 -验证过程的最复杂的阶段。 本阶段对数据流和控制流(主要为方法体)进行语义分析。字节码验证将对类的方法进行校验分析,保证被校验的方法在运行时不会做出危害虚拟机的事,一个类方法体的字节码没有通过字节码验证,那一定有问题,但若一个方法通过了验证,也不能说明它一定安全。 -- 符号引用验证 -发生在JVM将符号引用转化为直接引用的时候,这个转化动作发生在解析阶段,对类自身以外的信息进行匹配校验,确保解析能正常执行. -# 3 准备 -完成两件事情 -- 为已在方法区中的类的静态成员变量分配内存 -- 为静态成员变量设置初始值 -初始值为0、false、null等 -![](https://imgconvert.csdnimg.cn/aHR0cDovL3VwbG9hZC1pbWFnZXMuamlhbnNodS5pby91cGxvYWRfaW1hZ2VzLzQ2ODU5NjgtZjYyYTU2YzAzNGM1MTgyMC5qcGc) -``` -public static final int value = 123; -``` -准备阶段后 a 的值为 0,而不是 123,要在初始化之后才变为 123,但若被final修饰的常量如果有初始值,那么在编译阶段就会将初始值存入constantValue属性中,在准备阶段就将constantValue的值赋给该字段(此处将value赋为123). -# 4 解析 -把常量池中的**符号引用**转换成**直接引用**的过程。包括: -- 符号引用 -以一组无歧义的符号来描述所引用的目标,与虚拟机的实现无关。 -- 直接引用 -直接指向目标的指针、相对偏移量、或是能间接定位到目标的句柄,是和虚拟机实现相关的。 - -主要针对:类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符。 - -# 5 初始化 -真正开始执行类中定义的Java程序代码(或说是字节码),类的初始化就是为类的静态变量赋初始值,初始化阶段就是执行类构造器``的过程。 - -- 如果类还没有加载和连接,就先加载和连接 -- 如果类存在父类,且父类没有初始化,就先初始化父类 -- 如果类中存在初始化语句,就依次执行这些初始化语句 -- 如果是接口 - - 初始化一个类时,并不会先初始化它实现的接口 - - 初始化一个接口时,并不会初始化它的父接口 -只有当程序首次使用接口里面的变量或者是调用接口方法的时候,才会导致接口初始化 -- 调用Classloader类的loadClass方法来装载一个类,并不会初始化这个类,不属于对类的主动使用 - - -clinit()方法由编译器自动产生,收集类中static{}代码块中的类变量赋值语句和类中静态成员变量的赋值语句。 -在准备阶段,类中静态成员变量已经完成了默认初始化,而在初始化阶段,clinit()方法对静态成员变量进行显示初始化。 - -## 类的初始化时机 -Java程序对类的使用方式分为: -- 主动使用 -- 被动使用 - -JVM必须在每个类或接口“首次主动使用”时才初始化它们,被动使用类不会导致类的初始化。主动使用的场景: -- **创建类实例** -- 访问某个类或接口的**静态变量** -如果是 final 常量,而常量在编译阶段就会在常量池,没有引用到定义该常量的类,因此不会触发定义该常量类的初始化 -- 调用类的**静态方法** -- **反射**某个类 -- 初始化某个类的子类,而父类还没有初始化 -- JVM启动的时候运行的主类(等于第三条) -- 定义了 default 方法的接口,当接口实现类初始化时 - - -## 初始化过程的注意点 -- clinit()方法是IDE自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,IDE收集的顺序是由语句在源文件中出现的顺序所决定的. -- 静态代码块只能访问到出现在静态代码块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问. -```java -public class Test { - static { - i=0; - System.out.println(i); //编译失败:"非法向前引用" - } - static int i = 1; -} -``` -- 实例构造器init()需要显式调用父类构造器,而类的clinit()无需调用父类的类构造器,JVM会确保子类的clinit()方法执行前已执行完毕父类的clinit()方法。 -因此在JVM中第一个被执行的clinit()方法的类肯定是java.lang.Object. -- 如果一个类/接口无static代码块,也无 static成员变量的赋值操作,则编译器不会为此类生成clinit()方法 -- 接口也需要通过clinit()方法为接口中定义的static成员变量显示初始化。 -- 接口中不能使用静态代码块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成clinit()方法.不同的是,执行接口的clinit()方法不需要先执行父接口的clinit()方法.只有当父接口中的静态成员变量被使用到时才会执行父接口的clinit()方法. -- 虚拟机会保证在多线程环境中一个类的clinit()方法别正确地加锁,同步.当多条线程同时去初始化一个类时,只会有一个线程去执行该类的clinit()方法,其它线程都被阻塞等待,直到活动线程执行clinit()方法完毕. - -> 其他线程虽会被阻塞,只要有一个clinit()方法执行完,其它线程唤醒后不会再进入clinit()方法。同一个类加载器下,一个类型只会初始化一次。 - -# 6 类的卸载 -当代表一个类的Class对象不再被引用,那么Class对象的生命周期就结束了,对应的在方法区中的数据也会被卸载。 -Jvm自带的类加载器装载的类,是不会卸载的,由用户自定义的类加载器加载的类是可以卸载的。 - -参考 -- 《码到成功》 -- 《深入理解Java虚拟机第三版》 \ No newline at end of file diff --git "a/JDK/JVM/JVM\347\261\273\345\212\240\350\275\275\345\231\250.md" "b/JDK/JVM/JVM\347\261\273\345\212\240\350\275\275\345\231\250.md" deleted file mode 100644 index 706d5f0785..0000000000 --- "a/JDK/JVM/JVM\347\261\273\345\212\240\350\275\275\345\231\250.md" +++ /dev/null @@ -1,135 +0,0 @@ -类加载器是如何定位具体的类文件并读取的呢? - -# 1 类加载器 -在类加载器家族中存在着类似人类社会的权力等级制度: -## 1.1 `Bootstrap` -由C/C++实现,启动类加载器,属最高层,JVM启动时创建,通常由与os相关的本地代码实现,是最根基的类加载器。 -### JDK8 时 -> **需要注意的是**,Bootstrap ClassLoader智慧加载特定名称的类库,比如rt.jar.这意味我们自定义的jar扔到`\jre\lib`也不会被加载. - -负责将`/jre/lib`或`- -Xbootclasspath`参数指定的路径中的,且是虚拟机识别的类库加载到内存中(按照名字识别,比如rt.jar,对于不能识别的文件不予装载),比如: -- Object -- System -- String -- Java运行时的rt.jar等jar包 -- 系统属性sun.boot.class.path指定的目录中特定名称的jar包 - -在JVM启动时,通过**Bootstrap ClassLoader**加载`rt.jar`,并初始化`sun.misc.Launcher`从而创建**Extension ClassLoader**和**Application ClassLoader**的实例。 -查看Bootstrap ClassLoader到底初始化了那些类库: - -```java -URL[] urLs = Launcher.getBootstrapClassPath().getURLs(); - for (URL urL : urLs) { - System.out.println(urL.toExternalForm()); - } -``` -### JDK9 后 -负责加载启动时的基础模块类,比如: -- java.base -- java.management -- java.xml -## 1.2 `Platform ClassLoader` -### JDK8 时`Extension ClassLoader` -只有一个实例,由sun.misc.Launcher$ExtClassLoader实现: -- 负责加载`\lib\ext`或`java.ext.dirs`系统变量指定的路径中的所有类库 -- 加载一些扩展的系统类,比如XML、加密、压缩相关的功能类等 - -### JDK9时替换为平台类加载器 -加载一些平台相关的模块,比如`java.scripting`、`java.compiler*`、 `java.corba*`。 -### 那为何 9 时废除替换了呢? -JDK8 的主要加载 jre lib 的ext,扩展 jar 包时使用,这样操作并不推荐,所以废除。而 JDK9 有了模块化,更无需这种扩展加载器。 -## 1.3 `Application ClassLoader` -只有一个实例,由`sun.misc.Launcher$AppClassLoader`实现。 -### JDK8 时 -负责加载系统环境变量ClassPath或者系统属性java.class.path指定目录下的所有类库。 -如果应用程序中没有定义自己的加载器,则该加载器也就是默认的类加载器。该加载器可以通过java.lang.ClassLoader.getSystemClassLoader获取。 -### JDK9 后 -应用程序类加载器,用于加载应用级别的模块,比如: -- jdk.compiler -- jdk.jartool -- jdk.jshell -![](https://img-blog.csdnimg.cn/2021011914324377.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - -- classpath路径中的所有类库 - -第二、三层类加载器为Java语言实现,用户也可以 -## 1.4 自定义类加载器 -用户自定义的加载器,是`java.lang.ClassLoader`的子类,用户可以定制类的加载方式;只不过自定义类加载器其加载的顺序是在所有系统类加载器的最后。 - -## 1.5 Thread Context ClassLoader -每个线程都有一个类加载器(jdk 1.2后引入),称之为Thread Context ClassLoader,如果线程创建时没有设置,则默认从父线程中继承一个,如果在应用全局内都没有设置,则所有Thread Context ClassLoader为Application ClassLoader.可通过Thread.currentThread().setContextClassLoader(ClassLoader)来设置,通过Thread.currentThread().getContextClassLoader()来获取. - -线程上下文加载器有什么用? -该类加载器容许父类加载器通过子类加载器加载所需要的类库,也就是打破了我们下文所说的双亲委派模型。 -这有什么好处呢? -利用线程上下文加载器,我们能够实现所有的代码热替换,热部署,Android中的热更新原理也是借鉴如此。 - -# 2 验证类加载器 -## 2.1 查看本地类加载器 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWM2YTlkN2YwOTEwZDNiZGMucG5n) -在JDK8环境中,执行结果如下 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWExYjA3N2UzYTRiYzRhZjMucG5n) -AppClassLoader的Parent为Bootstrap,它是通过C/C++实现的,并不存在于JVM体系内,所以输出为 null。 - - -# 类加载器的特点 -- 类加载器并不需要等到某个类"首次主动使用”的时候才加载它,JVM规范允许类加载器在预料到某个类将要被使用的时候就预先加载它。 -- Java程序不能直接引用启动类加载器,直接设置classLoader为null,默认就使用启动类加载器 -- 如果在加载的时候`.class`文件缺失,会在该类首次主动使用时通知LinkageError错误,如果一直没有被使用,就不会报错 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQ2ZGU1NDU2OTNmNGZjYjEucG5n) - - -低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类 -如果低层次的类加载器想加载一个未知类,要非常礼貌地向上逐级询问:“请问,这个类已经加载了吗?” -被询问的高层次类加载器会自问两个问题 -- 我是否已加载过此类 -- 如果没有,是否可以加载此类 - -只有当所有高层次类加载器在两个问题的答案均为“否”时,才可以让当前类加载器加载这个未知类 -左侧绿色箭头向上逐级询问是否已加载此类,直至`Bootstrap ClassLoader`,然后向下逐级尝试是否能够加载此类,如果都加载不了,则通知发起加载请求的当前类加载器,准予加载 -在右侧的三个小标签里,列举了此层类加载器主要加载的代表性类库,事实上不止于此 - -通过如下代码可以查看Bootstrap 所有已加载类库 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTg0MGEwNDUwNWI1Y2I1YTgucG5n) -执行结果 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWNiNTdkZGNmZjBmZDI0ZWMucG5n) - -Bootstrap加载的路径可以追加,不建议修改或删除原有加载路径 -在JVM中增加如下启动参数,则能通过`Class.forName`正常读取到指定类,说明此参数可以增加Bootstrap的类加载路径: -```bash --Xbootclasspath/a:/Users/sss/book/ easyCoding/byJdk11/src -``` -如果想在启动时观察加载了哪个jar包中的哪个类,可以增加 -```bash --XX:+TraceClassLoading -``` -此参数在解决类冲突时非常实用,毕竟不同的JVM环境对于加载类的顺序并非是一致的 -有时想观察特定类的加载上下文,由于加载的类数量众多,调试时很难捕捉到指定类的加载过程,这时可以使用条件断点功能 -比如,想查看HashMap的加载过程,在loadClass处打个断点,并且在condition框内输入如图 -![设置条件断点](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTZhMjk0YjhhNzVkYWM4MTIucG5n) - -### JVM如何确立每个类在JVM的唯一性 -类的全限定名和加载这个类的类加载器的ID - -在学习了类加载器的实现机制后,知道双亲委派模型并非强制模型,用户可以自定义类加载器,在什么情况下需要自定义类加载器呢? -- 隔离加载类 -在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境 -比如,阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包 -- 修改类加载方式 -类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载 -- 扩展加载源 -比如从数据库、网络,甚至是电视机机顶盒进行加载 -- 防止源码泄露 -Java代码容易被编译和篡改,可以进行编译加密。那么类加载器也需要自定义,还原加密的字节码。 - -实现自定义类加载器的步骤 -- 继承ClassLoader -- 重写findClass()方法 -- 调用defineClass()方法 - -一个简单的类加载器实现的示例代码如下 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTdiYjk1NGI1NThlOTAwMzAucG5n) - -由于中间件一般都有自己的依赖jar包,在同一个工程内引用多个框架时,往往被迫进行类的仲裁。按某种规则jar包的版本被统一指定, 导致某些类存在包路径、类名相同的情况,就会引起类冲突,导致应用程序出现异常。 -主流的容器类框架都会自定义类加载器,实现不同中间件之间的类隔离,有效避免了类冲突。 \ No newline at end of file diff --git "a/JDK/JVM/JVM\347\261\273\345\212\240\350\275\275\345\231\250\347\232\204\345\217\214\344\272\262\345\247\224\346\264\276\346\250\241\345\236\213.md" "b/JDK/JVM/JVM\347\261\273\345\212\240\350\275\275\345\231\250\347\232\204\345\217\214\344\272\262\345\247\224\346\264\276\346\250\241\345\236\213.md" deleted file mode 100644 index 3b7c8da6e8..0000000000 --- "a/JDK/JVM/JVM\347\261\273\345\212\240\350\275\275\345\231\250\347\232\204\345\217\214\344\272\262\345\247\224\346\264\276\346\250\241\345\236\213.md" +++ /dev/null @@ -1,195 +0,0 @@ -说是双亲,其实多级单亲,无奈迎合历史的错误翻译吧。 -# 1 工作流程 -- 当一个类加载器收到一个类加载请求 -在 JDK9 后,会首先搜索它的内建加载器定义的所有“具名模块”: - - 如果找到合适的模块定义,将会使用该加载器来加载 - - 如果未找到,则会将该请求委派给父级加载器去加载 -- 因此所有的类加载请求最终都应该被传入到启动类加载器(Bootstrap ClassLoader)中,只有当父级加载器反馈无法完成这个列的加载请求时(它的搜索范围内不存在这个类),子级加载器才尝试加载。 - -在类路径下找到的类将成为这些加载器的无名模块。 - -**这里的父子关系是组合而不是继承**。 - -- 双亲委派模型示意图 -![](https://img-blog.csdnimg.cn/img_convert/2f38d489c0a1a948239765913d64cfcf.png) - -# 双亲委派模型的优点 -- 避免重复加载 -父类已经加载了,子类就不需要再次加载。 -eg,object 类。它存放在 rt.jar 中,无论哪个类加载器要加载这个类,最终都是委派给处于模型顶端的启动类加载器加载,因此 object 类在程序的各种加载环境中都是同一个类。 -- 更安全 -解决了各个类加载器的基础类的统一问题,如果不使用该种方式,那么用户可以随意定义类加载器来加载核心 API,会带来安全隐患。 - -# 双亲委派模型的实现 -```java - protected Class loadClass(String name, boolean resolve) - throws ClassNotFoundException - { - synchronized (getClassLoadingLock(name)) { - // 首先先检查该类已经被加载过了 - Class c = findLoadedClass(name); - if (c == null) {//该类没有加载过,交给父类加载 - long t0 = System.nanoTime(); - try { - if (parent != null) {//交给父类加载 - c = parent.loadClass(name, false); - } else {//父类不存在,则交给启动类加载器加载 - c = findBootstrapClassOrNull(name); - } - } catch (ClassNotFoundException e) { - //父类加载器抛出异常,无法完成类加载请求 - } - - if (c == null) {// - long t1 = System.nanoTime(); - //父类加载器无法完成类加载请求时,调用自身的findClass方法来完成类加载 - c = findClass(name); - sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); - sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); - sun.misc.PerfCounter.getFindClasses().increment(); - } - } - if (resolve) { - resolveClass(c); - } - return c; - } - } - -``` - -## 3.3 类加载的方式 -1. 通过命令行启动应用时由JVM初始化加载含有main()方法的主类。 -2. 通过Class.forName()方法动态加载,会默认执行初始化块(static{}),但是Class.forName(name,initialize,loader)中的initialze可指定是否要执行初始化块。 -3. 通过ClassLoader.loadClass()方法动态加载,不会执行初始化块。 - -# 自定义类加载器 -### 实现方式 -- 遵守双亲委派模型 -继承ClassLoader,重写findClass()方法。 -- 破坏双亲委派模型 -继承ClassLoader,重写loadClass()方法。 - -通常我们推荐采用第一种方法自定义类加载器,最大程度上的遵守双亲委派模型。 - -如果有一个类加载器能加载某个类,称为**定义类加载器**,所有能成功返回该类的Class的类加载器都被称为**初始类加载器**。 - -自定义类加载的目的是想要手动控制类的加载,那除了通过自定义的类加载器来手动加载类这种方式,还有其他的方式么? - -> 利用现成的类加载器进行加载: -> -> ``` -> 1. 利用当前类加载器 -> Class.forName(); -> -> 2. 通过系统类加载器 -> Classloader.getSystemClassLoader().loadClass(); -> -> 3. 通过上下文类加载器 -> Thread.currentThread().getContextClassLoader().loadClass(); -> ``` -> -> l -> 利用URLClassLoader进行加载: -> -> ``` -> URLClassLoader loader=new URLClassLoader(); -> loader.loadClass(); -> ``` - -* * * - -**类加载实例演示:** -命令行下执行HelloWorld.java - -```java -public class HelloWorld{ - public static void main(String[] args){ - System.out.println("Hello world"); - } -} -``` - -该段代码大体经过了一下步骤: - -1. 寻找jre目录,寻找jvm.dll,并初始化JVM. -2. 产生一个Bootstrap ClassLoader; -3. Bootstrap ClassLoader加载器会加载他指定路径下的java核心api,并且生成Extended ClassLoader加载器的实例,然后Extended ClassLoader会加载指定路径下的扩展java api,并将其父设置为Bootstrap ClassLoader。 -4. Bootstrap ClassLoader生成Application ClassLoader,并将其父Loader设置为Extended ClassLoader。 -5. 最后由AppClass ClassLoader加载classpath目录下定义的类——HelloWorld类。 - -我们上面谈到 Extended ClassLoader和Application ClassLoader是通过Launcher来创建,现在我们再看看源代码: - -```java - public Launcher() { - Launcher.ExtClassLoader var1; - try { - //实例化ExtClassLoader - var1 = Launcher.ExtClassLoader.getExtClassLoader(); - } catch (IOException var10) { - throw new InternalError("Could not create extension class loader", var10); - } - - try { - //实例化AppClassLoader - this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); - } catch (IOException var9) { - throw new InternalError("Could not create application class loader", var9); - } - //主线程设置默认的Context ClassLoader为AppClassLoader. - //因此在主线程中创建的子线程的Context ClassLoader 也是AppClassLoader - Thread.currentThread().setContextClassLoader(this.loader); - String var2 = System.getProperty("java.security.manager"); - if(var2 != null) { - SecurityManager var3 = null; - if(!"".equals(var2) && !"default".equals(var2)) { - try { - var3 = (SecurityManager)this.loader.loadClass(var2).newInstance(); - } catch (IllegalAccessException var5) { - ; - } catch (InstantiationException var6) { - ; - } catch (ClassNotFoundException var7) { - ; - } catch (ClassCastException var8) { - ; - } - } else { - var3 = new SecurityManager(); - } - - if(var3 == null) { - throw new InternalError("Could not create SecurityManager: " + var2); - } - - System.setSecurityManager(var3); - } - - } -``` -# 破坏双亲委派模型 -双亲模型有个问题:父加载器无法向下识别子加载器加载的资源。 -- 如下证明 JDBC 是启动类加载器加载,但 mysql 驱动是应用类加载器。而 JDBC 运行时又需要去访问子类加载器加载的驱动,就破坏了该模型。 -![](https://img-blog.csdnimg.cn/2021011918265025.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -JDK 自己为解决该问题,引入线程上下问类加载器,可以通过Thread的setContextClassLoader()进行设置 -- 当为启动类加载器时,使用当前实际加载驱动类的类加载器 -![](https://img-blog.csdnimg.cn/20210119184938411.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) -## 热替换 -比如OSGI的模块化热部署,它的类加载器就不再是严格按照双亲委派模型,很多 -可能就在平级的类加载器中执行了。 - -# FAQ -1. ClassLoader通过一个类全限定名来获取二进制流,如果我们需通过自定义类加载其来加载一个Jar包的时候,难道要自己遍历jar中的类,然后依次通过ClassLoader进行加载吗?或者说我们怎么来加载一个jar包呢? -对于动态加载jar而言,JVM默认会使用第一次加载该jar中指定类的类加载器作为默认的ClassLoader。 - -假设我们现在存在名为sbbic的jar包,该包中存在ClassA和ClassB类(ClassA中没有引用ClassB)。 -现在我们通过自定义的ClassLoaderA来加载在ClassA这个类,此时ClassLoaderA就成为sbbic.jar中其他类的默认类加载器。即ClassB默认也会通过ClassLoaderA去加载。 - -2. 如果一个类引用的其他的类,那么这个其他的类由谁来加载? - -如果ClassA中引用了ClassB呢? -当类加载器在加载ClassA的时候,发现引用了ClassB,此时类加载如果检测到ClassB还没有被加载,则先回去加载。当ClassB加载完成后,继续回来加载ClassA。即类会通过自身对应的来加载其加载其他引用的类。 - -3. 既然类可以由不同的加载器加载,那么如何确定两个类如何是同一个类? - -JVM规定:对于任何一个类,都需要由加载它的类加载器和这个类本身一同确立在java虚拟机中的唯一性。即在jvm中判断两个类是否是同一个类取决于类加载和类本身,也就是同一个类加载器加载的同一份Class文件生成的Class对象才是相同的,类加载器不同,那么这两个类一定不相同。 \ 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" deleted file mode 100644 index 59c68099d5..0000000000 --- "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" +++ /dev/null @@ -1,158 +0,0 @@ -## 1.1 jps -类似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) -## 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) - -### 1.2.1 jstat -class pid -显示加载class的数量及所占空间等信息 -- -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 -``` - -### 1.2.3 -gccapacity -比-gc多了各个代的最大值和最小值 -![jstat -gccapacity 3661](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LThmMGMwNTY4YTgzM2M5MzkucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWI1ZGVjMWVmNDgzYzM5ODUucG5n?x-oss-process=image/format,png) -### -gccause -最近一次GC统计和原因 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTE5ZTcxOTQ3NThlNGI5MzgucG5n?x-oss-process=image/format,png) -- LGCC:上次GC原因 -- GCC:当前GC原因 -![ -gcnew pid:new对象的信息。](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTMwNzQwZDQ1ODIyNzRmZGUucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWU4ZTkxN2Y1YjQ2YjgzMWMucG5n?x-oss-process=image/format,png) -### jstat -gcnewcapacity pid:new对象的信息及其占用量 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTVkNDIyNjNhNWNmYjQ1MjAucG5n?x-oss-process=image/format,png) - -![-gcold 显示老年代和永久代信息](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTI3OTI1YWUyNzA3YWI5Y2MucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWRkYzU1MTQ2ZDI3ZmY4ZDgucG5n?x-oss-process=image/format,png) -![-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/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) -## 1.3 jinfo -`jinfo