Java Web 实战 03 - 多线程基础篇 2
- 二 . Thread类常见方法
- 2.1 Thread 的常见构造方法
- 2.2 Thread 的几个常见属性
- getId()
- getName()
- getState()
- getPriority()
- isDaemon()
- 案例 : 实现 getId()、getName()、 getState()、getPriority()、isDaemon()、isAlive()
- 2.3 启动一个线程-start()
- 2.4 中断线程(线程执行结束)
- 2.4.1 手动创建的标志位
- 2.4.2 Thread自带的标志位
- 2.5 线程等待 join
- 2.5.1 join 是怎么回事?
- 2.5.2 栗子1 : 让 main 等待 t2 , t2 去等待 t1
- 2.5.3 栗子2 : 控制 main 先运行 t1 , t1 执行完再运行 t2
- 2.5.4 带参数版本的 join
- 2.6 获取到线程引用 : Thread.currentThread()
- 2.7 线程的休眠 : sleep
- 三 . 线程的状态
- 3.1 通过代码来看线程的状态
- 3.2 线程状态和状态转移的意义
大家好 , 这篇文章给大家带来的是多线程相关的基础知识 , 我们会介绍一下Thread 类常见的方法都有什么 , 以及启动线程、中断线程、线程等待、获取线程引用、现成的休眠等问题 , 然后再给大家介绍一下线程的状态都有什么这几个问题。
上一篇文章的链接在这里 , 大家移步观看
http://t.csdn.cn/JVErX
由于 C 站的编辑器不太好用 , 导致许多排版没能生效 , 大家可移步至这里观看https://www.yuque.com/jialebihaitao/study/qzym2pw332lm6k7q?singleDoc# 《2. 多线程 (基础)》
感谢大家的支持~
二 . Thread类常见方法
2.1 Thread 的常见构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 (已经用过了) |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
其中第一个和第二个我们已经使用过
- Thread() : 自己创建线程的子类 , 再去 new 他的实例
Thread t = new MyThread();
- Thread(Runnable target) : 使用 Runnable 创建一个任务 , 然后将 Runnable 的任务放到线程中 , 这样也可以创建一个线程
Runnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
- Thread(String name) : 这个操作是给线程起名字,我们之前看到的 Thread-0 就是 Java 帮我们命名的,线程在操作系统里面是没有名字的,Java 为了帮助程序员便于理解,就在 JVM 里面给对应的 Thread 对象起了个名,这个线程是和内核中的线程一一对应的 , 这个名字对于程序的执行 , 没有任何影响 , 对于程序员来说调试挺有用的
Thread t3 = new Thread("这是我的名字");
- Thread(Runnable target, String name) : 既实现了命名 , 又实现了使用 Runnable 对象创建线程对象
public class Demo7 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
System.out.println("Hello MyThread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"俺的线程");
t.start();
while(true) {
System.out.println("Hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
我们使用 jconsole 工具查看当前线程 , 就发现有我们自己重新命名的线程了
2.2 Thread 的几个常见属性
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isinterrupted() |
getId()
获取到的是 线程 在 JVM 里面的身份标识
线程里面的身份标识是有好几个的
- 内核的 PCB 上有标识
- 用户态线程库里面也有标识 (pthread , 操作系统提供的线程库)
- JVM里面也有标识 (JVM 的 Thread 类底层也是调用操作系统的 pThread 库)
这三个标识各不相同,但是目的都是作为身份的区分,我们可以理解为身份标识的各种小名
getName()
获取在Thread的构造方法里面,传入的姓名 (刚才在 Thread 构造方法中传入的名字)
getState()
获取到 PCB 里面的状态 , 这个状态表示线程当前所处的一个情况 . 此处得到的状态是 JVM 里面设立的状态体系 , 这个状态比操作系统内置的状态要更丰富
getPriority()
获取到线程的优先级 , 优先级高的线程理论上来说更容易被调度到
isDaemon()
判断是不是后台线程
Daemon的意思是 “守护线程” 的意思 , 我们可以理解为 “后台线程”
线程分为"前台线程"与"后台线程" , 我们可以通过类比手机的正在运行应用 (前台应用) 与后台应用来理解前台线程与后台线程
一个线程 , 创建出来默认就是前台线程 , 前台线程会阻止进程结束 , 进程会保证所有的前台线程都执行完毕 , 才会退出 . main 线程就是一个前台线程
如果我们屏蔽了 main 线程 , 正常情况下线程就会退出
但是运行并非如此
这是因为我们上面新建的线程默认就是前台线程 , 我们需要将自己创建的线程修改成后台线程
public class Demo7 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
System.out.println("Hello MyThread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"俺的线程");
t.setDaemon(true);// 设置成后台线程
t.start();
// while(true) {
// System.out.println("Hello main");
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// }
}
}
这样我们的线程就会嗖的一下执行完
有个小问题 :
❗ 我们发现 , 怎么运行了多次 , 怎么有的时候打印有的时候就不打印呢?
✅ 举个栗子 : 当我们刚打完一局游戏 , 想要打开 B 站去学习 , 这时候点开了 B 站 , B 站这个时候就是"前台应用" , 必须是在执行的 , 不能被销毁
前台线程 : 前台线程会阻止进程结束 , 一个线程创建出来默认就是前台线程 , 进程会保证所有前台进程执行完毕才结束程序
后台的游戏就被隐藏到任务栏里面了 , 其实还运不运行已经无所谓了 , 因为我们已经玩完了
后台线程 : 后台线程不会阻止进程结束 , 比如 : main线程执行结束 , 整个线程就结束了
这是因为我们把 t 线程设置成了后台线程 , 当 main线程执行完毕整个程序就执行完了 , 我们这里并未对 main 线程做任何操作 , 所以有可能是在 main 线程结束之前运行了一下 t 线程 , 也有可能是直接就执行结束了 , t 线程还没来得及执行
案例 : 实现 getId()、getName()、 getState()、getPriority()、isDaemon()、isAlive()
接下来 , 我们写一个方法 , 来实现 getId()、getName()、 getState()、getPriority()、isDaemon()、isAlive()
public class Demo8 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true) {
System.out.println("Hello MyThread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"俺的线程");
System.out.println(t.getId());// 获取线程ID
System.out.println(t.getName());// 获取线程名称
System.out.println(t.getState());// 获取线程状态
System.out.println(t.getPriority());// 获取线程优先级
System.out.println(t.isDaemon());// 获取是否是后台线程
System.out.println(t.isAlive());// 获取线程是否存活
}
}
2.3 启动一个线程-start()
我们之前已经使用过这个方法了
给大家强调一点 :
(安排任务)创建 Thread 实例 , 并没有真正的在操作系统内核中创建出线程 . 这里的任务是通过 Thread 的 run 或者 Runnable 或者 lambda 来体现具体的任务内容
run 方法只是描述任务 , 这一点要与 start 方法区分开
(发令枪响)调用 start 方法 , 才是真正在系统里创建出线程 , 才真正开始执行任务
2.4 中断线程(线程执行结束)
线程的执行结束 , 其实就是让线程的入口方法执行完成 , 线程也就执行结束了
普通线程的入口方法就是 run 方法
主线程的入口方法就是 main 方法
那么我们想要中断线程 , 其实就只需要把线程的入口方法执行结束即可
我们有两种方法来中断线程
2.4.1 手动创建的标志位
我们可以自己设置个标志位来区分线程是否要结束
public class Demo11 {
// 用一个布尔变量表示线程是否要结束
// 注意使用成员变量
private static boolean flag = false;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while(!flag) {
System.out.println("线程运行中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("新线程执行结束...");
});
t.start();
try {
Thread.sleep(5000);//休息5s
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("这里可以控制新线程退出");
flag = true;
}
}
这种方案是可行的 , 其实 Thread 自己就提供了这种方法 , 内置了标志位 , 就不需要我们自己创建了
2.4.2 Thread自带的标志位
获取到当前线程的实例:Thread.currentThread()
// currentThread是静态方法,'.'的方式就可以调用他的方法
获取内置的标志位的值:Thread.currentThread().isInterrupted()
// 为true表示要被结束
修改标志位(控制进程退出):t.interrupt();
public class Demo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(!Thread.currentThread().isInterrupted()) {
System.out.println("线程运行中~");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(5000);
System.out.println("控制新线程退出");
t.interrupt();
}
}
我们运行之后发现 , 5s之后程序报错了 , 报错之后还在继续运行
异常是出现了 , 但是线程好像还在继续进行
这里我们就需要理解 interrupt 方法的行为 :
- 如果 t 线程没有处在阻塞状态(处在运行状态) , 此时 interrupt 就会修改内置的标志位
- 如果 t 线程处在阻塞状态 , 此时 interrupt 就会让线程内部产生异常的方法 , 例如 : interrupt 让线程里的 sleep 方法 , 抛出一个 InterruptedException 的异常.
异常被 catch 捕获了 , 但是捕获之后 , 啥也没干 , 只是打印了个调用栈
如果我们把打印栈注释掉 , 那么这次什么都不会打印
这就相当于把异常信息捕获之后 , 啥也没干 , 略过去了
正是因为这样的捕获行为 , 程序员就需要自己控制线程的退出行为了
1. 可以立即退出
public class Demo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(!Thread.currentThread().isInterrupted()) {
System.out.println("线程运行中~");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
// 1. 立即退出
break;
}
}
});
t.start();
Thread.sleep(5000);
System.out.println("控制新线程退出");
t.interrupt();
}
}
2. 也可以稍后退出
public class Demo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程运行中~");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
// 2. 稍后退出
System.out.println("新线程即将退出");
try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
break;
}
}
System.out.println("新线程已经退出");
});
t.start();
Thread.sleep(5000);
System.out.println("控制新线程退出");
t.interrupt();
}
}
3. 就不退出 : 啥都不干 , 就是忽略了异常
主线程发出 “退出” 命令的时候 , 新线程自己决定如何处理退出的行为
比如 : 室友叫你去食堂
- 立即跟室友去食堂 [立即退出]
- 打完这局再去 [稍后退出]
- 装没听见 [不去退出]
另外我们在定义的部分是这样说的 :
如果 t 线程处在阻塞状态 , 此时 interrupt 就会让线程内部产生阻塞的方法
他的意思就是 interrupt 不会自己抛出异常 , 他会在中间捣乱 , 让线程内部的 sleep 抛出异常
除了上述方法 , 判定标志位还有另一种方法 : Thread.interrupted()
这种方法的标志位会自动清除
比如 : 刚开始的时候 , 我们去控制他中断 , 这里的标志位先设为 true
等到读取的时候会读到这个 true , 但是读取完之后这个标志位就自动恢复成 false 了
就类似于开关 : 开关一按 , 自己就又弹起来了
而我们上面讲的 Thread.currentThread().isInterrupted()
就是开关按下去 , 就不弹起来了
我再帮大家梳理一下思路
2.5 线程等待 join
2.5.1 join 是怎么回事?
我们知道 , 线程之间的执行顺序是完全随机的 , 是由系统决定的 .
但是我们对于这种随机性的东西很头疼 , 所以让顺序能够确定下来 , join 关键字就应运而生.
join 就是一种确定线程执行顺序的辅助手段
咱们虽然不能决定多个线程开始的顺序 , 但是这回有了 join , 我们就可以决定结束的顺序了.
还是看一下我们之前的代码
public class Demo8 {
private static final long num = 20_0000_0000;
public static void concurrency() {
long begin_time = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
long a = 0;
for (long i = 0; i < num; i++) {
a++;
}
});
Thread t2 = new Thread(() -> {
long b = 0;
for (long i = 0; i < num; i++) {
b++;
}
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end_time = System.currentTimeMillis();
System.out.println("多线程消耗的时间:" + (end_time - begin_time) + "ms");
}
public static void main(String[] args) {
concurrency();
}
}
在这里面 , main t1 t2 三个线程是随机调度执行的
那么此处的需求 , 就是希望让 t1 和 t2 都执行完了之后 , main 再开始计时 , 来统计执行时间
也就是让 t1 和 t2 先执行完 , main 后执行完
虽然我们无法干预调度器的行为 , 但是我们可以控制 main 线程主动的去进行等待 , 所以我们在 main 线程中分别调用了 t1.join() [main 阻塞 , 等待 t1 执行完]、t2.join() [main 阻塞 , 等待 t2 执行完]
当 t1 t2 都执行完毕 , main 解除阻塞 , 然后程序继续向下执行 , 才能执行完
可以类比领导与职工 , 我们的 main 线程就是 领导 , 我们的 t1 线程和 t2 线程就是员工
早上上班 , 领导给两位职工派活
接下来 , 领导就一直摸鱼 , 等待 t1 和 t2 汇报成果
直到 t1 和 t2 完成工作 , main 线程拿过来验收 , 整个流程才算完事
谁先调用 join 谁后调用 join 都是无所谓的
我们想让谁阻塞 , 谁就调用 join 即可
比如我们想让 t1 以及 t2 阻塞
我们就在 main 线程中调用 t1.join() t2.join() 即可
但是 main 线程比较特殊 , 没有这样的写法 main.join()
, 剩下其他自己创建的线程 , 都可以 join
另外 , join 自身也是有可能发生阻塞的 , 所以我们也要去处理一下异常
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
join 的行为 :
- 如果被等待的线程还没执行完 , 就阻塞等待
- 如果等待的线程已经执行完了 , 直接就返回
2.5.2 栗子1 : 让 main 等待 t2 , t2 去等待 t1
让 main 调用 t2.join() , 让 t2 调用 t1.join()
public class Demo13 {
//先创建两个线程,但是不指向任何元素,方便后面访问
private static Thread t1 = null;
private static Thread t2 = null;
public static void main(String[] args) throws InterruptedException {
System.out.println("main开始");
t1 = new Thread(() -> {
System.out.println("t1开始");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1结束");
});
t1.start();
t2 = new Thread(() -> {
System.out.println("t2开始");
try {
t1.join();//让t2等待t1执行完再去执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2结束");
});
t2.start();
t2.join();//main等待t2
System.out.println("main结束");
}
}
2.5.3 栗子2 : 控制 main 先运行 t1 , t1 执行完再运行 t2
实际上就是谁后调用 join , 谁就后结束
所以先调用 t1.join(), 后调用 t2.join() 就代表让 t1 先执行完 , t2 后执行完 , 就达到我们想要的结果了
public class Demo10 {
public static void main(String[] args) throws InterruptedException {
System.out.println("main 开始");
Thread t1 = new Thread(() -> {
System.out.println("t1 开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 结束");
});
t1.start();
t1.join();
Thread t2 = new Thread(() -> {
System.out.println("t2 开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2 结束");
});
t2.start();
t2.join();
System.out.println("main 结束");
}
}
2.5.4 带参数版本的 join
函数 | 作用 |
---|---|
public void join() | 死等线程结束 (不见不散) |
public void join(long mills) | 设定了最大等待时间 |
public void join(long mills,int nanos) | 传了更加精确的时间 |
未来实际开发程序的时候 , 像这种的等待操作 , 一般不会采用死等的方式 , 这种方式有风险 ! 万一代码出了 bug 没控制好 , 死等就容易让服务器卡死 , 无法继续工作 .
其中 , public void join(long mills,int nanos)
这个方法 , 第一个参数传的是毫秒 , 第二个参数传的是纳秒 , 比如第一个参数是 100 , 第二个参数是 1000 , 相当于等待了 100.001 ms
2.6 获取到线程引用 : Thread.currentThread()
为了对线程进行操作(线程等待、线程中断、获取各种线程的属性…) , 就需要获取到线程引用
这个方法我们刚才已经用到了
Thread.currentThread();
在线程操作之前 , 我们需要先获取到线程的引用 , 才能对线程进行具体操作
如果是继承 Thread , 然后重写 run 方法的话 , 可以直接在 run 中使用 this 即可获取到线程的实例
但是如果是 Runnable 或者 lambda , this 的方式就不可以了 , 这两种情况下 , this 就不是指向 Thread 实例
所以更通用的办法就是使用 Thread.currentThread()
, 这是一个静态方法 , 哪个线程调用这个方法 , 得到的结果就是哪个线程的实例 , 以后获取到线程实例 , 我们就无脑使用这个方法即可
2.7 线程的休眠 : sleep
sleep 能让线程休眠一会
我们先讲解一下 相关的原理
那么关于 PCB , 由于我们之前只介绍了进程 , 但是现在新学了线程 , PCB 这个概念还是需要重新提一下的
一组进程包含多组线程 , 每个线程都有自己的 PCB , 那么每一组进程其实就是对应一组 PCB 了
但是同一个进程里面的若干 PCB 还是有关联的
- PCB里面有一个 “线程组ID” , 是一样的
- PCB 里面的内存指针和文件描述符表 , 都是一样的
三 . 线程的状态
我们之前介绍过就绪状态与阻塞状态 , 在 Java / JVM 中 , 对于线程的状态 , 做了一个更明确的区分
NEW: 安排了工作, 还未开始行动
RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
BLOCKED: 这几个都表示排队等着其他事情
WAITING: 这几个都表示排队等着其他事情
TIMED_WAITING: 这几个都表示排队等着其他事情
TERMINATED: 工作完成了
3.1 通过代码来看线程的状态
public class Demo13 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("Hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// NEW 状态:
// 在 start 之前获取
// 获取到的是线程还未创建的状态
System.out.println(t.getState());
t.start();
// RUNNABLE 状态
// 线程就绪状态
// 线程正在工作中
System.out.println(t.getState());
// TIMED_WAITING 状态
// 线程调用了 sleep 方法
Thread.sleep(500);
System.out.println(t.getState());
t.join();// 等待t线程结束
// TERMINATED 状态
// 在 join 之后获取
// 获取到的是线程已经结束后的状态
System.out.println(t.getState());
}
}
3.2 线程状态和状态转移的意义
这个图看起来吓人 , 实际上我们可以简化成下面这样
我们再给大家简单介绍一个方法 : yield
yield 的作用是让调用者暂时放弃CPU , 回到阻塞队列里面去排队
我们可以粗暴的理解成 sleep(0)
用的不多 , 有印象即可
关于 yield , 举个栗子 :
我跟我的妈妈去超市 , 到结账的位置了 , 她突然想起来 “艾玛 , 忘打酱油了” , 让我继续排队 , 但是我都要交钱了她还没回来 , 这就很尴尬 . 那么 yield 就是我让后面结账的那个人先结账 , 我的妈妈要是还没回来 , 那就再让后面的人继续往前去结账 , 这就相当于我们一直没出就绪队列 . sleep(0)的意思就是我觉得不好意思了 , 刚退出队伍 , 我妈妈就赶回来了 , 我又赶紧回去队伍 , 这就是出了阻塞队列然后又立马回来了
yield 就是短暂的放弃 CPU , 排到就绪队列中的后面位置 (还是在就绪队列中 , 没进阻塞队列)
slepp(0) 就是马上进入阻塞队列 , 又马上回就绪队列 , 效果和 sleep 类似