1 线程概述

1.1 线程相关概念

进程

  • 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动, 是操作系统进行资源分配与调度的基本单位.
  • 可以把进程简单的理解为正在操作系统中运行的一个程序.

线程

  • 线程(thread)是进程的一个执行单元.
  • 一个线程就是进程中一个单一顺序的控制流, 进程的一个执行分支
  • 进程是线程的容器, 一个进程至少有一个线程. 一个进程中也可以有多个线程.
  • 在操作系统中是以进程为单位分配资源, 如虚拟存储空间, 文件描述符等. 每个线程都有各自的线程栈, 自己的寄存器环境, 自己的线程本地存储.

主线程与子线程

  • JVM 启动时会创建一个主线程, 该主线程负责执行 main 方法 . 主线程就是运行 main 方法的线程
  • Java 中的线程不孤立的, 线程之间存在一些联系. 如果在 A 线程中 创建了 B 线程, 称 B 线程为 A 线程的子线程, 相应的 A 线程就是 B 线 程的父线程

    串行, 并发与并行
    image.png

  • 并发可以提高以事物的处理效率, 即一段时间内可以处理或者完成更多的事情.

  • 并行是一种更为严格, 理想的并发
  • 从硬件角度来说, 如果单核 CPU, 一个处理器一次只能执行一个线程的情况下, 处理器可以使用时间片轮转技术 , 可以让 CPU 快速的在 各个线程之间进行切换, 对于用户来说, 感觉是三个线程在同时执行. 如果是多核心 CPU, 可以为不同的线程分配不同的 CPU 内核.

    1.2 线程的创建和启动

    在 Java 中, 创建一个线程就是创建一个 Thread 类(子类)的对象(实例).
    Thread 类有两个常用的构造方法: Thread()与 Thread(Runnable).对应的创建线程的两种方式:

  • 定义 Thread 类的子类

  • 定义一个 Runnable 接口的实现类

这两种创建线程的方式没有本质的区别

  1. /**
  2. * 方法1:继承 Thread类
  3. */
  4. // 1)定义类继承 Thread
  5. public class MyThread extends Thread {
  6. // 2) 重写 Thread 父类中的 run()
  7. @Override
  8. public void run() {
  9. System.out.println("我是子线程");
  10. }
  11. public static void main(String[] args) throws InterruptedException {
  12. System.out.println("启动主线程");
  13. // 3) 创建子线程对象
  14. MyThread myThread1 = new MyThread();
  15. myThread1.start();
  16. /*
  17. 调用线程的 start()方法来启动线程, 启动线程的实质就是请求 JVM 运行相应的
  18. 线程,这个线程具体在什么时候运行由线程调度器(Scheduler)决定
  19. 注意:
  20. start()方法调用结束并不意味着子线程开始运行
  21. 新开启的线程会执行 run()方法
  22. 如果开启了多个线程,start()调用的顺序并不一定就是线程启动的顺序
  23. 多线程运行结果与代码执行顺序或调用顺序无关
  24. */
  25. System.out.println("主线程其他内容");
  26. }
  27. }
  1. /**
  2. * * 方法2:当线程类已经有父类了,就不能用继承 Thread 类的形式创建线程,可以使用实现 Runnable接口的形式
  3. */
  4. // 1)定义类实现 Runnable 接口
  5. public class MyRunnable implements Runnable {
  6. // 2)重写 Runnable 接口中的抽象方法 run(), run()方法就是子线程要执行的代码
  7. @Override
  8. public void run() {
  9. for (int i = 1; i <= 1000; i++) {
  10. System.out.println("sub thread --> " + i);
  11. }
  12. }
  13. public static void main(String[] args) {
  14. // 3) 创建 Runnable 接口的实现类
  15. MyRunnable myRunnable = new MyRunnable();
  16. // 4) 创建线程对象
  17. Thread thread = new Thread(myRunnable);
  18. // 5) 开启线程
  19. thread.start();
  20. for (int i = 1; i <= 1000; i++) {
  21. System.out.println("main --> " + i);
  22. }
  23. }
  24. }
  1. /**
  2. * 有时也会使用匿名内部类的方式传递Runnable对象
  3. * @param args
  4. */
  5. public static void main(String[] args) {
  6. // 1) 创建线程对象,传入Runnable实现类 lambda表达式
  7. Thread thread = new Thread(new Runnable() {
  8. @Override
  9. public void run() {
  10. for (int i = 1; i <= 1000; i++) {
  11. System.out.println("sub thread --> " + i);
  12. }
  13. }
  14. });
  15. // 2) 开启线程
  16. thread.start();
  17. for (int i = 1; i <= 1000; i++) {
  18. System.out.println("main --> " + i);
  19. }
  20. }

1.3 线程的常用方法

1.3.1 currentThread()

Thread.currentThread()方法可以获得当前线程

  • Java 中的任何一段代码都是执行在某个线程当中的. 执行当前代码的线程就是当前线程.
  • 同一段代码可能被不同的线程执行, 因此当前线程是相对的,Thread.currentThread()方法的返回值是在代码实际运行时候的线程对象

1.3.2 setName()/getName()

thread.setName(线程名称): 设置线程名称 thread.getName(): 返回线程名称 通过设置线程名称, 有助于程序调试, 提高程序的可读性, 建议为每个线程都设置一个能够体现线程功能的名称

1.3.3 isAlive()

thread.isAlive() 判断当前线程是否处于活动状态 活动状态就是线程已启动并且尚未终止

1.3.4 sleep()

Thread.sleep(millis); 让当前线程休眠指定的毫秒数 当前线程是指Thread.currentThread()返回的线程

1.3.5 getId()

thread.getId()可以获得线程的唯一标识 注意:

  • 某个编号的线程运行结束后, 该编号可能被后续创建的线程使用
  • 重启JVM 后, 同一个线程的编号可能不一样

1.3.6 yield()

Thread.yield()方法的作用是放弃当前的 CPU 资源

1.3.7 setPriority()

thread.setPriority(num); 设置线程的优先级

  • java 线程的优先级取值范围是 1 ~ 10 , 如果超出这个范围会抛出异常 IllegalArgumentException.
  • 在操作系统中, 优先级较高的线程获得 CPU 的资源越多
  • 线程优先级本质上是只是给线程调度器一个提示信息, 以便于调度器决定先调度哪些线程. 注意不能保证优先级高的线程先运行.
  • Java 优先级设置不当或者滥用可能会导致某些线程永远无法得到运行, 即产生了线程饥饿.
  • 线程的优先级并不是设置的越高越好, 一般情况下使用普通的优先级即可, 即在开发时不必设置线程的优先级
  • 线程的优先级具有继承性, 在 A 线程中创建了 B 线程,则 B 线程的 优先级与 A 线程是一样的.

1.3.8 interrupt()

中断线程. 注意调用 interrupt()方法仅仅是在当前线程打一个停止标志, 并不是真正的停止线程

1.3.9 setDaemon()

Java 中的线程分为用户线程守护线程 守护线程是为其他线程提供服务的线程, 如垃圾回收器(GC)就是一个典型的守护线程 守护线程不能单独运行, 当 JVM 中没有其他用户线程, 只有守护线程时, 守护线程会自动销毁, JVM 会退出

1.4 线程的生命周期

线程的生命周期是线程对象的生老病死, 即线程的状态
线程生命周期可以通过 getState()方法获得, 线程的状态是 Thread.State 枚举类型定义的, 由以下几种:

  • NEW, 新建状态. 创建了线程对象, 在调用 start()启动之前的状态;
  • RUNNABLE, 可运行状态. 它是一个复合状态, 包 含:READY 和 RUNNING 两个状态.
    • READY状态该线程可以被线程调度器进行调度使它处于 RUNNING 状 态 ,
    • RUNING 状态表示该线程正在执行 .
    • Thread.yield()方法可以把线程由 RUNNING 状态转换为 READY 状态
  • BLOCKED 阻塞状态. 线程发起阻塞的 I/O 操作, 或者申请由其他线程占用的独占资源, 线程会转换为 BLOCKED 阻塞状态. 处于阻塞状态的 线程不会占用CPU 资源. 当阻塞I/O 操作执行完, 或者线程获得了其申请的资源, 线程可以转换为 RUNNABLE.
  • WAITING 等待状态. 线程执行了object.wait(), thread.join()方法会 把线程转换为 WAITING 等待状态, 执行 object.notify()方法, 或者加入的线程执行完毕, 当前线程会转换为RUNNABLE 状态
  • TIMED_WAITING 状态, 与 WAITING 状态类似, 都是等待状态. 区别在于处于该状态的线程不会无限的等待, 如果线程没有在指定的时间范围内完成期望的操作, 该线程自动转换为 RUNNABLE
  • TERMINATED 终止状态, 线程结束处于终止状态

image.png

1.5 多线程编程的优势与存在的风险

多线程编程具有以下优势:

  • 提高系统的吞吐率(Throughout). 多线程编程可以使一个进程有多个并发(concurrent, 即同时进行的)的操作
  • 提高响应性(Responsiveness). Web 服务器会采用一些专门的线程负责用户的请求处理, 缩短了用户的等待时间
  • 充分利用多核(Multicore)处理器资源. 通过多线程可以充分的 利用 CPU 资源

多线程编程存在的问题与风险:

  • 线程安全(Thread safe)问题. 多线程共享数据时,如果没有采取正确的并发访问控制措施, 就可能会产生数据一致性问题, 如读取脏数据(过期的数据), 如丢失数据更新.
  • 线程活性(thread liveness)问题. 由于程序自身的缺陷或者由资源稀缺性导致线程一直处于非 RUNNABLE 状态,这就是线程活性问题, 常见的活性故障有以下几种: (1) 死锁(Deadlock). 类似鹬蚌相争. (2) 锁死(Lockout), 类似于睡美人故事中王子挂了 (3) 活锁(Livelock). 类似于小猫咬自己尾巴 (4) 饥饿(Starvation).类似于健壮的雏鸟总是从母鸟嘴中抢到食 物.
  • 上下文切换(Context Switch). 处理器从执行一个线程切换到执行另外一个线程
  • 可靠性. 可能会由一个线程导致 JVM 意外终止, 其他的线程也无法执行

    2 线程安全问题

    2.1 原子性

    原子(Atomic)就是不可分割的意思. 原子操作的不可分割有两层含义:

  • 访问(读, 写)某个共享变量的操作从其他线程来看, 该操作要么已经执行完毕, 要么尚未发生, 即其他线程年示到当前操作的中间结果

  • 访问同一组共享变量的原子操作是不能够交错的

如现实生活中从 ATM 机取款, 对于用户来说,要么操作成功, 用户拿到钱, 余额减少了, 增加了一条交易记录; 要么没拿到钱, 相当于取款操作没有发生
Java 有两种方式实现原子性: 一种是使用锁; 另一种利用处理器 的 CAS(Compare and Swap)指令.

  • 锁具有排它性, 保证共享变量在某一时刻只能被一个线程访问.
  • CAS 指令直接在硬件(处理器和内存)层次上实现, 看作是硬件锁

    2.2 可见性

    在多线程环境中, 一个线程对某个共享变量进行更新之后, 后续其他的线程可能无法立即读到这个更新的结果, 这就是线程安全问题的另外一种形式: 可见性(visibility).
    如果一个线程对共享变量更新后, 后续访问该变量的其他线程可以读到更新的结果, 称这个线程对共享变量的更新对其他线程可见, 否则称这个线程对共享变量的更新对其他线程不可见. 多线程程序因为可见性问题可能会导致其他线程读取到了旧数据 (脏数据)

    2.3 有序性

    有序性(Ordering)是指在什么情况下一个处理器上运行的一个线程所执行的内存访问操作在另外一个处理器运行的其他线程看来是乱序的(Out of Order).
    乱序是指内存访问操作的顺序看起来发生了变化

    2.3.1 重排序

    在多核处理器的环境下, 编写的顺序结构, 这种操作执行的顺序可能是没有保障的:

  • 编译器可能会改变两个操作的先后顺序;

  • 处理器也可能不会按照目标代码的顺序执行;

这种一个处理器上执行的多个操作, 在其他处理器来看它的顺序与目标代码指定的顺序可能不一样, 这种现象称为重排序. 重排序是对内存访问有序操作的一种优化, 可以在不影响单线程程序正确的情况下提升程序的性能. 但是,可能对多线程程序的正确性产生影响, 即可能导致线程安全问题
重排序与可见性问题类似, 不是必然出现的. 与内存操作顺序有关的几个概念:

  • 源代码顺序, 就是源码中指定的内存访问顺序.
  • 程序顺序, 处理器上运行的目标代码所指定的内存访问顺序
  • 执行顺序, 内存访问操作在处理器上的实际执行顺序
  • 感知顺序, 给定处理器所感知到的该处理器及其他处理器的内存访问操作的顺序

可以把重排序分为指令重排序与存储子系统重排序两种.

  • 指令重排序主要是由 JIT 编译器, 处理器引起的, 指程序顺序与执行顺序不一样
  • 存储子系统重排序是由高速缓存, 写缓冲器引起的, 感知顺序与执行顺序不一致

    2.3.2 指令重排序

    在源码顺序与程序顺序不一致, 或者程序顺序与执行顺序不一致的情况下, 我们就说发生了指令重排序(Instruction Reorder).
    指令重排是一种动作, 确实对指令的顺序做了调整, 重排序的对象指令.
    javac 编译器一般不会执行指令重排序, 而 JIT 编译器可能执行指令重排序.
    处理器也可能执行指令重排序, 使得执行顺序与程序顺序不一致.
    指令重排不会对单线程程序的结果正确性产生影响, 可能导致多线程程序出现非预期的结果.

    2.3.3 存储子系统重排序

    存储子系统是指写缓冲器与高速缓存.
    高速缓存(Cache)是 CPU 中为了匹配与主内存处理速度不匹配而设计的一个高速缓存写缓冲器(Store buffer, Write buffer)用来提高写高速缓存操作的效率
    即使处理器严格按照程序顺序执行两个内存访问操作, 在存储子系统的作用下, 其他处理器对这两个操作的感知顺序与程序顺序不一致, 即这两个操作的顺序看起来像是发生了变化, 这种现象称为存储子系统重排序
    存储子系统重排序并没有真正的对指令执行顺序进行调整, 而是造成一种指令执行顺序被调整的现象. 存储子系统重排序对象是内存操作的结果.
    从处理器角度来看, 读内存就是从指定的 RAM 地址中加载数据到寄存器, 称为 Load 操作; 写内存就是把数据存储到指定的地址表示 的 RAM 存储单元中,称为 Store 操作.
    内存重排序有以下四种可能:

  • LoadLoad 重排序, 一个处理器先后执行两个读操作 L1 和 L2, 其他处理器对两个内存操作的感知顺序可能是 L2->L1

  • StoreStore重排序, 一个处理器先后执行两个写操作W1和W2, 其他处理器对两个内存操作的感知顺序可能是 W2->W1
  • LoadStore 重排序, 一个处理器先执行读内存操作 L1 再执行写内存操作 W1, 其他处理器对两个内存操作的感知顺序可能是 W1->L1
  • StoreLoad重排序, 一个处理器先执行写内存操作W1再执行读内存操作 L1, 其他处理器对两个内存操作的感知顺序可能是 L1->W1
  • 内存重排序与具体的处理器微架构有关, 不同架构的处理器所允许的内存重排序不同 内存重排序可能会导致线程安全问题. 假设有两个共享变量 int data = 0; boolean ready = false;

image.png

2.3.4 貌似串行语义

JIT 编译器, 处理器, 存储子系统是按照一定的规则对指令, 内存操作的结果进行重排序, 给单线程程序造成一种假象——指令是按照源码的顺序执行的. 这种假象称为貌似串行语义. 并不能保证多线程环境程序的正确性
为了保证貌似串行语义, 有数据依赖关系的语句不会被重排序, 只有不存在数据依赖关系的语句才会被重排序. 如果两个操作(指令)访问同一个变量, 且其中一个操作(指令)为写操作, 那么这两个操作之间就存在数据依赖关系(Data dependency).
如:
x = 1; y = x + 1; 后一条语句的操作数包含前一条语句的执 行结果;
y = x; x = 1; 先读取 x 变量,再更新 x 变量的值;
x = 1; x = 2; 两条语句同时对一个变量进行写操作
如果不存在数据依赖关系则可能重排序,如:
double price = 45.8;
int quantity = 10;
double sum = price * quantity;
存在控制依赖关系的语句允许重排. 一条语句(指令)的执行结果会决定另一条语句(指令)能否被执行, 这两条语句(指令)存在控制依赖关系(Control Dependency). 如在 if 语句中允许重排,可能存在处理器先执行 if 代码块,再判断 if 条件是否成立

2.3.5 保证内存访问的顺序性

可以使用 volatile 关键字, synchronized 关键字实现有序性

2.4 Java 内存模型

image.png

3 线程同步

3.1 线程同步机制简介

线程同步机制是一套用于协调线程之间的数据访问的机制. 该机制可以保障线程安全. Java 平台提供的线程同步机制包括: 锁, volatile 关键字, final 关键字, static 关键字, 以及相关的 API, 如 Object.wait()/Object.notify()等

3.2 锁概述

线程安全问题的产生前提是多个线程并发访问共享数据. 将多个线程对共享数据的并发访问转换为串行访问, 即一个共享数据一次只能被一个线程访问. 锁就是复用这种思路来保障线程安全的
锁(Lock)可以理解为对共享数据进行保护的一个许可证. 对于同一个许可证保护的共享数据来说, 任何线程想要访问这些共享数据必须先持有该许可证. 一个线程只有在持有许可证的情况下才能对这些共享数据进行访问; 并且一个许可证一次只能被一个线程持有; 许可证线程在结束对共享数据的访问后必须释放其持有的许可证
一线程在访问共享数据前必须先获得锁; 获得锁的线程称为锁的持有线程; 一个锁一次只能被一个线程持有. 锁的持有线程在获得锁之后和释放锁之前这段时间所执行的代码称为临界区(Critical Section).
锁具有排他性(Exclusive), 即一个锁一次只能被一个线程持有. 这 种锁称为排它锁或互斥锁(Mutex).
image.png
JVM 把锁分为内部锁和显示锁两种. 内部锁通过 synchronized 关键字实现; 显示锁通过java.concurrent.locks.Lock 接口的实现类实现的

3.2.1 锁的作用

锁可以实现对共享数据的安全访问. 保障线程的原子性, 可见性与有序性.
锁是通过互斥保障原子性. 一个锁只能被一个线程持有, 这就保证临界区的代码一次只能被一个线程执行. 使得临界区代码所执行的操作自然而然的具有不可分割的特性, 即具备了原子性.
可见性的保障是通过写线程冲刷处理器的缓存和读线程刷新处理器缓存这两个动作实现的. 在 java 平台中, 锁的获得隐含着刷新处理器缓存的动作, 锁的释放隐含着冲刷处理器缓存的动作.
锁能够保障有序性. 写线程在临界区所执行的在读线程所执行的临界区看来像是完全按照源码顺序执行的.
注意: 使用锁保障线程的安全性, 必须满足以下条件: 这些线程在访问共享数据时必须使用同一个锁即使是读取共享数据的线程也需要使用同步锁

3.2.2 锁的相关概念

  1. 可重入

可重入性(Reentrancy)描述这样一个问题: 一个线程持有该锁的时候能再次(多次)申请该锁
image.png
如果一个线程持有一个锁的时候还能够继续成功申请该锁, 称该锁是可重入的, 否则就称该锁为不可重入的

  1. 锁的争用与调度

Java 平台中内部锁属于非公平锁, 显示 Lock 锁既支持公平锁又支持非公平锁

  1. 锁的粒度

一个锁可以保护的共享数据的数量大小称为锁的粒度. 锁保护共享数据量大, 称该锁的粒度粗, 否则就称该锁的粒度细.
锁的粒度过粗会导致线程在申请锁时会进行不必要的等待. 锁的粒度过细会增加锁调度的开销.

3.3 内部锁:synchronized 关键字

3.4 轻量级同步机制:volative 关键字

3.5 CAS

3.6 原子变量类

4 线程间通信

4.1 等待-通知机制

4.1.1 什么是等待通知机制

在单线程编程中, 要执行的操作需要满足一定的条件才能执行, 可以把这个操作放在 if 语句块中。
在多线程编程中, 可能 A 线程的条件没有满足只是暂时的, 稍后其他的线程 B 可能会更新条件使得 A 线程的条件得到满足. 可以将 A 线程暂停, 直到它的条件得到满足后再将 A 线程唤醒. 它的伪代码:

  1. atomics{
  2. //原子操作
  3. while( 条件不成立 ){
  4. 等待
  5. }
  6. 当前线程被唤醒条件满足后,继续执行下面的操作
  7. }

4.1.2 等待通知机制的实现

Object 类中的 wait()方法可以使执行当前代码的线程等待, 暂停执行, 直到接到通知或被中断为止.
注意:
1) wait()方法只能在同步代码块中由锁对象调用
2) 调用 wait()方法, 当前线程会释放锁
其伪代码如下:

  1. //在调用 wait()方法前获得对象的内部锁
  2. synchronized( 锁对象 ){
  3. while( 条件不成立 ){
  4. //通过锁对象调用 wait()方法暂停线程,会释放锁对象锁对象.wait();
  5. }
  6. //线程的条件满足了继续向下执行
  7. }

Object 类的 notify()可以唤醒线程, 该方法也必须在同步代码块中由锁对象调用 . 没有使用锁对象调用 wait()/notify() 会抛 出 IlegalMonitorStateExeption异常. 如果有多个等待的线程, notify()方法只能唤醒其中的一个. 在同步代码块中调用 notify()方法后, 并不会立即释放锁对象, 需要等当前同步代码块执行完后才会释放锁对象, 一般将 notify()方法放在同步代码块的最后. 它的伪代码如下:

  1. synchronized( 锁对象 ){
  2. //执行修改保护条件的代码
  3. //唤醒其他线程
  4. 锁对象.notify();
  5. }

使用wait-notify实现一个Lock-美团面试题目

class MyLock {
    // 当前占有锁的线程id,没有线程占有的时候是-1
    private long owner = -1;
    public synchronized void lock() {
        // 首先判断是否是合法操作,假设锁不可重入
        if (Thread.currentThread().getId() == owner) {
            throw new IllegalStateException("不可重复获取锁");
        }
        // 加锁操作是将当前占有锁的线程变成自己
        // 使用当前锁是否被占用作为判断条件,如果锁被占用,current thread等待
        while (owner != -1) {
            try {
                System.out.println(String.format("thread %s 等待...", Thread.currentThread().getId()));
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 此时锁对象被释放,之前等待的线程可以去竞争获得锁对象
        owner = Thread.currentThread().getId();
        System.out.println(String.format("thread %s 获得锁...", Thread.currentThread().getId()));
    }

    public synchronized void unlock() {
        // 首先判断是否是合法操作,只有持有锁对象的线程才可以释放锁
        if (Thread.currentThread().getId() != owner) {
            throw new IllegalStateException("只有持有锁的线程可以释放锁");
        }
        // 锁对象的占有设为null
        owner = -1;
        System.out.println(String.format("thread %s 释放锁...", Thread.currentThread().getId()));
        this.notifyAll();
    }
}

public class Test02 {
    public static void main(String[] args) throws Exception {
        MyLock lock = new MyLock();
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 20; i++) {
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    lock.lock();
                    System.out.println(String.format("thread %s 正在运行...", Thread.currentThread().getId()));
                    try {
                        // 休眠随机时间,模拟执行任务
                        Thread.sleep(new Random().nextInt(1000));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        lock.unlock();
                    }
                }
            });
        }
    }
}
thread 12 获得锁...
thread 12 正在运行...
thread 16 等待...
thread 15 等待...
thread 14 等待...
thread 13 等待...
thread 12 释放锁...
thread 13 获得锁...
thread 13 正在运行...
thread 14 等待...
thread 15 等待...
thread 16 等待...
thread 12 等待...
...

4.1.3 interrupt()方法会中断 wait()

当线程处于 wait()等待状态时, 调用线程对象的 interrupt()方法会中断线程的等待状态, 会产生 InterruptedException 异常

/**
 * interrupt()结束等待
 */
public class Test03 {

    // 锁对象
    private static final Object lockObj = new Object();

    static class SubThread extends Thread {
        @Override
        public void run() {
            synchronized (lockObj) {
                try {
                    // wait
                    System.out.println("开始等待...");
                    lockObj.wait();
                    System.out.println("结束等待...");
                } catch (InterruptedException e) {
                    System.out.println("wait被中断...");
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SubThread t = new SubThread();
        t.start();
        // 主线程睡眠3秒,确保子线程进入wait状态
        Thread.sleep(3000);
        t.interrupt();
    }
}
开始等待...
wait被中断...

4.1.4 notify()与notifyAll()

notify()一次只能唤醒一个线程, 如果有多个等待的线程, 只能随机唤醒其中的某一个; 想要唤醒所有等待线程, 需要调用 notifyAll().

public class Test04 {

    static class SubThread extends Thread {
        // 定义实例变量作为锁对象
        private Object lockObj;

        SubThread(Object lockObj) {
            this.lockObj = lockObj;
        }

        @Override
        public void run() {
            synchronized (lockObj) {
                try {
                    // wait
                    System.out.println("thread" + Thread.currentThread().getId() + "开始等待...");
                    lockObj.wait();
                    System.out.println("thread" + Thread.currentThread().getId() + "结束等待...");
                } catch (InterruptedException e) {
                    System.out.println("thread" + Thread.currentThread().getId() + "wait被中断...");
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 定义一个对象作为子线程的锁对象
        Object obj = new Object();
        SubThread t1 = new SubThread(obj);
        SubThread t2 = new SubThread(obj);
        SubThread t3 = new SubThread(obj);
        t1.start();
        t2.start();
        t3.start();
        // 主线程睡眠3秒,确保子线程进入wait状态
        Thread.sleep(3000);
        // 调用notify唤醒子进程
        synchronized (obj) {
            // 调用一次notify只能唤醒其中的一个线程,其他等待的线程仍然处于等待状态
            // 对于处于等待状态的线程来说,错过了通知信号,这种现象也称为信号丢失
            // obj.notify();
            // 唤醒所以线程
            obj.notifyAll();
        }
    }
}

4.1.5 wait(long)的使用

wait(long)带有 long 类型参数的 wait()等待, 如果在参数指定的时间内没有被唤醒, 超时后会自动唤醒

public class Test05 {
    public static void main(String[] args) {
        Object obj = new Object();
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj) {
                    try {
                        System.out.println("start wait");
                        // 5000毫秒内没有被唤醒,则自动唤醒
                        obj.wait(5000);
                        System.out.println("end wait");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t.start();
    }
}

4.1.6 通知过早

线程 wait()等待后, 可以调用 notify()唤醒线程, 如果 notify()唤醒的过早, 在等待之前就调用了notify()可能会打乱程序正常的运行逻辑

public class Test06 {
    public static void main(String[] args) {
        Object obj = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj) {
                    try {
                        System.out.println("start wait");
                        obj.wait();
                        System.out.println("end wait");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj) {
                    System.out.println("start notify");
                    obj.notify();
                    System.out.println("end notify");
                }
            }
        });
        // 先开启t1再开启t2,大多数情况下,先执行t1的等待,然后由t2把t1唤醒
        // t1.start();
        // t2.start();
        /*
        start wait
        start notify
        end notify
        end wait
        */

        // 如果先开启t2再开启t1,可能会出现t1线程没有收到通知的情况
        t2.start();
        t1.start();
        /*
        start notify
        end notify
        start wait
         */
    }
}

4.1.7 wait等待条件发生变化

在使用 wait/nofity 模式时, 注意 wait 条件发生了变化, 也可能会造成逻辑的混乱

/**
 * wait 条件发生变化
 * 定义一个集合
 * 定义一个线程向集合中添加数据,添加完数据后通知另外的线程从集合中取数据
 * 定义一个线程从集合中取数据,如果集合中没有数据就等待
 */
public class Test07 {

    static final List<Integer> list = new ArrayList<>();
    static Random random = new Random();

    // 添加元素到链表尾部
    static class AddThread extends Thread {
        @Override
        public void run() {
            synchronized (list) {
                int item = random.nextInt();
                System.out.println("添加一个元素:" + item);
                list.add(item);
                list.notify();
            }
        }
    }

    // 取出链表尾部
    static class GetThread extends Thread {
        @Override
        public void run() {
            synchronized (list) {
                // 注意,这里的条件判断应该用while
                // 如果用if,当前线程被唤醒并竞争到锁之后,直接执行下面的取数据逻辑,万一list为空,就会有空指针异常
                // 用while的话竞争到锁之后还会再去判断一下list是否为空(条件发生了变化),为空还会再去wait
                if (list.size() == 0) {
                    try {
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 取出这个数据
                System.out.println("取出一个元素:" + list.get(0));
            }
        }
    }

    public static void main(String[] args) {
        AddThread add = new AddThread();
        GetThread get = new GetThread();
        // 测试一: 先开启添加数据的线程,再开启一个取数据的线程,大多数情况下会正常
        // add.start();
        // get.start();

        // 测试二: 先开启取数据的线程,再开启添加数据的线程, 取数据的线程会先等待, 等到添加数据之后, 再取数据
        // get.start();
        // add.start();

        // 测试三: 开启两个取数据的线程, 再开启添加数据的线程
        GetThread get2 = new GetThread();
        get.start();
        get2.start();
        add.start();
    }
}

4.1.8 生产者消费者模式

在 Java 中, 负责产生数据的模块是生产者, 负责使用数据的模块是消费者. 生产者消费者解决数据的平衡问题, 即先有数据然后才能使用, 没有数据时, 消费者需要等待

4.2 ThreadLocal的使用

除了控制资源的访问外, 还可以通过增加资源来保证线程安全. ThreadLocal 主要解决为每个线程绑定自己的值.
ThreadLocal的基本使用

public class Test08 {
    static ThreadLocal threadLocal = new ThreadLocal();

    static class SubThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                // 设置线程关联的值
                threadLocal.set(Thread.currentThread().getName() + "-" + i);
                // 调用get方法读取关联的值
                System.out.println(Thread.currentThread().getName() + " value= " + threadLocal.get());
            }
        }
    }

    public static void main(String[] args) {
        SubThread t1 = new SubThread();
        SubThread t2 = new SubThread();
        t1.start();
        t2.start();
        /*
        Thread-0 value= Thread-0-0
        Thread-1 value= Thread-1-0
        Thread-0 value= Thread-0-1
        Thread-1 value= Thread-1-1
        ...
         */
    }
}

在多线程环境中,把字符串转换为日期对象

  • 多个线程使用同一个 SimpleDateFormat 对象可能会产生线程安全问题,有异常
  • 为每个线程指定自己的 SimpleDateFormat 对象,使用 ThreadLocal

    public class Test09 {
    
      static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<>();
    
      static class ParseDate implements Runnable {
          private int i = 0;
    
          public ParseDate(int i) {
              this.i = i;
          }
    
          @Override
          public void run() {
              try {
                  // 构建日期字符串,秒的值不同
                  String text = "2068 年 11 月 22 日 08:28:" + i % 60;
                  // 先判断当前线程是否有SimpleDateFormat对象,如果没有需要创建一个
                  if (threadLocal.get() == null) {
                      threadLocal.set(new SimpleDateFormat("yyyy 年 MM 月 dd 日 HH:mm:ss"));
                  }
                  // 使用当前线程的SimpleDateFormat对象进行解析字符串
                  Date date = threadLocal.get().parse(text);
                  System.out.println(i + "--" + date);
              } catch (ParseException e) {
                  e.printStackTrace();
              }
    
          }
      }
    
      public static void main(String[] args) {
          // 创建100个线程
          for (int i = 0; i < 100; i++) {
              new Thread(new ParseDate(i)).start();
          }
      }
    }
    

    ThreadLocal初始值
    - 定义ThreadLocal类的子类,在子类中重写initialValue方法指定初始值
    - 这样第一次调用get方法就不会返回null ```java public class Test10 { // 定义ThreadLocal的子类 static class SubThreadLocal extends ThreadLocal {

      // 重写initialValue方法,设置初始值
      @Override
      protected Date initialValue() {
          return new Date();
      }
    

    }

    // 使用自定义的ThreadLocal对象 static SubThreadLocal threadLocal = new SubThreadLocal();

    static class SubThread extends Thread {

      @Override
      public void run() {
          // 在threadLocal没有set的情况下去get
          // 由于重写了initialValue方法,所以取出的是当前时间
          System.out.println(Thread.currentThread().getName() + " value = " + threadLocal.get());
      }
    

    }

    public static void main(String[] args) {

      SubThread t = new SubThread();
      t.start();
    

    } }

<a name="gpz4d"></a>
# 5 Lock 显示锁  
在JDK5中增加了Lock锁接口, 有ReentrantLock实现类, ReentrantLock 锁称为可重入锁, 它功能比 synchronized 多.
<a name="jdy45"></a>
## 5.1 锁的可重入
锁的可重入是指, 当一个线程获得一个对象锁后, 再次请求该对象锁时是可以获得该对象的锁的.
```java
/**
 * Lock 锁的基本使用
 */
public class Test01 {
    public synchronized void sm1() {
        System.out.println("同步方法1执行");
        /*
        线程执行sm1方法,当前对象this作为锁对象,在sm1中调用sm2,注意当前线程还是持有this锁对象的
        sm2同步方法默认的锁对象也是this对象,要执行sm2必须先获得this锁对象
        当前this对象被当前线程持有,可以再次获得this对象,这就是锁的可重入性,假设锁不可重入的话,可能会造成死锁
         */
        sm2();
    }

    private synchronized void sm2() {
        System.out.println("同步方法2执行");
        sm3();
    }

    private synchronized void sm3() {
        System.out.println("同步方法2执行");
    }

    public static void main(String[] args) {
        Test01 test01 = new Test01();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                test01.sm1();
            }
        });
        thread.start(); 
    }
}
同步方法1执行
同步方法2执行
同步方法2执行

5.2 ReentrantLock

5.2.1 ReentrantLock的基本使用

调用 lock()方法获得锁, 调用 unlock()释放锁

public class Test02 {

    // 定义锁
    static Lock lock = new ReentrantLock();

    // 定义方法
    public static void sm() {
        // 获得锁
        lock.lock();
        // 这里是同步代码块
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + "--" + i);
        }
        // 释放锁
        lock.unlock();
    }

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                sm();
            }
        };
        // 启动三个线程
        new Thread(runnable).start();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}
Thread-0--0
Thread-0--1
...
Thread-0--99
Thread-1--0
Thread-1--1
...
Thread-1--99
Thread-2--0
Thread-2--1
...
Thread-2--99
/**
 * 使用 Lock 锁同步不同方法中的同步代码块
 */
public class Test03 {

    // 定义锁对象
    static Lock lock = new ReentrantLock();

    // 方法1
    public static void sm1() {
        try {
            // 获得锁
            lock.lock();
            // 执行业务逻辑
            System.out.println(Thread.currentThread().getName() + "--method 1 start--" + System.currentTimeMillis());
            Thread.sleep(new Random().nextInt(1000));
            System.out.println(Thread.currentThread().getName() + "--method 1 end--" + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    public static void sm2() {
        try {
            // 获得锁
            lock.lock();
            // 执行业务逻辑
            System.out.println(Thread.currentThread().getName() + "--method 2 start--" + System.currentTimeMillis());
            Thread.sleep(new Random().nextInt(1000));
            System.out.println(Thread.currentThread().getName() + "--method 2 end--" + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                sm1();
            }
        };
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                sm2();
            }
        };
        // 每个方法开启3个线程
        new Thread(r1).start();
        new Thread(r1).start();
        new Thread(r1).start();

        new Thread(r2).start();
        new Thread(r2).start();
        new Thread(r2).start();
    }
}
Thread-0--method 1 start--1623502762196
Thread-0--method 1 end--1623502763101
Thread-1--method 1 start--1623502763101
Thread-1--method 1 end--1623502763369
Thread-3--method 2 start--1623502763369
Thread-3--method 2 end--1623502764006
Thread-2--method 1 start--1623502764006
Thread-2--method 1 end--1623502764993
Thread-4--method 2 start--1623502764993
Thread-4--method 2 end--1623502765552
Thread-5--method 2 start--1623502765552
Thread-5--method 2 end--1623502765755

可以看出无论线程执行的是sm1还是sm2,都保证了同步代码块的正确执行。
由于锁对象是全局的,所以某个线程在执行sm1的时候,其他线程无论要执行sm1还是sm2都会被阻塞

5.2.2 ReentrantLock锁的可重入性

public class Test04 {

    static class SubThread extends Thread {
        // 定义锁对象
        // 注意这个Lock不能是实例变量,必须是静态变量,否则不同线程就拥有了自己的锁对象,无法实现同步
        private static Lock lock = new ReentrantLock();
        // 定义变量
        public static int num = 0;

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                try {
                    // 可重入锁指可以重复获得该锁
                    lock.lock();
                    lock.lock();
                    num++;
                } finally {
                    // 加锁两次则必须也释放两次
                    lock.unlock();
                    lock.unlock();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SubThread t1 = new SubThread();
        SubThread t2 = new SubThread();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(SubThread.num);
    }
}
20000
两个线程各加10000次,如果没有线程同步,则会出现丢失修改,得到小于20000的数

5.2.3 lockInterrruptibly()方法

lockInterruptibly() 方法的作用: 如果当前线程未被中断则获得锁, 如果当前线程被中断则出现异常.

public class Test05 {
    static class Servier {
        private Lock lock = new ReentrantLock();
        public void servierMethod(){
            try {
                // 如果使用lock方法,获得锁对象,即使调用了线程的interrupt()方法,也没有真正地中断线程
                // lock.lock();
                // 使用lockInterruptibly方法,如果线程被中断了,不会获得锁,会产生异常
                lock.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()+" -- begin lock");
                // 执行一段耗时的操作
                for (int i = 0; i < Integer.MAX_VALUE; i++) {
                    new StringBuffer();
                }
                System.out.println(Thread.currentThread().getName()+" -- end lock");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName()+" -- release lock");
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Servier s = new Servier();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                s.servierMethod();
            }
        };
        Thread t1 = new Thread(r);
        t1.start();
        Thread.sleep(50);

        Thread t2 = new Thread(r);
        t2.start();
        Thread.sleep(50);
        // 中断Thread-1
        t2.interrupt();
    }
}

Thread-0 -- begin lock
java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
    at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
    at me.nic.lock.Test05$Servier.servierMethod(Test05.java:18)
    at me.nic.lock.Test05$1.run(Test05.java:39)
    at java.lang.Thread.run(Thread.java:748)
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException
    at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
    at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
    at me.nic.lock.Test05$Servier.servierMethod(Test05.java:29)
    at me.nic.lock.Test05$1.run(Test05.java:39)
    at java.lang.Thread.run(Thread.java:748)
Thread-1 -- release lock
Thread-0 -- end lock
Thread-0 -- release lock

对于 synchronized 内部锁来说, 如果一个线程在等待锁, 只有两个结果: 要么该线程获得锁继续执行; 要么就保持等待.
对于 ReentrantLock 可重入锁来说, 提供另外一种可能, 在等待锁的过程中, 程序可以根据需要取消对锁的请求.

/**
 * 通过 ReentrantLock 锁的 lockInterruptibly()方法避免死锁的产生
 */
public class Test06 {
    static class IntLock implements Runnable {
        // 创建两个ReentrantLock锁对象
        public static ReentrantLock lock1 = new ReentrantLock();
        public static ReentrantLock lock2 = new ReentrantLock();
        // 定义整数变量,决定使用哪个锁
        int lockNum;

        public IntLock(int lockNum) {
            this.lockNum = lockNum;
        }

        @Override
        public void run() {
            try {
                // lockNum是奇数,先锁1,再锁2
                if (lockNum % 2 == 1) {
                    lock1.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName() + "获得锁1,还需要获得锁2");
                    Thread.sleep(new Random().nextInt(500));
                    lock2.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName() + "同时获得了锁1和锁2");
                }
                // lockNum是偶数,先锁2,再锁1
                else {
                    lock2.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName() + "获得锁2,还需要获得锁1");
                    Thread.sleep(new Random().nextInt(500));
                    lock1.lockInterruptibly();
                    System.out.println(Thread.currentThread().getName() + "同时获得了锁1和锁2");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 判断当前线程是否持有该锁
                if (lock1.isHeldByCurrentThread())
                    lock1.unlock();
                if (lock2.isHeldByCurrentThread())
                    lock2.unlock();
                System.out.println(Thread.currentThread().getName() + "线程退出");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        IntLock intLock1 = new IntLock(11);
        IntLock intLock2 = new IntLock(22);

        Thread t1 = new Thread(intLock1);
        Thread t2 = new Thread(intLock2);
        t1.start();
        t2.start();
        // 这时候出现了死锁,Thread-0获得锁1,还需要获得锁2;Thread-1获得锁2,还需要获得锁1
        // 在 main 线程,等待 3000 秒,如果还有线程没有结束就中断该线程
        Thread.sleep(3000);
        // 可以中断任何一个线程来解决死锁, t2 线程会放弃对锁 1 的申请,同时释放锁 2, t1 线程会完成它的任务
        if (t2.isAlive())
            t2.interrupt();
    }
}

Thread-0获得锁1,还需要获得锁2
Thread-1获得锁2,还需要获得锁1
java.lang.InterruptedException
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
    at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
    at me.nic.lock.Test06$IntLock.run(Test06.java:37)
    at java.lang.Thread.run(Thread.java:748)
Thread-1线程退出
Thread-0同时获得了锁1和锁2
Thread-0线程退出

5.2.4 tryLock()方法

tryLock(long time, TimeUnit unit) 的作用在给定等待时长内锁没有被另外的线程持有, 并且当前线程也没有被中断, 则获得该锁. 通过该方法可以实现锁对象的限时等待.

/**
 * tryLock(long time, TimeUnit unit) 的基本使用
 */
public class Test06 {
    static class TimeLock implements Runnable {
        // 定义锁对象
        private static ReentrantLock lock = new ReentrantLock();

        @Override
        public void run() {
            try {
                // 3秒内获得锁则返回true
                if (lock.tryLock(3, TimeUnit.SECONDS)) {
                    System.out.println(Thread.currentThread().getName() + "获得锁,执行耗时任务");
                    /*
                    假设Thread-0线程先持有锁,完成任务需要4秒钟
                    Thread-1线程尝试获得锁,Thread-1线程在3秒内还没有获得锁的话,则会放弃
                    Thread-0获得锁,执行耗时任务
                    Thread-1没有获得锁
                     */
                    Thread.sleep(4000);

                    /*
                    假设Thread-0线程先持有锁,完成任务需要2秒钟
                    Thread-1线程尝试获得锁,3秒内可以获得锁对象
                    Thread-0获得锁,执行耗时任务
                    Thread-1获得锁,执行耗时任务
                     */
                    //Thread.sleep(2000);
                }
                // 没有获得锁
                else {
                    System.out.println(Thread.currentThread().getName() + "没有获得锁");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
        }
    }

    public static void main(String[] args) {
        TimeLock timeLock = new TimeLock();
        Thread t1 = new Thread(timeLock);
        Thread t2 = new Thread(timeLock);
        t1.start();
        t2.start();
    }
}

tryLock()在调用时仅锁定未被其他线程持有的锁, 如果调用方法时, 锁对象对其他线程持有, 则放弃. 调用方法尝试获得锁, 如果该锁没有被其他线程占用则返回 true 表示锁定成功; 如果锁被其他线程占用则返回 false, 不等待.

/**
 * tryLock()
 * 当锁对象没有被其他线程持有的情况下才会获得该锁定
 */
public class Test08 {
    static class Service {
        private ReentrantLock lock = new ReentrantLock();

        public void serviceMethod() {
            try {
                if (lock.tryLock()) {
                    System.out.println(Thread.currentThread().getName() + "获得锁");
                    // 模拟执行任务的时长
                    Thread.sleep(3000);
                } else {
                    System.out.println(Thread.currentThread().getName() + "没有获得锁");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放锁");
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Service service = new Service();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                service.serviceMethod();
            }
        };
        Thread t1 = new Thread(r);
        t1.start();
        // 睡眠50毫秒,确保t1线程获得锁
        Thread.sleep(50);
        Thread t2 = new Thread(r);
        t2.start();
    }
}
Thread-0获得锁
Thread-1没有获得锁
Thread-0释放锁
/**
 * 使用 tryLock()可以避免死锁
 */
public class Test09 {
    static class IntLock implements Runnable {
        // 创建两个ReentrantLock锁对象
        public static ReentrantLock lock1 = new ReentrantLock();
        public static ReentrantLock lock2 = new ReentrantLock();
        // 定义整数变量,决定使用哪个锁
        int lockNum;

        public IntLock(int lockNum) {
            this.lockNum = lockNum;
        }

        @Override
        public void run() {
            // lockNum是奇数,先锁1,再锁2
            if (lockNum % 2 == 1) {
                while (true) {
                    try {
                        if (lock1.tryLock()) {
                            System.out.println(Thread.currentThread().getName() + "获得锁1,还需要获得锁2");
                            Thread.sleep(new Random().nextInt(100));
                            try {
                                if (lock2.tryLock()) {
                                    System.out.println(Thread.currentThread().getName() + "同时获得了锁1和锁2");
                                    // 结束run方法,即当前线程结束
                                    return;
                                }
                            } finally {
                                if (lock2.isHeldByCurrentThread()) {
                                    lock2.unlock();
                                }
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        if (lock1.isHeldByCurrentThread()) {
                            lock1.unlock();
                        }
                    }
                }
            }
            // lockNum是偶数,先锁2,再锁1
            else {
                while (true) {
                    try {
                        if (lock2.tryLock()) {
                            System.out.println(Thread.currentThread().getName() + "获得锁2,还需要获得锁1");
                            Thread.sleep(new Random().nextInt(100));
                            try {
                                if (lock1.tryLock()) {
                                    System.out.println(Thread.currentThread().getName() + "同时获得了锁1和锁2");
                                    // 结束run方法,即当前线程结束
                                    return;
                                }
                            } finally {
                                if (lock1.isHeldByCurrentThread()) {
                                    lock1.unlock();
                                }
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        if (lock2.isHeldByCurrentThread()) {
                            lock2.unlock();
                        }
                    }
                }
            }
        }

    }

    public static void main(String[] args) throws InterruptedException {
        IntLock intLock1 = new IntLock(11);
        IntLock intLock2 = new IntLock(22);

        Thread t1 = new Thread(intLock1);
        Thread t2 = new Thread(intLock2);
        t1.start();
        t2.start();

        // 运行后,使用tryLock尝试获得锁,不会傻傻地等待
        // 通过循环不停地再次尝试,如果等待的时间足够长,线程总是会获得想要的资源
    }
}

5.2.5 newCondition()方法

  • 关键字 synchronized 与 wait()/notify()这两个方法一起使用可以实现等待/通知模式.
  • Lock 锁的 newContition()方法返回 Condition 对象, Condition 类也可以实现等待/通知模式.

使用 notify()通知时, JVM 会随机唤醒某个等待的线程. 使用 Condition 类可以进行选择性通知. Condition 比较常用的两个方法:

  • await()会使当前线程等待, 同时会释放锁, 当其他线程调用 signal() 时, 线程会重新获得锁并继续执行.
  • signal()用于唤醒一个等待的线程
  • 注意: 在调用 Condition 的 await()/signal()方法前, 也需要线程持有相关的 Lock 锁. 调用 await()后线程会释放这个锁,在 singal()调用后会从当前 Condition 对象的等待队列中,唤醒一个线程, 唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行.

    /**
    * Condition 等待与通知
    */
    public class Test10 {
      // 定义锁
      static Lock lock = new ReentrantLock();
      // 获得Condition对象
      static Condition condition = lock.newCondition();
    
      static class SubThread extends Thread {
          @Override
          public void run() {
              try {
                  // 在调用await前必须先获得锁
                  lock.lock();
                  System.out.println("method lock");
                  // 等待
                  condition.await();
                  System.out.println("method await");
              } catch (InterruptedException e) {
                  e.printStackTrace();
              } finally {
                  // 释放锁
                  lock.unlock();
                  System.out.println("method unlock");
              }
          }
      }
    
      public static void main(String[] args) throws InterruptedException {
          SubThread t = new SubThread();
          t.start();
          // 子线程启动后,会转入等待状态
          Thread.sleep(3000);
          // 主线程睡眠3秒后,唤醒子线程的等待
          try {
              lock.lock();
              condition.signal();
          } finally {
              lock.unlock();
          }
      }
    }
    method lock
    method await
    method unlock
    
    /**
    * 多个 Condition 实现通知部分线程, 使用更灵活
    */
    public class Test11 {
      static class Service {
          // 定义锁对象
          private ReentrantLock lock = new ReentrantLock();
          // 定义两个Condition对象
          private Condition conditionA = lock.newCondition();
          private Condition conditionB = lock.newCondition();
    
          // 定义方法,使用conditionA等待
          public void waitMethodA() {
              try {
                  lock.lock();
                  System.out.println(Thread.currentThread().getName() + " begin wait A " + System.currentTimeMillis());
                  conditionA.await();  // 等待
                  System.out.println(Thread.currentThread().getName() + " end wait A " + System.currentTimeMillis());
              } catch (InterruptedException e) {
                  e.printStackTrace();
              } finally {
                  lock.unlock();
              }
          }
    
          // 定义方法,使用conditionB等待
          public void waitMethodB() {
              try {
                  lock.lock();
                  System.out.println(Thread.currentThread().getName() + " begin wait B " + System.currentTimeMillis());
                  conditionB.await();  // 等待
                  System.out.println(Thread.currentThread().getName() + " end wait B " + System.currentTimeMillis());
              } catch (InterruptedException e) {
                  e.printStackTrace();
              } finally {
                  lock.unlock();
              }
          }
    
          // 定义方法唤醒conditionA对象上的等待
          public void signalA() {
              try {
                  lock.lock();
                  System.out.println(Thread.currentThread().getName() + " begin signal A " + System.currentTimeMillis());
                  conditionA.signal();
                  System.out.println(Thread.currentThread().getName() + " end signal A " + System.currentTimeMillis());
              } finally {
                  lock.unlock();
              }
          }
    
          // 定义方法唤醒conditionB对象上的等待
          public void signalB() {
              try {
                  lock.lock();
                  System.out.println(Thread.currentThread().getName() + " begin signal B " + System.currentTimeMillis());
                  conditionB.signal();
                  System.out.println(Thread.currentThread().getName() + " end signal B " + System.currentTimeMillis());
              } finally {
                  lock.unlock();
              }
          }
      }
    
      public static void main(String[] args) throws InterruptedException {
          Service service = new Service();
          // 开启两个线程,分别调用waitMethodA、waitMethodB方法
          new Thread(new Runnable() {
              @Override
              public void run() {
                  service.waitMethodA();
              }
          }).start();
          new Thread(new Runnable() {
              @Override
              public void run() {
                  service.waitMethodB();
              }
          }).start();
          // main线程睡眠3秒
          Thread.sleep(3000);
          // 唤醒conditionA对象上的等待,conditionB上仍然继续等待
          service.signalA();
    //        service.signalB();
      }
    }
    Thread-0 begin wait A 1623682450114
    Thread-1 begin wait B 1623682450115
    main begin signal A 1623682453120
    main end signal A 1623682453120
    Thread-0 end wait A 1623682453120
    

    ```java /**

    • 使用 Condition 实现生产者/消费者设计模式, 两个线程交替打印 */ public class Test12 { static class MyService {

      // 创建锁对象
      private Lock lock = new ReentrantLock();
      // 创建condition对象
      private Condition condition = lock.newCondition();
      // 定义交替打印标志
      private boolean flag = true;
      
      // 定义方法只打印--横线
      public void printOne() {
          try {
              // 加锁
              lock.lock();
              while (flag) {
                  System.out.println(Thread.currentThread().getName() + "waiting...");
                  condition.await();
              }
              // 当flag为false时打印
              System.out.println(Thread.currentThread().getName() + "-----------");
              // 修改交替打印标志
              flag = true;
              // 通知另外的线程打印
              condition.signal();
          } catch (InterruptedException e) {
              e.printStackTrace();
          } finally {
              lock.unlock();
          }
      }
      
      // 定义方法只打印**横线
      public void printTwo() {
          try {
              // 加锁
              lock.lock();
              // 当flag为false时等待
              while (!flag) {
                  System.out.println(Thread.currentThread().getName() + "waiting...");
                  condition.await();
              }
              // 当flag为true时打印
              System.out.println(Thread.currentThread().getName() + "**********");
              // 修改交替打印标志
              flag = false;
              // 通知另外的线程打印
              condition.signal();
          } catch (InterruptedException e) {
              e.printStackTrace();
          } finally {
              lock.unlock();
          }
      }
      

      }

      public static void main(String[] args) {

      MyService myService = new MyService();
      // 创建线程打印----
      new Thread(new Runnable() {
          @Override
          public void run() {
              for (int i = 0; i < 100; i++) {
                  myService.printOne();
              }
          }
      }).start();
      // 创建线程打印****
      new Thread(new Runnable() {
          @Override
          public void run() {
              for (int i = 0; i < 100; i++) {
                  myService.printTwo();
              }
          }
      }).start();
      

      } }

Thread-0waiting… Thread-1** Thread-1waiting… Thread-0—————- Thread-0waiting… Thread-1** Thread-1waiting… Thread-0—————- …

<a name="Gg2Kd"></a>
### 5.2.6 公平锁与非公平锁
大多数情况下, 锁的申请都是非公平的. 如果线程 1 与线程 2 都在请求锁 A, 当锁 A 可用时, 系统只是会从阻塞队列中随机的选择一个线程, 不能保证其公平性. <br />公平的锁会按照时间先后顺序, 保证先到先得, 公平锁的这一特点不会出现线程饥饿现象. <br />synchronized 内部锁就是非公平的. ReentrantLock 重入锁提供了一 个构造方法:ReentrantLock(boolean fair) , 当在创建锁对象时实参传递 true 可以把该锁设置为公平锁. 公平锁看起来很公平, 但是要实现公平锁必须要求系统维护一个有序队列, 公平锁的实现成本较高, 性能也低. 因此默认情况下锁是非公平的. 不是特别的需求, 一般不使用公平锁.  
```java
/**
 * 公平锁与非公平锁
 */
public class Test13 {

    // 默认是非公平锁
    static ReentrantLock lock = new ReentrantLock(true);

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        lock.lock();
                        System.out.println(Thread.currentThread().getName() + "获得了锁对象");
                    } finally {
                        lock.unlock();
                    }
                }
            }
        };
        for (int i = 0; i < 5; i++) {
            new Thread(runnable).start();
        }
        /* 运行程序
            1)如果是非公平锁, 系统倾向于让一个线程再次获得已经持有的锁, 这种分配策略是高效的, 非公平的
            2)如果是公平锁, 多个线程不会发生同一个线程连续多次获得锁的可能, 保证了公平性
         */
    }
}
Thread-4获得了锁对象
Thread-1获得了锁对象
Thread-3获得了锁对象
Thread-2获得了锁对象
Thread-0获得了锁对象
Thread-4获得了锁对象
Thread-1获得了锁对象
Thread-3获得了锁对象
Thread-2获得了锁对象
Thread-0获得了锁对象
....

5.2.7 几个常用方法

int getHoldCount() 返回当前线程调用 lock()方法的次数
int getQueueLength() 返回正等待获得锁的线程预估数
int getWaitQueueLength(Condition condition) 返回与 Condition 条件相关的等待的线程预估数
boolean hasQueuedThread(Thread thread) 查询参数指定的线程是否在等待获得锁
boolean hasQueuedThreads() 查询是否还有线程在等待获得该锁
boolean hasWaiters(Condition condition) 查询是否有线程正在等待指定的 Condition 条件
boolean isFair() 判断是否为公平锁
boolean isHeldByCurrentThread() 判断当前线程是否持有该锁
boolean isLocked() 查询当前锁是否被线程持有

5.3 ReentrantReadWriteLock 读写锁

  • synchronized 内部锁与 ReentrantLock 锁都是独占锁(排它锁), 同一时间只允许一个线程执行同步代码块, 可以保证线程的安全性, 但是执行效率低.
  • ReentrantReadWriteLock 读写锁是一种改进的排他锁, 也可以称作共享/排他锁. 允许多个线程同时读取共享数据,但是一次只允许一个线程对共享数据进行更新.
  • 读写锁通过读锁与写锁来完成读写操作. 线程在读取共享数据前必须先持有读锁, 该读锁可以同时被多个线程持有,即它是共享的. 线程在修改共享数据前必须先持有写锁, 写锁是排他的, 一个线程持有写锁时其他线程无法获得相应的锁
  • 读锁只是在读线程之间共享, 任何一个线程持有读锁时, 其他线程都无法获得写锁, 保证线程在读取数据期间没有其他线程对数据进行更新, 使得读线程能够读到数据的最新值, 保证在读数据期间共享变量不被修改
    | | 获得条件 | 排他性 | 作用 | | —- | —- | —- | —- | | 读锁 | 写锁未被任意线程持有 | 对读线程是共享的, 对写线程是排他的 | 允许多个读线程可以同时读取共享数据, 保证在读共享数据时, 没有其他线程对共享数据进行修改 | | 写锁 | 该写锁未被其他线程持 有,并且相应的读锁也未 被其他线程持有 | 对读线程或者写线程都是排他的 | 保证写线程以独占的方式修改共享数据 |

  • 读写锁允许读读共享, 读写互斥, 写写互斥

  • 在java.util.concurrent.locks包中定义了ReadWriteLock接口, 该接口中定义了readLock()返回读锁, 定义 writeLock()方法返回写锁. 该接口的实现类是 ReentrantReadWriteLock.
  • 注意 readLock()与 writeLock()方法返回的锁对象是同一个锁的两个不同的角色, 不是分别获得两个不同的锁. ReadWriteLock 接口实例可以充当两个角色. ```java /**

    • 读写锁基本使用 */ public class Test01 { public static void main(String[] args) {

      // 定义读写锁
      ReadWriteLock rwLock = new ReentrantReadWriteLock();
      // 获得读锁
      Lock readLock = rwLock.readLock();
      // 获得写锁
      Lock writeLock = rwLock.writeLock();
      
      // 读数据
      // 申请读锁
      readLock.lock();
      try {
         读取共享数据
      } finally {
          readLock.unlock();
      }
      
      // 写数据
      // 申请写锁
      writeLock.lock();
      try {
          更新修改共享数据
      } finally {
          writeLock.unlock();
      }
      

      } }

<a name="RbMMk"></a>
### 5.3.1 读读共享
ReadWriteLock 读写锁可以实现多个线程同时读取共享数据, 即读读共享, 可以提高程序的读取数据的效率
```java
/**
 * ReadWriteLock 读写锁可以实现读读共享,允许多个线程同时获得读锁
 */
public class Test02 {
    static class Service {
        // 定义读写锁
        ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        // 定义方法读取数据
        public void read() {
            try {
                // 获得读锁
                readWriteLock.readLock().lock();
                System.out.println(Thread.currentThread().getName() + "获得读锁,开始读取数据" + System.currentTimeMillis());
                // 模拟读取数据用时
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                readWriteLock.readLock().unlock();
            }
        }
    }

    public static void main(String[] args) {
        Service service = new Service();
        // 创建5个线程,调用read()方法
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    service.read();
                }
            }).start();
        }
        // 运行程序后,这多个线程几乎可以同时获得锁读,执行lock后面的代码
    }
}

Thread-1获得读锁,开始读取数据1624020370337
Thread-0获得读锁,开始读取数据1624020370337
Thread-2获得读锁,开始读取数据1624020370338
Thread-3获得读锁,开始读取数据1624020370338
Thread-4获得读锁,开始读取数据1624020370338

5.3.2 写写互斥

通过 ReadWriteLock 读写锁中的写锁, 只允许有一个线程执行 lock() 后面的代码.

/**
 * 演示 ReadWriteLock 的 writeLock()写锁是互斥的,只允许有一个线程持有
 */
public class Test03 {
    static class Service {
        // 先定义读写锁
        ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        // 定义方法修改数据
        public void write() {
            try {
                // 获得写锁
                readWriteLock.writeLock().lock();
                System.out.println(Thread.currentThread().getName() + "获得写锁,开始修改数据" + System.currentTimeMillis());
                // 模拟读取数据用时
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName() + "修改数据完毕" + System.currentTimeMillis());
                readWriteLock.writeLock().unlock();
            }
        }
    }

    public static void main(String[] args) {
        Service service = new Service();
        // 创建5个线程,修改数据
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    service.write();
                }
            }).start();
        }
        // 从执行结果来看,同一时间只有一个线程获得写锁
    }
}
Thread-2获得写锁,开始修改数据1624021488969
Thread-2修改数据完毕1624021491984
Thread-1获得写锁,开始修改数据1624021491984
Thread-1修改数据完毕1624021494994
Thread-0获得写锁,开始修改数据1624021494994
Thread-0修改数据完毕1624021497999
Thread-3获得写锁,开始修改数据1624021497999
Thread-3修改数据完毕1624021500999
Thread-4获得写锁,开始修改数据1624021500999
Thread-4修改数据完毕1624021503999

5.3.3 读写互斥

写锁是独占锁, 是排他锁, 读线程与写线程也是互斥的

/**
 * ReadWriteLock 的读写互斥
 * 一个线程获得读锁时,写线程等待; 
 * 一个线程获得写锁时,其他线程等待
 */
public class Test04 {
    static class Service {
        // 先定义读写锁
        ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        // 获得读锁
        Lock readLock = readWriteLock.readLock();
        // 获得写锁
        Lock writeLock = readWriteLock.writeLock();

        // 定义方法读取数据
        public void read() {
            try {
                // 获得读锁
                readLock.lock();
                System.out.println(Thread.currentThread().getName() + "获得读锁,开始读取数据" + System.currentTimeMillis());
                // 模拟读取数据用时
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName() + "读取数据完毕" + System.currentTimeMillis());
                readLock.unlock();
            }
        }

        // 定义方法修改数据
        public void write() {
            try {
                // 获得写锁
                writeLock.lock();
                System.out.println(Thread.currentThread().getName() + "获得写锁,开始修改数据" + System.currentTimeMillis());
                // 模拟读取数据用时
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName() + "修改数据完毕" + System.currentTimeMillis());
                writeLock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        Service service = new Service();
        // 定义一个线程读数据
        new Thread(new Runnable() {
            @Override
            public void run() {
                service.read();
            }
        }).start();
        // 定义一个线程写数据
        new Thread(new Runnable() {
            @Override
            public void run() {
                service.write();
            }
        }).start();
    }
}

Thread-0获得读锁,开始读取数据1624021589985
Thread-0读取数据完毕1624021592999
Thread-1获得写锁,开始修改数据1624021592999
Thread-1修改数据完毕1624021596009

6 线程管理

6.1 线程组

类似于在计算机中使用文件夹管理文件, 也可以使用线程组来管理线程. 在线程组中定义一组相似(相关)的线程, 在线程组中也可以定义子线程组
Thread 类有几个构造方法允许在创建线程时指定线程组, 如果在创建线程时没有指定线程组则该线程就属于父线程所在的线程组. JVM 在创建 main 线程时会为它指定一个线程组, 因此每个 Java 线程都有一个线程组与之关联, 可以调用线程的 getThreadGroup()方法返回线程组.
线程组开始是出于安全的考虑设计用来区分不同的 Applet, 然而 ThreadGroup 并未实现这一目标, 在新开发的系统中, 已经不常用线程组, 现在一般会将一组相关的线程存入一个数组或一个集合中, 如果仅仅是用来区分线程时,可以使用线程名称来区分, 多数情况下,可以忽略线程组.

6.1.1 创建线程组

public class Test01 {
    public static void main(String[] args) {
        // 1.当前main线程的线程组
        ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
        System.out.println("mainGroup: " + mainGroup); // mainGroup: java.lang.ThreadGroup[name=main,maxpri=10]

        // 2.定义线程组。如果不指定所属线程组,则自动归属到当前线程所属的线程组中
        ThreadGroup group1 = new ThreadGroup("group1");
        System.out.println("group1: " + group1); // group1: java.lang.ThreadGroup[name=group1,maxpri=10]

        // 3.定义线程组,同时指定父线程组
        ThreadGroup group2 = new ThreadGroup(mainGroup, "group2");
        System.out.println("group2: " + group2);  // group2: java.lang.ThreadGroup[name=group2,maxpri=10]

        // 现在 group1 与 group2 都是 maingroup 线程组中的子线程组, 调用线程组的 getParent()方法返回父线程组
        System.out.println(group1.getParent() == group2.getParent());  // true
        System.out.println(group1.getParent() == mainGroup);  // true

        // 4.创建线程时指定所属线程组
        // 在创建线程时,如果没有指定线程组,则默认线程归属到父线程的线程组中
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread());
            }
        };
        // 在main线程中创建了t1线程,称main线程为父线程,t1线程为子线程,
        // t1没有指定线程组则t1线程就归属到父线程main线程的线程组中
        Thread t1 = new Thread(runnable, "t1");
        System.out.println(t1);  // Thread[t1,5,main]

        // 创建线程时,可以指定线程所属线程组
        Thread t2 = new Thread(group1, runnable, "t1");
        Thread t3 = new Thread(group2, runnable, "t2");
        System.out.println(t2);  // Thread[t1,5,group1]
        System.out.println(t3);  // Thread[t2,5,group2]
    }
}

6.1.2 线程组的基本操作

  • activeCount() 返回当前线程组及子线程组中活动线程的数量(近似值)
  • activeGroupCount() 返回当前线程组及子线程组中活动线程组的数量 (近似值)
  • int enumerate(Thread[] list) 将当前线程组中的活动线程复制到参数数组中
  • enumerate(ThreadGroup[] list) 将当前线程组中的活动线程组复制到参数数组中
  • getMaxPriority() 返回线程组的最大优先级,默认是 10
  • getName() 返回线程组的名称
  • getParent() 返回父线程组
  • interrupt() 中断线程组中所有的线程
  • isDaemon() 判断当前线程组是否为守护线程组
  • list() 将当前线程组中的活动线程打印出来
  • parentOf(ThreadGroup g) 判断当前线程组是否为参数线程组的父线程组
  • setDaemon(boolean daemon) 设置线程组为守护线程组

    6.1.3 复制线程组中的线程及子线程组

6.1.4 线程组的批量中断

6.1.5 设置守护线程组

6.2 捕获线程的执行异常

在线程的run方法中, 如果有受检异常必须进行捕获处理, 如果想要获得 run() 方法中出现的运行时异常信息 , 可以 通过回调 UncaughtExceptionHandler 接口获得哪个线程出现了运行时异常. 在 Thread 类中有关处理运行异常的方法有:

  • getDefaultUncaughtExceptionHandler() 获 得 全 局 的 ( 默 认 的)UncaughtExceptionHandler
  • getUncaughtExceptionHandler() 获 得 当 前 线 程 的 UncaughtExceptionHandler
  • setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHan dler eh) 设置全局的 UncaughtExceptionHandler
  • setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)设置当前线程的UncaughtExceptionHandler

当线程运行过程中出现异常, JVM会调用 Thread 类 的 dispatchUncaughtException(Throwable e) 方 法 , 该 方 法 会 调 用 getUncaughtExceptionHandler().uncaughtException(this, e); 如果想要获得线程中出现异常的信息 , 就需 要设置线程的 UncaughtExceptionHandler

6.3 注入 Hook 钩子线程

现在很多软件包括 MySQL, Zookeeper, kafka 等都存在 Hook 线程的 校验机制, 目的是校验进程是否已启动, 防止重复启动程序
Hook 线程也称为钩子线程, 当 JVM 退出的时候会执行 Hook 线程. 经常在程序启动时创建一个.lock 文件, 用.lock 文件校验程序是否启动, 在程序退出(JVM 退出)时删除该.lock 文件, 在 Hook 线程中除了防止重新启动进程外, 还可以做资源释放, 尽量避免在 Hook 线程中进行 复杂的操作.

6.4 线程池

6.4.1 什么是线程池

可以以 new Thread( () -> { 线程执行的任务 }).start(); 这种形式开启一个线程. 当 run()方法运行结束, 线程对象会被 GC 释放.
在真实的生产环境中, 可能需要很多线程来支撑整个应用, 当线程数量非常多时, 反而会耗尽 CPU 资源. 如果不对线程进行控制与管理, 反而会影响程序的性能. 线程开销主要包括: 创建与启动线程的开销; 线程销毁开销; 线程调度的开销; 线程数量受限 CPU 处理器数量.
线程池就是有效使用线程的一种常用方式. 线程池内部可以预先创建一定数量的工作线程, 客户端代码直接将任务作为一个对象提交给线程池, 线程池将这些任务缓存在工作队列中, 线程池中的工作线程不断地从队列中取出任务并执行.
image.png

6.4.2 JDK 对线程池的支持

JDK 提供了一套 Executor 框架,可以帮助开发人员有效的使用线程池
image.png

/**
 * 线程池基本使用
 */
public class Test01 {
    public static void main(String[] args) {
        // 创建有5个线程大小的线程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
        // 向线程池中提交18个任务,这18个任务存储到线程池的阻塞队列中,线程池中这5个线程就从阻塞队列中取任务执行
        for (int i = 0; i < 18; i++) {
            // 传入的target是Runnable接口的实现类
            fixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getId() + "编号的任务在执行任务,开始时间:" + System.currentTimeMillis());
                    try {
                        // 模拟任务执行时长
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

可以看到打印出的内容没有出现过这五个线程id之外的线程id,也就是说一直是这5个工作线程在执行任务

/**
 * 线程池的计划任务
 */
public class Test02 {
    public static void main(String[] args) {
        // 创建一个有调度功能的线程池
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);

        // 1) 延迟2秒后执行任务
        scheduledExecutorService.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId() + "----延迟任务-- - " + System.currentTimeMillis());
            }
        }, 2, TimeUnit.SECONDS);

        // 2) 以固定频率执行任务,开启任务的时间是固定的,在3秒后执行任务,以后每隔2秒重新执行一次
        // 如果任务执行时长超过了时间间隔, 则任务完成后立即开启下个任务
        // 也就是说这里打印出任务结束之后会立即执行下个任务,不会停顿2秒
        scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId() + "----在固定频率开启任务-- - " + System.currentTimeMillis());
                try {
                    // 睡眠模拟任务执行时间
                    TimeUnit.SECONDS.sleep(5);
                    System.out.println("任务结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 3, 2, TimeUnit.SECONDS);

        // 3) 和上面方法的区别在于,无论任务执行用了多久,delay的时间一定会等
        // 此处打印出任务结束后会等待2秒再去执行任务
        scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getId() + "----在固定频率开启任务-- - " + System.currentTimeMillis());
                // 睡眠模拟任务执行时间,如果任务执行时长超过了时间间隔, 则任务完成后立即开启下个任务
                try {
                    TimeUnit.SECONDS.sleep(5);
                    System.out.println("任务结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 3, 2, TimeUnit.SECONDS);
    }
}

6.4.3 核心线程池的底层实现

6.4.4 拒绝策略

7 保障线程安全的设计技术

8 锁的优化及注意事项