目录💨
- 1.线程(Thread)
- 1.1 概念
- 1.2 进程和线程的区别
- 1.3 Java 的线程 和 操作系统线程 的关系
- 2. 创建线程的方法
- 2.1 继承 Thread 类
- 2.2 实现 Runnable 接口
- 2.3 其他变形写法
- 3. Thread 类及常见方法
- 3.1 Thread 的常见构造方法
- 3.2 Thread 的几个常见属性
- 4. Thread 中的一些重要方法
- 4.1 start方法
- 4.2 中断一个线程
- 4.3 等待一个线程-join()
- 4.4 获取当前线程引用
- 4.5 休眠当前线程
1.线程(Thread)
1.1 概念
线程是什么:一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码.
为啥要有线程:“并发编程” 成为 “刚需”.
- 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU
资源. - 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编
程
其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量3
- 创建线程比创建进程更快.
- 销毁线程比销毁进程更快.
- 调度线程比调度进程更快.
1.2 进程和线程的区别
- 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
1.3 Java 的线程 和 操作系统线程 的关系
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使
用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.
2. 创建线程的方法
2.1 继承 Thread 类
- 继承 Thread 来创建一个线程类.
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
- 创建 MyThread 类的实例
MyThread t = new MyThread();
- 调用 start 方法启动线程
t.start(); // 线程开始运行
2.2 实现 Runnable 接口
- 实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
- 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
Thread t = new Thread(new MyRunnable());
- 调用 start 方法
t.start(); // 线程开始运行
对比上面两种方法:
- 继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
- 实现 Runnable 接口, this 表示的是
MyRunnable
的引用. 线程引用需要使用Thread.currentThread()
2.3 其他变形写法
- 匿名内部类创建 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 子类对象");
});
3. Thread 类及常见方法
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关
联。用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
3.1 Thread 的常见构造方法
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
3.2 Thread 的几个常见属性
- ID 是线程的唯一标识,不同线程不会重复
- 名称是各种调试工具用到
- 状态表示线程当前所处的一个情况
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
如果线程是后台线程,就不影响进程退出;如果是前台线程,就会影响到进程退出~
创建的线程默认都是前台的线程,即使main方法执行完毕,进程也不能退出;得等前台线程执行完,整个进程才能退出
如果是后台线程,main线程执行完毕,整个进程就直接退出了,此时这个后台线程就会被强制终止了; - 是否存活,即简单的理解,为 run 方法是否运行结束了
如果调用start之后,run执行完之前,isAlive就返回TRUE
如果调用start之前,run执行完之后,isAlive就返回FALSE - 线程的中断问题 :
4. Thread 中的一些重要方法
4.1 start方法
之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程
就开始运行了。
- 覆写 run 方法是提供给线程要做的事情的指令清单
- 线程对象可以认为是把 李四、王五叫过来了
- 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了
- 调用 start 方法, 才真的在操作系统的底层创建出一个线程.
run单纯的只是一个普通的方法,描述了任务的内容
start则是一个特殊的方法,内部会在系统中创建线程
4.2 中断一个线程
目前常见的有以下两种方式:
- 通过共享的标记来进行沟通
- 调用 interrupt() 方法来通知
示例-1: 使用自定义的变量来作为标志位.
- 需要给标志位上加 volatile 关键字
public class ThreadDemo {
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(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
target.isQuit = true; //此时得知对方是骗子 让线程终端
}
}
示例-2: 使用 Thread.interrupted()
或者 Thread.currentThread().isInterrupted()
代替自定义标志位.
- Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记
- 使用 thread 对象的 interrupted() 方法通知线程结束.
public class ThreadDemo {
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(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": 老板来电话了,得赶紧通知李四对方是个骗子!");
thread.interrupt();
}
}
thread 收到通知的方式有两种:
- 如果线程因为调用
wait/join/sleep
等方法而阻塞挂起,则以InterruptedException
异常的形式通知,清除中断标志
- 当出现
InterruptedException
的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择
忽略这个异常, 也可以跳出循环(break)结束线程.
- 否则,只是内部的一个中断标志被设置,thread 可以通过
-
Thread.interrupted()
判断当前线程的中断标志被设置,清除中断标志
-
Thread.currentThread().isInterrupted()
判断指定线程的中断标志被设置,不清除中断标志(常用)
这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。
标志位是否清除, 就类似于一个开关.
-
Thread.isInterrupted()
相当于按下开关, 开关自动弹起来了. 这个称为 “清除标志位” - 使用
Thread.isInterrupted()
, 线程中断会清除标志位.
// 只有一开始是 true(标志位),后边都是 false(恢复默认),因为标志位被清除
-
Thread.currentThread().isInterrupted()
相当于按下开关之后, 开关弹不起来, 这个称为"不清除标志位". - 使用
Thread.currentThread().isInterrupted()
, 线程中断标记位不会清除.
4.3 等待一个线程-join()
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。
多个线程之间,调度顺序的不确定的,线程等待,就是其中一种,控制线程执行顺序的手段;主要是控制线程结束的先后顺序。
调用join时候,哪个线程调用join,哪个线程就会阻塞等待,等到对应的线程执行完毕为止(对应线程的run执行完)
列如:在main方法(main线程)里面t.join
,是针对t这个线程对象调用的,此时就是让main线程等待t线程执行完。
调用join之后,main线程就会阻塞状态(暂时无法在CPU上执行)
需要注意的一点是:代码执行到join这一行,就暂时停下了,不继续往下执行了!等到t线程执行完毕(t的run方法跑完了),main线程才继续执行(恢复就绪状态);
通过线程等待,就是在控制让t先结束,main再结束:一定程度上的干预了这两个线程的执行顺序;
- join操作默认情况下就是死等,这不合理
- 我们应该给join传参,这个参数就是等待的时间:
t.join(10000)
(ms)
进入join也会产生阻塞,但是这个阻塞不会一直下去;
如果10s之内,t线程结束了,那么join直接返回
如果10s之后,t线程还没有结束,此时join也会直接返回,不等了~
4.4 获取当前线程引用
main方法里面的就是main线程:
4.5 休眠当前线程
有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
这里的意思是让main线程休眠3000ms(因为线程的调度是不可控,这里并不是精确的等了3000ms,而是大于3000ms):
- 如果某个线程调用了sleep方法,这个线程对应的pcb就会进入到阻塞队列(操作系统调度线程的时候,就只是从就绪队列中挑选合适的pcb到CPU上运行,阻塞队列里面的pcb就只能干等着…),当睡眠时间到了,系统就会把刚才这个pcb从阻塞队列挪回到就绪队列(针对Linux来说,Windows不是开源的,我们无从得知系统内部是怎么进行线程调度的,但是推测和Linux差别不会太大!)
- over ~ 🎈