本文开始,我们进入Java多线程模块。
现代计算机的CPU基本都是多个核心,每个核心都可以独立执行计算任务。
所谓的多线程编程,就是允许我们的程序创建多个执行流(线程),然后把它们分配到不同的CPU核心上同时运行,从而充分利用硬件资源,提高程序的响应速度和处理能力。
一、基本概念
在学习Java的多线程之前,我们先弄清楚一些基本的概念。
进程:
操作系统资源分配的最小单位,拥有独立的内存空间。一个程序运行起来就是一个进程。
你在电脑上双击wechat.exe启动微信的时候,系统就创建了一个微信进程。这个进程有自己的内存空间和资源。
线程:
线程是进程中的执行单元,同一个进程可以有很多个线程,这些线程共享内存资源,用来同时执行多个任务。
微信里面一个线程负责收消息,另外一个线程可能负责渲染聊天界面,他们都是属于同一个微信进程的。
虚拟线程:
JDK21正式引入了虚拟线程这个类似协程的机制。虚拟线程由Java自己调度,不需要操作系统的管理。
大家都在微信上发消息、看朋友圈、登录,如果微信服务器给每个用户分配一个轻量的虚拟线程,就不会像传统的线程那样,每个用户都占用一个系统线程,资源就不会那么紧张。
并发:
多个任务在同一时间段交替执行,提高效率,看起来是同时进行,但实际上可能是轮流执行。
你在微信上发语音的同时,微信还能加载朋友圈,其实两个操作在一个线程里交替完成。
并行:
多个任务在同一时刻真正同时执行,依赖于多核CPU支持。
打开微信和打开浏览器两个进程分别运行在不同CPU核心上,彼此互不干扰,同时运行。
二、创建线程
接下来我们看一下在Java中如何创建线程。
2.1 继承Thread
这是最直观的创建线程的方式。Thread类是Java提供的用来创建和控制线程的核心类。
他代表的就是一个独立执行的任务单元。
java
package com.lazy.snail.day35;
/**
* @ClassName Day35Demo
* @Description TODO
* @Author lazysnail
* @Date 2025/7/17 10:38
* @Version 1.0
*/
public class Day35Demo {
public static void main(String[] args) {
System.out.println("主线程开始...");
MyThread thread1 = new MyThread();
thread1.setName("我的线程");
thread1.start();
System.out.println("主线程结束。");
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程 " + Thread.currentThread().getName() + " 正在运行: " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
MyThread是我自定义的线程类,继承了Thread。
这个类中只做了一件事情,重写run方法。这个方法中的内容其实就是你想让线程帮你执行的逻辑。
在main方法中创建了自定义线程的对象,通过对象调用start方法,那么这个线程就启动了。
初学者容易把run方法和start方法搞混。记住run方法是你需要交给线程执行的任务逻辑。
start方法是启动线程,相当于激活这个线程。
如果你直接用线程对象调用了run方法,像这样:thread1.run。
就不会创建新的线程去执行你的逻辑,只是在当前线程中执行run()方法体,变成了普通的同步方法调用。

正常启动线程的输出

直接调用了run方法的输出
两者输出的对比,后者其实是按照代码顺序执行的。前者主线程和新启动的线程独立执行。
main方法是一个独立的线程,当你在main方法某处创建启动了新的线程,这个新线程在某一时刻也会执行自己的逻辑。 正常启动线程的输出也不是一成不变的,你的机器上或者你多运行几次,可能输出的顺序就会发生改变。因为线程的调度权在操作系统手里。操作系统会根据系统负载,CPU状态等等情况合理的调度线程。我们是没办法预测多线程精确的执行顺序的。
2.2 实现Runnable
我们都知道Java有单继承的限制,这在某些情况下就不够灵活。
所以在有需要的情况下,我们一般都会选择实现Runnable的方式。
java
package com.lazy.snail.day35;
/**
* @ClassName Day35Demo2
* @Description TODO
* @Author lazysnail
* @Date 2025/7/17 11:19
* @Version 1.0
*/
public class Day35Demo2 {
public static void main(String[] args) {
System.out.println("主线程开始...");
MyRunnable task = new MyRunnable();
Thread thread1 = new Thread(task, "我的线程");
thread1.start();
System.out.println("主线程结束。");
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("子线程 " + Thread.currentThread().getName() + " 正在运行: " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
MyRunnable实现Runnable接口,类中也只做了一件事情,就是实现run方法。
在main方法中,需要创建一个Thread实例,然后把Runnable实现类的实例喂给他。
启动线程一样是通过线程实例的start方法。
这种写法的好处就在于把要执行的任务(Runnable)和执行任务的载体(Thread)解耦了。
如果你去翻看过Thread的源码,你会发现Thread类也实现了Runnable接口

从本质上来说,Thread是用来构建线程的,Runnable是用来包装任务的。
三、线程的生命周期
生命周期就是一个事物从诞生到消亡的过程中所经历的一系列阶段。
线程从创建到终止会经历不同的状态。
对于开发者来说,这些状态可以让我们清晰精准的控制程序的运行。
对于操作系统和虚拟机来说,这些状态可以让他们合理的调度CPU资源。

Thread.State是Thread类中的线程状态枚举。

在Java层面,线程的状态被抽象成了6个:
NEW:新建,new Thread()之后,线程对象已经创建,但是还没调用start()方法。
RUNNABLE:可运行,调用start()方法后,线程就准备就绪了,等待CPU的调度。这个状态包含了我们通常理解的正在运行和准备运行两种状态。
BLOCKED:阻塞,线程在等待锁资源,比如想要进入一个synchronized块,此时锁被其他线程持有,只能等着。
WAITING:无限等待,线程无限期的等待另一个线程的通知。wait()方法和join()方法都可以让线程记入WAITING状态。
TIMED_WAITING:限时等待,在指定时间后会自动唤醒的等待状态。sleep(long)、wait(long)、join(long)等方法可以让线程进入这个状态。
TERMINATED:终止,线程的run()方法执行完或因为异常退出,线程生命周期就结束了。
这几个状态是Java虚拟机在Java层面提供的逻辑视图,主要是方便我们在写多线程代码的时候更好的理解和控制线程。
在操作系统层面,比如Linux中,他更关心的是调度和资源的控制。
Running:正在CPU上执行。
Runnable (Ready):可运行,等待被调度。
Blocked/Sleeping:因等待资源、IO、锁等而挂起。
Zombie:已退出,但父进程尚未回收。
Stopped:被外部信号暂停。
这几个状态是Linux操作系统调度器和内核真实管理线程时使用的状态。
二者对比,虽然不完全等价,但是存在一定的映射关系。
一个大致的映射关系
| Java线程状态 | 操作系统线程可能状态(粗略) |
|---|---|
| NEW | 不存在(还未调用start()) |
| RUNNABLE | Ready/Running(可运行或正在运行) |
| BLOCKED | Sleeping(等待锁) |
| WAITING | Sleeping(无限等待,需其他线程唤醒) |
| TIMED_WAITING | Sleeping(限时挂起) |
| TERMINATED | Zombie(Java层已销毁,可能系统层还未清理) |
Java中的RUNNABLE包括了操作系统的Ready + Running,没有细分。
四、Thread类的常用方法
Thread类是学习Java多线程的一个基础,他是创建、启动、管理线程的核心类,几乎所有多线程操作的入口都离不开它。
Thread对象内部包装了一个原生操作系统线程(JVM映射),是Java抽象线程和系统调度机制之间的桥梁。
后续的线程池、虚拟线程、并发工具类等等的学习都需要建立在对Thread机制的理解之上。
下面我们一起看下Thread类中一些常见的方法:
start()
这个方法用来启动线程,触发 run() 方法执行。只有调用start(),线程才会真正并发执行。
java
Thread t = new Thread(() -> {
System.out.println("子线程正在运行...");
});
t.start();
需要注意的是start()只能调用一次;重复调用会抛出IllegalThreadStateException。
它会在新的线程中执行run(),不要手动调用run()。
run()
线程启动之后执行的实际逻辑代码,要么是自己覆写的,要么是通过Lambda传入的。
java
Thread t = new Thread(() -> {
System.out.println("这是run()方法里的代码");
});
t.start();
run()本身不具有并发性,直接调用run()只是普通方法执行,不会启动线程。
sleep(long millis)
让当前线程休眠指定毫秒数,不释放锁。
java
System.out.println("开始");
Thread.sleep(1000);
System.out.println("1秒后执行");
Thread.sleep()休眠的是当前的线程,不是目标线程。
会抛出InterruptedException,需要捕获或者抛出。
不会释放资源,容易出现并发问题。
join()
等待另一个线程执行完成后再继续执行当前线程。
java
Thread t = new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println("子线程执行完毕");
} catch (InterruptedException e) {}
});
t.start();
t.join();
System.out.println("主线程继续执行");
主线程等待子线程执行完成。
这个方法会让当前线程进入WAITING状态。
也可以通过join(long timeout)方法,让线程进入TIMED_WAITING状态。
interrupt()
中断线程的休眠或等待状态,这个方法不会强制停止线程,只是设置一个中断标志。
java
Thread t = new Thread(() -> {
try {
Thread.sleep(5000);
System.out.println("醒来继续执行");
} catch (InterruptedException e) {
System.out.println("被中断了!");
}
});
t.start();
Thread.sleep(1000);
t.interrupt();
这个方法对处于sleep()、wait()、join()状态的线程有效,会抛出InterruptedException。
对于正常运行中的线程,只是设置一个标志位,要手动检查:
java
while (!Thread.currentThread().isInterrupted()) { ... }
isAlive()
判断线程是否仍处于活动状态(已启动但还没终止)。
java
Thread t = new Thread(() -> {});
System.out.println(t.isAlive());
t.start();
System.out.println(t.isAlive());
setPriority(int newPriority)
设置线程的优先级(范围是1~10,默认是5)。
java
t.setPriority(Thread.MAX_PRIORITY);
JVM不保证高优先级线程一定先执行,只是作为调度建议。
在现代操作系统下基本没什么作用,不推荐依赖它来做控制。
getName()/setName(String name)
获取或设置线程的名称,主要是用来输出日志和调试。
java
Thread t = new Thread(() -> {});
t.setName("我的线程");
System.out.println(t.getName());
currentThread()
用来获取当前正在执行的线程对象。
java
Thread t = Thread.currentThread();
System.out.println("当前线程:" + t.getName());
setDaemon(boolean on)
把线程设置成守护线程。
当JVM里只剩下守护线程的时候,JVM会直接退出,不会等待守护线程执行完。
像日志记录、监控这种后台服务比较常用。
垃圾回收线程(GC)就是一个守护线程。
java
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true) {
System.out.println("守护线程运行中...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {}
}
});
t.setDaemon(true);
t.start();
try {
Thread.sleep(1000);
System.out.println("主线程结束");
} catch (InterruptedException e) {}
}
主线程结束之后,JVM不会管守护线程,守护线程的while循环也不再执行了。
结语
今天讲了一些Java多线程的基础知识,这些都是并发程编程的基础。
并发编程也不是什么高大上或者进阶的东西,可以把他看成一个简单的模块。
掌握多线程要转变一下思维,之前都是独自按顺序完成任务,现在把任务分发给更多的人一起完成。
怎么让这些人按照有序的规则,高效的协作,共同完成任务。
这些规则其实就是我们关心的,我们需要掌握的,一旦掌握了,那么你写的代码就是安全、高效的。
下一篇预告
Day36 | Java中的线程池技术
如果你觉得这系列文章对你有帮助,欢迎关注专栏,我们一起坚持下去!
更多文章请关注我的公众号《懒惰蜗牛工坊》