Skip to content

Commit a817816

Browse files
committed
完成多线程章节
1 parent c0cd0ef commit a817816

8 files changed

Lines changed: 45 additions & 46 deletions

RedSpiderConcurrent/12.线程池原理.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
### 为什么要使用线程池
2-
1. 创建、销毁线程需要消耗系统资源,线程池可以**复用已创建的线程**
3-
2. **控制并发的数量**,并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃;(主要原因)
2+
1. 创建、销毁线程需要消耗系统资源,线程池可以 **复用已创建的线程**
3+
2. **控制并发的数量** ,并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃主要原因);
44
3. **可以对线程做统一管理**
55

66
### 线程池的原理
@@ -52,9 +52,9 @@ public ThreadPoolExecutor(int corePoolSize,
5252
- TimeUnit unit:keepAliveTime 的单位
5353
- BlockingQueue workQueue:阻塞队列,维护着等待执行的 Runnable 任务对象
5454
> 常用的阻塞队列:
55-
> 1. LinkedBlockingQueue:链式阻塞队列,底层数据结构是链表,默认大小是Integer.MAX_VALUE,也可以指定大小。
55+
> 1. LinkedBlockingQueue:链式阻塞队列,底层数据结构是链表,默认大小是 Integer.MAX_VALUE,也可以指定大小。
5656
> 2. ArrayBlockingQueue:数组阻塞队列,底层数据结构是数组,需要指定队列的大小。
57-
> 3. SynchronousQueue:同步队列,内部容量为0,每个put操作必须等待一个take操作,反之亦然。
57+
> 3. SynchronousQueue:同步队列,内部容量为 0,每个 put 操作必须等待一个 take 操作,反之亦然。
5858
> 4. DelayQueue:延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。
5959
6060
- ThreadFactory threadFactory:创建线程的工厂 ,用于批量创建线程,统一在创建线程时设置一些参数,如是否守护线程、线程的优先级等。如果不指定,会新建一个默认的线程工厂。
@@ -124,16 +124,16 @@ public void execute(Runnable command) {
124124
```
125125

126126
总结一下处理流程:
127-
1. 线程总数量 < corePoolSize,无论线程是否空闲,都会新建一个核心线程执行任务(让核心线程数量快速达到 corePoolSize,在核心线程数量 < corePoolSize 时)。**注意,这一步需要获得全局锁**
127+
1. 线程总数量 < corePoolSize,无论线程是否空闲,都会新建一个核心线程执行任务(让核心线程数量快速达到 corePoolSize,在核心线程数量 < corePoolSize 时)。 **注意,这一步需要获得全局锁**
128128
2. 线程总数量 >= corePoolSize 时,新来的线程任务会进入任务队列中等待,然后空闲的核心线程会依次去缓存队列中取任务来执行(**体现了线程复用**)。
129-
3. 当缓存队列满了,说明这个时候任务已经多到爆棚,需要一些“临时工”来执行这些任务了。于是会创建非核心线程去执行这个任务。**注意,这一步需要获得全局锁**
129+
3. 当缓存队列满了,说明这个时候任务已经多到爆棚,需要一些“临时工”来执行这些任务了。于是会创建非核心线程去执行这个任务。 **注意,这一步需要获得全局锁**
130130
4. 缓存队列满了, 且总线程数达到了 maximumPoolSize,则会采取上面提到的拒绝策略进行处理。
131-
![](http://concurrent.redspider.group/article/03/imgs/%E7%BA%BF%E7%A8%8B%E6%B1%A0%E4%B8%BB%E8%A6%81%E7%9A%84%E5%A4%84%E7%90%86%E6%B5%81%E7%A8%8B.png)
131+
![](img/线程池主要的处理流程.png)
132132

133133
#### ThreadPoolExecutor 如何做到线程复用的?
134134
ThreadPoolExecutor 在创建线程时,会将线程封装成工作线程 worker,并放入工作线程组中,然后这个 worker 反复从阻塞队列中拿任务去执行。
135135

136-
首先去执行创建这个 worker 时就有的任务,当执行完这个任务后,worker 的生命周期并没有结束,在 while 循环中,worker 会不断地调用 getTask 方法从阻塞队列中获取任务然后调用 task.run() 执行任务,从而达到复用线程的目的。只要 getTask 方法不返回 null,此线程就不会退出。
136+
首先去执行创建这个 worker 时就有的任务,当执行完这个任务后,worker 的生命周期并没有结束,在 while 循环中,worker 会不断地调用 getTask 方法从阻塞队列中获取任务然后调用 task.run() 执行任务,从而达到复用线程的目的。只要 getTask 方法不返回 null此线程就不会退出。
137137

138138
核心线程的会一直卡在 workQueue.take 方法,被阻塞并挂起,不会占用 CPU 资源,直到拿到 Runnable 然后返回(当然如果 allowCoreThreadTimeOut 设置为 true,那么核心线程就会去调用 poll 方法,因为 poll 可能会返回 null,所以这时候核心线程满足超时条件也会被销毁)。
139139

@@ -168,4 +168,3 @@ CacheThreadPool 的运行流程如下:
168168
#### newScheduleThreadPool
169169
创建一个定长线程池,支持定时及周期性任务执行。
170170

171-

RedSpiderConcurrent/13.阻塞队列.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ BlockingQueue 一般用于生产者-消费者模式,生产者是往队列里
66
### BlockingQueue 的操作方法
77

88
方法、处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出
9-
---|---|---|---|---
9+
:-:|:-:|:-:|:-:|:-:
1010
插入方法 | add(e) | offer(e) | put(e) | offer(e, time, unit)
1111
移除方法 | remove() | poll() | take() | poll(time, unit)
1212
检查方法 | element() | peek() | <br> | <br>
@@ -15,15 +15,15 @@ BlockingQueue 一般用于生产者-消费者模式,生产者是往队列里
1515
- 返回特殊值:如果试图的操作无法立即执行,返回一个特殊值,通常是 true / false。
1616
- 一直阻塞:如果试图的操作无法立即执行,则一直阻塞或者响应中断。
1717
- 超时退出:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功,通常是 true / false。
18-
- 不能往阻塞队列中插入 null,会抛出空指针异常。
18+
- 不能往阻塞队列中插入 null会抛出空指针异常。
1919
- 可以访问阻塞队列中的任意元素,调用 remove(o) 可以将队列之中的特定对象移除,但并不高效,尽量避免使用。
2020

2121
### BlockingQueue 的实现类
2222
#### ArrayBlockingQueue
23-
**数组**结构组成的**有界阻塞队列**。可以初始化队列大小, 且一旦初始化不能改变。构造方法中的 fair 表示控制对象的内部锁是否采用公平锁,默认是**非公平锁**
23+
**数组** 结构组成的 **有界阻塞队列**。可以初始化队列大小, 且一旦初始化不能改变。构造方法中的 fair 表示控制对象的内部锁是否采用公平锁,默认是 **非公平锁**
2424

2525
#### LinkedBlockingQueue
26-
**链表**结构组成的**有界**阻塞队列。默认队列的大小是 Integer.MAX_VALUE,也可以指定大小。此队列按照**先进先出**的原则对元素进行排序。
26+
**链表** 结构组成的 **有界** 阻塞队列。默认队列的大小是 Integer.MAX_VALUE,也可以指定大小。此队列按照 **先进先出** 的原则对元素进行排序。
2727

2828
#### DelayQueue
2929
该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素 。注入其中的元素必须实现 java.util.concurrent.Delayed 接口。
@@ -42,9 +42,9 @@ DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据
4242
PriorityBlockingQueue 不会阻塞数据生产者(因为队列是无界的),而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。对于使用默认大小的 LinkedBlockingQueue 也是一样的。
4343

4444
### 阻塞队列的原理
45-
阻塞队列的原理很简单,利用了 Lock 锁的多条件(Condition)阻塞控制。
45+
阻塞队列的原理很简单,利用了 Lock 锁的多条件Condition阻塞控制。
4646

47-
首先是构造器,除了初始化队列的大小和是否是公平锁之外,还对同一个锁(lock)初始化了两个监视器,分别是 notEmpty 和 notFull。这两个监视器的作用目前可以简单理解为标记分组,当该线程是 put 操作时,给他加上监视器 notFull,标记这个线程是一个生产者;当线程是 take 操作时,给他加上监视器 notEmpty,标记这个线程是消费者。
47+
首先是构造器,除了初始化队列的大小和是否是公平锁之外,还对同一个锁lock初始化了两个监视器,分别是 notEmpty 和 notFull。这两个监视器的作用目前可以简单理解为标记分组,当该线程是 put 操作时,给他加上监视器 notFull,标记这个线程是一个生产者;当线程是 take 操作时,给他加上监视器 notEmpty,标记这个线程是消费者。
4848
```java
4949
//数据元素数组
5050
final Object[] items;

RedSpiderConcurrent/14.锁接口和类.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
### synchronized 的不足之处
22
- 如果临界区是只读操作,其实可以多线程一起执行,但使用 synchronized 的话,同一时间只能有一个线程执行。
3-
- synchronized 无法知道线程有没有成功获取到锁
3+
- synchronized 无法知道线程有没有成功获取到锁
44
- 使用 synchronized,如果临界区因为 IO 或者 sleep 方法等原因阻塞了,而当前线程又没有释放锁,就会导致所有线程等待。
55

66
### 锁的几种分类
@@ -25,7 +25,7 @@ synchronized 用的锁和 ReentrantLock,其实都是“排它锁”。也就
2525

2626
### JDK 中有关锁的一些接口和类
2727
#### 抽象类 AQS/AQLS/AOS
28-
AQLS(AbstractQueuedLongSynchronizer)。它的代码跟 AQS 几乎一样,只是把资源的类型变成了 long 类型。AQS 和 AQLS 都继承了一个类叫 AOS(AbstractOwnableSynchronizer)
28+
AQLSAbstractQueuedLongSynchronizer。它的代码跟 AQS 几乎一样,只是把资源的类型变成了 long 类型。AQS 和 AQLS 都继承了一个类叫 AOSAbstractOwnableSynchronizer
2929
```java
3030
// 独占模式,锁的持有者
3131
private transient Thread exclusiveOwnerThread;
@@ -45,7 +45,7 @@ protected final Thread getExclusiveOwnerThread() {
4545

4646

4747
对比项 | Object 监视器 | Condition
48-
---|---|---
48+
:-:|:-:|:-:
4949
前置条件 | 获取对象的锁 | 调用 Lock.lock 获取锁,调用 Lock.newCondition 获取 Condition 对象
5050
调用方式 | 直接调用,比如 object.notify() | 直接调用,比如 condition.await()
5151
等待队列的个数 | 一个 | 多个
@@ -59,7 +59,7 @@ protected final Thread getExclusiveOwnerThread() {
5959
Condition 和 Object 的 wait/notify 基本相似。其中,Condition 的 await 方法对应的是 Object 的 wait 方法,而 Condition 的 signal/signalAll 方法则对应 Object 的 notify/notifyAll()。但 Condition 类似于 Object 的等待/通知机制的加强版。
6060

6161
方法名称 | 描述
62-
--- | ---
62+
:-: | :-:
6363
await() | 当前线程进入等待状态直到被通知(signal)或者中断;当前线程进入运行状态并从 await() 方法返回的场景包括:(1)其他线程调用相同 Condition 对象的 signal/signalAll 方法,并且当前线程被唤醒;(2)其他线程调用 interrupt 方法中断当前线程;
6464
awaitUninterruptibly() | 当前线程进入等待状态直到被通知,在此过程中对中断信号不敏感,不支持中断当前线程
6565
awaitNanos(long) | 当前线程进入等待状态,直到被通知、中断或者超时。如果返回值小于等于 0,可以认定就是超时了
@@ -165,9 +165,9 @@ private transient volatile long state;
165165
private transient int readerOverflow;
166166
```
167167

168-
StampedLock 用 long 类型的变量的前 7 位(LG_READERS)来表示读锁,每获取一个悲观读锁,就加 1(RUNIT),每释放一个悲观读锁,就减 1。而悲观读锁最多只能装 128个(7 位限制),很容易溢出,所以用一个 int 类型的变量来存储溢出的悲观读锁。
168+
StampedLock 用 long 类型的变量的前 7 位LG_READERS来表示读锁,每获取一个悲观读锁,就加 1RUNIT,每释放一个悲观读锁,就减 1。而悲观读锁最多只能装 128个7 位限制,很容易溢出,所以用一个 int 类型的变量来存储溢出的悲观读锁。
169169

170-
写锁用 state 变量剩下的位来表示,每次获取一个写锁,就加 0000 1000 0000(WBIT)。需要注意的是,写锁在释放的时候,并不是减 WBIT,而是再加 WBIT。这是为了让每次写锁都留下痕迹,解决 CAS 中的 ABA 问题,也为乐观锁检查变化 validate 方法提供基础。
170+
写锁用 state 变量剩下的位来表示,每次获取一个写锁,就加 0000 1000 0000WBIT。需要注意的是,写锁在释放的时候,并不是减 WBIT,而是再加 WBIT。这是为了让每次写锁都留下痕迹,解决 CAS 中的 ABA 问题,也为乐观锁检查变化 validate 方法提供基础。
171171

172172
乐观读锁就比较简单了,并没有真正改变 state 的值,而是在获取锁的时候记录 state 的写状态,在操作完成后去检查 state 的写状态部分是否发生变化,上文提到了,每次写锁都会留下痕迹,也是为了这里乐观锁检查变化提供方便。
173173

RedSpiderConcurrent/15.并发集合容器简介.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
### 同步容器与并发容器
22

3-
在 java.util 包下提供了一些容器类,而 Vector 和 Hashtable 是线程安全的容器类,但是这些容器实现同步的方式是通过对方法加锁(sychronized)方式实现的,这样读写均需要锁操作,导致性能低下。而即使是 Vector 这样线程安全的类,在面对多线程下的复合操作的时候也是需要通过客户端加锁的方式保证原子性。
3+
在 java.util 包下提供了一些容器类,而 Vector 和 Hashtable 是线程安全的容器类,但是这些容器实现同步的方式是通过对方法加锁sychronized方式实现的,这样读写均需要锁操作,导致性能低下。而即使是 Vector 这样线程安全的类,在面对多线程下的复合操作的时候也是需要通过客户端加锁的方式保证原子性。
44

55
### 并发容器类介绍
66

7-
![](http://concurrent.redspider.group/article/03/imgs/并发容器.png)
7+
![](img/并发容器.png)
88

99
#### 并发 Map
1010

@@ -32,29 +32,29 @@ public interface ConcurrentMap<K, V> extends Map<K, V> {
3232

3333
putIfAbsent:与原有 put 方法不同的是,putIfAbsent 方法中如果插入的 key 相同,则不替换原有的 value 值;
3434

35-
remove:与原有 remove 方法不同的是,新 remove 方法中增加了对 value 的判断,如果要删除的 key-value 不能与 Map 中原有的 key-value 对应上,则不会删除该元素;
35+
remove:与原有 remove 方法不同的是,新 remove 方法中增加了对 value 的判断,如果要删除的 key-value 不能与 Map 中原有的 key-value 对应上,则不会删除该元素
3636

37-
replace(K,V,V):增加了对 value 值的判断,如果 key-oldValue 能与 Map 中原有的 key-value 对应上,才进行替换操作;
37+
replace(K,V,V):增加了对 value 值的判断,如果 key-oldValue 能与 Map 中原有的 key-value 对应上,才进行替换操作;
3838

39-
replace(K,V):与上面的 replace 不同的是,此 replace 不会对 Map 中原有的 key-value 进行比较,如果 key 存在则直接替换。
39+
replace(KV):与上面的 replace 不同的是,此 replace 不会对 Map 中原有的 key-value 进行比较,如果 key 存在则直接替换。
4040

4141
##### ConcurrentHashMap 类
4242

4343
**在 JDK 1.7 中:**
4444

45-
ConcurrentHashMap 在 JDK 1.7 中,提供了一种粒度更细的加锁机制来实现在多线程下更高的性能,这种机制叫分段锁(Lock Striping)
45+
ConcurrentHashMap 在 JDK 1.7 中,提供了一种粒度更细的加锁机制来实现在多线程下更高的性能,这种机制叫分段锁Lock Striping
4646

4747
提供的优点是:在并发环境下将实现更高的吞吐量,而在单线程环境下只损失非常小的性能。
4848

49-
可以这样理解分段锁,就是**将数据分段,对每一段数据分配一把锁**。当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
49+
可以这样理解分段锁,就是 **将数据分段,对每一段数据分配一把锁** 。当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
5050

5151
有些方法需要跨段,比如 size()、isEmpty()、containsValue(),它们可能需要锁定整个表而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。
5252

5353
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。Segment 是一种可重入锁 ReentrantLock,默认大小为 16,也就是说默认并发度是 16,HashEntry 则用于存储键值对数据。
5454

55-
一个 ConcurrentHashMap 里包含一个Segment数组,Segment 的结构和 HashMap 类似,是一种数组和链表结构, 一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素, 每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁。
55+
一个 ConcurrentHashMap 里包含一个 Segment 数组,Segment 的结构和 HashMap 类似,是一种数组和链表结构, 一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素, 每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁。
5656

57-
![](http://concurrent.redspider.group/article/03/imgs/%E5%88%86%E6%AE%B5%E9%94%81%E6%9C%BA%E5%88%B6.png)
57+
![](img/分段锁机制.png)
5858

5959
**在 JDK 1.8 中:**
6060

@@ -71,7 +71,7 @@ ConcurrentNavigableMap 接口的主要实现类是 ConcurrentSkipListMap 类。
7171

7272
#### 并发 Queue
7373

74-
JDK 并没有提供线程安全的 List 类,因为对 List 来说,**很难去开发一个通用并且没有并发瓶颈的线程安全的 List**。因为即使简单的读操作,拿 contains() 这样一个操作来说,很难想到搜索的时候如何避免锁住整个 list。
74+
JDK 并没有提供线程安全的 List 类,因为对 List 来说, **很难去开发一个通用并且没有并发瓶颈的线程安全的 List** 。因为即使简单的读操作,拿 contains() 这样一个操作来说,很难想到搜索的时候如何避免锁住整个 list。
7575

7676
所以退一步,JDK 提供了对队列和双端队列的线程安全的类:ConcurrentLinkedQueue 和 ConcurrentLinkedDeque。因为队列相对于 List 来说,有更多的限制。这两个类是使用 CAS 来实现线程安全的。
7777

0 commit comments

Comments
 (0)