目录
[2.1. 调度相关](#2.1. 调度相关)
[2.2. 状态](#2.2. 状态)
[2.3. 优先级](#2.3. 优先级)
[2.4. 记账信息](#2.4. 记账信息)
[2.5. 上下文](#2.5. 上下文)
[4.1. 核心区别](#4.1. 核心区别)
[4.2. 关键细节](#4.2. 关键细节)
[4. 3 常见误区](#4. 3 常见误区)
[join() 方法的作用](#join() 方法的作用)
[为什么需要 join()?](#为什么需要 join()?)
这一章博客全是干货,彦祖们都需要掌握!!!!!!!!!!!
想象一下你是一个孤岛上的国王,你日理万机,王国里只有你一个人,一次只能做一件事情·。批阅奏章,做饭,睡觉。当你做饭的时候,奏章堆积如山,当你睡觉的时候,厨房着火了........你多希望自己有影分身术啊,就是我们要探讨的(多线程)!
一、线程和进程的区别
进程是一个正在运行的程序,创建和销毁需要浪费很多资源,创建销毁的开销太大,我们可以说进程里面有很多线程,线程之间可以协同工作,干的活比较多,创建和销毁的开销要小得多!所以引入了多线程。
进程是资源分配的基本单位,而线程是CPU调度的基本单位。你可以把进程想象成一个正在运行的完整程序,线程就是这个程序执行的任务。当我们引入了线程,我们的效率就会指数级上升,有很多人帮我们干活。
资源拥有:进程有自己独立的内存空间和系统资源,而线程资源是共享的。
开销成本:创建或销毁进程的开销很大,而创建线程就小得多。
cpu执行的是线程而不是进程。
二、线程的五大特性
线程的这几个特性是操作系统进行线程调度和管理的关键依据,主要包括以下几个方面:
2.1. 调度相关
-
概念:线程的调度是操作系统决定哪个线程获得CPU时间片并执行的过程。
-
特性:线程是CPU调度的基本单位,操作系统会根据一定的调度算法(如优先级调度、时间片轮转等)来决定线程的执行顺序和时间。
-
影响:调度的好坏直接影响系统的性能和响应时间。例如,在实时系统中,调度算法需要确保高优先级的线程能够及时得到执行。
当我们创建了多个线程,他们的执行顺序是不确定,这是因为,线程的调度是随机的!
2.2. 状态
-
概念:线程在其生命周期中会经历不同的状态,这些状态反映了线程当前的执行情况。
-
常见状态:
-
新建状态(New):线程被创建但尚未启动。
-
就绪状态(Runnable):线程已经准备好执行,等待CPU分配时间片。
-
运行状态(Running):线程正在CPU上执行。
-
阻塞状态(Blocked):线程由于等待某个事件(如I/O操作完成、等待锁等)而暂时无法执行。
-
终止状态(Terminated):线程执行完毕或因异常退出。
-
状态转换:线程的状态会随着其执行过程和外部事件的发生而转换,例如从就绪状态转换为运行状态,或者从运行状态转换为阻塞状态。
2.3. 优先级
-
概念:每个线程都有一个优先级,操作系统在调度时会考虑线程的优先级。
-
特性:优先级高的线程通常会获得更多的CPU时间片和更高的执行机会。
-
注意事项:不同的操作系统对线程优先级的支持和实现可能不同,而且过度依赖线程优先级可能会导致线程饥饿(低优先级线程长时间无法获得执行机会)等问题。
2.4. 记账信息
-
概念:记账信息是操作系统为了跟踪线程的资源使用情况而记录的数据。
-
内容:包括线程的执行时间、CPU使用时间、内存使用情况等。
-
作用:这些信息可以帮助操作系统进行资源管理和调度决策,例如根据线程的CPU使用情况调整其优先级。
2.5. 上下文
-
概念:线程的上下文是指线程执行时的环境,包括程序计数器、寄存器的值、栈指针等。
-
特性:当线程被切换出CPU时,操作系统会保存其上下文;当线程再次被调度执行时,操作系统会恢复其上下文,使得线程能够从上次暂停的地方继续执行。
-
重要性:上下文切换是线程调度的重要过程,其开销会影响系统的性能。因此,减少不必要的上下文切换是提高系统性能的一个重要手段。
总结
这些特性共同构成了线程的基本属性,操作系统通过对这些特性的管理和调度,实现了多线程的并发执行,提高了系统的资源利用率和性能。在多线程编程中,了解这些特性对于编写高效、稳定的程序非常重要。
三、线程的诞生(如何创建一个线程)
在Java中创建一个线程非常重要,我们之前写的代码,main方法就是一个线程。
方法一:继承Tread类:
Tread我们就可以理解成线程的标志,和他相关的就脱离不了多线程.
由于Thread这个类继承了runnable接口所以我们要重写里面run方法.run方法我们就可以理解成线程里要执行的内容.
package thread;
class MyThread extends Thread{
@Override
public void run() {
while (true){
System.out.println("Hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}}
public class Demo1 {
public static void main(String[] args)throws InterruptedException {
Thread thread=new MyThread();
thread.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
方法二:实现Runnable接口-------能者多劳
相比于方法一这个方法优势在于继承在于,继承类只有一个,但是继承接口可以有多个。这种方法相比于其更加灵活。
package thread;
class MyRunnable implements Runnable{
@Override
public 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)throws InterruptedException {
Runnable runnable=new MyRunnable();
Thread thread=new Thread(runnable);
thread.start();;
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
方法三:匿名内部类
相比于上边俩个方法不由构建一个新的类,他是匿名的,代码量减少,缺点在于只能使用一次。
package thread;
public class Demo3 {
public static void main(String[] args)throws InterruptedException {
Thread thread=new Thread(){
public void run(){
while (true){
System.out.println("heloo world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
thread.start();;
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
方法四:Runnable的匿名内部类
疑点1
需要重写run方法,这里有个问题,Runnable不是一个接口么可以创建一个对象,这里注意了,这是一个匿名内部类的写法,相当于是在这个接口的匿名内部类对象。这个类也相当于一个任务,多线程是需要靠Thread来实现的,所以我们要把这个任务交给Thread来处理!
package thread;
public class Demo4 {
public static void main(String[] args)throws InterruptedException {
Runnable runnable=new Runnable() {
@Override
public void run() {
while (true){
System.out.println("hello world");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
Thread thread=new Thread(runnable);
thread.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
方法五:Lambda方法
这个方法是我们最常用的的方法:简单、高效、函数式
package thread;
public class Demo5 {
public static void main(String[] args)throws InterruptedException{
Thread thread=new Thread(()->{
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
疑点2
使用Thread.sleep()这个为什么没有import,为什么不需要导入包呢?
这是因为Thread位于Java.lang包中,这是java中的默认包,像Thread、String、System这些都不需要手写import.
重点标记带:
1.
thread.start().就是开启一个新的线程,多一个执行流,main是一个线程,这个也是一个线程。

在Thread的run方法中没有抛出异常,所以我们用try catch去处理,但是在main方法中我们可以向上抛异常交给jvm处理
2.为什么不直接调用Thread.run() 还要那么麻烦?
我们的初心是建立一个新的线程一起工作,提高效率,如果直接调用,和以前调用方法一样,没有提高效率,所以Thread.start()创建新的线程是必不可少的。
四、前台线程和后台线程
4.1. 核心区别
-
JVM 退出机制
-
用户线程:只要存在至少一个用户线程运行,JVM 就不会退出。例如 main() 主线程是用户线程,若创建新用户线程执行耗时任务,JVM 会等待所有用户线程结束后才终止。
-
守护线程:当所有用户线程终止后,JVM 会立即退出,无论守护线程是否执行完毕。例如垃圾回收(GC)线程是典型的守护线程,负责后台清理内存。
-
创建方式
-
用户线程:默认创建的线程均为用户线程(如 new Thread() )。
-
守护线程:需显式调用 thread.setDaemon(true) (必须在 start() 前设置)。
-
应用场景
-
用户线程:执行核心业务逻辑,如 Web 请求处理、数据库操作、文件 IO 等。
-
守护线程:执行辅助性任务,如日志记录、GC、监控服务、心跳检测等。
4.2. 关键细节
-
线程优先级:用户线程与守护线程在优先级上无本质区别,由操作系统调度。
-
资源释放风险:守护线程被强制终止时可能导致资源泄漏(如文件句柄未关闭),需谨慎处理。
-
子线程继承性:守护线程创建的子线程默认也是守护线程。
-
线程池默认类型: Executors 创建的线程池默认是用户线程;若需后台执行,需自定义线程工厂并设置 setDaemon(true) 。
package thread;
public class Demo7 {
public static void main(String[] args)throws InterruptedException {
Thread thread=new Thread(()->{
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//这样设置后台线程必须放在start之前
//主线程结束后台线程也将会结束
thread.setDaemon(true);
thread.start();
for (int i = 0; i <3 ; i++) {
System.out.println("hello main");
Thread.sleep(1000);
}
System.out.println("main 结束");
}
}
4. 3 常见误区
-
误区 1:守护线程会执行完任务。
-
正确:若 JVM 退出,守护线程可能被强制终止,未完成的任务会丢失。
-
误区 2:守护线程不能做 IO 操作。
-
正确:守护线程可以执行 IO,但需确保操作幂等或可恢复。
-
误区 3: setDaemon(true) 可随时调用。
-
正确:必须在 start() 前设置,否则抛出 IllegalThreadStateException 。
4.4总结
-
用户线程:业务逻辑主力,JVM 需等待其完成。
-
守护线程:后台辅助任务,随 JVM 生命周期终止。
合理选择线程类型可优化资源利用率,例如关键任务用用户线程,非关键任务用守护线程。
五、线程使用外面变量的那些事
Lambda一般使用外面的变量,当变量在mian方法里面的时候,这里就会有一个问题,lambda是一个回调函数(理解执行的时候会慢一点),这里就一种可能,当我们开始执行线程的时候·,main方法执行完了,这个局部变量就被销毁了,执行线程的时候,就捕获不了这个变量了。
public static void main(String[] args) throws InterruptedException{
boolean isFinished =false;
//lambda是回调函数,执行是很久以后,很有可能线程建好了,main方法也执行完了,才开始这个线程
Thread t=new Thread(()->{
while (!isFinished){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("thread线程结束");
});
所以我们最好把这个变量放在main方法外面这个问题就迎刃而解了
public class Demo10 {
public static boolean isFinished=false;
public static void main(String[] args) throws InterruptedException{
//boolean isFinished =false;
//lambda是回调函数,执行是很久以后,很有可能线程建好了,main方法也执行完了,才开始这个线程
Thread t=new Thread(()->{
while (!isFinished){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("thread线程结束");
});
t.start();
Thread.sleep(3000);
isFinished=true;
}
}
对于终止线程Thread提供了自己变量所以我们就不用创建了。

对于Thread.currentThread()这个方法,和this用法差不多在哪个线程调用就指的是哪个线程。

主动去终止,但是这个方法还有一个作用是唤醒sleep,当线程还在休眠的时候直接把线程唤醒了,这样会使代码到catch里面处理,如果我们只是仅仅继续抛出异常解决不了问题,所以我们可以加上break,终止循环,如果什么都不写就是不终止。
public class Demo11 {
public static void main(String[] args) throws InterruptedException{
Thread t=new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//throw new RuntimeException(e);
//这个线程翻桌了
//加上break就是终止
//啥都不就是不终止
//有其他逻辑,然后再break,就是稍后终止
// break;
}
}
System.out.println("thread线程终止");
});
t.start();
Thread.sleep(3000);
System.out.println("main线程尝试终止t线程");
t.interrupt();
//main方法调用的,此时就是main
//System.out.println("main:"+Thread.currentThread().getName());
}
}
那么如果catch里面什么不加,这个循环会退出吗?
答案是否定的,其实是sleep在捣鬼,正常情况下isIterrupted的值是fasle,如果调用interrupt可以把这个值改成true,但是sleep被唤醒他又把这个值改回去了,所以这个循环依旧会继续,不会中断。
六、使用join()等待线程结束
多线程编程:使用 join() 等待线程结束
在多线程编程中,线程的调度是随机的,我们不喜欢随机调度,我们想要掌控感,这给程序执行顺序带来了不确定性。为了解决这个问题,Java 提供了 join() 方法,让我们能够控制线程的结束顺序。
join() 方法的作用
join() 方法的主要作用是让一个线程等待另一个线程执行完毕。具体来说:
· 当在某个线程(如主线程)中调用 t.join() 时,当前线程会进入等待状态
· 直到线程 t 执行完成后,当前线程才会继续执行
· 这样可以确保线程之间的执行顺序符合预期
public class Demo12 {
public static void main(String[] args) throws InterruptedException{
Thread t=new Thread(()->{
for (int i = 0; i <3 ; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("thread 线程结束");
});
t.start();
t.join(3000,599);
System.out.println("main线程结束");
}
}
正常情况下main线程如果没有任务的话瞬间会结束进程,但是有等待方法的话会等待t线程结束后结束。
还有一个问题就是如果t线程的任务需要很就或者一直进行。我们不规定等多久他会一直死等下去,我们不希望看到这种现象所以通常情况下我们要规定时间!

为什么需要 join()?
虽然可以通过 sleep() 方法设置休眠时间来控制线程执行顺序,但这种方法存在明显缺陷:
-
时间难以精确控制:无法准确预测线程需要执行多长时间
-
资源浪费:可能设置过长等待时间,降低程序效率
-
不可靠:如果线程执行时间超过预设值,仍会出现顺序问题
核心要点
-
确定性执行顺序:join() 提供了确定性的线程结束顺序控制
-
异常处理:join() 可能抛出 InterruptedException,需要进行异常处理
-
替代方案:相比通过睡眠时间控制线程顺序,join() 更加科学和可靠
总结
join() 方法是多线程编程中重要的同步工具,它解决了线程执行顺序的不确定性问题,让开发者能够编写出更加可控和可靠的多线程程序。在实际开发中,当需要确保某个线程完成后才能继续执行后续操作时,join() 是最佳选择。