网课: 黑马-JUC
进程:程序是静止的,进程实体的运行过程就是进程,是系统进行资源分配的基本单位
进程的特征:并发性、异步性、动态性、独立性、结构性
线程:线程是属于进程的,是一个基本的 CPU 执行单元,是程序执行流的最小单元。线程是进程中的一个实体,是系统独立调度的基本单位,线程本身不拥有系统资源,只拥有一点在运行中必不可少的资源,与同属一个进程的其他线程共享进程所拥有的全部资源
关系:一个进程可以包含多个线程,这就是多线程,比如看视频是进程,图画、声音、广告等就是多个线程
线程的作用:使多道程序更好的并发执行,提高资源利用率和系统吞吐量,增强操作系统的并发性能
并发并行:
同步异步:
线程进程对比:
进程基本上相互独立的,而线程存在于进程内,是进程的一个子集
进程拥有共享的资源,如内存空间等,供其内部的线程共享
进程间通信较为复杂
同一台计算机的进程通信称为 IPC(Inter-process communication)
不同计算机之间的进程通信,需要通过网络,并遵守共同的协议,例如 HTTP
线程通信相对简单,因为线程之间共享进程内的内存,一个例子是多个线程可以访问同一个共享变量
Java 中的通信机制:volatile、等待/通知机制、join 方式、InheritableThreadLocal、MappedByteBuffer
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.jio</groupId>
<artifactId>JUC</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
/**
* @author : JAT
* @version : 1.0
* @email : zgt9321@qq.com
* @since : 2023/12/29
**/
@Slf4j
public class CreateWay {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 继承Thread
Way01 way01 = new Way01();
way01.setName("way01");
way01.start();
// 实现Runnable接口
Way02 way02 = new Way02();
Thread way02T = new Thread(way02, "way02");
way02T.start();
// lambda写法
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("way02 lambda {}", i);
}
}, "way02 lambda").start();
// 实现Callable 传入FutureTask 可以返回结果
Way03 way03 = new Way03();
FutureTask<String> futureTask = new FutureTask<>(way03);
Thread way03T = new Thread(futureTask, "way03");
way03T.start();
// 获取结果
String result = futureTask.get();
log.debug("result way03 futureTask {}", result);
for (int i = 0; i < 10; i++) {
Thread.sleep(100);
log.debug("main {}", i);
}
}
}
@Slf4j
class Way01 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("way01 {}", i);
}
}
}
@Slf4j
class Way02 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("way02 {}", i);
}
}
}
@Slf4j
class Way03 implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.debug("way03 {}", i);
}
return "I am Callable";
}
}
1 - Thread
Thread 创建线程方式:创建线程类,匿名内部类方式
Thread 构造器:
public Thread()
public Thread(String name)
继承 Thread 类的优缺点:
2 - Runnable
Runnable 创建线程方式:创建线程类,匿名内部类方式
Thread 的构造器:
public Thread(Runnable target)
public Thread(Runnable target, String name)
Thread 类本身也是实现了 Runnable 接口,Thread 类中持有 Runnable 的属性,执行线程 run 方法底层是调用 Runnable#run:
public class Thread implements Runnable {
private Runnable target;
public void run() {
if (target != null) {
// 底层调用的是 Runnable 的 run 方法
target.run();
}
}
}
Runnable 方式的优缺点:
缺点:代码复杂一点。
优点:
线程任务类只是实现了 Runnable 接口,可以继续继承其他类,避免了单继承的局限性
同一个线程任务对象可以被包装成多个线程对象
适合多个多个线程去共享同一个资源
实现解耦操作,线程任务代码可以被多个线程共享,线程任务代码和线程独立
线程池可以放入实现 Runnable 或 Callable 线程任务对象
3 - Callable
实现 Callable 接口:
public FutureTask(Callable<V> callable)
:未来任务对象,在线程执行完后得到线程的执行结果
public V get()
:同步等待 task 执行完毕的结果,如果在线程中获取另一个线程执行结果,会阻塞等待,用于线程同步
优缺点:
windows
? 任务管理器可以查看进程和线程数,也可以用来杀死进程
? tasklist 查看进程
? taskkill 杀死进程
linux
? ps -fe 查看所有进程
? ps -fT -p 查看某个进程(PID)的所有线程
? kill 杀死进程
? top 按大写 H 切换是否显示线程
? top -H -p 查看某个进程(PID)的所有线程
Java
? jps 命令查看所有 Java 进程
? jstack 查看某个 Java 进程(PID)的所有线程状态
? jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
? jconsole 远程监控配置
? 需要以如下方式运行你的 java 类
? 修改 /etc/hosts 文件将 127.0.0.1 映射至主机名
? 如果要认证访问,还需要做如下步骤
? 复制 jmxremote.password 文件
? 修改 jmxremote.password 和 jmxremote.access 文件的权限为 600 即文件所有者可读写
? 连接时填入 controlRole(用户名),R&D(密码)
Thread 类 API:
方法 | 说明 |
---|---|
public void start() | 启动一个新线程,Java虚拟机调用此线程的 run 方法 |
public void run() | 线程启动后调用该方法 |
public void setName(String name) | 给当前线程取名字 |
public void getName() | 获取当前线程的名字线程存在默认名称:子线程是 Thread-索引,主线程是 main |
public static Thread currentThread() | 获取当前线程对象,代码在哪个线程中执行 |
public static void sleep(long time) | 让当前线程休眠多少毫秒再继续执行Thread.sleep(0) : 让操作系统立刻重新进行一次 CPU 竞争 |
public static native void yield() | 提示线程调度器让出当前线程对 CPU 的使用 |
public final int getPriority() | 返回此线程的优先级 |
public final void setPriority(int priority) | 更改此线程的优先级,常用 1 5 10 |
public void interrupt() | 中断这个线程,异常处理机制 |
public static boolean interrupted() | 判断当前线程是否被打断,清除打断标记 |
public boolean isInterrupted() | 判断当前线程是否被打断,不清除打断标记 |
public final void join() | 等待这个线程结束 |
public final void join(long millis) | 等待这个线程死亡 millis 毫秒,0 意味着永远等待 |
public final native boolean isAlive() | 线程是否存活(还没有运行完毕) |
public final void setDaemon(boolean on) | 将此线程标记为守护线程或用户线程 |
run start
? run:称为线程体,包含了要执行的这个线程的内容,方法运行结束,此线程随即终止。直接调用 run 是在主线程中执行了 run,没有启动新的线程,需要顺序执行
? start:使用 start 是启动新的线程,此线程处于就绪(可运行)状态,通过新的线程间接执行 run 中的代码
? 说明:线程控制资源类
? run() 方法中的异常不能抛出,只能 try/catch
? 因为父类中没有抛出任何异常,子类不能比父类抛出更多的异常
? 异常不能跨线程传播回 main() 中,因此必须在本地进行处理
sleep yield
? sleep:
Running
进入 Timed Waiting
状态(阻塞)? yield:
join
? public final void join():等待这个线程结束
? 原理:调用者轮询检查线程 alive 状态,t1.join() 等价于:
public final synchronized void join(long millis) throws InterruptedException {
// 调用者线程进入 thread 的 waitSet 等待, 直到当前线程运行结束
while (isAlive()) {
wait(0);
}
}
join 方法是被 synchronized 修饰的,本质上是一个对象锁,其内部的 wait 方法调用也是释放锁的,但是释放的是当前的线程对象锁,而不是外面的锁
当调用某个线程(t1)的 join 方法后,该线程(t1)抢占到 CPU 资源,就不再释放,直到线程执行完毕
线程同步:
public class Test {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r = 10;
});
t1.start();
t1.join();//不等待线程执行结束,输出的10
System.out.println(r);
}
}
interrupt
打断线程
public void interrupt()
:打断这个线程,异常处理机制
public static boolean interrupted()
:判断当前线程是否被打断,打断返回 true,清除打断标记,连续调用两次一定返回 false
public boolean isInterrupted()
:判断当前线程是否被打断,不清除打断标记
打断的线程会发生上下文切换,操作系统会保存线程信息,抢占到 CPU 后会从中断的地方接着运行(打断不是停止)
sleep、wait、join 方法都会让线程进入阻塞状态,打断线程会清空打断状态(false)
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t1.start();
Thread.sleep(500);
t1.interrupt();
System.out.println(" 打断状态: {}" + t1.isInterrupted());// 打断状态: {}false
}
打断正常运行的线程:不会清空打断状态(true)
public static void main(String[] args) throws Exception {
Thread t2 = new Thread(()->{
while(true) {
Thread current = Thread.currentThread();
boolean interrupted = current.isInterrupted();
if(interrupted) {
System.out.println(" 打断状态: {}" + interrupted);//打断状态: {}true
break;
}
}
}, "t2");
t2.start();
Thread.sleep(500);
t2.interrupt();
}
打断 park
park 作用类似 sleep,打断 park 线程,不会清空打断状态(true)
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
System.out.println("park...");
LockSupport.park();
System.out.println("unpark...");
System.out.println("打断状态:" + Thread.currentThread().isInterrupted());//打断状态:true
}, "t1");
t1.start();
Thread.sleep(2000);
t1.interrupt();
}
如果打断标记已经是 true, 则 park 会失效
LockSupport.park();
System.out.println("unpark...");
LockSupport.park();//失效,不会阻塞
System.out.println("unpark...");//和上一个unpark同时执行
可以修改获取打断状态方法,使用 Thread.interrupted()
,清除打断标记
LockSupport 类在 同步 → park-un 详解
终止模式
终止模式之两阶段终止模式:Two Phase Termination
目标:在一个线程 T1 中如何优雅终止线程 T2?优雅指的是给 T2 一个后置处理器
错误思想:
两阶段终止模式图示:
打断线程可能在任何时间,所以需要考虑在任何时刻被打断的处理方法:
public class Test {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start();
Thread.sleep(3500);
tpt.stop();
}
}
class TwoPhaseTermination {
private Thread monitor;
// 启动监控线程
public void start() {
monitor = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
Thread thread = Thread.currentThread();
if (thread.isInterrupted()) {
System.out.println("后置处理");
break;
}
try {
Thread.sleep(1000); // 睡眠
System.out.println("执行监控记录"); // 在此被打断不会异常
} catch (InterruptedException e) { // 在睡眠期间被打断,进入异常处理的逻辑
e.printStackTrace();
// 重新设置打断标记,打断 sleep 会清除打断状态
thread.interrupt();
}
}
}
});
monitor.start();
}
// 停止监控线程
public void stop() {
monitor.interrupt();
}
}
daemon
public final void setDaemon(boolean on)
:如果是 true ,将此线程标记为守护线程
线程启动前调用此方法:
Thread t = new Thread() {
@Override
public void run() {
System.out.println("running");
}
};
// 设置该线程为守护线程
t.setDaemon(true);
t.start();
用户线程:平常创建的普通线程
守护线程:服务于用户线程,只要其它非守护线程运行结束了,即使守护线程代码没有执行完,也会强制结束。守护进程是脱离于终端并且在后台运行的进程,脱离终端是为了避免在执行的过程中的信息在终端上显示
说明:当运行的线程都是守护线程,Java 虚拟机将退出,因为普通线程执行完后,JVM 是守护线程,不会继续运行下去
常见的守护线程:
不推荐使用的方法,这些方法已过时,容易破坏同步代码块,造成线程死锁:
public final void stop()
:停止线程运行
废弃原因:方法粗暴,除非可能执行 finally 代码块以及释放 synchronized 外,线程将直接被终止,如果线程持有 JUC 的互斥锁可能导致锁来不及释放,造成其他线程永远等待的局面
public final void suspend()
:挂起(暂停)线程运行
废弃原因:如果目标线程在暂停时对系统资源持有锁,则在目标线程恢复之前没有线程可以访问该资源,如果恢复目标线程的线程在调用 resume 之前会尝试访问此共享资源,则会导致死锁
public final void resume()
:恢复线程运行
Java Virtual Machine Stacks(Java 虚拟机栈)
:每个线程启动后,虚拟机就会为其分配一块栈内存
线程上下文切换(Thread Context Switch)
:一些原因导致 CPU 不再执行当前线程,转而执行另一个线程
程序计数器(Program Counter Register)
:记住下一条 JVM 指令的执行地址,是线程私有的
当 Context Switch 发生时,需要由操作系统保存当前线程的状态(PCB 中),并恢复另一个线程的状态,包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
JVM 规范并没有限定线程模型,以 HotSopot 为例:
Java 中 main 方法启动的是一个进程也是一个主线程,main 方法里面的其他线程均为子线程,main 线程是这些线程的父线程
线程调度指系统为线程分配处理器使用权的过程,方式有两种:协同式线程调度、抢占式线程调度(Java 选择)
协同式线程调度:线程的执行时间由线程本身控制
抢占式线程调度:线程的执行时间由系统分配
Java 提供了线程优先级的机制,优先级会提示(hint)调度器优先调度该线程,但这仅仅是一个提示,调度器可以忽略它。在线程的就绪状态时,如果 CPU 比较忙,那么优先级高的线程会获得更多的时间片,但 CPU 闲时,优先级几乎没作用
说明:并不能通过优先级来判断线程执行的先后顺序
内核级线程调度的成本较大,所以引入了更轻量级的协程。用户线程的调度由用户自己实现(多对一的线程模型,多个用户线程映射到一个内核级线程),被设计为协同式调度,所以叫协程
有栈协程中有一种特例叫纤程,在新并发模型中,一段纤程的代码被分为两部分,执行过程和调度器:
进程的状态参考操作系统:创建态、就绪态、运行态、阻塞态、终止态
线程由生到死的完整过程(生命周期):当线程被创建并启动以后,既不是一启动就进入了执行状态,也不是一直处于执行状态,在 API 中 java.lang.Thread.State
这个枚举中给出了六种线程状态:
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动,还没调用 start 方法,只有线程对象,没有线程特征 |
Runnable(可运行) | 线程可以在 Java 虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器,调用了 t.start() 方法:就绪(经典叫法) |
Blocked(阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变成 Runnable 状态 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒 |
Timed Waiting (限期等待) | 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait |
Teminated(结束) | run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡 |
NEW → RUNNABLE:当调用 t.start() 方法时,由 NEW → RUNNABLE
RUNNABLE <–> WAITING:
调用 obj.wait() 方法时
调用 obj.notify()、obj.notifyAll()、t.interrupt():
当前线程调用 t.join() 方法,注意是当前线程在 t 线程对象的监视器上等待
当前线程调用 LockSupport.park() 方法
RUNNABLE <–> TIMED_WAITING:调用 obj.wait(long n) 方法、当前线程调用 t.join(long n) 方法、当前线程调用 Thread.sleep(long n)
RUNNABLE <–> BLOCKED:t 线程用 synchronized(obj) 获取了对象锁时竞争失败