Skip to content

Commit 3770118

Browse files
committed
📝 Writing docs.
1 parent d86e658 commit 3770118

1 file changed

Lines changed: 328 additions & 14 deletions

File tree

docs/concurrent/Java并发容器.md

Lines changed: 328 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ tags:
1717

1818
- [概述](#概述)
1919
- [ConcurrentHashMap](#concurrenthashmap)
20+
- [要点](#要点)
21+
- [源码](#源码)
22+
- [示例](#示例)
2023
- [CopyOnWriteArrayList](#copyonwritearraylist)
24+
- [要点](#要点-1)
25+
- [源码](#源码-1)
26+
- [示例](#示例-1)
2127
- [资料](#资料)
2228

2329
<!-- /TOC -->
@@ -26,31 +32,339 @@ tags:
2632

2733
JDK 的 `java.util.concurrent` 包(即 juc)中提供了几个非常有用的并发容器。
2834

29-
| 类名 | 类型 | 功能 |
30-
| --------------------- | ----- | --------------------------------------------------------------------------------------------------- |
31-
| CopyOnWriteArrayList | List | 线程安全的 ArrayList |
32-
| CopyOnWriteArraySet | Set | 线程安全的 Set,它内部包含了一个 CopyOnWriteArrayList,因此本质上是由 CopyOnWriteArrayList 实现的。 |
33-
| ConcurrentSkipListSet | Set | 相当于线程安全的 TreeSet。它是有序的 Set。它由 ConcurrentSkipListMap 实现。 |
34-
| ConcurrentHashMap | Map | 线程安全的 HashMap。采用分段锁实现高效并发。 |
35-
| ConcurrentSkipListMap | Map | 线程安全的有序 Map。使用跳表实现高效并发。 |
36-
| ConcurrentLinkedQueue | Queue | 线程安全的无界队列。底层采用单链表。支持 FIFO。 |
37-
| ConcurrentLinkedDeque | Queue | 线程安全的无界双端队列。底层采用双向链表。支持 FIFO 和 FILO。 |
38-
| ArrayBlockingQueue | Queue | 数组实现的阻塞队列。 |
39-
| LinkedBlockingQueue | Queue | 链表实现的阻塞队列。 |
40-
| LinkedBlockingDeque | Queue | 双向链表实现的双端阻塞队列。 |
35+
* CopyOnWriteArrayList - 线程安全的 ArrayList
36+
* CopyOnWriteArraySet - 线程安全的 Set,它内部包含了一个 CopyOnWriteArrayList,因此本质上是由 CopyOnWriteArrayList 实现的。
37+
* ConcurrentSkipListSet - 相当于线程安全的 TreeSet。它是有序的 Set。它由 ConcurrentSkipListMap 实现。
38+
* ConcurrentHashMap - 线程安全的 HashMap。采用分段锁实现高效并发。
39+
* ConcurrentSkipListMap - 线程安全的有序 Map。使用跳表实现高效并发。
40+
* ConcurrentLinkedQueue - 线程安全的无界队列。底层采用单链表。支持 FIFO。
41+
* ConcurrentLinkedDeque - 线程安全的无界双端队列。底层采用双向链表。支持 FIFO 和 FILO。
42+
* ArrayBlockingQueue - 数组实现的阻塞队列。
43+
* LinkedBlockingQueue - 链表实现的阻塞队列。
44+
* LinkedBlockingDeque - 双向链表实现的双端阻塞队列。
4145

4246
## ConcurrentHashMap
4347

48+
### 要点
49+
4450
* 作用:ConcurrentHashMap 是线程安全的 HashMap。
45-
* 原理:ConcurrentHashMap 采用了分段锁机制实现高效的并发访问。
51+
* 原理:JDK6 与 JDK7 中,ConcurrentHashMap 采用了分段锁机制。JDK8 中,摒弃了锁分段机制,改为利用 CAS 算法。
52+
53+
### 源码
54+
55+
#### JDK7
56+
57+
ConcurrentHashMap 类在 jdk1.7 中的设计,其基本结构如图所示:
58+
59+
<p align="center">
60+
<img src="https://raw.githubusercontent.com/dunwu/javase-notes/master/images/concurrent/ConcurrentHashMap-jdk7.png">
61+
</p>
62+
63+
每一个 segment 都是一个 HashEntry<K,V>[] table, table 中的每一个元素本质上都是一个 HashEntry 的单向队列。比如 table[3]为首节点,table[3]->next 为节点 1,之后为节点 2,依次类推。
64+
65+
```java
66+
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
67+
implements ConcurrentMap<K, V>, Serializable {
68+
69+
// 将整个hashmap分成几个小的map,每个segment都是一个锁;与hashtable相比,这么设计的目的是对于put, remove等操作,可以减少并发冲突,对
70+
// 不属于同一个片段的节点可以并发操作,大大提高了性能
71+
final Segment<K,V>[] segments;
72+
73+
// 本质上Segment类就是一个小的hashmap,里面table数组存储了各个节点的数据,继承了ReentrantLock, 可以作为互拆锁使用
74+
static final class Segment<K,V> extends ReentrantLock implements Serializable {
75+
transient volatile HashEntry<K,V>[] table;
76+
transient int count;
77+
}
78+
79+
// 基本节点,存储Key, Value值
80+
static final class HashEntry<K,V> {
81+
final int hash;
82+
final K key;
83+
volatile V value;
84+
volatile HashEntry<K,V> next;
85+
}
86+
}
87+
```
88+
89+
#### JDK8
90+
91+
* jdk8 中主要做了 2 方面的改进
92+
* 取消 segments 字段,直接采用 `transient volatile HashEntry<K,V>[] table` 保存数据,采用 table 数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。
93+
* 将原先 table 数组+单向链表的数据结构,变更为 table 数组+单向链表+红黑树的结构。对于 hash 表来说,最核心的能力在于将 key hash 之后能均匀的分布在数组中。如果 hash 之后散列的很均匀,那么 table 数组中的每个队列长度主要为 0 或者 1。但实际情况并非总是如此理想,虽然 ConcurrentHashMap 类默认的加载因子为 0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为 O(n);因此,对于个数超过 8(默认值)的列表,jdk1.8 中采用了红黑树的结构,那么查询的时间复杂度可以降低到 O(logN),可以改进性能。
94+
95+
```java
96+
final V putVal(K key, V value, boolean onlyIfAbsent) {
97+
if (key == null || value == null) throw new NullPointerException();
98+
int hash = spread(key.hashCode());
99+
int binCount = 0;
100+
for (Node<K,V>[] tab = table;;) {
101+
Node<K,V> f; int n, i, fh;
102+
// 如果table为空,初始化;否则,根据hash值计算得到数组索引i,如果tab[i]为空,直接新建节点Node即可。注:tab[i]实质为链表或者红黑树的首节点。
103+
if (tab == null || (n = tab.length) == 0)
104+
tab = initTable();
105+
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
106+
if (casTabAt(tab, i, null,
107+
new Node<K,V>(hash, key, value, null)))
108+
break; // no lock when adding to empty bin
109+
}
110+
// 如果tab[i]不为空并且hash值为MOVED,说明该链表正在进行transfer操作,返回扩容完成后的table。
111+
else if ((fh = f.hash) == MOVED)
112+
tab = helpTransfer(tab, f);
113+
else {
114+
V oldVal = null;
115+
// 针对首个节点进行加锁操作,而不是segment,进一步减少线程冲突
116+
synchronized (f) {
117+
if (tabAt(tab, i) == f) {
118+
if (fh >= 0) {
119+
binCount = 1;
120+
for (Node<K,V> e = f;; ++binCount) {
121+
K ek;
122+
// 如果在链表中找到值为key的节点e,直接设置e.val = value即可。
123+
if (e.hash == hash &&
124+
((ek = e.key) == key ||
125+
(ek != null && key.equals(ek)))) {
126+
oldVal = e.val;
127+
if (!onlyIfAbsent)
128+
e.val = value;
129+
break;
130+
}
131+
// 如果没有找到值为key的节点,直接新建Node并加入链表即可。
132+
Node<K,V> pred = e;
133+
if ((e = e.next) == null) {
134+
pred.next = new Node<K,V>(hash, key,
135+
value, null);
136+
break;
137+
}
138+
}
139+
}
140+
// 如果首节点为TreeBin类型,说明为红黑树结构,执行putTreeVal操作。
141+
else if (f instanceof TreeBin) {
142+
Node<K,V> p;
143+
binCount = 2;
144+
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
145+
value)) != null) {
146+
oldVal = p.val;
147+
if (!onlyIfAbsent)
148+
p.val = value;
149+
}
150+
}
151+
}
152+
}
153+
if (binCount != 0) {
154+
// 如果节点数>=8,那么转换链表结构为红黑树结构。
155+
if (binCount >= TREEIFY_THRESHOLD)
156+
treeifyBin(tab, i);
157+
if (oldVal != null)
158+
return oldVal;
159+
break;
160+
}
161+
}
162+
}
163+
// 计数增加1,有可能触发transfer操作(扩容)。
164+
addCount(1L, binCount);
165+
return null;
166+
}
167+
```
168+
169+
### 示例
170+
171+
```java
172+
public class ConcurrentHashMapDemo {
173+
174+
public static void main(String[] args) throws InterruptedException {
175+
176+
// HashMap 在并发迭代访问时会抛出 ConcurrentModificationException 异常
177+
// Map<Integer, Character> map = new HashMap<>();
178+
Map<Integer, Character> map = new ConcurrentHashMap<>();
179+
180+
Thread wthread = new Thread(() -> {
181+
System.out.println("写操作线程开始执行");
182+
for (int i = 0; i < 26; i++) {
183+
map.put(i, (char) ('a' + i));
184+
}
185+
});
186+
Thread rthread = new Thread(() -> {
187+
System.out.println("读操作线程开始执行");
188+
for (Integer key : map.keySet()) {
189+
System.out.println(key + " - " + map.get(key));
190+
}
191+
});
192+
wthread.start();
193+
rthread.start();
194+
Thread.sleep(1000);
195+
}
196+
}
197+
```
46198

47199
## CopyOnWriteArrayList
48200

201+
### 要点
202+
49203
* 作用:CopyOnWrite 字面意思为写入时复制。CopyOnWriteArrayList 是线程安全的 ArrayList。
50-
* 原理:写入时复制(CopyOnWrite)容器的迭代器保留一个指向底层基础数组的引用,这个数组当前位于迭代器的起始位置,由于它不会被修改,因此在对其进行同步时只需确保数组内容的可见性。
204+
* 原理:
205+
* 在 CopyOnWriteAarrayList 中,读操作不同步,因为它们在内部数组的快照上工作,所以多个迭代器可以同时遍历而不会相互阻塞(1,2,4)。
206+
* 所有的写操作都是同步的。他们在备份数组(3)的副本上工作。写操作完成后,后备阵列将被替换为复制的阵列,并释放锁定。支持数组变得易变,所以替换数组的调用是原子(5)。
207+
* 写操作后创建的迭代器将能够看到修改的结构(6,7)。
208+
* 写时复制集合返回的迭代器不会抛出 ConcurrentModificationException,因为它们在数组的快照上工作,并且无论后续的修改(2,4)如何,都会像迭代器创建时那样完全返回元素。
209+
210+
<p align="center">
211+
<img src="https://raw.githubusercontent.com/dunwu/javase-notes/master/images/concurrent/CopyOnWriteArrayList.png">
212+
</p>
213+
214+
### 源码
215+
216+
#### 重要属性
217+
218+
* lock - 执行写时复制操作,需要使用可重入锁加锁
219+
* array - 对象数组,用于存放元素
220+
221+
```java
222+
/** The lock protecting all mutators */
223+
final transient ReentrantLock lock = new ReentrantLock();
224+
225+
/** The array, accessed only via getArray/setArray. */
226+
private transient volatile Object[] array;
227+
```
228+
229+
#### 重要方法
230+
231+
* 添加操作
232+
* 添加的逻辑很简单,先将原容器 copy 一份,然后在新副本上执行写操作,之后再切换引用。当然此过程是要加锁的。
233+
234+
```java
235+
public boolean add(E e) {
236+
//ReentrantLock加锁,保证线程安全
237+
final ReentrantLock lock = this.lock;
238+
lock.lock();
239+
try {
240+
Object[] elements = getArray();
241+
int len = elements.length;
242+
//拷贝原容器,长度为原容器长度加一
243+
Object[] newElements = Arrays.copyOf(elements, len + 1);
244+
//在新副本上执行添加操作
245+
newElements[len] = e;
246+
//将原容器引用指向新副本
247+
setArray(newElements);
248+
return true;
249+
} finally {
250+
//解锁
251+
lock.unlock();
252+
}
253+
}
254+
```
255+
256+
* 删除操作
257+
* 删除操作同理,将除要删除元素之外的其他元素拷贝到新副本中,然后切换引用,将原容器引用指向新副本。同属写操作,需要加锁。
258+
259+
```java
260+
public E remove(int index) {
261+
//加锁
262+
final ReentrantLock lock = this.lock;
263+
lock.lock();
264+
try {
265+
Object[] elements = getArray();
266+
int len = elements.length;
267+
E oldValue = get(elements, index);
268+
int numMoved = len - index - 1;
269+
if (numMoved == 0)
270+
//如果要删除的是列表末端数据,拷贝前len-1个数据到新副本上,再切换引用
271+
setArray(Arrays.copyOf(elements, len - 1));
272+
else {
273+
//否则,将除要删除元素之外的其他元素拷贝到新副本中,并切换引用
274+
Object[] newElements = new Object[len - 1];
275+
System.arraycopy(elements, 0, newElements, 0, index);
276+
System.arraycopy(elements, index + 1, newElements, index,
277+
numMoved);
278+
setArray(newElements);
279+
}
280+
return oldValue;
281+
} finally {
282+
//解锁
283+
lock.unlock();
284+
}
285+
}
286+
```
287+
288+
* 读操作
289+
* CopyOnWriteArrayList 的读操作是不用加锁的,性能很高。
290+
291+
```java
292+
public E get(int index) {
293+
return get(getArray(), index);
294+
}
295+
private E get(Object[] a, int index) {
296+
return (E) a[index];
297+
}
298+
```
299+
300+
### 示例
301+
302+
```java
303+
public class CopyOnWriteArrayListDemo {
304+
305+
static class ReadTask implements Runnable {
306+
307+
List<String> list;
308+
309+
ReadTask(List<String> list) {
310+
this.list = list;
311+
}
312+
313+
public void run() {
314+
for (String str : list) {
315+
System.out.println(str);
316+
}
317+
}
318+
}
319+
320+
static class WriteTask implements Runnable {
321+
322+
List<String> list;
323+
int index;
324+
325+
WriteTask(List<String> list, int index) {
326+
this.list = list;
327+
this.index = index;
328+
}
329+
330+
public void run() {
331+
list.remove(index);
332+
list.add(index, "write_" + index);
333+
}
334+
}
335+
336+
public void run() {
337+
final int NUM = 10;
338+
// ArrayList 在并发迭代访问时会抛出 ConcurrentModificationException 异常
339+
// List<String> list = new ArrayList<>();
340+
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
341+
for (int i = 0; i < NUM; i++) {
342+
list.add("main_" + i);
343+
}
344+
ExecutorService executorService = Executors.newFixedThreadPool(NUM);
345+
for (int i = 0; i < NUM; i++) {
346+
executorService.execute(new ReadTask(list));
347+
executorService.execute(new WriteTask(list, i));
348+
}
349+
executorService.shutdown();
350+
}
351+
352+
public static void main(String[] args) {
353+
new CopyOnWriteArrayListDemo().run();
354+
}
355+
}
356+
```
51357

52358
## 资料
53359

54360
* [Java 并发编程实战](https://item.jd.com/10922250.html)
55361
* [Java 并发编程的艺术](https://item.jd.com/11740734.html)
56362
* https://blog.csdn.net/u010425776/article/details/54890215
363+
* https://blog.csdn.net/wangxiaotongfan/article/details/52074160
364+
* https://my.oschina.net/hosee/blog/675884
365+
* https://www.jianshu.com/p/c0642afe03e0
366+
* https://www.jianshu.com/p/f6730d5784ad
367+
* http://www.javarticles.com/2012/06/copyonwritearraylist.html
368+
* https://www.cnblogs.com/xrq730/p/5020760.html
369+
* https://www.cnblogs.com/leesf456/p/5547853.html
370+
* http://www.cnblogs.com/chengxiao/p/6881974.html

0 commit comments

Comments
 (0)