纠错与更改所有文章的图床

This commit is contained in:
Dragon
2021-03-22 20:01:15 +08:00
parent cdb811a48a
commit bf0d1bcccd
27 changed files with 767 additions and 642 deletions

View File

@@ -6,10 +6,10 @@ tags:
- 源码
categories:
- Java并发
- 原理
keywords: Java并发原理源码
description: 万字系列长文讲解Java并发-第一阶段-多线程基础知识。
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/logo_1.png'
top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg'
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/logo_1.png'
abbrlink: efc79183
date: 2020-10-05 22:40:58
---
@@ -31,13 +31,13 @@ date: 2020-10-05 22:40:58
概念:进程可进一步细化为线程,是一个程序内部的一条执行路径。
说明线程作为CPU调度和执行的单位每个线程拥独立的运行栈和程序计数器(pc),线程切换的开销小。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/First_stage/0001.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/First_stage/0001.png">
补充:
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/First_stage/0002.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/First_stage/0002.png">
进程可以细化为多个线程。
每个线程,拥有自己独立的:栈、程序计数器
@@ -96,7 +96,10 @@ public class ThreadTest {
//问题一我们不能通过直接调用run()的方式启动线程。
// t1.run();
//问题二再启动一个线程遍历100以内的偶数。不可以还让已经start()的线程去执行。会报IllegalThreadStateException
/*
问题二再启动一个线程遍历100以内的偶数。不可以还让已经start()的线程去执行。
会报IllegalThreadStateException
*/
// t1.start();
//我们需要重新创建一个线程的对象
MyThread t2 = new MyThread();
@@ -158,7 +161,11 @@ public class ThreadTest1 {
//4. 将此对象作为参数传递到Thread类的构造器中创建Thread类的对象
Thread t1 = new Thread(mThread);
t1.setName("线程1");
//5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run()
/*
5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->
调用了Runnable类型的target的run()
*/
t1.start();
//再启动一个线程遍历100以内的偶数
@@ -260,7 +267,8 @@ private void init(ThreadGroup g, Runnable target, String name,
tid = nextThreadID();
}
/*如果你是实现了runnable接口那么在上面的代码中target便不会为null那么最终就会通过重写的规则去调用真正实现了Runnable接口(你之前传进来的那个Runnable接口实现类)的类里的run方法*/
/*如果你是实现了runnable接口那么在上面的代码中target便不会为null那么最终就会通过重写的
规则去调用真正实现了Runnable接口(你之前传进来的那个Runnable接口实现类)的类里的run方法*/
@Override
public void run() {
@@ -273,7 +281,7 @@ private void init(ThreadGroup g, Runnable target, String name,
1、多线程的设计之中使用了代理设计模式的结构用户自定义的线程主体只是负责项目核心功能的实现而所有的辅助实现全部交由Thread类来处理。
2、在进行Thread启动多线程的时候调用的是start()方法而后找到的是run()方法但通过Thread类的构造方法传递了一个Runnable接口对象的时候那么该接口对象将被Thread类中的target属性所保存在start()方法执行的时候会调用Thread类中的run()方法。而这个run()方法去调用实现了Runnable接口的那个类所重写过run()方法进而执行相应的逻辑。多线程开发的本质实质上是在于多个线程可以进行同一资源的抢占那么Thread主要描述的是线程而资源的描述是通过Runnable完成的。如下图所示
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/First_stage/0003.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/First_stage/0003.png">
@@ -361,7 +369,8 @@ private void init(ThreadGroup g, Runnable target, String name,
tid = nextThreadID();
}
/*由于这里是通过继承Thread类来实现的线程那么target这个东西就是Null。但是因为你继承了Runnable接口并且重写了run()所以最终还是调用子类的run()*/
/*由于这里是通过继承Thread类来实现的线程那么target这个东西就是Null。但是因为你继承
了Runnable接口并且重写了run()所以最终还是调用子类的run()*/
@Override
public void run() {
if (target != null) {
@@ -635,7 +644,7 @@ public void run() {
1、如果直接调用run()方法,相当于就是简单的调用一个普通方法。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/First_stage/0004.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/First_stage/0004.png">
2、run()的调用是在start0()这个Native C++方法里调用的
@@ -645,11 +654,11 @@ public void run() {
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态这几个状态在Java源码中用枚举来表示。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/First_stage/0005.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/First_stage/0005.png">
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/First_stage/0006.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/First_stage/0006.png">
> 图中 wait到 runnable状态的转换中`join`实际上是`Thread`类的方法,但这里写成了`Object`。
@@ -690,7 +699,7 @@ public static void main(String[] args) {
2、当JVM启动后实际有多个线程但是至少有一个非守护线程比如main线程
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/First_stage/0007.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/First_stage/0007.png">
- FinalizerGC守护线程
@@ -780,7 +789,8 @@ public static void main(String[] args) {
}
/*
设置该线程为守护线程必须在启动它之前。如果t.start()之后再t.setDaemon(true);会抛出IllegalThreadStateException
设置该线程为守护线程必须在启动它之前。如果t.start()之后再t.setDaemon(true);
会抛出IllegalThreadStateException
*/
```
@@ -872,7 +882,108 @@ public static void main(String[] args) throws InterruptedException {
# 中断
记住中断只是一个状态Java的方法可以选择对这个中断进行响应也可以选择不响应。响应的意思就是写相对应的代码执行相对应的操作不响应的意思就是什么代码都不写。
1、Java 中的中断和操作系统的中断还不一样,这里就按照**状态**来理解吧,不要和操作系统的中断联系在一起
2、记住中断只是一个状态Java的方法可以选择对这个中断进行响应也可以选择不响应。响应的意思就是写相对应的代码执行相对应的操作不响应的意思就是什么代码都不写。
## 几个方法
```java
// Thread 类中的实例方法,持有线程实例引用即可检测线程中断状态
public boolean isInterrupted() {}
/*
1、Thread 中的静态方法,检测调用这个方法的线程是否已经中断
2、注意这个方法返回中断状态的同时会将此线程的中断状态重置为 false
如果我们连续调用两次这个方法的话,第二次的返回值肯定就是 false 了
*/
public static boolean interrupted() {}
// Thread 类中的实例方法,用于设置一个线程的中断状态为 true
public void interrupt() {}
```
## 小tip
```java
public static boolean interrupted()
public boolean isInterrupted()//这个会清除中断状态
```
为什么要这么设置呢?原因在于:
* interrupted()是一个静态方法可以在Runnable接口实例中使用
* isInterrupted()是一个Thread的实例方法在重写Thread的run方法时使用
```java
public class ThreadInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(Thread.interrupted());
}); //这个new Thread用的是runnable接口那个构造函数
Thread t2 = new Thread(){
@Override
public void run() {
System.out.println(isInterrupted());
}
};//这个new Thread用的就是Thread的空参构造
}
}
```
也就是说接口中不能调用Thread的实例方法只能通过静态方法来判断是否发生中断
## 重难点
当然,中断除了是线程状态外,还有其他含义,否则也不需要专门搞一个这个概念出来了。
> 初学者肯定以为 thread.interrupt() 方法是用来暂停线程的,主要是和它对应中文翻译的“中断”有关。中断在并发中是常用的手段,请大家一定好好掌握。可以将中断理解为线程的状态,它的特殊之处在于设置了中断状态为 true 后,这几个方法会感知到:
>
> 1. wait(), wait(long), wait(long, int), join(), join(long), join(long, int), sleep(long), sleep(long, int)
>
> 这些方法都有一个共同之处,方法签名上都有`throws InterruptedException`,这个就是用来响应中断状态修改的。
>
> 2. 如果线程阻塞在 InterruptibleChannel 类的 IO 操作中,那么这个 channel 会被关闭。
>
> 3. 如果线程阻塞在一个 Selector 中,那么 select 方法会立即返回。
>
> 对于以上 3 种情况是最特殊的,因为他们能自动感知到中断(这里说自动,当然也是基于底层实现),**并且在做出相应的操作后都会重置中断状态为 false**。然后执行相应的操作(通常就是跳到 catch 异常处)。
>
> 如果不是以上3种情况那么线程的 interrupt() 方法被调用,会将线程的中断状态设置为 true。
>
> 那是不是只有以上 3 种方法能自动感知到中断呢?不是的,如果线程阻塞在 LockSupport.park(Object obj) 方法,也叫挂起,这个时候的中断也会导致线程唤醒,但是唤醒后不会重置中断状态,所以唤醒后去检测中断状态将是 true。
> 资料: [Oracle官方文档](https://docs.oracle.com/javase/specs/index.html) ---> [ The Java® Language Specification Java SE 8 Edition](https://docs.oracle.com/javase/specs/jls/se8/html/index.html) ---> 第17章 Threads and Locks
## InterruptedException
它是一个特殊的异常,不是说 JVM 对其有特殊的处理,而是它的使用场景比较特殊。通常,我们可以看到,像 Object 中的 wait() 方法ReentrantLock 中的 lockInterruptibly() 方法Thread 中的 sleep() 方法等等,这些方法都带有 `throws InterruptedException`我们通常称这些方法为阻塞方法blocking method
阻塞方法一个很明显的特征是,它们需要花费比较长的时间(不是绝对的,只是说明时间不可控),还有它们的方法结束返回往往依赖于外部条件,如 wait 方法依赖于其他线程的 notifylock 方法依赖于其他线程的 unlock等等。
当我们看到方法上带有 `throws InterruptedException` 时,我们就要知道,这个方法应该是阻塞方法,我们如果希望它能早点返回的话,我们往往可以通过中断来实现。
除了几个特殊类(如 ObjectThread等感知中断并提前返回是通过轮询中断状态来实现的。我们自己需要写可中断的方法的时候就是通过在合适的时机通常在循环的开始处去判断线程的中断状态然后做相应的操作通常是方法直接返回或者抛出异常。当然我们也要看到如果我们一次循环花的时间比较长的话那么就需要比较长的时间才能**感知**到线程中断了。
## wait()中断测试
@@ -912,17 +1023,9 @@ public static void main(String[] args) {
> 注释掉e.printStackTrace();的输出
>
> false //pos_4
> true //pos_5 t.isInterrupted()之后会立即清除中断状态
> true //pos_5
> wait响应中断 //pos_1
> false //pos_3 因为pos_5清除了中断状态所以这里检测到就是flase没有被中断过
* 当该线程在wait()、join()、sleep(long, int)状态时,如果被打断,则会收到一个异常提醒。因为这些方法都抛出了
```throws InterruptedException``` 这个异常通过try catch可以做相应的处理。
* 但是只要线程被打断,无论哪个方法都可以通过`isInterrupted()`方法检测到打断的状态。
> false //pos_3
@@ -956,47 +1059,6 @@ try {
## 两个判断中断状态的方法
```java
public static boolean interrupted()
public boolean isInterrupted()//这个会清除中断状态
```
为什么要这么设置呢?原因在于:
* interrupted()是一个静态方法可以在Runnable接口实例中使用
* isInterrupted()是一个Thread的实例方法在重写Thread的run方法时使用
```java
public class ThreadInterrupt {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println(Thread.interrupted());
}); //这个new Thread用的是runnable接口那个构造函数
Thread t2 = new Thread(){
@Override
public void run() {
System.out.println(isInterrupted());
}
};//这个new Thread用的就是Thread的空参构造
}
}
```
也就是说接口中不能调用Thread的实例方法只能通过静态方法来判断是否发生中断
# 关闭线程
## 优雅的关闭(通过一个Boolean)
@@ -1494,13 +1556,10 @@ volatile自己虽然不能保证原子性但是和CAS结合起来就可以保
## CAS 是什么?
- CAS比较并交换<compareAndSet>,它是一条 CPU 并发原语
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子性的。
- CAS比较并交换compareAndSet,它是一条 CPU 并发原语,它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子性的。
- 例: AtomicInteger 的 compareAndSet('期望值','设置值') 方法,期望值与目标值一致时,修改目标变量为设置值,期望值与目标值不一致时,返回 false 和最新主存的变量值
- 例: AtomicInteger 的 compareAndSet('期望值','设置值') 方法
期望值与目标值一致时,修改目标变量为设置值
期望值与目标值不一致时,返回 false 和最新主存的变量值
- CAS 的底层原理
例: AtomicInteger.getAndIncrement()
调用 Unsafe 类中的 CAS 方法JVM 会帮我们实现出 CAS 汇编指令
@@ -1515,7 +1574,7 @@ volatile自己虽然不能保证原子性但是和CAS结合起来就可以保
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/First_stage/0009.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/First_stage/0009.png">
@@ -1723,7 +1782,7 @@ public Unsafe getUnsafe() throws IllegalAccessException {
Unsafe的功能如下图
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/First_stage/0008.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/First_stage/0008.png">
## CAS相关

View File

@@ -6,12 +6,12 @@ tags:
- 源码
categories:
- Java并发
- 原理
keywords: Java并发原理源码
description: 万字系列长文讲解-Java并发体系-第三阶段-JUC并发包。JUC在高并发编程中使用频率非常高这里会详细介绍其用法。
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/logo_1.png'
top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg'
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/logo_1.png'
abbrlink: 5be45d9e
date: 2020-10-19 22:13:58
date: 2020-10-09 22:13:58
---
@@ -1869,7 +1869,7 @@ public class ForkJoinRecursiveAction {
ForkJoinTask就是ForkJoinPool里面的每一个任务。他主要有两个子类`RecursiveAction``RecursiveTask`。然后通过fork()方法去分配任务执行任务通过join()方法汇总任务结果。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Third_stage/0001.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Third_stage/0001.png">
## 小总结

View File

@@ -6,12 +6,12 @@ tags:
- 源码
categories:
- Java并发
- 原理
keywords: Java并发原理源码
description: 万字系列长文讲解-Java并发体系-第三阶段-JUC并发包。JUC在高并发编程中使用频率非常高这里会详细介绍其用法。
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/logo_1.png'
top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg'
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/logo_1.png'
abbrlink: 70c90e5d
date: 2020-10-19 22:13:58
date: 2020-10-10 22:13:58
---
@@ -563,7 +563,7 @@ public int getUnarrivedParties()
根据上面的代码,我们可以画出下面这个很简单的图:
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Third_stage/0002.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Third_stage/0002.png">
这棵树上有 7 个 phaser 实例,每个 phaser 实例在构造的时候,都指定了 parties 为 5但是对于每个拥有子节点的节点来说每个子节点都是它的一个 party我们可以通过 phaser.getRegisteredParties() 得到每个节点的 parties 数量:
@@ -896,32 +896,26 @@ Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 E
**七大参数**
- corePoolSize
线程池中的常驻核心线程数
- corePoolSize 线程池中的常驻核心线程数
创建线程池后,当有请求任务进来,就安排池中的线程去执行请求任务
当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列中
当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放到缓存队列中
- maximumPoolSize
线程池能够容纳同时执行的最大线程数此值必须大于等于1
- keepAliveTime
多余的空闲线程的存活时间
- keepAliveTime 多余的空闲线程的存活时间
当前线程池数量超过 corePoolSize 时,当空闲时间达到 keepAliveTime 值时,
多余空闲线程会被销毁直到只剩下 corePoolSize 个线程为止
多余空闲线程会被销毁直到只剩下 corePoolSize 个线程为止
- unit
keepAliveTime 的单位
- workQueue
任务队列,被提交但尚未被执行的任务
- threadFactory
表示生成线程池中工作线程的线程工厂<线程名字、线程序数...>
用于创建线程一般用默认的即可
- handler
拒接策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时
如何拒绝新的任务
- threadFactory,表示生成线程池中工作线程的线程工厂<线程名字、线程序数...>,用于创建线程一般用默认的即可
- handler拒接策略表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时,如何拒绝新的任务
@@ -958,7 +952,7 @@ public class ThreadPoolDemo {
## 线程池的底层工作流程
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Third_stage/0003.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Third_stage/0003.png">
1、创建线程池后等待请求任务
@@ -1652,7 +1646,7 @@ public class ExecutorCompletionService<V> implements CompletionService<V> {
**执行流程:**
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Third_stage/0004.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Third_stage/0004.png">

View File

@@ -6,12 +6,12 @@ tags:
- 源码
categories:
- Java并发
- 原理
keywords: Java并发原理源码
description: '万字系列长文讲解-Java并发体系-第二阶段,从C++和硬件方面讲解。'
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/logo_1.png'
top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg'
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/logo_1.png'
abbrlink: 230c5bb3
date: 2020-10-19 22:09:58
date: 2020-10-06 22:09:58
---
@@ -291,7 +291,7 @@ d = e - f ;
由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0001.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0001.png">
@@ -343,7 +343,7 @@ int c = a + b;
冯诺依曼,提出计算机由五大组成部分,输入设备,输出设备存储器,控制器,运算器。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0002.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0002.png">
输入设备:鼠标,键盘等等
@@ -367,7 +367,7 @@ int c = a + b;
CPU的运算速度和内存的访问速度相差比较大。这就导致CPU每次操作内存都要耗费很多等待时间。内 存的读写速度成为了计算机运行的瓶颈。于是就有了在CPU和主内存之间增加缓存的设计。靠近CPU 的缓存称为L1然后依次是 L2L3和主内存CPU缓存模型如图下图所示。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0003.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0003.png">
CPU Cache分成了三个级别: L1 L2 L3。级别越小越接近CPU速度也更快同时也代表着容量越小。速度越快的价格越贵。
@@ -377,7 +377,7 @@ CPU Cache分成了三个级别: L1 L2 L3。级别越小越接近CPU
3、L3 Cache是三级缓存中大的一级例如12MB同时也是缓存中慢的一级在同一个CPU插槽 之间的核共享一个L3 Cache。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0004.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0004.png">
上面的图中有一个Latency指标。比如Memory这个指标为59.4ns表示CPU在操作内存的时候有59.4ns的延迟一级缓存最快只有1.2ns。
@@ -409,7 +409,7 @@ Cache的出现是为了解决CPU直接访问内存效率低下问题的。
每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操 作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接 访问对方工作内存中的变量。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0005.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0005.png">
Java的线程不能直接在主内存中操作共享变量。而是首先将主内存中的共享变量赋值到自己的工作内存中再进行操作操作完成之后刷回主内存。
@@ -424,7 +424,7 @@ Java内存模型是一套在多线程读写共享数据时对共享数据的
JMM内存模型与CPU硬件内存架构的关系
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0006.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0006.png">
工作内存可能对应CPU寄存器也可能对应CPU缓存也可能对应内存。
@@ -434,9 +434,9 @@ JMM内存模型与CPU硬件内存架构的关系
## 再谈可见性
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0007.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0007.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0008.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0008.png">
1、图中所示是 个双核 CPU 系统架构 每个核有自己的控制器和运算器其中控制器包含一组寄存器和操作控制器运算器执行算术逻辅运算。每个核都有自己的1级缓存在有些架构里面还有1个所有 CPU 共享的2级缓存。 那么 Java 内存模型里面的工作内存,就对应这里的 Ll 或者 L2 存或者 CPU 寄存器。
@@ -452,7 +452,7 @@ JMM内存模型与CPU硬件内存架构的关系
为了保证数据交互时数据的正确性Java内存模型中定义了8种操作来完成这个交互过程这8种操作本身都是原子性的。虚拟机实现时必须保证下面 提及的每一种操作都是原子的、不可再分的。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0009.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0009.png">
> (1)lock:作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
>
@@ -478,7 +478,7 @@ JMM内存模型与CPU硬件内存架构的关系
如果没有synchronized那就是下面这样的
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0010.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0010.png">
@@ -589,7 +589,7 @@ volatile不保证原子性只保证可见性和禁止指令重排
## CPU术语介绍
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0011.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0011.png">
@@ -717,7 +717,7 @@ public class VolatileExample {
**1、下面是保守策略下volatile写插入内存屏障后生成的指令序列示意图**
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0012.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0012.png">
> 图中的StoreStore屏障可以保证在volatile写之前其前面的所有普通写操作已经对任 意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。这里比较有意思的是volatile写后面的StoreLoad屏障。此屏障的作用是避免volatile写与 后面可能有的volatile读/写操作重排序。因为编译器常常无法准确判断在一个volatile写的后面 是否需要插入一个StoreLoad屏障比如一个volatile写之后方法立即return。为了保证能正确 实现volatile的内存语义JMM在采取了保守策略在每个volatile写的后面或者在每个volatile 读的前面插入一个StoreLoad屏障。从整体执行效率的角度考虑JMM最终选择了在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见使用模式是:一个 写线程写volatile变量多个读线程读同一个volatile变量。当读线程的数量大大超过写线程时 选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里可以看到JMM在实现上的一个特点首先确保正确性然后再去追求执行效率
@@ -725,7 +725,7 @@ public class VolatileExample {
**2、下面是在保守策略下volatile读插入内存屏障后生成的指令序列示意图**
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0013.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0013.png">
> 图中的LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。 LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。 上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。
@@ -752,7 +752,7 @@ class VolatileBarrierExample {
针对readAndWrite()方法,编译器在生成字节码时可以做如下的优化
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0014.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0014.png">
注意最后的StoreLoad屏障不能省略。因为第二个volatile写之后方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写为了安全起见编译器通常会在这里插入一个StoreLoad屏障。
@@ -766,7 +766,7 @@ class VolatileBarrierExample {
X86处理器仅会对写-读操作做重排序。X86不会对读-读、读-写和写-写操作 做重排序因此在X86处理器中会省略掉这3种操作类型对应的内存屏障。在X86中JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义。这意味着在X86处理器中volatile写的开销比volatile读的开销会大很多因为执行StoreLoad屏障开销会比较大
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0015.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0015.png">

View File

@@ -6,19 +6,19 @@ tags:
- 源码
categories:
- Java并发
- 原理
keywords: Java并发原理源码
description: '万字系列长文讲解-Java并发体系-第二阶段,从C++和硬件方面讲解。'
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/logo_1.png'
top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg'
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/logo_1.png'
abbrlink: '8210870'
date: 2020-10-19 22:10:58
date: 2020-10-07 22:10:58
---
# 可见性设计的硬件
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0016.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0016.png">
从硬件的级别来考虑一下可见性的问题
@@ -305,13 +305,13 @@ MESI协议规定了一组消息就说各个处理器在操作内存数据的
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0017.jpg">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0017.jpg">
## MESI-优化
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0018.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0018.png">
MESI协议如果每次写数据的时候都要发送invalidate消息等待所有处理器返回ack然后获取独占锁后才能写数据那可能就会导致性能很差了因为这个对共享变量的写操作实际上在硬件级别变成串行的了。所以为了解决这个问题硬件层面引入了写缓冲器和无效队列
@@ -449,7 +449,7 @@ int b = c; //load
## 相关术语
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0019.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0019.png">
@@ -461,7 +461,7 @@ int b = c; //load
**第一个机制是通过总线锁保证原子性。**如果多个处理器同时对共享变量进行读改写操作 i++就是经典的读改写操作),那么共享变量就会被多个处理器同时进行操作,这样读改写操 作就不是原子的操作完之后共享变量的值会和期望的不一致。举个例子如果i=1我们进行 两次i++操作我们期望的结果是3但是有可能结果是2如图所示。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0020.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0020.png">
原因可能是多个处理器同时从各自的缓存中读取变量i分别进行加1操作然后分别写入 系统内存中。那么想要保证读改写共享变量的操作是原子的就必须保证CPU1读改写共享 变量的时候CPU2不能操作缓存了该共享变量内存地址的缓存。

View File

@@ -6,12 +6,12 @@ tags:
- 源码
categories:
- Java并发
- 原理
keywords: Java并发原理源码
description: '万字系列长文讲解-Java并发体系-第二阶段,从C++和硬件方面讲解。'
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/logo_1.png'
top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg'
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/logo_1.png'
abbrlink: 113a3931
date: 2020-10-19 22:10:58
date: 2020-10-08 22:10:58
---
@@ -357,17 +357,17 @@ monitorexit释放锁。 monitorexit插入在方法结束处和异常处JVM保
术语参考: http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html 在JVM中对象在内存中的布局分为三块区域对象头、实例数据和对齐填充。如下图所示
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0021.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0021.png">
## 对象头
当一个线程尝试访问synchronized修饰的代码块时它首先要获得锁那么这个锁到底存在哪里呢是 存在锁对象的对象头中的。 HotSpot采用instanceOopDesc和arrayOopDesc来描述对象头arrayOopDesc对象用来描述数组类型。instanceOopDesc的定义的在Hotspot源码的 instanceOop.hpp 文件中另外arrayOopDesc 的定义对应 arrayOop.hpp
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0022.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0022.png">
从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDescoopDesc的定义载Hotspot 源码中的 oop.hpp 文件中。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0023.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0023.png">
- 在普通实例对象中oopDesc的定义包含两个成员分别是 _mark 和 _metadata
@@ -383,21 +383,21 @@ monitorexit释放锁。 monitorexit插入在方法结束处和异常处JVM保
Mark Word用于存储对象自身的运行时数据如哈希码HashCode、GC分代年龄、锁状态标志、 线程持有的锁、偏向线程ID、偏向时间戳等等占用内存大小与虚拟机位长一致。Mark Word对应的类 型是 markOop 。源码位于 markOop.hpp 中。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0024.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0024.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0025.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0025.png">
在64位虚拟机下Mark Word是64bit大小的其存储结构如下
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0026.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0026.png">
在32位虚拟机下Mark Word是32bit大小的其存储结构如下
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0027.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0027.png">
再加一个图对比一下,有一丁点的补充
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0028.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0028.png">
@@ -447,7 +447,7 @@ Mark Word用于存储对象自身的运行时数据如哈希码HashCode
线程在执行同步块之前JVM会先在当前的线程的栈帧中创建一个`Lock Record`,其包括一个用于存储对象头中的 `mark word`(官方称之为`Displaced Mark Word`)以及一个指向对象的指针。下图右边的部分就是一个`Lock Record`
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0029.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0029.png">
@@ -571,7 +571,7 @@ synchronized(obj){
}
```
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0030.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0030.png">
## 轻量级锁什么时候升级为重量级锁?
@@ -608,7 +608,7 @@ synchronized(obj){
- 重量级锁的状态下,对象的`mark word`为指向一个堆中monitor对象的指针。一个monitor对象包括这么几个关键字段cxq下图中的ContentionListEntryList WaitSetowner。其中cxq EntryList WaitSet都是由ObjectWaiter的链表结构owner指向持有锁的线程。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0031.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0031.png">
在HotSpot虚拟机中monitor是由ObjectMonitor实现的。其源码是用c++来实现的位于HotSpot虚 拟机源码ObjectMonitor.hpp文件中(src/share/vm/runtime/objectMonitor.hpp)。ObjectMonitor主 要数据结构如下:
@@ -661,7 +661,7 @@ ObjectMonitor() {
1、执行monitorenter时会调用InterpreterRuntime.cpp (位于src/share/vm/interpreter/interpreterRuntime.cpp) 的 InterpreterRuntime::monitorenter函 数。具体代码可参见HotSpot源码。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0032.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0032.png">
@@ -1147,7 +1147,7 @@ if (TryLock(Self) > 0) break ;
可以看到ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptrAtomic::inc_ptr等内核函数 执行同步代码块没有竞争到锁的对象会park()被挂起竞争到锁的线程会unpark()唤醒。这个时候就 会存在操作系统用户态和内核态的转换这种切换会消耗大量的系统资源。所以synchronized是Java语 言中是一个重量级(Heavyweight)的操作。 用户态和和内核态是什么东西呢要想了解用户态和内核态还需要先了解一下Linux系统的体系架构
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Second_stage/0033.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Second_stage/0033.png">
从上图可以看出Linux操作系统的体系架构分为用户空间应用程序的活动空间和内核。 内核:本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。 用户空间上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源包括CPU资源、存 储资源、I/O资源等。 系统调用:为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。

View File

@@ -5,10 +5,11 @@ tags:
- AQS源码
categories:
- Java并发
- 原理
keywords: Java并发AQS源码
description: '万字系列长文讲解-Java并发体系-第四阶段-AQS源码解读-[1]。'
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/logo_1.png'
top_img: 'https://cdn.jsdelivr.net/gh/youthlql/lql_img/blog/top_img.jpg'
cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/logo_1.png'
abbrlink: 92c4503d
date: 2020-10-26 17:59:42
---
@@ -26,8 +27,8 @@ date: 2020-10-26 17:59:42
* <p>
* 可重入锁:
* 1、可重复可递归调用的锁在外层使用锁之后在内层仍然可以使用并且不发生死锁这样的锁就叫做可重入锁。
* 2、是指在同一个线程在外层方法获取锁的时候再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象)
* 不会因为之前已经获取过还没释放而阻塞
* 2、是指在同一个线程在外层方法获取锁的时候再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个
* 对象)不会因为之前已经获取过还没释放而阻塞
*/
public class ReEnterLockDemo {
@@ -307,8 +308,8 @@ LockSupport底层还是UNSAFE前面讲过
- 形象的理解
线程阻塞需要消耗凭证(permit)这个凭证最多只有1个。
当调用park方法时
*如果有凭证,则会直接消耗掉这个凭证然后正常退出;
*如果无凭证,就必须阻塞等待凭证可用;
如果有凭证,则会直接消耗掉这个凭证然后正常退出;
如果无凭证,就必须阻塞等待凭证可用;
而unpark则相反它会增加一个凭证但凭证最多只能有1个累加无效。
我们用LockSupport来测试下之前的异常场景
@@ -409,11 +410,11 @@ Process finished with exit code 0
**技术翻译:**是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石 通过内置的FIFO队列来完成资源获取线程的排队工作并通过一个int类变量`state`表示持有锁的状态。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0001.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0001.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0011.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0011.png">
AbstractOwnableSynchronizer
AbstractQueuedLongSynchronizer
@@ -425,7 +426,7 @@ AbstractQueuedSynchronizer
AQS是一个抽象的父类可以将其理解为一个框架。基于AQS这个框架我们可以实现多种同步器比如下方图中的几个Java内置的同步器。同时我们也可以基于AQS框架实现我们自己的同步器以满足不同的业务场景需求。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0002.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0002.png">
@@ -433,7 +434,7 @@ AQS是一个抽象的父类可以将其理解为一个框架。基于AQS这
加锁会导致阻塞:有阻塞就需要排队,实现排队必然需要有某种形式的队列来进行管理
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0003.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0003.png">
1、抢到资源的线程直接使用办理业务抢占不到资源的线程的必然涉及一种**排队等候机制**,抢占资源失败的线程继续去等待(类似办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
@@ -514,7 +515,7 @@ Node 的数据结构其实也挺简单的,就是 thread + waitStatus + pre + n
## AQS队列基本结构
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0004.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0004.png">
注意排队队列不包括head也就是后文要说的哨兵节点
@@ -582,7 +583,7 @@ public class AQSDemo {
以这样的一个实际例子说明。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0005.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0005.png">
@@ -643,7 +644,8 @@ public class AQSDemo {
public final void acquire(int arg) { // 此时 arg == 1
/*
1、首先调用tryAcquire(1)一下,名字上就知道,这个只是试一试。因为有可能直接就成功了呢,也就不需要进队 列排队了。
1、首先调用tryAcquire(1)一下,名字上就知道,这个只是试一试。因为有可能直接就成功了呢,也就不需要
进队列排队了。
2、有可能成功的情况就是在走到这一步的时候前面占锁的线程刚好释放锁
*/
if (!tryAcquire(arg) &&
@@ -712,7 +714,8 @@ public class AQSDemo {
/*
1、假设tryAcquire(arg) 返回false那么代码将执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这个方法首先需要执行addWaiter(Node.EXCLUSIVE)
1、假设tryAcquire(arg) 返回false那么代码将执行acquireQueued(addWaiter(Node.EXCLUSIVE),
arg)这个方法首先需要执行addWaiter(Node.EXCLUSIVE)
2、此方法的作用是把线程包装成node同时进入到队列中。参数mode此时是Node.EXCLUSIVE代表独占模式
3、以下几行代码想把当前node加到链表的最后面去也就是进到队列的最后
*/
@@ -728,7 +731,8 @@ public class AQSDemo {
// 用CAS把自己设置为队尾, 如果成功后tail == node 了,这个节点成为排队队列新的尾巴
if (compareAndSetTail(pred, node)) {
/*
1、进到这里说明设置成功当前node==tail, 将自己与之前的队尾相连,上面已经有 node.prev = pred加上下面这句也就实现了和之前的尾节点双向连接了
1、进到这里说明设置成功当前node==tail, 将自己与之前的队尾相连,上面已经有
node.prev = pred加上下面这句也就实现了和之前的尾节点双向连接了
*/
pred.next = node;
// 线程入队了,可以返回了
@@ -761,7 +765,7 @@ public class AQSDemo {
2、C在if逻辑里准备入队进行相应设置后变成下面这样。
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0006.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0006.png">
@@ -778,7 +782,8 @@ public class AQSDemo {
Node t = tail;
/*
1、进入这个分支说明是队列为空的这种情况那么就准备初始化一个空的节点new Node()作为排队队列 的head。
1、进入这个分支说明是队列为空的这种情况那么就准备初始化一个空的节点new Node()
作为排队队列的head。
*/
if (t == null) { // Must initialize
/*
@@ -786,15 +791,22 @@ public class AQSDemo {
2、还是一步CAS因为可能是很多线程同时进来呢
*/
if (compareAndSetHead(new Node()))
/*
1、注意这里传的参数是new Node()说明是一个空的节点并不是我们B线程封装的节点这 个空节点只作为占位符称作傀儡节点或者哨兵节点。这个时候head节点的waitStatus==0, 看 new Node()构造方法就知道了。注意new Node()虽然是空节点但他不是null
2、这个时候有了head但是tail还是null设置一下把tail指向head放心马上就有线程要 来了到时候tail就要被抢了
3、注意这里只是设置了tail=head这里可没return哦。所以设置完了以后继续for循环 下次就到下面的else分支了
*/
/*
1、注意这里传的参数是new Node()说明是一个空的节点并不是我们B线程封装的节点
这个空节点只作为占位符,称作傀儡节点或者哨兵节点)。这个时候head节点的waitStatus==0,
看new Node()构造方法就知道了。注意new Node()虽然是空节点但他不是null
2、这个时候有了head但是tail还是null设置一下把tail指向head放心马上就有
线程要来了到时候tail就要被抢了
3、注意这里只是设置了tail=head这里可没return哦。所以设置完了以后继续for
循环下次就到下面的else分支了
*/
tail = head;
} else {
/*
1、下面几行和上一个方法 addWaiter 是一样的,只是这个套在无限循环里,就是将当前线程排到 队尾有线程竞争的话排不上重复排直到排上了再return 【这里看不懂的话就看下面的例子】 */
1、下面几行和上一个方法 addWaiter 是一样的,只是这个套在无限循环里,就是将当前
线程排到队尾有线程竞争的话排不上重复排直到排上了再return
【这里看不懂的话就看下面的例子】
*/
node.prev = t;
if (compareAndSetTail(t, node)) {
@@ -819,7 +831,7 @@ public class AQSDemo {
此时队列变成了下面的样子:
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0007.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0007.png">
3、然后if结束之后继续空的for循环B线程开始了第二轮循环。
@@ -831,11 +843,11 @@ public class AQSDemo {
2、`node.prev = t`进入if之后让B节点的prev指针指向t然后`compareAndSetTail(t, node)`设置尾节点
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0008.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0008.png">
3、CAS设置尾节点成功之后执行if里的逻辑
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0009.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0009.png">
@@ -851,7 +863,8 @@ public class AQSDemo {
&& acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
2、acquireQueued这个方法参数node经过addWaiter(Node.EXCLUSIVE),此时已经进入排队队列队尾
3、注意一下如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的话意味着上面这段代码将 进入selfInterrupt()
3、注意一下如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的话意味着上面这段
代码将进入selfInterrupt()
4、这个方法非常重要真正的线程挂起然后被唤醒后去获取锁都在这个方法里了
*/
final boolean acquireQueued(final Node node, int arg) {
@@ -861,10 +874,13 @@ public class AQSDemo {
for (;;) {
final Node p = node.predecessor();
/*
1、p == head 说明当前节点虽然进到了排队队列但是是队列的第一个因为它的前驱是head(或者说 是哨兵节点因为head指向了哨兵节点
1、p == head 说明当前节点虽然进到了排队队列但是是队列的第一个因为它的前驱是head
或者说是哨兵节点因为head指向了哨兵节点
2、注意队列不包含head节点head一般指的是占有锁的线程head后面的才称为排队队列
3、所以当前节点可以去试抢一下锁
4、这里我们说一下为什么可以去试试它是排队队列队头所以作为队头可以去试一试能不能拿到 锁,因为可能之前的线程已经释放锁了。如果尝试成功,那它就不需要被挂起,直接拿锁,效率会高
4、这里我们说一下为什么可以去试试它是排队队列队头所以作为队头可以去试一试能不能
拿到锁,因为可能之前的线程已经释放锁了。如果尝试成功,那它就不需要被挂起,直接拿锁,
效率会高
5、tryAcquire已经分析过了, 忘记了请往前看一下就是简单用CAS试操作一下state
*/
if (p == head && tryAcquire(arg)) {
@@ -915,7 +931,9 @@ public class AQSDemo {
/*
1、前驱节点 waitStatus大于0 之前说过大于0说明前驱节点取消了排队。
2、这里需要知道这点进入阻塞队列排队的线程会被挂起而唤醒的操作是由前驱节点完成的。所以下面这块代码说 的是将当前节点的prev指向waitStatus<=0的节点简单说就是为了找个好爹因为你还得依赖它来唤醒呢如 果前驱节点取消了排队,找前驱节点的前驱节点做爹,往前遍历总能找到一个好爹的。
2、这里需要知道这点进入阻塞队列排队的线程会被挂起而唤醒的操作是由前驱节点完成的。所以
下面这块代码说的是将当前节点的prev指向waitStatus<=0的节点简单说就是为了找个好爹因为你还
得依赖它来唤醒呢,如果前驱节点取消了排队,找前驱节点的前驱节点做爹,往前遍历总能找到一个好爹的。
*/
if (ws > 0) {
/*
@@ -935,8 +953,13 @@ public class AQSDemo {
/*
1、如果进入到这个分支意味着什么前驱节点的waitStatus不等于-1和1那也就是只可能是0-2-3
在我们前面的源码中都没有看到有设置waitStatus的所以每个新的node入队时waitStatu都是0
2、正常情况下前驱节点是之前的 tail那么它的 waitStatus 应该是 0用CAS将前驱节点的 waitStatus设置为Node.SIGNAL(也就是-1),表示我后面有节点需要被唤醒。
3、这里可以简单说下 waitStatus 中 SIGNAL(-1) 状态的意思Doug Lea 注释的是:代表后继节点需要 被唤醒。也就是说这个 waitStatus 其实代表的不是自己的状态,而是后继节点的状态,我们知道,每个 node 在入队的时候,都会把前驱节点的状态改为 SIGNAL然后阻塞等待被前驱唤醒。这里涉及的是两个问 题:有线程取消了排队、唤醒操作。其实本质是一样的,读者也可以顺着 “waitStatus代表后继节点的状态” 这种思路去看一遍源码
2、正常情况下前驱节点是之前的 tail那么它的 waitStatus 应该是 0用CAS将前驱节点
的waitStatus设置为Node.SIGNAL(也就是-1),表示我后面有节点需要被唤醒
3、这里可以简单说下 waitStatus 中 SIGNAL(-1) 状态的意思Doug Lea 注释的是:代表后继
节点需要被唤醒。也就是说这个 waitStatus 其实代表的不是自己的状态,而是后继节点的状态,
我们知道每个node 在入队的时候,都会把前驱节点的状态改为 SIGNAL然后阻塞等待被前驱唤醒。
这里涉及的是两个问 题:有线程取消了排队、唤醒操作。其实本质是一样的,读者也可以顺着
“waitStatus代表后继节点的状态”这种思路去看一遍源码。
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
@@ -993,11 +1016,15 @@ public class AQSDemo {
}
/*
1、接下来说说如果shouldParkAfterFailedAcquire(p, node)返回false的情况
2、仔细看shouldParkAfterFailedAcquire(p, node),我们可以发现,其实第一次进来的时候,一般都不会返回 true的原因很简单前驱节点的waitStatus=-1是依赖于后继节点设置的。也就是说我都还没给前驱设置-1呢怎么 可能是true呢但是要看到这个方法是套在循环里的所以第二次进来的时候状态就是-1了。
2、仔细看shouldParkAfterFailedAcquire(p, node),我们可以发现,其实第一次进来的时候,一般都不会
返回true的原因很简单前驱节点的waitStatus=-1是依赖于后继节点设置的。也就是说我都还没给前驱
设置-1呢怎么可能是true呢但是要看到这个方法是套在循环里的所以第二次进来的时候状态就是-1了。
3、解释下为什么shouldParkAfterFailedAcquire(p, node)返回false的时候不直接挂起线程
主要是为了应对在经过这个方法后node已经是head的直接后继节点了。
4、假设返回fasle的时候node已经是head的直接后继节点了但是你直接挂起了线程就要走别人唤醒你的那几步代 码。那这里完全可以重新走一遍for循环直接尝试下获取锁可能会更快。注意是可能不代表一定因为你也无法确定 unparkSuccessor释放锁通知后继节点这个方法执行的快慢。但是你多尝试一次获取锁总归是快的。
4、假设返回fasle的时候node已经是head的直接后继节点了但是你直接挂起了线程就要走别人唤醒你的那
几步代码。那这里完全可以重新走一遍for循环直接尝试下获取锁可能会更快。注意是可能不代表一定因为
你也无法确定unparkSuccessor释放锁通知后继节点这个方法执行的快慢。但是你多尝试一次获取锁总归是快的。
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
@@ -1074,7 +1101,8 @@ private void unparkSuccessor(Node node) {
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
1、下面的代码就是唤醒后继节点但是有可能后继节点取消了等待waitStatus==1从队尾往前找找到 waitStatus<=0的所有节点中排在最前面的
1、下面的代码就是唤醒后继节点但是有可能后继节点取消了等待waitStatus==1从队尾往前找
找到waitStatus<=0的所有节点中排在最前面的
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
@@ -1169,7 +1197,7 @@ protected final void setExclusiveOwnerThread(Thread thread) {
<img src="https://cdn.jsdelivr.net/gh/youthlql/lql_img/Java_concurrency/Source_code/Fourth_stage/0010.png">
<img src="https://cdn.jsdelivr.net/gh/youthlql/lqlp@v1.0.0/Java_concurrency/Source_code/Fourth_stage/0010.png">