Skip to content

Commit 3909cc8

Browse files
committed
add LeetCode
1 parent 8743909 commit 3909cc8

154 files changed

Lines changed: 14595 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
### 进程产生的背景
2+
3+
最初的计算机只能接受一些特定的指令,用户每输入一个指令,计算机就做出一个操作。
4+
5+
##### 批处理操作系统
6+
7+
把一系列需要操作的指令写下来,形成一个清单,一次性交给计算机。
8+
9+
批处理操作系统在一定程度上提高了计算机的效率,但是由于**批处理操作系统的指令运行方式仍然是串行的,内存中始终只有一个程序在运行**,后面的程序需要等待前面的程序执行完成后才能开始执行,而前面的程序有时会由于I/O操作、网络等原因阻塞,所以**批处理操作效率也不高**
10+
11+
##### 进程的提出
12+
13+
进程就是**应用程序在内存中分配的空间,也就是正在运行的程序**,各个进程之间互不干扰。同时进程保存着程序每一个时刻运行的状态。
14+
15+
> 程序:用某种编程语言(Java、Python 等)编写,能够完成一定任务或者功能的代码集合,是指令和数据的有序集合,是**一段静态代码**
16+
17+
##### 线程的提出
18+
19+
人们又提出了线程的概念,**让一个线程执行一个子任务,这样一个进程就包含了多个线程,每个线程负责一个单独的子任务。**
20+
21+
总之,进程和线程的提出极大的提高了操作系统的性能。**进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。**
22+
23+
多进程方式确实可以实现并发,但使用多线程,有以下几个好处:
24+
25+
- 进程间的通信比较复杂,而线程间的通信比较简单,通常情况下,我们需要使用共享资源,这些资源在线程间的通信比较容易。
26+
- 进程是重量级的,而线程是轻量级的,故多线程方式的系统开销更小。
27+
28+
##### 进程和线程的区别
29+
30+
进程是一个独立的运行环境,而线程是在进程中执行的一个任务。他们两个本质的区别是**是否单独占有内存地址空间及其它系统资源(比如 I/O)**
31+
32+
- 进程单独占有一定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
33+
- 进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
34+
- 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。
35+
36+
另外一个重要区别是,**进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位**,即CPU分配时间的单位 。
37+
38+
### 上下文切换
39+
40+
上下文切换(有时也称做进程切换或任务切换)是指 CPU 从一个进程(或线程)切换到另一个进程(或线程)。上下文是指**某一时间点 CPU 寄存器和程序计数器的内容。**
41+
42+
CPU 通过为每个线程分配 CPU 时间片来实现多线程机制。CPU 通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。
43+
44+
但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
45+
46+
上下文切换通常是计算密集型的,意味着此操作会**消耗大量的 CPU 时间,故线程也不是越多越好**
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
## 乐观锁与悲观锁的概念
2+
3+
悲观锁:总是认为每次访问共享变量资源时会发生冲突。
4+
5+
乐观锁:又称为"无锁",当线程发生冲突时,乐观锁通常是使用一种称为 CAS 的技术来保证线程执行的安全性。**由于无锁操作中没有锁的存在,因此乐观锁天生免疫死锁。**
6+
7+
## CAS 的概念
8+
9+
CAS 的全称是:比较并交换(Compare And Swap),在 CAS 中,有这样三个值:
10+
11+
- V:要更新的变量(var)
12+
- E:预期值(expected)
13+
- N:新值(new)
14+
15+
比较并交换的过程:判断 V 是否等于 E,如果等于,将 V 的值设置为 N;如果不等,则当前线程放弃更新,什么都不做,**所以这里的预期值 E 本质上指的是"旧值"。**
16+
17+
> CAS 是一种原子操作,是一种系统原语,是一条 CPU 的原子指令,从 CPU 层面保证它的原子性。
18+
19+
**当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。**
20+
21+
### Java 实现 CAS 的原理-Unsafe 类
22+
23+
```java
24+
public native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);
25+
public native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
26+
public native boolean compareAndSwapLong(Object o, long offset,long expected,long x);
27+
```
28+
29+
Linux 的 X86 下主要是通过 cmpxchgl 这个指令在 CPU 级完成 CAS 操作的,但在多处理器情况下必须使用 lock 指令加锁来完成。
30+
31+
### 原子操作-AtomicInteger 类源码解析
32+
33+
```java
34+
@HotSpotIntrinsicCandidate
35+
public final int getAndAddInt(Object o, long offset, int delta) {
36+
int v;
37+
do {
38+
v = getIntVolatile(o, offset);
39+
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
40+
return v;
41+
}
42+
```
43+
这里使用的是 do-while 循环,它的目的是保证循环体内的语句至少会被执行一遍。这样才能保证 return 的值 v 是我们期望的值。
44+
45+
```java
46+
public final boolean weakCompareAndSetInt(Object o, long offset,
47+
int expected,
48+
int x) {
49+
return compareAndSetInt(o, offset, expected, x);
50+
}
51+
52+
public final native boolean compareAndSetInt(Object o, long offset,
53+
int expected,
54+
int x);
55+
```
56+
这里可以看到,其实最终调用的就是 Unsafe 类中的 native 方法。
57+
58+
> 为什么不直接调用 compareAndSetInt 而是要通过 weakCompareAndSetInt 方法?
59+
> 这两个方法上面增加了 @HotSpotIntrinsicCandidate 注解,这个注解允许虚拟机自己来写汇编或 IR 编译器来实现该方法以提高性能,所以虽然底层调用的方法一样,但是不排除 HotSpot VM 会手动实现 weakCompareAndSet 真正含义功能的可能性。也就是说 weakCompareAndSet 无法保证处理操作目标的 volatile 变量外的其他变量的执行顺序( 编译器和处理器为了优化程序性能而对指令序列进行重新排序 ),同时也无法保证这些变量的可见性。这在一定程度上可以提高性能。
60+
61+
### CAS 实现原子操作的三大问题
62+
#### ABA 问题
63+
64+
所谓ABA问题,就是一个值原来是A,变成了B,又变回了A。这个时候使用CAS是检查不出变化的,但实际上却被更新了两次。
65+
66+
ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。从 JDK 1.5 开始,JDK 的 atomic 包里提供了一个类 AtomicStampedReference 类来解决 ABA 问题。
67+
68+
这个类的 compareAndSet 方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果二者都相等,才使用CAS设置为新的值和标志。
69+
```java
70+
public boolean compareAndSet(V expectedReference,
71+
V newReference,
72+
int expectedStamp,
73+
int newStamp) {
74+
Pair<V> current = pair;
75+
return
76+
expectedReference == current.reference &&
77+
expectedStamp == current.stamp &&
78+
((newReference == current.reference &&
79+
newStamp == current.stamp) ||
80+
casPair(current, Pair.of(newReference, newStamp)));
81+
}
82+
```
83+
84+
#### 循环时间长开销大
85+
86+
解决思路是让 JVM 支持处理器提供的 pause 指令。pause 指令能让自旋失败时 CPU 睡眠一小段时间再继续自旋。
87+
88+
#### 只能保证一个共享变量的原子操作
89+
这个问题有两种解决方案:
90+
1. 使用 JDK 1.5 提供的 AtomicReference 类保证对象之间的原子性,把多个变量放到一个对象里面进行 CAS 操作;
91+
2. 使用锁。

Java多线程/11.AQS.md

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
### AQS 简介
2+
AQS 是 AbstractQueuedSynchronizer 的简称,即**抽象队列同步器**。AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的同步器,比如我们提到的 ReentrantLock,Semaphore,ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于 AQS 的。
3+
4+
### AQS 的数据结构
5+
AQS 内部使用了一个 volatile 的变量 state 来作为资源的标识。同时定义了几个获取和改变 state 的 protected 方法,子类可以覆盖这些方法来实现自己的逻辑:
6+
```java
7+
getState()
8+
setState()
9+
compareAndSetState()
10+
```
11+
这三种均是原子操作,其中 compareAndSetState 的实现依赖于 Unsafe 的 compareAndSwapInt() 方法。
12+
13+
![](http://concurrent.redspider.group/article/02/imgs/AQS%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.png)
14+
15+
AQS 内部使用了一个先进先出的双端队列,并使用了两个指针 head 和 tail 用于标识队列的头部和尾部。但它并不是直接存储线程,而是存储拥有线程的 Node 节点。
16+
17+
### 资源共享模式
18+
资源有两种同步模式:
19+
1. 独占模式(Exclusive):资源是独占的,一次只能一个线程获取。
20+
2. 共享模式(Share):同时可以被多个线程获取,具体的资源个数可以通过参数指定。
21+
22+
```java
23+
static final class Node {
24+
// 标记一个结点(对应的线程)在共享模式下等待
25+
static final Node SHARED = new Node();
26+
// 标记一个结点(对应的线程)在独占模式下等待
27+
static final Node EXCLUSIVE = null;
28+
29+
// waitStatus的值,表示该结点(对应的线程)已被取消
30+
static final int CANCELLED = 1;
31+
// waitStatus的值,表示后继结点(对应的线程)需要被唤醒
32+
static final int SIGNAL = -1;
33+
// waitStatus的值,表示该结点(对应的线程)在等待某一条件
34+
static final int CONDITION = -2;
35+
/*waitStatus的值,表示有资源可用,新head结点需要继续唤醒后继结点(共享模式下,多线程并发释放资源,而head唤醒其后继结点后,需要把多出来的资源留给后面的结点;设置新的head结点时,会继续唤醒其后继结点)*/
36+
static final int PROPAGATE = -3;
37+
38+
// 等待状态,取值范围,-3,-2,-1,0,1
39+
volatile int waitStatus;
40+
volatile Node prev; // 前驱结点
41+
volatile Node next; // 后继结点
42+
volatile Thread thread; // 结点对应的线程
43+
Node nextWaiter; // 等待队列里下一个等待条件的结点
44+
45+
46+
// 判断共享模式的方法
47+
final boolean isShared() {
48+
return nextWaiter == SHARED;
49+
}
50+
51+
Node(Thread thread, Node mode) { // Used by addWaiter
52+
this.nextWaiter = mode;
53+
this.thread = thread;
54+
}
55+
56+
// 其它方法忽略,可以参考具体的源码
57+
}
58+
59+
// AQS里面的addWaiter私有方法
60+
private Node addWaiter(Node mode) {
61+
// 使用了Node的这个构造函数
62+
Node node = new Node(Thread.currentThread(), mode);
63+
// 其它代码省略
64+
}
65+
```
66+
67+
### AQS 的主要方法源码
68+
- isHeldExclusively():该线程是否正在独占资源。只有用到 condition 才需要去实现它;
69+
- tryAcquire(int):独占方式。尝试获取资源,成功则返回 true,失败则返回 false;
70+
- tryRelease(int):独占方式。尝试释放资源,成功则返回 true,失败则返回 false;
71+
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源;
72+
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回 true,否则返回 false。
73+
74+
上述方法都是 protected 方法,之所以不使用抽象方法的目的是可以灵活的让子类根据需要选择实现某些方法而不是强迫子类实现所有的抽象方法。
75+
76+
#### 获取资源
77+
```java
78+
public final void acquire(int arg) {
79+
if (!tryAcquire(arg) &&
80+
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
81+
selfInterrupt();
82+
}
83+
84+
private Node addWaiter(Node mode) {
85+
// 生成该线程对应的Node节点
86+
Node node = new Node(Thread.currentThread(), mode);
87+
// 将Node插入队列中
88+
Node pred = tail;
89+
if (pred != null) {
90+
node.prev = pred;
91+
// 使用CAS尝试,如果成功就返回
92+
if (compareAndSetTail(pred, node)) {
93+
pred.next = node;
94+
return node;
95+
}
96+
}
97+
// 如果等待队列为空或者上述CAS失败,再自旋CAS插入
98+
enq(node);
99+
return node;
100+
}
101+
102+
// 自旋CAS插入等待队列
103+
private Node enq(final Node node) {
104+
for (;;) {
105+
Node t = tail;
106+
if (t == null) { // Must initialize
107+
if (compareAndSetHead(new Node()))
108+
tail = head;
109+
} else {
110+
node.prev = t;
111+
if (compareAndSetTail(t, node)) {
112+
t.next = node;
113+
return t;
114+
}
115+
}
116+
}
117+
}
118+
119+
final boolean acquireQueued(final Node node, int arg) {
120+
boolean failed = true;
121+
try {
122+
boolean interrupted = false;
123+
// 自旋
124+
for (;;) {
125+
final Node p = node.predecessor();
126+
// 如果node的前驱结点p是head,表示node是第二个结点,就可以尝试去获取资源了
127+
if (p == head && tryAcquire(arg)) {
128+
// 拿到资源后,将head指向该结点。
129+
// 所以head所指的结点,就是当前获取到资源的那个结点或null。
130+
setHead(node);
131+
p.next = null; // help GC
132+
failed = false;
133+
return interrupted;
134+
}
135+
// 如果自己可以休息了,就进入waiting状态,直到被unpark()
136+
if (shouldParkAfterFailedAcquire(p, node) &&
137+
parkAndCheckInterrupt())
138+
interrupted = true;
139+
}
140+
} finally {
141+
if (failed)
142+
cancelAcquire(node);
143+
}
144+
}
145+
```
146+
> parkAndCheckInterrupt方法内部使用了LockSupport.park(this),LockSupport 类是 Java 6 引入的一个类,提供了基本的线程同步原语。LockSupport 实际上是调用了 Unsafe 类里面的函数:
147+
1. park(boolean isAbsolute, long time):阻塞当前线程;
148+
2.unpark(Thread jthread):使给定的线程停止阻塞。
149+
150+
**所以结点进入等待队列后,是调用park使它进入阻塞状态的。只有头结点的线程是处于活跃状态的。**
151+
152+
获取资源的方法除了 acquire 外,还有以下三个:
153+
- acquireInterruptibly:申请可中断的资源(独占模式);
154+
- acquireShared:申请共享模式的资源;
155+
- acquireSharedInterruptibly:申请可中断的资源(共享模式)
156+
157+
![](http://concurrent.redspider.group/article/02/imgs/acquire%E6%B5%81%E7%A8%8B.jpg)
158+
159+
#### 释放资源
160+
```java
161+
162+
public final boolean release(int arg) {
163+
if (tryRelease(arg)) {
164+
Node h = head;
165+
if (h != null && h.waitStatus != 0)
166+
unparkSuccessor(h);
167+
return true;
168+
}
169+
return false;
170+
}
171+
172+
private void unparkSuccessor(Node node) {
173+
// 如果状态是负数,尝试把它设置为0
174+
int ws = node.waitStatus;
175+
if (ws < 0)
176+
compareAndSetWaitStatus(node, ws, 0);
177+
// 得到头结点的后继结点head.next
178+
Node s = node.next;
179+
// 如果这个后继结点为空或者状态大于0
180+
// 通过前面的定义我们知道,大于0只有一种可能,就是这个结点已被取消
181+
if (s == null || s.waitStatus > 0) {
182+
s = null;
183+
// 等待队列中所有还有用的结点,都向前移动
184+
for (Node t = tail; t != null && t != node; t = t.prev)
185+
if (t.waitStatus <= 0)
186+
s = t;
187+
}
188+
// 如果后继结点不为空,
189+
if (s != null)
190+
LockSupport.unpark(s.thread);
191+
}
192+
```

0 commit comments

Comments
 (0)