10、Java 基础硬核复习:多线程(并发核心)的核心逻辑与面试考点
一、核心知识体系:多线程的"三大支柱"与"四大模块"
本章的知识围绕"线程的创建、安全、通信"展开,可以归纳为三大支柱 (怎么建线程、怎么保安全、怎么搞通信)和四大模块(基础概念、线程生命周期、同步机制、JDK5.0新增特性)。
1. 基础概念:程序、进程、线程,并行与并发
- 程序(Program) :静态的代码文件(如
.java),是计算机执行的指令集合。 - 进程(Process):程序的一次执行(如打开浏览器、运行QQ),是资源分配的单位(每个进程有独立的内存空间、文件句柄等)。
- 线程(Thread):进程中的执行单元(如浏览器中的"下载线程""渲染线程"),是CPU调度的单位(一个进程可以有多个线程,线程共享进程的内存空间)。
- 并行(Parallelism):多核CPU同时执行多个任务(如4核CPU同时运行4个线程,真正"同时")。
- 并发(Concurrency):单核CPU快速切换执行多个任务(如单核CPU在1秒内交替执行线程A和线程B,看起来像"同时")。
2. 线程创建方式:4种方法,从"基础"到"高级"
Java提供了4种创建线程的方式,满足不同场景需求:
- 方式一:继承
Thread类 (基础方式)
步骤:创建Thread子类→重写run()方法→调用start()启动线程。
缺点:无法共享资源(每个线程有自己的Thread实例,内存不共享);无法继承其他类(Java不支持多继承)。
案例:两个线程遍历100以内偶数(通过继承Thread实现)。 - 方式二:实现
Runnable接口 (推荐方式)
步骤:创建Runnable实现类→重写run()方法→将Runnable实例传入Thread构造器→调用start()启动线程。
优点:可共享资源(多个线程共享同一个Runnable实例,内存共享);避免单继承限制;更灵活(如结合线程池使用)。
案例:两个线程共同遍历1-100自然数(通过实现Runnable实现)。 - 方式三:实现
Callable接口(JDK5.0新增) (有返回值场景)
特点:call()方法有返回值(通过Future获取);可抛异常;需配合FutureTask使用。
案例:计算1+2+...+100的和(通过Callable实现,返回结果)。 - 方式四:使用线程池(JDK5.0新增) (高并发场景)
优点:复用线程(避免频繁创建销毁线程的开销);控制线程数量(避免资源耗尽);提供任务队列管理(如ExecutorService的submit()方法)。
案例:电商秒杀场景(通过线程池处理大量用户请求)。
3. 线程生命周期:5种状态,理解"线程的生死"
线程的生命周期分为5种状态(JDK源码中是6种,面试常考5种),状态之间可相互转换:
- 新建(New) :创建线程但未启动(如
Thread thread = new Thread())。 - 就绪(Runnable) :调用
start()后,等待CPU调度(如thread.start()后,线程进入就绪状态)。 - 运行(Running) :CPU分配时间片,线程执行
run()方法(如线程获得CPU,开始执行任务)。 - 阻塞(Blocked) :线程暂时无法执行(如
sleep()、wait()、同步锁等待、IO操作)。 - 死亡(Terminated) :线程执行结束(如
run()方法结束)或异常终止(如抛出未捕获的异常)。
4. 线程安全:解决"共享资源"的冲突
多线程环境下,共享资源(如全局变量、文件)可能导致数据不一致(如"卖票案例"中,两个线程同时卖同一张票)。解决线程安全的核心是同步(让线程按顺序访问共享资源)。
- 方式一:
synchronized关键字 (隐式锁)- 同步代码块:
synchronized(同步监视器) { 共享资源操作 }(如synchronized(this) { ticket-- })。 - 同步方法:在方法声明中加
synchronized(如public synchronized void sellTicket() { ticket-- })。
原理:同一时间只有一个线程能进入同步代码块/方法(通过对象监视器锁定资源)。
- 同步代码块:
- 方式二:
Lock锁(JDK5.0新增) (显式锁)- 优点:比
synchronized更灵活(如可尝试获取锁lock.tryLock()、可中断获取锁lock.lockInterruptibly());需手动unlock()(务必放在finally中)。 - 案例:车站窗口售票(通过
ReentrantLock解决线程安全问题)。
- 优点:比
- 死锁 :线程互相等待对方释放锁(如线程A持有锁1等待锁2,线程B持有锁2等待锁1)。
避免:破坏循环等待(按顺序加锁,如先加锁1再加锁2);使用tryLock()尝试获取锁(避免无限等待)。
5. 线程通信:wait()/notify(),实现"线程间的交互"
线程之间需要通信(如"生产者-消费者"模型),wait()、notify()、notifyAll()是核心方法:
wait():让线程等待(释放锁,进入阻塞状态);notify():唤醒单个等待线程(不释放锁);notifyAll():唤醒所有等待线程(不释放锁)。
注意:必须在同步代码块/方法中使用(因为需要获取对象监视器);wait()释放锁,notify()不释放锁。
二、高频面试考点:必考"死穴",掌握这些=掌握多线程核心
本章的面试题非常"硬核",主要考察对并发原理的理解,以下是必考考点:
1. run()和start()的区别
- 考点 :可以直接调用
run()方法吗? - 答案 :可以,但那只是普通方法调用(不会启动新线程)。只有调用
start()才会启动线程,让JVM调用run()方法(start()会创建线程栈,将run()方法加入线程调度队列)。
2. Runnable vs Callable接口
- 考点:两种创建方式有何不同?
- 答案 :
Callable的call()方法有返回值 (通过Future获取),Runnable的run()没有;Callable可以抛异常(如Callable<Integer> task = () -> 1/0;会抛ArithmeticException),Runnable只能内部消化异常;Callable需配合FutureTask使用(如FutureTask<Integer> future = new FutureTask<>(task); new Thread(future).start(); future.get();)。
3. sleep()和wait()的区别(死记硬背)
- 考点:两者都能让线程暂停,有什么本质区别?
- 答案 :
- 类不同 :
sleep是Thread类的静态方法;wait是Object类的成员方法; - 锁释放 :
sleep不释放锁 (抱着锁睡,如synchronized代码块中调用sleep(),其他线程无法进入);wait释放锁 (让出资源,如synchronized代码块中调用wait(),其他线程可以进入); - 使用场景 :
sleep可以在任何地方用(如普通方法、同步代码块);wait必须在同步代码块/同步方法中使用(因为需要获取对象监视器)。
- 类不同 :
4. synchronized vs Lock
- 考点 :JDK5为什么要推
Lock? - 答案 :
synchronized是隐式锁 (自动加锁解锁,如同步代码块结束时自动释放锁);Lock是显式锁 (需手动lock()和unlock(),务必放在finally中,避免死锁);Lock更灵活(如tryLock()尝试获取锁、lockInterruptibly()可中断获取锁、newCondition()实现多条件等待);synchronized是JVM层面的锁(重量级锁,性能较低);Lock是API层面的锁(如ReentrantLock是可重入锁,性能更高)。
5. 死锁(Deadlock)
- 考点:什么是死锁?怎么避免?
- 答案 :
- 死锁:两个或多个线程互相等待对方释放锁(如线程A持有锁1等待锁2,线程B持有锁2等待锁1);
- 避免:破坏循环等待(按顺序加锁,如先加锁1再加锁2);使用
tryLock()尝试获取锁(避免无限等待);设置锁的超时时间(如lock.tryLock(1, TimeUnit.SECONDS))。
6. 单例模式的线程安全
- 考点:懒汉式单例在多线程下安全吗?
- 答案 :不安全(如两个线程同时调用
getInstance(),可能创建两个实例)。 - 优化 :
- 双重检查锁(DCL):
private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }(volatile防止指令重排序); - 静态内部类:
private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; }(利用类加载机制,线程安全且懒加载)。
- 双重检查锁(DCL):
三、学习建议:从"理论"到"实践"的跃迁
- 多画图 :画线程生命周期状态转换图(新建→就绪→运行→阻塞→死亡),结合
start()、sleep()、wait()等方法,理解状态转换条件。 - 多写Demo:亲手写一遍"三个窗口卖票"(解决线程安全)和"生产者-消费者"(解决线程通信)的代码(这是理解并发编程的"基石")。
- 理解同步机制 :通过同步代码块和
Lock锁,解决线程安全问题(如卖票案例中的数据不一致)。 - 学习线程池 :使用
ExecutorService创建线程池(如Executors.newFixedThreadPool(5)),理解线程复用的好处(如电商秒杀场景)。
四、总结:多线程是"高并发"的核心
第10章是Java后端开发的"并发核心",它将你从"单线程处理"带入"高并发处理"的层面。掌握多线程,你就能写出高性能、高并发的程序,也能在面试中轻松应对"Java基础"的"硬核"问题。
记住,多线程不是"越多越好"------线程数量过多会导致上下文切换开销增大,反而降低性能。合理使用线程池、控制线程数量,才是多线程开发的"精髓"。