Skip to content

Commit

Permalink
并发编程
Browse files Browse the repository at this point in the history
  • Loading branch information
heibaiying committed Nov 24, 2019
1 parent 910790b commit 5333702
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.heibaiying.createThread;

public class J1_Method01 {
public static void main(String[] args) {
System.out.println("Main线程的ID为:" + Thread.currentThread().getId());
Thread thread = new Thread(new CustomRunner());
thread.start();
}
}

class CustomRunner implements Runnable {
@Override
public void run() {
System.out.println("CustomThread线程的ID为:" + Thread.currentThread().getId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.heibaiying.createThread;

public class J2_Method02 {
public static void main(String[] args) {
System.out.println("Main线程的ID为:" + Thread.currentThread().getId());
CustomThread customThread = new CustomThread();
customThread.start();
}
}

class CustomThread extends Thread {
@Override
public void run() {
System.out.println("CustomThread线程的ID为:" + Thread.currentThread().getId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.heibaiying.threeCharacteristics;

/**
* 竞态
*/
public class J1_RaceCondition {

private static int i = 0;

public static void main(String[] args) throws InterruptedException {
IncreaseTask task = new IncreaseTask();
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
//等待线程结束后 才打印返回值
thread1.join();
thread2.join();
System.out.println(i);
}

static class IncreaseTask implements Runnable {
@Override
public void run() {
for (int j = 0; j < 100000; j++) {
inc();
}
}

private void inc() {
i++;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.heibaiying.threeCharacteristics;

import java.util.concurrent.atomic.AtomicInteger;

/**
* 原子性
*/
public class J2_Atomic {

private static AtomicInteger i = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
IncreaseTask task = new IncreaseTask();
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
//等待线程结束后 才打印返回值
thread1.join();
thread2.join();
System.out.println(i);
}

static class IncreaseTask implements Runnable {
@Override
public void run() {
for (int j = 0; j < 100000; j++) {
inc();
}
}

private void inc() {
i.incrementAndGet();
}
}
}
159 changes: 125 additions & 34 deletions notes/Java_并发.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,62 +4,63 @@

### 1.1 创建线程

创建线程通常以下两种方式
创建线程通常有以下两种方式

- 实现 Runnable 接口:
- 实现 Runnable 接口,并重写其 run 方法

```java
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable:" + Thread.currentThread().getId());
public class J1_Method01 {
public static void main(String[] args) {
System.out.println("Main线程的ID为:" + Thread.currentThread().getId());
Thread thread = new Thread(new CustomRunner());
thread.start();
}
}

Thread thread = new Thread(new MyRunnable());
thread.start();
```

- 采用匿名内部类:

```java
// 1.创建线程
Thread thread = new Thread(new Runnable() {
class CustomRunner implements Runnable {
@Override
public void run() {
System.out.println("Thread:" + Thread.currentThread().getId());
System.out.println("CustomRunner线程的ID为:" + Thread.currentThread().getId());
}
});
// 2.启动线程
thread.start();
}
```

- 继承自 Thread 类,并重写其 run 方法:

```java
// 使用Java 8的lambda可以简写如下:
new Thread(() -> System.out.println("Thread:" + Thread.currentThread().getId())).start();
```
public class J2_Method02 {
public static void main(String[] args) {
System.out.println("Main线程的ID为:" + Thread.currentThread().getId());
CustomThread customThread = new CustomThread();
customThread.start();
}
}

由于 Thread类 实现了 Runnable 接口,所以两种方式本质上还是同一种。需要注意的是,当你使用 `new` 关键字创建了一个线程后,此时线程并没有开始执行,所以你还需要调用`start`方法启动线程。启动一个线程的实质是请求 Java 虚拟机运行相应的线程,而这个线程具体何时能够运行是由线程调度器(Scheduler)决定的,线程可能立即执行,也可能稍后运行,甚至有可能永远不会被执行。
class CustomThread extends Thread {
@Override
public void run() {
System.out.println("CustomThread线程的ID为:" + Thread.currentThread().getId());
}
}
```

### 1.2 线程属性

**编号(ID)** :用于标识线程的唯一编号,只读属性。

**名称(Name)**:用于区分不同线程的名称,可读可写。
**编号 (ID)** :用于标识线程的唯一编号,只读属性。

**线程类别(Daemon)**布尔类型,为 true 表示是守护线程,否则为用户线程,用户线程会阻止 Java 虚拟机正常停止,守护线程则不会。通常可以把一些不重要的线程设置为守护线程,比如监控其他线程工作的线程,当工作线程停止后,虚拟机就可以正常退出。在开发中我们可以使用`setDaemonn`方法设置线程为守护线程,该方法必须在`start`方法前调用,如果在其后调用,则会抛出`IllegalThreadStateException`异常
**名称 (Name)**用于定义线程名称,可读可写

**优先级(Priority)**Java 线程支持 1~10 的 10个优先级,默认值为5,代表一般优先级。Java 线程的优先级本质上只是给线程调度器一个提示信息,它并不能保证线程一定按照优先级的高低顺序运行,所以它是不可靠的,需要谨慎使用
**线程类别 (Daemon)**通过线程的 `setDaemon(boolean on)` 方法进行设置,为 true 表示设置为守护线程,否则为用户线程。用户线程会阻止 Java 虚拟机正常停止,守护线程则不会。通常可以把一些不重要的线程设置为守护线程,比如监控其他线程状态的监控线程,当其他工作线程停止后,虚拟机就可以正常退出

需要说明的是在 Java 平台中,一个线程的线程类别,优先级都默认与其父线程相同
**优先级 (Priority)**Java 线程支持 1到10 十个优先级,默认值为 5 。Java 线程的优先级本质上只是给线程调度器一个提示信息,它并不能保证线程一定按照优先级的高低顺序运行,所以它是不可靠的,需要谨慎使用。在 Java 平台中,子线程的优先级默认与其父线程相同

### 1.3 线程状态

Java 线程的生命周期分为以下几个状态
Java 线程的生命周期分为以下五类状态

**RUNABLE**:该状态包括两个子状态:READY 和 RUNING。处于 READY 状态的线程被称为活跃线程,被线程调度器选中后则开始运行,转化为 RUNING 状态。
**RUNABLE**:该状态包括两个子状态:READY 和 RUNING 。处于 READY 状态的线程被称为活跃线程,被线程调度器选中后则开始运行,转化为 RUNING 状态。

**BLOCKED**:一个线程发起一个阻塞式 IO 操作后,或者申请一个由其他线程持有的独占资源(比如锁)时,相应的线程就会处于该状态。
**BLOCKED**:一个线程发起一个阻塞式 IO 操作后(如文件读写或者阻塞式 Socket 读写),或者申请一个由其他线程持有的独占资源(比如锁)时,相应的线程就会处于该状态。

**WAITING**:线程处于无时间限制的等待状态。

Expand All @@ -69,9 +70,99 @@ Java 线程的生命周期分为以下几个状态:

各个状态之间的转换关系如下图:

![线程完整生命周期](D:\Full-Stack-Notes\pictures\线程完整生命周期.jpg)
![线程完整生命周期](../pictures/线程完整生命周期.jpg)

## 二、状态变量与共享变量

**状态变量 (State Variable)** :即类的实例变量,非共享的静态变量。

**共享变量 (Shared Variable)** : 即可以被多个线程共同访问的变量。

## 三、原子性

### 3.1 定义

对于涉及共享变量的操作,若该操作从其执行线程以外的任意线程来看都是不可分割的,那么我们就说该操作具有原子性。它包含以下两层含义:

+ 访问(读、写)某个共享变量的操作从其执行线程以外的其他任何线程来看,该操作要么已经执行结束要么尚未发生,即其他线程不会看到该操作执行了部分的中间效果。
+ 访问同一组共享变量的原子操作不能被交错执行。

### 3.2 非原子性协定

在 Java 语言中,除了 long 类型 和 double 类型以外的任何类型的变量的写操作都是具有原子性的,但对于没有使用 volatile 关键字修饰的 64 位的 long 类型和 double 类型,允许将其的读写操作划分为两次 32 位的操作来进行,这就是 long 和 double 的非原子性协定 ( Nonatomic Treatment of double and long Variables ) 。

## 四、可见性

### 4.1 定义

如果一个线程对某个共享进行更新之后,后续访问该变量的线程可以读取到这个更新结果,那么我们就称该更新对其他线程可见,反之则是不可见,这种特性就是可见性。出现可见性问题,往往意味着某的线程读取到了旧数据,这会导致更新丢失,从而导致运行结果与预期结果存在差异。可见性问题与计算机的存储结构和 Java 的内存模型都有着密切的关系。

### 4.2 高速缓存

由于现代处理器对数据的处理能力远高于主内存(DRAM)的访问速率,为了弥补它们之间在处理能力上的鸿沟,通常在处理器和主内存之间都会存在高速缓存(Cache)。高速缓存相当于一个由硬件实现的容量极小的散列表(Hash Table),其键是一个内存地址,其值是内存数据的副本或者准备写入内存的数据。

现代处理器一般具有多个层次的高速缓存,如:一级缓存(L1 Cache)、二级缓存(L2 Cache)、三级缓存(L3 Cache)等。其中一级缓存通常包含两部分,其中一部分用于存储指令(L1i),另外一部分用于存储数据(L1d)。距离处理器越近的高速缓存,其存储速率越快,制造成本越高,因此其容量也越小。在 Linux 系统中,可以使用 `lscpu` 命令查看其高速缓存的情况:

![cahce](../pictures/cahce.png)

### 4.3 缓存一致性协议

在多线程环境下,每个线程运行在不同的处理器上,当多个线程并发访问同一个共享变量时,这些线程的执行处理器都会在高速缓存中保留一个该共享变量的副本,此时如何让一个处理器对数据的更改能被其他处理器够感知到 ? 为了解决这个问题,须要引入一个新的通讯机制 —— 缓存一致性协议。

缓存一致性协议有着多种不同的实现,这里以广泛使用的 MESI ( Modified-Exclusive-Shared-Invalid ) 协议为例,和其名字一样,它将高速缓存中的缓存条目分为以下四种状态:

+ **Invalid**:该状态表示相应的缓存行中不包含任何内存地址对应有效副本数据。
+ **Shared**:该状态表示相应的缓存行中包含相应内存地址所对应的副本数据,并且其他处理器的高速缓存中也可能存在该相同内存地址所对应的副本数据。
+ **Exclusive**:该状态表示相应的缓存行中包含相应内存地址所对应的副本数据,但其他处理器的高速缓存中不应该存在该相同内存地址所对应的副本数据,即独占的。
+ **Modified**:该状态表示相应的缓存行中包含对相应内存地址所做的更新的结果。MESI 协议限制任意一个时刻只能有一个处理器能对同一内存地址上的数据进行更新,因此任意一个时刻只能有一个缓存条目处于该状态。

根据以上状态,当某个处理器对共享变量进行读写操作时,其具体的行为如下:

+ **读取共享变量**:处理器首先在高速缓存上进行查找,如果对应缓存条目的状态为 M,E 或者 S,此时则直接读取;如果缓存条目为无效状态 I,此时需要向总线发送 Read 消息,其他处理器或主内存则需要回复 Read Response 来提供相应的数据,处理器在获取到数据后,将其存储相应的缓存条目,并将状态更新为 S 。
+ **写入共享变量**:此时处理器首先需要判断是否拥有对该数据的所有权,如果对应缓存条目的状态为 E 或者 M,代表此时均处于独占状态,此时可以直接写入,并将其状态变更为 M 。如果不为 E 或 M,此时处理器需要往总线上发送 Invalidate 消息通知其他处理器将对应的缓存条目失效,之后在收到其他处理器的 Invalidate Acknowledge 响应后再进行更改,并将其状态变更为 M。

在只有高速缓存的情况下,通过缓存一致性协议能够保证一个线程对共享变量的更新对于其他线程是可见的。如果只是这样,多线程编程就不会存在可见性问题了,但实际上缓存一致性协议并不能保证最终的可见性,这是由于写缓冲器和无效化队列导致的。

### 4.4 写缓冲器与无效化队列

在上面的缓存一致性协议中,处理器必须等待其他处理器的应答(如:Read Response \ Invalidate Acknowledge)后才去执行后续的操作,这会带来一定的时间开销,为了解决这个问题,现代计算机架构又引入了写缓冲器和无效化队列。

+ **写缓冲器**:当处理器发现缓存条目的状态不为 E 或 M 时,此时不再等待其他处理器返回 Invalidate Acknowledge 消息,而是直接将变更写入写缓冲器就认为操作完成。当收到对应的 Invalidate Acknowledge 消息,再将变更写入到对应的缓存条目中,此时写操作对于其他处理器而言,才算完成。
+ **无效化队列**:当其他处理器接收到 Invalidate 消息后,不再等待删除指定缓存条目中的副本数据后再回复 Invalidate Acknowledge ,而是将消息存入到无效化队列中后就直接回复。

写缓冲器是处理器的私有部件,一个处理器写缓冲器所存储的内容是不能被其他处理器所读取的,这就会导致一个更新即便已经发生并写入到写缓冲器,但是其他处理器上的线程读取到的还是旧值,从而导致可见性问题。除了写缓冲器外,无效化队列也会导致可见性问题,当某个写入发生后,其他处理器上的对应缓存条目应该都立即失效,但是由于无效化队列的存在,Invalidate 操作不会立即执行,导致其他处理器仍然读取到的是未失效的旧值。

### 4.5 内存屏障

想要解决写缓存器和无效化队列带来的问题,需要引入一个新的机制 —— 内存屏障:

+ **Store Barrier**:存储屏障,可以使执行该指令的处理器冲刷其写缓冲器。
+ **Load Barrier**:加载屏障,将无效化队列中所指定的缓存条目的状态都标志位 I ,从而保证处理器在读取共享变量时必须发送 Read 消息去获取更新后的值。

冲刷写缓冲器和清空无效化队列都是存在时间消耗的,所以只有在必须要保证可见性的场景下,才应该去使用内存屏障。何种场景下必须要保证可见性,这是由用户来决定的,这也是多线程编程所需要考虑的问题。在 Java 语言中,使用内存屏障来保证可见性的一个典型的例子就是 volatile 关键字。

### 4.6 Volatile 关键字

Volatile 关键字在 Java 语言中一共有三种作用:

+ **保证可见性**:Java 虚拟机( JIT 编译器 )会在 volatile 变量写操作之后插入一个通用的 StoreLoad 屏障,它可以充当存储屏障来清空执行处理器的写缓冲器;同时 JIT 编译器还会在变量的读操作前插入一个加载屏障来清空无效化队列。
+ **禁止指令重排序**:通过内存屏障, Java 虚拟机可以 volatile 变量之前的任何读写操作都先于这个 volatile 写操作之前被提交,而 volatile 变量的读操作先于之后任何变量的读写操作被提交。
+ 除了以上两类语义外,Java 虚拟机规范还特别规定了对于使用 volatile 修饰的 64 的 long 类型和 double 类型的变量的读写操作具有原子性。

### 4.7 Java 内存模型

以上主要介绍计算机的内存模型对可见性的影响,但是不同架构的处理器在内存模型和支持的指令集上都存在略微的差异。 Java 作为一种跨平台的语言,必须尽量屏蔽这种差异,而且还要尽量利用硬件的各种特性(如寄存器,高速缓存和指令集中的某些特有指令)来获取更好而定执行速度,这就是 Java 的内存模型。

+ **Main Memory**:主内存,Java 内存模型规定了所有的变量都存储在主内存中,主内存可以类比为计算机的主内存,但其只是虚拟机内存的一部分,并不能代表整个计算机内存。
+ **Work Memory**:工作内存,Java 内存模型规定了每条线程都有自己的工作内存,工作内存可以类比为计算机的高速缓存。工作内存中保存了被该线程使用到的变量的拷贝副本。

线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量;不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

![java内存模型](../pictures/java内存模型.png)

## 五、有序性


## 二、线程同步机制



Expand Down
Binary file added pictures/cahce.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 5333702

Please sign in to comment.