并发机制

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

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