一、线程创建方式(两种写法,解决“怎么开启多任务”问题 )
Java 里想让程序同时干多件事(比如一边下载、一边播放音乐 ),就得用多线程。而创建线程,最基础的就是继承 Thread 类和实现 Runnable 接口这两种方式,本质都是定义“线程要做的事(run() 方法 )”,再启动线程让它执行。
1. 继承 Thread 类方式
// 第一步:自定义类继承 Thread,重写 run() 方法
class MyThread extends Thread {
// run() 里写的是“这个线程要执行的任务”
@Override
public void run() {
// 这里简单打印线程名称,实际可以是复杂逻辑(比如文件下载、数据处理等)
System.out.println("线程执行:" + Thread.currentThread().getName());
}
}
// 第二步:在 main 方法(主线程)里启动线程
public class Main {
public static void main(String[] args) {
// 创建自定义线程对象
MyThread t1 = new MyThread();
// 启动线程!注意:必须用 start(),不能直接调 run()!
// start() 会让 JVM 给线程分配资源(比如栈空间),然后自动调用 run() 执行任务
// 如果直接调 run(),就和普通方法调用一样,不会开启新线程,还是在主线程里执行
t1.start();
}
}
- 关键点:
- 继承 Thread 后,run() 是线程的“任务入口”,JVM 会在启动线程后自动执行它。
- start() 是真正“启动新线程”的关键方法,它会触发 JVM 的线程调度机制,给线程分配 CPU 时间片。
- 缺点:Java 是单继承,要是你的类已经继承了其他类(比如 MyClass extends OtherClass ),就没法再继承 Thread 了,所以这种方式灵活性稍差。
2. 实现 Runnable 接口方式
// 第一步:自定义类实现 Runnable 接口,重写 run() 方法
class MyRunnable implements Runnable {
@Override
public void run() {
// 同样,这里定义线程要做的事,比如打印线程名
System.out.println("线程执行:" + Thread.currentThread().getName());
}
}
// 第二步:在 main 方法里,用 Thread 类“包装” Runnable 对象,再启动
public class Main {
public static void main(String[] args) {
// 创建 Runnable 任务对象(只是定义了任务,还没线程执行它)
MyRunnable runnableTask = new MyRunnable();
// 把任务丢给 Thread,让 Thread 来管理线程的启动、调度
Thread t2 = new Thread(runnableTask);
// 启动线程,和之前一样,start() 会触发 JVM 执行 run()
t2.start();
}
}
- 关键点:
- Runnable 是个接口,只规定了 run() 方法,本身不涉及线程的“启动”能力。所以必须借助 Thread 类的构造方法,把 Runnable 任务“包装”进去,才能启动线程。
- 优点:解决了单继承限制!比如MyRunnable 还能继承其他类(像 class MyRunnable extends OtherClass implements Runnable ),更灵活;而且,多个线程可以共享同一个 Runnable 对象(比如多个线程一起处理同一个任务队列 )。
举个共享的例子:
MyRunnable sharedTask = new MyRunnable();
// 线程 A 和 线程 B 共享同一个任务对象
Thread tA = new Thread(sharedTask, "线程A");
Thread tB = new Thread(sharedTask, "线程B");
tA.start();
tB.start();
这样,线程 A 和 B 会基于同一个 sharedTask 执行任务,适合处理“多个线程协作完成同一份工作”的场景(比如共同处理一个计数器、一个文件写入操作 )。
二、线程的生命周期(理解线程“一生”的状态变化 )
线程从创建到销毁,会经历一系列状态变化,图里总结的流程是:
新建(New)→ 可运行(Runnable)→ 运行(Running)→(阻塞/等待/超时等待)→ 可运行 → 运行 → 终止(Terminated)
1. 新建(New)
- 场景:刚用 new 创建线程对象,还没调用 start() 的时候。比如 MyThread t1 = new MyThread(); 这一步,线程只是个“空壳”,JVM 还没给它分配真正的线程资源(比如栈、程序计数器等 )。
- 此时程序没有任何变化,必须调 start() 才会进入下一步。
2. 可运行(Runnable)
- 场景:调用 start() 后,线程进入这个状态。此时,线程已经“准备好执行了”,但得等 CPU 调度(JVM 有个“线程调度器”,决定哪个线程先执行 )。
- 注意:“可运行”不代表真的在执行,只是在“就绪队列”里排队,等 CPU 时间片。比如电脑同时开了很多程序,CPU 同一时间只能干一件事(多核 CPU 可以并行,但调度逻辑更复杂 ),所以得排队。
3. 运行(Running)
- 场景:CPU 选中了这个线程,开始执行 run() 方法里的代码,真正“干活”的状态。
- 能持续多久:取决于 CPU 时间片!时间片用完,线程会回到可运行状态,重新排队;如果 run() 里的代码很快执行完(比如就打印一句话 ),也会直接进入终止状态。
4. 阻塞/等待/超时等待(Blocked / Waiting / Timed Waiting)
这三类状态都是线程“暂停执行”的情况,原因不同,处理方式也不同:
-
阻塞(Blocked):
-
场景:线程想进入 synchronized 修饰的代码块/方法,但锁被其他线程占用了,就会进入阻塞状态,一直等到锁释放。
-
例子: 线程 B 会卡在进入 synchronized 的地方,直到线程 A 释放锁。
synchronized (lock) {
// 假设线程 A 先拿到锁,线程 B 想进来就会阻塞
}
-
-
等待(Waiting):
-
场景:线程调用了 wait()(必须在 synchronized 里调用 )、join() 等方法,会进入“无限期等待”,直到被其他线程唤醒。
-
例子:
synchronized (lock) {
// 线程调用 wait(),释放锁,进入等待
lock.wait();
// 必须等其他线程调用 lock.notify() 或 notifyAll(),才会被唤醒,回到可运行状态
}
-
-
超时等待(Timed Waiting):
-
场景:线程调用了 sleep(long 毫秒)、wait(long 毫秒)、join(long 毫秒) 等方法,会进入“有限期等待”,到时间自动唤醒,或者提前被其他线程唤醒。
-
例子: 或者:
// 线程睡 3 秒,期间不占用 CPU,到时间自动回到可运行状态
Thread.sleep(3000);synchronized (lock) {
// 最多等 5 秒,要是没人唤醒,到时间也会自己醒
lock.wait(5000);
}
-
-
回到可运行状态:
不管是阻塞、等待还是超时等待,只要“阻碍因素消失”(比如锁释放了、被其他线程唤醒了、超时时间到了 ),线程就会回到 可运行(Runnable) 状态,重新排队等 CPU 调度。
5. 终止(Terminated)
- 场景:run() 方法执行完毕(正常跑完所有代码 ),或者线程被强制中断(比如抛了未捕获的异常 ),线程就会进入终止状态,生命周期结束,不能再重启。
- 注意:线程终止后,就算调用 start() 也没用,会报错!线程对象一旦终止,就“报废”了,想再执行任务,得重新 new 一个。
三、线程常用方法
这些方法是 Thread 类或 Object 类(wait、notify 等是 Object 的方法 )提供的,用来控制线程的状态、行为,解决“线程之间怎么配合、怎么调度”的问题。
start() | 启动线程!让 JVM 为线程分配资源(栈、PC 等 ),并把线程放入“可运行”队列,等待 CPU 调度。必须用这个方法启动线程,不能直接调 run()(直接调 run() 就是普通方法调用,不会开新线程 )。 | 线程只能 start() 一次,第二次调会抛 IllegalThreadStateException 异常。 |
run() | 线程的“任务逻辑”入口,JVM 会在 start() 后自动调用它。你在这方法里写线程要做的事(比如循环、IO 操作等 )。 | 直接调用 run() 不会开新线程,就是普通方法执行,谨慎使用! |
sleep(long millis) | 让当前线程“休眠”指定毫秒数,不释放锁(如果当前线程拿着锁,sleep 期间锁不会放 ),到时间后回到“可运行”状态。 | 静态方法,直接 Thread.sleep(1000); 调用;可能会抛出 InterruptedException(比如休眠时被其他线程中断 ),需要处理异常。 |
join() | 让当前线程等待另一个线程执行完毕。比如线程 A 调用 threadB.join(),线程 A 会进入“等待”,直到 threadB 的 run() 执行完,线程 A 才会继续跑。 | 常用来“协调线程执行顺序”,比如主线程等子线程跑完再汇总结果;也会抛 InterruptedException。 |
wait() / notify() / notifyAll() | – wait():让当前线程释放锁,进入“等待”状态,必须在 synchronized 代码块/方法里调用。<br>- notify():唤醒一个等待在同一个锁上的线程(随机选一个 )。<br>- notifyAll():唤醒所有等待在同一个锁上的线程。 | 这三个方法都是 Object 类的方法(因为锁可以是任意对象 ),必须在 synchronized 里用,否则抛 IllegalMonitorStateException 异常。 |
interrupt() | “中断”线程,但不是强制终止(别理解成杀死线程 )。会设置线程的“中断标志位”,如果线程在 sleep、wait、join 等方法里,会抛出 InterruptedException,让线程感知到“被中断”,然后自己决定怎么处理(比如提前结束任务 )。 | 只是打个“中断标记”,具体怎么响应中断,得看线程内部逻辑。比如:<br>java<br>if (Thread.currentThread().isInterrupted()) {<br> // 自己处理中断,比如结束循环、返回结果<br>}<br> |
setPriority(int priority) | 设置线程的优先级(1 – 10,默认 5 ),只是给线程调度器“建议”,不保证优先级高的线程一定先执行。 | 优先级高的线程被 CPU 选中的概率大一些,但别依赖它控制执行顺序(不同系统调度策略不同 )。 |
yield() | 让当前线程“让出”CPU 资源,回到“可运行”状态,重新和其他线程一起竞争 CPU。只是建议,调度器可能不理会。 | 常用来“给优先级低的线程机会”,但实际效果看 JVM 心情,别当精确控制手段。 |
四、线程同步与锁(解决“多线程抢资源”问题,避免数据混乱 )
多线程同时访问共享资源(比如同一个变量、同一个文件 )时,容易出现“数据竞争”,导致结果混乱。线程同步就是解决这个问题的,核心思路是让多个线程“有序访问”共享资源,常用 synchronized 和 Lock 接口两种方式。
1. synchronized 关键字(内置锁,简单直接 )
synchronized 可以修饰方法或代码块,保证同一时间只有一个线程能进入“被保护的区域”,从而避免多线程冲突。
(1)修饰代码块(指定锁对象 )
class BankAccount {
private int balance = 1000;
// 自定义锁对象(也可以用 this、字节码对象等当锁,看需求 )
private final Object lock = new Object();
public void withdraw(int amount) {
// 同步代码块:只有拿到 lock 锁的线程,才能进这个块
synchronized (lock) {
if (balance >= amount) {
balance -= amount;
System.out.println(
Thread.currentThread().getName() + " 取款 " + amount + ",余额:" + balance
);
}
}
}
}
- 原理:线程想进入 synchronized (lock) 代码块,必须先拿到 lock 对象的“锁”。如果锁被其他线程占用,当前线程就会进入阻塞(Blocked) 状态,直到锁释放。这样就保证了,同一时间只有一个线程能修改 balance,避免多线程同时减余额导致数据错误。
(2)修饰方法(隐式锁对象 )
-
修饰实例方法:
public synchronized void withdraw(int amount) {
// 锁对象是 this(当前实例)
if (balance >= amount) {
balance -= amount;
}
}相当于 synchronized (this) { … },锁是当前对象实例。
-
修饰静态方法:
public static synchronized void withdraw(int amount) {
// 锁对象是 类的字节码对象(比如 BankAccount.class )
}相当于 synchronized (BankAccount.class) { … },锁是类对象。
-
优缺点:
优点:简单易用,JVM 自动管理锁的获取和释放(进入同步块自动拿锁,退出自动放锁,包括抛异常的情况 )。
缺点:不够灵活(比如想尝试获取锁、设置超时时间,synchronized 做不到 );如果同步范围太大,会影响性能(多个线程排队太久 )。
2. Lock 接口(显式锁,更灵活 )
java.util.concurrent.locks.Lock 是更灵活的锁机制,常用实现类是 ReentrantLock(可重入锁,和 synchronized 类似,同一线程可以多次拿锁 )。
基本用法:
class BankAccount {
private int balance = 1000;
// 创建 Lock 对象,常用 ReentrantLock
private Lock lock = new ReentrantLock();
public void withdraw(int amount) {
// 手动加锁
lock.lock();
try {
// 临界区:操作共享资源的代码
if (balance >= amount) {
balance -= amount;
System.out.println(
Thread.currentThread().getName() + " 取款 " + amount + ",余额:" + balance
);
}
} finally {
// 手动释放锁!必须在 finally 里放,否则一旦代码抛异常,锁永远不会释放,导致死锁
lock.unlock();
}
}
}
- 原理:和 synchronized 类似,lock() 会尝试获取锁,拿到锁的线程才能执行临界区代码;执行完后,必须在 finally 里调用 unlock() 释放锁,保证锁一定能释放(即使代码抛异常 )。
优点(对比 synchronized ):
-
更灵活:
- 可以尝试获取锁(tryLock() ),拿不到锁可以不等待,去做别的事;
- 可以设置超时时间(tryLock(long time, TimeUnit unit) ),避免线程无限阻塞;
- 可以实现更复杂的锁逻辑(比如读写锁 ReentrantReadWriteLock,读锁共享、写锁独占,适合读多写少的场景 )。
-
可中断:
lockInterruptibly() 方法允许在获取锁的过程中响应中断(比如等待锁时,其他线程调用了 interrupt(),可以提前放弃等待
评论前必须登录!
注册