一、为什么学并发
1、加快响应用户的时间
2、使你的代码模块化,异步化,简单化
3、充分利用CPU的资源
二、基础概念
1、进程和线程的区别
(1)进程
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程,从某种角度来看,进程就是用来加载指令、管理内存、管理IO.
进程分为用户进程和系统进程,从操作系统角度来看,进程是程序运行资源分配的最小单位。
(2)线程
线程是CPU调度的最小单位,线程必须依赖于进程而存在,线程是进程中的一个实体,是CPU调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位。
(3)区别
- 进程基本上互相独立的,而线程存在于进程内,是进程的一个子集
- 进程拥有共享的资源,如内存空间等,供其内部的线程共享
- 进程间通信较复杂,线程通信相对简单,因为他们共享进程内的内存
- 线程更轻量,线程上下文切换成本一般要比进程切换上下文成本要低
2、上下文切换
(1)基本概念
它是指CPU(中央处理单元)从一个进程或线程到另一个进程或线程的切换
(2)活动具体秒杀
- 暂停一个进程的处理,并将该进程的CPU状态(即上下文)存储在内存中
- 从内存中获取下一个进程的上下文,并在CPU的寄存器中恢复它
- 返回到程序计数器指示的位置(即返回到进程被中断的代码行)以恢复进程
3、并发和并行
(1)并发Concurrent
一般会将这种线程轮流使用CPU的做法称为并发,总结就一句话就是:微观串行,宏观并行
(2)并行Palallel
多核cpu下,每个核(core)都可以调度运行线程,这时候线程可以是并行的
4、认识java里的线程
java程序天生就是多线程的,一个java程序的运行计算没有用户自己开启的线程,实际也有很多jvm自行启动的线程
三、线程基本使用和讲解
1、线程的创建和启动
(1)使用Thread类或继承Thread类
java
Thread t = new Thread() {
public void run() {
}
}
t.start();
(2)实现Runnable接口配合
java
Runnable able1 = new Runnable() {
public void run() {
}
}
Thread t1 = new Thread(able1, "t1");
t1.start();
//变种
FutureyTask<String> task1 = new FutureTask(() -> "2");
Thread t2 = new Thread(task1, "t2");
t2.start();
System.out.println(task1.get());
使用futureTask 可以获取异步方法返回的数据内容
2、java线程的生命周期
(1)从操作系统来说
- 初始状态
- 就绪状态
- 运行状态
- 阻塞状态
- 终止状态
(2)从java线程说
- 新建状态
- 就绪状态(因为java无法感知是否获取时间片,就绪状态,运行状态是一个状态)
- 超时等待状态
- 等待状态
- 阻塞状态
- 终止状态
3、sleep&yield方法详解
(1)sleep方法
- 调用sleep会让当前线程从Running 进入 timed waiting 状态(阻塞),不会释放对象所
- 其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出interruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用timeunit的sleep代替Thread的sleep来获得更好的可读性
(2)yield方法
- yield会释放CPU资源,让当前线程从running进入runnable状态,让优先级更高的(至少是相同)的线程获取执行机会,不会释放对象锁
- 假设当前进程只有main线程,当调用yield之后main线程会继续运行,因为没有比它优先级更高的线程
- 具体的实现依赖于操作系统的任务调度器
4、线程优先级
线程优先级会提示调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它。通过一个整型成员变量priority来控制优先级,优先级的范围从1-10但是,因为线程优先级是基于操作系统完成的,所以java的线程优先级在有的操作系统中可能会被忽略。
5、join方法详解
等待调用join方法的线程结束之后,程序再继续执行,一般用于等待异步线程执行完结构之后才能继续运行的场景。
6、守护线程
默认情况下,java进程需要等待所有线程都运行结束,才会结束,有一种特殊的线程叫做守护线程,只要其他非守护线程运行结束了,即使守护线程的代码没有完全结束,也会强制结束。
7、利用中断机制正确终止线程
(1)stop(不建议使用)
stop以废弃对于锁的释放是不可控的,
会立即跑出ThreadDeath异常,在run()方法中的任何一个执行指令都可能抛出ThreadDeath异常
(2)interrupt()
安全的中止,是调用某个线程的interrupt()方法对器进行中断操作,通过isInterrupted()来进行判断是否被中断,不建议自定一个取消标识位来中止线程的运行
处于死锁状态的线程无法被中断
8、java内核级别线程模型详解
(1)线程调度模型
- 协同式线程调度(Cooperative Threads-Scheduling) 虚拟线程
- 抢占式线程调度(Preemptive Threads-Scheduling) java线程、操作系统
(2)java线程模型
任何语言实现线程主要有三种方式:
- 使用内核线程实现(1:1实现)
- 使用用户线程实现(1:N实现)
- 使用用户线程加轻量级进程混合实现(N:M实现)
9、java用户级线程虚拟线程的实现
(1)java线程的实现
从JDK1.3起,主流商用java虚拟机的线程模型普遍都被替换为基于操作系统原生线程模型来实现,即采用1:1的线程模型
(2)虚拟线程
在java21中,引入虚拟线程,时一种用户级线程,虚拟线程时java中的一种轻量级线程,它旨在解决传统线程模型中的一些限制,提供了更高效的并发处理能力,允许创建数千甚至数万个虚拟线程,而无需占用大量操作系统资源。
四、等待通知机制
1、java线程间的通信
java的线程有类似的管道机制,用于线程之前的数据传输,而传输媒介为内存,java中的管道输入/输出流主要包括了如下4中具体实现:PipedOutputStream、PipedInputStream、PipedReader、PipedWriter 前面两种时面字节而后面两种是面向字符的。
volatile 最轻量的通信/同步机制 保证了不同线程对于这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说时立即可见的。
java线程之间的通信由Java内存模型(Java Memory Model,简称JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。根据JMM的规定,线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内存中读取。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序提供内存可见性的保证。
2、等待通知机制详解
(1)解决问题
- 难以确保及时性
- 难以降低开销,如果降低睡眠的时间,如果休眠1毫秒,这样消费者能更快迅速地发现条件变化,但是却可能消耗更多的处理器资源,造成无端的浪费。
(2)wait/notify/notifyAll
等待通知机制可以基于对象的wait和notify方式来实现,在一个线程内调用该线程锁对象的wait方法,线程将进入等待队列进行等待知道被唤醒。
等待方遵循如下原则
- 获取对象的锁
- 如果条件不满足,那么调用对象wait()方法,被通知后仍要检查条件
- 条件满足则执行对应的逻辑
(3)lockSupport#park/unpark
LockSupport是JDK中用来实现线程阻塞和唤醒的工具,线程调用park则等待"许可",调用unpark则为指定线程提供"许可"
3、java生命周期总结
图1.1
五、拓展:两阶段终止设计模式
两阶段终止(Two-phase Termination) 模式------优雅的终止线程
1、第一阶段
Java 线程进入终止状态的前提是线程进入 RUNNABLE 状态,而实际上线程也可能处在休眠状态,也就是说,我们要想终止一个线程,首先要把线程的状态从休眠状态转换到 RUNNABLE 状态。利用java线程中断机制的interrupt() 方法,可以让线程从休眠状态转换到RUNNABLE 状态。
2、第二阶段
interrupt() 方法和线程终止的标志位