博客简介:本文从零梳理Java线程基础、线程核心API、线程生命周期,再深入讲解并发安全、synchronized锁原理、死锁问题。
一、基础概念:进程 vs 线程
- 进程:操作系统资源分配的最小单位。一个运行起来的程序就是进程,拥有独立内存空间;进程之间互相隔离,进程切换开销很大。
- 线程 :进程内部的执行单元,是CPU调度的最小单位。一个进程可以包含多个线程,同进程内的线程共享进程资源(堆、方法区),线程切换开销远小于进程。
一句话理解:进程是工厂,线程就是工厂里的工人;工厂资源所有工人共用,工人之间独立干活。
二、线程创建方式
Java里创建线程常见4种写法:
- 继承
Thread类,重写run() - 实现
Runnable接口,传给Thread - 实现
Callable+FutureTask(可以获取返回值、抛异常) - 线程池(生产环境推荐,后面拓展学习)
注意:调用
thread.start()才会真正创建操作系统线程;直接调用run()只是普通方法调用,不会开启新线程。
守护线程(Daemon)
- 用户线程:普通业务线程,JVM必须等所有用户线程执行完毕才会退出。
- 守护线程:后台服务线程(GC垃圾回收就是守护线程)。当所有用户线程结束,不管守护线程有没有跑完,JVM直接退出。
java
thread.setDaemon(true); // 设置守护线程,必须在start()之前调用
三、核心API:sleep / join / interrupt
1. sleep()
Thread.sleep(long ms):让当前线程休眠指定时间,不会释放锁。
- 休眠期间线程进入
TIMED_WAITING,时间到后重新参与CPU调度,不一定立刻执行。 - 会抛出
InterruptedException中断异常;线程在sleep时收到中断标记,直接抛出异常、清除中断标志。
重点:sleep只是让出CPU,锁依旧持有!和
wait()有本质区别,wait会释放锁。
2. join()
等待另一个线程执行完毕。主线程调用t.join(),主线程阻塞,直到t线程执行结束才继续运行。
java
Thread t = new Thread(() -> { ... });
t.start();
t.join(); // 主线程在这里等待t执行完
支持带超时时间join(long timeout):最多等待指定毫秒,超时不等了。
3. interrupt 线程中断
不是强制终止线程,只是给线程打一个中断标记。
interrupt():给目标线程设置中断标志位isInterrupted():判断是否被中断,不清除标记Thread.interrupted():判断中断,会清除中断标记
当线程处于sleep/wait/join阻塞状态,调用interrupt会直接抛InterruptedException,并且清空中断标记。
最佳实践:捕获异常后,业务自行决定是否终止线程,而不是直接结束。
四、Java线程6大状态 & 状态流转
Java线程一共6种状态,定义在Thread.State枚举:
- NEW:新建,new Thread之后,还没start()
- RUNNABLE:可运行,包含【正在CPU执行】和【等待CPU分配时间片】两种情况
- BLOCKED:阻塞,等待synchronized锁,拿不到锁就进入这个状态
- WAITING :无限等待,调用
wait()/join()无参版本,需要别的线程唤醒 - TIMED_WAITING:限时等待,sleep、带超时wait/join,时间到自动唤醒
- TERMINATED:线程执行完毕,终止
状态流转简要: NEW → start() → RUNNABLE RUNNABLE遇到sleep/wait(time) → TIMED_WAITING RUNNABLE遇到无参wait/join → WAITING 抢不到synchronized锁 → BLOCKED 线程代码执行完 / 异常退出 → TERMINATED
重点区分: BLOCKED:仅仅抢锁失败;WAITING/TIMED_WAITING是主动放弃CPU等待唤醒。
五、并发带来的问题:线程安全
多个线程同时操作共享可变变量 ,就会出现线程安全问题。 经典例子:两个线程同时对count做count++ count++本质3条CPU指令:load读取内存 → add自增 → save写回内存。 线程切换会打断这个过程,出现指令交错,最终结果小于预期。
产生线程安全3个必要条件(全部满足才会出问题):
- 多线程并发执行
- 共享可变资源(多个线程访问同一个变量)
- 对资源存在非原子操作(操作不是一步完成,可以被打断)
解决思路:破坏任意一个条件即可。最常用:加锁,保证操作原子性。
六、synchronized 内置锁
基础用法
- 修饰实例方法:锁对象是
this - 修饰静态方法:锁对象是类的Class对象
- 同步代码块:手动指定锁对象
synchronized(锁对象){}
核心特性:
- 互斥:同一时刻,锁对象只能被一个线程持有;其他线程抢锁失败进入BLOCKED阻塞。
- 内存可见性:保证锁内修改,对后续拿到同一锁的线程立即可见(类似volatile)
- 可重入:同一个线程,已经拿到锁,再次获取同一个锁不会死锁(锁计数+1,退出计数-1)
锁升级(偏向锁 → 轻量级锁 → 重量级锁)
JDK优化,synchronized不是一开始就是重量级OS锁,会动态升级:
- 偏向锁:单线程无竞争,偏向第一个拿到锁的线程,几乎无开销。
- 轻量级锁:少量线程交替竞争,用自旋(循环CAS)尝试拿锁,不挂起线程。
- 重量级锁 :多线程激烈竞争,向操作系统申请互斥锁,抢不到的线程阻塞。 锁只能升级,不能降级。
七、死锁
死锁四个必要条件(必须同时满足)
- 互斥:资源是独占锁,一个线程持有,别人拿不到
- 占有且等待:线程拿着自己的锁,同时请求别人手里的锁
- 不可抢占:锁不能被强行抢走,只能持有者主动释放
- 循环等待:线程形成环形等待,A拿锁1等锁2,B拿锁2等锁1
只要破坏任意一条,就能避免死锁。最简单方案:统一锁获取顺序,所有线程申请锁顺序保持一致。
死锁示例:线程A持有lock1,等待lock2;线程B持有lock2,等待lock1,互相卡死。
八、volatile关键字
volatile不能保证原子性(不能解决count++这种复合操作),只保证两点:
- 内存可见性:一个线程修改volatile变量,其他线程立刻感知,不会停留在CPU缓存。
- 禁止指令重排:防止JVM、CPU为优化打乱代码执行顺序。
适用场景:状态标记、开关变量(布尔标记),适合读多写少场景。
易错点:volatile无法替代synchronized,不能保护复合操作。
九、补充:wait() 和 sleep() 对比
| sleep | wait | |
|---|---|---|
| 所属类 | Thread | Object |
| 锁 | 不释放锁 | 释放锁 |
| 唤醒 | 时间到自动唤醒 | 需要notify/notifyAll唤醒 |
| 状态 | TIMED_WAITING | WAITING / TIMED_WAITING |
| 使用位置 | 任意地方 | 必须写在synchronized同步代码块内 |
十、总结
多线程核心难点:不是开启线程,而是控制并发安全。
- 先分清线程状态、基础API
- 理解线程安全产生的底层原理(指令交错、缓存)
- 掌握synchronized锁原理、锁升级、死锁
- 拓展学习:线程池、Lock锁(AQS)、CAS、ThreadLocal