【JavaEE初阶】 多线程编程核心:解锁线程创建、方法与状态的创新实践密码
文章目录
- 一:初识线程(Thread)
- 1.1:线程的概念
- 1.2:为什么需要线程?
- 1.3:那么进程和线程的区别是什么?
- 1.4:Java 的线程和操作系统线程的关系
- 二:多线程程序的编写
- 2.1:继承 Thread 类
- 2.2:实现 Runnable 接口
- 2.3:匿名内部类创建 Thread 子类对象
- 2.4:匿名内部类创建 Runnable 子类对象
- 2.5:lambda 表达式创建 Runnable 子类对象
- 三:在多线程下-增加了运行速度
- 四:Thread类及常见的方法
- 4.1:Thread常见的构造方法
- 4.2:Thread几个常见的属性
- 4.3:启动一个线程 - start()
- 4.4:中断一个线程
- 4.5:线程等待-join()
- 五:线程的状态
- 5.1:线程的状态是一个枚举类型 Thread.State
一:初识线程(Thread)
1.1:线程的概念
一个线程就是一条 “执行流”。每个线程各自按顺序执行自己的代码,多个线程之间则 “并发” 执行多份代码。线程(Thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
举一个例子:厨房做菜
想象一个繁忙的厨房正在准备顾客丰盛的晚餐,这个厨房就是一个 “进程”。
进程:一个正在执行的程序实例。它拥有独立的资源,如内存空间、文件句柄等。就像这个厨房,它拥有自己的空间、厨具、食材等资源。
线程:进程内部的一个独立执行流。一个进程可以包含多个线程,它们共享进程的资源。就像厨房里的厨师,每位厨师都是一个独立的“线程”。
这里就可以分为:
单线程厨房,即一个厨师完成所有的做菜流程
多线程厨房:即多位厨师分工合作,每个厨师分工完成各自的任务。
1.2:为什么需要线程?
首先,“并发编程”已成为“刚需”。
- 单核CPU的发展遭遇瓶颈,要提升算力就需依赖多核CPU,而并发编程能更充分地利用多核CPU资源。
- 部分任务场景存在“等待IO”的情况,为了在等待IO的时间里处理其他工作,也需要用到并发编程。CPU可以切换到另一个线程去执行,避免了CPU时间的浪费。就像一位厨师在等待水烧开时,可以先去切菜,而不是空等。
其次,尽管多进程也能实现并发编程,但线程比进程更轻量:
- 创建线程比创建进程更快;
- 销毁线程比销毁进程更快;
- 调度线程比调度进程更高效。
但是,即便线程已比进程轻量,人们仍不满足,进而发展出了“线程池”(ThreadPool)和“协程”(Coroutine)。
1.3:那么进程和线程的区别是什么?
-
进程包含线程,每个进程至少存在一个线程,即主线程。
-
进程之间不共享内存空间,而同一进程内的线程共享该进程的内存空间。
-
进程是系统分配资源的最小单位,线程是系统调度的最小单位。
-
一个进程崩溃通常不会影响其他进程,但一个线程崩溃可能导致整个进程(及其中所有线程)一同崩溃。
1.4:Java 的线程和操作系统线程的关系
线程是操作系统层面的概念,其机制由操作系统内核实现,并向用户层提供API供调用(例如Linux的pthread库)。
Java标准库中的Thread类,可视为对操作系统提供的线程API进行了进一步的抽象与封装。
二:多线程程序的编写
2.1:继承 Thread 类
package ThreadDemo;
class MyThread extends 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 {Thread thread=new MyThread();thread.start();while(true){System.out.println("hello main");Thread.sleep(1000);}}
}
可以看到hello thread和hello main都打印出来了,但是不是交替出现的。这里就体现了操作系统调度线程是随机的.
每个线程都是独立的工作流,多个线程之间是并发执行的
当我们这里将主线程换成thread.run(),因为上面的是死循环只看到了打印hello thread
在这里我们可以利用Java中的jconsole 命令观察线程
右键点击以管理员身份运行,选择我们正在运行的线程,点击连接即可
其中main就是我们的主线程,Thread-0就是我们通过MyThread类创建的线程,其他的线程都是我们jvm自带的,来负责对线程进行一些背后的操作。
点击main可以看到详细信息
这里是使用thread.start()情况下当我们使用thread.run()就没有Thread-0线程了。
2.2:实现 Runnable 接口
package ThreadDemo;
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 {Runnable runnable=new MyRunnable();Thread thread=new Thread(runnable);thread.start();while(true){System.out.println("hello main");thread.sleep(1000);}}
}
注意:
runnable 没有 start 方法.
要想启动线程, 需要搭配 Thread. runnable.start();
2.3:匿名内部类创建 Thread 子类对象
package ThreadDemo;
public class demo3 {public static void main(String[] args) throws InterruptedException {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");Thread.sleep(1000);}}
}
2.4:匿名内部类创建 Runnable 子类对象
package ThreadDemo;
public class demo4 {public static void main(String[] args) throws InterruptedException {Runnable runnable=new Runnable(){public void run(){while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};Thread t=new Thread(runnable);t.start();while(true){System.out.println("hello main");Thread.sleep(1000);}}
}
2.5:lambda 表达式创建 Runnable 子类对象
package ThreadDemo;
public class demo4 {public static void main(String[] args) throws InterruptedException {Runnable runnable=new Runnable(){public void run(){while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};Thread t=new Thread(runnable);t.start();while(true){System.out.println("hello main");Thread.sleep(1000);}}
}
三:在多线程下-增加了运行速度
使用 System.nanoTime()
可记录系统的纳秒级时间戳:
- 串行(serial):单线程依次完成一系列运算
- 并行(concurrency):通过两个线程同时执行相同运算
以一个代码为例:
package ThreadDemo;
public class demo6 {private static final long count = 10_0000_0000;public static void main(String[] args) throws InterruptedException {// 使⽤并发⽅式concurrency();// 使⽤串⾏⽅式serial();}private static void concurrency() throws InterruptedException {long begin = System.nanoTime();// 利⽤⼀个线程计算 a 的值Thread thread = new Thread(new Runnable() {@Overridepublic void run() {int a = 0;for (long i = 0; i < count; i++) {a--;}}});thread.start();// 主线程内计算 b 的值int b = 0;for (long i = 0; i < count; i++) {b--;}// 等待 thread 线程运⾏结束thread.join();// 统计耗时long end = System.nanoTime();double ms = (end - begin) * 1.0 / 1000 / 1000;System.out.printf("并发: %f 毫秒%n", ms);}private static void serial() {// 全部在主线程内计算 a、b 的值long begin = System.nanoTime();int a = 0;for (long i = 0; i < count; i++) {a--;}int b = 0;for (long i = 0; i < count; i++) {b--;}long end = System.nanoTime();double ms = (end - begin) * 1.0 / 1000 / 1000;System.out.printf("串⾏: %f 毫秒%n", ms);}}
需要注意的是 :多线程并不一定就能提高速度,可以观察,count 不同,实际的运行效果也是不同的。
四:Thread类及常见的方法
在 Java 中,Thread
类是 JVM 用来管理线程的核心类。每个线程在 JVM 中都对应一个唯一的 Thread
对象,通过这个对象可以对线程的状态、行为进行控制和管理。
4.1:Thread常见的构造方法
构造方法声明 | 参数说明 | 用途 |
---|---|---|
Thread() | 无参数 | 创建一个线程对象,需通过重写 run() 方法定义线程任务(默认线程名由 JVM 生成,如 Thread-0 )。 |
Thread(Runnable target) | target :实现 Runnable 接口的对象(封装线程任务) | 创建线程并关联任务,推荐使用(解耦线程与任务逻辑,避免单继承限制)。 |
Thread(String name) | name :线程名称(字符串) | 创建线程并指定名称,便于调试(如日志输出、线程监控时区分不同线程)。 |
Thread(Runnable target, String name) | target :任务对象;name :线程名称 | 同时指定任务和线程名称,兼顾任务解耦与调试便利性。 |
Thread(ThreadGroup group, Runnable target) | group :线程组;target :任务对象 | 将线程加入指定线程组(线程组用于批量管理线程,如统一设置优先级、中断等)。 |
说明:
- 最常用的构造方法是
Thread(Runnable target)
和Thread(Runnable target, String name)
,通过传入Runnable
对象分离任务逻辑,更符合面向对象设计。 - 线程名称可通过
setName()
后续修改,但建议在构造时指定,便于早期调试。 - 线程组(
ThreadGroup
)主要用于线程的批量管理,日常开发中使用较少,除非需要对一组线程进行统一操作(如销毁所有子线程)。
那么如何为一个线程修改一下名字呢?以下面代码为例细述
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);}}},"我的线程1");t.start();}
}
通过jconsole就可以看到我刚刚修改的线程名字
注意:为什么这个线程中没有看到main,是因为main线程已经执行完了。
- 在这个代码中start执行完毕,main方法就运行完了
- main方法结束,对应的main线程就结束了
- 也就是对于一个线程来说,线程对应的"入口方法"执行完毕,对应的线程就会自动销毁。
4.2:Thread几个常见的属性
属性 | 获取方法 | 说明 |
---|---|---|
ID | getId() | 线程的唯一标识,不同线程不会重复 |
名称 | getName() | 用于调试工具区分线程,可自定义 |
状态 | getState() | 表示线程当前所处的情况(如 NEW 、RUNNABLE 、TERMINATED 等) |
优先级 | getPriority() | 优先级高的线程理论上更容易被调度(范围 1~10,默认 5) |
是否后台线程 | isDaemon() | JVM会在一个进程的所有非后台线程结束后,才会结束运行。 |
是否存活 | isAlive() | 简单理解为 run 方法是否运行结束 |
是否被中断 | isInterrupted() | 标识线程是否被中断(需结合 interrupt() 方法协作处理) |
这里讲一下是否为后台进程
,引入思考那么前台线程又是什么?
前台线程
:线程没有运行完,进程就不会结束(即线程可以阻止进程结束)
后台线程
:线程没有运行完,进程可以结束(即线程不能阻止进程结束)
应用场景:当一个线程做的任务很重要,必须要完成这个任务,这样就应该设置为前台线程。
如果一个线程做的任务无关紧要/周期性无期限执行,就可以设置为后台线程。
我们可以通过.setDaemon()
设置线程为前台还是后台,但是这个操作必须在start之前。
以下面代码为例,详细描述一下现成的存活isAlive()
package ThreadDemo;
public class demo7 {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);}}});t.start();System.out.println("是否存活:"+t.isAlive());Thread.sleep(4000);System.out.println("是否存活:"+t.isAlive());}
}
可以看到判断线程是否存活,看线程的状态即可。
- 线程正在执行,isAlive()就是true的。
- 线程执行完毕,isAlive()就是false的。
4.3:启动一个线程 - start()
以这个代码为例
package ThreadDemo;
public class demo8 {public static void main(String[] args) {Thread t=new Thread(()->{System.out.println("hello thread");});t.start();System.out.println("hello main");}
}
当我们执行几次结果后发现,都是先打印hello main
在打印hello thread
那么是不是都是一直这个顺序呢?
当start后,main线程和t线程两个执行流,是并发执行的关系。由于操作系统对于线程的调度是随机的,所有也有可能出现先打印hello thread
的情况。
重点:一个线程对象只能start一次。
当一个线程对象出现一次以上start就会出现如下图所示报错
4.4:中断一个线程
Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记
此处针对lambda表达式定义,实际上实在new Thread()之前的,就会出现t没有进行初始化的报错信息。
我们可以使用Thread提供的一个静态方法currentThread
package ThreadDemo;
public class demo9 {public static void main(String[] args) throws InterruptedException {Thread t=new Thread(()->{//由于currentThread方法是在start之后在执行的//并且是在t线程中执行的,返回的结果就是指向t线程对象的引用while(!Thread.currentThread().isInterrupted()){System.out.println("hello thread");}});t.start();Thread.sleep(2000);//在main线程中尝试中止t线程t.interrupt();}
}
这样就可以使我们的t线程结束
但是我们在循环中加入sleep
后
就会触发异常
在这里如果t线程正在休眠,此时的main调用Interrupt方法,就会把sleep提前给唤醒。sleep被提前唤醒,就触发异常,就把isInterrupted标志位给重置为了false。这样我们的线程就不会结束
那么在这种情况下该怎么结束线程呢?
①:可以在e.printStackTrace();后面加上一个break
,让线程立即结束
package ThreadDemo;
public class demo9 {public static void main(String[] args) throws InterruptedException {Thread t=new Thread(()->{//由于currentThread方法是在start之后在执行的//并且是在t线程中执行的,返回的结果就是指向t线程对象的引用while(!Thread.currentThread().isInterrupted()){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();break;}}});t.start();Thread.sleep(2000);//在main线程中尝试中止t线程t.interrupt();}
}
当然也可以把这个e.printStackTrace();注释掉,这样异常就不会打印了
②:也可以通过ideal自动生成的catch: throw new RuntimeException(e)
,也可以在break之前在添加其他逻辑
这几种方式,本质上都是t线程自己决定自己是否要终止,相当于main只是对t提供了一个“建议”,而不是:“强制执行的”.
使用Interrupt方法的时候:
- 1:t 线程没有进行sleep等阻塞操作,t 的
isInterrupted()
方法返回true,通过循环结束t 线程。 - 2:t 线程进行sleep等阻塞操作 ,还是会返回true,但是sleep如果被提前唤醒InterruptException,同时也会把
isInterrupted()
返回的结果设置为false,此时就需要手动去决定是否要结束线程。
4.5:线程等待-join()
mian线程等待t 线程结束
package ThreadDemo;
public class demo10 {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);}}});t.start();System.out.println("主线程等待之前");t.join();System.out.println("主线程等待之后");}
}
t 线程等待 main 线程结束
package ThreadDemo;
public class demo11 {public static void main(String[] args) throws InterruptedException {Thread myThread=Thread.currentThread();Thread t=new Thread(()->{try {System.out.println("t线程等待之前");myThread.join();System.out.println("t线程等待之后");} catch (InterruptedException e) {throw new RuntimeException(e);}});t.start();for(int i=0;i<10;i++){System.out.println("hello main");Thread.sleep(1000);}}
}
注意这里需要获取main线程的引用:Thread myThread=Thread.currentThread();
- 哪个线程调用的join,该线程就是等的一方
- join前面是那个引用,该线程就是被等的一方
join默认的是死等,但是join还有重载版本,可以指定最大的等待时间
方法声明 | 等待时间参数说明 |
---|---|
void join(long millis) | millis :等待时间(单位:毫秒),0 表示无限等待(与无参 join() 效果一致) |
void join(long millis, int nanos) | millis :毫秒数(主单位),nanos :纳秒数(范围 0-999999,辅助精度),两者总和为总等待时间 |
以上面代码为例,加入3000毫秒的最大等待时间
五:线程的状态
5.1:线程的状态是一个枚举类型 Thread.State
在 Java 中,线程(Thread
)的生命周期包含以下 6 种状态,定义在 Thread.State
枚举类中,如下图所示:
状态名称 | 说明 |
---|---|
NEW (新建) | 线程对象已创建,但尚未调用 start() 方法,未开始执行。 |
RUNNABLE (可运行) | 线程已调用 start() 方法,正在 JVM 中执行,或等待 CPU 资源(就绪状态)。 |
BLOCKED (阻塞) | 线程因竞争同步锁(synchronized )被阻塞,等待获取锁。 |
WAITING (等待) | 线程无限期等待另一个线程的特定操作(如 wait() 、join() 无参版),需被显式唤醒。 |
TIMED_WAITING (计时等待) | 线程在指定时间内等待(如 sleep(time) 、wait(time) 、join(time) ),超时后自动唤醒。 |
TERMINATED (终止) | 线程执行完毕(run() 方法结束)或因异常终止。 |
可通过 Thread.getState()
方法获取线程当前状态。
package ThreadDemo;
public class demo12 {public static void main(String[] args) {Thread t=new Thread(()->{});System.out.println(t.getState());t.start();t.join();System.out.println(t.getState());}
}
可以看到还没有开始start时候就是NEW
状态,线程执行完了就是TERMINATED
RUNNABLE
,当在t.start();
后加入
System.out.println(t.getState()+" "+t.isAlive());
此时就可以看到RUNNABLE状态
,代码中不触发阻塞类操作,都是RUNNABLE状态
其他三个都是阻塞状态,以下面代码为例
package ThreadDemo;
public class demo12 {public static void main(String[] args) throws InterruptedException {Thread t=new Thread(()->{while(true){try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});System.out.println(t.getState()+" "+t.isAlive());t.start();System.out.println(t.getState()+" "+t.isAlive());t.join();System.out.println(t.getState()+" "+t.isAlive());}
}
再利用jconsole就可以看到我们main线程的状态
以及Thread-0的状态,此时t线程使在join操作所以是 TIMED_WAITING
状态。
状态转换关键点:
NEW
→RUNNABLE
:调用start()
方法后进入可运行状态。RUNNABLE
→BLOCKED
:竞争同步锁失败时进入阻塞状态,获取锁后回到RUNNABLE
。RUNNABLE
→WAITING
/TIMED_WAITING
:调用wait()
、join()
等方法时进入等待状态,被唤醒或超时后回到RUNNABLE
。- 所有状态 →
TERMINATED
:线程执行完成或异常终止后进入终止状态,无法再切换到其他状态。