1. 进程与线程
1.1 进程与线程
进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备,进程就是用来加载指令、管理内存、管理IO的。
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程,也有的程序只能启动一个实例进程
线程
- 一个进程之内可以分到一到多个线程
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
- Java中,线程作为最小调度单位,进程作为资源分配的最小单位。在windows中进程是不活动的,只是作为线程的容器。
二者对比
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较为复杂
- 同一台计算机的进程通信称为IPC
- 不同计算机之间的进程通信,需要通过网络,并遵循共同的协议,例如http
- 线程通信相对简单,因为他们共享进程内的空间,一个例子是多个线程可以访问同一个共享变量
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
1.2 并行与并发
并发:一般将 线程轮流使用CPU
的做法称为并发,concurrent
并行:多核cpu下,每个核都可以调度运行线程,这时候线程是可以并行的,parallel
1.3 应用
从方法调用的角度来讲:
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
2. java线程
2.1 创建和运行线程
方法一. 直接使用Thread
// 创建线程对象
Thread t = new Thread("t") {
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
方法二.使用Runnable配合Thread
把线程和任务分开
- Thread 代表线程
- Runnable 代表可运行的任务 (线程要执行的代码)
Runnable runnable = new Runnable() {
public void run() {
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread(runnable);
// 启动线程
t,start();
方法三.FutureTask配合Thread
FutureTask能够接收Callable类型的参数,用来处理有返回结果的情况
// 创建任务对象
FutureTask<Integer> task = new FutureTask<>(()->{
log.debug("hello");
return 100;
});
// 参数1是任务对象,参数2是线程名字
new Thread(task,"t").start();
// 主线程阻塞,同步等待task执行完毕的结果
Integer result = task.get();
log.debug("结果是:{}",result);
2.2 查看进程线程的方法
windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
- tasklist 查看进程
- taskkill 杀死进程
linux
- ps -ef 查看所有进程
- ps -ft -p 查看某个进程的所有线程
- kill 杀死进程
- top 按大写的 H 是否显示进程
- top -H -p 查看某个进程的所有线程
Java
- jps 查看所有 Java 进程
- jstack 查看某个 Java 进程的所有线程状态
- jconsole 来查看某个 Java 进程中线程的运行情况 (图形界面)
2.3 线程运行原理
栈与栈帧
我们都知道 JVM 中由堆,栈,方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
线程上下文切换
因为以下一些原因导致CPU不在执行当前的线程,转而执行另一个线程的代码:
- 线程的CPU时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、synchronized、lock等方法
当线程上下文切换发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器,它的作用是记住下一条JVM指令的执行地址,是线程私有的。
- 状态包括程序计数器,虚拟机栈中每个栈帧的信息,如局部变量,操作数栈,返回地址等
- 上下文切换频繁发生会影响性能
2.4 sleep 与 yield
sleep
- 调用 sleep 会让当前线程从 Running 到 Timed Waiting 状态
- 其他线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 状态,然后调度执行其它同优先级的线程。如果这时没有同优先级的线程,那么不能保证让当前线程暂停的效果
- 具体的实现依赖于操作系统的任务调度器
线程优先级
- 线程优先级会提示 (hint) 调度器优先调读该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 CPU 比较忙,那么优先级高的线程会获得更多的时间片,但CPU闲时,优先级几乎没作用
2.5 主线程与守护线程
默认情况下,Java进成需要等待所有的线程都运行结束才会结束。但有一种特殊的线程叫做守护线程 ,只要其它非守护线程运行结束了,即时剩余的代码没有执行完,也会强制结束。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
if(Thread.currentThread().isInterrupted()){
break;
}
}
log.debug("结束");
},"t1");
t1.setDaemon(true); // 设置为守护线程
t1.start();
Thread.sleep(1000);
log.debug("结束");
}
- 垃圾回收器就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所有 Tomcat 接收到 Shutdown 命令后,不会等待它们处理完当前请求
2.6 五种状态
这是从操作系统
层面来描述的
-
初始状态:
仅是在语言层面创建了线程对象,还未与操作系统线程关联 -
可运行状态(就绪状态):
指该线程已经被创建(与操作系统线程关联),可以由CPU调度 -
运行状态:
指获取了CPU时间片运行中的状态- 当CPU时间片用完,会从
运行状态
转换为可运行状态
,会导致线程的上下文切换
- 当CPU时间片用完,会从
-
阻塞状态:
- 如果调入了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入
阻塞状态
- 等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至
可运行状态
- 如果调入了阻塞API,如BIO读写文件,这时该线程实际不会用到CPU,会导致线程上下文切换,进入
-
终止状态:
表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态
2.7 六种状态
这是从 Java API 层面来描述的
根据 Thread.State 枚举,分为六种状态
NEW:
线程刚被创建,但是还没有调用 start() 方法RUNNABLE:
当调用了 start() 方法后,注意,Java API 层面的 RUNNABLE 状态涵盖了操作系统的可运行状态、运行状态和阻塞状态。(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)BLOCKED、WAITING、TIMED_WAITING:
都是 Java API 层面对阻塞状态的细分TERMINATED:
当线程代码运行结束
3.共享模型之管程 (有锁 悲观锁 阻塞)
3.1 共享带来的问题
java代码体现:
两个线程对初始值为0的静态变量一个做自增,一个做自减,各做500次,最终结果是0吗?
static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
count--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("" + count); // 结果并不是0
}
临界区
-
一个程序运行多个线程本身是没有问题的
-
问题出在多个线程访问共享资源
- 多个线程读共享资源也不会出现问题
- 在多个线程内对共享资源读写操作时发生指令交错,就会出现问题
-
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
3.2 synchronized 解决方案
为了避免临界区的的竞态条件发生,有多种手段可以达到目的
- 非阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
synchronized 俗称对象锁
,它采用互斥的方式让同一时刻至多只有一个线程能够持有对象锁,其它线程再想获取这个对象锁时就会发生阻塞。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
static int count = 0;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
count++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (lock) {
count--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("" + count);
}
synchronized 实际使用对象锁
保证了临界区内代码的原子性
,临界区内的代码对外是不可分割的,不会被线程切换所打断
3.3 方法上的 synchronized
加在成员方法上,锁住的是this对象
加上静态方法上,锁住的是类对象
3.4 变量的线程安全分析
成员变量和静态变量是否线程安全?
-
如果他们没有共享,则线程安全
-
如果他们被共享了,根据他们的状态能否被改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
-
局部变量是线程安全的
-
但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用范围,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
3.5 常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- LocalDateTime (jdb8新加入的日期安全类)
- java.util.concurrent 包下的类
这里说他们是线程安全的是指,多个线程调用他们同一个实例的某个方法时,是线程安全的。也可以理解为:
- 它们的每个方法是原子的
- 但它们多个方法的组合不是原子的
不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此他们的方法都是线程安全的
4. synchronized 原理
4.1 轻量级锁
轻量级锁的使用场景:如果一个对象虽然有多线程访问,但多线程访问的时间是错开的 (也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized
4.2 锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其他线程为此对象加上了清轻量级锁 (有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
4.3 自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功 (即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
4.4 偏向锁
轻量级锁在没有竞争时 (就自己这个线程),每次重入仍然性需要执行 CAS 操作
Java6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头。之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
5. wait / notify
- Owner 线程发生条件不满足时,调用 wait 方法,即可进入 WaitSet 变为 Waiting 状态
- Blocked 和 Waiting 的线程都处于阻塞状态,不占用 CPU 的时间
- Blocked 线程会在 Owner 线程释放锁时唤醒
- Waiting 线程会在 Owner 线程调用 notify 和 notify all 时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入 EntryList 阻塞对列重新竞争
5.1 API
- obj.wait():让进入 obj 的监视器的线程到 waitSet 等待
- obj.notify():让 obj 上正在 waitSet 等待的线程挑一个唤醒
- obj.notifyAll():让obj 上正在 waitSet 等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于 obj 对象的方法。必须获得此对象的锁,才能调用这几个方法
5.2 wait / notify 的使用
sleep(long n) 和 wait(long n) 的区别:
- sleep 是 Thread 的方法,而 wait 是 Object 的方法
- sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
- sleep 在睡眠的同时,不会释放对象锁,但 wait 在等待的时候会释放对象锁
正确使用
synchronized(lock) {
while(条件不成立){ // 解决虚假唤醒的问题
lock.wait();
}
// 干活
}
// 另一个线程负责唤醒
synchronized(lock) {
lock.notifyAll();
}
5.2 Park & Unpark
它们是 LockSupport 类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象);
案例:
thread t1 = new Thread(()-> {
log.debug("start...");
sleep(1);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
},"t1").start();
sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);
特点:
与 Object 的 wait & niotify 相比
- wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 unpark 不用
- park & unpark 是以线程为单位来
阻塞
和唤醒
线程,而 notify 只能随机唤醒一个等待线程,notifyall 是唤醒所有的线程,就不那么精确
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
6. ReentrantLock
相对于 synchronized 它具备如下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
与 Synchronized 一样,都支持可重入
基本语法
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
6.1 可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的持有者,因此有权利再次获取这把锁,如果是不可重入锁,那么第二次获得锁时,自己也会被挡住
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
log.debug("execute method1");
method2();
}finally{
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
log.debug("execute method2");
}finally{
lock.unlock();
}
}
6.2 可打断
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
// 如果没有竞争,那么此方法就会获取 Lock 对象锁
// 如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 方法打断
log.debug("尝试获取锁");
lock.lockInterruptibly(); // 可打断的锁
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("没有获取到锁,返回");
return;
}
try {
log.debug("获取到锁");
}finally {
lock.unlock();
}
},"t1");
lock.lock();
t1.start();
Thread.sleep(1000);
log.debug("打断t1");
t1.interrupt();
}
6.3 锁超时
立刻失败
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();
}
6.4 公平锁
ReentrantLock 默认也是不公平的
可以通过构造方法设置成公平锁
ReentrantLock lock = new ReentrantLock(true);
6.5 条件变量
synchronized 中也有条件变量,就是 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比:
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室
使用流程
- await 前先要获得锁
- await 执行后会释放锁,进入 conditionObject 等待
- await 的线程被唤醒去重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
// 创建一个新的条件变量
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
lock.lock();
// 进入休息室等待
condition1.await();
condition1.signal();
condition1.signalAll();
}
7. Java 内存模型
JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念。底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等
JMM 体现在以下几个方面:
- 原子性:保证指令不会受到线程上下文切换的影响
- 可见性:保证指令不会受到 CPU 缓存的影响
- 有序性:保证指令不会受到 CPU 指令并行优化的影响
7.1 可见性
案例一:不会停下来的线程
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (run) {
log.debug("run...");
}
},"t1").start();
Thread.sleep(1000);
log.debug("停下来");
run = false; // 现象是t1线程并不会停下来
}
这是由于 t1 线程要频繁的从主内存中读取 run 的值,JIT 即时编译器会将 run 的值缓存至自己的工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
即时 main 线程修改了 run 的值并同步到了主存中,而 t1 线程是从自己的工作内存中的高速缓存中读取的这个变量的值,结果就永远是 true
解决方案:加上 volatile
关键字 (volatile 关键字适用于一个线程写,多个线程读的场景,不能保证原子性)
// 易变
volatile static boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (run) {
log.debug("run...");
}
},"t1").start();
Thread.sleep(1000);
log.debug("停下来");
run = false;
}
volatile
(易变关键字)
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
volatile 的原理:
volatile 的底层原理是内存屏障:
- 对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令前会加入读屏障
注意:
synchronized 语句块既可以保证代码快的原子性,又可以保证块内代码的可见性和有序性。但缺点是 synchronizd
是属于重量级操作,性能相对更低。
7.2 有序性
JVM 在不影响正确性的前提下,可以调整语句的执行顺序。这种特性称之为指令重排
7.3 volatile 原理
7.3.1 如何保证可见性
- 写屏障保证在该屏障之前,对共享变量的改动,都同步到主存当中
public static opreate(I_Result r){
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障 => 在这之前的改动都会同步到主存中去
}
- 而读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中的最新数据
public static opreate(I_Result r) {
// 读屏障 => 保证之后的读取都是在主存中取值
// ready 是 volatile 读取值带屏障
if(ready) {
num = num + num;
}else {
r = 1;
}
}
7.3.2 如何保证有序性
- 写屏障会确保指令重排序时,不会屏障之前的代码排在屏障之后
public static opreate(I_Result r){
num = 2;
ready = true; // ready 是 volatile 赋值带写屏障
// 写屏障
}
- 读屏障会确保指令重排时,不会将读屏障之后的代码排在读屏障之前
public static opreate(I_Result r) {
// ready 是 volatile 读取值带屏障
if(ready) {
num = num + num;
}else {
r = 1;
}
}
线程安全单例习题:
单例模式有很多实现,饿汉、懒汉、静态内部类、枚举类,试分析每种实现下获取单例对象时的线程安全
- 饿汉式:类加载就会导致该实例对象被创建
- 懒汉式:类加载不会导致该单例对象被创建,而是首次使用该对象时才会被创建
实现1:
// 问题1:为什么加final:防止它有子类,子类中有一些不恰当的方法破坏它的单例
// 问题2:如果实现了序列化接口,还要做什么来防止反序列化破坏单例:提供一个readResovle方法
public final class Singleton implements Serializable {
// 问题3:为什么设置为私有?(防止其他会创建,破坏单例对象) 是否能防止反射创建新的实例?(不能)
private Singleton(){}
// 问题4:这样初始化是否能保证单例对象创建时的线程安全? 是
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
// 防止反序列化破坏单例
public Object readResovle() {
return INSTANCE;
}
}
8. 共享模型之无锁 (无锁 乐观锁 非阻塞)
8.1 CAS 与 Volatile
CAS:比较并交换,在每次更新时,都会先将自己刚开始获得的最新值和现在的最新值进行比较,相同才会执行更新操作,不同则循环。
CAS 和 Volatile
获取共享变量时,为了保证该变量的可见性,需要使用 Volatile 修饰
它可以用来修饰成员变量和静态成员变量。它可以避免线程从自己的工作缓存中查找变量的值,必须到主存获取它的值,线程操作 Volatile 变量都是直接操作主存,即一个线程对 Volatile 变量的修改,对另一个线程可见。
注意:Volatile 仅仅保证了共享变量的可见性,让其他线程能够看到最新值,但不能解决指令交错问题 (不能保证原子性)
CAS 必须结束 Volatile 才能读取到共享变量的最新值来实现 比较并交换
的效果
8.2 为什么无锁效率高
- 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得所的情况下,发生上下文切换,进入阻塞。
8.3 CAS 的特点
结合 CAS 和Volatile 可以实现无锁并发,试用于线程数少、多核 CPU 的场景下。
-
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,比较失败重试就行了
-
synchronized 是基于悲观锁的思想:最悲观的估计,需要预防别的线程来修改共享变量,在我操作的时候,其它线程都得等待。
-
CAS 体现的是无锁并发、无阻塞并发
- 因为没有使用 synchronized,所以线程不会阻塞,这是提升效率的因素之一
- 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受到影响
8.4 Unsafe
Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得
8.4.1 Unsafe CAS 操作
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
System.out.println(unsafe);
// 1.获取域的偏移地址
long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));
Teacher t = new Teacher();
// 2.执行 cas 操作
unsafe.compareAndSwapInt(t,idOffset,0,10);
unsafe.compareAndSwapObject(t,nameOffset,null,"lzf");
// 3.验证结果
System.out.println(t);
}
}
@Data
class Teacher {
volatile int id;
volatile String name;
}
9. 共享模型之不可变
9.1 线程安全的日期类 DateTimeFormatter
public static void main(String[] args) {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd"); // 线程安全
for (int i = 0;i < 10; i++) {
new Thread(() -> {
TemporalAccessor parse = dtf.parse("2022-07-15");
log.debug("{}",parse);
}).start();
}
}
9.2 不可变设计
9.2.1 final 的使用
- 属性用 final 修饰保证了该属性是只读的,不能修改
- 类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性
10. 并发工具
10.1 线程池
自定义线程池
public class Test14 {
public static void main(String[] args) {
ThreadPool threadPool = new ThreadPool(2,
1000, TimeUnit.MILLISECONDS, 10,(queue,task)->{
// 死等
queue.put(task);
});
for (int i = 0; i < 5; i++) {
int j = i;
threadPool.execute(() -> {
log.debug("{}",j);
});
}
}
}
@FunctionalInterface // 拒绝策略
interface RejectPolicy<T> {
void reject(BlockingQueue<T> queue,T task);
}
@Slf4j(topic = "c.ThreadPool")
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);
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 queueCapacity,RejectPolicy<Runnable> rejectPolicy) {
this.coreSize = coreSize;
this.timeout = timeout;
this.timeUnit = timeUnit;
this.taskQueue = new BlockingQueue<>(queueCapacity);
this.rejectPolicy = rejectPolicy;
}
class Worker extends Thread{
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
// 执行任务
// 当 task 不为空,执行任务
// 当 task 执行完毕,再接着从任务队列获取任务并执行
//while (task != null || (task = taskQueue.take()) != null) {
while (task != null || (task = taskQueue.poll(timeout,timeUnit)) != null) {
try {
task.run();
}catch (Exception e) {
}finally {
task = null;
}
}
synchronized (workers) {
workers.remove(this);
}
}
}
}
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 capacity;
public BlockingQueue(int capacity) {
this.capacity = capacity;
}
// 带超时的阻塞获取
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(); // ReentrantLock 必须要手动释放
}
}
// 阻塞获取
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(); // ReentrantLock 必须要手动释放
}
}
// 阻塞添加
public void put(T task) {
lock.lock();
try {
while (queue.size() == capacity) {
try {
fullWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
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() == capacity) {
try {
if (nanos <= 0) {
return false;
}
nanos = fullWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
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() == capacity) {
rejectPolicy.reject(this,task);
}else { // 队列还有空闲
queue.addLast(task);
emptyWaitSet.signal();
}
}finally {
lock.unlock();
}
}
}
10.2 ThreadPoolExecutor
10.2.1 线程池状态
ThreadPoolExecutor 使用 int 的高3位来表示线程池状态,低29位表示线程数量。目的是将线程池状态和线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值
—
10.2.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:拒绝策略
-
线程池中刚开始没有进程,当一个任务提交给线程池之后,线程池会创建一个新线程来执行任务
-
当线程数达到 corePoolSize 并没有线程空闲,这时再加入任务,新加的任务会被加入 workQueue 对列排队,直到有空闲的线程
-
如果队列选择了有界队列,那么任务超过了队列大小,会创建 maximumPoolSize - corePoolSize 数目的救急线程来救急
-
如果线程到达 maximumPoolSize 仍有新任务这时会执行拒绝策略,拒绝策略 JDK 体统四种实现,其它著名框架也提供了实现。
- AbortPolicy:让调用者抛出 RejectedExecutionException 异常,这是默认策略
- CallerRunsPolicy:让调用者运行任务
- DiscardPolicy:放弃本次任务
- DiscardOldestPolicy:放弃队列中最早的任务,本任务取而代之
- Dubbo 的实现:在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题
- Netty 的实现:是创建一个新线程来执行任务
- ActiveMQ 的实现:带超时等待 (60s) 尝试放入队列,会逐一尝试策略链中每种拒绝策略
- PinPoint 的实现:它使用一个拒绝策略链,会逐一尝试策略链中每种拒绝策略
-
当高峰过去后,超过 corePoolSize 的救急线程如果一段时间没有任务做,需要结束节省资源,这个事件由 keepAliveTime 和 unit 来控制
10.2.3 newFixedThreadPool
特点:
- 最大线程数 = 核心线程数 (没有救急线程被创建),因此也不需要超时时间
- 阻塞队列是无界的,可以放任意数量的任务
适用于任务量已知,相对耗时的任务
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2, new ThreadFactory() {
private AtomicInteger t = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
return new Thread(r,"mypool_t" + t.getAndIncrement());
}
});
pool.execute(() -> {
log.debug("1");
});
pool.execute(() -> {
log.debug("2");
});
pool.execute(() -> {
log.debug("3");
});
}
10.2.4 newCachedThreadPool
特点:
-
核心线程数是0,最大线程数是 Intger.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着
- 全部都是救急线程 (60s后可以回收)
- 救急线程可以无限创建
-
对列采用了 SynchroouosQueue 实现特点是,它没有容量,没有线程来取是放不进去的 (一手交钱,一手交货)
整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲一分钟后释放线程 适合任务数比较密集,但每个任务执行时间较短的情况
10.2.5 newSingleThreadExecutor
使用场景:
希望多个任务排队执行。线程数固定为1,任务数多于1时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会释放。
区别:
- 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会创建一个线程,保证池的正常工作
public static void main(String[] args) {
test2();
}
public static void test2() {
ExecutorService pool = Executors.newSingleThreadExecutor();
pool.execute(() -> {
log.debug("1");
int i = 1 / 0;
});
pool.execute(() -> {
log.debug("2");
});
pool.execute(() -> {
log.debug("3");
});
}
10.2.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 EnterruptedException;
// 提交 tasks 中所有任务,带超时时间
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,long timeout,TimeUnit unit)
throws EnterruptedException;
// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
<T> T invokeAny(Collection<? extends Callable<T> tasks>)
throws InterruptedException,ExecutionExecption;
// 提交 tesks 中所有任务,哪个任务先成功执行完毕,返回此任务结果,其它任务取消,带超时时间
<T> T invokeAny(Collection<? extends Callable<T> tasks>,long timeout,TimeUnit unit)
throws InterruptedException,ExecutionExecption,TimeoutExecption;
10.2.7 关闭线程池
shutdown:不会阻塞 (调用线程)
/*
线程池状态变为 shutdown
- 不会接收新任务
- 但已提交任务会执行完
- 此方法不会阻塞调用线程的执行
*/
void shutdown();
shutdownNow
/*
线程池状态变为 stop
- 不会接收新任务
- 会将队列中的任务返回
- 并用 interrupt 的方式中断正在执行的任务
*/
List<Runnable> shutdownNow();
10.3 JUC
10.3.1 AQS 原理
全称是 AbstractQueueSynchronizer,是阻塞式锁和相关的同步器工具的框架
特点:
-
用 state 属性来表示资源的状态 (分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
- getState:获取 state 状态
- setState:设置 state 状态
- compareAndSetState:乐观锁机制设置 state 状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
-
提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
-
条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
获取锁:
// 如果获取锁失败
if(!tryAcquire(arg)){
// 入队,可以选择阻塞当前线程 park unpark 机制
}
释放锁:
// 如果释放锁成功
if(tryRelease(arg)){
// 让阻塞线程恢复运行
}
10.3.2 线程安全集合类
10.3.2.1 ConcurrentHashMap
…