【JavaEE】多线程 -- 初识线程
目录
- 认识线程
- 线程是什么
- 为啥要有线程
- 进程和线程的区别(重要)
- 第一个多线程程序
- 为什么先打印main再打印thread
- 抛异常的小问题
- 创建多线程的方式
- 继承Thread, 重写run方法
- 实现Runnable接口
- 继承Thread, 使用匿名内部类
- 实现Runnable接口, 使用匿名内部类
- lambda表达式(推荐写法)
- Thread类常见属性和方法
- 常见的构造方法
- Thread类的几个常见属性
- 前台线程和后台线程
- 使用 setDaemon(true) 可以将线程设为后台线程
- start和run的区别
- 中断一个线程
- 通过一个标记位变量来中断
- 使⽤ Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替⾃定义标志位.
- 线程等待
- 获取当前线程引⽤
- 休眠当前线程
认识线程
线程是什么
- 也叫轻量级进程, 创建线程和销毁线程的开销要比创建进程和销毁进程的开销要小很多.
- 每个线程都是一个执行流, 都可以放到CPU上调度执行. 所以准确的来说, 我们CPU调度执行的其实是线程.
- 线程是操作系统调度执行的基本单位
为啥要有线程
- 首先就是并发编程的需要, 如图 我们的服务器就需要同时处理多个客户端的请求. 这个时候我们就可以创建多个进程来解决这种需要.
- 但是随着业务规模扩大, 一个服务器要处理的客户端越来越多, 那么我们服务器所需要处理的请求也越来越多, 这个时候我们创建的进程也就多了. 这个时候我们服务器就需要大量频繁的创建和销毁进程, 这是比较耗时间的.
- 所以为了解决大量频繁创建和销毁进程, 我们提出了线程的概念. 线程他是被包含在进程里面, 也就是说一个进程中至少有一个线程, 也可以包含N个线程. 这些被包含在进程里面的线程, 是可以共享进程所拥有的资源的(注意: 同一个进程的多线程之间, 共用PCB的内存指针和文件描述符表, 但是各自的状态, 上下文, 优先级, 记账信息是不同的, 也就是各有各的一份)
- 下面通过一个例子来说明多进程和多线程方案的区别
- 我们现在呢要吃100只坤坤, 多进程的方案我们两套房间, 两个人各消灭50只坤坤. 效率比较高(原来1个人吃100只10分钟, 那么一个人吃50只就需要5分钟, 现在两个人吃100只需要5分钟), 但是我们可以看到这样的成本也不低, 需要两个房间, 两张桌子.
- 多线程则是在一张桌子中有两个老铁来吃这100只坤坤, 他们也各吃50只和上面多进程的方案效率是一样的. 这个时候我们的成本却比多进程方案要低, 我们只需要一个房间和一张桌子. 并且随着我们人越来越多, 效率也越高.(这里的人就是我们线程, 一个房子和一个桌子就是我们一个进程, 吃100只坤坤则是我们的任务. 可以看到我们100个人共享了房子和桌子)
- 当然, 线程也不是越多越好, 当线程多的时候可能会导致调度开销很大, 影响到执行效率. 也就是我们例子中, 桌子外的老铁要吃坤坤需要挤过拥挤的人群
- 我们线程之间有可能还会发生抢夺资源冲突, 比如A哥们要吃1号坤坤, B哥们也要吃1号坤坤, A哥们说: 是我先看到的, B哥们说: 你先看到就是你的啊, 我还是先拿到的呢. 这个时候他们两个谁也不服谁, 就打起来了. 最后把桌子一起掀了. 这个时候就是整个进程都受到影响被终止了, 所以说我们要尽量不要让这样的情况发生. 就需要尽量捕获这样的异常并且处理
进程和线程的区别(重要)
- 进程是包含线程的. 每个进程⾄少有⼀个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间.
- 进程是系统分配资源的最⼩单位,线程是系统调度的最⼩单位。
- ⼀个进程挂了⼀般不会影响到其他进程. 但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带⾛(整 个进程崩溃). 后面会说, 前台线程和后台线程
- 进程之间通常不会有资源冲突的情况, 但是同一个进程的线程之间经常发生这样的情况.’
第一个多线程程序
- 这来Thread类就是一个标准库的线程类, 我们要创建一个线程要继承他的类并且重写run方法
class MyThread extends Thread{ //创建一个新的类, 让这个类继承标准库的Thread类@Overridepublic void run(){ //线程的入口函数while(true){System.out.println("hello thread!");try { //休息1秒, 让线程进入阻塞状态Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
public class Demo1 {public static void main(String[] args) {// 1. 创建 Thread 的实例Thread t = new MyThread();t.start(); //启动线程, 这里会自动调用run函数while(true) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}}
-
在上面的代码中有两个线程分别是主线程main 和一般线程t(注意线程只有主线程和一般线程之分, 没有父子线程的说法), 当主线程执行到t.start()这个方法的时候, 就会启动t这个线程.
-
其中start方法会自动取调用t线程中的run方法. 所以这个时候就会循环打印hello thread, sleep方法让线程进入阻塞状态持续1秒
-
我们运行程序的执行效果, 可以看到我们thread打印同时也在打印main函数里面的hello main, 这就多线程的特点, 主线程打印hello main, t线程打印hello thread
为什么先打印main再打印thread
- 因为我们创建t线程, 让他运行并不影响我们主线程的执行, 所以主线程创建完t线程后就继续执行了. 而对于创建t线程呢, 我们通过操作系统提供的api取创建线程, 需要花费时间. 这个时候我们主线程就先打印到hello main了. 之后才打印thread
- 但是我们后面居然出现了先打印thread的情况, 这是为啥呢. 因为后面的代码逻辑并没有出现创建线程了. 所以t线程不需要花费额外的时间, 这个时候就看CPU先调度那个线程了.但由于我们的线程调度是随机的(线程调度不能完全依赖优先级来决定, 还有一些复杂的情况所以是随机的), 所以就有可能出现t线程比主线程先被CPU调度, 自然就先打印hello thread了
抛异常的小问题
- 针对我们使用sleep这个函数会让线程进入阻塞状态, 但是可能会发生InterruptedException, 这个异常的意义是当前线程的阻塞状态被强制打断
- 我们在主线程main这里可以直接抛出这个异常交给JVM来处理
- 但是我们t线程的run方法不能直接抛出这个异常, 是因为编译的问题, 如图所示, 这里的被重写的run方法(父类的run方法)没有抛出这个异常, 这是因为重写的run方法和Thread这个父类的run方法的方法签名不一样
- 可以看到我们父类Thread的run方法并没有声明抛出InterruptedException这个异常, 所以根据我们的方法重写的要求中, 重写的方法要和父类被重写的方法方法签名一样: 即方法名, 参数列表, 声明的抛出异常所以对于t线程这个run方法处理这个受查异常, 必须要用try catch来处理
创建多线程的方式
继承Thread, 重写run方法
- 上面我们介绍的例子就是这种写法
class MyThread extends Thread{ //创建一个新的类, 让这个类继承标准库的Thread类@Overridepublic 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{// 1. 创建 Thread 的实例Thread t = new MyThread();t.start(); //启动线程, 这里会自动调用run函数while(true) {System.out.println("hello main");Thread.sleep(1000);}}}
实现Runnable接口
package thread;/*** Created with IntelliJ IDEA.* Description:* User: 19182* Date: 2025-08-13* Time: 17:09*/
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) throws InterruptedException {MyRunnable myRunnable = new MyRunnable();Thread t = new Thread(myRunnable);t.start();while(true){System.out.println("hello main");Thread.sleep(1000);}}
}
继承Thread, 使用匿名内部类
public class demo3 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(){public 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");Thread.sleep(1000);}}
}
实现Runnable接口, 使用匿名内部类
public class demo4 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(new Runnable(){public 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 world");Thread.sleep(1000);}}
}
lambda表达式(推荐写法)
public class demo5 {public static void main(String[] args) throws InterruptedException {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");Thread.sleep(1000);}}
}
Thread类常见属性和方法
- Thread类是JVM用来管理线程的一个类,换句话说,每个线程都有⼀个唯⼀的Thread 对象与之关联.我们上⾯的例⼦来看,每个执⾏流,也需要有⼀个对象来描述,类似下图所⽰,⽽ Thread 类的对象 就是⽤来描述⼀个线程执⾏流的,JVM 会将这些 Thread 对象组织起来,⽤于线程调度,线程管理。
注意: 一个Thread类对象只能管理一个线程, 也就是一个Thread类对象只能创建一个线程, 不能用一个Thread类对象创建多个进程, 因为我们希望后面都线程的操作一定是准确对应的, 如果一个类对象对应多个线程. 到底该操作那个线程?
常见的构造方法
- 对于其中的两个方可以对线程命名, 方便对线程进行调试
public class demo7 {public static void main(String[] args) {Thread t1 = new Thread(()->{while(true){System.out.println("t1");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "线程1");t1.start();Thread t2 = new Thread(()->{while(true){System.out.println("t2");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}, "线程2");t2.start();}
}
Thread类的几个常见属性
- ID 是线程的唯⼀标识,不同线程不会重
- 名称是各种调试⼯具⽤到
- 状态表⽰线程当前所处的⼀个情况,下⾯我们会进⼀步说明
- 优先级⾼的线程理论上来说更容易被调度到(但是我们说过, 线程调度是随机的)
- 关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有⾮后台线程(比如前台线程)结束后,才会结束运⾏。
- 是否存活,即简单的理解,为 run ⽅法是否运⾏结束了
- 线程的中断问题,下⾯我们进⼀步说明
前台线程和后台线程
- 我们代码创建的线程和main这个主线程默认是前台线程, 前台线程的特点就是他会阻止进程的结束, 只要前台线程没有执行完, 进程就不能结束. 包括main函数已经指向完毕了(因为main函数执行流也是一个线程)
package thread;
public class ThreadDemo7 {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) {e.printStackTrace();}}}},"这是我的线程");t.start();System.out.println("main 执行完毕");}
}
- 这其实就像我们请领导喝酒一样, 领导就是前台线程, 我们打工人就是后台线程, 只有当领导退出酒席了这个酒席才真正结束了, 我们小小打工人退出酒局并不影响酒局正常进行, 领导才是这个酒局的老大.
使用 setDaemon(true) 可以将线程设为后台线程
- 与前台线程对应的就是后台线程, 那么后台线程就不会阻止进程的结束.
public class demo8 {public static void main(String[] args) {Thread t = new Thread(()->{System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);};});t.setDaemon(true); //把前台线程设置为后台线程t.start();System.out.println("main 执行完毕");}
}
- main这个前台线程执行完后, 由于t也被设置为后台线程, 那么这个进程的所有前台线程已经结束, 整个进程就结束了.
start和run的区别
中断一个线程
通过一个标记位变量来中断
public class demo9 {private static boolean running = true;public static void main(String[] args) {Thread t = new Thread(()->{while(running){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("t线程结束");});t.start();// 主线程中, 让用户进行输入Scanner sc = new Scanner(System.in);System.out.println("请输入整数, 0 表示让 t 线程终止");int n = sc.nextInt();if(n == 0){running = false;}}
}
- 注意: running不能是局部变量, 这里的变量如果是局部变量,必须是 final 或 “事实final"修饰(lambda表达式本质是匿名内部类, 匿名内部类只能访问final或事实final), 由于此处的running要被修改, 不能写成 final 或 “事实final”(也就是保证后面不修改), 所以只能写成成员变量. 为啥写作成员变量就可以了呢? 因为lambda表达式本质是"函数式接口” ->匿名内部类, 内部类访问外部类的成员, 这是可以的.
使⽤ Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替⾃定义标志位.
- 加上 break 的作用:在执行 sleep 的过程中,调用 interrupt,大概率 sleep 休眠时间还没到,就被提前唤醒了
- 提前唤醒,会做两件事:通过 interrupt 方法,已经把标志位设为 true 了但是 sleep 提前唤醒操作,就把标位又设回 false(此时循环还是会继续执行了)
- 要想线程结束,只需要在 catch 中加上 break 就行了
public class demo10 {public static void main(String[] args) {Thread t = new Thread(()->{Thread cur = Thread.currentThread(); //获取当前线程的引用while(!cur.isInterrupted()){ //Thread类内部设置了一个boolean变量System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();// 执行一些其他逻辑~~// 退出之前做一些释放资源类工作// break;}}});t.start();Scanner scanner = new Scanner(System.in);System.out.println("输入 0 表示让线程 t 结束: ");int n = scanner.nextInt();if(n == 0){t.interrupt(); //手动修改标志为为ture, 但是这里会唤醒sleep, 但是sleep被提前唤醒后, 会把标志位又设置成true}}
}
线程等待
- 这里我们希望用t线程来算出result从1加到10000000的值, 但是结果却是0, 为什么呢?
- 因为我们前面说过, 线程调度是随机的, 这里我们就是先调度执行完了主线程, 我们才调度执行t线程来计算result, 所以这里的主线程是先被调度执行的那个, result还没计算就是0.
- 可是也有可能我们先调度执行t线程, 然后执行主线程, 这些调度都是不确定的. 我们为了排除这种不确定性, 让我们主线程一定是等t线程执行完后, 再继续执行. 这个时候我们就可以让主线程等待t线程
public class demo11 {private static long result = 0;public static void main(String[] args) throws InterruptedException {// 创建一个线程, 让这个线程计算 1 + 2 + 3 + ... + 1000 => 500500// 主线程在这个计算线程执行完毕后, 打印此处的结果.Thread t = new Thread(()->{for(int i = 1; i <= 1000_0000; i++){result += i;}});t.start();System.out.println(result);//这里主线程并没有等t线程计算完就打印result}
}
结果:
- 用t.join()让主线程等待, 等待意味着主线程进入阻塞状态. (也就是不让去参与CPU的调度执行)
public class demo11 {private static long result = 0;public static void main(String[] args) throws InterruptedException {// 创建一个线程, 让这个线程计算 1 + 2 + 3 + ... + 1000 => 500500// 主线程在这个计算线程执行完毕后, 打印此处的结果.Thread t = new Thread(()->{for(int i = 1; i <= 1000_0000; i++){result += i;}});t.start();t.join(); //让主线程等待t线程执行完后, 再继续执行System.out.println(result); }
}
- 除了第一种没有参数的等待方法join()必须要等待指定线程结束才继续参与CPU调度执行, 还有可以指定等待时间的.
获取当前线程引⽤
- 如果是继承 Thread, 直接使用 this 拿到线程(Thread)的引用
package thread;
class MyThread extends Thread {@Overridepublic void run() {// 这个代码中,如果想要获取到线程的引用,直接使用 this 即可System.out.println(this.getId() + ", " + this.getName());}
}
public class ThreadDemo16 {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();t1.start();t2.start();}
}
- 如果是 Runnable 或者 lambda 的方式, this 就无能为力了, 此时 this 已经不再指向 Thread 对象了.
- 就只能使用 Thread.currentThread() 了
package thread;
public class ThreadDemo17 {public static void main(String[] args) {Thread t1 = new Thread(() -> {Thread t = Thread.currentThread();System.out.println(t.getName());});Thread t2 = new Thread(() ->{Thread t = Thread.currentThread();System.out.println(t.getName());});t1.start();t2.start();}
}
休眠当前线程
- 这两个方法都是让我们的线程进入阻塞状态
- sleep线程让线程进入阻塞队列阻塞指定时间后再回就绪队列调度
- sleep(0)是一个特殊情况, 相当于主动放弃CPU调度执行, 让其他线程先执行. 也就是在阻塞待0ms, 马上进入就绪重新排队(也就是我们背书的时候太紧张了, 让别人先背书, 重新排背书队列)