09多线程
基本概念
程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process):是程序的一次执行过程,或是正在运行的一个程序。
- 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域。
- 是一个动态的过程:有它自身的产生、存在和消亡的过程,称为生命周期。
线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。
- 若一个进程同一时间并行执行多个线程,就是支持多线程的。
- 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
- 一个进程中的多个线程共享相同的内存单元/内存地址空间→它们从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
并行:多个任务在同一时刻由多个处理器或核心同时执行,强调的是物理上的同时性。例如,在多核处理器上,两个不同的程序或线程可以真正地同时运行,各自占用一个核心。
并发:多个任务在逻辑上交错执行,可能在同一个时间段内交替进行,但不一定同时发生。它更关注的是任务的调度和管理,使得系统能够高效处理多个任务,即使在单核处理器上也能实现“看似同时”运行的效果。
实现方式
Java 中创建线程主要有以下方式:
继承
Thread类:重写run()方法,然后调用start()启动线程。实现
Runnable接口:将任务与线程分离,更灵活,推荐使用。实现
Callable接口:配合FutureTask使用,可以获取线程执行结果或抛出异常。线程池:通过
ExecutorService管理线程,避免频繁创建和销毁线程。
继承Thread类
执行步骤
- 创建一个继承Thread类的子类
- 重写Thread类的run()方法——此线程执行的操作执行在run()方法中
- 创建一个Thread类子类的对象
- 通过此对象调用start()方法
public class StartThread extends Thread { // 重写 run() 方法,定义线程执行的具体任务 @Override public void run() { // 输出当前线程的运行状态信息 System.out.println("线程1运行中"); } // 程序入口点,主方法 public static void main(String[] args) { // 创建 StartThread 类的实例对象 StartThread st = new StartThread(); // 调用 start() 方法,启动新线程,系统会自动调用 run() 方法 st.start(); // 主线程继续执行,输出另一条信息 System.out.println("线程2运行中"); } }执行结果说明
st.start()启动一个独立的线程(线程1);调用当前线程的run()方法。- 主线程(线程2)在
start()调用后立即继续执行后续代码,输出“线程2运行中”。 - 由于线程调度的不确定性,输出顺序可能为:
- 线程1运行中
线程2运行中
或 - 线程2运行中
线程1运行中
- 线程1运行中
注意事项
- 不应直接调用
run()方法,否则不会创建新线程,而是在主线程中同步执行。 - 正确方式是使用 start(),由 JVM 调度并创建新的执行路径。
- 每个线程对象只能启动一次,重复调用
start()会抛出IllegalThreadStateException。可以重新创建一个线程对象再调用。
线程的调度
调度策略
- 时间片
- 抢占式:高优先级的线程抢占CPU
调度方法
- 同优先级线程组成先进先出队列,使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
线程的优先级等级
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5——默认优先级
涉及的方法
getPriority():返回线程优先值
setPriority(int newPriority):改变线程的优先级
public class TestThread { public static void main(String[] args) { MyThread1 t1 = new MyThread1(); MyThread2 t2 = new MyThread2(); t1.setPriority(Thread.MAX_PRIORITY); t2.setPriority(Thread.MIN_PRIORITY); t1.setName("线程一"); t2.setName("线程二"); t1.start(); t2.start(); } static class MyThread1 extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i); } } } } static class MyThread2 extends Thread { @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 != 0) { System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i); } } } } }高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
实现Runnable接口
执行步骤
- 创建一个实现
Runnable接口的类 - 在该类中实现
Runnable接口中定义的抽象方法run() - 创建该实现类的实例对象
- 将该对象作为参数传入
Thread类的构造器,创建一个Thread对象 - 通过
Thread对象调用start()方法
// 1. 创建一个实现 Runnable 接口的类 class MyThread implements Runnable { // 2. 实现 Runnable 接口中的抽象方法 run() @Override public void run() { for (int i = 0; i < 100; i++) { if (i % 2 == 0) { // 因未继承 Thread 类,无法直接调用 getName(),需通过 Thread.currentThread() 获取当前线程名称 System.out.println(Thread.currentThread().getName() + ":" + i); } } } } public class ThreadTest1 { public static void main(String[] args) { // 3. 创建实现类的对象 MyThread mt = new MyThread(); // 4. 将该对象作为参数传递给 Thread 构造器,创建 Thread 对象 Thread t1 = new Thread(mt); t1.setName("线程一"); // 5. 调用 start() 方法:① 启动线程;② 执行当前线程的 run() —— 实际调用的是 Runnable 目标对象的 run() t1.start(); // 启动第二个线程,同样遍历 100 以内的偶数 Thread t2 = new Thread(mt); t2.setName("线程二"); t2.start(); } }比较创建线程的两种方式
| 方式 | 类的身份 | 是否有 start () | 使用方式 |
|---|---|---|---|
extends Thread | 既是线程对象,又是任务 | ✅ 继承得到 start () | 对象.start () |
implements Runnable | 仅仅是任务,不是线程 | ❌ 没有 start () | new Thread (任务).start () |
Thread本身就是线程对象,它里面包含start()方法用来创建操作系统线程。Runnable只是任务,只包含要执行的run(),本身不是线程,必须交给Thread对象来启动。
优先选择实现Runnable接口的方式,原因如下
- 实现的方式没有类的单继承性的局限性
- 实现的方式更适合来处理多个线程有共享数据的情况。
线程的生命周期
新建(New)
线程对象被创建,但尚未启动。此时线程仅存在于内存中,未与操作系统线程关联,也未开始执行任何任务。例如:Thread thread = new Thread();。
可运行(Runnable)
线程已调用start()方法,进入可运行状态。此时线程由操作系统调度,具备运行的资格,但是否正在运行取决于系统资源分配。该状态包括就绪和运行两个子状态。
运行中(Running)
线程已被操作系统选中并正在执行其run()方法中的代码。这是线程实际执行任务的阶段,属于可运行状态的一部分。
阻塞(Blocked)
线程因等待某个条件而暂停执行,无法继续运行。常见原因包括:
- 等待获取锁(如进入
synchronized代码块或方法); - 调用
wait()、sleep()、join()等方法; - 等待 I/O 操作完成。
在此状态下,线程不占用处理器时间,但保持在内存中,等待外部事件触发。
终止(Terminated)
线程执行完毕,或因异常退出,或被强制终止(如调用stop(),虽不推荐)。线程不再参与调度,其资源被系统回收,生命周期结束。此时线程对象仍存在,但不可再次启动。
线程安全
线程安全性是指在多线程环境下,程序的行为符合预期,不会因为多个线程同时访问共享资源而产生错误结果。一个线程安全的类或方法能够在并发访问时保持数据的一致性和正确性,不出现竞态条件(race condition)、脏读、死锁等异常行为。
举例
创建三个窗口卖票,总票数为100张,使用实现Runnable接口的方式。
- 问题:卖票过程中,出现了重票、错票即出现了线程的安全问题。
- 问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
- 如何解决:当一个线程a在操作ticket的时候,其他线程不能所以你参与进来。直到线程a操作完ticket时,线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
class Window implements Runnable { private int ticket = 100; @Override public void run() { while (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; } } } public class WindowTest1 { public static void main(String[] args) { Window window = new Window(); Thread t1 = new Thread(window, "窗口一"); Thread t2 = new Thread(window, "窗口二"); Thread t3 = new Thread(window, "窗口三"); t1.start(); t2.start(); t3.start(); } }执行结果
线程二:卖票,票号为:100 窗口一:卖票,票号为:100 线程三:卖票,票号为:100 窗口一:卖票,票号为:97 线程三:卖票,票号为:97 线程二:卖票,票号为:97 窗口一:卖票,票号为:94同步机制
方法一:同步代码块
synchronized(同步监视器){ //需要被同步的代码 }
- 操作共享数据的代码,即为需要被同步的代码。
- 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
- 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。多个线程必须要共用同一把锁。
- 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
- 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当监视器。
- 同步的方式,解决了线程的安全问题。但操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。
使用 synchronized 关键字实现线程安全
实现Runnable接口
class Window implements Runnable { private int ticket = 100; private final Object lock = new Object(); @Override public void run() { while (true) { synchronized (lock) { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } System.out.println(Thread.currentThread().getName() + ": 售出票号为 " + ticket); ticket--; } else { break; } } } } }继承Thread类
class Window1 extends Thread { private static int ticket = 100; private static final Object obj = new Object(); @Override public void run() { while (true) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj) { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket); ticket--; } else { break; } } } } } public class TestThread02 { public static void main(String[] args) { Window1 w1 = new Window1(); Window1 w2 = new Window1(); Window1 w3 = new Window1(); w1.setName("窗口一"); w2.setName("窗口二"); w3.setName("窗口三"); w1.start(); w2.start(); w3.start(); } }| 代码特征 | 是否超卖 | 运行现象 |
|---|---|---|
| sleep 在锁内 | 否 | 单窗口连续卖票 |
| sleep 在锁外 + 锁内 if 判断 | 否 | 窗口交替卖票(标准 demo) |
| 只有锁,无 sleep | 否 | 大概率单窗口一次性卖完 |
| while 外层判断 ticket>0,锁内没有 if | ✅可能超卖 | 最后卖出负数票 |
sleep(long ms):让当前线程休眠指定毫秒,休眠期间,线程不会释放已经持有的锁!(最重要!)- sleep 是静态方法:
Thread.sleep(),作用的是当前正在执行这行代码的线程,不是别的线程。 - sleep 会抛出受检异常
InterruptedException,必须 try-catch 或者 throws。 - 休眠时间只是最小等待时间,时间到了线程进入就绪状态,不一定立刻拿到 CPU 执行。
sleep 只管睡觉,不放手锁;锁里面尽量不要写 sleep。
方法二:同步方法
实现Runnable接口
class Window3 implements Runnable { private int ticket = 100; @Override public void run() { while (true) { try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断状态 break; } if (!show()) { break; } } } private synchronized boolean show() { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket); ticket--; return true; } else { return false; } } } public class WindowTest3 { public static void main(String[] args) { Window3 window = new Window3(); Thread t1 = new Thread(window, "窗口一"); Thread t2 = new Thread(window, "窗口二"); Thread t3 = new Thread(window, "窗口三"); t1.start(); t2.start(); t3.start(); } }继承Thread类
class Window4 extends Thread { private static int ticket = 100; @Override public void run() { while (true) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } if (!show()) { break; } } } public static synchronized boolean show() { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + " 卖票,票号为:" + ticket); ticket--; return true; } else { return false; } } } public class WindowTest4 { public static void main(String[] args) { Window4 w1 = new Window4(); Window4 w2 = new Window4(); Window4 w3 = new Window4(); w1.setName("窗口一"); w2.setName("窗口二"); w3.setName("窗口三"); w1.start(); w2.start(); w3.start(); } }
- 同步方法仍涉及到同步监视器,只是不需要显式声明
- 非静态的同步监视器:this
- 静态同步监视器:当前类本身(Window4.class)
死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
// 线程A lock(A); lock(B); // 可能阻塞,因为线程B已持有B // 执行临界区代码 // 线程B lock(B); lock(A); // 可能阻塞,因为线程A已持有A // 执行临界区代码如何避免编程中的死锁
- 统一加锁顺序:所有线程按照相同的顺序获取锁。
- 使用超时机制:尝试获取锁时设置超时时间,避免无限等待。
- 避免嵌套锁:尽量减少多锁嵌套操作。
- 使用高级同步工具:如Java中的
ReentrantLock配合tryLock()方法,可实现非阻塞式锁获取。
Lock锁解决线程安全
import java.util.concurrent.locks.ReentrantLock; class Window01 implements Runnable { private int ticket = 100; private final ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { lock.lock(); try { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + " 卖票,票号:" + ticket); ticket--; Thread.sleep(100); // 模拟卖票耗时 } else { break; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } finally { lock.unlock(); } } } public static void main(String[] args) { Window01 window = new Window01(); Thread t1 = new Thread(window, "窗口1"); Thread t2 = new Thread(window, "窗口2"); Thread t3 = new Thread(window, "窗口3"); t1.start(); t2.start(); t3.start(); } }synchronized与Lock的异同
相同:二者都可以解决线程安全问题
不同:
- synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
- Lock需要手动的启动同步lock(),同时结束同步也需要手动的实现unlock()
线程通信
/** * Number 类实现 Runnable 接口,用于多线程环境下按顺序打印 1 到 100 的数字。 * 通过 synchronized 同步块和 wait/notify 机制实现线程间的协调。 */ class Number implements Runnable { private int number = 1; // 当前待打印的数字,初始为 1 @Override public void run() { while (true) { try { Thread.sleep(100); // 模拟处理延迟,避免过快输出 } catch (InterruptedException e) { e.printStackTrace(); // 处理中断异常 } synchronized (this) { // 唤醒其他等待线程(若存在),确保下一个线程可以继续执行 notify(); // 判断是否已打印完所有数字 if (number <= 100) { // 输出当前线程名称及当前数字 System.out.println(Thread.currentThread().getName() + ": " + number); number++; // 数字递增 try { // 当前线程调用 wait() 进入阻塞状态,释放锁,等待被唤醒 wait(100); // 等待最多 100 毫秒,或被 notify 唤醒 } catch (InterruptedException e) { e.printStackTrace(); // 处理中断异常 } } else { // 打印完毕,退出循环 break; } } } } }wait():执行该方法后,当前线程进入阻塞状态,并释放所持有的同步监视器。notify():执行该方法后,唤醒一个正在等待的线程。若有多个线程处于等待状态,则唤醒优先级较高的那个。notifyAll():执行该方法后,唤醒所有正在等待的线程。
wait()、notify()和notifyAll()三个方法必须在同步代码块或同步方法中调用。- 它们的调用者必须是同步代码块或同步方法中的同步监视器(即锁对象),否则会抛出
IllegalMonitorStateException异常。- 这三个方法定义在
java.lang.Object类中。
sleep() 和 wait()的区别
| 特性 | sleep() | wait() |
|---|---|---|
| 所属类 | Thread | Object |
| 是否释放锁 | 否 | 是 |
| 唤醒方式 | 时间到期 | notify()/notifyAll() / 中断 |
| 调用环境要求 | 无限制 | 必须在同步块/方法中 |
| 适用场景 | 延迟执行、定时任务 | 线程协作、条件等待 |
实现Callable接口
与使用Runnable相比,Callable功能更强大
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
import java.util.concurrent.Callable; public class MyCallable implements Callable<String> { @Override public String call() throws Exception { // 任务逻辑 return "任务执行完成,返回结果"; } }示例
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; public class TestCallable { public static void main(String[] args) { // 3. 创建 Callable 接口实现类的对象 Numbertest task = new Numbertest(); // 4. 将 Callable 实现类对象传入 FutureTask 构造器,创建 FutureTask 对象 FutureTask<Integer> futureTask = new FutureTask<>(task); // 5. 将 FutureTask 对象作为参数创建 Thread 并启动 new Thread(futureTask).start(); try { // 6. 获取 call() 方法的返回值 // get() 返回值即为 Callable 中重写的 call() 方法的返回结果 Integer sum = futureTask.get(); System.out.println("总和为:" + sum); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } } } // 1. 创建实现 Callable 接口的类 class Numbertest implements Callable<Integer> { // 2. 重写 call() 方法,定义线程执行的任务逻辑 @Override public Integer call() throws Exception { int sum = 0; for (int i = 0; i <= 100; i++) { if (i % 2 == 0) { System.out.println(i); sum += i; } } return sum; } }线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
创建线程池的方法
使用java.util.concurrent.Executors工具类可以快速创建不同类型的线程池:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; // 固定大小线程池(5个线程) ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5); // 单线程执行器 ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); // 可缓存线程池(根据需要创建新线程,空闲线程可复用) ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); // 定时任务线程池 ExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);提交任务到线程池
可以通过execute(Runnable)或submit(Callable<T>)方法提交任务:
// 提交无返回值的任务 fixedThreadPool.execute(() -> { System.out.println("任务正在运行,线程:" + Thread.currentThread().getName()); }); // 提交有返回值的任务 import java.util.concurrent.Callable; Callable<String> task = () -> { Thread.sleep(1000); return "任务完成,结果为:Hello World"; }; try { String result = fixedThreadPool.submit(task).get(); System.out.println(result); } catch (Exception e) { e.printStackTrace(); }关闭线程池
必须在不再提交新任务后关闭线程池,防止内存泄漏或程序无法退出:
// 平滑关闭:不再接受新任务,等待已有任务完成 fixedThreadPool.shutdown(); // 强制关闭:立即停止所有任务,不等待 // fixedThreadPool.shutdownNow();检查是否已完全关闭:
if (fixedThreadPool.isShutdown()) { System.out.println("线程池已关闭"); } if (fixedThreadPool.isTerminated()) { System.out.println("所有任务已结束"); }自定义线程池(推荐方式)
使用ThreadPoolExecutor构造函数实现更精细的控制:
import java.util.concurrent.*; ThreadPoolExecutor customPool = new ThreadPoolExecutor( 2, // 核心线程数 4, // 最大线程数 60L, // 空闲线程存活时间(秒) TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), // 任务队列 new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略 );- 核心线程数:始终存在的线程数量。
- 最大线程数:允许创建的最大线程数。
- 空闲超时时间:超过核心线程数的线程在空闲时等待的时间。
- 任务队列:存放待执行任务的缓冲区。
- 拒绝策略:当任务队列满且线程数已达上限时的处理方式。
常见拒绝策略:
AbortPolicy:抛出异常。DiscardPolicy:直接丢弃任务。DiscardOldestPolicy:丢弃最老的任务,尝试重新插入。CallerRunsPolicy:由调用线程直接执行任务。
使用建议与最佳实践
- 避免使用
Executors.newCachedThreadPool(),因其可能导致无限创建线程,引发资源耗尽。 - 尽量使用
ThreadPoolExecutor自定义线程池,以便精确控制资源使用。 - 合理设置线程池大小,通常参考 CPU 核心数和任务类型(计算密集型或 I/O 密集型)。
- 对于长时间运行的任务,应考虑设置合理的超时机制。
- 在高并发场景下,监控线程池状态(如活跃线程数、队列长度)有助于排查性能瓶颈。
示例:完整使用流程
public class ThreadPoolExample { public static void main(String[] args) { ExecutorService executor = new ThreadPoolExecutor( 2, 4, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), new ThreadPoolExecutor.CallerRunsPolicy() ); for (int i = 0; i < 10; i++) { final int taskId = i; executor.submit(() -> { System.out.println("任务 " + taskId + " 正在执行,线程:" + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } executor.shutdown(); try { if (!executor.awaitTermination(30, TimeUnit.SECONDS)) { executor.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); } } }