从0开始学习Java+AI知识点总结-12.多线程
System.out.println(Thread.currentThread().getName() + "取款成功,余额:" + account.getMoney());System.out.println(Thread.currentThread().getName() + "取款成功,余额:" + account.getMoney());System.out.println(Thread.
一、线程与多线程基础
1. 什么是线程?
线程(Thread)是程序内部的一条执行流程。Java 程序的main方法是主线程的入口,程序中若只有一条执行流程,则称为单线程程序。
public static void main(String[] args) { // 单线程执行:从main方法开始依次执行 for (int i = 0; i < 10; i++) { System.out.println(i); } } |
2. 什么是多线程?
多线程是从软硬件层面实现多条执行流程的技术,多条线程由 CPU 调度执行。它能让程序同时处理多个任务,提高资源利用率。
3. 多线程的应用场景
- 消息通信系统、电商平台(如淘宝、京东)、车票预订系统等需要同时处理多个用户请求的场景。
- 计算密集型任务(如大数据量运算)可通过多线程拆分任务提升效率。
二、线程的创建方式
Java 提供三种核心线程创建方式,各有适用场景,需根据需求选择。
方式一:继承 Thread 类
实现步骤
- 定义子类继承java.lang.Thread;
- 重写run()方法(定义线程执行逻辑);
- 创建子类对象;
- 调用start()方法启动线程(不可直接调用 run ())。
代码示例
// 1.定义线程类 class MyThread extends Thread { @Override public void run() { System.out.println("子线程执行..."); } } // 2.使用线程 public class Demo { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // 启动线程 System.out.println("主线程执行..."); } } |
优缺点
优点 |
缺点 |
编码简单,可直接使用 Thread 类方法 |
线程类继承 Thread 后无法继承其他类,存在单继承局限性 |
方式二:实现 Runnable 接口
实现步骤
- 定义任务类实现Runnable接口;
- 重写run()方法;
- 创建任务对象,交给Thread处理;
- 调用Thread的start()方法启动线程。
代码示例
// 1.定义任务类 class MyRunnable implements Runnable { @Override public void run() { System.out.println("子线程执行..."); } } // 2.使用线程 public class Demo { public static void main(String[] args) { // 匿名内部类写法 Thread thread1 = new Thread(new Runnable() { @Override public void run() { System.out.println("匿名内部类线程执行..."); } }); thread1.start(); // Lambda表达式写法(JDK 8+) Thread thread2 = new Thread(() -> System.out.println("Lambda线程执行...")); thread2.start(); } } |
优缺点
优点 |
缺点 |
任务类可继承其他类或实现其他接口,扩展性强 |
无法直接返回线程执行结果 |
方式三:实现 Callable 接口(JDK 5+)
前两种方式无法返回执行结果,Callable接口解决了这一问题。
实现步骤
- 定义任务类实现Callable<T>接口(T为返回值类型);
- 重写call()方法(有返回值);
- 将Callable对象封装为FutureTask<T>;
- 交给Thread并调用start()启动;
- 通过FutureTask.get()获取结果。
代码示例
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; // 1.定义任务类 class MyCallable implements Callable<Integer> { @Override public Integer call() throws Exception { System.out.println("子线程计算中..."); return 100; // 返回计算结果 } } // 2.使用线程 public class Demo { public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable()); Thread thread = new Thread(futureTask); thread.start();
Integer result = futureTask.get(); // 阻塞等待结果 System.out.println("子线程结果:" + result); } } |
优缺点
优点 |
缺点 |
扩展性强,支持返回执行结果 |
编码较复杂,get()方法会阻塞 |
三种方式对比
方式 |
核心特点 |
适用场景 |
继承 Thread |
编码简单,单继承局限 |
简单场景,无需返回结果 |
实现 Runnable |
扩展性强,无返回值 |
多线程共享资源,无需结果 |
实现 Callable |
支持返回结果,扩展性强 |
需获取线程执行结果的场景 |
三、Thread 类常用方法
Thread类提供线程操作的核心 API,掌握这些方法是多线程编程的基础。
1. 构造器
构造器 |
说明 |
Thread(String name) |
为线程指定名称 |
Thread(Runnable target) |
封装 Runnable 任务为线程 |
Thread(Runnable target, String name) |
封装任务并指定线程名称 |
2. 常用方法
方法 |
说明 |
void run() |
线程任务方法,定义执行逻辑 |
void start() |
启动线程(底层调用 run ()) |
String getName() |
获取线程名称(默认格式:Thread - 索引) |
void setName(String name) |
设置线程名称 |
static Thread currentThread() |
获取当前执行的线程对象 |
static void sleep(long time) |
让当前线程休眠指定毫秒数(阻塞) |
void join() |
让调用该方法的线程先执行完毕 |
代码示例
public class ThreadMethodDemo { public static void main(String[] args) throws InterruptedException { // 获取并修改主线程名称 Thread mainThread = Thread.currentThread(); mainThread.setName("主线程"); System.out.println("主线程名称:" + mainThread.getName()); // 创建子线程 Thread t1 = new Thread(() -> { for (int i = 0; i <= 3; i++) { System.out.println(Thread.currentThread().getName() + "执行:" + i); try { Thread.sleep(100); // 休眠100毫秒 } catch (InterruptedException e) { e.printStackTrace(); } } }, "子线程A"); t1.start(); t1.join(); // 等待子线程A执行完毕 System.out.println("主线程继续执行"); } } |
四、线程安全问题
1. 什么是线程安全问题?
当多个线程同时操作同一个共享资源且存在修改操作时,可能导致业务逻辑异常。
经典场景:夫妻共同账户取钱
- 共享资源:余额 10 万元的银行账户;
- 线程操作:小明和小红同时取 10 万元;
- 问题风险:两人都判断余额充足,最终账户余额可能变为 - 10 万元。
2. 线程安全问题的成因
- 多个线程同时执行;
- 线程同时访问同一个共享资源;
- 存在对共享资源的修改操作。
3. 模拟线程安全问题代码
import java.math.BigDecimal; // 共享资源:银行账户 class Account { private String cardNo; private BigDecimal money; // 余额 public Account(String cardNo, BigDecimal money) { this.cardNo = cardNo; this.money = money; } // getter和setter省略 } // 取款操作类 class ATM { public void drawMoney(Account account, BigDecimal money) { // 判断余额是否充足 if (account.getMoney().compareTo(money) < 0) { System.out.println(Thread.currentThread().getName() + ":余额不足"); return; } // 模拟网络延迟 try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } // 扣减余额 account.setMoney(account.getMoney().subtract(money)); System.out.println(Thread.currentThread().getName() + "取款成功,余额:" + account.getMoney()); } } // 线程类 class DrawThread extends Thread { private ATM atm; private Account account; private BigDecimal money; public DrawThread(ATM atm, Account account, BigDecimal money, String name) { super(name); this.atm = atm; this.account = account; this.money = money; } @Override public void run() { atm.drawMoney(account, money); } } // 测试类 public class ThreadSafeDemo { public static void main(String[] args) { Account account = new Account("ICBC001", new BigDecimal(100000)); ATM atm = new ATM(); new DrawThread(atm, account, new BigDecimal(100000), "小明").start(); new DrawThread(atm, account, new BigDecimal(100000), "小红").start(); } } |
可能结果:两人都取款成功,余额变为 - 100000(线程安全问题)。
五、线程同步:解决线程安全问题
线程同步的核心思想是让多个线程依次访问共享资源,通过 "加锁" 实现。
方式一:同步代码块
语法
synchronized(同步锁对象) { // 访问共享资源的核心代码 } |
代码示例(修复取款问题)
class ATM { public void drawMoney(Account account, BigDecimal money) { // 同步代码块:使用共享资源account作为锁 synchronized (account) { if (account.getMoney().compareTo(money) < 0) { System.out.println(Thread.currentThread().getName() + ":余额不足"); return; } try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } account.setMoney(account.getMoney().subtract(money)); System.out.println(Thread.currentThread().getName() + "取款成功,余额:" + account.getMoney()); } } } |
锁对象规范
- 必须是同一对象(对所有并发线程唯一);
- 实例方法推荐用this;
- 静态方法推荐用类名.class(字节码对象)。
方式二:同步方法
语法
修饰符 synchronized 返回值类型 方法名(参数列表) { // 操作共享资源的代码 } |
代码示例
class ATM { // 同步方法:锁对象为this(ATM实例) public synchronized void drawMoney(Account account, BigDecimal money) { // 取款逻辑同上 } } |
锁对象原理
- 实例方法:默认锁对象为this;
- 静态方法:默认锁对象为类名.class。
与同步代码块对比
维度 |
同步代码块 |
同步方法 |
锁范围 |
仅核心代码(小) |
整个方法(大) |
灵活性 |
高(可精确控制) |
低 |
可读性 |
稍差 |
好 |
方式三:Lock 锁(JDK 5+)
Lock是接口,常用实现类ReentrantLock,比synchronized更灵活。
核心 API
方法 |
说明 |
ReentrantLock() |
创建 Lock 锁对象 |
void lock() |
获取锁 |
void unlock() |
释放锁 |
代码示例
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class ATM { // 定义锁对象(推荐用final修饰) private final Lock lock = new ReentrantLock(); public void drawMoney(Account account, BigDecimal money) { lock.lock(); // 加锁 try { // 取款逻辑同上 } finally { lock.unlock(); // 释放锁(必须放finally中) } } } |
Lock 锁的高级功能
- 公平锁:new ReentrantLock(true),按线程请求顺序获取锁;
- 锁中断:lock.lockInterruptibly(),可中断等待锁的线程;
- 尝试锁:lock.tryLock(10, TimeUnit.SECONDS),超时未获取则返回 false。
三种同步方式对比
方式 |
特点 |
适用场景 |
同步代码块 |
灵活控制锁范围 |
局部核心代码需同步 |
同步方法 |
简单直观,锁范围大 |
整个方法需同步 |
Lock 锁 |
功能强大(公平锁、中断等) |
复杂同步场景 |
六、线程池:高效管理线程资源
1. 为什么需要线程池?
- 频繁创建线程会消耗大量资源(CPU、内存);
- 线程池可复用线程,减少创建 / 销毁开销,控制线程数量。
2. 线程池核心概念
- ExecutorService:线程池接口;
- 核心线程:长期保留的线程(正式工);
- 临时线程:任务繁忙时创建,空闲后销毁(临时工);
- 任务队列:存放等待执行的任务;
- 拒绝策略:任务队列满且线程都繁忙时的处理方式。
3. 创建线程池的方式
方式一:ThreadPoolExecutor(推荐)
通过ThreadPoolExecutor类直接创建,可精确控制参数。
核心构造器
public ThreadPoolExecutor( int corePoolSize, // 核心线程数(正式工数量) int maximumPoolSize, // 最大线程数(正式工+临时工) long keepAliveTime, // 临时线程存活时间 TimeUnit unit, // 时间单位(秒、分等) BlockingQueue<Runnable> workQueue, // 任务队列 ThreadFactory threadFactory, // 线程工厂 RejectedExecutionHandler handler // 拒绝策略 ) |
代码示例
import java.util.concurrent.*; public class ThreadPoolDemo { public static void main(String[] args) { // 创建线程池 ExecutorService pool = new ThreadPoolExecutor( 3, // 核心线程数3 5, // 最大线程数5(临时工2人) 5, // 临时线程存活5秒 TimeUnit.SECONDS, new ArrayBlockingQueue<>(3), // 任务队列容量3 Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() // 拒绝策略 ); // 提交任务 for (int i = 1; i <= 9; i++) { int taskNo = i; pool.execute(() -> { System.out.println(Thread.currentThread().getName() + "执行任务" + taskNo); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); } pool.shutdown(); // 任务执行完毕后关闭线程池 } } |
线程池执行流程
- 新任务提交时,先判断核心线程是否空闲,空闲则执行;
- 核心线程繁忙,任务加入队列等待;
- 队列满且未达最大线程数,创建临时线程执行;
- 队列满且达最大线程数,执行拒绝策略。
方式二:Executors 工具类(不推荐大型系统)
Executors提供静态方法快速创建线程池,但存在资源耗尽风险。
方法 |
说明 |
风险 |
newFixedThreadPool(n) |
固定线程数的线程池 |
队列容量过大(Integer.MAX_VALUE),可能 OOM |
newSingleThreadExecutor() |
单线程线程池 |
同上 |
newCachedThreadPool() |
动态增减线程 |
线程数过大(Integer.MAX_VALUE),可能 OOM |
newScheduledThreadPool(n) |
延迟 / 定期执行任务 |
同上 |
注意:阿里巴巴开发手册强制要求避免使用Executors,推荐ThreadPoolExecutor手动创建。
4. 线程池常用方法
方法 |
说明 |
void execute(Runnable task) |
执行 Runnable 任务(无返回值) |
Future<T> submit(Callable<T> task) |
执行 Callable 任务,返回 Future 对象 |
void shutdown() |
等待所有任务完成后关闭线程池 |
List<Runnable> shutdownNow() |
立即关闭,返回未执行的任务 |
5. 任务拒绝策略
策略 |
说明 |
AbortPolicy |
丢弃任务并抛出异常(默认) |
DiscardPolicy |
丢弃任务,不抛异常(不推荐) |
DiscardOldestPolicy |
丢弃队列中最老任务,加入新任务 |
CallerRunsPolicy |
由提交任务的线程执行被拒绝的任务 |
6. 核心线程数配置建议(面试重点)
- IO 密集型任务(如文件读写、网络请求):核心线程数 ≈ CPU 核心数 × 2~3;
- 计算密集型任务(如大数据运算):核心线程数 ≈ CPU 核心数 + 1;
解释:IO 密集型任务 CPU 空闲时间多,可多创建线程;计算密集型任务 CPU 繁忙,线程数过多会增加切换开销。
七、并发与并行
多线程的执行方式分为并发和并行,两者通常同时存在。
1. 并发(Concurrency)
- 含义:CPU 通过轮询调度为多个线程服务(同一时刻仅一个线程执行),因切换速度快,给人 "同时执行" 的感觉;
- 适用场景:单核 CPU 处理多线程。
2. 并行(Parallelism)
- 含义:同一时刻有多个线程被 CPU 执行(多核 CPU 的多个核心同时处理);
- 适用场景:多核 CPU 处理多线程。
总结
多线程执行是并发与并行的结合:CPU 在多核间分配线程实现并行,单个核心通过轮询实现并发。
结语
多线程是 Java 开发的核心知识点,掌握线程创建、同步机制、线程池优化等内容,能显著提升程序性能和稳定性。实际开发中需根据场景选择合适的线程创建方式和同步策略,尤其注意线程安全和资源管理。建议多通过实战案例练习,加深对多线程原理的理解。
更多推荐
所有评论(0)