文章目录
有不清楚概念的可以点击阅读相关的知识
👇
操作系统,进程,线程的初步总结
☝️
1️⃣观察多线程现象
感受多线程程序和普通程序的区别:
- 每个线程都是一个独立的执行流
- 多个线程之间是 “并发” 执行的.
示例
public class TreadTest {
static class SomeThead extends Thread{
@Override
public void run() {
while(true){
System.out.println("我是另一个线程(执行流B)");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
SomeThead thead = new SomeThead();
thead.start();
while(true){
System.out.println("我是主线程(执行流A)");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
![]()
如果按照之前的逻辑,一段代码的死循环在执行中,是不能执行其他代码的。
而现在有多个执行流,A、B两个执行流之间各自执行自己的,相互不影响。
使用Java自带的 jconsole 命令观察线程
默认路径:C:\Program Files\Java\jdk1.8.0_131\bin

2️⃣多线程的优势-增加运行速度
分别实现四个线程分别对各自的每一段数据进行排序,和一个线程线程中分别对四段进行排序。
省略了最后一步将四个部分的数据进行归并的步骤。
生成数组
public class ArrayHelper {
public static long[] generateArray(int n){
Random random = new Random(20220420);//随机种子
long[] array = new long[n];
for(int i = 0; i < n ; i++){
array[i] = random.nextInt();
}
return array;
}
}
实现并发的排序
public class ConcurrentSort {
static class SortWorker extends Thread{
private final long[] array;
private final int fromIndex;
private final int toIndex;
// 利用构造方法,将待排序的数组区间情况,传入
// 对 array 的 [fromIndex, toIndex) 进行排序
public SortWorker(long[] array,int fromIndex,int toIndex){
this.array = array;
this.fromIndex = fromIndex;
this.toIndex = toIndex;
}
@Override
public void run() {
Arrays.sort(array,fromIndex,toIndex);
}
}
public static void main(String[] args) throws InterruptedException {
long[] array = ArrayHelper.generateArray(4_000_0000);
long start = System.currentTimeMillis();
SortWorker s1 = new SortWorker(array,0,1_000_0000);
s1.start();// 让 s1 进入就绪队列
SortWorker s2 = new SortWorker(array,1_000_0001,2_000_0000);
s2.start();// 让 s2 进入就绪队列
SortWorker s3 = new SortWorker(array,2_000_0000,3_000_0000);
s3.start();// 让 s3 进入就绪队列
SortWorker s4 = new SortWorker(array,3_000_0000,4_000_0000);
s4.start();// 让 s4 进入就绪队列
// 等待 4 个线程全部排序完毕,这 4 个 join 没有前后顺序
s1.join();
s2.join();
s3.join();
s4.join();
// 4 个线程一定全部结束了
// TODO:进行 4 路归并,将 4 个有序数组,归并成一个有序数组
long end = System.currentTimeMillis();
System.out.println("并发排序用时:" + (end - start) + "ms");
}
}
并发排序用时:1401ms
单线程排序
public class SingleSort {
public static void main(String[] args) {
long[] array = ArrayHelper.generateArray(4_000_0000);
long start = System.currentTimeMillis();
Arrays.sort(array,0,1_000_0000);
Arrays.sort(array,1_000_0001,2_000_0000);
Arrays.sort(array,2_000_0001,3_000_0000);
Arrays.sort(array,3_000_0001,4_000_0000);
// TODO:进行 4 路归并,将 4 个有序数组,归并成一个有序数组
long end = System.currentTimeMillis();
System.out.println("非并发排序用时:" + (end - start) + "ms");
}
}
非并发排序用时:3733ms
1.多核环境下,并发排序用时 < 串行排序的耗时
单线程一定可以能跑在一个CPU(核)上,多线程可能工作在多个核上(核亲和性)
2.单核环境下,并发的排序也可能较少!
本身,计算机下有很多线程在等待分配CPU,假如现在有100个线程。
在公平的情况下,排序主线程,只会被分配 1/100 的时间。
当并发时,使用四个线程进行排序,算上其他的99个线程,计算机中共有 99 + 4 = 103个线程
四个线程和单个线程,分给我们的时间占比 4 / 103 > 1 / 100。
OS进行资源调度的单位是线程,衡量耗时是以进程为单位的。
所以,即使单核情况下,一个进程中的线程越多,被分到的时间片越多。
3.那线程越多越好吗?
并不是,创建线程本身是有消耗的,线程调度也需要耗时。
CPU是公共资源,要合理分配使用。
4.并发排序的耗时一定小于串行吗?
不一定,在单核的情况下考虑:
串行的排序:t = t(排区间1) + t(排区间2) + t(排区间3) + t(排区间4)
并发的排序:t = 4*t(创建线程) + t(排区间1) + t(排区间2) + t(排区间3) + t(排区间4) + t(销毁)
并发额外需要创建线程和销毁线程的时间,如果排序用时越短,那么并发的效率越低。
为什么要多线程?
提升整个进程的执行速度,尤其是计算密集性的程序。
单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源。
当一个执行流因故堵塞时,我们可以启用一个新的执行流进行处理(比如因网络堵塞)。
3️⃣创建线程
方法一:继承 Thread 类
1.继承 Thread 来创建一个线程类.
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
2.创建 MyThread 类的实例
MyThread t = new MyThread();
3.调用 start 方法启动线程
t.start(); // 线程开始运行
方法二:实现 Runnable 接口
1.实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
2.创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
Thread t = new Thread(new MyRunnable());
3.调用 start 方法
t.start(); // 线程开始运行
对比上面两种方法:
- 继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
- 实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 则需要使用 Thread.currentThread()
注意:
当有了 Thread 对象后,调用其 start() 方法。
- 已经调用过 start() 后就不能再调用 start() ,否则会发生异常
- 千万不要调用成 run() ,否则就跟调用普通方法运行一样,在主线程下运行的代码,和多线程就没有关系了。
理解start():
调用 start() 方法把线程的状态从新建态变成了就绪态。子线程进入线程调度器的就绪队列中,等待被调度器选中分配CPU,从子线程进入就绪队列后就和主线程在被调度时保持同等地位。
先执行子线程还是主线程呢?
当主线程执行 start() ,说明主线程还在占用资源运行中。但是先执行子线程还是主线程都是有可能的,如果发生线程调度选中子线程就执行子线程,但是调度顺序不可预知。不过大概率还是继续执行主线程,因为刚执行完 start() 就发生线程调度的可能性比较小。
只能保证在调用 start() 前的代码先执行。
什么情况下会发生线程调度(重新选择线程分配CPU)?
- CPU空闲
1.当前运行的线程执行结束。运行 -> 结束
2.当前运行的线程等待外部条件 。运行 -> 阻塞
3.当前运行的线程主动放弃CPU。运行 -> 就绪 - 被调度器主动调度
1.高优先级线程抢占
2.时间片耗尽(最常见)
理解线程的随机性
在多线程的运行过程中,固定的代码会产生不一样的结果,主要原因就是因为在线程的运行过程中,调度的随机性。
只有一个线程就不会有问题。
其他变形
- 匿名内部类创建 Thread 子类对象
// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("使用匿名类创建 Thread 子类对象");
}
};
- 匿名内部类创建 Runnable 子类对象
// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名类创建 Runnable 子类对象");
}
});
- lambda 表达式创建 Runnable 子类对象
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
Thread t4 = new Thread(() -> {
System.out.println("使用匿名类创建 Thread 子类对象");
});
4️⃣Thread 类及常见方法
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关
联。
用我们上面的例子来看,每个执行流,也需要有一个对象来描述,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
1.Thread 的常见构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
【了解】Thread(ThreadGroup group,Runnable target) | 线程可以被用来分组管理,分好的组即为线程组 |
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
2.Thread 的几个常见属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
-
ID 是线程的唯一标识,不同线程不会重复,只能 get 不能 set
-
名称是各种调试工具用到,为了方便给开发者看
可以 get 和 set,也可以重复,可以通过setName()设置,也可以通过 Thead() 构造方法设置
默认情况下,如果没有设置名称,线程名称遵守Thread-num
,num从0往后递增 -
状态表示线程当前所处的一个情况,只能 get 不能 set ,状态的变更由JVM控制
-
线程可以 get 和 set 自己的优先级,优先级高的线程理论上来说更容易被调度到
但是这个优先级设置,只是给JVM一些建议,不能强制让线程有限被调度 -
后台线程,又叫守护线程/精灵线程,可以 get 和 set
后台线程一般是做一些支持工作的线程,前台线程一般是做一些有交互工作的。
比如:写了一个月播放器
1.线程响应用户点击工作(前台)
2.线程取网络上下载歌曲(后台)
注意:
1.JVM会在一个进程的所有非后台线程结束后,才会结束运行,和主线程没有关系
2.和后台线程没有关系,即使后台线程还在工作,也正常退出
-
是否存活,即简单的理解,为 run 方法是否运行结束了
-
线程的中断问题,后面说明
3.启动一个线程-start()
之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。
覆写 run 方法是提供给线程要做的任务。
调用 start 方法, 才真的在操作系统的底层创建出一个线程.
4.中断一个线程
李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如何通知李四停止呢?这就涉及到我们的停止线程的方式了。
目前常见的有以下两种方式:
- 通过共享的标记来进行沟通
- 调用 interrupt() 方法来通知
示例-1: 使用自定义的变量来作为标志位
需要给标志位上加 volatile 关键字(这个关键字的功能后面介绍).
public class ThreadDemo1 {
private static class MyRunnable implements Runnable {
public volatile boolean isQuit = false;
@Override
public void run() {
while (!isQuit) {
System.out.println(Thread.currentThread().getName()
+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()
+ ": 啊!险些误了大事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(5 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
target.isQuit = true;
}
}
//运行结果:
main: 让李四开始转账。
李四: 别管我,我忙着转账呢!
李四: 别管我,我忙着转账呢!
李四: 别管我,我忙着转账呢!
李四: 别管我,我忙着转账呢!
李四: 别管我,我忙着转账呢!
main: 老板来电话了,得赶紧通知李四对方是个骗子!
李四: 啊!险些误了大事
这种中断方式如果线程处于休眠状态时,无法即使停止。
示例-2: 调用 interrupt() 方法来通知
使用 Thread.interrupted()
或者 Thread.currentThread().isInterrupted()
代替自定义标志位.
Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记.
方法 | 说明 |
---|---|
public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位 |
public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
public class ThreadDemo2 {
private static class MyRunnable implements Runnable {
@Override
public void run() {
// 两种方法均可以
while (!Thread.interrupted()) {
//while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName()
+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()
+ ": 有内鬼,终止交易!");
// 注意此处的 break
break;
}
}
System.out.println(Thread.currentThread().getName()
+ ": 啊!险些误了大事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(5 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
thread.interrupt();
}
}
//运行结果:
李四: 别管我,我忙着转账呢!
李四: 别管我,我忙着转账呢!
李四: 别管我,我忙着转账呢!
李四: 别管我,我忙着转账呢!
main: 老板来电话了,得赶紧通知李四对方是个骗子!
李四: 有内鬼,终止交易!
李四: 啊!险些误了大事
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at thread.thread_04_20.test_interrupt.ThreadDemo2$MyRunnable.run(ThreadDemo2.java:18)
at java.lang.Thread.run(Thread.java:748)
thread 收到通知的方式有两种:
- 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通
知,清除中断标志
当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程. - 否则,只是内部的一个中断标志被设置,thread 可以通过
Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
这种方式相比于标志位,通知收到的更及时,即使线程正在 sleep 也可以马上收到。
标志位是否清除, 就类似于一个开关
Thread.interrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 “清除标志位”,返回的是 Thred 内部的中断标志位,默认值为 false,线程请求中断后,值变为了 true,然后又立即清楚标志位变为 false。
Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为"不清除标志位".也就是有线程发送中断请求后,该标志位及以后都为 true,没有清除变为 false。
5.等待一个线程-join()
方法 | 说明 |
---|---|
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos) | 同理,但可以更高精度 |
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。
class Account extends Thread{
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(5);//等待五秒
System.out.println("张三转账成功");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main throws InterruptedException{
public static void main(String[] args) {
Account t = new Account();
t.start();
//t.join();
System.out.println("李四存钱");
}
}
//没有join()的结果:
李四存钱
张三转账成功
//有join()的结果
张三转账成功
李四存钱
6.获取当前线程引用
方法 | 说明 |
---|---|
public static Thread currentThread(); | 返回当前线程对象的引用 |
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
7.休眠当前线程
方法 | 说明 |
---|---|
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis毫秒 |
public static void sleep(long millis, int nanos) throws InterruptedException | 可以更高精度的休眠 |
或者使用TimeUnit.SECONDS.sleep() == Thread.sleep(1000)
从线程的状态的角度,调用 sleep() ,就是让当前线程从 “运行” -> “阻塞”
注意:因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
8.yield关键字
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("张三");
// 先注释掉, 再放开
// Thread.yield();
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("李四");
}
}
}, "t2");
t2.start();
}
可以看到:
- 不使用 yield 的时候, 张三李四大概五五开
- 使用 yield 时, 张三的数量远远少于李四
结论:
- yield 不改变线程的状态, 但是会重新去排队.
- yield 主要用于执行一些耗时较旧的计算任务,为了防止计算机处于“卡顿”状态,时不时得让出一些CPU资源,给OS内的其他进程。
5️⃣线程的状态和状态转移
线程的状态是一个枚举类型 Thread.State
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
- NEW: 安排了工作, 还未开始行动
- RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
- BLOCKED: 表示等待获取锁
- WAITING: 线程在无限等待唤醒
- TIMED_WAITING: 线程在等待唤醒,但设置了时限
- TERMINATED: 工作完成了.

看不太懂状态转移图没事,重点是要理解状态的意义以及各个状态的具体意思。
举个栗子:

刚把李四、王五找来,还是给他们在安排任务,没让他们行动起来,就是 NEW 状态;
当李四、王五开始去窗口排队,等待服务,就进入到 RUNNABLE 状态。
该状态并不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;
当李四、王五因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会等等时,进入
BLOCKED 、WATING 、TIMED_WAITING 状态;
如果李四、王五已经忙完,为 TERMINATED 状态。
所以,之前我们讲过的 isAlive() 方法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着
的。