并发机制

This commit is contained in:
Estom
2021-09-06 23:06:18 +08:00
parent f1e9be6377
commit 32e2f87c5e
25 changed files with 521 additions and 13 deletions

View File

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

View File

@@ -0,0 +1,21 @@
# java并发控制
## 锁
## 信号量
## 条件变量
## ThreadLocal

View File

@@ -0,0 +1,125 @@
# java并发机制与底层实现原理
## 1 volatile
volatile是轻量级的synchronize,它在多处理器开发中保证了共享变量的“可见性”因为它不会引起线程上下文的切换和调度所以比synchronize的使用和执行成本更底。
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。使用volatile变量在操作后JVM会发出lock指令
将当前处理器缓存行的数据写回到系统内存
这个写回内存的操作会使在其他cpu里缓存了该内存地址的数据无效
## 2 synchronize
### 同步基础
synchronize实现同步的基础具体表现为三种形式
* 对于普通同步方法,锁是当前实例对象
* 对于静态同步方法锁是当前类的class对象
* 对于同步方法块锁是Synchronize括号里配置的对象
* 当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。那么锁到底存在那里,锁里会存储什么信息。
### java对象头
synchonize用的锁是存在java对象头里的。如果对象是数组类型则JVM用三个字宽存储对象头如果对象为非数组类型则用二个字宽存储对象头。32位中一字宽等于四字节(32bit)
<table>
<tbody><tr>
<td>长度</td>
<td>内容</td>
<td>说明</td>
</tr>
<tr>
<td>32/64bit</td>
<td>Mark Word</td>
<td>存储对象的hashCode或锁信息等。</td>
</tr>
<tr>
<td>32/64bit</td>
<td>Class Metadata Address</td>
<td>存储到对象类型数据的指针</td>
</tr>
<tr>
<td>32/64bit</td>
<td>Array length</td>
<td>数组的长度(如果当前对象是数组)</td>
</tr>
</tbody></table>
在运行期间Mark Word里存存储的数据会随着锁标志位的变化而变化。会成为下面的一种
![](image/2021-09-06-21-50-41.png)
## 3 锁类型
为了减少获得锁与释放锁所带来的性能消耗,引入“偏向锁”和“轻量级锁'.所以在java中存在四种状态
* 无锁状态
* 偏向锁状态
* 轻量级锁状态
* 自旋锁
* 重量级锁状态
它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁
### 偏向锁
Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争而且总是由同一线程多次获得为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时会在对象头和栈帧中的锁记录里存储锁偏向的线程ID以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁。
流程图中展示偏向锁的获取释放以及升级至轻量锁
![](image/2021-09-06-21-51-16.png)
### 轻量级锁
1. 轻量级锁加锁:
线程在执行同步块之前JVM会先在当前线程的栈桢中创建用于存储锁记录的空间并将对象头中的Mark Word复制到锁记录中官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功当前线程获得锁如果失败表示其他线程竞争锁当前线程便尝试使用自旋来获取锁。
2. 轻量级锁解锁
轻量级解锁时会使用原子的CAS操作来将Displaced Mark Word替换回到对象头如果成功则表示没有竞争发生。如果失败表示当前锁存在竞争锁就会膨胀成重量级锁。下图是两个线程同时争夺锁导致锁膨胀的流程图。
借用网上流程图如下:
![](image/2021-09-06-21-51-37.png)
### 自旋锁
当竟争存在时如果线程可以很快获得锁那么可以不在OS层挂起线程线程切换平均消耗8K个时钟周期让线程多做几个空操作自旋
1. 如果同步块过长,自旋失败,会降低系统性能
2. 如果同步块很短,自旋成功,节省线程挂起切换时间,担升系统性能
## 4 锁对比
<table>
<tbody><tr>
<td>锁</td>
<td align="center">优点</td>
<td align="center">缺点</td>
<td align="center">适用场景</td>
</tr>
<tr>
<td>偏向锁</td>
<td>加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。</td>
<td>如果线程间存在锁竞争,会带来额外的锁撤销的消耗。</td>
<td>适用于只有一个线程访问同步块场景。&lt;/td</td>
</tr>
<tr>
<td>轻量级锁</td>
<td>竞争的线程不会阻塞,提高了程序的响应速度。</td>
<td>如果始终得不到锁竞争的线程使用自旋会消耗CPU</td>
<td>追求响应时间。同步块执行速度非常快。</td>
</tr>
<tr>
<td>重量级锁</td>
<td>线程竞争不使用自旋不会消耗CPU。</td>
<td>线程阻塞,响应时间缓慢。</td>
<td>追求吞吐量。同步块执行速度较长。</td>
</tr>
</tbody></table>
总结
* 偏向锁轻量级锁自旋锁不是JAVA语言层上的优化方法
* 内置于JVM中的获取锁的优化方法与获取锁的步骤
* 偏向锁可用可先尝试偏向锁
* 轻量级锁可用可先尝试轻量级锁
* 1与2都失败则尝试自旋锁
* 再失败尝试普通锁使用OS互斥量在操作系统层挂起

View File

@@ -0,0 +1,284 @@
# java并发机制
## java多线程
### 继承Thread类
Thread类本质上是实现了Runnable接口的一个实例代表一个线程的实例。启动方法就是通过继承了Thread类的start()实例方法。执行run()方法(重写的)。就可以启动新线程并执行自己定义。例如:
```java
//实现方法的类
public class Demo1 extends Thread {
public void run(){
System.out.println("继承Thread类");
}
}
//执行的方法
public static void main(String[] args) {
Demo1 demo1=new Demo1();
demo1.start();
}
```
### 实现Runnable接口
由于java是单继承的那么在平时开发中就提倡使用接口的方式实现。则需要实现多线程的类通过实现Runnable接口的run方法。通过Thread的start()方法进行启动,例如:
```java
//实现的方法类:
public class Demo2 implements Runnable {
@Override
public void run() {
System.out.println("实现runnable接口");
}
//执行方法:
public static void main(String[] args) {
Demo2 demo2=new Demo2();
Thread thread=new Thread(demo2);
thread.start();
}
```
### 通过内部类的方式实现多线程
直接可以通过Thread类的start()方法进行实现因为Thread类实现了Runnable接口并重写了run方法在run方法中实现自己的逻辑例如
```java
//这里通过了CountDownLatch来进行阻塞来观察两个线程的启动这样更加体现的明显一些
public static CountDownLatch countDownLatch=new CountDownLatch(2);
public static void main(String[] args) {
new Thread(()->{
countDownLatch.countDown();
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T1");
}).start();
new Thread(()->{
countDownLatch.countDown();
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T2");
}).start();
}
```
### 通过实现Callable接口
通过实现Callable接口的call方法可以通过FutureTask的get()方法来获取call方法中的返回值具体实现如下
```java
//实现类方法:
public class Demo3 implements Callable {
@Override
public Object call() {
return "1";
}
}
//执行方法:
public static void main(String[] args) {
//创建实现类对象
Callable demo3=new Demo3();
FutureTask oneTask = new FutureTask(demo3);
Thread thread=new Thread(oneTask);
thread.start();
Object o = null;
try {
//获取返回值
o = oneTask.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(o);
}
```
### 通过线程池来实现多线程
线程池可以根据不同的场景来选择不同的线程池来进行实现,这里我仅使用其中之一进行演示,后续会单独写一个线程池相关的单独介绍:
```java
//实现代码如下:
public class Demo5 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for(int i=0;i<5;i++){
int finalI = i;
executorService.execute(()-> {
System.out.println(finalI);
});
}
}
}
```
### 通过Timer定时器来实现多线程
就Timer来讲就是一个调度器,而TimerTask呢只是一个实现了run方法的一个类,而具体的TimerTask需要由你自己来实现,同样根据参数得不同存在多种执行方式,例如其中延迟定时任务这样:
```
//具体代码如下:
public class Demo6 {
public static void main(String[] args) {
Timer timer=new Timer();
timer.schedule(new TimerTask(){
@Override
public void run() {
System.out.println(1);
}
},2000l,1000l);
}
}
```
### 通过stream实现多线程
jdk1.8 API添加了一个新的抽象称为流Stream可以让你以一种声明的方式处理数据。
Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。
具体简单代码实现如下:
```
//代码实现:
public class Demo7 {
//为了更形象体现并发通过countDownLatch进行阻塞
static CountDownLatch countDownLatch=new CountDownLatch(6);
public static void main(String[] args) {
List list=new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.parallelStream().forEach(p->{
//将所有请求在打印之前进行阻塞,方便观察
countDownLatch.countDown();
try {
System.out.println("线程执行到这里啦");
Thread.sleep(10000);
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(p);
});
}
}
```
## java异步IO
### java NIO
> 参考文献
> * [java nio一篇博客](https://blog.csdn.net/forezp/article/details/88414741)
> * [java nio并发编程网](http://ifeve.com/overview/)
### java akka
1. 前情提要
面向对象编程理论中对象之间通信依赖的是消息但java里对象之间通信用的是对象方法
2. Actor模型
计算模型计算单位Actor所有的计算都在Actor中执行。Actor中一切都是actor,actor之间完全隔离,不共享任何变量。不共享变量,就不会有并发问题。java本身不支持actor模型,需要引入第三方类库Akka
3. 代码范例
```
//该Actor当收到消息message后
//会打印Hello message
static class HelloActor
extends UntypedActor {
@Override
public void onReceive(Object message) {
System.out.println("Hello " + message);
}
}
public static void main(String[] args) {
//创建Actor系统
ActorSystem system = ActorSystem.create("HelloSystem");
//创建HelloActor
ActorRef helloActor =
system.actorOf(Props.create(HelloActor.class));
//发送消息给HelloActor
helloActor.tell("Actor", ActorRef.noSender());
}
```
actor之间通信完美遵循了消息机制。而不是通过调用对象的方式
4. 消息和对象方法的区别
actor内部有一个邮箱mailbox接受到的消息先放到邮箱如果有积压新消息不会马上得到处理。actor是单线程处理消息。所以不会有并发问题
说白了,就是消费者线程的生产者-消费者模式
5. 区别
对相关的方法调用一般是同步的而actor的消息机制是异步的。
6. Actor规范定义
1. 处理能力,处理接收到的消息
2. 存储能力actor可以存储自己的内部状态
3. 通信能力actor可以和其他actor之间通信
7. actor实现线程安全的累加器
无锁算法,因为只有1个线程在消费不会存在并发问题
```
//累加器
static class CounterActor extends UntypedActor {
private int counter = 0;
@Override
public void onReceive(Object message){
//如果接收到的消息是数字类型,执行累加操作,
//否则打印counter的值
if (message instanceof Number) {
counter += ((Number) message).intValue();
} else {
System.out.println(counter);
}
}
}
public static void main(String[] args) throws InterruptedException {
//创建Actor系统
ActorSystem system = ActorSystem.create("HelloSystem");
//4个线程生产消息
ExecutorService es = Executors.newFixedThreadPool(4);
//创建CounterActor
ActorRef counterActor =
system.actorOf(Props.create(CounterActor.class));
//生产4*100000个消息
for (int i=0; i<4; i++) {
es.execute(()->{
for (int j=0; j<100000; j++) {
counterActor.tell(1, ActorRef.noSender());
}
});
}
//关闭线程池
es.shutdown();
//等待CounterActor处理完所有消息
Thread.sleep(1000);
//打印结果
counterActor.tell("", ActorRef.noSender());
//关闭Actor系统
system.shutdown();
}
```
8. 总结
actor计算模型基本计算单元。消息通信。
9. 应用
sparkfilinkplay

View File

View File

View File

View File

View File

View File

@@ -0,0 +1,16 @@
## 任务
* [x] 并发机制
* [x] 并发控制
* [x] 并发通信
* [ ] java并发机制
* [ ] java并发控制
* [ ] C++并发机制
* [ ] C++并发控制
* [ ] Python并发机制
* [ ] Python并发控制
* [ ] Linux并发机制总结
* [ ] Linux并发控制总结
* [ ] 网络编程机制、设备IO机制总结
* [ ] java网络编程
* [ ] C++网络编程
* [ ] Python网络编程

View File

@@ -1,6 +1,6 @@
## Rector设计模式
## 1 Rector设计模式
### reactor模式结构

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -1,4 +1,6 @@
单线程并发
# IO多路复用
IO多路复用即单线程并发事件驱动模型。有事件响应机制、事件回调机制等。
单线程并发并非真正意义上的单线程。而是只有单一的用户线程。还包括数据库socket等系统多线程。

View File

@@ -9,13 +9,25 @@
3. 并发编程的核心问题(并发机制、并发同步、并发通信)
4. 并发编程的具体实例(在各种系统和场景下的表现)
### 并发概念
### 并发概念
并发和独占对应。
* 在程序设计的角度,希望通过某些机制让计算机可以在一个时间段内,执行多个任务。
* 一个或多个物理 CPU 在多个程序之间多路复用,提高对计算机资源的利用率。
* 任务数多余 CPU 的核数,通过操作系统的任务调度算法,实现多个任务一起执行。
* 有多个线程在执行,计算机只有一个 CPU不可能真正同时运行多个线程操作系统只能把 CPU 运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。
### 并发编程的三大特性
1. 可见性。可见性是指当一个线程修改了共享变量后,其他线程能够立即看见这个修改。
2. 原子性。原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败。
3. 有序性。有序性是指程序指令按照预期的顺序执行而非乱序执行乱序又分为编译器乱序和CPU执行乱序。
### 并发编程与其他领域的关系
* 并发编程与网络编程的关系
* 并发编程主要是应用在**服务端**。并发编程与网络编程不通过。并发编程主要实现的是服务端如何满足大量并发的请求。而网络编程考虑的是客户端和服务器如何建立通信链接,和通信链接的过程和方式。
* 网络编程更像是并发编程的一种应用场景。并发编程可以用在单机的任务处理、界面编程上,也可以用在网络编程上。这种理解看上去更合理。
* 并发编程与设备IO、进程同步、进程通信的关系
* 是基于Linux的以上理论实现了并发的机制、并发的控制、并发的通信。分别对应设备IO、进程同步、进程通信。
* 并发与并行的关系
@@ -68,13 +80,14 @@ object 网络编程
2. 多线程机制
3. 多协程机制
4. 单线程的IO多路复用基于事件响应机制方案
2. 并发控制。并发任务之间的同步方案,实现资源互斥、操作顺序。
2. 并发控制。实现并发任务之间的同步方案,实现资源互斥、操作顺序,解决并发过程中上下文切换和线程不安全等问题
1. 锁与信号量
2. MVVC多版本并发控制
3. 并发通信。并发任务之间的数据交换方案
1. 共享内存
2. 管道文件
3. 消息队列
4. Socket编程
### 并发编程的具体实例
> 针对每一个具体实例在其相应的学习和开发模块中都有对应是说明。这里就不再赘述了有空的话学习Linux的并发编程和网络编程。C++并发编程
@@ -91,48 +104,95 @@ object 网络编程
## 2 并发机制
> 这里所谓的什么机制、什么方法。都是设计模式的一部分。通过某种设计模式,实现并发编程:异步回调模式、事件回调模式等。
> 这里所谓的什么机制、什么方法。都是设计模式的一部分。通过某种设计模式,实现并发编程:多线程进程并发、事件驱动模式等。
通常,我们写服务器处理模型的程序时,有以下几种模型:
1. 每收到一个请求,创建一个新的进程,来处理该请求;由于创建新的进程:实现比较简单,但开销比较大,导致服务器性能比较差。
2. 每收到一个请求,创建一个新的线程,来处理该请求;由于要涉及到线程的同步,有可能会面临死锁等问题。
3. 每收到一个请求放入一个事件列表让主进程通过非阻塞I/O方式来处理请求。在写应用程序代码时逻辑比前面两种都复杂。
![](image/2021-09-06-22-21-28.png)
### 基于多进程、多线程、多协程的并发
基于子程序的并发思路:
1. 在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。
2. 开启多进程或都线程的方式,在遇到要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而且线程与进程本身也更容易进入假死状态。
3. 很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率其维持一定合理数量的线程并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销都被广泛应用很多大型系统如websphere、tomcat和各种数据库等。
4. “线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且所谓“池”始终有其上限当请求大大超过上限时“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模并根据响应规模调整“池”的大小。
5. 对应上例中的所面临的可能同时出现的上千甚至上万次的客户端请求,“线程池”或“连接池”或许可以缓解部分压力,但是不能解决所有问题。总之,多线程模型可以方便高效的解决小规模的服务请求,但面对大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来尝试解决这个问题。
**实例**
* 几乎每一种语言都有这种并发方案。java的thread、C++的thread、Python的thread、go的协程、MySQL数据库中的事务并发等。
### 基于IO多路复用、事件驱动IO的并发
### 基于IO多路复用、事件响应机制、事件驱动IO的并发
基于事件驱动模型的并发思路:
1. 有一个事件(消息)队列;
2. 鼠标按下时,往这个队列中增加一个点击事件(消息);
3. 有个循环不断从队列取出事件根据不同的事件调用不同的函数如onClick()、onKeyDown()等;
4. 事件(消息)一般都各自保存各自的处理函数指针,这样,每个消息都有独立的处理函数;
在事件驱动版本的程序中3个任务交错执行但仍然在一个单独的线程控制中。当处理I/O或者其他昂贵的操作时注册一个回调到事件循环中然后当I/O操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽可能的得以执行而不需要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为因为程序员不需要关心线程安全问题。
采用流水线并发模型的系统有时候也称为反应器系统或事件驱动系统。系统内的工作者对系统内出现的事件做出反应这些事件也有可能来自于外部世界或者发自其他工作者。事件可以是传入的HTTP请求也可以是某个文件成功加载到内存中等。在写这篇文章的时候已经有很多有趣的反应器/事件驱动平台可以使用了,并且不久的将来会有更多。
Actors 和 channels 是两种比较类似的事件驱动模型。
**Actor**
在Actor模型中每个工作者被称为actor。Actor之间可以直接异步地发送和处理消息。Actor可以被用来实现一个或多个像前文描述的那样的作业处理流水线。下图给出了Actor模型
![](image/2021-09-06-22-37-30.png)
actor模型又可以分为reactor和Proactor两种模型。在IO设计模式的文章中有降到。
**channel**
而在Channel模型中工作者之间不直接进行通信。相反它们在不同的通道中发布自己的消息事件。其他工作者们可以在这些通道上监听消息发送者无需知道谁在监听。下图给出了Channel模型
![](image/2021-09-06-22-37-43.png)
channel模型对于我来说似乎更加灵活。一个工作者无需知道谁在后面的流水线上处理作业。只需知道作业或消息等需要转发给哪个通道。通道上的监听者可以随意订阅或者取消订阅并不会影响向这个通道发送消息的工作者。这使得工作者之间具有松散的耦合。
**实例**
* UI编程都是事件驱动模型如很多UI平台都会提供onClick()事件。java中的NIO、Python里的asyncIO、nodejs里的单线程并发、Redis数据库中的事件响应机制。
## 3 并发控制
> 不同语言中,锁、信号量、条件变量的实现方案不同。这里不再赘述。在每种语言和场景下单独说明。
### 锁与互斥(操作系统和编程语言中实现并发同步的方法)
### 锁与互斥
### 信号量与同步(操作系统和编程语言中实现并发同步的方法)
### 信号量与同步
### MVVC在数据库中实现事务同步的方法
### MVVC
在数据库中实现事务同步的方法
## 4 并发通信
> 每种语言单独说明。
### 共享内存
### 管道文件
### 消息队列
### Socket通信

View File

@@ -11,7 +11,7 @@
## 2 并发分类
> 参考文献
> * [锁与并发](https://www.cnblogs.com/yaopengfei/p/8399358.html)
### 积极并发
### 积极并发(乐观锁)
积极并发(乐观并发、乐观锁):无论何时从数据库请求数据,数据都会被读取并保存到应用内存中。数据库级别没有放置任何显式锁。数据操作会按照数据层接收到的先后顺序来执行。
积极并发本质就是允许冲突发生,然后在代码本身采取一种合理的方式去解决这个并发冲突,常见的方式有:
@@ -22,7 +22,7 @@
4. 拒绝修改:当一个用户尝试更新一个记录时,但是该记录自从他读取之后已经被别人修改了,此时告诉该用户不允许更新该数据,因为数据已经被某人更新了。
### 消极并发
### 消极并发(悲观锁)
消极并发(悲观并发、悲观锁):无论何时从数据库请求数据,数据都会被读取,然后该数据上就会加锁,因此没有人能访问该数据。这会降低并发出现问题的机会,缺点是加锁是一个昂贵的操作,会降低整个应用程序的性能。