mirror of
https://github.com/Estom/notes.git
synced 2026-02-03 02:23:31 +08:00
并发机制
This commit is contained in:
BIN
Java/Java并发编程/image/2021-09-06-21-50-41.png
Normal file
BIN
Java/Java并发编程/image/2021-09-06-21-50-41.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
BIN
Java/Java并发编程/image/2021-09-06-21-51-16.png
Normal file
BIN
Java/Java并发编程/image/2021-09-06-21-51-16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 185 KiB |
BIN
Java/Java并发编程/image/2021-09-06-21-51-37.png
Normal file
BIN
Java/Java并发编程/image/2021-09-06-21-51-37.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 225 KiB |
21
Java/Java并发编程/并发控制.md
Normal file
21
Java/Java并发编程/并发控制.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# java并发控制
|
||||
|
||||
|
||||
## 锁
|
||||
|
||||
|
||||
|
||||
## 信号量
|
||||
|
||||
|
||||
|
||||
## 条件变量
|
||||
|
||||
|
||||
|
||||
## ThreadLocal
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
125
Java/Java并发编程/并发控制的原理.md
Normal file
125
Java/Java并发编程/并发控制的原理.md
Normal 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里存存储的数据会随着锁标志位的变化而变化。会成为下面的一种
|
||||
|
||||

|
||||
|
||||
|
||||
## 3 锁类型
|
||||
为了减少获得锁与释放锁所带来的性能消耗,引入“偏向锁”和“轻量级锁'.所以在java中存在四种状态
|
||||
|
||||
* 无锁状态
|
||||
* 偏向锁状态
|
||||
* 轻量级锁状态
|
||||
* 自旋锁
|
||||
* 重量级锁状态
|
||||
|
||||
|
||||
它会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁
|
||||
|
||||
### 偏向锁
|
||||
Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要花费CAS操作来加锁和解锁。
|
||||
|
||||
流程图中展示偏向锁的获取释放以及升级至轻量锁
|
||||
|
||||

|
||||
|
||||
### 轻量级锁
|
||||
1. 轻量级锁加锁:
|
||||
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
|
||||
|
||||
2. 轻量级锁解锁
|
||||
轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争夺锁,导致锁膨胀的流程图。
|
||||
|
||||
|
||||
借用网上流程图如下:
|
||||

|
||||
|
||||
### 自旋锁
|
||||
当竟争存在时,如果线程可以很快获得锁,那么可以不在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>适用于只有一个线程访问同步块场景。</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互斥量在操作系统层挂起
|
||||
284
Java/Java并发编程/并发机制.md
Normal file
284
Java/Java并发编程/并发机制.md
Normal 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. 应用
|
||||
spark,filink,play
|
||||
0
Java/Java并发编程/并发通信.md
Normal file
0
Java/Java并发编程/并发通信.md
Normal file
Reference in New Issue
Block a user