一、什么是线程?先搞懂3个核心概念
在聊线程之前,我们先区分三个容易混淆的概念:进程、线程、并发与并行,这是理解线程的基础
1. 进程与线程的区别
进程:是操作系统中资源分配的最小单位,比如我们打开的每一个Java程序,本质上就是一个JVM进程,每个进程都有独立的内存空间(堆、方法区等),进程之间相互独立,资源消耗较大。
线程:是进程中的执行单元,是CPU调度的最小单位,一个进程中可以包含多个线程,这些线程共享进程的内存空间(堆、方法区),但各自拥有独立的栈空间,资源消耗远小于进程。简单来说,进程是“容器”,线程是“容器里的执行者”。
举个例子:打开微信(一个进程),微信里的聊天、朋友圈、转账功能,就是一个个独立的线程,它们共享微信的内存资源,同时执行不同的任务。
2. 并发与并行
很多人会把这两个概念搞混,其实一句话就能分清:
-
并发:多个线程在同一时间段内交替执行(比如单核CPU,同一时间只能执行一个线程,通过快速切换实现“同时”的效果);
-
并行:多个线程在同一时刻同时执行(比如多核CPU,多个线程可以在不同核心上同时运行)。
Java中的线程调度采用“抢占式调度”,即优先级高的线程会优先获得CPU执行权,优先级相同则随机选择,这也是多线程执行顺序不确定的核心原因。
二、Java中线程的创建方式(3种核心,附代码)
Java提供了3种常用的线程创建方式,各有优劣,实际开发中根据场景选择,咱们逐一讲解,代码可直接复制运行~
方式1:继承Thread类(最基础)
步骤:自定义类继承Thread类,重写run()方法(线程执行的核心逻辑),创建该类实例,调用start()方法启动线程。
注意:不能直接调用run()方法,调用run()只是普通的方法调用,不会启动新线程;只有调用start(),JVM才会创建新线程,执行run()里的逻辑。
// 自定义线程类
class MyThread extends Thread {
// 重写run()方法,定义线程执行逻辑
@Override
public void run() {
for (int i = 0; i < 5; i++) {
// Thread.currentThread().getName() 获取当前线程名称
System.out.println(Thread.currentThread().getName() + " 执行:" + i);
try {
// 线程休眠100ms,模拟任务执行耗时
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 测试类
public class ThreadTest {
public static void main(String[] args) {
// 创建线程实例
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
// 设置线程名称(可选,默认是Thread-0、Thread-1)
thread1.setName("线程1");
thread2.setName("线程2");
// 启动线程
thread1.start();
thread2.start();
}
}
运行结果(顺序不确定,因为CPU调度随机):
线程1 执行:0 线程2 执行:0 线程1 执行:1 线程2 执行:1 ...
方式2:实现Runnable接口(推荐)
步骤:自定义类实现Runnable接口,重写run()方法,创建Runnable实现类实例,将其作为参数传入Thread类构造器,调用start()启动线程。
优势:避免Java单继承的局限性(如果自定义类已经继承了其他类,就无法再继承Thread),更符合“面向接口编程”思想,推荐实际开发中使用。
// 实现Runnable接口
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();
}
}
}
}
// 测试类
public class RunnableTest {
public static void main(String[] args) {
// 创建Runnable实现类实例
MyRunnable runnable = new MyRunnable();
// 将Runnable实例传入Thread构造器,创建线程
Thread thread1 = new Thread(runnable, "线程A");
Thread thread2 = new Thread(runnable, "线程B");
// 启动线程
thread1.start();
thread2.start();
}
}
简化写法(Java8 lambda表达式,无需单独定义实现类):
public class LambdaThreadTest {
public static void main(String[] args) {
// lambda表达式简化Runnable实现
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("线程A 执行:" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "线程A");
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("线程B 执行:" + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "线程B");
thread1.start();
thread2.start();
}
}
方式3:实现Callable接口(带返回值,处理异常)
前两种方式的run()方法没有返回值,也无法抛出checked异常(只能捕获)。如果需要线程执行完成后返回结果,或者需要处理异常,就可以使用Callable接口。
步骤:实现Callable接口(指定返回值类型),重写call()方法(带返回值、可抛异常),创建Callable实例,包装成FutureTask(用于接收返回值),将FutureTask传入Thread构造器,启动线程,通过FutureTask的get()方法获取返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
// 实现Callable接口,指定返回值类型为Integer
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
// 模拟耗时计算
for (int i = 1; i <= 10; i++) {
sum += i;
Thread.sleep(50);
}
// 返回计算结果
return sum;
}
}
// 测试类
public class CallableTest {
public static void main(String[] args) throws Exception {
// 创建Callable实例
MyCallable callable = new MyCallable();
// 包装成FutureTask,用于接收返回值
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// 启动线程
new Thread(futureTask, "计算线程").start();
// 主线程等待线程执行完成,获取返回值(get()方法会阻塞,直到线程执行完毕)
Integer result = futureTask.get();
System.out.println("线程执行结果:1-10的和为 " + result);
}
}
运行结果:线程执行结果:1-10的和为 55
三、Java线程的生命周期(6种状态,必记)
Java线程的生命周期在java.lang.Thread.State枚举中明确定义,共6种核心状态,线程在生命周期中会在不同状态间切换,由JVM或线程自身控制,无法手动直接设置。
6种状态及切换逻辑,结合实际场景理解更简单:
1. 新建状态(NEW)
创建线程对象,但未调用start()方法,此时线程未启动,仅存在对象实例。比如:Thread thread = new Thread();
2. 可运行状态(RUNNABLE)
调用start()方法后,线程进入可运行状态。该状态包含两种情况:一是正在CPU上执行(运行中),二是等待CPU调度(就绪),JVM不区分这两种子状态,统一归为RUNNABLE。
3. 阻塞状态(BLOCKED)
线程因竞争synchronized锁失败而进入阻塞状态,直到获取锁后,重新进入可运行状态。注意:Lock框架(如ReentrantLock)的等待状态不属于BLOCKED,而是属于WAITING/TIMED_WAITING。
4. 等待状态(WAITING)
线程调用wait()、join()(无参)、park()等方法后,进入无时间限制的等待状态,需等待其他线程调用notify()、notifyAll()或unpark()唤醒,才能重新进入可运行状态。
5. 超时等待状态(TIMED_WAITING)
线程调用sleep(long millis)、wait(long timeout)、join(long millis)等方法后,进入有时间限制的等待状态,超时后会自动唤醒,重新进入可运行状态;也可被提前唤醒。
6. 终止状态(TERMINATED)
线程执行完成(run()/call()方法正常结束),或抛出未捕获的异常,线程进入终止状态,状态不可逆,无法再次启动(再次调用start()会报IllegalThreadStateException)。
状态切换核心流程: NEW → (调用start())→ RUNNABLE → (竞争锁失败)→ BLOCKED → (获取锁)→ RUNNABLE → (调用sleep()/wait()等)→ TIMED_WAITING/WAITING → (唤醒/超时)→ RUNNABLE → (执行完毕)→ TERMINATED
四、线程的常用方法(实战高频)
掌握以下常用方法,能应对大部分日常开发场景,重点记清楚方法的作用和注意事项:
1. 启动与执行相关
-
start():启动线程,JVM底层调用start0()方法,创建新线程并执行run(),不能重复调用;
-
run():线程执行的核心逻辑,直接调用只是普通方法,不会创建新线程;
-
currentThread():静态方法,获取当前正在执行的线程实例。
2. 线程控制相关
-
sleep(long millis):静态方法,让当前线程休眠指定毫秒数,休眠期间不释放锁(synchronized),休眠结束后进入可运行状态;
-
yield():静态方法,线程礼让,让出CPU执行权,让其他线程有机会执行,但礼让时间不确定,可能礼让失败(自己再次获得CPU);
-
join():让调用该方法的线程插队,当前线程会阻塞,直到插队线程执行完毕,再继续执行当前线程;
-
interrupt():中断线程,并非直接终止线程,而是设置线程的中断标志,常用于唤醒休眠的线程(会抛出InterruptedException);
-
setName(String name)/getName():设置/获取线程名称,默认名称为Thread-0、Thread-1...
-
setPriority(int priority):设置线程优先级(1-10,默认5),优先级越高,获得CPU的概率越大,但不保证一定先执行。
3. 线程状态相关
-
getState():获取线程当前的状态(返回Thread.State枚举值);
-
isAlive():判断线程是否处于存活状态(NEW和TERMINATED状态为false,其他状态为true)。
五、线程安全问题(新手必避坑)
当多个线程共享同一个资源(比如共享变量、集合、文件等),且同时对资源进行修改时,就可能出现线程安全问题,导致数据错乱。
1. 线程安全问题演示(代码示例)
// 共享资源:计数器
class Counter {
private int count = 0;
// 线程执行的方法:每次加1
public void increment() {
count++; // 看似简单的一行代码,实际包含3步:读取count、加1、写入count
}
public int getCount() {
return count;
}
}
// 测试类:10个线程,每个线程执行1000次加1操作
public class ThreadSafeTest {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
// 创建10个线程
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.increment();
}
}).start();
}
// 等待所有线程执行完毕
Thread.sleep(2000);
// 预期结果:10*1000=10000,实际结果大概率小于10000
System.out.println("最终计数:" + counter.getCount());
}
}
运行结果:最终计数:9876(每次运行结果可能不同,均小于10000),这就是线程安全问题。
2. 解决线程安全问题的3种常用方式
方式1:使用synchronized关键字(同步方法/同步代码块)
核心:保证同一时刻只有一个线程能执行同步代码,实现线程互斥,避免资源竞争。
// 方式1:同步方法(给方法加synchronized)
class Counter {
private int count = 0;
// 同步方法,锁对象是当前Counter实例
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
// 方式2:同步代码块(更灵活,锁对象可以自定义)
class Counter {
private int count = 0;
private final Object lock = new Object(); // 自定义锁对象
public void increment() {
synchronized (lock) { // 锁对象必须是多个线程共享的对象
count++;
}
}
public int getCount() {
return count;
}
}
方式2:使用Lock锁(JDK1.5+,更灵活)
Lock是接口,常用实现类ReentrantLock(可重入锁),比synchronized更灵活,支持手动获取锁、释放锁,还支持中断、超时等特性。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
// 创建Lock锁实例
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 手动获取锁
try {
count++; // 核心逻辑
} finally {
lock.unlock(); // 手动释放锁,必须放在finally中,避免死锁
}
}
public int getCount() {
return count;
}
}
方式3:使用线程安全的容器/工具类
Java提供了一些线程安全的容器,比如Vector(ArrayList的线程安全版)、ConcurrentHashMap(HashMap的线程安全版),还有AtomicInteger(原子类,用于原子操作),无需手动加锁,直接使用即可。
import java.util.concurrent.atomic.AtomicInteger;
// 使用AtomicInteger解决计数安全问题
class Counter {
// 原子类,保证count的操作是原子性的(不可分割)
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子性的加1操作
}
public int getCount() {
return count.get();
}
}
六、常见面试题(高频)
整理了几个线程相关的高频面试题,结合本文知识点就能轻松回答:
-
Thread类的start()和run()方法的区别? 答:start()启动新线程,JVM调用run();run()只是普通方法,直接调用不会创建新线程。
-
synchronized和Lock的区别? 答:synchronized是关键字,自动释放锁;Lock是接口,手动获取/释放锁,更灵活,支持中断、超时。
-
Java线程的生命周期有哪些状态? 答:新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超时等待(TIMED_WAITING)、终止(TERMINATED)。
-
什么是线程安全?如何解决线程安全问题? 答:多个线程共享资源时,数据不会错乱就是线程安全;解决方式:synchronized、Lock、原子类、线程安全容器。
-
sleep()和wait()的区别? 答:sleep()是静态方法,不释放锁,休眠时间到自动唤醒;wait()是实例方法,释放锁,需手动唤醒(notify()/notifyAll())。
七、总结
线程是Java多任务和高并发的基础,本文从概念、创建方式、生命周期、常用方法、线程安全五个核心维度,结合代码示例讲解了Java线程的核心知识点,新手可以先掌握前两种线程创建方式和synchronized的使用,再逐步深入学习Lock、原子类、线程池等高级内容。
如果觉得本文对你有帮助,欢迎点赞、收藏,也可以在评论区留言讨论你遇到的线程问题~

1938

被折叠的 条评论
为什么被折叠?



