diff --git a/.gitignore b/.gitignore index 7ee001fcc8..6d26a3347a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,44 @@ -*.DS_Store -*.iml -*.xml -*.txt -*.dio +# Created by .ignore support plugin (hsz.mobi) +### Java template +# Compiled class file +*.class + +# Log file *.log +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +.DS_Store +.idea/JavaEdge.iml +.idea/compiler.xml .idea/inspectionProfiles/ +.idea/misc.xml +.idea/modules.xml +.idea/vcs.xml +.idea/workspace.xml +Python/.DS_Store +大数据/.DS_Store +操作系统/.DS_Store +数据存储/.DS_Store +面试题系列/.DS_Store + .metals/metals.h2.db +.metals/metals.log .vscode/settings.json -Spring/feign.md -TODO/MySQL会丢数据吗?.md -TODO/【阿里最新数据库面试题】MySQL主从一致性.md -TODO/【阿里数据库面试题解】MySQL高可用原理.md -TODO/MySQL执行更新语句时做了什么?.md -TODO/为何阿里不推荐MySQL使用join?.md -TODO/【阿里MySQL面试题】内部临时表.md -TODO/MySQL数据查询太多会OOM吗?.md -TODO/有了InnoDB,Memory存储引擎还有意义吗?.md -TODO/MySQL执行insert会如何加锁?.md -TODO/MySQL的自增id竟然用到头了怎么办?.md -TODO/MySQL全局锁和表锁.md -TODO/MySQL事务是怎么实现隔离的?.md - diff --git "a/Git/Git\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/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/AbstractQueuedSynchronizer\345\216\237\347\220\206\350\247\243\346\236\220.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/AbstractQueuedSynchronizer\345\216\237\347\220\206\350\247\243\346\236\220.md" deleted file mode 100644 index 75c4862c41..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/AbstractQueuedSynchronizer\345\216\237\347\220\206\350\247\243\346\236\220.md" +++ /dev/null @@ -1,448 +0,0 @@ -AbstractQueuedSynchronizer 抽象同步队列简称 AQS ,它是实现同步器的基础组件, -并发包中锁的底层就是使用 AQS 实现的. -大多数开发者可能永远不会直接使用AQS ,但是知道其原理对于架构设计还是很有帮助的,而且要理解ReentrantLock、CountDownLatch等高级锁我们必须搞懂 AQS. - -# 1 整体感知 -## 1.1 架构图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMjA3XzIwMjAwMjA5MTk1MDI3NjQ4LnBuZw?x-oss-process=image/format,png) -AQS框架大致分为五层,自上而下由浅入深,从AQS对外暴露的API到底层基础数据. - - -当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。当自定义同步器进行加锁或者解锁操作时,先经过第一层的API进入AQS内部方法,然后经过第二层进行锁的获取,接着对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层。 - -AQS 本身就是一套锁的框架,它定义了获得锁和释放锁的代码结构,所以如果要新建锁,只要继承 AQS,并实现相应方法即可。 - -## 1.2 类设计 -该类提供了一种框架,用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关的同步器(信号量,事件等)。此类的设计旨在为大多数依赖单个原子int值表示 state 的同步器提供切实有用的基础。子类必须定义更改此 state 的 protected 方法,并定义该 state 对于 acquired 或 released 此对象而言意味着什么。鉴于这些,此类中的其他方法将执行全局的排队和阻塞机制。子类可以维护其他状态字段,但是就同步而言,仅跟踪使用方法 *getState*,*setState* 和 *compareAndSetState* 操作的原子更新的int值。 -子类应定义为用于实现其所在类的同步属性的非公共内部帮助器类。 - -子类应定义为用于实现其所在类的同步属性的非 public 内部辅助类。类AbstractQueuedSynchronizer不实现任何同步接口。 相反,它定义了诸如*acquireInterruptible*之类的方法,可以通过具体的锁和相关的同步器适当地调用这些方法来实现其 public 方法。 - -此类支持默认的排他模式和共享模式: -- 当以独占方式进行获取时,其他线程尝试进行的获取将无法成功 -- 由多个线程获取的共享模式可能(但不一定)成功 - -该类不理解这些差异,只是从机制的意义上说,当共享模式获取成功时,下一个等待线程(如果存在)也必须确定它是否也可以获取。在不同模式下等待的线程们共享相同的FIFO队列。 通常,实现的子类仅支持这些模式之一,但也可以同时出现,比如在ReadWriteLock.仅支持排他模式或共享模式的子类无需定义支持未使用模式的方法. - -此类定义了一个内嵌的 **ConditionObject** 类,可由支持独占模式的子类用作Condition 的实现,该子类的 *isHeldExclusively* 方法报告相对于当前线程是否独占同步,使用当前 *getState* 值调用的方法 *release* 会完全释放此对象 ,并获得给定的此保存状态值,最终将该对象恢复为其先前的获取状态。否则,没有AbstractQueuedSynchronizer方***创建这样的条件,因此,如果无法满足此约束,请不要使用它。ConditionObject的行为当然取决于其同步器实现的语义。 - -此类提供了内部队列的检查,检测和监视方法,以及条件对象的类似方法。 可以根据需要使用 AQS 将它们导出到类中以实现其同步机制。 - -此类的序列化仅存储基础原子整数维护状态,因此反序列化的对象具有空线程队列。 需要序列化性的典型子类将定义一个readObject方法,该方法在反序列化时将其恢复为已知的初始状态。 - -# 2 用法 -要将此类用作同步器的基础,使用*getState* *setState*和/或*compareAndSetState*检查和/或修改同步状态,以重新定义以下方法(如适用) -- tryAcquire -- tryRelease -- tryAcquireShared -- tryReleaseShared -- isHeldExclusively - -默认情况下,这些方法中的每一个都会抛 *UnsupportedOperationException*。 -这些方法的实现必须在内部是线程安全的,并且通常应简短且不阻塞。 定义这些方法是使用此类的**唯一**受支持的方法。 所有其他方法都被声明为final,因为它们不能独立变化。 - -从 AQS 继承的方法对跟踪拥有排他同步器的线程很有用。 鼓励使用它们-这将启用监视和诊断工具,以帮助用户确定哪些线程持有锁。 - -虽然此类基于内部的FIFO队列,它也不会自动执行FIFO获取策略。 独占同步的核心采用以下形式: -- Acquire -```java -while (!tryAcquire(arg)) { - 如果线程尚未入队,则将其加入队列; - 可能阻塞当前线程; -} -``` -- Release - -```java -if (tryRelease(arg)) - 取消阻塞第一个入队的线程; -``` -共享模式与此相似,但可能涉及级联的signal。 - -acquire 中的检查是在入队前被调用,所以新获取的线程可能会在被阻塞和排队的其他线程之前插入。但若需要,可以定义tryAcquire、tryAcquireShared以通过内部调用一或多种检查方法来禁用插入,从而提供公平的FIFO获取顺序。 - -特别是,若 hasQueuedPredecessors()(公平同步器专门设计的一种方法)返回true,则大多数公平同步器都可以定义tryAcquire返回false. - -- 公平与否取决于如下一行代码: -```java -if (c == 0) { - if (!hasQueuedPredecessors() && - compareAndSetState(0, acquires)) { - setExclusiveOwnerThread(current); - return true; - } -} -``` -### hasQueuedPredecessors -```java -public final boolean hasQueuedPredecessors() { - // The correctness of this depends on head being initialized - // before tail and on head.next being accurate if the current - // thread is first in queue. - Node t = tail; // Read fields in reverse initialization order - Node h = head; - // s代表等待队列的第一个节点 - Node s; - // (s = h.next) == null 说明此时有另一个线程正在尝试成为头节点,详见AQS的acquireQueued方法 - // s.thread != Thread.currentThread():此线程不是等待的头节点 - return h != t && - ((s = h.next) == null || s.thread != Thread.currentThread()); -} -``` - - - -对于默认的插入(也称为贪婪,放弃和convoey-avoidance)策略,吞吐量和可伸缩性通常最高。 尽管不能保证这是公平的或避免饥饿,但允许较早排队的线程在较晚排队的线程之前进行重新竞争,并且每个重新争用都有一次机会可以毫无偏向地成功竞争过进入的线程。 -同样,尽管获取通常无需自旋,但在阻塞前,它们可能会执行tryAcquire的多次调用,并插入其他任务。 如果仅短暂地保持排他同步,则这将带来自旋的大部分好处,而如果不进行排他同步,则不会带来很多负担。 如果需要的话,可以通过在调用之前使用“fast-path”检查来获取方法来增强此功能,并可能预先检查*hasContended*()和/或*hasQueuedThreads()*,以便仅在同步器可能不存在争用的情况下这样做。 - -此类为同步提供了有效且可扩展的基础,部分是通过将其使用范围规范化到可以依赖于int状态,acquire 和 release 参数以及内部的FIFO等待队列的同步器。 当这还不够时,可以使用原子类、自定义队列类和锁支持阻塞支持从较低级别构建同步器。 - -# 3 使用案例 -这里是一个不可重入的排他锁,它使用值0表示解锁状态,使用值1表示锁定状态。虽然不可重入锁并不严格要求记录当前所有者线程,但是这个类这样做是为了更容易监视使用情况。它还支持条件,并暴露其中一个检测方法: - -```java -class Mutex implements Lock, java.io.Serializable { - - // 我们内部的辅助类 - private static class Sync extends AbstractQueuedSynchronizer { - // 报告是否处于锁定状态 - protected boolean isHeldExclusively() { - return getState() == 1; - } - - // 如果 state 是 0,获取锁 - public boolean tryAcquire(int acquires) { - assert acquires == 1; // Otherwise unused - if (compareAndSetState(0, 1)) { - setExclusiveOwnerThread(Thread.currentThread()); - return true; - } - return false; - } - - // 通过将 state 置 0 来释放锁 - protected boolean tryRelease(int releases) { - assert releases == 1; // Otherwise unused - if (getState() == 0) throw new IllegalMonitorStateException(); - setExclusiveOwnerThread(null); - setState(0); - return true; - } - - // 提供一个 Condition - Condition newCondition() { return new ConditionObject(); } - - // 反序列化属性 - private void readObject(ObjectInputStream s) - throws IOException, ClassNotFoundException { - s.defaultReadObject(); - setState(0); // 重置到解锁状态 - } - } - - // 同步对象完成所有的工作。我们只是期待它. - private final Sync sync = new Sync(); - - public void lock() { sync.acquire(1); } - public boolean tryLock() { return sync.tryAcquire(1); } - public void unlock() { sync.release(1); } - public Condition newCondition() { return sync.newCondition(); } - public boolean isLocked() { return sync.isHeldExclusively(); } - public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); } - public void lockInterruptibly() throws InterruptedException { - sync.acquireInterruptibly(1); - } - public boolean tryLock(long timeout, TimeUnit unit) - throws InterruptedException { - return sync.tryAcquireNanos(1, unit.toNanos(timeout)); - } -} -``` - -这是一个闩锁类,它类似于*CountDownLatch*,只是它只需要一个单信号就可以触发。因为锁存器是非独占的,所以它使用共享的获取和释放方法。 - -```java - class BooleanLatch { - - private static class Sync extends AbstractQueuedSynchronizer { - boolean isSignalled() { return getState() != 0; } - - protected int tryAcquireShared(int ignore) { - return isSignalled() ? 1 : -1; - } - - protected boolean tryReleaseShared(int ignore) { - setState(1); - return true; - } - } - - private final Sync sync = new Sync(); - public boolean isSignalled() { return sync.isSignalled(); } - public void signal() { sync.releaseShared(1); } - public void await() throws InterruptedException { - sync.acquireSharedInterruptibly(1); - } - } -``` - -# 4 基本属性与框架 -## 4.1 继承体系图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyOTU4XzIwMjAwMjEwMjMyNTQwMzUwLnBuZw?x-oss-process=image/format,png) -## 4.2 定义 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDk2XzIwMjAwMjEwMjMxMTIwMTMzLnBuZw?x-oss-process=image/format,png) - -可知 AQS 是一个抽象类,生来就是被各种子类锁继承的。继承自AbstractOwnableSynchronizer,其作用就是为了知道当前是哪个线程获得了锁,便于后续的监控 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDYyXzIwMjAwMjEwMjMyMzE4MzcyLnBuZw?x-oss-process=image/format,png) - - -## 4.3 属性 -### 4.3.1 状态信息 -- volatile 修饰,对于可重入锁,每次获得锁 +1,释放锁 -1 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMTEwXzIwMjAwMjEwMjMyOTI0MTczLnBuZw?x-oss-process=image/format,png) -- 可以通过 *getState* 得到同步状态的当前值。该操作具有 volatile 读的内存语义。 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyOTM5XzIwMjAwMjEwMjMzMjM0OTE0LnBuZw?x-oss-process=image/format,png) -- setState 设置同步状态的值。该操作具有 volatile 写的内存语义 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDc4XzIwMjAwMjEwMjMzOTI2NjQ3LnBuZw?x-oss-process=image/format,png) -- compareAndSetState 如果当前状态值等于期望值,则以原子方式将同步状态设置为给定的更新值。此操作具有 volatile 读和写的内存语义 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDMyXzIwMjAwMjEwMjM1MjM1NDAzLnBuZw?x-oss-process=image/format,png) -- 自旋比使用定时挂起更快。粗略估计足以在非常短的超时时间内提高响应能力,当设置等待时间时才会用到这个属性 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDY0XzIwMjAwMjExMDAzOTIzNjQxLnBuZw?x-oss-process=image/format,png) - -这写方法都是Final的,子类无法重写。 -- 独占模式 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyODIzXzIwMjAwMjExMDIyNTI0NjM5LnBuZw?x-oss-process=image/format,png) -- 共享模式 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDQzXzIwMjAwMjExMDIyNTUxODMwLnBuZw?x-oss-process=image/format,png) -### 4.3.2 同步队列 -- CLH 队列( FIFO) -![](https://img-blog.csdnimg.cn/2020100800492945.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70#pic_center)![在这里插入图片描述](https://img-blog.csdnimg.cn/img_convert/22686302fc050911b9dc9cdaf672934b.png) - -- 作用 -阻塞获取不到锁(独占锁)的线程,并在适当时机从队首释放这些线程。 - -同步队列底层数据结构是个双向链表。 - -- 等待队列的头,延迟初始化。 除初始化外,只能通过 *setHead* 方法修改 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyODc4XzIwMjAwMjExMDIzMjU4OTU4LnBuZw?x-oss-process=image/format,png) -注意:如果head存在,则其waitStatus保证不会是 *CANCELLED* - -- 等待队列的尾部,延迟初始化。 仅通过方法 *enq* 修改以添加新的等待节点 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDk0XzIwMjAwMjExMDIzNTU3MTkyLnBuZw?x-oss-process=image/format,png) -### 4.3.4 条件队列 -#### 为什么需要条件队列? -同步队列并非所有场景都能cover,遇到锁 + 队列结合的场景时,就需要 Lock + Condition,先使用 Lock 决定: -- 哪些线程可以获得锁 -- 哪些线程需要到同步队列里排队阻塞 - -获得锁的多个线程在碰到队列满或空时,可使用 Condition 来管理这些线程,让这些线程阻塞等待,然后在合适的时机后,被正常唤醒。 - -**同步队列 + 条件队列的协作多被用在锁 + 队列场景。** -#### 作用 -AQS 的内部类,结合锁实现线程同步。存放调用条件变量的 await 方法后被阻塞的线程 - -- 实现了 Condition 接口,而 Condition 接口就相当于 Object 的各种监控方法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMTEwXzIwMjAwMjExMDI0MDUzNi5wbmc?x-oss-process=image/format,png) -需要使用时,直接 new ConditionObject()。 - -### 4.3.5 Node -同步队列和条件队列的共用节点。 -入队时,用 Node 把线程包装一下,然后把 Node 放入两个队列中,我们看下 Node 的数据结构,如下: -#### 4.3.5.1 模式 -- 共享模式 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDUyXzIwMjAwMjExMDI1NTQxMTYyLnBuZw?x-oss-process=image/format,png) -- 独占模式 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyOTAzXzIwMjAwMjExMDI1NjE2NzkyLnBuZw?x-oss-process=image/format,png) - -#### 4.3.5.2 waitstatus - 等待状态 -```java -volatile int waitStatus; -``` -仅能为如下值: -##### SIGNAL -- 同步队列中的节点在自旋获取锁时,如果前一个节点的状态是 `SIGNAL`,那么自己就直接被阻塞,否则一直自旋 -- 该节点的后继节点会被(或很快)阻塞(通过park),因此当前节点释放或取消时必须unpark其后继节点。为避免竞争,acquire方法必须首先指示它们需要一个 signal,然后重试原子获取,然后在失败时阻塞。 -```java -static final int SIGNAL = -1; -``` - -##### CANCELLED -表示线程获取锁的请求已被取消了: -```java -static final int CANCELLED = 1; -``` -可能由于超时或中断,该节点被取消。 - -节点永远不会离开此状态,此为一种终极状态。具有 cancelled 节点的线程永远不会再次阻塞。 -##### CONDITION -该节点当前在条件队列,当节点从同步队列被转移到条件队列,状态就会被更改该态: -```java -static final int CONDITION = -2; -``` -在被转移之前,它不会用作同步队列的节点,此时状态将置0(该值的使用与该字段的其他用途无关,仅是简化了机制)。 - -##### PROPAGATE -线程处在 `SHARED` 情景下,该字段才会启用。 - -指示下一个**acquireShared**应该无条件传播,共享模式下,该状态的线程处Runnable态 -```java -static final int PROPAGATE = -3; -``` -*releaseShared* 应该传播到其他节点。 在*doReleaseShared*中对此进行了设置(仅适用于头节点),以确保传播继续进行,即使此后进行了其他操作也是如此。 -##### 0 -初始化时的默认值。 -##### 小结 -这些值是以数字方式排列,极大方便了开发者的使用。我们在平时开发也可以定义一些有特殊意义的常量值。 - -非负值表示节点不需要 signal。 因此,大多数代码并不需要检查特定值,检查符号即可。 - -- 对于普通的同步节点,该字段初始化为0 -- 对于条件节点,该字段初始化为`CONDITION` - -使用CAS(或在可能的情况下进行无条件的 volatile 写)对其进行修改。 - -注意两个状态的区别 -- state 是锁的状态,int 型,子类继承 AQS 时,都是要根据 state 字段来判断有无得到锁 -- waitStatus 是节点(Node)的状态 - -#### 4.3.5.3 数据结构 -##### 前驱节点 -- 链接到当前节点/线程所依赖的用来检查 *waitStatus* 的前驱节点 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyODY0XzIwMjAwMjEyMDMyNjI1NjYxLnBuZw?x-oss-process=image/format,png) - -在入队期间赋值,并且仅在出队时将其清空(为了GC)。 - -此外,在取消一个前驱结点后,在找到一个未取消的节点后会短路,这将始终存在,因为头节点永远不会被取消:只有成功 acquire 后,一个节点才会变为头。 - -取消的线程永远不会成功获取,并且线程只会取消自身,不会取消任何其他节点。 - -##### 后继节点 -链接到后继节点,当前节点/线程在释放时将其unpark。 在入队时赋值,在绕过已取消的前驱节点时进行调整,在出队时置null(为了GC)。 -入队操作直到附加后才赋值前驱节点的`next`字段,因此看到`next`字段为 null,并不一定意味该节点位于队尾(有时间间隙)。 - -但若`next == null`,则可从队尾开始扫描`prev`以进行再次检查。 -```java -// 若节点通过从tail向前搜索发现在在同步队列上,则返回 true -// 仅在调用了 isOnSyncQueue 且有需要时才调用 -private boolean findNodeFromTail(Node node) { - Node t = tail; - for (;;) { - if (t == node) - return true; - if (t == null) - return false; - t = t.prev; - } -} -``` -```java -final boolean isOnSyncQueue(Node node) { - if (node.waitStatus == Node.CONDITION || node.prev == null) - return false; - if (node.next != null) // If has successor, it must be on queue - return true; - /** - * node.prev 可以非null,但还没有在队列中,因为将它放在队列中的 CAS 可能会失败。 - * 所以必须从队尾向前遍历以确保它确实成功了。 - * 在调用此方法时,它将始终靠近tail,并且除非 CAS 失败(这不太可能) - * 否则它会在那里,因此几乎不会遍历太多 - */ - return findNodeFromTail(node); -} -``` -已取消节点的`next`字段设置为指向节点本身而不是null,以使isOnSyncQueue更轻松。 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDg3XzIwMjAwMjEyMDM1NTAzNjAyLnBuZw?x-oss-process=image/format,png) -- 使该节点入队的线程。 在构造时初始化,使用后消亡。![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyODcwXzIwMjAwMjEyMjAwMTI3OTkwLnBuZw?x-oss-process=image/format,png) - -在同步队列中,nextWaiter 表示当前节点是独占模式还是共享模式 -在条件队列中,nextWaiter 表示下一个节点元素 - -链接到在条件队列等待的下一个节点,或者链接到特殊值`SHARED`。 由于条件队列仅在以独占模式保存时才被访问,因此我们只需要一个简单的链接队列即可在节点等待条件时保存节点。 然后将它们转移到队列中以重新获取。 并且由于条件只能是独占的,因此我们使用特殊值来表示共享模式来保存字段。![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDUxXzIwMjAwMjEyMjAxMjMyODMucG5n?x-oss-process=image/format,png) -# 5 Condition 接口 -JDK5 时提供。 -- 条件队列 ConditionObject 实现了 Condition 接口 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDc1XzIwMjAwMjEyMjA0NjMxMTMzLnBuZw?x-oss-process=image/format,png) -- 本节就让我们一起来研究之 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDgwXzIwMjAwMjEyMjA1MzQ2NzIyLnBuZw?x-oss-process=image/format,png) - -Condition 将对象监视方法(wait,notify和notifyAll)分解为不同的对象,从而通过与任意Lock实现结合使用,从而使每个对象具有多个wait-sets。 当 Lock 替换了 synchronized 方法和语句的使用,Condition 就可以替换了Object监视器方法的使用。 - -Condition 的实现可以提供与 Object 监视方法不同的行为和语义,例如保证通知的顺序,或者在执行通知时不需要保持锁定。 如果实现提供了这种专门的语义,则实现必须记录这些语义。 - -Condition实例只是普通对象,它们本身可以用作 synchronized 语句中的目标,并且可以调用自己的监视器 wait 和 notification 方法。 获取 Condition 实例的监视器锁或使用其监视器方法与获取与该条件相关联的锁或使用其 await 和 signal 方法没有特定的关系。 建议避免混淆,除非可能在自己的实现中,否则不要以这种方式使用 Condition 实例。 - -```java - class BoundedBuffer { - final Lock lock = new ReentrantLock(); - final Condition notFull = lock.newCondition(); - final Condition notEmpty = lock.newCondition(); - - final Object[] items = new Object[100]; - int putptr, takeptr, count; - - public void put(Object x) throws InterruptedException { - lock.lock(); - try { - while (count == items.length) - notFull.await(); - items[putptr] = x; - if (++putptr == items.length) putptr = 0; - ++count; - notEmpty.signal(); - } finally { - lock.unlock(); - } - } - - public Object take() throws InterruptedException { - lock.lock(); - try { - while (count == 0) - notEmpty.await(); - Object x = items[takeptr]; - if (++takeptr == items.length) takeptr = 0; - --count; - notFull.signal(); - return x; - } finally { - lock.unlock(); - } - } - } -``` -(ArrayBlockingQueue类提供了此功能,因此没有理由实现此示例用法类。) -定义出一些方法,这些方法奠定了条件队列的基础 -## API -### await -- 使当前线程等待,直到被 signalled 或被中断 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyOTkwXzIwMjAwMjEyMjExNzU3ODYyLnBuZw?x-oss-process=image/format,png) - -与此 Condition 相关联的锁被原子释放,并且出于线程调度目的,当前线程被禁用,并且处于休眠状态,直到发生以下四种情况之一: -- 其它线程为此 Condition 调用了 signal 方法,并且当前线程恰好被选择为要唤醒的线程 -- 其它线程为此 Condition 调用了 signalAll 方法 -- 其它线程中断了当前线程,并且当前线程支持被中断 -- 发生“虚假唤醒”。 - -在所有情况下,在此方法可以返回之前,必须重新获取与此 Condition 关联的锁,才能真正被唤醒。当线程返回时,可以保证保持此锁。 -### await 超时时间 -- 使当前线程等待,直到被 signal 或中断,或经过指定的等待时间 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDczXzIwMjAwMjEyMjIxMjU3NTcyLnBuZw?x-oss-process=image/format,png) - -此方法在行为上等效于: - - -```java -awaitNanos(unit.toNanos(time)) > 0 -``` -所以,虽然入参可以是任意单位的时间,但其实仍会转化成纳秒 -### awaitNanos -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyOTg0XzIwMjAwMjEyMjIxOTMxNTU5LnBuZw?x-oss-process=image/format,png) -注意这里选择纳秒是为了避免计算剩余等待时间时的截断误差 - - -### signal() -- 唤醒条件队列中的一个线程,在被唤醒前必须先获得锁 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkzMDYzXzIwMjAwMjEyMjMwNTQ3NjMzLnBuZw?x-oss-process=image/format,png) -### signalAll() -- 唤醒条件队列中的所有线程 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMTMvNTA4ODc1NV8xNTgxNTMzMTkyODkyXzIwMjAwMjEyMjMwNjUzNTM5LnBuZw?x-oss-process=image/format,png) \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/FurureTask.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/FurureTask.md" index 149a2a9585..3c1bbd6ce7 100644 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/FurureTask.md" +++ "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/FurureTask.md" @@ -1,26 +1,32 @@ -# 1 简介 -- 使用继承方式的好处是方便传参,可以在子类里面添加成员变量,通过 set 方法设置参数或者通过构造函数进行传递 +# 1 引导语 +研究源码,一般我们都从整体以及实例先入手,再研究细节,不至于一开始就“深陷其中而"当局者迷". +本文,我们来看最后一种有返回值的线程创建方式。 + +- 使用继承方式的好处是方便传参,可以在子类里面添加成员变量,通过 set 方法设置参数或者通过构造函数进行传递 - 使用 Runnable 方式,则只能使用主线程里面被声明为 final 变量 -不好的地方是 Java 不支持多继承,如果继承了 Thread 类,那么子类不能再继承其他 ,而 Runable接口则没有这个限制 。而且 Thread 类和 Runnable 接口都不允许声明检查型异常,也不能定义返回值。没有返回值这点稍微有点麻烦。前两种方式都没办法拿到任务的返回结果,但今天的主角 FutureTask 却可以。 +不好的地方是 Java 不支持多继承,如果继承了 Thread 类,那么子类不能再继承其他 ,而 Runable接口则没有这个限制 。而且 Thread 类和 Runnable 接口都不允许声明检查型异常,也不能定义返回值。没有返回值这点稍微有点麻烦。前两种方式都没办法拿到任务的返回结果,但今天的主角 FutureTask 却可以. 不能声明抛出检查型异常则更麻烦一些。run()方法意味着必须捕获并处理检查型异常。即使小心地保存了异常信息(在捕获异常时)以便稍后检查,但也不能保证这个 Runnable 对象的所有使用者都读取异常信息。你也可以修改Runnable实现的getter,让它们都能抛出任务执行中的异常。但这种方法除了繁琐也不是十分安全可靠,你不能强迫使用者调用这些方法,程序员很可能会调用join()方法等待线程结束然后就不管了。 但是现在不用担心了,以上的问题终于在1.5中解决了。Callable接口和Future接口的引入以及他们对线程池的支持优雅地解决了这两个问题。 # 2 案例 先看一个demo,了解 FutureTask 相关组件是如何使用的 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY3MjUwXzIwMjAwMjA3MjIxNjM4OTk2LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177167250_20200207221638996.png) +CallerTask 类实现了 Callable 接口的 call() 方法 。在 main 函数内首先创建FutrueTask对 象(构造函数为 CallerTask 实例), 然后使用创建的 FutureTask 作为任务创建了一个线程并且启动它, 最后通过 futureTask.get()等待任务执行完毕并返回结果. + + # 3 Callable Callable函数式接口定义了唯一方法 - call(). 我们可以在Callable的实现中声明强类型的返回值,甚至是抛出异常。同时,利用call()方法直接返回结果的能力,省去读取值时的类型转换。 - 源码定义 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NjQ1XzIwMjAwMjAyMjA0MjA0MjIyLnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166645_20200202204204222.png) 注意到返回值是一个泛型,使用的时候,不会直接使用 Callable,而是和 FutureTask 协同. # 4 Future - Callable 可以返回线程的执行结果,在获取结果时,就需要用到 Future 接口. -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2OTIwXzIwMjAwMjA0MDQwMDA3MzMucG5n?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166920_2020020404000733.png) Future是 Java5 中引入的接口,当提交一个Callable对象给线程池时,将得到一个Future对象,并且它和传入的Callable有相同的结果类型声明。 @@ -34,7 +40,7 @@ Future表示异步计算的结果。提供了一些方法来检查计算是否 ## 4.1 Future API ### 4.1.1 cancel - 尝试取消执行任务 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2Njc4XzIwMjAwMjA0MDIxOTEwMTI1LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166678_20200204021910125.png) 一个比较复杂的方法,当任务处于不同状态时,该方法有不同响应: - 任务 已经完成 / 已经取消 / 由于某些其他原因无法被取消,该尝试会直接失败 - 尝试成功,且此时任务尚未开始,调用后是可以取消成功的 @@ -45,42 +51,34 @@ Future表示异步计算的结果。提供了一些方法来检查计算是否 如果此方法返回 true,则随后对 *isCancelled* 的调用将始终返回 true. ### 4.1.2 isCancelled - 是否被取消 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2ODM0XzIwMjAwMjA0MDMwMzU2OTM1LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166834_20200204030356935.png) 如果此任务在正常完成之前被取消,则返回true. ### 4.1.3 isDone - 是否完成 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NTc0XzIwMjAwMjA0MDMxMDA1NDg4LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166574_20200204031005488.png) 如果此任务完成,则返回true. 完成可能是由于正常终止,异常或取消引起的,在所有这些情况下,此方法都将返回true. ### 4.1.4 get - 获取结果 -等待任务完成,然后获取其结果。 -![](https://img-blog.csdnimg.cn/20210615233538777.png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166906_20200204031206355.png) +等待任务完成,然后获取其结果. -若: -- 任务被取消,抛 *CancellationException* -- 当前线程在等待时被中断,抛 *InterruptedException* -- 任务抛出了异常,抛 *ExecutionException* +- 如果任务被取消,抛 *CancellationException* +- 如果当前线程在等待时被中断,抛 *InterruptedException* +- 如果任务抛出了异常,抛 *ExecutionException* ### 4.1.5 timed get - 超时获取 -- 必要时最多等待给定时间以完成任务,然后获取其结果(如果有的话)。 -![](https://img-blog.csdnimg.cn/20210615233634165.png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166633_20200204033827757.png) +必要时最多等待给定时间以完成任务,然后获取其结果(如果有的话)。 - 抛CancellationException 如果任务被取消 - 抛 ExecutionException 如果任务抛了异常 - 抛InterruptedException 如果当前线程在等待时被中断 - 抛TimeoutException 如果等待超时了 -两个get()方法都是阻塞的,若被调用时,任务还没有执行完,则调用get()方法的线程会阻塞,直到任务执行完才会被唤醒。所以future.get()是会阻塞当前调用线程。 -- 阻塞异步线程 -![](https://img-blog.csdnimg.cn/20210420165206559.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -![](https://img-blog.csdnimg.cn/2021042016492740.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -- 阻塞主线程 -![](https://img-blog.csdnimg.cn/20210420165240137.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) +需要注意:这两个get()方法都是阻塞式的,如果被调用的时候,任务还没有执行完,那么调用get()方法的线程会阻塞,直到任务执行完才会被唤醒。 -![](https://img-blog.csdnimg.cn/20210420165122388.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -Future 接口定义了许多对任务进行管理的 API,极大地方便了我们的开发调控。 +Future 接口定义了许多对任务进行管理的 API,极大地方便了我们的开发调控. # 5 RunnableFuture Java6 时提供的持有 Runnable 性质的 Future. @@ -88,7 +86,7 @@ Java6 时提供的持有 Runnable 性质的 Future. 成功执行run方法导致Future的完成,并允许访问其结果. RunnableFuture接口比较简单,就是继承了 Runnable 和 Future 接口。只提供一个*run*方法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2OTE2XzIwMjAwMjA0MDQwMjM0NzM1LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166916_20200204040234735.png) 现在,我们应该都知道,创建任务有两种方式 - 无返回值的 Runnable @@ -99,8 +97,8 @@ RunnableFuture接口比较简单,就是继承了 Runnable 和 Future 接口。 所以铺垫了这么多,本文的主角 FutureTask 来了! # 6 FutureTask -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NTk5XzIwMjAwMjAyMjA1MzM1MzA3LnBuZw?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2OTQ3XzIwMjAwMjA4MjI0MjEzNDYucG5n?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166599_20200202205335307.png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166947_2020020822421346.png) 前面的Future是一个接口,而 FutureTask 才是一个实实在在的工具类,是线程运行的具体任务. - 实现了 RunnableFuture 接口 - 也就是实现了 Runnnable 接口,即FutureTask 本身就是个 Runnnable @@ -114,7 +112,7 @@ RunnableFuture接口比较简单,就是继承了 Runnable 和 Future 接口。 - 注意这些常量字段的定义方式,遵循避免魔鬼数字的编程规约. -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2ODg4XzIwMjAwMjA4MTM0OTU2MTQ1LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166888_20200208134956145.png) - NEW 线程任务创建,开始状态 @@ -139,28 +137,28 @@ RunnableFuture接口比较简单,就是继承了 Runnable 和 Future 接口。 ### 6.1.2 其他属性 - 组合的 callable,这样就具备了转化 Callable 和 Runnable 的功能 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2ODQ3XzIwMjAwMjA4MjI0MDM0NzU0LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166847_20200208224034754.png) - 从ge()返回或抛出异常的结果,非volatile,受状态读/写保护 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NTYzXzIwMjAwMjA4MjI0MzQzNjQ2LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166563_20200208224343646.png) - 运行 callable 的线程; 在run()期间进行CAS -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NzkyXzIwMjAwMjA4MjI1NDU3Mzc2LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166792_20200208225457376.png) - 记录调用 get 方法时被等待的线程 - 栈形式 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NjI5XzIwMjAwMjA4MjI1NzM1NzE5LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166629_20200208225735719.png) 从属性上我们明显看到 Callable 是作为 FutureTask 的属性之一,这也就让 FutureTask 接着我们看下 FutureTask 的构造器,看看两者是如何转化的。 ## 6.2 构造方法 ### 6.2.1 Callable 参数 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY3MTI4XzIwMjAwMjA4MjMwMjIyNDg2LnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177167128_20200208230222486.png) ### 6.2.2 Runnable 参数 为协调 callable 属性,辅助result 参数 Runnable 是没有返回值的,所以 result 一般没有用,置为 null 即可,正如 JDK 所推荐写法 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2ODcwXzIwMjAwMjA4MjMxNTEwMzEwLnBuZw?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2OTcwXzIwMjAwMjA4MjMwMzM2MjIxLnBuZw?x-oss-process=image/format,png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166870_20200208231510310.png) +![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166970_20200208230336221.png) - Executors.callable 方法负责将 runnable 适配成 callable. - ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY2NzE0XzIwMjAwMjA4MjMyMDUxMjQ2LnBuZw?x-oss-process=image/format,png) + ![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177166714_20200208232051246.png) - 通过转化类 RunnableAdapter进行适配 - ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAyMDAyMDgvNTA4ODc1NV8xNTgxMTc3MTY3MzE0XzIwMjAwMjA4MjMyMjExNjE2LnBuZw?x-oss-process=image/format,png) + ![](https://uploadfiles.nowcoder.com/files/20200208/5088755_1581177167314_20200208232211616.png) ### 6.2.3 小结 我们可以学习这里的适配器模式,目标是要把 Runnable 适配成 Callable,那么我们首先要实现 Callable 接口,并且在 Callable 的 call 方法里面调用被适配对象即 Runnable的方法即可. @@ -236,12 +234,12 @@ private int awaitDone(boolean timed, long nanos) } ``` -get 是一种阻塞式方法,当发现任务还在进行中,没有完成时,就会阻塞当前进程,等待任务完成后再返回结果值。 - -阻塞底层使用的是 LockSupport.park 方法,使当前线程进入 `WAITING` 或 `TIMED_WAITING` 态。 +get 是一种阻塞式方法,当发现任务还在进行中,没有完成时,就会阻塞当前进程,等待任务完成后再返回结果值. +阻塞底层使用的是 LockSupport.park 方法,使当前线程进入 `WAITING` 或 `TIMED_WAITING` 态. ## 6.4 run -该方法可被直接调用,也可由线程池调用 +该方法可被直接调用,也可由线程池调用 + ```java public void run() { // 状态非 NEW 或当前任务已有线程在执行,直接返回 @@ -277,9 +275,12 @@ public void run() { } ``` -run 方法没有返回值,通过给 outcome 属性赋值(set(result)),get 时就能从 outcome 属性中拿到返回值。 +run 方法我们再说明几点: + +run 方法是没有返回值的,通过给 outcome 属性赋值(set(result)),get 时就能从 outcome 属性中拿到返回值; FutureTask 两种构造器,最终都转化成了 Callable,所以在 run 方法执行的时候,只需要执行 Callable 的 call 方法即可,在执行 c.call() 代码时,如果入参是 Runnable 的话, 调用路径为 c.call() -> RunnableAdapter.call() -> Runnable.run(),如果入参是 Callable 的话,直接调用。 ## 6.5 cancel + ```java // 取消任务,如果正在运行,尝试去打断 public boolean cancel(boolean mayInterruptIfRunning) { @@ -308,4 +309,4 @@ public boolean cancel(boolean mayInterruptIfRunning) { ``` # 7 总结 -FutureTask 统一了 Runnnable 和 Callable,方便了我们后续对线程池的使用。 \ No newline at end of file +FutureTask 统一了 Runnnable 和 Callable,方便了我们后续对线程池的使用. \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/CountDownLatch.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/JDK\346\272\220\347\240\201\350\247\243\346\236\220\345\256\236\346\210\230 - CountDownLatch.md" similarity index 100% rename from "JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/CountDownLatch.md" rename to "JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/JDK\346\272\220\347\240\201\350\247\243\346\236\220\345\256\236\346\210\230 - CountDownLatch.md" diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/Java\351\233\206\345\220\210\344\271\213HashMap\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/Java\351\233\206\345\220\210\344\271\213HashMap\346\272\220\347\240\201\350\247\243\346\236\220.md" deleted file mode 100644 index 59050eb978..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/Java\351\233\206\345\220\210\344\271\213HashMap\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ /dev/null @@ -1,715 +0,0 @@ -# 1 概述 -HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长. - -HashMap是非线程安全的,只适用于单线程环境,多线程环境可以采用并发包下的`concurrentHashMap` - -HashMap 实现了Serializable接口,因此它支持序列化,实现了Cloneable接口,能被克隆 - -HashMap是基于哈希表的Map接口的非同步实现.此实现提供所有可选的映射操作,并允许使用null值和null键.此类不保证映射的顺序,特别是它不保证该顺序恒久不变. - -Java8中又对此类底层实现进行了优化,比如引入了红黑树的结构以解决哈希碰撞 -  -# 2 HashMap的数据结构 -在Java中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造,HashMap也不例外. -HashMap实际上是一个"链表散列"的数据结构,即数组和链表的结合体. - -![HashMap的结构](https://img-blog.csdnimg.cn/img_convert/c5ac3aacefa3b745d9d2fa48c577b11d.png) -HashMap的主结构类似于一个数组,添加值时通过`key`确定储存位置. -每个位置是一个Entry的数据结构,该结构可组成链表. -当发生冲突时,相同hash值的键值对会组成链表. -这种`数组+链表`的组合形式大部分情况下都能有不错的性能效果,Java6、7就是这样设计的. -然而,在极端情况下,一组(比如经过精心设计的)键值对都发生了冲突,这时的哈希结构就会退化成一个链表,使HashMap性能急剧下降. - -所以在Java8中,HashMap的结构实现变为数组+链表+红黑树 -![Java8 HashMap的结构](https://img-blog.csdnimg.cn/img_convert/7668e49d6bb167520dcf09ab09537378.png) -可以看出,HashMap底层就是一个数组结构 -数组中的每一项又是一个链表 -当新建一个HashMap时,就会初始化一个数组. - -# 3 三大集合与迭代子 -HashMap使用三大集合和三种迭代子来轮询其Key、Value和Entry对象 -```java -public class HashMapExam { - public static void main(String[] args) { - Map map = new HashMap<>(16); - for (int i = 0; i < 15; i++) { - map.put(i, new String(new char[]{(char) ('A'+ i)})); - } - - System.out.println("======keySet======="); - Set set = map.keySet(); - Iterator iterator = set.iterator(); - while (iterator.hasNext()) { - System.out.println(iterator.next()); - } - - System.out.println("======values======="); - Collection values = map.values(); - Iterator stringIterator=values.iterator(); - while (stringIterator.hasNext()) { - System.out.println(stringIterator.next()); - } - - System.out.println("======entrySet======="); - for (Map.Entry entry : map.entrySet()) { - System.out.println(entry); - } - } -} -``` - -# 4 源码分析 -```java - //默认的初始容量16,且实际容量是2的整数幂 - static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; - - //最大容量(传入容量过大将被这个值替换) - static final int MAXIMUM_CAPACITY = 1 << 30; - - // 默认加载因子为0.75(当表达到3/4满时,才会再散列),这个因子在时间和空间代价之间达到了平衡.更高的因子可以降低表所需的空间,但是会增加查找代价,而查找是最频繁操作 - static final float DEFAULT_LOAD_FACTOR = 0.75f; - - //桶的树化阈值:即 链表转成红黑树的阈值,在存储数据时,当链表长度 >= 8时,则将链表转换成红黑树 - static final int TREEIFY_THRESHOLD = 8; - // 桶的链表还原阈值:即 红黑树转为链表的阈值,当在扩容(resize())时(HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 <= 6时,则将 红黑树转换成链表 - static final int UNTREEIFY_THRESHOLD = 6; - //最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表 (即 将链表 转换成红黑树) -``` -因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要 -链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短 - -还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换 -假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。 -``` - // 为了避免扩容/树形化选择的冲突,这个值不能小于 4 * TREEIFY_THRESHOLD - // 小于该值时使用的是扩容哦!!! - static final int MIN_TREEIFY_CAPACITY = 64; - - // 存储数据的Node数组,长度是2的幂. - // HashMap采用链表法解决冲突,每一个Node本质上是一个单向链表 - //HashMap底层存储的数据结构,是一个Node数组.上面得知Node类为元素维护了一个单向链表.至此,HashMap存储的数据结构也就很清晰了:维护了一个数组,每个数组又维护了一个单向链表.之所以这么设计,考虑到遇到哈希冲突的时候,同index的value值就用单向链表来维护 - //与 JDK 1.7 的对比(Entry类),仅仅只是换了名字 - transient Node[] table; - - // HashMap的底层数组中已用槽的数量 - transient int size; - // HashMap的阈值,用于判断是否需要调整HashMap的容量(threshold = 容量*加载因子) - int threshold; - - // 负载因子实际大小 - final float loadFactor; - - // HashMap被改变的次数 - transient int modCount; - - // 指定“容量大小”和“加载因子”的构造函数,是最基础的构造函数 - public HashMap(int initialCapacity, float loadFactor) { - if (initialCapacity < 0) - throw new IllegalArgumentException("Illegal initial capacity: " + - initialCapacity); - // HashMap的最大容量只能是MAXIMUM_CAPACITY - if (initialCapacity > MAXIMUM_CAPACITY) - initialCapacity = MAXIMUM_CAPACITY; - //负载因子须大于0 - if (loadFactor <= 0 || Float.isNaN(loadFactor)) - throw new IllegalArgumentException("Illegal load factor: " + - loadFactor); - // 设置"负载因子" - this.loadFactor = loadFactor; - // 设置"HashMap阈值",当HashMap中存储数据的数量达到threshold时,就需将HashMap的容量加倍 - this.threshold = tableSizeFor(initialCapacity); - } -``` - -- 上面的tableSizeFor有何用? -tableSizeFor方法保证函数返回值是大于等于给定参数initialCapacity最小的2的幂次方的数值 -``` - static final int tableSizeFor(int cap) { - int n = cap - 1; - n |= n >>> 1; - n |= n >>> 2; - n |= n >>> 4; - n |= n >>> 8; - n |= n >>> 16; - return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; - } -``` - 可以看出该方法是一系列的二进制位操作 - ->a |= b 等同于 a = a|b - -逐行分析 -- `int n = cap - 1` -给定的cap 减 1,为了避免参数cap本来就是2的幂次方,这样一来,经过后续操作,cap将会变成2 * cap,是不符合我们预期的 - -- `n |= n >>> 1` -n >>> 1 : n无符号右移1位,即n二进制最高位的1右移一位 -n | (n >>> 1) 导致 n二进制的高2位值为1 -目前n的高1~2位均为1 -- `n |= n >>> 2` -n继续无符号右移2位 -n | (n >>> 2) 导致n二进制表示的高3~4位经过运算值均为1 -目前n的高1~4位均为1 -- `n |= n >>> 4` -n继续无符号右移4位 -n | (n >>> 4) 导致n二进制表示的高5~8位经过运算值均为1 -目前n的高1~8位均为1 -- `n |= n >>> 8` -n继续无符号右移8位 -n | (n >>> 8) 导致n二进制表示的高9~16位经过运算值均为1 -目前n的高1~16位均为1 - -可以看出,无论给定cap(cap < MAXIMUM_CAPACITY )的值是多少,经过以上运算,其值的二进制所有位都会是1.再将其加1,这时候这个值一定是2的幂次方. -当然如果经过运算值大于MAXIMUM_CAPACITY,直接选用MAXIMUM_CAPACITY. -![例子](https://img-blog.csdnimg.cn/img_convert/e58fef4c158d1c978777f5aa40ebee5e.png) -至此tableSizeFor如何保证cap为2的幂次方已经显而易见了,那么问题来了 - -## 4.1 **为什么cap要保持为2的幂次方?** -主要与HashMap中的数据存储有关. - -在Java8中,HashMap中key的Hash值由Hash(key)方法计得 -![](https://img-blog.csdnimg.cn/img_convert/0882f5d36b225a33c5e17666f5fb6695.png) - -HashMap中存储数据table的index是由key的Hash值决定的. -在HashMap存储数据时,我们期望数据能均匀分布,以防止哈希冲突. -自然而然我们就会想到去用`%`取余操作来实现我们这一构想 - ->取余(%)操作 : 如果除数是2的幂次则等价于与其除数减一的与(&)操作. - -这也就解释了为什么一定要求cap要为2的幂次方.再来看看table的index的计算规则: -![](https://img-blog.csdnimg.cn/img_convert/d8df9e11f3a143218a08f515ba6e805e.png) - 等价于: -``` - index = e.hash % newCap -``` -采用二进制位操作&,相对于%,能够提高运算效率,这就是cap的值被要求为2幂次的原因 -![](https://img-blog.csdnimg.cn/img_convert/6fcd40c3f37371a2a9ebb2a209f3be58.png) -![数据结构 & 参数与 JDK 7 / 8](https://img-blog.csdnimg.cn/img_convert/3d946e0e1bd86c8ae41c1d6857377445.png) -## 4.2 **Node类** - -``` -static class Node implements Map.Entry { - final int hash; - final K key; - V value; - Node next; - - Node(int hash, K key, V value, Node next) { - this.hash = hash; - this.key = key; - this.value = value; - this.next = next; - } - - public final K getKey() { return key; } - public final V getValue() { return value; } - public final String toString() { return key + "=" + value; } - - public final int hashCode() { - return Objects.hashCode(key) ^ Objects.hashCode(value); - } - - public final V setValue(V newValue) { - V oldValue = value; - value = newValue; - return oldValue; - } - - public final boolean equals(Object o) { - if (o == this) - return true; - if (o instanceof Map.Entry) { - Map.Entry e = (Map.Entry)o; - if (Objects.equals(key, e.getKey()) && - Objects.equals(value, e.getValue())) - return true; - } - return false; - } - } -``` -Node 类是HashMap中的静态内部类,实现Map.Entry接口.定义了key键、value值、next节点,也就是说元素之间构成了单向链表. - -## 4.3 TreeNode -``` -static final class TreeNode extends LinkedHashMap.Entry { - TreeNode parent; // red-black tree links - TreeNode left; - TreeNode right; - TreeNode prev; // needed to unlink next upon deletion - boolean red; - TreeNode(int hash, K key, V val, Node next) {} - - // 返回当前节点的根节点 - final TreeNode root() { - for (TreeNode r = this, p;;) { - if ((p = r.parent) == null) - return r; - r = p; - } - } - } -``` -红黑树结构包含前、后、左、右节点,以及标志是否为红黑树的字段 -此结构是Java8新加的 - -## 4.4 hash方法 -Java 8中的散列值优化函数 -![](https://img-blog.csdnimg.cn/img_convert/5a02fb83605c5435e8585b2c37175e69.png) -只做一次16位右位移异或 -key.hashCode()函数调用的是key键值类型自带的哈希函数,返回int型散列值 - -理论上散列值是一个int型,如果直接拿散列值作为下标访问HashMap主数组的话,考虑到2进制32位带符号的int范围大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。 -但问题是一个40亿长度的数组,内存是放不下的.HashMap扩容之前的数组初始大小才16,所以这个散列值是不能直接拿来用的. -用之前还要先做对数组的长度取模运算,得到的余数才能用来访问数组下标 -源码中模运算就是把散列值和数组长度做一个"与"操作, -![](https://img-blog.csdnimg.cn/img_convert/d92c8b227a759d2c17736bc2b6403d57.png) -这也正好解释了为什么HashMap的数组长度要取2的整次幂 -因为这样(数组长度-1)正好相当于一个“低位掩码” -“与”操作的结果就是散列值的高位全部归零,只保留低位值,用来做数组下标访问 - -以初始长度16为例,16-1=15 -2进制表示是00000000 00000000 00001111 -和某散列值做“与”操作如下,结果就是截取了最低的四位值 -![](https://img-blog.csdnimg.cn/img_convert/553e83380e5587fd4e40724b373d79e4.png) -但这时候问题就来了,这样就算我的散列值分布再松散,要是只取最后几位的话,碰撞也会很严重 - -这时候“扰动函数”的价值就体现出来了 -![](https://img-blog.csdnimg.cn/img_convert/b2051fb82b033621ca8d154861ff5c15.png) -右位移16位,正好是32位一半,自己的高半区和低半区做异或,就是为了混合原始hashCode的高位和低位,以此来加大低位的随机性 -而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相保留下来。 - -index的运算规则是 -``` -e.hash & (newCap - 1) -``` -newCap是2的幂,所以newCap - 1的高位全0 - -若e.hash值只用自身的hashcode,index只会和e.hash的低位做&操作.这样一来,index的值就只有低位参与运算,高位毫无存在感,从而会带来哈希冲突的风险 -所以在计算key的hashCode时,用其自身hashCode与其低16位做异或操作 -这也就让高位参与到index的计算中来了,即降低了哈希冲突的风险又不会带来太大的性能问题 - -## 4.5 Put方法 -![](https://img-blog.csdnimg.cn/img_convert/da29710b339ad70260610e2ed358305f.png) - -![](https://img-blog.csdnimg.cn/img_convert/977877dab11a1fcd23dffadecbe6b2cd.png) - -![HashMap-put(k,v)](https://img-blog.csdnimg.cn/img_convert/0df25cf27e264797c7f1451e11c927f1.png) -①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容 - -②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③ - -③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals - -④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤ - -⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可 - -⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,执行resize()扩容 -``` - public V put(K key, V value) { - // 对key的hashCode()做hash - return putVal(hash(key), key, value, false, true); - } - -final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { - Node[] tab; Node p; int n, i; - // 步骤① tab为空则调用resize()初始化创建 - if ((tab = table) == null || (n = tab.length) == 0) - n = (tab = resize()).length; - // 步骤② 计算index,并对null做处理 - //tab[i = (n - 1) & hash对应下标的第一个节点 - if ((p = tab[i = (n - 1) & hash]) == null) - // 无哈希冲突的情况下,将value直接封装为Node并赋值 - tab[i] = newNode(hash, key, value, null); - else { - Node e; K k; - // 步骤③ 节点的key相同,直接覆盖节点 - if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) - e = p; - // 步骤④ 判断该链为红黑树 - else if (p instanceof TreeNode) - // p是红黑树类型,则调用putTreeVal方式赋值 - e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); - // 步骤⑤ p非红黑树类型,该链为链表 - else { - // index 相同的情况下 - for (int binCount = 0; ; ++binCount) { - if ((e = p.next) == null) { - // 如果p的next为空,将新的value值添加至链表后面 - p.next = newNode(hash, key, value, null); - if (binCount >= TREEIFY_THRESHOLD - 1) - // 如果链表长度大于8,链表转化为红黑树,执行插入 - treeifyBin(tab, hash); - break; - } - // key相同则跳出循环 - if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) - break; - //就是移动指针方便继续取 p.next - - p = e; - } - } - if (e != null) { // existing mapping for key - V oldValue = e.value; - //根据规则选择是否覆盖value - if (!onlyIfAbsent || oldValue == null) - e.value = value; - afterNodeAccess(e); - return oldValue; - } - } - ++modCount; - // 步骤⑥:超过最大容量,就扩容 - if (++size > threshold) - // size大于加载因子,扩容 - resize(); - afterNodeInsertion(evict); - return null; - } -``` -在构造函数中最多也只是设置了initialCapacity、loadFactor的值,并没有初始化table,table的初始化工作是在put方法中进行的. -## 4.6 resize -![](https://img-blog.csdnimg.cn/img_convert/24a22ebcda082c0c13df1c2193ee18d6.png) -扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,内部的数组无法装载更多的元素时,就需要扩大数组的长度. -当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组 -``` - /** - * 该函数有2种使用情况:1.初始化哈希表 2.当前数组容量过小,需扩容 - */ -final Node[] resize() { - Node[] oldTab = table; - int oldCap = (oldTab == null) ? 0 : oldTab.length; - int oldThr = threshold; - int newCap, newThr = 0; - - // 针对情况2:若扩容前的数组容量超过最大值,则不再扩充 - if (oldCap > 0) { - if (oldCap >= MAXIMUM_CAPACITY) { - threshold = Integer.MAX_VALUE; - return oldTab; - } - // 针对情况2:若无超过最大值,就扩充为原来的2倍 - else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && - oldCap >= DEFAULT_INITIAL_CAPACITY) - //newCap设置为oldCap的2倍并小于MAXIMUM_CAPACITY,且大于默认值, 新的threshold增加为原来的2倍 - newThr = oldThr << 1; // double threshold - } - - // 针对情况1:初始化哈希表(采用指定 or 默认值) - else if (oldThr > 0) // initial capacity was placed in threshold - // threshold>0, 将threshold设置为newCap,所以要用tableSizeFor方法保证threshold是2的幂次方 - newCap = oldThr; - else { // zero initial threshold signifies using defaults - // 默认初始化 - newCap = DEFAULT_INITIAL_CAPACITY; - newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); - } - - // 计算新的resize上限 - if (newThr == 0) { - // newThr为0,newThr = newCap * 0.75 - float ft = (float)newCap * loadFactor; - newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? - (int)ft : Integer.MAX_VALUE); - } - threshold = newThr; - @SuppressWarnings({"rawtypes","unchecked"}) - // 新生成一个table数组 - Node[] newTab = (Node[])new Node[newCap]; - table = newTab; - if (oldTab != null) { - // oldTab 复制到 newTab - for (int j = 0; j < oldCap; ++j) { - Node e; - if ((e = oldTab[j]) != null) { - oldTab[j] = null; - if (e.next == null) - // 链表只有一个节点,直接赋值 - //为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。 - newTab[e.hash & (newCap - 1)] = e; - else if (e instanceof TreeNode) - // e为红黑树的情况 - ((TreeNode)e).split(this, newTab, j, oldCap); - else { // preserve order链表优化重hash的代码块 - Node loHead = null, loTail = null; - Node hiHead = null, hiTail = null; - Node next; - do { - next = e.next; - // 原索引 - if ((e.hash & oldCap) == 0) { - if (loTail == null) - loHead = e; - else - loTail.next = e; - loTail = e; - } - // 原索引 + oldCap - else { - if (hiTail == null) - hiHead = e; - else - hiTail.next = e; - hiTail = e; - } - } while ((e = next) != null); - // 原索引放到bucket里 - if (loTail != null) { - loTail.next = null; - newTab[j] = loHead; - } - // 原索引+oldCap放到bucket里 - if (hiTail != null) { - hiTail.next = null; - newTab[j + oldCap] = hiHead; - } - } - } - } - } - return newTab; - } -``` -![图片发自简书App](https://img-blog.csdnimg.cn/img_convert/dd74408150f2d30938f08296a6501d71.png) - - - -## 4.7 remove方法 -remove(key) 方法 和 remove(key, value) 方法都是通过调用removeNode的方法来实现删除元素的 -```java - final Node removeNode(int hash, Object key, Object value, - boolean matchValue, boolean movable) { - Node[] tab; Node p; int n, index; - if ((tab = table) != null && (n = tab.length) > 0 && - (p = tab[index = (n - 1) & hash]) != null) { - Node node = null, e; K k; V v; - if (p.hash == hash && - ((k = p.key) == key || (key != null && key.equals(k)))) - // index 元素只有一个元素 - node = p; - else if ((e = p.next) != null) { - if (p instanceof TreeNode) - // index处是一个红黑树 - node = ((TreeNode)p).getTreeNode(hash, key); - else { - // index处是一个链表,遍历链表返回node - do { - if (e.hash == hash && - ((k = e.key) == key || - (key != null && key.equals(k)))) { - node = e; - break; - } - p = e; - } while ((e = e.next) != null); - } - } - // 分不同情形删除节点 - if (node != null && (!matchValue || (v = node.value) == value || - (value != null && value.equals(v)))) { - if (node instanceof TreeNode) - ((TreeNode)node).removeTreeNode(this, tab, movable); - else if (node == p) - tab[index] = node.next; - else - p.next = node.next; - ++modCount; - --size; - afterNodeRemoval(node); - return node; - } - } - return null; - } -``` -## 4.8 get -```java -/** - * 函数原型 - * 作用:根据键key,向HashMap获取对应的值 - */ - map.get(key); - - - /** - * 源码分析 - */ - public V get(Object key) { - Node e; - // 1. 计算需获取数据的hash值 - // 2. 通过getNode()获取所查询的数据 ->>分析1 - // 3. 获取后,判断数据是否为空 - return (e = getNode(hash(key), key)) == null ? null : e.value; -} - -/** - * 分析1:getNode(hash(key), key)) - */ -final Node getNode(int hash, Object key) { - Node[] tab; Node first, e; int n; K k; - - // 1. 计算存放在数组table中的位置 - if ((tab = table) != null && (n = tab.length) > 0 && - (first = tab[(n - 1) & hash]) != null) { - - // 4. 通过该函数,依次在数组、红黑树、链表中查找(通过equals()判断) - // a. 先在数组中找,若存在,则直接返回 - if (first.hash == hash && // always check first node - ((k = first.key) == key || (key != null && key.equals(k)))) - return first; - - // b. 若数组中没有,则到红黑树中寻找 - if ((e = first.next) != null) { - // 在树中get - if (first instanceof TreeNode) - return ((TreeNode)first).getTreeNode(hash, key); - - // c. 若红黑树中也没有,则通过遍历,到链表中寻找 - do { - if (e.hash == hash && - ((k = e.key) == key || (key != null && key.equals(k)))) - return e; - } while ((e = e.next) != null); - } - } - return null; -} -``` -> 在JDK1.7及以前的版本中,HashMap里是没有红黑树的实现的,在JDK1.8中加入了红黑树是为了防止哈希表碰撞攻击,当链表链长度为8时,及时转成红黑树,提高map的效率 - -如果某个桶中的记录过大的话(当前是TREEIFY_THRESHOLD = 8),HashMap会动态的使用一个专门的treemap实现来替换掉它。这样做的结果会更好,是O(logn),而不是糟糕的O(n)。它是如何工作的? -前面产生冲突的那些KEY对应的记录只是简单的追加到一个链表后面,这些记录只能通过遍历来进行查找。但是超过这个阈值后HashMap开始将列表升级成一个二叉树,使用哈希值作为树的分支变量,如果两个哈希值不等,但指向同一个桶的话,较大的那个会插入到右子树里。如果哈希值相等,HashMap希望key值最好是实现了Comparable接口的,这样它可以按照顺序来进行插入。这对HashMap的key来说并不是必须的,不过如果实现了当然最好。如果没有实现这个接口,在出现严重的哈希碰撞的时候,你就并别指望能获得性能提升了。 - -这个性能提升有什么用处?比方说恶意的程序,如果它知道我们用的是哈希算法,它可能会发送大量的请求,导致产生严重的哈希碰撞。然后不停的访问这些key就能显著的影响服务器的性能,这样就形成了一次拒绝服务攻击(DoS)。JDK 8中从O(n)到O(logn)的飞跃,可以有效地防止类似的攻击,同时也让HashMap性能的可预测性稍微增强了一些 -```java -/** - * 源码分析:resize(2 * table.length) - * 作用:当容量不足时(容量 > 阈值),则扩容(扩到2倍) - */ - void resize(int newCapacity) { - - // 1. 保存旧数组(old table) - Entry[] oldTable = table; - - // 2. 保存旧容量(old capacity ),即数组长度 - int oldCapacity = oldTable.length; - - // 3. 若旧容量已经是系统默认最大容量了,那么将阈值设置成整型的最大值,退出 - if (oldCapacity == MAXIMUM_CAPACITY) { - threshold = Integer.MAX_VALUE; - return; - } - - // 4. 根据新容量(2倍容量)新建1个数组,即新table - Entry[] newTable = new Entry[newCapacity]; - - // 5. (重点分析)将旧数组上的数据(键值对)转移到新table中,从而完成扩容 ->>分析1.1 - transfer(newTable); - - // 6. 新数组table引用到HashMap的table属性上 - table = newTable; - - // 7. 重新设置阈值 - threshold = (int)(newCapacity * loadFactor); -} - - /** - * 分析1.1:transfer(newTable); - * 作用:将旧数组上的数据(键值对)转移到新table中,从而完成扩容 - * 过程:按旧链表的正序遍历链表、在新链表的头部依次插入 - */ -void transfer(Entry[] newTable) { - // 1. src引用了旧数组 - Entry[] src = table; - - // 2. 获取新数组的大小 = 获取新容量大小 - int newCapacity = newTable.length; - - // 3. 通过遍历 旧数组,将旧数组上的数据(键值对)转移到新数组中 - for (int j = 0; j < src.length; j++) { - // 3.1 取得旧数组的每个元素 - Entry e = src[j]; - if (e != null) { - // 3.2 释放旧数组的对象引用(for循环后,旧数组不再引用任何对象) - src[j] = null; - - do { - // 3.3 遍历 以该数组元素为首 的链表 - // 注:转移链表时,因是单链表,故要保存下1个结点,否则转移后链表会断开 - Entry next = e.next; - // 3.3 重新计算每个元素的存储位置 - int i = indexFor(e.hash, newCapacity); - // 3.4 将元素放在数组上:采用单链表的头插入方式 = 在链表头上存放数据 = 将数组位置的原有数据放在后1个指针、将需放入的数据放到数组位置中 - // 即 扩容后,可能出现逆序:按旧链表的正序遍历链表、在新链表的头部依次插入 - e.next = newTable[i]; - newTable[i] = e; - // 访问下1个Entry链上的元素,如此不断循环,直到遍历完该链表上的所有节点 - e = next; - } while (e != null); - // 如此不断循环,直到遍历完数组上的所有数据元素 - } - } - } -``` -从上面可看出:在扩容resize()过程中,在将旧数组上的数据 转移到 新数组上时,转移数据操作 = 按旧链表的正序遍历链表、在新链表的头部依次插入,即在转移数据、扩容后,容易出现链表逆序的情况 - ->`设重新计算存储位置后不变,即扩容前 = 1->2->3,扩容后 = 3->2->1` - -此时若并发执行 put 操作,一旦出现扩容情况,则 容易出现 环形链表,从而在获取数据、遍历链表时 形成死循环(Infinite Loop),即死锁 -![](https://img-blog.csdnimg.cn/img_convert/cd3bbd816bcefb360280b591d5cf41cf.png) -![image.png](https://img-blog.csdnimg.cn/img_convert/cb6354c50e5af1ef24c1ab36b802217e.png) -![](https://img-blog.csdnimg.cn/img_convert/5e35431f5f3c179e389f5528e1b03d42.png) -![为什么 HashMap 中 String、Integer 这样的包装类适合作为 key 键](https://img-blog.csdnimg.cn/img_convert/38321f907a385a91616ff972204237d4.png) -## 4.9 getOrDefault -getOrDefault() 方法获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值。 -![](https://img-blog.csdnimg.cn/55e1bdebe6cd4fc9aaa22d6662c501e4.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBASmF2YUVkZ2Uu,size_20,color_FFFFFF,t_70,g_se,x_16) -# 5 单线程rehash -单线程情况下,rehash无问题 -[![HashMap rehash single thread](https://img-blog.csdnimg.cn/img_convert/50437495a5eda75989d04de58316b1f7.png)](http://www.jasongj.com/img/java/concurrenthashmap/single_thread_rehash.png) -# 6 多线程并发下的rehash - -这里假设有两个线程同时执行了put操作并引发了rehash,执行了transfer方法,并假设线程一进入transfer方法并执行完next = e.next后,因为线程调度所分配时间片用完而“暂停”,此时线程二完成了transfer方法的执行。此时状态如下。 - -[![HashMap rehash multi thread step 1](https://img-blog.csdnimg.cn/img_convert/a5aaefdb773217a54d29cbe9809e2aa9.png)](http://www.jasongj.com/img/java/concurrenthashmap/multi_thread_rehash_1.png) -接着线程1被唤醒,继续执行第一轮循环的剩余部分 -``` -e.next = newTable[1] = null -newTable[1] = e = key(5) -e = next = key(9) -``` -结果如下图所示 -[![HashMap rehash multi thread step 2](https://img-blog.csdnimg.cn/img_convert/a261863e3e27077ba7b3223a3914f0de.png)](http://www.jasongj.com/img/java/concurrenthashmap/multi_thread_rehash_2.png) - -接着执行下一轮循环,结果状态图如下所示 -[![HashMap rehash multi thread step 3](https://img-blog.csdnimg.cn/img_convert/03cea3cdb5a9477ca25e98ee6f37cf43.png)](http://www.jasongj.com/img/java/concurrenthashmap/multi_thread_rehash_3.png) - -继续下一轮循环,结果状态图如下所示 -[![HashMap rehash multi thread step 4](https://img-blog.csdnimg.cn/img_convert/22e59112a7635a44dff4fa42a7e6a840.png)](http://www.jasongj.com/img/java/concurrenthashmap/multi_thread_rehash_4.png) - -此时循环链表形成,并且key(11)无法加入到线程1的新数组。在下一次访问该链表时会出现死循环。 -# 7 Fast-fail -## 产生原因 - -在使用迭代器的过程中如果HashMap被修改,那么`ConcurrentModificationException`将被抛出,也即Fast-fail策略。 - -当HashMap的iterator()方法被调用时,会构造并返回一个新的EntryIterator对象,并将EntryIterator的expectedModCount设置为HashMap的modCount(该变量记录了HashMap被修改的次数)。 -``` -HashIterator() { - expectedModCount = modCount; - if (size > 0) { // advance to first entry - Entry[] t = table; - while (index < t.length && (next = t[index++]) == null) - ; - } -} -``` - - -在通过该Iterator的next方法访问下一个Entry时,它会先检查自己的expectedModCount与HashMap的modCount是否相等,如果不相等,说明HashMap被修改,直接抛出`ConcurrentModificationException`。该Iterator的remove方法也会做类似的检查。该异常的抛出意在提醒用户及早意识到线程安全问题。 - -## 线程安全解决方案 -单线程条件下,为避免出现`ConcurrentModificationException`,需要保证只通过HashMap本身或者只通过Iterator去修改数据,不能在Iterator使用结束之前使用HashMap本身的方法修改数据。因为通过Iterator删除数据时,HashMap的modCount和Iterator的expectedModCount都会自增,不影响二者的相等性。如果是增加数据,只能通过HashMap本身的方法完成,此时如果要继续遍历数据,需要重新调用iterator()方法从而重新构造出一个新的Iterator,使得新Iterator的expectedModCount与更新后的HashMap的modCount相等。 - -多线程条件下,可使用`Collections.synchronizedMap`方法构造出一个同步Map,或者直接使用线程安全的ConcurrentHashMap。 \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ReentrantLock\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ReentrantLock\346\272\220\347\240\201\350\247\243\346\236\220.md" index ad32141823..a1b5860985 100644 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ReentrantLock\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ReentrantLock\346\272\220\347\240\201\350\247\243\346\236\220.md" @@ -74,79 +74,90 @@ ReentrantLock 就负责实现这些接口,使用时,直接调用的也是这 ## 4.2 FairSync - 公平锁 只实现 *lock* 和 *tryAcquire* 两个方法 ### 4.2.1 lock -lock 方法加锁成功,直接返回,所以可以继续执行业务逻辑。 - 公平模式的 lock ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzJhNGEyZmViNzY?x-oss-process=image/format,png) + 直接调用 acquire,而没有像非公平模式先试图获取,因为这样可能导致违反“公平”的语义:在已等待在队列中的线程之前获取了锁。 -*acquire* 是 AQS 的方法,表示先尝试获得锁,失败之后进入同步队列阻塞等待。 +*acquire* 是 AQS 的方法,表示先尝试获得锁,失败之后进入同步队列阻塞等待,详情见本专栏的上一文 ### 4.2.2 tryAcquire +公平模式的 *tryAcquire*。不要授予访问权限,除非递归调用或没有等待线程或是第一个调用的。 - 该方法是 AQS 在 acquire 方法中留给子类去具体实现的 -![](https://img-blog.csdnimg.cn/20210705225313380.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -#### 公平模式 -不要授予访问权限,除非递归调用或没有等待线程或是第一个调用的。 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzJhMjJmYmVkZWM?x-oss-process=image/format,png) + +话不多说,看源码: ```java protected final boolean tryAcquire(int acquires) { - // 获取当前的线程 + // 获取当前的线程 final Thread current = Thread.currentThread(); - // 获取 state 锁的状态(volatile 读语义) + // 获取 state 锁的状态 int c = getState(); // state == 0 => 尚无线程获取锁 if (c == 0) { - // 判断 AQS 的同步对列里是否有线程等待 + // 判断 AQS 的同步对列里是否有线程等待,若没有则直接 CAS 获取锁 if (!hasQueuedPredecessors() && - // 若没有则直接 CAS(保证原子性,线程安全) 获取锁 compareAndSetState(0, acquires)) { // 获取锁成功,设置独占线程 setExclusiveOwnerThread(current); return true; } } - // 已经获取锁的是否为当前的线程? + // 判断已经获取锁是否为当前的线程 else if (current == getExclusiveOwnerThread()) { // 锁的重入, 即 state 加 1 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); - // 已经获取 lock,所以这里不考虑并发 setState(nextc); return true; } return false; } ``` -和Sync#nonfairTryAcquire类似,唯一不同的是当发现锁未被占用时,使用 **hasQueuedPredecessors** 确保了公平性。 +和 Sync 的 *nonfairTryAcquire* 方法实现类似,唯一不同的是当发现锁未被占用时,使用 *hasQueuedPredecessors* 确保了公平性。 #### hasQueuedPredecessors -判断当前线程是不是属于同步队列的头节点的下一个节点(头节点是释放锁的节点): -- 是(返回false),符合FIFO,可以获得锁 -- 不是(返回true),则继续等待 +会判断当前线程是不是属于同步队列的头节点的下一个节点(头节点是释放锁的节点) +- 如果是(返回false),符合FIFO,可以获得锁 +- 如果不是(返回true),则继续等待 ```java -public final boolean hasQueuedPredecessors() { - // 这种方法的正确性取决于头在尾之前初始化和头初始化。如果当前线程是队列中的第一个线程,则next是精确的 - Node t = tail; // 按反初始化顺序读取字段 - Node h = head; - Node s; - return h != t && - ((s = h.next) == null || s.thread != Thread.currentThread()); -} + public final boolean hasQueuedPredecessors() { + // 这种方法的正确性取决于头在尾之前初始化和头初始化。如果当前线程是队列中的第一个线程,则next是精确的 + Node t = tail; // 按反初始化顺序读取字段 + Node h = head; + Node s; + return h != t && + ((s = h.next) == null || s.thread != Thread.currentThread()); + } ``` + + + # 5 nonfairTryAcquire -执行非公平的 *tryLock*。 *tryAcquire* 是在子类中实现的,但是都需要对*trylock* 方法进行非公平的尝试。 +执行非公平的 *tryLock*。 +*tryAcquire* 是在子类中实现的,但是都需要对*trylock* 方法进行非公平的尝试。 + ```java final boolean nonfairTryAcquire(int acquires) { + // 获取当前的线程 final Thread current = Thread.currentThread(); + // 获取 AQS 中的 state 字段 int c = getState(); + // state 为 0,表示同步器的锁尚未被持有 if (c == 0) { - // 这里可能有竞争,所以可能失败 + // CAS state 获取锁(这里可能有竞争,所以可能失败) if (compareAndSetState(0, acquires)) { // 获取锁成功, 设置获取独占锁的线程 setExclusiveOwnerThread(current); + // 直接返回 true return true; } } + // 判断现在获取独占锁的线程是否为当前线程(可重入锁的体现) else if (current == getExclusiveOwnerThread()) { + // state 计数加1(重入获取锁) int nextc = c + acquires; - if (nextc < 0) - throw new Error("Maximum lock count exceeded"); + if (nextc < 0) // 整型溢出 + throw new Error("Maximum lock count exceeded"); + // 已经获取 lock,所以这里不考虑并发 setState(nextc); return true; } @@ -154,7 +165,6 @@ final boolean nonfairTryAcquire(int acquires) { } ``` 无参的 *tryLock* 调用的就是此方法 - # 6 tryLock ## 6.1 无参 Lock 接口中定义的方法。 @@ -166,6 +176,19 @@ Lock 接口中定义的方法。 如果当前线程已经持有该锁,那么持有计数将增加1,方法返回true。 如果锁被另一个线程持有,那么这个方法将立即返回值false。 +- 典型的使用方法 +```java + Lock lock = ...; + if (lock.tryLock()) { + try { + // manipulate protected state + } finally { + lock.unlock(); + } + } else { + // 执行可选的操作 + } +``` ## 6.2 有参 - 提供了超时时间的入参,在时间内,仍没有得到锁,会返回 false ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzE3LzE3MjIxYzJhMzZhNTAwMzQ?x-oss-process=image/format,png) @@ -191,4 +214,6 @@ protected final boolean tryRelease(int releases) { setState(c); return free; } -``` \ No newline at end of file +``` +# 8 总结 +AQS 搭建了整个锁架构,子类锁的实现只需要根据场景,实现 AQS 对应的方法即可。 \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ThreadLocal.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ThreadLocal.md" deleted file mode 100644 index 5f5a4c37b5..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/ThreadLocal.md" +++ /dev/null @@ -1,246 +0,0 @@ - -# 1 前言 - -此类提供线程本地变量,与普通变量不同,因为每个访问一个变量(通过其get或set方法)的线程都有其自己的,独立初始化的变量副本。 -ThreadLocal 实例通常是期望将状态与线程(例如,用户ID或事务ID)关联的类中的 private static 字段。 - -例如,下面的类生成每个线程本地的唯一标识符。线程的ID是在第一次调用ThreadId.get() 时赋值的,并且在以后的调用中保持不变。 - -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzcvMTcxZWZhYzdiMDdiMDMzNw?x-oss-process=image/format,png) - -只要线程是活跃的并且 ThreadLocal 实例是可访问的,则每个线程都对其线程本地变量的副本持有隐式的引用。线程消失后,线程本地实例的所有副本都会被 GC(除非存在对这些副本的其他引用)。 - -# 2 继续体系 -- 继承?不存在的,这其实也是 java.lang 包下的工具类,但是 ThreadLocal 定义带有泛型,说明可以储存任意格式的数据。 -![](https://img-blog.csdnimg.cn/20210615235535658.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - -# 3 属性 -ThreadLocal 依赖于附加到每个线程(Thread.threadLocals和InheritableThreadLocals)的线程线性探测哈希表。 - -## threadLocalHashCode -ThreadLocal 对象充当key,通过 threadLocalHashCode 进行搜索。这是一个自定义哈希码(仅在ThreadLocalMaps 中有用),它消除了在相同线程使用连续构造的threadlocal的常见情况下的冲突,而在不太常见的情况下仍然表现良好。 - -ThreadLocal 通过这样的 hashCode,计算当前 ThreadLocal 在 ThreadLocalMap 中的索引 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzcvMTcxZWZhYzdiNDcxM2Q2Mw?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzcvMTcxZWZhYzdhZjZjMDQwMg?x-oss-process=image/format,png) - -- 连续生成的哈希码之间的差值,该值的设定参考文章[ThreadLocal的hash算法(关于 0x61c88647)](https://juejin.im/post/5cced289f265da03804380f2) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzcvMTcxZWZhYzdiNmUwNjA0NQ?x-oss-process=image/format,png) - -- 注意 static 修饰。ThreadLocalMap 会被 set 多个 ThreadLocal ,而多个 ThreadLocal 就根据 threadLocalHashCode 区分 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC81LzcvMTcxZWZhYzdkMTkzOWUwMw?x-oss-process=image/format,png) -# 4 ThreadLocalMap -自定义的哈希表,仅适用于维护线程本地的值。没有操作导出到ThreadLocal类之外。 -该类包私有,允许在 Thread 类中的字段声明。为帮助处理非常长的使用寿命,哈希表节点使用 WeakReferences 作为key。 -但由于不使用引用队列,因此仅在表空间不足时,才保证删除过时的节点。 -```java -static class ThreadLocalMap { - - /** - * 此哈希表中的节点使用其主引用字段作为key(始终是一个 ThreadLocal 对象),继承了 WeakReference。 - * 空键(即entry.get()== null)意味着不再引用该键,因此可以从表中删除该节点。 - * 在下面的代码中,此类节点称为 "stale entries" - */ - static class Entry extends WeakReference> { - /** 与此 ThreadLocal 关联的值 */ - Object value; - - Entry(ThreadLocal k, Object v) { - super(k); - value = v; - } - } - - private static final int INITIAL_CAPACITY = 16; - - private Entry[] table; - - private int size = 0; - - private int threshold; // 默认为 0 -``` -## 特点 -- key 是 ThreadLocal 的引用 -- value 是 ThreadLocal 保存的值 -- 数组的数据结构 -# 5 set -## 5.1 ThreadLocal#set -将此线程本地变量的当前线程副本设置为指定值。子类无需重写此方法,而仅依靠initialValue方法设置线程本地变量的值。 -![](https://img-blog.csdnimg.cn/20210616000550930.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - -### 执行流程 -1. 获取当前线程 -2. 获取线程所对应的ThreadLocalMap。每个线程都是独立的,所以该方法天然线程安全 -3. 判断 map 是否为 null - - 否,K.V 对赋值,k 为this(即当前的 ThreaLocal 对象) - - 是,初始化一个 ThreadLocalMap 来维护 K.V 对 - -来具体看看ThreadLocalMap中的 set - -## 5.2 ThreadLocalMap#set -```java -private void set(ThreadLocal key, Object value) { - // 新引用指向 table - Entry[] tab = table; - int len = tab.length; - // 获取对应 ThreadLocal 在table 中的索引 - int i = key.threadLocalHashCode & (len-1); - - /** - * 从该下标开始循环遍历 - * 1、如遇相同key,则直接替换value - * 2、如果该key已经被回收失效,则替换该失效的key - */ - for (Entry e = tab[i]; - e != null; - e = tab[i = nextIndex(i, len)]) { - ThreadLocal k = e.get(); - // 找到内存地址一样的 ThreadLocal,直接替换 - if (k == key) { - e.value = value; - return; - } - // 若 k 为 null,说明 ThreadLocal 被清理了,则替换当前失效的 k - if (k == null) { - replaceStaleEntry(key, value, i); - return; - } - } - // 找到空位,创建节点并插入 - tab[i] = new Entry(key, value); - // table内元素size自增 - int sz = ++size; - // 达到阈值(数组大小的三分之二)时,执行扩容 - if (!cleanSomeSlots(i, sz) && sz >= threshold) - rehash(); -} -``` -注意通过 hashCode 计算的索引位置 i 处如果已经有值了,会从 i 开始,通过 +1 不断的往后寻找,直到找到索引位置为空的地方,把当前 ThreadLocal 作为 key 放进去。 - -# 6 get -```java -public T get() { - // 获取当前线程 - Thread t = Thread.currentThread(); - // 获取当前线程对应的ThreadLocalMap - ThreadLocalMap map = getMap(t); - - // 如果map不为空 - if (map != null) { - // 取得当前ThreadLocal对象对应的Entry - ThreadLocalMap.Entry e = map.getEntry(this); - // 如果不为空,读取当前 ThreadLocal 中保存的值 - if (e != null) { - @SuppressWarnings("unchecked") - T result = (T)e.value; - return result; - } - } - // 否则都执行 setInitialValue - return setInitialValue(); -} -``` -### setInitialValue -```java -private T setInitialValue() { - // 获取初始值,一般是子类重写 - T value = initialValue(); - - // 获取当前线程 - Thread t = Thread.currentThread(); - - // 获取当前线程对应的ThreadLocalMap - ThreadLocalMap map = getMap(t); - - // 如果map不为null - if (map != null) - - // 调用ThreadLocalMap的set方法进行赋值 - map.set(this, value); - - // 否则创建个ThreadLocalMap进行赋值 - else - createMap(t, value); - return value; -} -``` - -接着我们来看下 -## ThreadLocalMap#getEntry -```java -// 得到当前 thradLocal 对应的值,值的类型是由 thradLocal 的泛型决定的 -// 由于 thradLocalMap set 时解决数组索引位置冲突的逻辑,导致 thradLocalMap get 时的逻辑也是对应的 -// 首先尝试根据 hashcode 取模数组大小-1 = 索引位置 i 寻找,找不到的话,自旋把 i+1,直到找到索引位置不为空为止 -private Entry getEntry(ThreadLocal key) { - // 计算索引位置:ThreadLocal 的 hashCode 取模数组大小-1 - int i = key.threadLocalHashCode & (table.length - 1); - Entry e = table[i]; - // e 不为空,并且 e 的 ThreadLocal 的内存地址和 key 相同,直接返回,否则就是没有找到,继续通过 getEntryAfterMiss 方法找 - if (e != null && e.get() == key) - return e; - else - // 这个取数据的逻辑,是因为 set 时数组索引位置冲突造成的 - return getEntryAfterMiss(key, i, e); -} -// 自旋 i+1,直到找到为止 -private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { - Entry[] tab = table; - int len = tab.length; - // 在大量使用不同 key 的 ThreadLocal 时,其实还蛮耗性能的 - while (e != null) { - ThreadLocal k = e.get(); - // 内存地址一样,表示找到了 - if (k == key) - return e; - // 删除没用的 key - if (k == null) - expungeStaleEntry(i); - // 继续使索引位置 + 1 - else - i = nextIndex(i, len); - e = tab[i]; - } - return null; -} -``` -# 6 扩容 -ThreadLocalMap 中的 ThreadLocal 的个数超过阈值时,ThreadLocalMap 就要开始扩容了,我们一起来看下扩容的逻辑: -```java -private void resize() { - // 拿出旧的数组 - Entry[] oldTab = table; - int oldLen = oldTab.length; - // 新数组的大小为老数组的两倍 - int newLen = oldLen * 2; - // 初始化新数组 - Entry[] newTab = new Entry[newLen]; - int count = 0; - // 老数组的值拷贝到新数组上 - for (int j = 0; j < oldLen; ++j) { - Entry e = oldTab[j]; - if (e != null) { - ThreadLocal k = e.get(); - if (k == null) { - e.value = null; // Help the GC - } else { - // 计算 ThreadLocal 在新数组中的位置 - int h = k.threadLocalHashCode & (newLen - 1); - // 如果索引 h 的位置值不为空,往后+1,直到找到值为空的索引位置 - while (newTab[h] != null) - h = nextIndex(h, newLen); - // 给新数组赋值 - newTab[h] = e; - count++; - } - } - } - // 给新数组初始化下次扩容阈值,为数组长度的三分之二 - setThreshold(newLen); - size = count; - table = newTab; -} -``` -扩容时是绝对没有线程安全问题的,因为 ThreadLocalMap 是线程的一个属性,一个线程同一时刻只能对 ThreadLocalMap 进行操作,因为同一个线程执行业务逻辑必然是串行的,那么操作 ThreadLocalMap 必然也是串行的。 -# 7 总结 -我们在写中间件的时候经常会用到,比如说流程引擎中上下文的传递,调用链ID的传递等。 \ No newline at end of file diff --git "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/\343\200\220\346\255\273\347\243\225JDK\346\272\220\347\240\201\343\200\221ThreadPoolExecutor\346\272\220\347\240\201\344\277\235\345\247\206\347\272\247\350\257\246\350\247\243.md" "b/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/\343\200\220\346\255\273\347\243\225JDK\346\272\220\347\240\201\343\200\221ThreadPoolExecutor\346\272\220\347\240\201\344\277\235\345\247\206\347\272\247\350\257\246\350\247\243.md" deleted file mode 100644 index 6968385400..0000000000 --- "a/JDK/JDK\346\272\220\347\240\201\350\247\243\346\236\220/\343\200\220\346\255\273\347\243\225JDK\346\272\220\347\240\201\343\200\221ThreadPoolExecutor\346\272\220\347\240\201\344\277\235\345\247\206\347\272\247\350\257\246\350\247\243.md" +++ /dev/null @@ -1,686 +0,0 @@ -位运算表示线程池状态,因为位运算是改变当前值的一种高效手段。 - -# 属性 -## 线程池状态 -Integer 有32位: -- 最左边3位表示线程池状态,可表示从0至7的8个不同数值 -- 最右边29位表工作线程数 -```java -private static final int COUNT_BITS = Integer.SIZE - 3; -``` - - -线程池的状态用高3位表示,其中包括了符号位。五种状态的十进制值按从小到大依次排序为: - -```bash -RUNNING < SHUTDOWN < STOP < TIDYING =核心线程数 或线程创建失败,则将当前任务放到工作队列中 -// 只有线程池处于 RUNNING 态,才执行后半句 : 置入队列 -if (isRunning(c) && workQueue.offer(command)) { - int recheck = ctl.get(); - - // 只有线程池处于 RUNNING 态,才执行后半句 : 置入队列 - if (! isRunning(recheck) && remove(command)) - reject(command); - // 若之前的线程已被消费完,新建一个线程 - else if (workerCountOf(recheck) == 0) - addWorker(null, false); -// 核心线程和队列都已满,尝试创建一个新线程 -} -else if (!addWorker(command, false)) - // 抛出RejectedExecutionException异常 - // 若 addWorker 返回是 false,即创建失败,则唤醒拒绝策略. - reject(command); -} -``` -发生拒绝的理由有两个 -( 1 )线程池状态为非RUNNING状态 -(2)等待队列已满。 - -下面继续分析`addWorker` - -## addWorker 源码解析 -原子性地检查 runState 和 workerCount,通过返回 false 来防止在不应该添加线程时出现误报。 - -根据当前线程池状态,检查是否可以添加新的线程: -- 若可 -则创建并启动任务;若一切正常则返回true; -- 返回false的可能原因: -1. 线程池没有处`RUNNING`态 -2. 线程工厂创建新的任务线程失败 -### 参数 -- firstTask -外部启动线程池时需要构造的第一个线程,它是线程的母体 -- core -新增工作线程时的判断指标 - - true -需要判断当前`RUNNING`态的线程是否少于`corePoolsize` - - false -需要判断当前`RUNNING`态的线程是否少于`maximumPoolsize` -### JDK8源码 -```java -private boolean addWorker(Runnable firstTask, boolean core) { - // 1. 不需要任务预定义的语法标签,响应下文的continue retry - // 快速退出多层嵌套循环 - retry: - // 外自旋,判断线程池的运行状态 - for (;;) { - int c = ctl.get(); - int rs = runStateOf(c); - // 2. 若RUNNING态,则条件为false,不执行后面判断 - // 若STOP及以上状态,或firstTask初始线程非空,或队列为空 - // 都会直接返回创建失败 - // Check if queue empty only if necessary. - if (rs >= SHUTDOWN && - ! (rs == SHUTDOWN && - firstTask == null && - ! workQueue.isEmpty())) - return false; - - for (;;) { - int wc = workerCountOf(c); - // 若超过最大允许线程数,则不能再添加新线程 - if (wc >= CAPACITY || - wc >= (core ? corePoolSize : maximumPoolSize)) - return false; - // 3. 将当前活动线程数+1 - if (compareAndIncrementWorkerCount(c)) - break retry; - // 线程池状态和工作线程数是可变化的,需经常读取最新值 - c = ctl.get(); // Re-read ctl - // 若已关闭,则再次从retry 标签处进入,在第2处再做判断(第4处) - if (runStateOf(c) != rs) - continue retry; - //如果线程池还是RUNNING态,说明仅仅是第3处失败 -//继续循环执行(第5外) - // else CAS failed due to workerCount change; retry inner loop - } - } - - // 开始创建工作线程 - boolean workerStarted = false; - boolean workerAdded = false; - Worker w = null; - try { - // 利用Worker 构造方法中的线程池工厂创建线程,并封装成工作线程Worker对象 - // 和 AQS 有关!!! - w = new Worker(firstTask); - // 6. 注意这是Worker中的属性对象thread - final Thread t = w.thread; - if (t != null) { - // 在进行ThreadpoolExecutor的敏感操作时 - // 都需要持有主锁,避免在添加和启动线程时被干扰 - final ReentrantLock mainLock = this.mainLock; - mainLock.lock(); - try { - // Recheck while holding lock. - // Back out on ThreadFactory failure or if - // shut down before lock acquired. - int rs = runStateOf(ctl.get()); - // 当线程池状态为RUNNING 或SHUTDOWN - // 且firstTask 初始线程为空时 - if (rs < SHUTDOWN || - (rs == SHUTDOWN && firstTask == null)) { - if (t.isAlive()) // precheck that t is startable - throw new IllegalThreadStateException(); - workers.add(w); - int s = workers.size(); - // 整个线程池在运行期间的最大并发任务个数 - if (s > largestPoolSize) - // 更新为工作线程的个数 - largestPoolSize = s; - // 新增工作线程成功 - workerAdded = true; - } - } finally { - mainLock.unlock(); - } - if (workerAdded) { - // 看到亲切迷人的start方法了! - // 这并非线程池的execute 的command 参数指向的线程 - t.start(); - workerStarted = true; - } - } - } finally { - // 线程启动失败,把刚才第3处加,上的工作线程计数再减-回去 - if (! workerStarted) - addWorkerFailed(w); - } - return workerStarted; -} -``` -#### 第1处 -配合循环语句出现的标签,类似于goto语法作用。label 定义时,必须把标签和冒号的组合语句紧紧相邻定义在循环体之前,否则编译报错。目的是在实现多重循环时能够快速退出到任何一层。出发点似乎非常贴心,但在大型软件项目中,滥用标签行跳转的后果将是无法维护的! - - -在 **workerCount** 加1成功后,直接退出两层循环。 - -#### 第2处,这样的表达式不利于阅读,应如是 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzg1XzQ2ODU5NjgtMDg2ZTlkNWY5ZGEyYWZkNC5wbmc?x-oss-process=image/format,png) - -#### 第3处 -与第1处的标签呼应,`AtomicInteger`对象的加1操作是原子性的。`break retry`表 直接跳出与`retry` 相邻的这个循环体 - -#### 第4处 -此`continue`跳转至标签处,继续执行循环. -如果条件为false,则说明线程池还处于运行状态,即继续在`for(;)`循环内执行. - -#### 第5处 -`compareAndIncrementWorkerCount `方法执行失败的概率非常低. -即使失败,再次执行时成功的概率也是极高的,类似于自旋原理. -这里是先加1,创建失败再减1,这是轻量处理并发创建线程的方式; -如果先创建线程,成功再加1,当发现超出限制后再销毁线程,那么这样的处理方式明显比前者代价要大. - -#### 第6处 -`Worker `对象是工作线程的核心类实现。它实现了`Runnable`接口,并把本对象作为参数输入给`run()`中的`runWorker (this)`。所以内部属性线程`thread`在`start`的时候,即会调用`runWorker`。 - -```java -private final class Worker - extends AbstractQueuedSynchronizer - implements Runnable -{ - /** - * This class will never be serialized, but we provide a - * serialVersionUID to suppress a javac warning. - */ - private static final long serialVersionUID = 6138294804551838833L; - - /** Thread this worker is running in. Null if factory fails. */ - final Thread thread; - /** Initial task to run. Possibly null. */ - Runnable firstTask; - /** Per-thread task counter */ - volatile long completedTasks; - - /** - * Creates with given first task and thread from ThreadFactory. - * @param firstTask the first task (null if none) - */ - Worker(Runnable firstTask) { - setState(-1); // 直到调用runWorker前,禁止被中断 - this.firstTask = firstTask; - this.thread = getThreadFactory().newThread(this); - } - - /** 将主线程的 run 循环委托给外部的 runWorker 执行 */ - public void run() { - runWorker(this); - } - - // Lock methods - // - // The value 0 represents the unlocked state. - // The value 1 represents the locked state. - - protected boolean isHeldExclusively() { - return getState() != 0; - } - - protected boolean tryAcquire(int unused) { - if (compareAndSetState(0, 1)) { - setExclusiveOwnerThread(Thread.currentThread()); - return true; - } - return false; - } - - protected boolean tryRelease(int unused) { - setExclusiveOwnerThread(null); - setState(0); - return true; - } - - public void lock() { acquire(1); } - public boolean tryLock() { return tryAcquire(1); } - public void unlock() { release(1); } - public boolean isLocked() { return isHeldExclusively(); } - - void interruptIfStarted() { - Thread t; - if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) { - try { - t.interrupt(); - } catch (SecurityException ignore) { - } - } - } -} -``` -#### setState(-1)是为何 -设置个简单的状态,检查状态以防止中断。在调用停止线程池时会判断state 字段,决定是否中断之。 -![](https://img-blog.csdnimg.cn/20210713174701301.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70)![](https://img-blog.csdnimg.cn/20210713175625198.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70)![](https://img-blog.csdnimg.cn/20210713175645371.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70)![](https://img-blog.csdnimg.cn/20210713175718745.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) -#### t 到底是谁? -![](https://img-blog.csdnimg.cn/20210713180707150.png) -# 源码分析 -```java - /** - * 检查是否可以根据当前池状态和给定的边界(核心或最大) - * 添加新工作线程。如果是这样,工作线程数量会相应调整,如果可能的话,一个新的工作线程创建并启动 - * 将firstTask作为其运行的第一项任务。 - * 如果池已停止此方法返回false - * 如果线程工厂在被访问时未能创建线程,也返回false - * 如果线程创建失败,或者是由于线程工厂返回null,或者由于异常(通常是在调用Thread.start()后的OOM)),我们干净地回滚。 - */ - private boolean addWorker(Runnable firstTask, boolean core) { - /** - * Check if queue empty only if necessary. - * - * 如果线程池已关闭,并满足以下条件之一,那么不创建新的 worker: - * 1. 线程池状态大于 SHUTDOWN,也就是 STOP, TIDYING, 或 TERMINATED - * 2. firstTask != null - * 3. workQueue.isEmpty() - * 简单分析下: - * 状态控制的问题,当线程池处于 SHUTDOWN ,不允许提交任务,但是已有任务继续执行 - * 当状态大于 SHUTDOWN ,不允许提交任务,且中断正在执行任务 - * 多说一句:若线程池处于 SHUTDOWN,但 firstTask 为 null,且 workQueue 非空,是允许创建 worker 的 - * - */ - if (rs >= SHUTDOWN && - ! (rs == SHUTDOWN && - firstTask == null && - ! workQueue.isEmpty())) - return false; - - for (;;) { - int wc = workerCountOf(c); - if (wc >= CAPACITY || - wc >= (core ? corePoolSize : maximumPoolSize)) - return false; - // 如果成功,那么就是所有创建线程前的条件校验都满足了,准备创建线程执行任务 - // 这里失败的话,说明有其他线程也在尝试往线程池中创建线程 - if (compareAndIncrementWorkerCount(c)) - break retry; - // 由于有并发,重新再读取一下 ctl - c = ctl.get(); // Re-read ctl - // 正常如果是 CAS 失败的话,进到下一个里层的for循环就可以了 - // 可如果是因为其他线程的操作,导致线程池的状态发生了变更,如有其他线程关闭了这个线程池 - // 那么需要回到外层的for循环 - if (runStateOf(c) != rs) - continue retry; - // else CAS failed due to workerCount change; retry inner loop - } - } - - /* * - * 到这里,我们认为在当前这个时刻,可以开始创建线程来执行任务 - */ - - // worker 是否已经启动 - boolean workerStarted = false; - // 是否已将这个 worker 添加到 workers 这个 HashSet 中 - boolean workerAdded = false; - Worker w = null; - try { - // 把 firstTask 传给 worker 的构造方法 - w = new Worker(firstTask); - // 取 worker 中的线程对象,Worker的构造方法会调用 ThreadFactory 来创建一个新的线程 - final Thread t = w.thread; - if (t != null) { - //先加锁 - final ReentrantLock mainLock = this.mainLock; - // 这个是整个类的全局锁,持有这个锁才能让下面的操作“顺理成章”, - // 因为关闭一个线程池需要这个锁,至少我持有锁的期间,线程池不会被关闭 - mainLock.lock(); - try { - // Recheck while holding lock. - // Back out on ThreadFactory failure or if - // shut down before lock acquired. - int rs = runStateOf(ctl.get()); - - // 小于 SHUTTDOWN 即 RUNNING - // 如果等于 SHUTDOWN,不接受新的任务,但是会继续执行等待队列中的任务 - if (rs < SHUTDOWN || - (rs == SHUTDOWN && firstTask == null)) { - // worker 里面的 thread 不能是已启动的 - if (t.isAlive()) // precheck that t is startable - throw new IllegalThreadStateException(); - // 加到 workers 这个 HashSet 中 - workers.add(w); - int s = workers.size(); - if (s > largestPoolSize) - largestPoolSize = s; - workerAdded = true; - } - } finally { - mainLock.unlock(); - } - // 若添加成功 - if (workerAdded) { - // 启动线程 - t.start(); - workerStarted = true; - } - } - } finally { - // 若线程没有启动,做一些清理工作,若前面 workCount 加了 1,将其减掉 - if (! workerStarted) - addWorkerFailed(w); - } - // 返回线程是否启动成功 - return workerStarted; - } -``` -看下 `addWorkFailed` -![](https://img-blog.csdnimg.cn/20210714141244398.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_16,color_FFFFFF,t_70) - - -![记录 workers 中的个数的最大值,因为 workers 是不断增加减少的,通过这个值可以知道线程池的大小曾经达到的最大值](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzA4XzQ2ODU5NjgtMDc4NDcyYjY4MmZjYzljZC5wbmc?x-oss-process=image/format,png) -继续看 -### runWorker -```java -// worker 线程启动后调用,while 循环(即自旋!)不断从等待队列获取任务并执行 -// worker 初始化时,可指定 firstTask,那么第一个任务也就可以不需要从队列中获取 -final void runWorker(Worker w) { - Thread wt = Thread.currentThread(); - // 该线程的第一个任务(若有) - Runnable task = w.firstTask; - w.firstTask = null; - // 允许中断 - w.unlock(); - - boolean completedAbruptly = true; - try { - // 循环调用 getTask 获取任务 - while (task != null || (task = getTask()) != null) { - w.lock(); - // 若线程池状态大于等于 STOP,那么意味着该线程也要中断 - /** - * 若线程池STOP,请确保线程 已被中断 - * 如果没有,请确保线程未被中断 - * 这需要在第二种情况下进行重新检查,以便在关中断时处理shutdownNow竞争 - */ - if ((runStateAtLeast(ctl.get(), STOP) || - (Thread.interrupted() && - runStateAtLeast(ctl.get(), STOP))) && - !wt.isInterrupted()) - wt.interrupt(); - try { - // 这是一个钩子方法,留给需要的子类实现 - beforeExecute(wt, task); - Throwable thrown = null; - try { - // 到这里终于可以执行任务了 - task.run(); - } catch (RuntimeException x) { - thrown = x; throw x; - } catch (Error x) { - thrown = x; throw x; - } catch (Throwable x) { - // 这里不允许抛出 Throwable,所以转换为 Error - thrown = x; throw new Error(x); - } finally { - // 也是一个钩子方法,将 task 和异常作为参数,留给需要的子类实现 - afterExecute(task, thrown); - } - } finally { - // 置空 task,准备 getTask 下一个任务 - task = null; - // 累加完成的任务数 - w.completedTasks++; - // 释放掉 worker 的独占锁 - w.unlock(); - } - } - completedAbruptly = false; - } finally { - // 到这里,需要执行线程关闭 - // 1. 说明 getTask 返回 null,也就是说,这个 worker 的使命结束了,执行关闭 - // 2. 任务执行过程中发生了异常 - // 第一种情况,已经在代码处理了将 workCount 减 1,这个在 getTask 方法分析中说 - // 第二种情况,workCount 没有进行处理,所以需要在 processWorkerExit 中处理 - processWorkerExit(w, completedAbruptly); - } -} -``` -看看 -### getTask() -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWRmaWxlcy5ub3djb2Rlci5jb20vZmlsZXMvMjAxOTA2MjUvNTA4ODc1NV8xNTYxNDczODUyNzgwXzQ2ODU5NjgtNWU5NDc3MzE5M2Q5Y2Y0OS5wbmc?x-oss-process=image/format,png) -```java -// 此方法有三种可能 -// 1. 阻塞直到获取到任务返回。默认 corePoolSize 之内的线程是不会被回收的,它们会一直等待任务 -// 2. 超时退出。keepAliveTime 起作用的时候,也就是如果这么多时间内都没有任务,那么应该执行关闭 -// 3. 如果发生了以下条件,须返回 null -// 池中有大于 maximumPoolSize 个 workers 存在(通过调用 setMaximumPoolSize 进行设置) -// 线程池处于 SHUTDOWN,而且 workQueue 是空的,前面说了,这种不再接受新的任务 -// 线程池处于 STOP,不仅不接受新的线程,连 workQueue 中的线程也不再执行 -private Runnable getTask() { - boolean timedOut = false; // Did the last poll() time out? - - for (;;) { - // 允许核心线程数内的线程回收,或当前线程数超过了核心线程数,那么有可能发生超时关闭 - - // 这里 break,是为了不往下执行后一个 if (compareAndDecrementWorkerCount(c)) - // 两个 if 一起看:如果当前线程数 wc > maximumPoolSize,或者超时,都返回 null - // 那这里的问题来了,wc > maximumPoolSize 的情况,为什么要返回 null? - // 换句话说,返回 null 意味着关闭线程。 - // 那是因为有可能开发者调用了 setMaximumPoolSize 将线程池的 maximumPoolSize 调小了 - - // 如果此 worker 发生了中断,采取的方案是重试 - // 解释下为什么会发生中断,这个读者要去看 setMaximumPoolSize 方法, - // 如果开发者将 maximumPoolSize 调小了,导致其小于当前的 workers 数量, - // 那么意味着超出的部分线程要被关闭。重新进入 for 循环,自然会有部分线程会返回 null - int c = ctl.get(); - int rs = runStateOf(c); - - // Check if queue empty only if necessary. - if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { - // CAS 操作,减少工作线程数 - decrementWorkerCount(); - return null; - } - - int wc = workerCountOf(c); - - // Are workers subject to culling? - boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; - - if ((wc > maximumPoolSize || (timed && timedOut)) - && (wc > 1 || workQueue.isEmpty())) { - if (compareAndDecrementWorkerCount(c)) - return null; - continue; - } - - try { - Runnable r = timed ? - workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : - workQueue.take(); - if (r != null) - return r; - timedOut = true; - } catch (InterruptedException retry) { - // 如果此 worker 发生了中断,采取的方案是重试 - // 解释下为什么会发生中断,这个读者要去看 setMaximumPoolSize 方法, - // 如果开发者将 maximumPoolSize 调小了,导致其小于当前的 workers 数量, - // 那么意味着超出的部分线程要被关闭。重新进入 for 循环,自然会有部分线程会返回 null - timedOut = false; - } - } -} -``` -到这里,基本上也说完了整个流程,回到 execute(Runnable command) 方法,看看各个分支,我把代码贴过来一下: -```java - public void execute(Runnable command) { - if (command == null) - throw new NullPointerException(); - //表示 “线程池状态” 和 “线程数” 的整数 - int c = ctl.get(); - // 如果当前线程数少于核心线程数,直接添加一个 worker 执行任务, - // 创建一个新的线程,并把当前任务 command 作为这个线程的第一个任务(firstTask) - if (workerCountOf(c) < corePoolSize) { - // 添加任务成功,即结束 - // 执行的结果,会包装到 FutureTask - // 返回 false 代表线程池不允许提交任务 - if (addWorker(command, true)) - return; - - c = ctl.get(); - } - // 到这说明,要么当前线程数大于等于核心线程数,要么刚刚 addWorker 失败 - - // 如果线程池处于 RUNNING ,把这个任务添加到任务队列 workQueue 中 - if (isRunning(c) && workQueue.offer(command)) { - /* 若任务进入 workQueue,我们是否需要开启新的线程 - * 线程数在 [0, corePoolSize) 是无条件开启新线程的 - * 若线程数已经大于等于 corePoolSize,则将任务添加到队列中,然后进到这里 - */ - int recheck = ctl.get(); - // 若线程池不处于 RUNNING ,则移除已经入队的这个任务,并且执行拒绝策略 - if (! isRunning(recheck) && remove(command)) - reject(command); - // 若线程池还是 RUNNING ,且线程数为 0,则开启新的线程 - // 这块代码的真正意图:担心任务提交到队列中了,但是线程都关闭了 - else if (workerCountOf(recheck) == 0) - addWorker(null, false); - } - // 若 workQueue 满,到该分支 - // 以 maximumPoolSize 为界创建新 worker, - // 若失败,说明当前线程数已经达到 maximumPoolSize,执行拒绝策略 - else if (!addWorker(command, false)) - reject(command); - } -``` -**工作线程**:线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会循环获取工作队列里的任务来执行.我们可以从Worker类的run()方法里看到这点 - -```java - public void run() { - try { - Runnable task = firstTask; - firstTask = null; - while (task != null || (task = getTask()) != null) { - runTask(task); - task = null; - } - } finally { - workerDone(this); - } - } - boolean workerStarted = false; - boolean workerAdded = false; - Worker w = null; - try { - w = new Worker(firstTask); - - final Thread t = w.thread; - if (t != null) { - //先加锁 - final ReentrantLock mainLock = this.mainLock; - mainLock.lock(); - try { - // Recheck while holding lock. - // Back out on ThreadFactory failure or if - // shut down before lock acquired. - int rs = runStateOf(ctl.get()); - - if (rs < SHUTDOWN || - (rs == SHUTDOWN && firstTask == null)) { - if (t.isAlive()) // precheck that t is startable - throw new IllegalThreadStateException(); - workers.add(w); - int s = workers.size(); - if (s > largestPoolSize) - largestPoolSize = s; - workerAdded = true; - } - } finally { - mainLock.unlock(); - } - if (workerAdded) { - t.start(); - workerStarted = true; - } - } - } finally { - if (! workerStarted) - addWorkerFailed(w); - } - return workerStarted; - } -``` -线程池中的线程执行任务分两种情况 - - 在execute()方法中创建一个线程时,会让这个线程执行当前任务 - - 这个线程执行完上图中 1 的任务后,会反复从BlockingQueue获取任务来执行 \ No newline at end of file diff --git "a/JDK/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213\346\267\261\345\205\245\350\257\246\350\247\243(JMM).md" "b/JDK/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213\346\267\261\345\205\245\350\257\246\350\247\243(JMM).md" deleted file mode 100644 index 1dbb544b41..0000000000 --- "a/JDK/JVM/Java\345\206\205\345\255\230\346\250\241\345\236\213\346\267\261\345\205\245\350\257\246\350\247\243(JMM).md" +++ /dev/null @@ -1,128 +0,0 @@ -# 前言 -定义俩共享变量及俩方法: -- 第一个方法, -- 第二个方法 -- (r1,r2)的可能值有哪些? -![](https://img-blog.csdnimg.cn/05139ccfbb40447a869632ff35959841.png) - -在单线程环境下,可先调用第一个方法,最终(r1,r2)为(1,0) -也可以先调用第二个方法,最终为(0,2)。 - -![](https://img-blog.csdnimg.cn/20200404214401993.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -# 1 Java内存模型的意义 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTkyYTFmZGY0OGJlMTllMDYucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTIzZTVlOWE0OWFkZWI1YTEucG5n?x-oss-process=image/format,png) -JMM 与硬件内存架构对应关系![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTVlMTM3NGEwYWJmOWM5MjkucG5n?x-oss-process=image/format,png) -JMM抽象结构图 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQ0ZWE4ODQzYTg4YTk0MGQucG5n?x-oss-process=image/format,png) -内存模型描述程序的可能行为。 - -Java虚拟机规范中试图定义一种Java内存模型,`来屏蔽掉各种硬件和os的内存访问差异`,规定: -- 线程如何、何时能看到其他线程修改过的共享变量的值 -- 必要时,如何同步地访问共享变量 - -以实现让Java程序在各种平台下都能达到一致性的内存访问效果。 - -JMM通过检查执行跟踪中的每个读操作,并根据某些规则检查该读操作观察到的写操作是否有效来工作。 - -只要程序的所有执行产生的结果都可由JMM预测。具体实现者任意实现,包括操作的重新排序和删除不必要的同步。 - -JMM决定了在程序的每个点上可以读取什么值。 -## 1.1 共享变量(Shared Variables) -可在线程之间共享的内存称为`共享内存或堆内存`。所有实例字段、静态字段和数组元素都存储在堆内存。 -不包括局部变量与方法参数,因为这些是线程私有的,不存在共享。 - -对同一变量的两次访问(读或写),若有一个是写请求,则是冲突的! -# 2 主内存与工作内存 -工作内存缓存 -![](https://img-blog.csdnimg.cn/20191014024209488.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70) -JMM的主要是定义了`各个变量的访问规则`,在JVM中的如下底层细节: -- 将变量存储到内存 -- 从内存中取出变量值 - -为获得较好执行效率,JMM并未限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器调整代码执行顺序这类权限。 - -JMM规定: -- 所有变量都存储在主内存(Main Memory) -- 每条线程有自己的工作内存(Working Memory) -保存了该线程使用到的`变量的主内存副本拷贝`(线程所访问对象的引用或者对象中某个在线程访问到的字段,不会是整个对象的拷贝) -线程对变量的所有操作(读,赋值等)都必须在工作内存进行,不能直接读写主内存中的变量 -volatile变量依然有工作内存的拷贝,,是他特殊的操作顺序性规定,看起来如同直接在主内存读写 -不同线程间,无法直接访问对方工作内存中的变量,线程间变量值的传递均要通过主内存 - -线程、主内存、工作内存三者的交互关系: -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTEyMjA5YjEyZDU3OGEyZWQucG5n?x-oss-process=image/format,png) -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWJiM2QzN2MxNTVjZDgyZDgucG5n?x-oss-process=image/format,png) - -JVM模型与JMM不是同一层次的内存划分,基本毫无关系的,硬要对应起来,从变量,内存,工作内存的定义来看 -- 主内存 《=》Java堆中的对象实例数据部分 -- 工作内存 《=》虚拟机栈中的部分区域 - -从更底层的层次来看: -- 主内存直接对应物理硬件的内存 -- 为更好的运行速度,虚拟机(甚至硬件系统的本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存器,因为程序运行时主要访问读写的是工作内存 -# 3 内存间同步操作 -## 3.1 线程操作的定义 -### 操作定义 -write要写的变量以及要写的值。 -read要读的变量以及可见的写入值(由此,我们可以确定可见的值)。 -lock要锁定的管程(监视器monitor)。 -unlock要解锁的管程。 -外部操作(socket等等..) -启动和终止 -### 程序顺序 -如果一个程序没有数据竞争,那么程序的所有执行看起来都是顺序一致的 - -本规范只涉及线程间的操作; -一个变量如何从主内存拷贝到工作内存,从工作内存同步回主内存的实现细节 - -JMM 本身已经定义实现了以下8种操作来完成,且都具备`原子性` -- lock(锁定) -作用于主内存变量,把一个变量标识为一条线程独占的状态 -- unlock(解锁) -作用于主内存变量,把一个处于锁定状态的变量释放,释放后的变量才可以被其它线程锁定 -unlock之前必须将变量值同步回主内存 -- read(读取) -作用于主内存变量,把一个变量的值从主内存传输到工作内存,以便随后的load -- load(载入) -作用于工作内存变量,把read从主内存中得到的变量值放入工作内存的变量副本 -- use(使用) -作用于工作内存变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到的变量的值得字节码指令时将会执行这个操作 -- assign(赋值) -作用于工作内存变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 -- store(存储) -作用于工作内存变量,把工作内存中一个变量的值传送到主内存,以便随后的write操作使用 -- write(写入) -作用于主内存变量,把store操作从工作内存中得到的值放入主内存的变量中 - -- 把一个变量从主内存`复制`到工作内存 -就要顺序执行read和load - -- 把变量从工作内存`同步`回主内存 -就要顺序地执行store和write操作 - -JMM只要求上述两个操作必须`按序执行`,而没有保证连续执行 -也就是说read/load之间、store/write之间可以插入其它指令 -如对主内存中的变量a,b访问时,一种可能出现的顺序是read a->readb->loadb->load a - -JMM规定执行上述八种基础操作时必须满足如下 -## 3.1 同步规则 -◆ 对于监视器 m 的解锁与所有后续操作对于 m 的加锁 `同步`(之前的操作保持可见) -◆对 volatile变量v的写入,与所有其他线程后续对v的读同步 - -◆ `启动` 线程的操作与线程中的第一个操作同步 -◆ 对于每个属性写入默认值(0, false, null)与每个线程对其进行的操作同步 -◆ 线程 T1的最后操作与线程T2发现线程T1已经结束同步。( isAlive ,join可以判断线程是否终结) -◆ 如果线程 T1中断了T2,那么线程T1的中断操作与其他所有线程发现T2被中断了同步通过抛出*InterruptedException*异常,或者调用*Thread.interrupted*或*Thread.isInterrupted* - -- 不允许read/load、store/write操作之一单独出现 -不允许一个变量从主内存读取了但工作内存不接收,或从工作内存发起回写但主内存不接收 -- 不允许一个线程丢弃它的最近的assign -即变量在工作内存中改变(为工作内存变量赋值)后必须把该变化同步回主内存 -- 新变量只能在主内存“诞生”,不允许在工作内存直接使用一个未被初始化(load或assign)的变量 -换话说就是一个变量在实施use,store之前,必须先执行过assign和load -- 如果一个变量事先没有被load锁定,则不允许对它执行unlock,也不允许去unlock一个被其它线程锁定的变量 -- 对一个变量执行unloack前,必须把此变量同步回主内存中(执行store,write) - -> 参考 -> - https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.1 \ No newline at end of file diff --git "a/JDK/JVM/Java\346\200\247\350\203\275\350\260\203\344\274\230\345\267\245\345\205\267\344\271\213JDK\345\221\275\344\273\244\350\241\214.md" "b/JDK/JVM/Java\346\200\247\350\203\275\350\260\203\344\274\230\345\267\245\345\205\267\344\271\213JDK\345\221\275\344\273\244\350\241\214.md" index 59c68099d5..fd762cc410 100644 --- "a/JDK/JVM/Java\346\200\247\350\203\275\350\260\203\344\274\230\345\267\245\345\205\267\344\271\213JDK\345\221\275\344\273\244\350\241\214.md" +++ "b/JDK/JVM/Java\346\200\247\350\203\275\350\260\203\344\274\230\345\267\245\345\205\267\344\271\213JDK\345\221\275\344\273\244\350\241\214.md" @@ -1,30 +1,21 @@ ## 1.1 jps -类似Linux的ps,但jps只列出Java的进程。可方便查看Java进程的启动类、传入参数和JVM参数。直接运行,不加参数,列出Java程序的进程ID及Main函数名称。 +类似Linux的ps,但是jps只用于列出Java的进程 +可以方便查看Java进程的启动类,传入参数和JVM参数等 +直接运行,不加参数,列出Java程序的进程ID以及Main函数等名称 ![jps命令本质也是Java程序](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTljMjE4OWRlZDljYmQ1M2UucG5n?x-oss-process=image/format,png) -- -m 输出传递给Java进程的参数![](https://img-blog.csdnimg.cn/20210117135731422.png) - -- -l 输出主函数的完整路径![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWJkOGY1NzU2NWQzNDY1NDMucG5n?x-oss-process=image/format,png) -- -q 只输出进程ID![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWUzODhhY2U5MmYzMDNkYWYucG5n?x-oss-process=image/format,png) -- -v 显示传递给jvm的参数![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTlhM2JhYjkzZjk0Y2U2YzgucG5n?x-oss-process=image/format,png) +![-m 输出传递给Java进程的参数](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTE1ZDAzZGJkNWY4MDIxMDkucG5n?x-oss-process=image/format,png) +![-l 输出主函数的完整路径](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWJkOGY1NzU2NWQzNDY1NDMucG5n?x-oss-process=image/format,png) +![-q 只输出进程ID](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWUzODhhY2U5MmYzMDNkYWYucG5n?x-oss-process=image/format,png) +![-v 显示传递给jvm的参数](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTlhM2JhYjkzZjk0Y2U2YzgucG5n?x-oss-process=image/format,png) ## 1.2 jstat -观察Java应用程序运行时信息的工具,详细查看堆使用情况以及GC情况 -- jstat -options -![](https://img-blog.csdnimg.cn/20210117142257563.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_1,color_FFFFFF,t_70) - +用于观察Java应用程序运行时信息的工具,详细查看堆使用情况以及GC情况 +![jstat -options](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWZiYzg1NWU5NDk3MTljNzcucG5n?x-oss-process=image/format,png) ### 1.2.1 jstat -class pid 显示加载class的数量及所占空间等信息 -- -compiler -t:显示JIT编译的信息 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTcyM2Y5ZjA4MjMyMjcyMDQucG5n?x-oss-process=image/format,png) +![-compiler -t:显示JIT编译的信息](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTcyM2Y5ZjA4MjMyMjcyMDQucG5n?x-oss-process=image/format,png) ### 1.2.2 -gc pid -显示gc信息,查看gc的次数及时间 -![](https://img-blog.csdnimg.cn/20210117144320641.png) - -```shell -➜ ~ jstat -gc 87552 - S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT -25088.0 20992.0 0.0 20992.0 500224.0 56227.0 363008.0 35238.1 76672.0 72902.5 10368.0 9590.5 9 0.078 3 0.162 - - 0.239 -``` - +可以显示gc的信息,查看gc的次数及时间 +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTZiODFlOTZjOWNlNGExMjIucG5n?x-oss-process=image/format,png) ### 1.2.3 -gccapacity 比-gc多了各个代的最大值和最小值 ![jstat -gccapacity 3661](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LThmMGMwNTY4YTgzM2M5MzkucG5n?x-oss-process=image/format,png) @@ -44,28 +35,25 @@ ![-gcoldcapacity展现老年代的容量信息](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTc0NjhlMDA4YTUwZGMyMDQucG5n?x-oss-process=image/format,png) ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTA0OWRjZjBhYzRkNjM0YjYucG5n?x-oss-process=image/format,png) ### -gcutil -相比于-gc 参数,只显示使用率而非使用量了。 显示GC回收相关信息 -![](https://img-blog.csdnimg.cn/20210117144845233.png) - +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTNhYmVkNDJmOTdiOWZmYzQucG5n?x-oss-process=image/format,png) ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWY5MDZjZTFhZDA2MDA3YTAucG5n?x-oss-process=image/format,png) ### -printcompilation 当前VM执行的信息 ![jstat -printcompilation 3661](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWMxMzk3NmMzZjE4OTViYWIucG5n?x-oss-process=image/format,png) ### 还可以同时加两个数 -- 输出进程4798的ClassLoader信息,每1秒统计一次,共输出2次 -![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTBjMjcyNGI5MzIwOWU4ZjIucG5n?x-oss-process=image/format,png) +![输出进程4798的ClassLoader信息,每1秒统计一次,共输出2次](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTBjMjcyNGI5MzIwOWU4ZjIucG5n?x-oss-process=image/format,png) ## 1.3 jinfo `jinfo