【Java SE】多线程(一):认识线程与Thread类
一、认识线程:什么是线程,为什么需要线程?
1.1 线程的本质:独立的执行流
线程(Thread)是进程内的执行单元,一个线程对应一条独立的执行流。多个线程可以“同时”执行不同的代码逻辑
举个例子:一家公司办理业务时,会计张三、李四、王五分别负责转账、发福利、缴社保——三人都是独立执行流,但共同完成公司的整体任务,其中张三可视为“主线程”,李四和王五是“子线程”。
特性:
- 每个线程有自己的执行顺序,按代码逻辑逐步执行;
- 多线程的“同时”是宏观上的,微观上CPU通过时间片轮转实现切换(单核CPU),多核CPU可真正并行执行;
- 线程依托于进程存在,一个进程至少包含一个主线程(Java程序的
main线程就是主线程)。
1.2 为什么需要线程?
线程的存在,本质是为了解决效率问题,主要体现在三个层面:
-
应对硬件发展:充分利用多核CPU
单核CPU的性能提升已遇瓶颈,现在主流服务器、PC都是多核架构。若程序是单线程,即使有8核CPU,也只能用到1核资源;而多线程可将任务拆分到不同核上并行执行,直接提升算力利用率。 -
优化IO等待:减少时间浪费
很多程序存在大量IO操作(读取文件、网络请求),IO操作的速度远慢于CPU运算。- 举个例子:一个线程发起网络请求后,需要等待1秒才能收到响应——若此时该线程阻塞等待,CPU就处于空闲状态。而多线程可在一个线程等待IO时,让其他线程执行计算任务,把等待时间充分利用起来。
-
比进程更轻量:降低资源开销
进程是系统分配资源的最小单位,线程是系统调度的最小单位。相比进程,线程的创建、销毁、调度成本更低:- 创建线程:无需为其分配独立的内存空间(共享进程的内存),仅需少量栈空间;
- 销毁线程:释放的资源远少于进程;
- 调度线程:无需切换进程地址空间,上下文切换速度更快。
1.3 进程与线程的区别
通过“银行办理业务”的例子理解:
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 资源分配 | 系统分配资源的最小单位(如内存、文件句柄) | 不独立分配资源,共享进程的内存空间 |
| 内存隔离 | 进程间内存完全隔离(如A客户的业务数据不会被B客户看到) | 同一进程的线程共享内存(如张三、李四共享公司的业务票据) |
| 调度单位 | 不是调度最小单位 | 系统调度的最小单位(CPU直接调度线程) |
| 稳定性 | 一个进程崩溃,不影响其他进程 | 一个线程崩溃,可能导致整个进程崩溃(如李四的线程出错,公司业务整体中断) |
增加线程一定程度上可以提高效率,但是线程数目达到一定程度后,线程的调度开销也会非常明显,而调度开销会拖慢程序性能
1.4 Java线程与操作系统线程的关系
Java中的Thread类并非“自创”线程,而是对操作系统线程的封装:
- 操作系统内核实现了线程的核心机制(如Linux的
pthread库); - Java的
Thread类通过JNI(Java Native Interface)调用操作系统的线程API,创建、管理线程; - 简单来说:Java线程是“壳”,操作系统线程是“核”,Java线程的状态、调度最终由操作系统控制。
Java中的Thread在Java.lang中,每个工程都默认导入,无需手动import
二、Java多线程
2.1 主线程与子线程并行
创建一个子线程,与主线程同时循环打印线程名称,两者随机交:)
public class demo01 {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
//真正在系统中创建一个线程
t.start();
while(true){
System.out.println("hello main");
//sleep是Thread类的静态方法,不需要通过实例调用
Thread.sleep(1000);
}
}
}
class MyThread extends Thread{
//run方法 相当于线程的入口
@Override
public void run() {
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
start()vsrun():- 调用
start()会让JVM创建操作系统线程,再执行run()中的逻辑;若直接调用run(),只是普通方法调用,不会创建新线程,这样就会永远卡在run方法的死循环中,main方法内的循环不会进入。; run()是线程的入口,不需要手动调用,在新线程创建好以后自动去执行,相当于回调函数
- 调用
- 执行结果:主线程(
main)和子线程(Thread-0)的打印会随机交替,因为CPU会在两者间切换调度; - 可通过
jconsole.exe查看线程状态。左侧都是当前进程中的线程。除了Thread-0外,其他是JVM内置的线程,启动任意一个Java进程,都会自带这些线程
- 堆栈追踪:线程的调用栈,获取线程状态的时刻,线程里的代码执行到哪一行
2.2 创建线程的4种方式
Java提供了多种创建线程的方式,核心是“定义线程要执行的逻辑(run()方法),具体可分为4种:
方式1:继承Thread类
- 自定义类继承
Thread,重写run(),创建实例后调用start(); - 优点:实现简单,可直接使用
this指代当前线程; - 缺点:Java单继承限制,若类已继承其他类,则无法使用此方式。
方式2:实现Runnable接口
- 自定义类实现
Runnable接口,重写run(),将实例传入Thread构造器,调用start();
// 1. 实现Runnable接口
public 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);
}
}
}
}
// 2. 创建线程并启动
public class RunnableDemo {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}
}
- 优点:能够让执行任务本身和线程这个概念解耦和,后续修改代码(比如不通过线程而通过其他方式执行任务)只需要采用Runnable这样的方案,代码修改更简单(
Runnable负责逻辑,Thread负责线程管理) - 缺点:需通过
Thread.currentThread()获取当前线程,稍显繁琐。
方式3:匿名内部类
Thread
本质上就是方式1。若线程逻辑简单,无需单独定义类,可使用匿名内部类:
Thread r = new Thread(){
//...
};
这个语句主要干了三件事:
- 创建一个Thread子类,没有名字
- { }; 内可以编写子类定义的代码,包括子类应该有哪些属性,哪些方法,重写父类方法等
- 创建了匿名内部类的实例,并且把实例的引用赋值给t
// 匿名内部类继承Thread
public class demo03 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(){
@Override
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
Runnable runnable = new Runnable() {
//...
};
与上面的Thread类似,同样做了三件事:
- 创建一个Runnable子类,没有名字
- { }; 内可以编写子类定义的代码,包括子类应该有哪些属性,哪些方法,重写方法等
- 创建了匿名内部类的实例,并且把实例的引用赋值给t
方式4:Lambda表达式
Runnable是函数式接口(只有一个抽象方法),可通过Lambda表达式简化:
// 单行逻辑
Thread t3 = new Thread(() -> System.out.println("Lambda线程1"));
// 多行逻辑
Thread t4 = new Thread(() -> {
System.out.println("Lambda线程2");
System.out.println("多行逻辑示例");
});
t3.start();
t4.start();
- 优点:代码最简洁,适合短期使用的线程;
- 缺点:逻辑复杂时可读性较差。
注意:lambda表达式如果使用了外部变量,就会触发变量捕获,这个变量应该是被final修饰或者赋值一次后不再修改

lambda表达式是回调函数,执行时机可能在很久之后,操作系统真正创建出线程才会执行。很有可能当这个线程创建好,main线程已经结束,对应的isFinished已经销毁。
为了防止上述问题,Java会把被捕获的变量拷贝一份,给lambda表达式使用,这样外部变量是否销毁就不会影响lambda表达式的执行。
拷贝意味着这个变量不适合修改,本质上这个外部变量和被拷贝的变量是两个变量,一个修改,另一个不会改变。因此这个变量就应该是不变的:被final修饰或赋值一次不再改变
- 如果这个外部变量是引用类型呢?

这段代码中,test是局部变量,会随着main线程结束而销毁;而创建的Test()对象的生命周期由JVM中的垃圾回收管理,不存在main线程结束就销毁。因此可以修改这个对象的值,但是不可以修改test这个变量的指向。
- 如果把外部变量改成成员变量
此时不再是“变量捕获”,而是“内部类访问外部类的成员”。
lambda表达式本质上是函数式接口,相当于一个内部类,内部类本来就可以访问外部类的成员,因为这个成员变量的生命周期也是由JVM垃圾回收管理,不需要担心生命周期失效的问题,也就不必拷贝,不需要限制final
三、Thread类详解:常用方法与线程属性
3.1 Thread的常见构造方法
通过构造方法,可指定线程的名称、关联的Runnable逻辑等,常用构造方法如下:
| 构造方法 | 说明 |
|---|---|
Thread() |
创建一个默认名称的线程(如Thread-0) |
Thread(Runnable target) |
创建线程,关联Runnable逻辑(核心逻辑由target的run()实现) |
Thread(String name) |
创建指定名称的线程(便于调试,如Thread t = new Thread("转账线程")) |
Thread(Runnable target, String name) |
关联Runnable逻辑,并指定线程名称 |
示例:
// 1. 默认构造
Thread t1 = new Thread();
// 2. 关联Runnable
Thread t2 = new Thread(new MyRunnable());
// 3. 指定名称
Thread t3 = new Thread("用户登录线程");
// 4. 关联Runnable+指定名称(推荐)
Thread t4 = new Thread(new MyRunnable(), "订单处理线程");
3.2 Thread的核心属性
| 属性 | 含义 | 获取方法 | 关键说明 |
|---|---|---|---|
| ID | 线程唯一标识 | getId() |
每个线程的ID唯一,由JVM分配,不可修改 |
| 名称 | 线程名称 | getName() |
可通过构造方法指定,默认Thread-n,用于调试(如jconsole中显示) |
| 状态 | 线程当前状态 | getState() |
线程有6种状态(如NEW、RUNNABLE),后续详细讲解 |
| 优先级 | 线程调度优先级 | getPriority() |
范围1-10,默认5,优先级高的线程更易被调度(但不保证) |
| 后台线程 | 是否为后台线程 | isDaemon() |
后台线程:JVM退出时不等待其完成;前台线程:JVM需等待所有前台线程完成才退出 |
| 存活状态 | 是否存活 | isAlive() |
存活:线程已启动且未终止(run()未执行完);非存活:NEW或TERMINATED状态 |
| 中断状态 | 是否被中断 | isInterrupted() |
表示线程是否收到中断信号,后续讲解“中断线程”时详细说明 |
前台线程 vs 后台线程
以下图为例

- 前台线程:可以决定整个进程是否结束。所有前台线程都结束时,整个进程就可以结束了。main线程以及其他手动创建线程默认是前台线程,可以通过
isDaemon()来需改属性 - 后台线程:图中除了main和Thread-0以外都是JVM自带的线程,属于有特殊功能的线程,会跟随整个进程持续执行。这些属于后台线程,他们不能决定整个进程是否结束。如果前台线程全部结束,进程结束,那么后台线程也会随之结束。
示例:获取线程属性
public class ThreadAttrDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ": 运行中");
Thread.sleep(1000); // 休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 即将终止");
}, "测试线程");
// 启动前获取属性(此时线程为NEW状态,未存活)
System.out.println("启动前 - 名称:" + thread.getName());
System.out.println("启动前 - ID:" + thread.getId());
System.out.println("启动前 - 状态:" + thread.getState());
System.out.println("启动前 - 是否存活:" + thread.isAlive());
System.out.println("启动前 - 是否后台线程:" + thread.isDaemon());
// 启动线程
thread.start();
// 等待线程执行完成
while (thread.isAlive()) {}
// 线程终止后获取属性
System.out.println("终止后 - 状态:" + thread.getState());
System.out.println("终止后 - 是否存活:" + thread.isAlive());
}
}

- 启动前:状态为
NEW,isAlive()为false; - 终止后:状态为
TERMINATED,isAlive()为false; - 后台线程默认是
false(前台线程),若要设置为后台线程,需在start()前调用thread.setDaemon(true)。
3.3 启动线程
启动线程必须调用start(),而非run()。这是因为start()做了两件关键事情:
- 向JVM申请创建操作系统线程 ,将Java线程与操作系统线程关联;
- 操作系统线程创建成功后,由操作系统调度执行
run()方法中的逻辑。
若直接调用run(),只是普通的方法调用,不会创建新线程——相当于主线程执行了run()中的代码,没有并发效果。
每个Thread对象和操作系统中的线程是一一对应的,每个Thread对象start方法只能调用一次

3.4 终止线程
线程启动后,若想让其停止,不能直接调用stop()方法,会强制终止线程,导致资源泄漏,需通过“协作式”方式停止。
常用的终止方式有两种:
方式1:通过共享标记位终止
定义一个volatile修饰的布尔变量作为标记,线程循环判断标记位,若为true则终止执行。
public class InterruptByFlag {
// 共享标记位:volatile保证内存可见性
private static volatile boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!isQuit) { // 循环判断标记位
System.out.println("子线程:正在转账...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("子线程:收到中断信号,停止转账");
}, "转账线程");
t.start();
// 主线程等待3秒后,设置标记位为true(中断子线程)
Thread.sleep(3000);
isQuit = true;
System.out.println("主线程:发送中断信号");
}
}
方式2:通过Thread的interrupt()方法终止
Thread类内置了中断标记位,可通过interrupt()方法设置标记,线程通过Thread.interrupted()或Thread.currentThread().isInterrupted()判断标记。
不可以在lambda表达式中直接调用这个线程的isInterrupted()方法,因为lambda的定义在new Thread()之前

Thread提供了currentThread()方法来访问当前线程(类似于this),currentThread()是静态方法,可以通过类名直接调用,返回结果是当前线程。
public class demo11 {
public static void main(String[] args) {
Thread t = new Thread(()->{
//方法在lambda表达式中(也就是t线程的入口方法)调用,返回结果就是t的名称:Thread-0
System.out.println(Thread.currentThread().getName());
});
t.start();
//方法在main方法调用,返回结果就是main线程的名称:main
System.out.println(Thread.currentThread().getName());
}
}
核心方法:
| 方法 | 说明 |
|---|---|
thread.interrupt() |
给线程thread发送中断信号:若线程阻塞(如sleep、wait),则抛出InterruptedException;若线程运行中,则设置中断标记位 |
Thread.interrupted() |
判断当前线程的中断标记位,调用后清除标记位(返回true后,标记位变为false) |
Thread.currentThread().isInterrupted() |
判断当前线程的中断标记位,调用后不清除标记位 |
示例:使用interrupt()中断线程
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;
}
}
});
t.start();
Thread.sleep(3000);
System.out.println("main线程尝试终止t线程");
t.interrupt();
System.out.println("线程结束");
}
}
注意:
- 若线程处于
sleep、wait、join等阻塞状态,interrupt()会导致线程抛出InterruptedException,并清除中断标记位; - 异常抛出后,线程不会自动终止,需在
catch块中手动处理。- 这样让程序员有更多的操作空间:可以选择立即终止线程(
break),或处理一部分逻辑再终止(写一些代码再break),也可以让线程继续执行(啥都不写); - 这里也可以看出Java中的线程终止不是强制性的,main让t终止,t不一定终止,选择权在t手里。
- 这样让程序员有更多的操作空间:可以选择立即终止线程(
- 相比共享标记位,
interrupt()的优势是:即使线程处于阻塞状态,也能立即响应终止(无需等待阻塞结束)。
3.5 等待线程
多线程之间并发执行是随机调度的,而站在程序员的视角,我们不希望太多不可控情况的出现,join()方法可以控制多个线程之间结束的先后顺序
核心方法:
| 方法 | 说明 |
|---|---|
thread.join() |
当前的线程阻塞,直到thread线程执行完成后,才继续执行 |
thread.join(long millis) |
调用线程阻塞最多millis毫秒,若thread仍未执行完,也会继续执行 |
thread.join(long millis,long nanos) |
同上,时间精确到纳秒 |
在应用程序中很难进行纳秒级别的时间测量,并且线程本身的调度就有可能达到毫秒级别
public class demo12 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for(int i = 0;i<3;i++){
try {
Thread.sleep(3000);
System.out.println("hello thread");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t线程结束");
});
t.start();
//main线程等待t线程,t结束后main继续执行
t.join();
System.out.println("主线程结束");
}
}
输出结果:
3.6 休眠线程
Thread.sleep(long millis)让当前线程休眠指定毫秒数,休眠期间线程会释放CPU资源,进入TIMED_WAITING状态。
- 休眠时间是“最小保证时间”:实际休眠时间可能大于
millis,因线程调度有延迟; - 休眠时若收到中断信号(
interrupt()),会抛出InterruptedException并清除中断标记; sleep()是静态方法,只能休眠当前线程,不能休眠其他线程。- 休眠时不可以被调度,休眠后处于可调度状态,但是不一定立即执行
- sleep(0) :表示让出当前的线程,立即放弃CPU资源,等待操作系统重新调度
四、线程的状态:6种状态及流转逻辑
线程从创建到终止,会经历多种状态的变化。Java定义了6种线程状态,通过Thread.State枚举表示,理解这些状态是排查线程问题(死锁、阻塞)的关键。
4.1 6种线程状态及含义
通过代码打印所有状态:
public class ThreadStateEnum {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
输出结果:
NEW
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
| 状态 | 含义 | 典型场景 |
|---|---|---|
NEW |
线程已创建,但未启动,未调用start() |
Thread t = new Thread();后,t的状态为NEW |
RUNNABLE |
线程可运行,包含正在运行和等待CPU调度两种情况 | 调用start()后,线程进入RUNNABLE;sleep()结束后也会回到此状态 |
BLOCKED |
线程阻塞,等待获取锁(如synchronized锁) |
线程A持有锁,线程B尝试获取同一把锁,线程B进入BLOCKED |
WAITING |
线程无限期等待,需其他线程唤醒 | 调用object.wait()(无超时)、thread.join()(无超时)后进入此状态 |
TIMED_WAITING |
线程限时等待,超时后自动唤醒 | 调用Thread.sleep(1000)、object.wait(1000)、thread.join(1000)后进入此状态 |
TERMINATED |
线程已终止(run()执行完或抛出未捕获异常) |
线程执行完run()中的逻辑后,状态变为TERMINATED |
4.2 线程状态流转图

-
NEW→RUNNABLE:- 触发条件:调用
start()方法;
- 触发条件:调用
-
RUNNABLE→TERMINATED:- 触发条件:
run()执行完成或抛出未捕获异常;
- 触发条件:
-
RUNNABLE→TIMED_WAITING→RUNNABLE:- 触发条件:调用
sleep(ms)、wait(ms)等限时等待方法,超时后自动唤醒;
- 触发条件:调用
-
RUNNABLE→WAITING→RUNNABLE:- 触发条件:调用
wait()、join()等无限期等待方法,需其他线程调用notify()唤醒;
- 触发条件:调用
-
RUNNABLE→BLOCKED→RUNNABLE:- 触发条件:尝试获取
synchronized锁,但锁被其他线程持有; - 类比:李四需要使用柜台(锁),但柜台被王五占用(王五持有锁),李四只能在柜台外等待(
BLOCKED),王五用完后,李四进入柜台(获取锁,回到RUNNABLE)。
- 触发条件:尝试获取
更多推荐


所有评论(0)