你写的Java代码 就像一本《英文操作手册》(因为电脑只懂0和1,但人类写英文更方便)。
JVM的作用 一个超级翻译官+工厂,它:
翻译:把英文手册(Java代码)逐句翻译成机器能懂的方言(字节码)。
️ 执行:指挥电脑的CPU、内存等硬件,像流水线一样按翻译好的指令干活。
️ 兜底:万一手册里有错误(比如让机器“用空气造手机”),JVM会直接喊停,防止工厂爆炸(系统崩溃)。
跨平台:
同一本Java手册(代码),交给不同国家的JVM(Windows/Linux/Mac版),都能翻译成当地机器方言。
❌ 没有JVM的话:你得为每个操作系统重写一遍代码,累死程序员。
自动管理内存:
JVM自带清洁工(垃圾回收GC),自动扫除程序用完的垃圾内存,避免内存泄漏(类似工厂垃圾堆满后瘫痪)。
❌ 没有JVM的话:像C++程序员得自己倒垃圾,稍不注意就内存溢出。
安全沙箱:
Java程序运行在JVM的隔离沙箱里,比如你下载的小游戏无法偷删你电脑的文件。
❌ 没有JVM的话:恶意代码可能直接攻击你的硬盘。
当你用电脑打开一个《我的世界》Java版游戏:
游戏代码(Java编写) → 交给JVM → JVM根据你的操作系统翻译成机器指令 → 游戏流畅运行。
如果你在Windows和Mac上玩同一份游戏,只需装对应的JVM,游戏代码不用改。
JVM是Java的核心保镖+翻译+保姆,让程序员写一次代码到处运行,还不用操心内存和安全问题。没有它,Java可能早就消失了。
想象你上班时,公司给每个员工(线程)发了一个 私人储物柜(ThreadLocal)。
你的东西(数据)只能你自己存取,别人碰不到,也看不到。
避免大家共用同一个柜子(共享变量)时,互相拿错东西的混乱场面。
一句话:ThreadLocal
是给每个线程单独存数据的“私有保险箱”。
线程隔离:
多线程同时操作时,防止数据互相干扰(比如:A线程改了自己的数据,不小心把B线程的数据也改了)。
典型场景:用户登录信息、数据库连接、SimpleDateFormat(这货不是线程安全的!)。
避免传参:
把需要跨方法调用的数据(比如用户ID)塞到ThreadLocal
里,后续直接用,不用层层传递参数。
举个栗子:
// 定义一个“储物柜”(存储用户ID)
static ThreadLocal userStorage = new ThreadLocal<>();
// 线程A存数据
userStorage.set("用户A的ID");
// 线程A在任何地方都能取自己的数据
System.out.println(userStorage.get()); // 输出"用户A的ID"
// 线程B存自己的数据(和线程A互不干扰)
userStorage.set("用户B的ID");
每个线程(Thread)内部有一个隐藏的 ThreadLocalMap
(类似一个私人抽屉)。
当你调用 threadLocal.set(value)
:
实际上是往 当前线程的私人抽屉 里存数据,key是ThreadLocal
对象本身,value是你存的值。
// 伪代码解释
Thread.currentThread().threadLocalMap.set(this, value);
当你调用 threadLocal.get()
:
是从当前线程的抽屉里,用ThreadLocal
对象当key取出对应的value。
关键点:
数据是存在线程对象里的,不是存在ThreadLocal
里!
ThreadLocal
只是帮你操作线程私有数据的“工具人”。
内存泄漏:
如果线程池复用线程(比如Web服务器),用完必须 remove()
清理数据,否则可能导致内存泄漏(因为线程一直活着,数据一直被引用)。
不要滥用:
如果数据本来就该共享(比如全局配置),用ThreadLocal
反而增加复杂度。
场景:银行有4个窗口(4个线程),每个窗口的柜员需要记录当前办理业务的客户ID。
不用ThreadLocal:所有柜员共用一个白板写客户ID,可能互相覆盖。
用ThreadLocal:每个柜员自己带一个小本本(ThreadLocal),只记自己的客户ID,互不干扰。
ThreadLocal = 线程的私有储物柜,解决多线程数据冲突问题,底层靠线程自己的ThreadLocalMap
实现隔离。
想象一个场景:
你辞职了(线程任务结束),但你的 私人储物柜(ThreadLocal) 里还堆满了东西(数据)。
由于柜子的钥匙(ThreadLocal
引用)和你的工牌(线程对象)没被销毁,清洁工(垃圾回收器GC)以为这些垃圾还有用,不敢清理。
久而久之,公司(JVM)的储物区(内存)被塞满,最终爆仓(OOM内存溢出)。
本质:ThreadLocal
使用不当,导致无用数据无法被GC回收,占满内存。
关键在于这两点:
ThreadLocalMap
的key是弱引用,value是强引用:
弱引用key:如果外界没有强引用指向ThreadLocal
对象(比如threadLocal=null
),GC时会回收key。
强引用value:但对应的value依然被ThreadLocalMap
强引用,无法被回收!
线程池的线程长期存活:
比如Tomcat的线程池会复用线程,线程不会销毁 → 它的ThreadLocalMap
一直存在 → 泄漏的value越积越多。
伪代码演示泄漏:
static ThreadLocal threadLocal = new ThreadLocal<>();
void doSomething() {
threadLocal.set(new byte[1024 * 1024]); // 存1MB数据
// 忘记调用 threadLocal.remove()!
}
// 虽然threadLocal=null,但value还在ThreadLocalMap中占用内存!
ThreadLocal
对象失去强引用(比如置为null或方法结束)。
GC回收弱引用的key(因为只有弱引用指向ThreadLocal
)。
但value仍被ThreadLocalMap
强引用 → 形成key=null, value=占用内存
的无效条目。
线程池的线程长期存活 → 无效条目越来越多 → 最终OOM。
用完必须 remove()
:
try {
threadLocal.set(data);
// ...业务代码
} finally {
threadLocal.remove(); // 强制清理!
}
尽量用static
修饰ThreadLocal
:
减少ThreadLocal
实例数量(非static时,每次new对象都会创建新ThreadLocal
)。
避免存储大对象:
比如存1MB的byte数组,泄漏时危害更大。
场景:健身房更衣室的储物柜(ThreadLocal
)。
泄漏:会员(线程)退卡后,柜子里的衣物(value)没清空,但管理员(GC)以为钥匙(key)还在,不敢清理。
结果:更衣室爆满(OOM),新会员没柜子可用。
泄漏原因:
ThreadLocalMap
的key弱引用被回收,value强引用无法回收 + 线程长期存活。
危害:
内存中堆积大量无用数据 → OOM(尤其在线程池场景)。
解决方案:
用完立即remove()
!
就像退租前清空房间,避免被扣押金(内存)!
默认的引用类型,只要强引用存在,对象就不会被GC回收,即使内存不足(OOM 也不回收)。
比如:Object obj = new Object();
,obj
就是强引用。
Object obj = new Object(); // 强引用
obj = null; // 取消强引用,此时对象可以被GC回收
就像你手里紧紧握着一把钥匙(强引用),只要你不松手(obj
不置为 null
),这把钥匙对应的门(对象)就永远不会被拆掉(GC 回收)。
内存不足时才会被回收(GC 发现内存不够时,会清理软引用对象)。
适用于缓存场景(比如图片缓存),内存紧张时自动释放。
SoftReference softRef = new SoftReference<>(new byte[1024 * 1024]); // 1MB 数据
byte[] data = softRef.get(); // 获取对象(可能已被GC回收)
if (data == null) {
System.out.println("对象已被GC回收");
}
就像公司里的临时工位(软引用),平时可以正常使用,但如果公司空间紧张(内存不足),HR(GC)会优先清理这些工位。
只要发生GC,就会被回收(不管内存是否充足)。
典型应用:ThreadLocal
、WeakHashMap
(防止内存泄漏)。
WeakReference weakRef = new WeakReference<>(new byte[1024]); // 1KB 数据
System.gc(); // 手动触发GC(仅示例,生产环境不要用!)
byte[] data = weakRef.get(); // 大概率返回 null
if (data == null) {
System.out.println("对象已被GC回收");
}
就像临时便签(弱引用),只要保洁阿姨(GC)来打扫,就会直接撕掉(回收),不管办公室是否拥挤。
最弱的引用,无法通过 get()
获取对象,仅用于跟踪对象被GC回收的时机。
必须配合 ReferenceQueue
使用,适用于资源清理(如堆外内存管理)。
ReferenceQueue
就像监控摄像头(虚引用),你无法直接拿到被监控的人(get()
返回 null
),但可以知道人什么时候离开(GC 回收)。
引用类型 | 回收时机 | 是否可 get() |
典型用途 |
---|---|---|---|
强引用 | 永不回收(除非显式置 null ) |
✅ | 普通对象 |
软引用 | 内存不足时回收 | ✅ | 缓存 |
弱引用 | GC 时立即回收 | ✅ | ThreadLocal 、WeakHashMap |
虚引用 | GC 时回收,但无法获取对象 | ❌ | 资源清理(如堆外内存) |
强引用:默认方式,除非手动 =null
,否则不回收。
软引用:适合缓存,内存不足时自动清理。
弱引用:适合临时数据,GC 必删(如 ThreadLocal
)。
虚引用:仅用于 GC 事件监听(如 DirectByteBuffer
堆外内存管理)。
记住:
WeakHashMap
的 key 是弱引用,适合做缓存(key 被回收时,整个 Entry 自动移除)。
ThreadLocal
内存泄漏 就是因为 key 是弱引用,但 value 是强引用,必须手动 remove()
!
synchronized
(同步锁)保证同一时间只有一个线程能执行某段代码(互斥锁)。
用于解决多线程并发导致的数据竞争问题。
自动加锁 & 解锁(进入代码块加锁,退出自动释放)。
可重入(同一个线程可以重复获取同一把锁)。
非公平锁(不保证先等待的线程先获得锁)。
// 1. 同步方法
public synchronized void doSomething() {
// 同一时间只有一个线程能执行
}
// 2. 同步代码块
public void doSomething() {
synchronized (this) { // 锁对象
// 临界区代码
}
}
简单的线程同步,比如单机环境下的共享变量保护。
就像公共厕所的单间(锁),一个人进去后会自动锁门(synchronized
),其他人必须排队,出来时自动开门(解锁)。
ReentrantLock
(可重入锁)和 synchronized
类似,但更灵活,支持公平锁、可中断、超时等待等高级功能。
手动加锁 & 解锁(必须显式调用 lock()
和 unlock()
)。
可重入(和 synchronized
一样)。
支持公平锁(先等待的线程先获得锁)。
可中断(lockInterruptibly()
,等待锁时可被中断)。
超时尝试获取锁(tryLock(timeout)
)。
ReentrantLock lock = new ReentrantLock(); // 默认非公平锁
// ReentrantLock lock = new ReentrantLock(true); // 公平锁
lock.lock(); // 加锁
try {
// 临界区代码
} finally {
lock.unlock(); // 必须手动解锁!
}
需要更精细控制的锁,比如:
避免死锁(可超时、可中断)。
需要公平锁(避免线程饥饿)。
就像高级会议室预约系统:
你可以选择公平模式(先到先得)。
可以设置超时(等太久就放弃)。
可以被老板打断(interrupt()
)。
volatile
(易变变量)保证变量的可见性(一个线程修改后,其他线程立即可见)。
禁止指令重排序(避免多线程下的诡异问题)。
不保证原子性(比如 i++
仍然不安全)。
轻量级同步,比 synchronized
性能高。
private volatile boolean flag = false;
// 线程A
flag = true; // 修改后,线程B能立即看到
// 线程B
if (flag) { // 能立即读取到最新值
// do something
}
状态标记(如 while (!flag)
循环退出)。
单例模式(Double-Check Locking):
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
就像公告栏(volatile
变量):
任何人修改后,所有人都能立刻看到最新内容(可见性)。
但如果你要在公告栏上做复杂操作(比如 i++
),还是得加锁(synchronized
)。
特性 | synchronized |
ReentrantLock |
volatile |
---|---|---|---|
锁类型 | 内置锁(JVM 实现) | 显式锁(JDK 实现) | 无锁,仅变量可见性 |
是否需要手动释放 | 自动 | 必须 unlock() |
无锁 |
可重入 | ✅ | ✅ | ❌ |
公平锁 | ❌(非公平) | ✅(可配置) | ❌ |
可中断 | ❌ | ✅ | ❌ |
超时获取锁 | ❌ | ✅ | ❌ |
适用场景 | 简单同步 | 复杂锁控制 | 状态标记、单例模式 |
简单同步 → synchronized
(代码简洁,自动管理锁)。
高级需求(公平锁、可中断) → ReentrantLock
。
仅需可见性,不涉及复合操作 → volatile
。
i++
为什么不能用 volatile
?
因为 i++
是 读取-修改-写入
三个操作,volatile
只能保证读取时是最新值,但不能保证整个操作的原子性。
解决方案:用 synchronized
或 AtomicInteger
。
银行取钱:
synchronized
:ATM 机一次只服务一个人(简单互斥)。
ReentrantLock
:VIP 窗口可以设置超时或插队(更灵活)。
volatile
:余额变动后,所有 ATM 立即显示最新金额(可见性)。
单例模式:
volatile + DCL
:避免指令重排序导致的问题。
synchronized
:简单、自动,适合大多数场景。
ReentrantLock
:灵活、强大,适合复杂需求。
volatile
:轻量级,仅保证可见性,不替代锁。
想象一个银行排队办业务的场景:
AQS = 银行排队系统
它管理着一个队列(排队的人),和一个状态变量(比如“当前可用的窗口数”)。
如果窗口被占满(资源被占用),新来的人(线程)必须排队等待。
当窗口空闲时(资源释放),系统按规则叫下一个排队的人(线程)去办理业务。
一句话:AQS 是一套管理线程排队和唤醒的通用模板,让开发者能轻松实现各种锁和同步工具。
state
(状态变量)
像银行窗口的剩余数量,用 volatile
保证可见性。
比如 ReentrantLock
中,state=0
表示锁空闲,state=1
表示锁被占用。
CLH队列(线程等待队列)
像银行的排队取号机,用双向链表实现,公平地管理等待的线程。
tryAcquire
/tryRelease
(需开发者自定义)
像银行的业务规则(比如VIP优先),由具体的锁(如ReentrantLock
)自己实现。
final void lock() {
if (compareAndSetState(0, 1)) { // 尝试抢锁(CAS修改state)
setExclusiveOwnerThread(Thread.currentThread()); // 抢成功,记录持有者
} else {
acquire(1); // 抢失败,进入队列排队
}
}
步骤:
线程A尝试用CAS把 state
从 0
改成 1
(模拟抢锁)。
如果成功,线程A获得锁。
如果失败,线程A进入队列等待,并可能被挂起(park()
)。
public void unlock() {
sync.release(1); // 释放锁
}
// AQS的release方法
protected final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试释放锁(由子类实现)
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒队列中的下一个线程
return true;
}
return false;
}
步骤:
线程A释放锁,state
变回 0
。
AQS唤醒队列中的下一个线程(线程B)。
场景:奶茶店点单
state
:当前可用的点单柜台数(比如1个)。
队列:顾客(线程)排队,先到先得。
tryAcquire
:规则是“每人限购1杯”(state
从1变0)。
tryRelease
:顾客拿到奶茶后,柜台空闲(state
从0变1),叫下一个顾客。
ReentrantLock
通过 state
记录重入次数,支持公平/非公平锁。
CountDownLatch
state
初始值为N,每调用 countDown()
减1,减到0时唤醒所有等待线程。
Semaphore
state
表示剩余许可证数量,获取/释放许可证时修改 state
。
解耦:
AQS处理排队和唤醒的复杂逻辑,开发者只需实现 tryAcquire
等少量方法。
高性能:
通过CAS和自旋减少线程挂起,提升并发效率。
统一标准:
JDK中的锁、同步器基于AQS,保证行为一致。
AQS = 线程同步的通用模板,核心是 state
+ 队列 + CAS。
开发者只需关注:如何获取/释放资源(实现 tryAcquire
/tryRelease
)。
像银行排队:资源(state
)不够时线程排队,资源释放时按规则唤醒。
理解AQS后,再学 ReentrantLock
、Semaphore
等会发现它们只是AQS的“皮肤”!
Lock 是 Java 提供的手动锁接口,用于替代传统的 synchronized
,提供更灵活的加锁、解锁控制。
手动加锁 & 解锁(不像 synchronized
自动释放)。
支持更高级功能:可中断锁、超时锁、公平锁等。
基于 AQS(AbstractQueuedSynchronizer) 实现(如 ReentrantLock
)。
synchronized
的局限性:
无法手动控制锁的获取和释放(必须等代码块执行完)。
不可中断(线程一直阻塞,无法被 interrupt()
打断)。
不支持超时(不能设置“等锁最多等5秒”)。
非公平锁(不保证先等待的线程先拿到锁)。
Lock 接口解决了这些问题!
方法 | 作用 |
---|---|
lock() |
加锁(如果锁被占用,线程会阻塞) |
unlock() |
释放锁(必须手动调用,否则死锁!) |
tryLock() |
尝试加锁(非阻塞,成功返回 true ,失败 false ) |
tryLock(timeout) |
超时尝试加锁(等一段时间,超时放弃) |
lockInterruptibly() |
可中断加锁(等待锁时,线程可被 interrupt() 打断) |
Lock lock = new ReentrantLock();
lock.lock(); // 加锁
try {
// 临界区代码(线程安全)
} finally {
lock.unlock(); // 必须放在 finally,防止异常导致锁未释放!
}
if (lock.tryLock(3, TimeUnit.SECONDS)) { // 最多等3秒
try {
// 拿到锁,执行任务
} finally {
lock.unlock();
}
} else {
System.out.println("等了3秒还没锁,干别的去!");
}
try {
lock.lockInterruptibly(); // 可被 interrupt() 中断
// 拿到锁后的操作
} catch (InterruptedException e) {
System.out.println("被中断了,不等了!");
} finally {
if (lock.isHeldByCurrentThread()) { // 检查是否持有锁
lock.unlock();
}
}
特性 | Lock |
synchronized |
---|---|---|
锁的获取 | 手动 lock() / unlock() |
自动(代码块结束释放) |
可中断 | ✅(lockInterruptibly() ) |
❌ |
超时尝试 | ✅(tryLock(timeout) ) |
❌ |
公平锁 | ✅(new ReentrantLock(true) ) |
❌(非公平) |
性能 | 高竞争时更优 | 低竞争时更优 |
代码复杂度 | 需要手动管理锁 | 简单,但功能有限 |
必须手动 unlock()
,否则会导致死锁(建议用 try-finally
确保释放)。
不要嵌套使用,比如:
lock.lock();
lock.lock(); // 同一个线程重复加锁(可重入锁可以,但容易忘记解锁次数)
公平锁可能降低吞吐量(维护队列需要额外开销)。
需要精细控制锁(如超时、可中断)→ Lock
简单同步 → synchronized
(代码更简洁)
高并发竞争 → Lock
(性能更好)
Lock
是 synchronized
的增强版,提供更灵活的锁控制。
核心实现类:ReentrantLock
(可重入锁)、ReentrantReadWriteLock
(读写锁)。
一定要记得 unlock()
! 否则锁泄漏,系统卡死。
一句话:
Lock
= 手动挡汽车(精准控制),synchronized
= 自动挡汽车(简单但功能有限)。
让一个或多个线程等待,直到其他线程完成一组操作后再继续执行。
类似火箭发射倒计时:所有准备工作(线程)完成后,主线程才能点火。
方法 | 说明 |
---|---|
CountDownLatch(int count) |
初始化计数器(count 是需要等待的线程数) |
countDown() |
计数器减1(表示一个线程已完成任务) |
await() |
阻塞当前线程,直到计数器归零 |
// 模拟火箭发射(主线程等待3个检查线程完成)
CountDownLatch latch = new CountDownLatch(3);
// 3个检查线程
for (int i = 1; i <= 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 检查完成!");
latch.countDown(); // 计数器-1
}, "检查员-" + i).start();
}
// 主线程等待所有检查完成
latch.await();
System.out.println("所有检查完成,火箭发射!");
主线程等待多个子线程初始化完成。
并行任务完成后汇总结果。
让一组线程互相等待,直到所有线程都到达某个屏障点(Barrier)后,再一起继续执行。
类似团建爬山:所有人到齐后,才能一起出发。
方法 | 说明 |
---|---|
CyclicBarrier(int parties) |
初始化屏障(parties 是需要等待的线程数) |
CyclicBarrier(int parties, Runnable barrierAction) |
所有线程到达后,先执行barrierAction |
await() |
线程到达屏障点,并等待其他线程 |
// 模拟3个玩家组队打BOSS
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有玩家已就位,开始战斗!⚔️");
});
for (int i = 1; i <= 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 已准备!");
barrier.await(); // 等待其他玩家
System.out.println(Thread.currentThread().getName() + " 开始攻击!");
}, "玩家-" + i).start();
}
多阶段任务(如游戏关卡、并行计算)。
需要线程间协同工作的场景。
控制同时访问某个资源的线程数量(限流)。
类似停车场空位:只有空闲车位时,新车才能进入。
方法 | 说明 |
---|---|
Semaphore(int permits) |
初始化许可证数量(permits =最大并发数) |
acquire() |
获取许可证(如果没有空位,线程阻塞) |
release() |
释放许可证(空出位置,让其他线程进入) |
tryAcquire() |
尝试获取许可证(非阻塞,失败返回false ) |
// 停车场只有2个车位
Semaphore semaphore = new Semaphore(2);
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取车位
System.out.println(Thread.currentThread().getName() + " 停车中...");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放车位
System.out.println(Thread.currentThread().getName() + " 离开!");
}
}, "车辆-" + i).start();
}
数据库连接池限制。
接口限流(防止高并发压垮系统)。
工具 | 作用 | 是否可重用 | 适用场景 |
---|---|---|---|
CountDownLatch |
一个线程等多个线程 | ❌(一次性) | 主线程等待子线程初始化 |
CyclicBarrier |
多个线程互相等 | ✅(可循环用) | 多线程协同工作 |
Semaphore |
限制并发访问数 | ✅ | 限流、资源池管理 |
需要“主线程等子线程” → CountDownLatch
比如:启动服务前,等待所有组件初始化完成。
需要“所有线程到齐后一起执行” → CyclicBarrier
比如:并行计算,多个线程分阶段协作。
需要“限制同时运行的线程数” → Semaphore
比如:数据库连接池、接口限流。
CountDownLatch
:“人等事”(主线程等子线程)。
CyclicBarrier
:“事等人”(所有线程互相等)。
Semaphore
:“资源限流”(控制并发访问数)。
它们都是基于 AQS(AbstractQueuedSynchronizer) 实现的,理解AQS后,这些工具的原理就一目了然了!
CAS(Compare And Swap,比较并交换) 是计算机的一种无锁并发操作,用来保证多线程修改变量时的安全性。
它的核心思想是:
“我认为现在的值是 A,如果是的话,我就把它改成 B;如果不是 A,说明被别人改过了,那我就不改了。”
想象你在玩一个多人抢红包游戏:
你看到红包里还剩 10元(这就是你读取的旧值 A=10
)。
你想抢 5元,于是系统检查红包是否还是 10元:
如果是 → 成功扣掉 5元,红包变成 5元(新值 B=5
)。
如果不是 → 说明别人已经抢过,你得重新尝试。
这就是 CAS 的过程!
CAS 是一个原子操作,通常由 CPU 指令直接支持(比如 x86
的 CMPXCHG
指令)。
它的伪代码如下:
boolean CAS(int oldValue, int newValue) {
if (当前值 == oldValue) { // 比较
当前值 = newValue; // 交换
return true; // 成功
}
return false; // 失败
}
实际 Java 中的使用(以 AtomicInteger
为例):
AtomicInteger balance = new AtomicInteger(10); // 红包初始值=10
// 线程1尝试扣减5元
balance.compareAndSet(10, 5); // CAS(10, 5) → 成功,balance变成5
// 线程2尝试扣减3元(但此时balance已经是5,不是10)
balance.compareAndSet(10, 7); // CAS(10, 7) → 失败,线程2需要重试
无锁(Lock-Free)
不需要 synchronized
,减少线程阻塞,性能更高。
轻量级
比锁更高效,适合简单的原子操作(如计数器)。
ABA 问题
如果值从 A → B → A
,CAS 会误以为没变(可以用 AtomicStampedReference
解决)。
自旋开销
如果竞争激烈,线程会一直重试(消耗 CPU)。
只能保证一个变量的原子性
多个变量要用锁或其他方式。
AtomicInteger
、AtomicLong
高并发计数器(如网站访问量统计)。
乐观锁(如数据库版本号控制)
更新前检查版本号是否变化。
无锁数据结构(如 ConcurrentHashMap
)
用 CAS 实现线程安全的插入、删除。
CAS | 锁(synchronized) | |
---|---|---|
实现方式 | 无锁(CPU 指令) | 阻塞(JVM 管程) |
性能 | 高(无上下文切换) | 低(竞争激烈时退化) |
适用场景 | 简单原子操作 | 复杂同步逻辑 |
ABA 问题 | 存在 | 不存在 |
CAS = 乐观无锁并发控制,像抢红包一样“先看再改”,失败就重试,适合高并发简单操作!
特点:
短连接:每次请求完就断开TCP连接(像打电话说完就挂)。
无状态:服务器不记得你是谁,每次请求都要带完整信息(如Cookie)。
简单但慢:下载网页要反复建立连接,效率低。
问题:
❌ 打开一个网页(含图片/CSS/JS)要建立多次TCP连接,浪费资源。
改进:
长连接:一个TCP连接可以发多个请求(像电话不挂,连续聊天)。
管道化(Pipelining):允许连续发多个请求(但响应必须按顺序返回,容易堵车)。
缓存优化:支持 Cache-Control
等头部,减少重复下载。
遗留问题:
❌ 队头阻塞(Head-of-Line Blocking)—— 如果第一个请求卡住,后面的全得等。
❌ 头部冗余(每次请求都带相同的Cookie/User-Agent,浪费流量)。
核心改进:
二进制分帧:
数据拆成小块(帧),乱序发送,接收方组装(像乐高积木拼图)。
多路复用(Multiplexing):
一个TCP连接上并行传输多个请求/响应(彻底解决队头阻塞)。
头部压缩(HPACK):
用字典压缩重复的头部(比如 User-Agent
只传一次)。
服务器推送(Server Push):
服务器可以主动推送资源(如提前把CSS推给浏览器)。
优点:
网页加载速度显著提升(尤其多资源页面)。
遗留问题:
❌ 仍基于TCP,TCP的丢包重传会拖累所有流(底层协议限制)。
革命性变化:
改用QUIC协议(基于UDP,非TCP):
解决TCP队头阻塞问题(即使丢包,其他流不受影响)。
0-RTT快速连接:
首次连接后,下次访问无需握手(类似“记住密码”)。
更好的移动网络支持:
网络切换(WiFi→4G)时连接不中断。
优点:
⚡ 延迟更低,抗丢包更强,适合现代互联网(视频会议、手游等)。
现状:
逐步普及中(Chrome/Facebook等已支持,但部分老旧设备不兼容)。
版本 | 核心改进 | 关键问题 | 现实类比 |
---|---|---|---|
1.0 | 最基础,每次请求完断连接 | 效率极低 | 打电话说一句就挂断 |
1.1 | 长连接、管道化 | 队头阻塞、头部冗余 | 电话不挂但必须按顺序回复 |
2.0 | 二进制分帧、多路复用、头部压缩 | TCP层队头阻塞 | 多条车道并行跑车 |
3.0 | 基于QUIC(UDP)、0-RTT、抗丢包 | 兼容性待提升 | 空中无人机送货(无视地面堵车) |
HTTP 1.0 → 1.1:从“短连接”升级到“长连接”。
HTTP 1.1 → 2.0:从“单车道排队”升级到“多车道并行”。
HTTP 2.0 → 3.0:从“依赖堵车的TCP”换成“灵活的UDP(QUIC)”。
目的始终是:更快、更稳、更省流量!