目录💨
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 {@Overridepublic void run() {System.out.println("这里是线程运行的代码");}}
创建 MyThread 类的实例
MyThread t = new MyThread();
调用 start 方法启动线程
t.start(); // 线程开始运行
2.2 实现 Runnable 接口
实现 Runnable 接口class MyRunnable implements Runnable {@Overridepublic 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() {@Overridepublic void run() {System.out.println("使用匿名类创建 Thread 子类对象");}};
匿名内部类创建 Runnable 子类对象
// 使用匿名类创建 Runnable 子类对象Thread t2 = new Thread(new Runnable() {@Overridepublic 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;@Overridepublic 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 {@Overridepublic 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()+ ": 有内鬼,终止交易!");// 注意此处的 breakbreak;}}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 ~ 🎈