并发编程之概述篇

184 阅读48分钟

一、思维导图

image.png

链接:www.processon.com/mindmap/646…

二、环境与依赖

jdk1.8、maven

pom文件依赖

<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.10</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>
</dependencies>

三、进程与线程

1、进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的。

  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。

  • 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程,也有的程序只能启动一个实例进程。

2、线程

  • 一个进程之内可以分为一到多个线程。

  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。

  • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。在windows中进程是不活动的,只是作为线程的容器。

3、二者对比

  • 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集

  • 进程拥有共享的资源,如内存空间等,供其内部的线程共享

  • 进程间通信较为复杂

    • 同一台计算机的进程通信称为 IPC(Inter-process communication)

    • 不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP

  • 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量

  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低

四、并行与并发

1、概述

单核cpu下,线程实际还是串行执行的。作系统中有一个组件叫做任务调度器,将 cpu 的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于 cpu 在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。

总结为一句话就是:微观串行,宏观并行,一般会将这种 线程轮流使用 CPU 的做法称为并发(concurrent)

image.png

image.png

多核cpu下,每个核(core)都可以调度运行线程,这时候线程可以是并行的。

image.png

image.png

小结:

并发(concurrent):是同一时间应对(dealing with)多件事情的能力

并行(parallel):是同一时间动手做(doing)多件事情的能力

2、应用

2.1、应用之异步调用

以调用方角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步

  • 不需要等待结果返回,就能继续运行就是异步

1)、 设计

多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如 果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停...

2)、 结论
  • 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程

  • tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程

  • ui 程序中,开线程进行其他操作,避免阻塞 ui 线程

2.2、应用之提高效率

充分利用多核 cpu 的优势,提高运行效率。想象下面的场景,执行 3 个计算,最后将计算结果汇总。

比如:

计算 1 花费 10 ms

计算 2 花费 11 ms

计算 3 花费 9 ms

汇总需要 1 ms

  • 如果是串行执行,那么总共花费的时间是 10 + 11 + 9 + 1 = 31ms

  • 但如果是四核 cpu,各个核心分别使用线程 1 执行计算 1,线程 2 执行计算 2,线程 3 执行计算 3,那么 3 个 线程是并行的,花费时间只取决于最长的那个线程运行的时间,即 11ms 最后加上汇总时间只会花费 12ms

注意:需要在多核 cpu 才能提高效率,单核仍然时是轮流执行

1)、 结论
  1. 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活

2、多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的

  • 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分(阿姆达尔定律)

  • 也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义

3、IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一 直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化

五、Java 线程

1、创建和运行线程

  • 方法一,直接使用 Thread

  • 方法二,使用 Runnable 配合 Thread

    把【线程】和【任务】(要执行的代码)分开

    • Thread 代表线程

    • Runnable 可运行的任务(线程要执行的代码)

    原理之 Thread 与 Runnable 的关系,分析 Thread 的源码,理清它与 Runnable 的关系

    • 用 Runnable 更容易与线程池等高级 API 配合
    • 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
  • 方法三,FutureTask 配合 Thread

FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况

2、 观察多个线程同时运行

主要是理解

  • 交替执行

  • 谁先谁后,不由我们控制

3、 查看进程线程的方法

3.1、windows

  • 任务管理器可以查看进程和线程数,也可以用来杀死进程

  • tasklist 查看进程

  • taskkill 杀死进程

3.2、linux

  • ps -fe 查看所有进程

  • ps -fT -p <PID> 查看某个进程(PID)的所有线程

  • kill 杀死进程

  • top 按大写 H 切换是否显示线程

  • top -H -p <PID> 查看某个进程(PID)的所有线程

3.3、Java

  • jps 命令查看所有 Java 进程

  • jstack <PID> 查看某个 Java 进程(PID)的所有线程状态

  • jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)

jconsole 远程监控配置

  • 需要以如下方式运行你的 java 类
java -Djava.rmi.server.hostname=`ip地址` -Dcom.sun.management.jmxremote -

Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -

Dcom.sun.management.jmxremote.authenticate=是否认证 java类

  • 修改 /etc/hosts 文件将 127.0.0.1 映射至主机名

如果要认证访问,还需要做如下步骤

  • 如果要认证访问,还需要做如下步骤

  • 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写

  • 连接时填入 controlRole(用户名),R&D(密码)

4、原理之线程运行

4.1、栈与栈帧

Java Virtual Machine Stacks (Java 虚拟机栈)

我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。

  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存

  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

4.2、线程上下文切换(Thread Context Switch)

因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码

  • 线程的 cpu 时间片用完

  • 垃圾回收

  • 有更高优先级的线程需要运行

  • 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念 就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的

  • 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等

  • Context Switch 频繁发生会影响性能

5 、常见方法

image.png image.png

6、 start 与 run

  • 直接调用 run 是在主线程中执行了 run,没有启动新的线程

  • 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码

7、 sleep 与 yield

7.1、 sleep

  1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)

  2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException

  3. 睡眠结束后的线程未必会立刻得到执行

  4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性

7.2、 yield

  1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程

  2. 具体的实现依赖于操作系统的任务调度器

7.3、 线程优先级

  • 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它

  • 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用

8 、join 方法详解

8.1、应用之同步

以调用方角度来讲,如果

  • 需要等待结果返回,才能继续运行就是同步

  • 不需要等待结果返回,就能继续运行就是异步

image.png

image.png

9、 interrupt 方法详解

打断 sleep,wait,join 的线程 比如:打断 sleep 的线程, 会清空打断状态

打断正常运行的线程

打断正常运行的线程, 不会清空打断状态

模式之两阶段终止

打断 park 线程 , 不会清空打断状态 ,如果打断标记已经是 true, 则 park 会失效。

注意: 可以使用 Thread.interrupted() 清除打断状态

10、 不推荐的方法

还有一些不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁

image.png

11 、主线程与守护线程

默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守 护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。

注意:

  • 垃圾回收器线程就是一种守护线程

  • Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求

12 、五种状态

这是从 操作系统 层面来描述的

image.png

  • 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联

  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行

  • 【运行状态】指获取了 CPU 时间片运行中的状态

    • 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
  • 【阻塞状态】

    • 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】

    • 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】

    • 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们

  • 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态

13 、六种状态

从 Java API 层面来描述的,根据 Thread.State 枚举,分为六种状态

image.png

  • NEW 线程刚被创建,但是还没有调用 start() 方法

  • RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)

  • BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述

  • TERMINATED 当线程代码运行结束

五、共享模型之管程

对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i // 获取静态变量i的值

iconst_1 // 准备常量1

iadd // 自增

putstatic i // 将修改后的值存入静态变量i

而对应 i-- 也是类似:

getstatic i // 获取静态变量i的值

iconst_1 // 准备常量1

isub // 自减

putstatic i // 将修改后的值存入静态变量i

而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:

image.png

如果是单线程以上代码是顺序执行(不会交错)没有问题:

image.png

但多线程下代码可能交错运行:

出现负数的情况:

image.png

出现正数的情况:

image.png

临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的

  • 问题出在多个线程访问共享资源

    • 多个线程读共享资源其实也没有问题

    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题

  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

1、synchronized 解决方案

应用之互斥

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock

  • 非阻塞式的解决方案:原子变量

使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁 的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

注意:

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码

  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

image.png

用图来表示

image.png

synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切 换所打断。

2、变量的线程安全分析

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全

  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况

    • 如果只有读操作,则线程安全

    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

局部变量是否线程安全?

  • 局部变量是线程安全的

  • 但局部变量引用的对象则未必

    • 如果该对象没有逃离方法的作用访问,它是线程安全的

    • 如果该对象逃离方法的作用范围,需要考虑线程安全

局部变量线程安全分析

public static void test1() {

int i = 10;

i++;

}

每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享

public static void test1();

descriptor: ()V

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=1, locals=1, args_size=0

0: bipush 10

2: istore_0

3: iinc 0, 1

6: return

LineNumberTable:

line 10: 0

line 11: 3

line 12: 6

LocalVariableTable:

Start Length Slot Name Signature

3 4 0 i I

如图

image.png

局部变量的引用稍有不同

image.png

将 list 修改为局部变量

image.png

2.1、 常见线程安全类

  • String

  • Integer

  • StringBuffffer

  • Random

  • Vector

  • Hashtable

  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为

Hashtable table = new Hashtable();

new Thread(()->{

table.put("key", "value1");

}).start();

new Thread(()->{

table.put("key", "value2");

}).start();

  • 它们的每个方法是原子的

  • 但注意它们多个方法的组合不是原子的,见后面分析

线程安全类方法的组合

分析下面代码是否线程安全?

Hashtable table = new Hashtable();

// 线程1,线程2

if( table.get("key") == null) {

table.put("key", value);

}

image.png

不可变类线程安全性

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的

3、Monitor 概念

Java 对象头

以 32 位虚拟机为例

普通对象

image.png

数组对象

image.png

其中 Mark Word 结构为

image.png

64 位虚拟机 Mark Word

image.png

参考资料: stackoverflflow.com/questions/2…

4、wait notify

API 介绍

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待

  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒

  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify 为止

wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify

5、wait notify 的正确姿势

sleep(long n) 和 wait(long n) 的区别

  1. sleep 是 Thread 方法,而 wait 是 Object 的方法

  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用

  3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁

  4. 它们状态 TIMED_WAITING

6、Park & Unpark

基本使用

它们是 LockSupport 类中的方法

// 暂停当前线程

LockSupport.park();

// 恢复某个线程的运行

LockSupport.unpark(暂停线程对象)

特点

与 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必

  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】

  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

7、重新理解线程状态转换

image.png

假设有线程 Thread t

情况 1 NEW --> RUNNABLE

  • 当调用 t.start() 方法时,由 NEW --> RUNNABLE

情况 2 RUNNABLE <--> WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING

  • 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时

    • 竞争锁成功,t 线程从 WAITING --> RUNNABLE

    • 竞争锁失败,t 线程从 WAITING --> BLOCKED

情况 3 RUNNABLE <--> WAITING

  • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING

    • 注意是当前线程在t 线程对象的监视器上等待
  • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

情况 4 RUNNABLE <--> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING

  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING -->RUNNABLE

情况 5 RUNNABLE <--> TIMED_WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING

  • t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时

    • 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE

    • 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

情况 6 RUNNABLE <--> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING

    • 注意是当前线程在t 线程对象的监视器上等待
  • 当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从TIMED_WAITING --> RUNNABLE

情况 7 RUNNABLE <--> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING

  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

情况 8 RUNNABLE <--> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从RUNNABLE --> TIMED_WAITING

  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从TIMED_WAITING--> RUNNABLE

情况 9 RUNNABLE <--> BLOCKED

  • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED

  • 持obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

情况 10 RUNNABLE <--> TERMINATED

当前线程所有代码运行完毕,进入 TERMINATED

8、多把锁

将锁的粒度细分

  • 好处,是可以增强并发度

  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

9、活跃性

死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁 t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁 例:

定位死锁

  • 检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:
cmd > jps

Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8

12320 Jps

22816 KotlinCompileDaemon

33200 TestDeadLock // JVM 进程

11508 Main

28468 Launcher

cmd > jstack 33200

Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8

2018-12-29 05:51:40

Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b14 mixed mode):

"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000003525000 nid=0x2f60 waiting on condition

[0x0000000000000000]

java.lang.Thread.State: RUNNABLE

"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry

[0x000000001f54f000]

java.lang.Thread.State: BLOCKED (on object monitor)

at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)

- waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)

- locked <0x000000076b5bf1d0> (a java.lang.Object)

at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)

at java.lang.Thread.run(Thread.java:745)

"Thread-0" #11 prio=5 os_prio=0 tid=0x000000001eb68800 nid=0x1b28 waiting for monitor entry

[0x000000001f44f000]

java.lang.Thread.State: BLOCKED (on object monitor)

at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)

- waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)

- locked <0x000000076b5bf1c0> (a java.lang.Object)

at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source)

at java.lang.Thread.run(Thread.java:745)

// 略去部分输出

Found one Java-level deadlock:

=============================

"Thread-1":

waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object),

which is held by "Thread-0"

"Thread-0":

waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object),

which is held by "Thread-1"

Java stack information for the threads listed above:

===================================================

"Thread-1":

at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)

- waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)

- locked <0x000000076b5bf1d0> (a java.lang.Object)

at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)

at java.lang.Thread.run(Thread.java:745)

"Thread-0":

at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)

- waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)

- locked <0x000000076b5bf1c0> (a java.lang.Object)

at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source)

at java.lang.Thread.run(Thread.java:745)

Found 1 deadlock.

  • 避免死锁要注意加锁顺序

  • 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。

饥饿

饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题

下面的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题

image.png

顺序加锁的解决方案

image.png

10、ReentrantLock

相对于 synchronized 它具备如下特点

  • 可中断

  • 可以设置超时时间

  • 可以设置为公平锁

  • 支持多个条件变量

与 synchronized 一样,都支持可重入

可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁,如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

可打断

注意:如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断

锁超时

立刻失败

ReentrantLock lock = new ReentrantLock();

Thread t1 = new Thread(() -> {

log.debug("启动...");

if (!lock.tryLock()) {

log.debug("获取立刻失败,返回");

return;

}

try {

log.debug("获得了锁");

} finally {

lock.unlock();

}

}, "t1");

lock.lock();

log.debug("获得了锁");

t1.start();

try {

sleep(2);

} finally {

lock.unlock();

}

超时失败

ReentrantLock lock = new ReentrantLock();

Thread t1 = new Thread(() -> {

log.debug("启动...");

try {

if (!lock.tryLock(1, TimeUnit.SECONDS)) {

log.debug("获取等待 1s 后失败,返回");

return;

}

} catch (InterruptedException e) {

e.printStackTrace();

}

try {

log.debug("获得了锁");

} finally {

lock.unlock();

}

}, "t1");

lock.lock();

log.debug("获得了锁");

t1.start();

try {

sleep(2);

} finally {

lock.unlock();

}

公平锁

ReentrantLock 默认是不公平的

ReentrantLock lock = new ReentrantLock(false);

lock.lock();

for (int i = 0; i < 500; i++) {

new Thread(() -> {

lock.lock();

try {

System.out.println(Thread.currentThread().getName() + " running...");

} finally {

lock.unlock();

}

}, "t" + i).start();

}

// 1s 之后去争抢锁

Thread.sleep(1000);

new Thread(() -> {

System.out.println(Thread.currentThread().getName() + " start...");

lock.lock();

try {

System.out.println(Thread.currentThread().getName() + " running...");

} finally {

lock.unlock();

}

}, "强行插入").start();

lock.unlock();

改为公平锁后

ReentrantLock lock = new ReentrantLock(true);

公平锁一般没有必要,会降低并发度

条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息

  • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点

  • await 前需要获得锁

  • await 执行后,会释放锁,进入 conditionObject 等待

  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁

  • 竞争 lock 锁成功后,从 await 后继续执行

六、共享模型之内存

上面讲解的 Monitor 主要关注的是访问共享变量时,保证临界区代码的原子性。

下面进一步深入学习共享变量在多线程间的【可见性】问题与多条指令执行时的【有序性】问题

1、 Java 内存模型

JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。

JMM 体现在以下几个方面

  • 原子性 - 保证指令不会受到线程上下文切换的影响

  • 可见性 - 保证指令不会受 cpu 缓存的影响

  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

2、 可见性

退不出的循环

先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

static boolean run = true;

public static void main(String[] args) throws InterruptedException {

Thread t = new Thread(()->{

while(run){

// ....

}

});

t.start();

sleep(1);

run = false; // 线程t不会如预想的停下来

}

分析一下:

  1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。

image.png

  1. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中, 减少对主存中 run 的访问,提高效率

image.png

  1. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量 的值,结果永远是旧值

image.png

解决方法

volatile(易变关键字)

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取 它的值,线程操作 volatile 变量都是直接操作主存

可见性 vs 原子性

前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可 见,不能保证原子性,仅用在一个写线程,多个读线程的情况:上例从字节码理解是这样的:

getstatic run // 线程 t 获取 run true

getstatic run // 线程 t 获取 run true

getstatic run // 线程 t 获取 run true

getstatic run // 线程 t 获取 run true

putstatic run // 线程 main 修改 run 为 false, 仅此一次

getstatic run // 线程 t 获取 run false

比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i-- ,只能保证看到最新值,不能解决指令交错

// 假设i的初始值为0

getstatic i // 线程2-获取静态变量i的值 线程内i=0

getstatic i // 线程1-获取静态变量i的值 线程内i=0

iconst_1 // 线程1-准备常量1

iadd // 线程1-自增 线程内i=1

putstatic i // 线程1-将修改后的值存入静态变量i 静态变量i=1

iconst_1 // 线程2-准备常量1

isub // 线程2-自减 线程内i=-1

putstatic i // 线程2-将修改后的值存入静态变量i 静态变量i=-1

注意: synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是 synchronized 是属于重量级操作,性能相对更低

如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到 对 run 变量的修改了,想一想为什么?

3 、有序性

JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

static int i;

static int j;

// 在某个线程内执行如下赋值操作

i = ...;

j = ...;

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是

i = ...;

j = ...;

也可以是

j = ...;

i = ...;

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU执行指令的原理来理解一下吧

原理之指令级并行

int num = 0;

boolean ready = false;

// 线程1 执行此方法

public void actor1(I_Result r) {

if(ready) {

r.r1 = num + num;

} else {

r.r1 = 1;

}

}

// 线程2 执行此方法

public void actor2(I_Result r) {

num = 2;

ready = true;

}

I_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种?

分析:

情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1

情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1

情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)

但我告诉你,结果还有可能是 0

这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2

这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现:借助 java 并发压测工具 jcstress wiki.openjdk.java.net/display/Cod…

mvn archetype:generate -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress -

DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=cn.itcast -

DartifactId=ordering -Dversion=1.0

创建 maven 项目,提供如下测试类

@JCStressTest

@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")

@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")

@State

public class ConcurrencyTest {

int num = 0;

boolean ready = false;

@Actor

public void actor1(I_Result r) {

if(ready) {

r.r1 = num + num;

} else {

r.r1 = 1;

}

}

@Actor

public void actor2(I_Result r) {

num = 2;

ready = true;

}

}

执行

mvn clean install java -jar target/jcstress.jar

会输出我们感兴趣的结果,摘录其中一次结果:

*** INTERESTING tests

Some interesting behaviors observed. This is for the plain curiosity.

2 matching test results.

[OK] test.ConcurrencyTest

(JVM args: [-XX:-TieredCompilation])

Observed state Occurrences Expectation Interpretation

0 1,729 ACCEPTABLE_INTERESTING !!!!

1 42,617,915 ACCEPTABLE ok

4 5,146,627 ACCEPTABLE ok

[OK] test.ConcurrencyTest

(JVM args: [])

Observed state Occurrences Expectation Interpretation

0 1,652 ACCEPTABLE_INTERESTING !!!!

1 46,460,657 ACCEPTABLE ok

4 4,571,072 ACCEPTABLE ok

可以看到,出现结果为 0 的情况有 638 次,虽然次数相对很少,但毕竟是出现了。

解决方法

volatile 修饰的变量,可以禁用指令重排

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")

@Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")

@State

public class ConcurrencyTest {

int num = 0;

volatile boolean ready = false;

@Actor

public void actor1(I_Result r) {

if(ready) {

r.r1 = num + num;

} else {

r.r1 = 1;

}

}

@Actor

public void actor2(I_Result r) {

num = 2;

ready = true;

}

}

结果为:

*** INTERESTING tests

Some interesting behaviors observed. This is for the plain curiosity.

0 matching test results.

原理之 volatile

happens-before

happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见

  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
static int x;

static Object m = new Object();

new Thread(()->{

synchronized(m) {

x = 10;

}

},"t1").start();

new Thread(()->{

synchronized(m) {

System.out.println(x);

}

},"t2").start();

  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int x;

new Thread(()->{

x = 10;

},"t1").start();

new Thread(()->{

System.out.println(x);

},"t2").start();

  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x;

x = 10;

new Thread(()->{

System.out.println(x);

},"t2").start();

  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
static int x;

Thread t1 = new Thread(()->{

x = 10;

},"t1");

t1.start();

t1.join();

System.out.println(x);

  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过t2.interrupted 或 t2.isInterrupted)
static int x;

public static void main(String[] args) {

Thread t2 = new Thread(()->{

while(true) {

if(Thread.currentThread().isInterrupted()) {

System.out.println(x);

break;

}

}

},"t2");

t2.start();

new Thread(()->{

sleep(1);

x = 10;

t2.interrupt();

},"t1").start();

while(!t2.isInterrupted()) {

Thread.yield();

}

System.out.println(x);

}

  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见

  • 具有传递性,如果 x hb-> y 并且 y hb-> z 那么有 x hb-> z ,配合 volatile 的防指令重排,有下面的例子

volatile static int x;

static int y;

new Thread(()->{

y = 10;

x = 20;

},"t1").start();

new Thread(()->{

// x=20 对 t2 可见, 同时 y=10 也对 t2 可见

System.out.println(x);

},"t2").start();

变量都是指成员变量或静态成员变量

七、共享模型之无锁

1 、问题提出

有如下需求,保证 account.withdraw 取款方法的线程安全

interface Account {

// 获取余额

Integer getBalance();

// 取款

void withdraw(Integer amount);

/**

* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作

* 如果初始余额为 10000 那么正确的结果应当是 0

*/

static void demo(Account account) {

List<Thread> ts = new ArrayList<>();

long start = System.nanoTime();

for (int i = 0; i < 1000; i++) {

ts.add(new Thread(() -> {

account.withdraw(10);

}));

}

ts.forEach(Thread::start);

ts.forEach(t -> {

try {

t.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

});

long end = System.nanoTime();

System.out.println(account.getBalance()

+ " cost: " + (end-start)/1000_000 + " ms");

}

}

原有实现并不是线程安全的

class AccountUnsafe implements Account {

private Integer balance;

public AccountUnsafe(Integer balance) {

this.balance = balance;

}

@Override

public Integer getBalance() {

return balance;

}

@Override

public void withdraw(Integer amount) {

balance -= amount;

}

}

public static void main(String[] args) {

Account.demo(new AccountUnsafe(10000));

}


执行测试代码,查看执行结果


330 cost: 306 ms

为什么不安全

withdraw 方法

public void withdraw(Integer amount) {

balance -= amount;

}

对应的字节码

ALOAD 0 // <- this

ALOAD 0

GETFIELD cn/itcast/AccountUnsafe.balance : Ljava/lang/Integer; // <- this.balance

INVOKEVIRTUAL java/lang/Integer.intValue ()I // 拆箱

ALOAD 1 // <- amount

INVOKEVIRTUAL java/lang/Integer.intValue ()I // 拆箱

ISUB // 减法

INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; // 结果装箱

PUTFIELD cn/itcast/AccountUnsafe.balance : Ljava/lang/Integer; // -> this.balance

多线程执行流程

ALOAD 0 // thread-0 <- this

ALOAD 0

GETFIELD cn/itcast/AccountUnsafe.balance // thread-0 <- this.balance

INVOKEVIRTUAL java/lang/Integer.intValue // thread-0 拆箱

ALOAD 1 // thread-0 <- amount

INVOKEVIRTUAL java/lang/Integer.intValue // thread-0 拆箱

ISUB // thread-0 减法

INVOKESTATIC java/lang/Integer.valueOf // thread-0 结果装箱

PUTFIELD cn/itcast/AccountUnsafe.balance // thread-0 -> this.balance

ALOAD 0 // thread-1 <- this

ALOAD 0

GETFIELD cn/itcast/AccountUnsafe.balance // thread-1 <- this.balance

INVOKEVIRTUAL java/lang/Integer.intValue // thread-1 拆箱

ALOAD 1 // thread-1 <- amount

INVOKEVIRTUAL java/lang/Integer.intValue // thread-1 拆箱

ISUB // thread-1 减法

INVOKESTATIC java/lang/Integer.valueOf // thread-1 结果装箱

PUTFIELD cn/itcast/AccountUnsafe.balance // thread-1 -> this.balance

  • 单核的指令交错

  • 多核的指令交错

解决思路-锁

class AccountUnsafe implements Account {

private Integer balance;

public AccountUnsafe(Integer balance) {

this.balance = balance;

}

@Override

public synchronized Integer getBalance() {

return balance;

}

@Override

public synchronized void withdraw(Integer amount) {

balance -= amount;

}

}

解决思路-无锁

class AccountSafe implements Account {

private AtomicInteger balance;

public AccountSafe(Integer balance) {

this.balance = new AtomicInteger(balance);

}

@Override

public Integer getBalance() {

return balance.get();

}

@Override

public void withdraw(Integer amount) {

while (true) {

int prev = balance.get();

int next = prev - amount;

if (balance.compareAndSet(prev, next)) {

break;

}

}

// 可以简化为下面的方法

// balance.addAndGet(-1 * amount);

}

}

public static void main(String[] args) {

Account.demo(new AccountSafe(10000));

}


执行测试代码,查看执行结果

0 cost: 302 ms

2、 CAS 与 volatile

前面看到的 AtomicInteger 的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?

public void withdraw(Integer amount) {

while(true) {

// 需要不断尝试,直到成功为止

while (true) {

// 比如拿到了旧值 1000

int prev = balance.get();

// 在这个基础上 1000-10 = 990

int next = prev - amount;

/*

compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值

- 不一致了,next 作废,返回 false 表示失败

比如,别的线程已经做了减法,当前值已经被减成了 990

那么本线程的这次 990 就作废了,进入 while 下次循环重试

- 一致,以 next 设置为新值,返回 true 表示成功

*/

if (balance.compareAndSet(prev, next)) {

break;

}

}

}

}

其中的关键是 compareAndSet,它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。

image.png

注意:其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。

在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。

volatile

获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。

它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。

注意:volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)

CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

为什么无锁效率高

  • 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。打个比喻

  • 线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火, 等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大

  • 但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑 道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。

image.png

CAS 的特点

结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。

  • CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再 重试呗。

  • synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想 改,我改完了解开锁,你们才有机会。

  • CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思

    • 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一

    • 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响

3、 原子整数

J.U.C 并发包提供了:

  • AtomicBoolean

  • AtomicInteger

  • AtomicLong

4、 原子引用

为什么需要原子引用类型?

  • AtomicReference

  • AtomicMarkableReference

  • AtomicStampedReference

ABA 问题及解决

ABA 问题

5、Unsafe

概述

Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得

Unsafe CAS 操作

八、共享模型之不可变

1、日期转换的问题

由于 SimpleDateFormat 不是线程安全的,有很大几率出现 java.lang.NumberFormatException 或者出现不正确的日期解析结果。

思路 - 同步锁

这样虽能解决问题,但带来的是性能上的损失,并不算很好。

思路 - 不可变

如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改啊!这样的对象在Java 中有很多,例如在 Java 8 后,提供了一个新的日期格式化类:DateTimeFormatter。

可以看 DateTimeFormatter 的文档:

@implSpec
This class is immutable and thread-safe.

不可变对象,实际是另一种避免竞争的方式。

2、 不可变设计

另一个大家更为熟悉的 String 类也是不可变的,以它为例,说明一下不可变设计的要素

public final class String

implements java.io.Serializable, Comparable<String>, CharSequence {

/** The value is used for character storage. */

private final char value[];

/** Cache the hash code for the string */

private int hash; // Default to 0

// ...

}

fifinal 的使用

发现该类、类中所有属性都是 fifinal 的

  • 属性用 fifinal 修饰保证了该属性是只读的,不能修改

  • 类用 fifinal 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性

保护性拷贝

使用字符串时,也有一些跟修改相关的方法啊,比如 substring 等,那么下面就看一看这些方法是如何实现的,就以 substring 为例:

public String substring(int beginIndex) {

if (beginIndex < 0) {

throw new StringIndexOutOfBoundsException(beginIndex);

}

int subLen = value.length - beginIndex;

if (subLen < 0) {

throw new StringIndexOutOfBoundsException(subLen);

}

return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);

}

发现其内部是调用 String 的构造方法创建了一个新字符串,再进入这个构造看看,是否对 fifinal char[] value 做出了修改:

public String(char value[], int offset, int count) {

if (offset < 0) {

throw new StringIndexOutOfBoundsException(offset);

}

if (count <= 0) {

if (count < 0) {

throw new StringIndexOutOfBoundsException(count);

}

if (offset <= value.length) {

this.value = "".value;

return;

}

}

if (offset > value.length - count) {

throw new StringIndexOutOfBoundsException(offset + count);

}

this.value = Arrays.copyOfRange(value, offset, offset+count);

}

结果发现也没有,构造新字符串对象时,会生成新的 char[] value,对内容进行复制 。这种通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】

3 、无状态

在 web 阶段学习时,设计 Servlet 时为了保证其线程安全,都会有这样的建议,不要为 Servlet 设置成员变量,这种没有任何成员变量的类是线程安全的

因为成员变量保存的数据也可以称为状态信息,因此没有成员变量就称之为【无状态】

九、共享模型之工具

1、 线程池

1.1、 自定义线程池

image.png

步骤1:自定义拒绝策略接口

@FunctionalInterface // 拒绝策略

interface RejectPolicy<T> {

void reject(BlockingQueue<T> queue, T task);

}

步骤2:自定义任务队列

class BlockingQueue<T> {

// 1. 任务队列

private Deque<T> queue = new ArrayDeque<>();

// 2. 锁

private ReentrantLock lock = new ReentrantLock();

// 3. 生产者条件变量

private Condition fullWaitSet = lock.newCondition();

// 4. 消费者条件变量

private Condition emptyWaitSet = lock.newCondition();

// 5. 容量

private int capcity;

public BlockingQueue(int capcity) {

this.capcity = capcity;

}

// 带超时阻塞获取

public T poll(long timeout, TimeUnit unit) {

lock.lock();

try {

// 将 timeout 统一转换为 纳秒

long nanos = unit.toNanos(timeout);

while (queue.isEmpty()) {

try {

// 返回值是剩余时间

if (nanos <= 0) {

return null;

}

nanos = emptyWaitSet.awaitNanos(nanos);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

T t = queue.removeFirst();

fullWaitSet.signal();

return t;

} finally {

lock.unlock();

}

}

// 阻塞获取

public T take() {

lock.lock();

try {

while (queue.isEmpty()) {

try {

emptyWaitSet.await();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

T t = queue.removeFirst();

fullWaitSet.signal();

return t;

} finally {

lock.unlock();

}

}

// 阻塞添加

public void put(T task) {

lock.lock();

try {

while (queue.size() == capcity) {

try {

log.debug("等待加入任务队列 {} ...", task);

fullWaitSet.await();

} catch (InterruptedException e) {

e.printStackTrace();

}

}

log.debug("加入任务队列 {}", task);

queue.addLast(task);

emptyWaitSet.signal();

} finally {

lock.unlock();

}

}

// 带超时时间阻塞添加

public boolean offer(T task, long timeout, TimeUnit timeUnit) {

lock.lock();

try {

long nanos = timeUnit.toNanos(timeout);

while (queue.size() == capcity) {

try {

if(nanos <= 0) {

return false;

}

log.debug("等待加入任务队列 {} ...", task);

nanos = fullWaitSet.awaitNanos(nanos);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

log.debug("加入任务队列 {}", task);

queue.addLast(task);

emptyWaitSet.signal();

return true;

} finally {

lock.unlock();

}

}

public int size() {

lock.lock();

try {

return queue.size();

} finally {

lock.unlock();

}

}

public void tryPut(RejectPolicy<T> rejectPolicy, T task) {

lock.lock();

try {

// 判断队列是否满

if(queue.size() == capcity) {

rejectPolicy.reject(this, task);

} else { // 有空闲

log.debug("加入任务队列 {}", task);

queue.addLast(task);

emptyWaitSet.signal();

}

} finally {

lock.unlock();

}

}

}

步骤3:自定义线程池

class ThreadPool {

// 任务队列

private BlockingQueue<Runnable> taskQueue;

// 线程集合

private HashSet<Worker> workers = new HashSet<>();

// 核心线程数

private int coreSize;

// 获取任务时的超时时间

private long timeout;

private TimeUnit timeUnit;

private RejectPolicy<Runnable> rejectPolicy;

// 执行任务

public void execute(Runnable task) {

// 当任务数没有超过 coreSize 时,直接交给 worker 对象执行

// 如果任务数超过 coreSize 时,加入任务队列暂存

synchronized (workers) {

if(workers.size() < coreSize) {

Worker worker = new Worker(task);

log.debug("新增 worker{}, {}", worker, task);

workers.add(worker);

worker.start();

} else {

// taskQueue.put(task);

// 1) 死等

// 2) 带超时等待

// 3) 让调用者放弃任务执行

// 4) 让调用者抛出异常

// 5) 让调用者自己执行任务

taskQueue.tryPut(rejectPolicy, task);

}

}

}

public ThreadPool(int coreSize, long timeout, TimeUnit timeUnit, int queueCapcity,

RejectPolicy<Runnable> rejectPolicy) {

this.coreSize = coreSize;

this.timeout = timeout;

this.timeUnit = timeUnit;

this.taskQueue = new BlockingQueue<>(queueCapcity);

this.rejectPolicy = rejectPolicy;

}

class Worker extends Thread{

private Runnable task;

public Worker(Runnable task) {

this.task = task;

}

@Override

public void run() {

// 执行任务

// 1) 当 task 不为空,执行任务

// 2) 当 task 执行完毕,再接着从任务队列获取任务并执行

// while(task != null || (task = taskQueue.take()) != null) {

while(task != null || (task = taskQueue.poll(timeout, timeUnit)) != null) {

try {

log.debug("正在执行...{}", task);

task.run();

} catch (Exception e) {

e.printStackTrace();

} finally {

task = null;

}

}

synchronized (workers) {

log.debug("worker 被移除{}", this);

workers.remove(this);

}

}

}

}

步骤4:测试

public static void main(String[] args) {

ThreadPool threadPool = new ThreadPool(1,

1000, TimeUnit.MILLISECONDS, 1, (queue, task)->{

// 1. 死等

// queue.put(task);

// 2) 带超时等待

// queue.offer(task, 1500, TimeUnit.MILLISECONDS);

// 3) 让调用者放弃任务执行

// log.debug("放弃{}", task);

// 4) 让调用者抛出异常

// throw new RuntimeException("任务执行失败 " + task);

// 5) 让调用者自己执行任务

task.run();

});

for (int i = 0; i < 4; i++) {

int j = i;

threadPool.execute(() -> {

try {

Thread.sleep(1000L);

} catch (InterruptedException e) {

e.printStackTrace();

}

log.debug("{}", j);

});

}

}

1.2、ThreadPoolExecutor

image.png

1) 线程池状态

ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量

image.png

从数字上比较,TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING

这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值

// c 为旧值, ctlOf 返回结果为新值

ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))));

// rs 为高 3 位代表线程池状态, wc 为低 29 位代表线程个数,ctl 是合并它们

private static int ctlOf(int rs, int wc) { return rs | wc; }

2) 构造方法
public ThreadPoolExecutor(int corePoolSize,

int maximumPoolSize,

long keepAliveTime,

TimeUnit unit,

BlockingQueue<Runnable> workQueue,

ThreadFactory threadFactory,

RejectedExecutionHandler handler)

  • corePoolSize 核心线程数目 (最多保留的线程数)

  • maximumPoolSize 最大线程数目

  • keepAliveTime 生存时间 - 针对救急线程

  • unit 时间单位 - 针对救急线程

  • workQueue 阻塞队列

  • threadFactory 线程工厂 - 可以为线程创建时起个好名字

  • handler 拒绝策略

工作方式:

image.png

  • 线程池中刚开始没有线程,当一个任务提交给线程池后,线程池会创建一个新线程来执行任务。

  • 当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入workQueue 队列排队,直到有空闲的线程。

  • 如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急。

  • 如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,其它著名框架也提供了实现

    • AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
    • CallerRunsPolicy 让调用者运行任务
    • DiscardPolicy 放弃本次任务
    • DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之
    • Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
    • Netty 的实现,是创建一个新线程来执行任务
    • ActiveMQ 的实现,带超时等待(60s)尝试放入队列,类似我们之前自定义的拒绝策略
    • PinPoint 的实现,它使用了一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
  • 当高峰过去后,超过corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个时间由keepAliveTime 和 unit 来控制。

image.png

根据这个构造方法,JDK Executors 类中提供了众多工厂方法来创建各种用途的线程池

3) newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {

return new ThreadPoolExecutor(nThreads, nThreads,

0L, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue<Runnable>());

}

特点

  • 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间

  • 阻塞队列是无界的,可以放任意数量的任务

适用于任务量已知,相对耗时的任务

4) newCachedThreadPool
public static ExecutorService newCachedThreadPool() {

return new ThreadPoolExecutor(0, Integer.MAX_VALUE,

60L, TimeUnit.SECONDS,

new SynchronousQueue<Runnable>());

}

特点

  • 核心线程数是 0,最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着

    • 全部都是救急线程(60s 后可以回收)
    • 救急线程可以无限创建
  • 队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)

整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况

5) newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {

return new FinalizableDelegatedExecutorService

(new ThreadPoolExecutor(1, 1,

0L, TimeUnit.MILLISECONDS,

new LinkedBlockingQueue<Runnable>()));

}

使用场景:

希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。

区别:

  • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作

  • Executors.newSingleThreadExecutor() 线程个数始终为1,不能修改

    • FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
  • Executors.newFixedThreadPool(1) 初始时为1,以后还可以修改

    • 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行修改
6) 提交任务
// 执行任务

void execute(Runnable command);

// 提交任务 task,用返回值 Future 获得任务执行结果

<T> Future<T> submit(Callable<T> task);

// 提交 tasks 中所有任务

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)

throws InterruptedException;

// 提交 tasks 中所有任务,带超时时间

<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,

long timeout, TimeUnit unit)

throws InterruptedException;

// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消

<T> T invokeAny(Collection<? extends Callable<T>> tasks)

throws InterruptedException, ExecutionException;

// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消,带超时时间

<T> T invokeAny(Collection<? extends Callable<T>> tasks,

long timeout, TimeUnit unit)

throws InterruptedException, ExecutionException, TimeoutException;

7) 关闭线程池

shutdown

/*

线程池状态变为 SHUTDOWN

- 不会接收新任务

- 但已提交任务会执行完

- 此方法不会阻塞调用线程的执行

*/

void shutdown();

public void shutdown() {

final ReentrantLock mainLock = this.mainLock;

mainLock.lock();

try {

checkShutdownAccess();

// 修改线程池状态

advanceRunState(SHUTDOWN);

// 仅会打断空闲线程

interruptIdleWorkers();

onShutdown(); // 扩展点 ScheduledThreadPoolExecutor

} finally {

mainLock.unlock();

}

// 尝试终结(没有运行的线程可以立刻终结,如果还有运行的线程也不会等)

tryTerminate();

}

shutdownNow

/*

线程池状态变为 STOP

- 不会接收新任务

- 会将队列中的任务返回

- 并用 interrupt 的方式中断正在执行的任务

*/

List<Runnable> shutdownNow();

public List<Runnable> shutdownNow() {

List<Runnable> tasks;

final ReentrantLock mainLock = this.mainLock;

mainLock.lock();

try {

checkShutdownAccess();

// 修改线程池状态

advanceRunState(STOP);

// 打断所有线程

interruptWorkers();

// 获取队列中剩余任务

tasks = drainQueue();

} finally {

mainLock.unlock();

}

// 尝试终结

tryTerminate();

return tasks;

}

其它方法

// 不在 RUNNING 状态的线程池,此方法就返回 true

boolean isShutdown();

// 线程池状态是否是 TERMINATED

boolean isTerminated();

// 调用 shutdown 后,由于调用线程并不会等待所有任务运行结束,因此如果它想在线程池 TERMINATED 后做些事

情,可以利用此方法等待

boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

8) 任务调度线程池

在『任务调度线程池』功能加入之前,可以使用 java.util.Timer 来实现定时功能,Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个 任务的延迟或异常都将会影响到之后的任务。

ScheduledExecutorService、ScheduledExecutorService#scheduleAtFixedRate

整个线程池表现为:线程数固定,任务数多于线程数时,会放入无界队列排队。任务执行完毕,这些线程也不会被释放。用来执行延迟或反复执行的任务

9) 正确处理执行任务异常

方法1:主动捉异常

方法2:使用 Future

10) Tomcat 线程池

Tomcat 在哪里用到了线程池呢

  • LimitLatch 用来限流,可以控制最大连接个数,类似 J.U.C 中的 Semaphore 后面再讲

  • Acceptor 只负责【接收新的 socket 连接】

  • Poller 只负责监听 socket channel 是否有【可读的 I/O 事件】

  • 一旦可读,封装一个任务对象(socketProcessor),提交给 Executor 线程池处理

  • Executor 线程池中的工作线程最终负责【处理请求】

Tomcat 线程池扩展了 ThreadPoolExecutor,行为稍有不同

  • 如果总线程数达到 maximumPoolSize

    • 这时不会立刻抛 RejectedExecutionException 异常

    • 而是再次尝试将任务放入队列,如果还失败,才抛出 RejectedExecutionException 异常

源码 tomcat-7.0.42

public void execute(Runnable command, long timeout, TimeUnit unit) {

submittedCount.incrementAndGet();

try {

super.execute(command);

} catch (RejectedExecutionException rx) {

if (super.getQueue() instanceof TaskQueue) {

final TaskQueue queue = (TaskQueue)super.getQueue();

try {

if (!queue.force(command, timeout, unit)) {

submittedCount.decrementAndGet();

throw new RejectedExecutionException("Queue capacity is full.");

}

} catch (InterruptedException x) {

submittedCount.decrementAndGet();

Thread.interrupted();

throw new RejectedExecutionException(x);

}

} else {

submittedCount.decrementAndGet();

throw rx;

}

}

}

TaskQueue.java

public boolean force(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {

if ( parent.isShutdown() )

throw new RejectedExecutionException(

"Executor not running, can't force a command into the queue"

);

return super.offer(o,timeout,unit); //forces the item onto the queue, to be used if the task

is rejected

}

Connector 配置

image.png

Executor 线程配置

image.png

image.png

1.3. Fork/Join

1) 概念

Fork/Join 是 JDK 1.7 加入的新的线程池实现,它体现的是一种分治思想,适用于能够进行任务拆分的 cpu 密集型运算

所谓的任务拆分,是将一个大任务拆分为算法上相同的小任务,直至不能拆分可以直接求解。跟递归相关的一些计算,如归并排序、斐波那契数列、都可以用分治思想进行求解

Fork/Join 在分治的基础上加入了多线程,可以把每个任务的分解和合并交给不同的线程来完成,进一步提升了运算效率

Fork/Join 默认会创建与 cpu 核心数大小相同的线程池

2) 使用

提交给 Fork/Join 线程池的任务需要继承 RecursiveTask(有返回值)或 RecursiveAction(没有返回值),例如对 1~n 之间的整数求和的任务

@Slf4j(topic = "c.AddTask")
class AddTask1 extends RecursiveTask<Integer> {

int n;

public AddTask1(int n) {

this.n = n;

}

@Override

public String toString() {

return "{" + n + '}';

}

@Override

protected Integer compute() {

// 如果 n 已经为 1,可以求得结果了

if (n == 1) {

log.debug("join() {}", n);

return n;

}

// 将任务进行拆分(fork)

AddTask1 t1 = new AddTask1(n - 1);

t1.fork();

log.debug("fork() {} + {}", n, t1);

// 合并(join)结果

int result = n + t1.join();

log.debug("join() {} + {} = {}", n, t1, result);

return result;

}

}

public static void main(String[] args) {

ForkJoinPool pool = new ForkJoinPool(4);

System.out.println(pool.invoke(new AddTask1(5)));

}

用图来表示

image.png

改进

class AddTask3 extends RecursiveTask<Integer> {

int begin;

int end;

public AddTask3(int begin, int end) {

this.begin = begin;

this.end = end;

}

@Override

public String toString() {

return "{" + begin + "," + end + '}';

}

@Override

protected Integer compute() {

// 5, 5

if (begin == end) {

log.debug("join() {}", begin);

return begin;

}

// 4, 5

if (end - begin == 1) {

log.debug("join() {} + {} = {}", begin, end, end + begin);

return end + begin;

}

// 1 5

int mid = (end + begin) / 2; // 3

AddTask3 t1 = new AddTask3(begin, mid); // 1,3

t1.fork();

AddTask3 t2 = new AddTask3(mid + 1, end); // 4,5

t2.fork();

log.debug("fork() {} + {} = ?", t1, t2);

int result = t1.join() + t2.join();

log.debug("join() {} + {} = {}", t1, t2, result);

return result;

}

}

然后提交给 ForkJoinPool 来执行

public static void main(String[] args) {

ForkJoinPool pool = new ForkJoinPool(4);

System.out.println(pool.invoke(new AddTask3(1, 10)));

}

用图来表示

image.png

2 J.U.C

AQS 原理、ReentrantLock 原理

2.1 ReentrantReadWriteLock

当读操作远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发,提高性能。 类似于数据库中的 select ... from ... lock in share mode

提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法

注意事项

  • 读锁不支持条件变量

  • 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待

  • 重入时降级支持:即持有写锁的情况下去获取读锁

2.2 StampedLock

该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用

加解读锁

long stamp = lock.readLock();

lock.unlockRead(stamp);

加解写锁


long stamp = lock.writeLock();

lock.unlockWrite(stamp);

乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通 过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。

long stamp = lock.tryOptimisticRead();

// 验戳

if(!lock.validate(stamp)){

// 锁升级

}

注意:

  • StampedLock 不支持条件变量

  • StampedLock 不支持可重入

2.3 Semaphore

信号量,用来限制能同时访问共享资源的线程上限。

public static void main(String[] args) {

// 1. 创建 semaphore 对象

Semaphore semaphore = new Semaphore(3);

// 2. 10个线程同时运行

for (int i = 0; i < 10; i++) {

new Thread(() -> {

// 3. 获取许可

try {

semaphore.acquire();

} catch (InterruptedException e) {

e.printStackTrace();

}

try {

log.debug("running...");

sleep(1);

log.debug("end...");

} finally {

// 4. 释放许可

semaphore.release();

}

}).start();

}

}

2.4 CountdownLatch

用来进行线程同步协作,等待所有线程完成倒计时。

其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一

2.5 CyclicBarrier

循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行

注意: CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的 CyclicBarrier 可以被比喻为『人满发车』

2.6 线程安全集合类概述

image.png

线程安全集合类可以分为三大类:

  • 遗留的线程安全集合如 Hashtable , Vector

  • 使用 Collections 装饰的线程安全集合,如:

    • Collections.synchronizedCollection

    • Collections.synchronizedList

    • Collections.synchronizedMap

    • Collections.synchronizedSet

    • Collections.synchronizedNavigableMap

    • Collections.synchronizedNavigableSet

    • Collections.synchronizedSortedMap

    • Collections.synchronizedSortedSet

  • java.util.concurrent.*

重点介绍 java.util.concurrent.* 下的线程安全集合类,可以发现它们有规律,里面包含三类关键词:

Blocking、CopyOnWrite、Concurrent

  • Blocking 大部分实现基于锁,并提供用来阻塞的方法

  • CopyOnWrite 之类容器修改开销相对较重

  • Concurrent 类型的容器

    • 内部很多操作使用 cas 优化,一般可以提供较高吞吐量

    • 弱一致性

      • 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的

      • 求大小弱一致性,size 操作未必是 100% 准确

      • 读取弱一致性

遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出ConcurrentModifificationException,不再继续遍历

2.7 ConcurrentLinkedQueue

ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是

  • 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行

  • dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争

  • 只是这【锁】使用了 cas 来实现

事实上,ConcurrentLinkedQueue 应用还是非常广泛的

例如:之前讲的 Tomcat 的 Connector 结构时,Acceptor 作为生产者向 Poller 消费者传递事件信息时,正是采用了ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用

2.8 CopyOnWriteArrayList

CopyOnWriteArraySet 是它的马甲 底层实现采用了 写入时拷贝 的思想,增删改操作会将底层数组拷贝一份,更 改操作在新数组上执行,这时不影响其它线程的并发读,读写分离。 以新增为例:

public boolean add(E e) {

synchronized (lock) {

// 获取旧的数组

Object[] es = getArray();

int len = es.length;

// 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程)

es = Arrays.copyOf(es, len + 1);

// 添加新元素

es[len] = e;

// 替换旧的数组

setArray(es);

return true;

}

}

这里的源码版本是 Java 11,在 Java 1.8 中使用的是可重入锁而不是 synchronized

其它读操作并未加锁,例如:

public void forEach(Consumer<? super E> action) {

Objects.requireNonNull(action);

for (Object x : getArray()) {

@SuppressWarnings("unchecked") E e = (E) x;

action.accept(e);

}

}

适合『读多写少』的应用场景

get 弱一致性

image.png

image.png

不容易测试,但问题确实存在

迭代器弱一致性

CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();

list.add(1);

list.add(2);

list.add(3);

Iterator<Integer> iter = list.iterator();

new Thread(() -> {

list.remove(0);

System.out.println(list);

}).start();

sleep1s();

while (iter.hasNext()) {

System.out.println(iter.next());

}

不要觉得弱一致性就不好

  • 数据库的 MVCC 都是弱一致性的表现

  • 并发高和一致性是矛盾的,需要权衡