本篇博客给大家带来的是线程的知识点, 由于内容较多分几天来写.
🐎文章专栏: JavaEE初阶
🚀若有问题 评论区见
⭐欢迎大家点赞 评论 收藏 分享 ❤❤❤
如果你不知道分享给谁,那就分享给薯条.
你们的支持是我不断创作的动力 .
1. 认识线程
1.1 概念
)1 线程是什么
⼀个线程就是⼀个 “执行流”. 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 “同时” 执行着多份代码.
⼀家公司要去银⾏办理业务,既要进⾏财务转账,⼜要进⾏福利发放,还得进⾏缴社保。
如果只有张三⼀个会计就会忙不过来,耗费的时间特别⻓。为了让业务更快的办理好,张三⼜找来两位同事李四、王五⼀起来帮助他,三个⼈分别负责⼀个事情,分别申请⼀个号码进⾏排队,⾃此就有了三个执⾏流共同完成任务,但本质上他们都是为了办理⼀家公司的业务。
此时,我们就把这种情况称为多线程,将⼀个⼤任务分解成不同⼩任务,交给不同执⾏流就分别排队执⾏。其中李四、王五都是张三叫来的,所以张三⼀般被称为主线程(Main Thread)。
)2 为什么要有线程
⾸先, “并发编程” 成为 “刚需(必备)”.
• 单核 CPU 的发展遇到了瓶颈. 要想提⾼算⼒, 就需要多核 CPU. ⽽并发编程能更充分利⽤多核 CPU资源.
• 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做⼀些其他的⼯作, 也需要⽤到并发编程.
其次, 虽然多进程也能实现并发编程, 但是线程比进程更轻量.
• 创建线程比创建进程更快.
• 销毁线程比销毁进程更快.
• 调度线程比调度进程更快.
总结就是线程创建更快,销毁更快,调度更快.
最后, 线程虽然比进程轻量, 但是⼈们还不满足, 于是又有了 “线程池”(ThreadPool) 和 (Coroutine)“协程”.(这两个后续的文章再说).
)3 进程和线程的区别(经典面试题)
• 1. 进程是包含线程的. 每个进程⾄少有⼀个线程存在,即主线程.
• 2. 进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间.
比如上面的多进程例子中,每个客户来银行办理各自的业务,但他们之间的票据肯定是不想让别人知道的,否则钱不就被其他⼈取走了么。而上面我们的公司业务中,张三、李四、王五虽然是不同的执行流,但因为办理的都是⼀家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最大区别。
• 3. 进程是系统分配资源的最小单位,线程是系统调度的最小单位.
• 4. ⼀个进程挂了⼀般不会影响到其他进程. 但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带走(整个进程崩溃).
• 5. 都是用来实现并发编程场景的. 但是线程比进程更轻量, 更高效.
)4 Java的线程 和 操作系统线程 的关系
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了⼀些 API 供用户
使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进⼀步的抽象和封装.
1.2 创建线程的方式(面试题)
⽅法1 继承 Thread 类
一. 继承 Thread重写 run()方法 来创建⼀个线程类.
class MyThread extends Thread {@Overridepublic void run() {//这个方法就是线程的入口方法while(true) {System.out.println("hello thread");try {//sleep() 让线程休眠 x ms(毫秒).Thread.sleep(1000);//此处抛异常只能 try catch 不能 throws 因为父类Thread没有throws, 子类也不行} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
二.创建 MyThread 类的实例
Thread t = new MyThread(); //向上转型
三. 调用 start 方法启动线程
t.start();
//创建线程的第一种写法
//创建一个类, 继承自 Threadclass MyThread extends Thread {@Overridepublic void run() {//这个方法就是线程的入口方法while(true) {System.out.println("hello thread");try {Thread.sleep(1000);//此处抛异常只能 try catch 不能 throws 因为父类Thread没有throws, 子类也不行} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}//创建线程
public class Demo1 {public static void main(String[] args) throws InterruptedException {Thread t = new MyThread(); //Java生态更鼓励向上转型// 封装本质上是让调用者不用了解类实现的细节降低了学习和使用的成本.
//多态则是在封装的基础上更进一步,多态则是都不需要你知道当前是啥类。//start 和 run 都是线程的入口(线程要做什么)//run只是描述了线程的入口 (线程要做什么任务)//start 则是真正调用了系统API, 在系统中创建出线程, 让线程再调用 run.t.start();//t.run();while(true) {System.out.println("hello main");Thread.sleep(1000); //自定义类 两种抛异常方式都可以.}}
}
方法2. 实现 Runnable 接⼝
一. 实现 Runnable 接口,重写run()方法.
class MyRunnable implements Runnable {@Overridepublic void run() {while(true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
二. 创建 Thread 类实例, 调⽤ Thread 的构造⽅法时将 Runnable 对象作为 target 参数.
Runnable runnable = new MyRunnable();
三. 调用 start 方法
//创建线程的第二种写法//实现Runnable接口,重写run方法.//使用 Runnable 的写法,和 直接继承 Thread 之间的区别, 主要就是三个字,解耦合
class MyRunnable implements Runnable {@Overridepublic void run() {while(true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}public class Demo2 {public static void main(String[] args) {Runnable runnable = new MyRunnable();Thread t = new Thread(runnable);t.start();while(true) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
方法3. 匿名内部类创建 Thread ⼦类对象
public class Demo3 {public static void main(String[] args) {Thread t = new Thread() {@Overridepublic void run() {while(true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};t.start();while(true) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
方法4. 匿名内部类创建 Runnable ⼦类对象
public class Demo4 {public static void main(String[] args) {Thread t = new Thread(new Runnable(){@Overridepublic void run() {while(true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}});t.start();while(true) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
方法5. lambda 表达式创建 Runnable ⼦类对象(常用)
public class Demo5 {public static void main(String[] args) {Thread t = new Thread(() -> {while(true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();while(true) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
2. Thread 类及常见方法
Thread 类是 JVM ⽤来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的 Thread 对象与之关联。
每个执行流,需要有⼀个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述⼀个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
2.1 Thread 的常见构造方法
1 Thread t1 = new Thread();
2 Thread t2 = new Thread(new MyRunnable());
3 Thread t3 = new Thread("线程1");
4 Thread t4 = new Thread(new MyRunnable(), "线程2")
2.2 Thread 的几个常见属性
1. ID 是线程的唯⼀标识,不同线程不会重复.(此处ID是Java给线程分配的,不是系统API提供的线程ID,也不是PCB中的ID.)
2. 名称是各种调试工具用到.
3. 状态表示线程当前所处的⼀个情况.
4. 优先级高的线程理论上来说更容易被调度到.
5. 关于后台线程(守护线程),需要记住⼀点:JVM会在⼀个进程的所有非后台线程结束后,才会结束运行。
线程默认情况下是前台线程.
一个Java进程中,如果前台线程没有执行结束,整个进程都不会结束. 相比之下后台线程是否结束不影响整个进程的结束.
//是否后台线程, isDaemon() true变 后台,//线程默认是前台线程.
public class Demo6 {public static void main(String[] args) {Thread t = new Thread(() -> {while(true) {System.out.println("hello Thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "这是新线程");//设置 t 为后台线程t.setDaemon(true);t.start();}
}
6. 是否存活,即简单的理解,为 run 方法是否运行结束了
//isAlive() 方法的用法.
public class Demo7 {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("线程开始");try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("线程结束");});t.start();System.out.println(t.isAlive());try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(t.isAlive() );}
}
两线程是"并发执行"的为什么是先打印 true 而不是: "线程开始"呢?
系统的调度顺序不确定, 但是大概率是先打印true, 因为调用了start方法之后, 新的线程被创建也是有一定的开销的, 创建线程的过程中, 主线程就执行了. 当然也存在先打印 "线程开始"的情况. 比如: 主线程刚好卡了一会.
7. 线程的中断问题
public class ThreadDemo {public static void main(String[] args) {Thread thread = new Thread(() -> {for (int i = 0; i < 10; i++) {try {System.out.println(Thread.currentThread().getName() + ": 我还活着");Thread.sleep(1 * 1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(Thread.currentThread().getName() + ": 我即将死去");});System.out.println(Thread.currentThread().getName()+ ": ID: " + thread.getId());System.out.println(Thread.currentThread().getName()+ ": 名称: " + thread.getName());System.out.println(Thread.currentThread().getName()+ ": 状态: " + thread.getState());System.out.println(Thread.currentThread().getName()+ ": 优先级: " + thread.getPriority());System.out.println(Thread.currentThread().getName()+ ": 后台线程: " + thread.isDaemon());System.out.println(Thread.currentThread().getName()+ ": 活着: " + thread.isAlive());System.out.println(Thread.currentThread().getName()+ ": 被中断: " + thread.isInterrupted());thread.start();while (thread.isAlive()) {}System.out.println(Thread.currentThread().getName()+ ": 状态: " + thread.getState());}
}
2.3 启动⼀个线程 - start()
之前我们已经看到了如何通过覆写 run 方法创建⼀个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。
• 覆写 run 方法是提供给线程要做的事情的指令清单.
• 线程对象可以认为是把 李四、王五叫过来了.
• 而调用 start() 方法,就是喊⼀声:”行动起来!“,线程才真正独立去执行了.
2.3.1 start方法 和 run方法 的区别(面试题)
start方法内部会调用到系统API,来在系统内核中创建出线程.(调⽤ start ⽅法, 才真的在操作系统的底层创建出⼀个线程)
run方法就只是单纯地描述了该线程要执行什么样的内容.
2.4 中断一个线程
李四⼀旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加⼀些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如何通知李四停⽌呢?这就涉及到我们的停止线程的方式了。
方法一. 通过共享的标记来进⾏沟通
//线程的打断//第一个方案 手动创建标志位
public class Demo8 {private static boolean isQuit = false;//此处isQuit不能定义到 main方法中,// 因为lambda表达式有一个语法规则变量捕获,可以自动的捕获到上层作用域中的局部变量.//实际上变量捕获还有一个前提就是 只能捕获final或者捕获一个final的变量(不改变的量)// 那此处也没有final修饰isQuit, 为什么lambda能访问到isQuit?//原来当isQuit是成员变量的时候, lambda访问它的时候就不是用变量捕获的语法了,//而是"内部类访问外部类属性", 此时就没有final的属性限制了.public static void main(String[] args) {Thread t = new Thread(() -> {// 此处的打印可以替换成任意的逻辑来表示线程的实际工作内容while(!isQuit) {System.out.println("线程工作中");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("线程工作完毕!");});t.start();try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}isQuit = true;System.out.println("设置 isQuit 为 true");}
}
方法二. 调⽤ interrupt() ⽅法来通知
使⽤ Thread.interrupted() 或者
Thread.currentThread().isInterrupted() 代替⾃定义标志位.
Thread 内部包含了⼀个 boolean 类型的变量作为线程是否被中断的标记
//线程终止//第二个方案 使用Thread类内部现有的一个标志位
public class Demo9 {public static void main(String[] args) {Thread t = new Thread(() -> {//Thread 类内部, 有一个现成的标志位,可以用来判定当前的循环是否要结束while(!Thread.currentThread().isInterrupted()) {System.out.println("线程工作中");try {Thread.sleep(1000);} catch (InterruptedException e) {//1. 假装没听见, 循环继续正常执行e.printStackTrace();/* throw new RuntimeException(e);*///2. 加上一个 break, 表示让线程立即结束//break;//3. 做一些其他工作, 完成之后再结束//其他工作的代码放到这里break;}}});t.start();try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(" 让 t 线程终止");t.interrupt();}
}
thread 收到通知的方式有两种:
1. 如果线程因为调⽤ wait/join/sleep 等⽅法⽽阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志。 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.
2. 否则,只是内部的⼀个中断标志被设置,thread 可以通过
。 Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志.
这种⽅式通知收到的更及时,即使线程正在 sleep 也可以马上收到。
本篇博客到这里也就结束啦, 感谢你的观看!! ❤❤❤