多线程编程基础
在现代计算机系统中,多线程编程是一种常见的编程范式,它能够充分利用多核处理器的计算资源,提高程序的执行效率和响应速度。本文将详细介绍多线程编程的基础知识,包括进程与线程的概念、并发与并行的区别、Java线程模型等。
进程与线程的概念
进程
进程是操作系统进行资源分配和程序运行的基本单位。它具有独立的地址空间、系统资源拥有者等特性。当用户运行一个程序时,操作系统会创建一个进程,并为它分配所需的资源,如内存空间、磁盘空间、I/O设备等。进程是系统中的并发执行单位,它在操作系统中以独立的方式运行,拥有完整的运行环境。
- 进程是操作系统对一个正在运行的程序的一种抽象结构。
- 进程是指在操作系统中能独立运行并作为资源分配的基本单位,由一组机器指令、数据和堆栈等组成的能独立运行的活动实体。
- 操作系统可以同时运行多个进程,多个进程直接可以并发执行和交换信息。
- 进程在运行是需要一定的资源,如CPU、存储空间和I/O设备等。
- Java编写的程序都运行在Java虚拟机(JVM)中,每当使用Java命令启动一个Java应用程序时,就会启动一个JVM进程。在这个JVM进程内部,所有Java程序代码都是以线程来运行的。JVM找到程序的入口点main()方法,然后运行main()方法,这样就产生了一个线程,这个线程被称为主线程。当main()方法结束后,主线程运行完成,JVM进程也随即退出。
线程
线程是进程中最小的调度单元,它是CPU调度执行的最小单位。一个进程可以包含多个线程,这些线程共享进程的资源,如内存、文件句柄等。线程间的切换通常比进程间快得多,因为线程不需要像进程那样进行完整的上下文切换。线程的引入使得程序能够更加灵活地进行并发执行,提高了系统的响应速度和资源利用率。
线程比进程更轻量
线程能独立运行,独立调度,拥有资源(一般是CPU资源,程序计数器等)
线程调度能大幅度减小调度的成本(相对于进程来说),线程的切换不会引起进程的切换
线程的引入进一步提高了操作系统的并发性,线程能并发执行
同一个进程的多个线程共享进程的资源(省去了资源调度现场保护的很多工作)
协程
- 协程是用户模式下的轻量级线程,操作系统内核对协程一无所知
- 协程的调度完全有应用程序来控制,操作系统不管这部分的调度
- 一个线程可以包含一个或多个协程
- 协程拥有自己的寄存器上下文和栈,协程调度切换时,将寄存器上下纹和栈保存起来,在切换回来时恢复先前保运的寄存上下文和栈
- 协程能保留上一次调用时的状态,看到这里各种生成器(生成器是被阉割的协程)的概念浮现出来了。。
- Windows下的实现叫纤程
管程
把管程放最后还加了一道分割线原因是管程跟上面的几个概念不是同一类东东,虽然长得很像,就像Car和Bar一样。
管程,字面意思,是用来管理进程的。所谓的管程实际上是定义的一种数据结构和控制进程的一些操作的集合。
临界资源的概念:
一次只允许一个进程访问的资源
多个进程只能互斥访问的资源
临界资源的访问需要同步操作,比如信号量就是一种方便有效的进程同步机制。但信号量的方式要求每个访问临界资源的进程都具有wait和signal操作。这样使大量的同步操作分散在各个进程中,不仅给系统管理带来了麻烦,而且会因同步操作的使用不当导致死锁。管程就是为了解决这样的问题而产生的。
管程就是代表共享资源的数据结构以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序共同构成的一个操作系统的资源管理模块。管程被请求和释放临界资源的进程所调用,确保每次仅有一个进程使用该共享资源,这样就可以统一管理对共享资源的所有访问,实现临界资源互斥访问。
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor,更常见的是直接将它称为“锁”)来实现的。
方法级的同步是隐式的,无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的ACCSYNCHRONIZED访问标志得知一个方法是否被声明为
同步方法。当方法调用时,调用指令将会检查方法的ACCSYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成
还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同
步方法所持有的管程将在异常抛到同步方法边界之外时自动释放。
线程上下文切换
线程上下文切换就是因为某些原因导致 cpu 不再执行当前线程的指令流,转而执行另一个线程的指令流,原因一般如下:
- 分配给该线程的当前的时间片刚好用完
- jvm开启垃圾回收,停止所有用户线程
- 有更高优先级的线程待执行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当线程上下文发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住所在线程的下一条 jvm 指令的执行地址,是线程私有的。如果 cpu 频繁的进行线程上下文切换,那么就会影响到程序的性能.
线程的调度模型
目前主要分为两种调度模型:分时调度模型、抢占式调度模型
- 分时调度模型: 平均分配CPU时间片,每个线程占有的CPU时间片长度一样,平均分配,一切平等
- 抢占式调度模型: 系统按照线程优先级分配CPU时间片。优先级高的线程,优先分配CPU时间片;如果所有的就绪线程的优先级相同,那么会随机选择一个;优先级高的线程获取的CPU时间片相对多一些。
- 由于目前大部分操作系统都是使用抢占式调度模型进行线程调度。Java的线程管理和调度是委托给了操作系统完成的,与之相对应,Java的线程调度也是使用抢占式调度模型。所以,Java的线程都有优先级。
线程越多越好?
- 单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活
- 多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的 有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分,也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
- IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一 直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化
并发与并行
IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化。
并发
并发是指多个线程在同一CPU核心上面进行轮流切换的串行执行。操作系统中的任务调度器会将CPU的时间片分给不同的线程,使得多个线程在宏观上看起来像是同时执行的。并发的实现依赖于线程的快速切换和调度,它能够在单核CPU上提高程序的响应速度,使得多个任务能够交替执行,不至于一个任务长时间占用CPU资源。
注意:单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用 cpu ,不至于一个线程总占用 cpu,别的线程没法干活
并行
并行是指多个线程在多个CPU核心上同时执行。并行的实现需要多核CPU的支持,每个核心可以独立地执行一个线程。并行能够显著提高程序的执行效率,特别是在进行大规模计算或数据处理时,通过将任务分配到多个核心上并行执行,可以大幅减少程序的运行时间。
注意:多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的。有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分(参考【阿姆达尔定律】)。也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
并发与并行的区别
- 执行方式:并发是多个线程在单个CPU核心上轮流执行;并行是多个线程在多个CPU核心上同时执行。
- 资源需求:并发不需要多核CPU,而并行需要多核CPU的支持。
- 效率提升:并发主要用于提高程序的响应速度和资源利用率;并行主要用于提高程序的执行效率和计算能力。
线程的生命周期
一个标准的线程主要由三部分组成,即线程描述信息、程序计数器(Program Counter,PC)和栈内存 在线程的结构中,栈内存是代码段中局部变量的存储空间,为线程所独立拥有,在线程之间不共享。在JDK 1.8中,每个线程在创建时默认被分配1MB的栈内存。
通用的线程生命周期
通用的线程生命周期基本上可以用下图这个“五态模型”来描述。这五态分别是:
初始状态
、可运行状态
、运行状态
、休眠状态
和终止状态
。
- 初始状态,指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。
- 可运行状态,指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行。
- 当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了运行状态。
- 运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
- 线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了
这五种状态在不同编程语言里会有简化合并。例如,C 语言的 POSIX Threads 规范,就把初始状态和可运行状态合并了;Java 语言里则把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,而 JVM 层面不关心这两个状态,因为 JVM 把线程调度交给操作系统处理了。除了简化合并,这五种状态也有可能被细化,比如,Java 语言里就细化了休眠状态(这个下面我们会详细讲解)。
Java 中线程的生命周期
Java 语言中线程共有六种状态,分别是:
- NEW(新建状态):线程对象被创建后,但尚未启动时的状态。
- RUNNABLE(可运行状态):线程正在JVM中执行时的状态,可能正在CPU上运行,也可能正在等待CPU时间片。
- BLOCKED(阻塞状态):线程在等待获取一个被其他线程持有的锁时的状态。
- WAITING(等待状态):线程在等待其他线程执行特定操作时的状态,如调用wait()方法。
- TIMED_WAITING(超时等待状态):线程在等待其他线程执行特定操作,但等待时间有限时的状态,如调用sleep()方法。
- TERMINATED(终止状态):线程执行完毕或因异常结束时的状态。
这看上去挺复杂的,状态类型也比较多。但其实在操作系统层面,Java 线程中的
BLOCKED
、WAITING
、TIMED_WAITING
是一种状态,即前面我们提到的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。其中,
BLOCKED
、WAITING
、TIMED_WAITING
可以理解为线程导致休眠状态的三种原因。那具体是哪些情形会导致线程从RUNNABLE
状态转换到这三种状态呢?而这三种状态又是何时转换回RUNNABLE
的呢?以及NEW
、TERMINATED
和RUNNABLE
状态是如何转换的?
1 | class TestState { |
1 | running... |
线程状态转换
假设有线程 Thread t
NEW <–> RUNNABLE
t.start()
方法时,NEW --> RUNNABLE
RUNNABLE <–> WAITING
- t 线程进入
synchronized(obj)
获取了对象锁后,调用obj.wait()
方法时,t 线程进入 waitSet 中,从RUNNABLE --> WAITING
- 调用
obj.notify()
,obj.notifyAll()
,t.interrupt()
时,唤醒的线程都到 entrySet 阻塞队列和其他线程进行锁的竞争- 竞争锁成功,t 线程从
WAITING --> RUNNABLE
- 竞争锁失败,t 线程从
WAITING --> BLOCKED
- 竞争锁成功,t 线程从
- t 线程进入
RUNNABLE <–> WAITING
- 当前线程调用
t.join()
方法时,当前线程从RUNNABLE --> WAITING
- 注意是
当前线程在 t 线程对象在 waitSet 上等待
- 注意是
- t 线程运行结束,或调用了当前线程的
interrupt()
时,当前线程从WAITING --> RUNNABLE
- 当前线程调用
RUNNABLE <–> WAITING
- 当前线程调用
LockSupport.park()
方法会让当前线程从RUNNABLE --> WAITING
- 调用
LockSupport.unpark(目标线程)
或调用了线程 的interrupt()
,会让目标线程从WAITING --> RUNNABLE
- 当前线程调用
RUNNABLE <–> TIMED_WAITING (带超时时间的 wait)
t 线程进入
synchronized(obj)
获取了对象锁后,调用obj.wait(long n)
方法时,t 线程从RUNNABLE --> TIMED_WAITING
t 线程等待时间超过了 n 毫秒,或调用
obj.notify()
,obj.notifyAll()
,t.interrupt()
时;, 唤醒的线程都到 entrySet 阻塞队列和其他线程进行锁的竞争- 竞争锁成功,t 线程从
TIMED_WAITING --> RUNNABLE
- 竞争锁失败,t 线程从
TIMED_WAITING --> BLOCKED
- 竞争锁成功,t 线程从
RUNNABLE <–> TIMED_WAITING
- 当前线程调用
t.join(long n)
方法时,当前线程从RUNNABLE --> TIMED_WAITING
- 注意是
当前线程在 t 线程对象在 waitSet 上等待
- 注意是
- 当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的
interrupt()
时,当前线程从TIMED_WAITING --> RUNNABLE
- 当前线程调用
RUNNABLE <–> TIMED_WAITING
- 当前线程调用
Thread.sleep(long n)
,当前线程从RUNNABLE --> TIMED_WAITING
- 当前线程等待时间超过了 n 毫秒或调用了线程的
interrupt()
,当前线程从TIMED_WAITING --> RUNNABLE
- 当前线程调用
RUNNABLE <–> TIMED_WAITING
- 当前线程调用
LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis)
时,当前线程从RUNNABLE --> TIMED_WAITING
- 调用
LockSupport.unpark(目标线程) 或调用了线程 的 interrupt()
,或是等待超时,会让目标线程从TIMED_WAITING--> RUNNABLE
- 当前线程调用
RUNNABLE <–> BLOCKED
- t 线程用
synchronized(obj)
获取了对象锁时如果竞争失败
,从RUNNABLE –> BLOCKED
\ - 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争
- 如果其中 t 线程竞争成功,从 BLOCKED –> RUNNABLE ,
- 其它失败的线程仍然 BLOCKED
- t 线程用
RUNNABLE <–> TERMINATED
- 当前线程所有代码运行完毕,进入 TERMINATED
Java中的线程实现方式
Java提供了多种方式来实现线程,主要包括以下三种:
- 第一种是继承Thread类
- 第二中是实现Runnable接
- 第三种则是使用FutureTask类
继承Thread类
继承Thread类:通过继承Thread类并重写run()方法来实现线程。这种方式简单直接,但缺点是线程类不能继承其他类,因为Java不支持多继承。
java
1
2
3
4
5
6public class MyThread extends Thread {
public void run() {
// 线程执行的代码
}
}
实现Runnable接口
实现Runnable接口:通过实现Runnable接口并实现run()方法来实现线程。这种方式更加灵活,允许线程类继承其他类,并且更容易与线程池等高级API配合使用。
1
2
3
4
5
6
7
8public class MyRunnable implements Runnable {
public void run() {
// 线程执行的代码
}
}
// 通过阅读Runnable接口的源码,可以看到是一个函数式接口,因此可以使用Lambda表达式简化过程。缺点:
不能获取异步执行目标的结果
不能取消异步执行的任务
解决方案可使用 Future接口和 FutureTask类型可以进行管理的异步任务类
FutureTask类
使用FutureTask类:FutureTask类实现了Runnable和Future接口,可以用于创建有返回值的线程任务。这种方式适用于需要获取线程执行结果的场景。
java
1
2
3
4
5
6
7
8
9
10
11
12public static void main(String[] args) {
FutureTask<Integer> task = new FutureTask<>(() -> {
// 线程执行的代码
return 100; // 返回值
});
new Thread(task).start();
try {
Integer result = task.get(); // 获取线程执行结果
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
通过线程池创建线程
Thread实例在执行完成之后都销毁了,这些线程实例都是不可复用的。实际上创建一个线程实例在时间成本、资源耗费上都很高(稍后会介绍),在高并发的场景中,断然不
能频繁进行线程实例的创建与销毁,而是需要对已经创建好的线程实例进行复用,这就涉及线程池的技术。
- submit()方法在提交异步target执行目标之后会返回Future异步任务实例,以便对target的异步执行过程进行控制,比如取消执行、获取结果等。
- execute()没有任何返回,target执行目标实例在执行之后没有办法对其异步执行过程进行控制,只能任其执行,直到其执行结束。
1 | //创建一个包含三个线程的线程池 |
查看线程
系统命令方式
- Windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
tasklist
查看进程taskkill
杀死进程/F
: 强行终止/T
: 终止进程和它的子进程 ,/PID processID
指定进程的 pid
- linux
ps -fe
查看所有进程ps -fT -p
查看某个进程(PID)的所有线程kill
杀死进程top
按大写 H 切换是否显示线程top -H -p
查看某个进程(PID)的所有线程
- Java
jps
命令查看所有 Java 进程jstack
查看某个 Java 进程(PID)的所有线程状态jconsole
来查看某个 Java 进程中线程的运行情况(图形界面)
jconsole 监控
对于服务器上的 jar 包,需要开启远程连接,以如下方式运行你的 java 类
1
2
3java -Djava.rmi.server.hostname=`ip 地址` -Dcom.sun.management.jmxremote -
Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -
Dcom.sun.management.jmxremote.authenticate=是否认证 java 类修改 /etc/hosts 文件将 127.0.0.1 映射至主机名
如果要认证访问,还需要做如下步骤
- 复制 jmxremote.password 文件,修改 jmxremote.password 和 jmxremote.access 文件的权限为 600, 即文件所有者可读写
- 连接时填入 controlRole(用户名),R&D(密码)
Jstack工具
Jstack命令的语法格式如下:
1 | jstack <pid> //pid表示Java进程id,可以用jps命令查看 |
一般情况下,通过Jstack输出的线程信息主要包括:JVM线程、用户线程等。其中,JVM线程在JVM启动时就存在,主要用于执行譬如垃圾回收、低内存的检测等后台任务,这些线程往往在JVM初始化的时候
就存在。而用户线程则是在程序创建了新的线程时才会生成。这里需要注意的是:
- 在实际运行中,往往一次DUMP的信息不足以确认问题。建议产生三次DUMP信息,如果每次DUMP都指向同一个问题,我们才能确定问题的典型性。
- 不同的Java虚拟机的线程导出来的DUMP信息格式是不一样的,并且同一个JVM的不同版本,DUMP信息也有差别。
Jstack指令所输出的信息中包含以下重要信息:
- tid:线程实例在JVM进程中的id。
- nid:线程实例在操作系统中对应的底层线程的线程id。
- prio:线程实例在JVM进程中的优先级。
- os_prio:线程实例在操作系统中对应的底层线程的优先级。
- 线程状态:如runnable、waiting on condition等。
- 用户线程往往是执行业务逻辑的线程,是大家所关注的重点,也是最容易产生死锁的地方。接下来会用Jstack命令来分析用户线程的WAITING、BLOCKED两种状态。
Thread类
常用方法概述
1 | start() |
线程名称的设置和获取
在Thread类中可以通过构造器Thread(…)初始化
设置线程名称,也可以通过setName(…)
实例方法设置线程名称,取得线程名称可以通过getName()
方法完成。关于线程名称有以下几个要点:
- 线程名称一般在启动线程前设置,但也允许为运行的线程设置名称。
- 允许两个Thread对象有相同的名称,但是应该避免。
- 如果程序没有为线程指定名称,系统会自动为线程设置名称。
Thread-加上自动编号的形式
进行自动命名,如Thread-0、Thread-1等。
一个简单的线程名称操作实例如下:
1 | new Thread(target).start(); // 系统自动设置线程名称 |
start 与 run
start是让线程进入就绪,里面代码不一定立刻运行(CPU 的时间片还没分给它)。每个线程对象的 start 方法只能调用一次,如果调用了多次会出现 illegalThreadStateException。
而 run 则是新线程启动后会调用的方法,如果在构造 Thread 对象时传递了 Runnable 参数,则线程启动后会调用 Runnable 中的 run 方法,否则默认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为。
1
2
3
4
5
6
7
8
9
10
11
12
13public static void main(String[] args) {
Thread thread = new Thread(){
public void run(){
log.debug("我是一个新建的线程正在运行中");
FileReader.read(fileName);
}
};
thread.setName("新建线程");
// thread.start();
thread.run();
log.debug("主线程");
}1
2
3
411:59:40.711 [main] DEBUG com.concurrent.test.Test4 - 主线程
11:59:40.711 [新建线程] DEBUG com.concurrent.test.Test4 - 我是一个新建的线程正在运行中
11:59:40.732 [新建线程] DEBUG com.concurrent.test.FileReader - read [test] start ...
11:59:40.735 [新建线程] DEBUG com.concurrent.test.FileReader - read [test] end ... cost: 3 ms1
2
3
412:03:46.711 [main] DEBUG com.concurrent.test.Test4 - 我是一个新建的线程正在运行中
12:03:46.727 [main] DEBUG com.concurrent.test.FileReader - read [test] start ...
12:03:46.729 [main] DEBUG com.concurrent.test.FileReader - read [test] end ... cost: 2 ms
12:03:46.730 [main] DEBUG com.concurrent.test.Test4 - 主线程只有当调用start方法的时候才是我们创建的Thread类对象t1去执行run方法的代码,但是如果直接调用run方法,则是调用这个方法的线程(即Main线程)直接去执行run方法里面的代码。
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
wait
wait 方法是属于 Object 类中的,wait 过程中线程会释放对象锁,只有当其他线程调用 notify 才能唤醒此线程。
wait 使用时必须先获取对象锁,即必须在 synchronized 修饰的代码块中使用,那么相应的 notify 方法同样必须在 synchronized 修饰的代码块中使用
,如果没有在 synchronized 修饰的代码块中使用时运行时会抛出 IllegalMonitorStateException 的异常示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20private static final Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (obj) {
System.out.println("thread1 start");
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1 end");
}
});
t1.start();
Thread.sleep(1000);
synchronized (obj) {
obj.notify();
}
}
sleep 与 yield
调用 sleep 会让当前线程从
Running
进入Timed Waiting
状态(阻塞)其它线程可以使用
interrupt 方法打断
正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
睡眠结束后的线程未必会立刻得到执行(需要分配到 cpu 时间片)
建议用
TimeUnit.sleep()
代替 Thread 的 sleep 来获得更好的可读性1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20Thread t1 = new Thread("t1") {
public void run() {
log.debug(Thread.currentThread().getName());
//线程睡眠3s
Thread.sleep(3000);
}
};
t1.start();
// TimeUnit
TimeUnit.DAYS.sleep(1);//天
TimeUnit.HOURS.sleep(1);//小时
TimeUnit.MINUTES.sleep(1);//分
TimeUnit.SECONDS.sleep(1);//秒
TimeUnit.MILLISECONDS.sleep(1000);//毫秒
TimeUnit.MICROSECONDS.sleep(1000);//微妙
TimeUnit.NANOSECONDS.sleep(1000);//纳秒调用 yield 会让当前线程从
Running
进入Runnable
就绪状态,然后调度执行其它线程, yield不能保证使得当前正在运行的线程迅速转换到就绪状态。即使完成了迅速切换,系统通过线程调度机制从所有就绪线程中挑选下一个执行线程时,就绪的线程有可能被选中,也有可能不被选中,其调度的过程受到其他因素(如优先级)的影响。具体的实现依赖于操作系统的任务调度器;而 sleep 需要等过了休眠时间之后才有可能被分配 cpu 时间片
线程优先级
- Java中线程优先级可以指定,范围是 1~10。但是并不是所有的操作系统都支持10级优先级的划分(比如有些操作系统只支持3级划分:低,中,高),Java只是给操作系统一个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。
- Java默认的线程优先级为5,线程的执行顺序由调度程序来决定,线程的优先级会在线程被调用之前设定。通常情况下,高优先级的线程将会比低优先级的线程有更高的几率得到执行。我们使用方法Thread类setPriority()实例方法来设定线程的优先级。
- 说明:
- 线程优先级会
提示(hint)调度器
优先调度该线程,但它仅仅是一个提示,调度器可以忽略它 - 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
- 线程优先级会
1 | public static void main(String[] args) throws Exception { |
join 方法
join方法的作用是在当前线程等待其它线程运行结束再运行该线程,即同步,在
主线程
中调用t1.join
,则主线程
会等待 t1 线程执行完之后
再继续执行
分析下面代码, 可以看到打印的结果是0,而不是10,分析如下:首先,线程t1和main线程是并行执行的,t1线程需要1s后才能计算出r=10,但是main线程是立刻就需要打印r的值,因此打印出来的r还是原来的0。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public class JoinTest {
static int r = 0;
public static void main(String[] args) {
test1();
}
private static void test1() {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结束");
r = 10;
});
t1.start();
log.debug("结果为:{}", r);
log.debug("结束");
}
}如果要让r打印的值变成10
我们可以使用sleep方法,让main线程sleep的时间长于t1线程
1
2
3t1.start();
Thread.sleep(3000);
log.debug("结果为:{}", r);sleep的方法明显有点硬编码的意思,不够灵活,这时候join方法就有用处了
1
2
3
4
5t1.start();
//在main线程中等待t1线程的结束才继续执行main线程的代码
t1.join();
t1.join(2000);
log.debug("结果为:{}", r);
interrupt 方法
interrupt方法是打断线程的方法,但是关于打断的线程,需要分情况而论:
- 如果是打断正在sleep、wait、join的线程则会抛出 InterruptedException 异常,并且打断标志置不会返回true,而是返回false
- 如果打断的是正常运行的线程,该线程的打断标记会置为true,但是不会去停止被打断的线程,只是告诉它我想要打断,要真正打断还是需要它自己去停止自己,即给它处理后事的机会
优雅地终止线程
可以调用Thread类的isInterrupted方法获取线程打断标志程:
两阶段终止模式: 它将终止过程分成两个阶段,第一阶段由线程T1向线程T2发送终止指令,第二阶段是由线程T2响应终止指令。这种模式通过将停止线程这个动作分解为准备阶段和执行阶段这两个阶段,提供了一种通用的用于优雅地停止线程的方法!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47/**
* @description 并发设计模式-两阶段终止模式-interrupt
*/
public class Demo {
public static void main(String[] args) throws InterruptedException {
TwoStageTermination t1 = new TwoStageTermination();
t1.start();
Thread.sleep(3000);
t1.stop();
}
}
class TwoStageTermination{
private Thread monitor;
// 启动线程
public void start(){
monitor = new Thread(() -> {
while(true){
Thread currentThread = Thread.currentThread();
// 根据打断标记,退出循环,线程结束
// isInterrupted() 获取打断标记的状态,不会清除打断标记
if(currentThread.isInterrupted()){
System.out.println("打断标记:true, 线程退出!");
break;
}
try {
// 情况一:睡眠中打断,抛出InterruptedException异常,唤醒线程,清除打断标记:false,需要手动重置打断标记为true
Thread.sleep(1000);
// 情况二:线程正常运行,打断后,线程不会自动停止,打断标记置为:true,用打断标记写if判断
System.out.println("线程运行中···");
} catch (InterruptedException e) {
e.printStackTrace();
// 再次打断:重置打断标记为true,使得循环退出
currentThread.interrupt();
}
}
});
monitor.start();
}
// 打断线程
public void stop(){
monitor.interrupt(); // interrupt() 打断线程
}
}
守护线程
setDaemon 方法
public final void setDaemon(boolean on)
,默认 false.- 将该线程标记为守护线程或用户线程。
默认情况下,java 进程需要等待所有的线程结束后才会停止,但是有一种特殊的线程,叫做守护线程,在其他线程全部结束的时候即使守护线程还未结束代码未执行完 java 进程也会停止。
注意要点:
- 该方法必须在启动线程前调用。
t1.setDeamon(true);
,启动之后不能再将用户线程设置为守护线程,否则JVM会抛出一个InterruptedException异常。 - 如果线程全部是守护线程,那么 jvm 就停止。
- 守护线程创建的线程也是守护线程。在创建之后,如果通过调用setDaemon(false)将新的线程显式地设置为用户线程,新的线程可以调整成用户线程。
- 该方法必须在启动线程前调用。
如:Java 垃圾回收线程就是一个典型的守护线程;
Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
sleep,yiled,wait,join 对比
sleep,join,yield,interrupted 是 Thread 类中的方法
wait/notify 是 object 中的方法
sleep 不释放锁、释放 cpu
yiled 不释放锁、释放 cpu
join 释放锁、抢占 cpu
wait 释放锁、释放 cpu
参考
关于 join 的原理和这几个方法的对比:Join() 会不会释放锁?A Short Life-CSDN 博客 join 释放锁吗
join 底层使用的 wait,synchronized(this), 锁的是 thread 对象,调用 join 方法会让调用者进入等待
1
2
3
4
5
6// join 核心
if (millis == 0) { //由于上一步传入参数为 0,因此调用当前判断
while (isAlive()) { //判断子线程是否存活
wait(0); //调用 wait(0) 方法
}
}
总结
多线程编程是现代软件开发中不可或缺的一部分,它能够充分利用多核处理器的计算资源,提高程序的执行效率和响应速度。掌握多线程编程的基础知识,如进程与线程的概念、并发与并行的区别、Java线程模型等,对于编写高效、可靠的并发程序至关重要。在实际开发中,还需要根据具体的应用场景和需求,合理地设计和使用多线程技术,以达到最佳的性能和效果。
参考
黑马程序员全面深入学习 Java 并发编程,JUC 并发编程全套教程