【JavaSE全面教学】Java多线程与并发基础Day15(2026年)
写在前面:这是JavaSE系列的最后一篇!多线程是Java进阶的必经之路,也是面试中区分初级和中高级开发者的分水岭。今天我们讲多线程的基础知识,包括线程的创建、生命周期、同步机制和线程池。
文章目录
- 一、进程与线程
- 1.1 基本概念
- 1.2 为什么需要多线程?
- 二、创建线程的四种方式
- 2.1 继承Thread类
- 2.2 实现Runnable接口(推荐)
- 2.3 实现Callable接口(有返回值)
- 2.4 线程池(生产环境推荐)
- 2.5 四种方式对比
- 三、线程的生命周期
- 3.1 六种状态
- 3.2 状态转换示例
- 四、线程同步
- 4.1 为什么需要同步?
- 4.2 synchronized关键字
- 4.3 Lock接口(更灵活)
- 4.4 volatile关键字
- 五、线程通信
- 5.1 wait和notify
- 5.2 sleep和wait的区别
- 六、线程池
- 6.1 为什么需要线程池?
- 6.2 创建线程池
- 6.3 ThreadPoolExecutor(推荐)
- 6.4 拒绝策略
- 参考资料
- 七、面试高频考点
- 考点1:start和run的区别
- 考点2:synchronized和Lock的区别
- 考点3:线程池的核心参数
- 八、总结
- 🎉 JavaSE全面教学系列完结
一、进程与线程
1.1 基本概念
进程(Process): - 操作系统分配资源的基本单位 - 每个进程有独立的内存空间 - 例如:打开一个浏览器就是一个进程 线程(Thread): - CPU调度的基本单位 - 一个进程可以包含多个线程 - 线程共享进程的内存空间 - 例如:浏览器中同时下载多个文件,每个下载任务是一个线程1.2 为什么需要多线程?
实际场景:想象一下你在浏览器里同时下载多个文件。如果是单线程,必须等第一个文件下载完才能开始第二个。而多线程可以让多个下载任务同时进行,大大缩短总时间。
// 单线程:任务串行执行// 任务1(3秒)→ 任务2(2秒)→ 任务3(5秒)= 总共10秒// 多线程:任务并行执行// 任务1(3秒)// 任务2(2秒) → 总共5秒(取决于最慢的任务)// 任务3(5秒)// 好处:// 1. 提高CPU利用率// 2. 提高程序响应速度// 3. 充分利用多核CPU经验之谈:在实际项目中,多线程最常见的应用场景是:批量数据处理、异步任务执行、定时任务调度。但也要注意,线程不是越多越好,过多的线程会导致上下文切换开销增大,反而降低性能。
二、创建线程的四种方式
2.1 继承Thread类
// 方式1:继承Thread类,重写run方法classMyThreadextendsThread{@Overridepublicvoidrun(){for(inti=0;i<5;i++){System.out.println("子线程:"+i);}}}// 启动线程publicclassTest{publicstaticvoidmain(String[]args){MyThreadt=newMyThread();t.start();// 启动线程(不是run!)for(inti=0;i<5;i++){System.out.println("主线程:"+i);}}}// 注意:调用start()才是启动线程// 调用run()只是普通方法调用,还是在主线程执行踩坑提醒:新手最容易犯的错误就是调用run()而不是start()。调用run()不会创建新线程,只是在当前线程执行方法体,完全达不到多线程的效果。
2.2 实现Runnable接口(推荐)
// 方式2:实现Runnable接口classMyRunnableimplementsRunnable{@Overridepublicvoidrun(){for(inti=0;i<5;i++){System.out.println("子线程:"+i);}}}// 启动线程publicclassTest{publicstaticvoidmain(String[]args){Threadt=newThread(newMyRunnable());t.start();}}// 方式3:Lambda表达式(Java 8+,最简洁)newThread(()->{System.out.println("子线程运行");}).start();2.3 实现Callable接口(有返回值)
importjava.util.concurrent.*;// 方式4:实现Callable接口(可以返回结果)classMyCallableimplementsCallable<Integer>{@OverridepublicIntegercall()throwsException{intsum=0;for(inti=1;i<=100;i++){sum+=i;}returnsum;// 返回结果}}publicclassTest{publicstaticvoidmain(String[]args)throwsException{// 创建FutureTaskFutureTask<Integer>task=newFutureTask<>(newMyCallable());// 启动线程newThread(task).start();// 获取结果(会阻塞直到线程执行完毕)Integerresult=task.get();System.out.println("1到100的和:"+result);// 5050}}2.4 线程池(生产环境推荐)
经验之谈:在实际项目中,千万不要用new Thread()创建线程!线程创建和销毁是有开销的,而且线程数不可控可能导致OOM。线程池可以复用线程、控制并发数,是生产环境的标准做法。
importjava.util.concurrent.*;// 创建线程池ExecutorServicepool=Executors.newFixedThreadPool(3);// 3个线程的固定线程池// 提交任务pool.execute(()->System.out.println("任务1"));pool.execute(()->System.out.println("任务2"));pool.execute(()->System.out.println("任务3"));// 关闭线程池pool.shutdown();2.5 四种方式对比
| 方式 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| 继承Thread | 简单直接 | Java单继承限制 | ★★☆☆☆ |
| 实现Runnable | 灵活,可继承其他类 | 无返回值 | ★★★★☆ |
| 实现Callable | 有返回值 | 稍复杂 | ★★★★☆ |
| 线程池 | 复用线程,性能好 | 需要理解线程池原理 | ★★★★★ |
三、线程的生命周期
3.1 六种状态
NEW(新建) ↓ start() RUNNABLE(可运行) ↓ 获得CPU时间片 ↓ 等待锁 / sleep / join / IO BLOCKED(阻塞) ← 获得锁 → RUNNABLE WAITING(等待) ← notify / notifyAll → RUNNABLE TIMED_WAITING(超时等待) ← 超时结束 → RUNNABLE ↓ run()执行完毕 TERMINATED(终止)3.2 状态转换示例
Threadt=newThread(()->{try{Thread.sleep(1000);// TIMED_WAITING}catch(InterruptedExceptione){e.printStackTrace();}});// t的状态:NEWt.start();// t的状态:RUNNABLE// t执行完毕后// t的状态:TERMINATED四、线程同步
4.1 为什么需要同步?
踩坑提醒:多线程操作共享数据时,不加同步机制会导致数据不一致。我曾经遇到过一个Bug:多个线程同时修改库存数量,结果出现超卖。这就是典型的竞态条件问题。
// 不加同步的例子:多个线程同时操作共享数据classCounter{privateintcount=0;publicvoidincrement(){count++;// 不是原子操作!}}// count++ 实际上是3步操作:// 1. 读取count的值// 2. count + 1// 3. 写回count// 如果两个线程同时执行count++,可能出现:// 线程A读取count=0 → 线程B读取count=0// 线程A写入count=1 → 线程B写入count=1// 结果应该是2,实际是1(数据不一致)4.2 synchronized关键字
// 方式1:同步代码块classCounter{privateintcount=0;privatefinalObjectlock=newObject();publicvoidincrement(){synchronized(lock){// 锁住对象count++;}}}// 方式2:同步方法classCounter{privateintcount=0;publicsynchronizedvoidincrement(){// 锁住thiscount++;}}// 方式3:同步静态方法classCounter{privatestaticintcount=0;publicstaticsynchronizedvoidincrement(){// 锁住Class对象count++;}}4.3 Lock接口(更灵活)
经验之谈:Lock比synchronized更灵活,可以实现公平锁、可中断锁、超时获取锁等。但使用Lock时一定要在finally中释放锁,否则一旦出现异常,锁就永远释放了,其他线程会一直阻塞。
importjava.util.concurrent.locks.Lock;importjava.util.concurrent.locks.ReentrantLock;classCounter{privateintcount=0;privatefinalLocklock=newReentrantLock();publicvoidincrement(){lock.lock();// 加锁try{count++;}finally{lock.unlock();// 一定要在finally中释放锁}}}踩坑提醒:忘记在finally中unlock是Lock使用的常见错误。相比之下synchronized会自动释放锁,更安全一些。
4.4 volatile关键字
// volatile:保证可见性,不保证原子性classVolatileDemo{privatevolatilebooleanrunning=true;publicvoidstop(){running=false;// 一个线程修改}publicvoidrun(){while(running){// 另一个线程能立即看到变化// 执行任务}}}// volatile vs synchronized// volatile:保证可见性,不保证原子性// synchronized:保证可见性和原子性五、线程通信
5.1 wait和notify
classSharedResource{privateintcount=0;// 生产者publicsynchronizedvoidproduce()throwsInterruptedException{while(count>=10){wait();// 等待(释放锁)}count++;System.out.println("生产:"+count);notifyAll();// 通知消费者}// 消费者publicsynchronizedvoidconsume()throwsInterruptedException{while(count<=0){wait();// 等待}count--;System.out.println("消费:"+count);notifyAll();// 通知生产者}}5.2 sleep和wait的区别
| 特性 | sleep | wait |
|---|---|---|
| 所属类 | Thread | Object |
| 释放锁 | 不释放 | 释放 |
| 使用位置 | 任何地方 | synchronized块中 |
| 唤醒方式 | 自动超时 | notify/notifyAll |
六、线程池
6.1 为什么需要线程池?
// 不用线程池:// 每次执行任务都创建新线程// 创建和销毁线程开销大// 线程数量不可控,可能OOM// 用线程池:// 复用线程,减少创建销毁开销// 控制最大并发数// 提供任务队列和拒绝策略6.2 创建线程池
importjava.util.concurrent.*;// 1. 固定大小线程池ExecutorServicepool1=Executors.newFixedThreadPool(5);// 5个线程,任务队列无界// 2. 缓存线程池ExecutorServicepool2=Executors.newCachedThreadPool();// 线程数量不固定,按需创建// 3. 单线程池ExecutorServicepool3=Executors.newSingleThreadExecutor();// 只有1个线程,保证任务按顺序执行// 4. 定时任务线程池ScheduledExecutorServicepool4=Executors.newScheduledThreadPool(3);pool4.schedule(()->System.out.println("3秒后执行"),3,TimeUnit.SECONDS);pool4.scheduleAtFixedRate(()->System.out.println("定时执行"),0,1,TimeUnit.SECONDS);6.3 ThreadPoolExecutor(推荐)
踩坑提醒:阿里巴巴Java开发手册明确规定,生产环境禁止使用Executors的快捷方法创建线程池!因为newFixedThreadPool和newSingleThreadExecutor使用的是无界队列,任务堆积会导致OOM;newCachedThreadPool允许无限创建线程,也会导致OOM。
// 生产环境推荐手动创建线程池(不用Executors工厂方法)ThreadPoolExecutorpool=newThreadPoolExecutor(2,// 核心线程数5,// 最大线程数60L,// 空闲线程存活时间TimeUnit.SECONDS,// 时间单位newArrayBlockingQueue<>(100),// 任务队列(有界队列)Executors.defaultThreadFactory(),// 线程工厂newThreadPoolExecutor.CallerRunsPolicy()// 拒绝策略);// 提交任务pool.execute(()->System.out.println("任务1"));// 无返回值Future<Integer>future=pool.submit(()->{// 有返回值return42;});Integerresult=future.get();// 获取结果// 关闭线程池pool.shutdown();// 不接受新任务,等待已提交的任务完成6.4 拒绝策略
| 策略 | 说明 |
|---|---|
| AbortPolicy | 抛出RejectedExecutionException(默认) |
| CallerRunsPolicy | 由提交任务的线程执行 |
| DiscardPolicy | 直接丢弃任务 |
| DiscardOldestPolicy | 丢弃队列中最老的任务 |
参考资料
- Oracle官方文档 - Concurrency
- Baeldung - Java Concurrency Tutorial
七、面试高频考点
考点1:start和run的区别
// start():启动新线程,在子线程中执行run()// run():普通方法调用,在当前线程中执行追问:多次调用start会怎样?
答案:会抛出IllegalThreadStateException。线程一旦启动,就不能再次启动。
考点2:synchronized和Lock的区别
| 特性 | synchronized | Lock |
|---|---|---|
| 锁释放 | 自动释放 | 手动unlock |
| 中断 | 不可中断 | 可以中断 |
| 公平性 | 非公平 | 可选公平 |
| 条件绑定 | 一个锁一个条件 | 一个锁多个条件 |
延伸:什么情况下用Lock不用synchronized?
答案:需要公平锁、可中断锁、超时获取锁、多个条件变量时,用Lock更灵活。
考点3:线程池的核心参数
// corePoolSize:核心线程数// maximumPoolSize:最大线程数// keepAliveTime:空闲线程存活时间// workQueue:任务队列// handler:拒绝策略追问:线程池的执行流程是怎样的?
答案:提交任务→核心线程是否已满?否:创建核心线程执行任务;是:加入队列;队列已满?否:加入队列;是:创建非核心线程;非核心线程数达上限?否:创建非核心线程;是:执行拒绝策略。
八、总结
今天我们学习了:
- ✅ 进程和线程的概念
- ✅ 创建线程的四种方式
- ✅ 线程的生命周期
- ✅ 线程同步(synchronized、Lock、volatile)
- ✅ 线程通信(wait/notify)
- ✅ 线程池的使用
重点记忆:
- 推荐用Runnable或线程池创建线程
- start()启动线程,run()只是普通调用
- synchronized保证原子性和可见性
- volatile只保证可见性
- 生产环境用手动创建的ThreadPoolExecutor
🎉 JavaSE全面教学系列完结
恭喜你学完了JavaSE全面教学系列的全部15篇文章!
学习路线回顾:
Week 1(Day1-5):基础语法 ✅ Week 2(Day6-10):面向对象 ✅ Week 3(Day11-15):进阶特性 ✅下一步建议:
- 复习本系列所有文章
- 把每篇的代码都自己敲一遍
- 刷LeetCode巩固语法
- 学习JavaWeb/框架(Spring Boot等)
- 做一个完整的项目
互动话题:恭喜你完成了JavaSE全面教学系列!你学完之后有什么收获或困惑?欢迎在评论区分享!
如果这个系列对你有帮助,欢迎点赞、收藏、关注三连支持!后续我会更新Java进阶系列,关注我不迷路👇
本文为【JavaSE全面教学】系列第15篇(完结),感谢你的学习!
