Skip to content

Commit 31fe1cc

Browse files
committed
📝 Writing docs.
1 parent 389a847 commit 31fe1cc

3 files changed

Lines changed: 276 additions & 8 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
title: Java 原子操作类
3+
date: 2018/05/18
4+
categories:
5+
- javase
6+
tags:
7+
- javase
8+
- concurrent
9+
- juc
10+
---
11+
12+
# Java 原子操作类
13+
14+
> 本文内容基于 JDK1.8。
15+
16+
<!-- TOC depthFrom:2 depthTo:4 -->
17+
18+
- [原子更新基本类型](#原子更新基本类型)
19+
- [原子更新数组](#原子更新数组)
20+
- [原子更新引用类型](#原子更新引用类型)
21+
- [原子更新字段类](#原子更新字段类)
22+
- [资料](#资料)
23+
24+
<!-- /TOC -->
25+
26+
## 原子更新基本类型
27+
28+
* AtomicBoolean - 原子更新布尔类型。
29+
* AtomicInteger - 原子更新整型。
30+
* AtomicLong - 原子更新长整型。
31+
32+
## 原子更新数组
33+
34+
* AtomicIntegerArray - 原子更新整型数组里的元素。
35+
* AtomicLongArray - 原子更新长整型数组里的元素。
36+
* AtomicReferenceArray - 原子更新引用类型数组的元素。
37+
* AtomicBooleanArray - 原子更新布尔类型数组的元素。
38+
39+
## 原子更新引用类型
40+
41+
* AtomicReference - 原子更新引用类型。
42+
* AtomicReferenceFieldUpdater - 原子更新引用类型里的字段。
43+
* AtomicMarkableReference - 原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和应用类型。
44+
45+
## 原子更新字段类
46+
47+
* AtomicIntegerFieldUpdater - 原子更新整型的字段的更新器。
48+
* AtomicLongFieldUpdater - 原子更新长整型字段的更新器。
49+
* AtomicStampedReference - 原子更新带有版本号的引用类型。该类将整型数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
50+
51+
## 资料
52+
53+
* [Java 并发编程实战](https://item.jd.com/10922250.html)
54+
* [Java 并发编程的艺术](https://item.jd.com/11740734.html)
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
---
2+
title: Java非阻塞机制
3+
date: 2018/05/19
4+
categories:
5+
- Javase
6+
tags:
7+
- Javase
8+
- concurrent
9+
- juc
10+
---
11+
12+
# Java 非阻塞机制
13+
14+
> 本文内容基于 JDK1.8。
15+
16+
<!-- TOC depthFrom:2 depthTo:3 -->
17+
18+
- [concurrent 包的实现](#concurrent-包的实现)
19+
- [CAS](#cas)
20+
- [简介](#简介)
21+
- [操作](#操作)
22+
- [应用](#应用)
23+
- [原理](#原理)
24+
- [特点](#特点)
25+
- [总结](#总结)
26+
- [资料](#资料)
27+
28+
<!-- /TOC -->
29+
30+
## concurrent 包的实现
31+
32+
由于 Java 的 CAS 同时具有 volatile 读和 volatile 写的内存语义,因此 Java 线程之间的通信现在有了下面四种方式:
33+
34+
1. A 线程写 volatile 变量,随后 B 线程读这个 volatile 变量。
35+
2. A 线程写 volatile 变量,随后 B 线程用 CAS 更新这个 volatile 变量。
36+
3. A 线程用 CAS 更新一个 volatile 变量,随后 B 线程用 CAS 更新这个 volatile 变量。
37+
4. A 线程用 CAS 更新一个 volatile 变量,随后 B 线程读这个 volatile 变量。
38+
39+
同时,volatile 变量的读/写和 CAS 可以实现线程之间的通信。把这些特性整合在一起,就形成了整个 concurrent 包得以实现的基石。如果我们仔细分析 concurrent 包的源代码实现,会发现一个通用化的实现模式:
40+
41+
首先,声明共享变量为 volatile;
42+
43+
然后,使用 CAS 的原子条件更新来实现线程之间的同步;
44+
45+
同时,配合以 volatile 的读/写和 CAS 所具有的 volatile 读和写的内存语义来实现线程之间的通信。
46+
47+
AQS,非阻塞数据结构和原子变量类(Java.util.concurrent.atomic 包中的类),这些 concurrent 包中的基础类都是使用这种模式来实现的,而 concurrent 包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent 包的实现示意图如下:
48+
49+
<p align="center">
50+
<img src="https://raw.githubusercontent.com/dunwu/Javase-notes/master/images/concurrent/juc-architecture.png">
51+
</p>
52+
53+
## CAS
54+
55+
### 简介
56+
57+
CAS(Compare and Swap),字面意思为比较并交换。CAS 有 3 个操作数,内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。
58+
59+
### 操作
60+
61+
我们常常做这样的操作
62+
63+
```Java
64+
if(a==b) {
65+
a++;
66+
}
67+
```
68+
69+
试想一下如果在做 a++之前 a 的值被改变了怎么办?a++还执行吗?出现该问题的原因是在多线程环境下,a 的值处于一种不定的状态。采用锁可以解决此类问题,但 CAS 也可以解决,而且可以不加锁。
70+
71+
```Java
72+
int expect = a;
73+
if(a.compareAndSet(expect,a+1)) {
74+
doSomeThing1();
75+
} else {
76+
doSomeThing2();
77+
}
78+
```
79+
80+
这样如果 a 的值被改变了 a++就不会被执行。按照上面的写法,a!=expect 之后,a++就不会被执行,如果我们还是想执行 a++操作怎么办,没关系,可以采用 while 循环
81+
82+
```Java
83+
while(true) {
84+
int expect = a;
85+
if (a.compareAndSet(expect, a + 1)) {
86+
doSomeThing1();
87+
return;
88+
} else {
89+
doSomeThing2();
90+
}
91+
}
92+
```
93+
94+
采用上面的写法,在没有锁的情况下实现了 a++操作,这实际上是一种非阻塞算法。
95+
96+
### 应用
97+
98+
非阻塞算法 (nonblocking algorithms)
99+
100+
一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。
101+
102+
现代的 CPU 提供了特殊的指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。
103+
104+
拿出 AtomicInteger 来研究在没有锁的情况下是如何做到数据正确性的。
105+
106+
```Java
107+
private volatile int value;
108+
```
109+
110+
首先毫无疑问,在没有锁的机制下可能需要借助 volatile 原语,保证线程间的数据是可见的(共享的)。
111+
112+
这样才获取变量的值的时候才能直接读取。
113+
114+
```Java
115+
public final int get() {
116+
return value;
117+
}
118+
```
119+
120+
然后来看看++i 是怎么做到的。
121+
122+
```Java
123+
public final int incrementAndGet() {
124+
for (;;) {
125+
int current = get();
126+
int next = current + 1;
127+
if (compareAndSet(current, next))
128+
return next;
129+
}
130+
}
131+
```
132+
133+
在这里采用了 CAS 操作,每次从内存中读取数据然后将此数据和+1 后的结果进行 CAS 操作,如果成功就返回结果,否则重试直到成功为止。
134+
135+
而 compareAndSet 利用 JNI 来完成 CPU 指令的操作。
136+
137+
```Java
138+
public final boolean compareAndSet(int expect, int update) {
139+
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
140+
}
141+
```
142+
143+
整体的过程就是这样子的,利用 CPU 的 CAS 指令,同时借助 JNI 来完成 Java 的非阻塞算法。其它原子操作都是利用类似的特性完成的。
144+
145+
其中 unsafe.compareAndSwapInt(this, valueOffset, expect, update)类似:
146+
147+
```Java
148+
if (this == expect) {
149+
this = update
150+
return true;
151+
} else {
152+
return false;
153+
}
154+
```
155+
156+
那么问题就来了,成功过程中需要 2 个步骤:比较 this == expect,替换 this = update,compareAndSwapInt 如何这两个步骤的原子性呢? 参考 CAS 的原理
157+
158+
### 原理
159+
160+
Java 代码如何确保处理器执行 CAS 操作?
161+
162+
CAS 通过调用 JNI(JNI:Java Native Interface 为 Java 本地调用,允许 Java 调用其他语言。)的代码实现的。JVM 将 CAS 操作编译为底层提供的最有效方法。在支持 CAS 的处理器上,JVM 将它们编译为相应的机器指令;在不支持 CAS 的处理器上,JVM 将使用自旋锁。
163+
164+
### 特点
165+
166+
#### 优点
167+
168+
一般情况下,比锁性能更高。因为 CAS 是一种非阻塞算法,所以其避免了线程被阻塞时的等待时间。
169+
170+
#### 缺点
171+
172+
##### ABA 问题
173+
174+
因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA 问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A-B-A 就会变成 1A-2B-3A。
175+
176+
从 Java1.5 开始 JDK 的 atomic 包里提供了一个类 AtomicStampedReference 来解决 ABA 问题。这个类的 compareAndSet 方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
177+
178+
##### 循环时间长开销大
179+
180+
自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高 CPU 的执行效率。
181+
182+
比较花费 CPU 资源,即使没有任何用也会做一些无用功。
183+
184+
##### 只能保证一个共享变量的原子操作
185+
186+
当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量 i = 2,j=a,合并一下 ij=2a,然后用 CAS 来操作 ij。从 Java1.5 开始 JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。
187+
188+
### 总结
189+
190+
可以用 CAS 在无锁的情况下实现原子操作,但要明确应用场合,非常简单的操作且又不想引入锁可以考虑使用 CAS 操作,当想要非阻塞地完成某一操作也可以考虑 CAS。不推荐在复杂操作中引入 CAS,会使程序可读性变差,且难以测试,同时会出现 ABA 问题。
191+
192+
## 资料
193+
194+
* [Java 并发编程实战](https://item.jd.com/10922250.html)
195+
* [Java 并发编程的艺术](https://item.jd.com/11740734.html)
196+
* https://www.jianshu.com/p/473e14d5ab2d
197+
* https://blog.csdn.net/ls5718/article/details/52563959
198+
* http://tutorials.jenkov.com/java-concurrency/non-blocking-algorithms.html

docs/concurrent/并发术语.md

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,17 @@ tags:
1010

1111
# 并发术语
1212

13-
## 关键词
13+
<!-- TOC depthFrom:2 depthTo:3 -->
1414

15-
术语的关键在于理解,而不在死记硬背,所以本文重点在于
15+
- [进程和线程](#进程和线程)
16+
- [同步和异步](#同步和异步)
17+
- [阻塞和非阻塞](#阻塞和非阻塞)
18+
- [并发和并行](#并发和并行)
19+
- [资料](#资料)
1620

17-
### 进程和线程
21+
<!-- /TOC -->
22+
23+
## 进程和线程
1824

1925
* 什么是进程
2026
* 简言之,进程可视为一个正在运行的程序。
@@ -32,26 +38,36 @@ tags:
3238

3339
> 参考:[进程和线程关系及区别](https://blog.csdn.net/yaosiming2011/article/details/44280797)
3440
35-
### 同步和异步
41+
## 同步和异步
3642

3743
* 例子
3844
* 同步就像是打电话,不挂电话,通话不会结束。
3945
* 异步就像是发短信,发完短信后,就可以做其他事,短信来了,手机会提醒。
40-
* 结论
46+
* 定义
4147
* 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
4248
* 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
4349

4450
<p align="center">
4551
<img src="https://raw.githubusercontent.com/dunwu/javase-notes/master/images/concurrent/synchronous-vs-asynchronous.gif" alt="synchronous-vs-asynchronous">
4652
</p>
4753

48-
### 并发和并行
54+
## 阻塞和非阻塞
55+
56+
* 例子
57+
* 阻塞调用就像是打电话,通话不结束,不能放下。
58+
* 非阻塞调用就像是发短信,发完短信后,就可以做其他事,短信来了,手机会提醒。
59+
* 定义
60+
* 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
61+
* 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
62+
* 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
63+
64+
## 并发和并行
4965

5066
* 例子
5167
* 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
5268
* 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
5369
* 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
54-
* 结论
70+
* 定义
5571
* **并发**的关键是你有处理多个任务的能力,不一定要同时。
5672
* **并行**的关键是你有同时处理多个任务的能力。
5773
* 最关键的点就是:是否是**同时**
@@ -65,4 +81,4 @@ tags:
6581
## 资料
6682

6783
* [进程和线程关系及区别](https://blog.csdn.net/yaosiming2011/article/details/44280797)
68-
* [知乎 - 并发与并行的区别?](https://www.zhihu.com/question/33515481)
84+
* [知乎 - 并发与并行的区别?](https://www.zhihu.com/question/33515481)

0 commit comments

Comments
 (0)