Skip to content

Commit 7a5cbb7

Browse files
authored
Merge pull request #1 from Wasabi1234/master
更新文件
2 parents 3a3ef45 + 0c46f97 commit 7a5cbb7

7 files changed

Lines changed: 1537 additions & 161 deletions
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# 前言
2+
定义俩共享变量及俩方法:
3+
- 第一个方法,
4+
- 第二个方法
5+
- (r1,r2)的可能值有哪些?
6+
![](https://img-blog.csdnimg.cn/05139ccfbb40447a869632ff35959841.png)
7+
8+
在单线程环境下,可先调用第一个方法,最终(r1,r2)为(1,0)
9+
也可以先调用第二个方法,最终为(0,2)。
10+
11+
![](https://img-blog.csdnimg.cn/20200404214401993.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70)
12+
# 1 Java内存模型的意义
13+
![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTkyYTFmZGY0OGJlMTllMDYucG5n?x-oss-process=image/format,png)
14+
![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTIzZTVlOWE0OWFkZWI1YTEucG5n?x-oss-process=image/format,png)
15+
JMM 与硬件内存架构对应关系![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTVlMTM3NGEwYWJmOWM5MjkucG5n?x-oss-process=image/format,png)
16+
JMM抽象结构图
17+
![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWQ0ZWE4ODQzYTg4YTk0MGQucG5n?x-oss-process=image/format,png)
18+
内存模型描述程序的可能行为。
19+
20+
Java虚拟机规范中试图定义一种Java内存模型,`来屏蔽掉各种硬件和os的内存访问差异`,规定:
21+
- 线程如何、何时能看到其他线程修改过的共享变量的值
22+
- 必要时,如何同步地访问共享变量
23+
24+
以实现让Java程序在各种平台下都能达到一致性的内存访问效果。
25+
26+
JMM通过检查执行跟踪中的每个读操作,并根据某些规则检查该读操作观察到的写操作是否有效来工作。
27+
28+
只要程序的所有执行产生的结果都可由JMM预测。具体实现者任意实现,包括操作的重新排序和删除不必要的同步。
29+
30+
JMM决定了在程序的每个点上可以读取什么值。
31+
## 1.1 共享变量(Shared Variables)
32+
可在线程之间共享的内存称为`共享内存或堆内存`。所有实例字段、静态字段和数组元素都存储在堆内存。
33+
不包括局部变量与方法参数,因为这些是线程私有的,不存在共享。
34+
35+
对同一变量的两次访问(读或写),若有一个是写请求,则是冲突的!
36+
# 2 主内存与工作内存
37+
工作内存缓存
38+
![](https://img-blog.csdnimg.cn/20191014024209488.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzNTg5NTEw,size_1,color_FFFFFF,t_70)
39+
JMM的主要是定义了`各个变量的访问规则`,在JVM中的如下底层细节:
40+
- 将变量存储到内存
41+
- 从内存中取出变量值
42+
43+
为获得较好执行效率,JMM并未限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器调整代码执行顺序这类权限。
44+
45+
JMM规定:
46+
- 所有变量都存储在主内存(Main Memory)
47+
- 每条线程有自己的工作内存(Working Memory)
48+
保存了该线程使用到的`变量的主内存副本拷贝`(线程所访问对象的引用或者对象中某个在线程访问到的字段,不会是整个对象的拷贝)
49+
线程对变量的所有操作(读,赋值等)都必须在工作内存进行,不能直接读写主内存中的变量
50+
volatile变量依然有工作内存的拷贝,,是他特殊的操作顺序性规定,看起来如同直接在主内存读写
51+
不同线程间,无法直接访问对方工作内存中的变量,线程间变量值的传递均要通过主内存
52+
53+
线程、主内存、工作内存三者的交互关系:
54+
![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LTEyMjA5YjEyZDU3OGEyZWQucG5n?x-oss-process=image/format,png)
55+
![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy80Njg1OTY4LWJiM2QzN2MxNTVjZDgyZDgucG5n?x-oss-process=image/format,png)
56+
57+
JVM模型与JMM不是同一层次的内存划分,基本毫无关系的,硬要对应起来,从变量,内存,工作内存的定义来看
58+
- 主内存 《=》Java堆中的对象实例数据部分
59+
- 工作内存 《=》虚拟机栈中的部分区域
60+
61+
从更底层的层次来看:
62+
- 主内存直接对应物理硬件的内存
63+
- 为更好的运行速度,虚拟机(甚至硬件系统的本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存器,因为程序运行时主要访问读写的是工作内存
64+
# 3 内存间同步操作
65+
## 3.1 线程操作的定义
66+
### 操作定义
67+
write要写的变量以及要写的值。
68+
read要读的变量以及可见的写入值(由此,我们可以确定可见的值)。
69+
lock要锁定的管程(监视器monitor)。
70+
unlock要解锁的管程。
71+
外部操作(socket等等..)
72+
启动和终止
73+
### 程序顺序
74+
如果一个程序没有数据竞争,那么程序的所有执行看起来都是顺序一致的
75+
76+
本规范只涉及线程间的操作;
77+
一个变量如何从主内存拷贝到工作内存,从工作内存同步回主内存的实现细节
78+
79+
JMM 本身已经定义实现了以下8种操作来完成,且都具备`原子性`
80+
- lock(锁定)
81+
作用于主内存变量,把一个变量标识为一条线程独占的状态
82+
- unlock(解锁)
83+
作用于主内存变量,把一个处于锁定状态的变量释放,释放后的变量才可以被其它线程锁定
84+
unlock之前必须将变量值同步回主内存
85+
- read(读取)
86+
作用于主内存变量,把一个变量的值从主内存传输到工作内存,以便随后的load
87+
- load(载入)
88+
作用于工作内存变量,把read从主内存中得到的变量值放入工作内存的变量副本
89+
- use(使用)
90+
作用于工作内存变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到的变量的值得字节码指令时将会执行这个操作
91+
- assign(赋值)
92+
作用于工作内存变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
93+
- store(存储)
94+
作用于工作内存变量,把工作内存中一个变量的值传送到主内存,以便随后的write操作使用
95+
- write(写入)
96+
作用于主内存变量,把store操作从工作内存中得到的值放入主内存的变量中
97+
98+
- 把一个变量从主内存`复制`到工作内存
99+
就要顺序执行read和load
100+
101+
- 把变量从工作内存`同步`回主内存
102+
就要顺序地执行store和write操作
103+
104+
JMM只要求上述两个操作必须`按序执行`,而没有保证连续执行
105+
也就是说read/load之间、store/write之间可以插入其它指令
106+
如对主内存中的变量a,b访问时,一种可能出现的顺序是read a->readb->loadb->load a
107+
108+
JMM规定执行上述八种基础操作时必须满足如下
109+
## 3.1 同步规则
110+
◆ 对于监视器 m 的解锁与所有后续操作对于 m 的加锁 `同步`(之前的操作保持可见)
111+
◆对 volatile变量v的写入,与所有其他线程后续对v的读同步
112+
113+
`启动` 线程的操作与线程中的第一个操作同步
114+
◆ 对于每个属性写入默认值(0, false, null)与每个线程对其进行的操作同步
115+
◆ 线程 T1的最后操作与线程T2发现线程T1已经结束同步。( isAlive ,join可以判断线程是否终结)
116+
◆ 如果线程 T1中断了T2,那么线程T1的中断操作与其他所有线程发现T2被中断了同步通过抛出*InterruptedException*异常,或者调用*Thread.interrupted**Thread.isInterrupted*
117+
118+
- 不允许read/load、store/write操作之一单独出现
119+
不允许一个变量从主内存读取了但工作内存不接收,或从工作内存发起回写但主内存不接收
120+
- 不允许一个线程丢弃它的最近的assign
121+
即变量在工作内存中改变(为工作内存变量赋值)后必须把该变化同步回主内存
122+
- 新变量只能在主内存“诞生”,不允许在工作内存直接使用一个未被初始化(load或assign)的变量
123+
换话说就是一个变量在实施use,store之前,必须先执行过assign和load
124+
- 如果一个变量事先没有被load锁定,则不允许对它执行unlock,也不允许去unlock一个被其它线程锁定的变量
125+
- 对一个变量执行unloack前,必须把此变量同步回主内存中(执行store,write)
126+
127+
> 参考
128+
> - https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.1
Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,55 @@
1-
# 1 信号量模型
2-
## 1.1 模型示意图
1+
2+
> 面试官一声冷笑:用过semaphore吧,说说信号量模型?
3+
4+
信号量模型可简单概括为:一个计数器,一个等待队列,三个方法。在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down()和up()。你可以结合下图来形象化地理解。
5+
6+
- 信号量模型
37
![](https://img-blog.csdnimg.cn/2021042214541150.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70)
48

5-
信号量模型可简单概括为:一个计数器,一个等待队列,三个方法。
6-
计数器和等待队列对外透明,所以只能通过信号量模型提供的三个方法来访问它们:
7-
## 1.2 三个方法
8-
### 1.2.1 init()
9+
10+
> 详细解释下里面提到的这些方法?
11+
12+
- init()
913
设置计数器初始值。
10-
### 1.2.2 down()
14+
- down()
1115
计数器-1;若此时计数器<0,则当前线程被阻塞,否则当前线程可继续执行
12-
### 1.2.3 up()
13-
计数器+1;若此时计数器≤0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。
14-
有的人可能认为这里的判断条件应该≥0,估计你是理解生产者-消费者模式中的生产者。可这样思考,`>0` 意味着无阻塞的线程,所以只有 ≤0 时才需唤醒一个等待的线程。
16+
- up()
17+
计数器+1;若此时计数器≤0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。有的人可能认为这里的判断条件应该≥0,估计你是理解生产者-消费者模式中的生产者。可这样思考,`>0` 意味着没有阻塞的线程,所以只有 ≤0 时才需要唤醒一个等待的线程。
1518

16-
down()和up()应成对出现 && 先调用down()获取锁,处理完成后再调用up()释放锁。若信号量init值为1,应该不会出现>0情况,除非故意调先用up(),这也失去了信号量本身的意义了。
19+
down()和up()应该成对出现,并且先调用down()获取锁,处理完成后再调用up()释放锁。若信号量init值为1,应该不会出现>0情况,除非故意调先用up(),这也失去了信号量本身的意义了。
1720

1821
这些方法都是原子性的,并且这个原子性是由信号量模型的实现方保证的。JDK里的信号量模型是由java.util.concurrent.Semaphore实现,Semaphore这个类能够保证这三个方法都是原子操作。
19-
## 1.3 代码实现
22+
23+
> talk is cheap,show me code?
24+
25+
- 代码
2026
![](https://img-blog.csdnimg.cn/20210422115452600.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70)
2127

2228
信号量模型中,down()、up()最早被称为P操作和V操作,信号量模型也称PV原语。还有的人会用semWait()和semSignal()表达它们,叫法不同,语义都相同。JUC的acquire()和release()就对应down()和up()。
2329

24-
# 2 如何使用信号量?
25-
## 2.1 实例
30+
> 如何使用信号量?
31+
2632
就像红绿信号灯,车必须先检查是否为绿灯,绿灯才能通过。
2733
比如累加器,count+=1操作是个临界区,只允许一个线程执行,也就是说要保证互斥。
2834
![](https://img-blog.csdnimg.cn/20210422122329760.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70)
29-
## 2.2 分析
30-
假设线程t1、t2同时访问add(),当同时调用`acquire()`时,由于`acquire()`是个原子操作,只可能有一个线程(假设t1)把信号量里的计数器减为0,t2则是将计数器减为-1:
35+
分析如下:假设线程t1、t2同时访问add(),当同时调用`acquire()`时,由于`acquire()`是一个原子操作,只可能有一个线程(假设t1)把信号量里的计数器减为0,t2则是将计数器减为-1:
3136
- 对t1,信号量里面的计数器的值是0,≥0,所以t1继续执行
3237
- 对t2,信号量里面的计数器的值是-1,<0,所以t2被阻塞
3338

3439
所以此时只有t1会进入临界区执行count+=1。
3540

3641
当t1执行release(),信号量里计数器的值是-1,加1之后的值是0,小于等于0,根据up()操作,此时等待队列中的t2会被唤醒。于是t2在t1执行完临界区代码后,才获得进入临界区执行的机会,这就保证了互斥性。
37-
## 2.3 既有JDK的Lock,为何还造个Semaphore?
38-
实现互斥锁,仅是 Semaphore的部分功能,还可允许多个线程访问一个临界区。
3942

40-
最常见的就是各种池化资源:连接池、对象池、线程池等。比如数据库连接池,同一时刻,是允许多个线程同时使用连接池的。每个连接在被释放前,不允许其他线程使用。
4143

42-
对象池要求一次性创建出N个对象,之后所有线程重复利用这N个对象,当然对象在被释放前,不允许其他线程使用。所以核心就是限流器的设计:不允许多于N个线程同时进入临界区
44+
既然有JDK提供了Lock,为啥还要提供一个Semaphore ?实现互斥锁,仅是 Semaphore部分功能,Semaphore还可以允许多个线程访问一个临界区
4345

44-
那如何快速实现一个这样的限流器呢?
45-
信号量!
46-
若把计数器的值设置成对象池里对象的个数N,就能完美解决对象池的限流问题。
46+
最常见的就是各种池化资源:连接池、对象池、线程池等。比如数据库连接池,同一时刻,一定是允许多个线程同时使用连接池的。每个连接在被释放前,是不允许其他线程使用的。
4747

48+
对象池要求一次性创建出N个对象,之后所有的线程重复利用这N个对象,当然对象在被释放前,也是不允许其他线程使用的。所以核心就是限流器的设计,这里限流指不允许多于N个线程同时进入临界区。
49+
如何快速实现一个这样的限流器呢?那就是信号量。
50+
如果我们把计数器的值设置成对象池里对象的个数N,就能完美解决对象池的限流问题了。
4851
代码如下:
4952
![](https://img-blog.csdnimg.cn/20210422144637459.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_SmF2YUVkZ2U=,size_16,color_FFFFFF,t_70)
50-
5153
注意这里使用的是 Vector,进入临界区的N个线程不安全。add/remove都是不安全的。比如 ArrayList remove() :
5254
```java
5355
public E remove(int index) {
@@ -66,4 +68,8 @@ public E remove(int index) {
6668

6769
return oldValue;
6870
}
69-
```
71+
```
72+
73+
74+
75+
> 好的,请回家等通知吧!

0 commit comments

Comments
 (0)