关于Java八股中那些应知必会的事

1、JVM是什么?

把JVM想象成「万能翻译官+工厂流水线」

  1. 你写的Java代码  就像一本《英文操作手册》(因为电脑只懂0和1,但人类写英文更方便)。

  2. JVM的作用  一个超级翻译官+工厂,它:

    •  翻译:把英文手册(Java代码)逐句翻译成机器能懂的方言(字节码)。

    • ️ 执行:指挥电脑的CPU、内存等硬件,像流水线一样按翻译好的指令干活。

    • ️ 兜底:万一手册里有错误(比如让机器“用空气造手机”),JVM会直接喊停,防止工厂爆炸(系统崩溃)。


为什么需要JVM?三大核心价值:

  1. 跨平台

    • 同一本Java手册(代码),交给不同国家的JVM(Windows/Linux/Mac版),都能翻译成当地机器方言。

    • ❌ 没有JVM的话:你得为每个操作系统重写一遍代码,累死程序员。

  2. 自动管理内存

    • JVM自带清洁工(垃圾回收GC),自动扫除程序用完的垃圾内存,避免内存泄漏(类似工厂垃圾堆满后瘫痪)。

    • ❌ 没有JVM的话:像C++程序员得自己倒垃圾,稍不注意就内存溢出。

  3. 安全沙箱

    • Java程序运行在JVM的隔离沙箱里,比如你下载的小游戏无法偷删你电脑的文件。

    • ❌ 没有JVM的话:恶意代码可能直接攻击你的硬盘。


⚡ 举个实际例子:

当你用电脑打开一个《我的世界》Java版游戏:

  1. 游戏代码(Java编写) → 交给JVM → JVM根据你的操作系统翻译成机器指令 → 游戏流畅运行。

  2. 如果你在Windows和Mac上玩同一份游戏,只需装对应的JVM,游戏代码不用改。


JVM是Java的核心保镖+翻译+保姆,让程序员写一次代码到处运行,还不用操心内存和安全问题。没有它,Java可能早就消失了。

2、ThreadLocal是什么?

 ThreadLocal 是什么?

想象你上班时,公司给每个员工(线程)发了一个 私人储物柜(ThreadLocal)。

  • 你的东西(数据)只能你自己存取,别人碰不到,也看不到。

  • 避免大家共用同一个柜子(共享变量)时,互相拿错东西的混乱场面。

一句话ThreadLocal 是给每个线程单独存数据的“私有保险箱”。


️ ThreadLocal 有什么用?

  1. 线程隔离

    • 多线程同时操作时,防止数据互相干扰(比如:A线程改了自己的数据,不小心把B线程的数据也改了)。

    • 典型场景:用户登录信息、数据库连接、SimpleDateFormat(这货不是线程安全的!)。

  2. 避免传参

    • 把需要跨方法调用的数据(比如用户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");

⚙️ 底层实现(简化版)

  1. 每个线程(Thread)内部有一个隐藏的 ThreadLocalMap(类似一个私人抽屉)。

  2. 当你调用 threadLocal.set(value)

    • 实际上是往 当前线程的私人抽屉 里存数据,key是ThreadLocal对象本身,value是你存的值。

    // 伪代码解释
    Thread.currentThread().threadLocalMap.set(this, value);

  3. 当你调用 threadLocal.get()

    • 是从当前线程的抽屉里,用ThreadLocal对象当key取出对应的value。

关键点

  • 数据是存在线程对象里的,不是存在ThreadLocal里!

  • ThreadLocal 只是帮你操作线程私有数据的“工具人”。


 注意事项

  1. 内存泄漏

    • 如果线程池复用线程(比如Web服务器),用完必须 remove() 清理数据,否则可能导致内存泄漏(因为线程一直活着,数据一直被引用)。

  2. 不要滥用

    • 如果数据本来就该共享(比如全局配置),用ThreadLocal反而增加复杂度。


 再举个现实例子

  • 场景:银行有4个窗口(4个线程),每个窗口的柜员需要记录当前办理业务的客户ID。

  • 不用ThreadLocal:所有柜员共用一个白板写客户ID,可能互相覆盖。

  • 用ThreadLocal:每个柜员自己带一个小本本(ThreadLocal),只记自己的客户ID,互不干扰。


ThreadLocal = 线程的私有储物柜,解决多线程数据冲突问题,底层靠线程自己的ThreadLocalMap实现隔离。

2.1、那什么是ThreadLocalOOM内存泄漏?

 ThreadLocal内存泄漏是什么?

想象一个场景:

  1. 你辞职了(线程任务结束),但你的 私人储物柜(ThreadLocal) 里还堆满了东西(数据)。

  2. 由于柜子的钥匙(ThreadLocal引用)和你的工牌(线程对象)没被销毁,清洁工(垃圾回收器GC)以为这些垃圾还有用,不敢清理。

  3. 久而久之,公司(JVM)的储物区(内存)被塞满,最终爆仓(OOM内存溢出)。

本质ThreadLocal使用不当,导致无用数据无法被GC回收,占满内存。


 为什么会泄漏?(底层原理)

关键在于这两点:

  1. ThreadLocalMap 的key是弱引用,value是强引用

    • 弱引用key:如果外界没有强引用指向ThreadLocal对象(比如threadLocal=null),GC时会回收key。

    • 强引用value:但对应的value依然被ThreadLocalMap强引用,无法被回收!

  2. 线程池的线程长期存活

    • 比如Tomcat的线程池会复用线程,线程不会销毁 → 它的ThreadLocalMap一直存在 → 泄漏的value越积越多。

伪代码演示泄漏

static ThreadLocal threadLocal = new ThreadLocal<>();

void doSomething() {
    threadLocal.set(new byte[1024 * 1024]); // 存1MB数据
    // 忘记调用 threadLocal.remove()!
} 
// 虽然threadLocal=null,但value还在ThreadLocalMap中占用内存!

 内存泄漏的流程

  1. ThreadLocal对象失去强引用(比如置为null或方法结束)。

  2. GC回收弱引用的key(因为只有弱引用指向ThreadLocal)。

  3. 但value仍被ThreadLocalMap强引用 → 形成key=null, value=占用内存的无效条目。

  4. 线程池的线程长期存活 → 无效条目越来越多 → 最终OOM。


️ 如何避免内存泄漏?

  1. 用完必须 remove()

    try {
        threadLocal.set(data);
        // ...业务代码
    } finally {
        threadLocal.remove(); // 强制清理!
    }

  2. 尽量用static修饰ThreadLocal

    • 减少ThreadLocal实例数量(非static时,每次new对象都会创建新ThreadLocal)。

  3. 避免存储大对象

    • 比如存1MB的byte数组,泄漏时危害更大。


 现实类比

  • 场景:健身房更衣室的储物柜(ThreadLocal)。

  • 泄漏:会员(线程)退卡后,柜子里的衣物(value)没清空,但管理员(GC)以为钥匙(key)还在,不敢清理。

  • 结果:更衣室爆满(OOM),新会员没柜子可用。


 总结

  1. 泄漏原因

    • ThreadLocalMap的key弱引用被回收,value强引用无法回收 + 线程长期存活。

  2. 危害

    • 内存中堆积大量无用数据 → OOM(尤其在线程池场景)。

  3. 解决方案

    • 用完立即remove()

    • 就像退租前清空房间,避免被扣押金(内存)!

3、四种引用 :强引用 软引用 弱引用 虚引用

 1. 强引用(Strong Reference)

 特点

  • 默认的引用类型,只要强引用存在,对象就不会被GC回收,即使内存不足(OOM 也不回收)。

  • 比如:Object obj = new Object();obj 就是强引用。

 示例

Object obj = new Object(); // 强引用
obj = null; // 取消强引用,此时对象可以被GC回收

 现实比喻

  • 就像你手里紧紧握着一把钥匙(强引用),只要你不松手(obj 不置为 null),这把钥匙对应的门(对象)就永远不会被拆掉(GC 回收)。


 2. 软引用(Soft Reference)

 特点

  • 内存不足时才会被回收(GC 发现内存不够时,会清理软引用对象)。

  • 适用于缓存场景(比如图片缓存),内存紧张时自动释放。

 示例

SoftReference softRef = new SoftReference<>(new byte[1024 * 1024]); // 1MB 数据
byte[] data = softRef.get(); // 获取对象(可能已被GC回收)
if (data == null) {
    System.out.println("对象已被GC回收");
}

 现实比喻

  • 就像公司里的临时工位(软引用),平时可以正常使用,但如果公司空间紧张(内存不足),HR(GC)会优先清理这些工位。


 3. 弱引用(Weak Reference)

 特点

  • 只要发生GC,就会被回收(不管内存是否充足)。

  • 典型应用:ThreadLocalWeakHashMap(防止内存泄漏)。

 示例

WeakReference weakRef = new WeakReference<>(new byte[1024]); // 1KB 数据
System.gc(); // 手动触发GC(仅示例,生产环境不要用!)
byte[] data = weakRef.get(); // 大概率返回 null
if (data == null) {
    System.out.println("对象已被GC回收");
}

 现实比喻

  • 就像临时便签(弱引用),只要保洁阿姨(GC)来打扫,就会直接撕掉(回收),不管办公室是否拥挤。


 4. 虚引用(Phantom Reference)

 特点

  • 最弱的引用,无法通过 get() 获取对象,仅用于跟踪对象被GC回收的时机

  • 必须配合 ReferenceQueue 使用,适用于资源清理(如堆外内存管理)。

 示例

ReferenceQueue queue = new ReferenceQueue<>();
PhantomReference phantomRef = new PhantomReference<>(new Object(), queue);
System.gc(); // 触发GC
Reference ref = queue.poll(); // 检查对象是否被回收
if (ref != null) {
    System.out.println("对象已被GC回收");
} 
  

 现实比喻

  • 就像监控摄像头(虚引用),你无法直接拿到被监控的人(get() 返回 null),但可以知道人什么时候离开(GC 回收)。


 四种引用对比

引用类型 回收时机 是否可 get() 典型用途
强引用 永不回收(除非显式置 null 普通对象
软引用 内存不足时回收 缓存
弱引用 GC 时立即回收 ThreadLocalWeakHashMap
虚引用 GC 时回收,但无法获取对象 资源清理(如堆外内存)

 关键结论

  1. 强引用:默认方式,除非手动 =null,否则不回收。

  2. 软引用:适合缓存,内存不足时自动清理。

  3. 弱引用:适合临时数据,GC 必删(如 ThreadLocal)。

  4. 虚引用:仅用于 GC 事件监听(如 DirectByteBuffer 堆外内存管理)。

 记住

  • WeakHashMap 的 key 是弱引用,适合做缓存(key 被回收时,整个 Entry 自动移除)。

  • ThreadLocal 内存泄漏 就是因为 key 是弱引用,但 value 是强引用,必须手动 remove()

4、关于Synchronized 和ReentrantLock和volatile的那些事

 1. synchronized(同步锁)

作用

  • 保证同一时间只有一个线程能执行某段代码(互斥锁)。

  • 用于解决多线程并发导致的数据竞争问题。

特点

  1. 自动加锁 & 解锁(进入代码块加锁,退出自动释放)。

  2. 可重入(同一个线程可以重复获取同一把锁)。

  3. 非公平锁(不保证先等待的线程先获得锁)。

使用方式

// 1. 同步方法
public synchronized void doSomething() {
    // 同一时间只有一个线程能执行
}

// 2. 同步代码块
public void doSomething() {
    synchronized (this) { // 锁对象
        // 临界区代码
    }
}

适用场景

  • 简单的线程同步,比如单机环境下的共享变量保护。

现实比喻

  • 就像公共厕所的单间(锁),一个人进去后会自动锁门(synchronized),其他人必须排队,出来时自动开门(解锁)。


 2. ReentrantLock(可重入锁)

作用

  • 和 synchronized 类似,但更灵活,支持公平锁、可中断、超时等待等高级功能。

特点

  1. 手动加锁 & 解锁(必须显式调用 lock() 和 unlock())。

  2. 可重入(和 synchronized 一样)。

  3. 支持公平锁(先等待的线程先获得锁)。

  4. 可中断lockInterruptibly(),等待锁时可被中断)。

  5. 超时尝试获取锁tryLock(timeout))。

使用方式

ReentrantLock lock = new ReentrantLock(); // 默认非公平锁
// ReentrantLock lock = new ReentrantLock(true); // 公平锁

lock.lock(); // 加锁
try {
    // 临界区代码
} finally {
    lock.unlock(); // 必须手动解锁!
}

适用场景

  • 需要更精细控制的锁,比如:

    • 避免死锁(可超时、可中断)。

    • 需要公平锁(避免线程饥饿)。

现实比喻

  • 就像高级会议室预约系统

    • 你可以选择公平模式(先到先得)。

    • 可以设置超时(等太久就放弃)。

    • 可以被老板打断interrupt())。


⚡ 3. volatile(易变变量)

作用

  • 保证变量的可见性(一个线程修改后,其他线程立即可见)。

  • 禁止指令重排序(避免多线程下的诡异问题)。

特点

  1. 不保证原子性(比如 i++ 仍然不安全)。

  2. 轻量级同步,比 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() 无锁
可重入
公平锁 ❌(非公平) ✅(可配置)
可中断
超时获取锁
适用场景 简单同步 复杂锁控制 状态标记、单例模式

 如何选择?

  1. 简单同步 → synchronized(代码简洁,自动管理锁)。

  2. 高级需求(公平锁、可中断) → ReentrantLock

  3. 仅需可见性,不涉及复合操作 → volatile

经典问题

  • i++ 为什么不能用 volatile

    • 因为 i++ 是 读取-修改-写入 三个操作,volatile 只能保证读取时是最新值,但不能保证整个操作的原子性。

    • 解决方案:用 synchronized 或 AtomicInteger


 现实场景举例

  1. 银行取钱

    • synchronized:ATM 机一次只服务一个人(简单互斥)。

    • ReentrantLock:VIP 窗口可以设置超时或插队(更灵活)。

    • volatile:余额变动后,所有 ATM 立即显示最新金额(可见性)。

  2. 单例模式

    • volatile + DCL:避免指令重排序导致的问题。


  • synchronized:简单、自动,适合大多数场景。

  • ReentrantLock:灵活、强大,适合复杂需求。

  • volatile:轻量级,仅保证可见性,不替代锁。

5、什么是AQS?

 AQS 是什么?

想象一个银行排队办业务的场景:

  • AQS = 银行排队系统

    • 它管理着一个队列(排队的人),和一个状态变量(比如“当前可用的窗口数”)。

    • 如果窗口被占满(资源被占用),新来的人(线程)必须排队等待。

    • 当窗口空闲时(资源释放),系统按规则叫下一个排队的人(线程)去办理业务。

一句话:AQS 是一套管理线程排队和唤醒的通用模板,让开发者能轻松实现各种锁和同步工具。


 AQS 的核心组成

  1. state(状态变量)

    • 像银行窗口的剩余数量,用 volatile 保证可见性。

    • 比如 ReentrantLock 中,state=0 表示锁空闲,state=1 表示锁被占用。

  2. CLH队列(线程等待队列)

    • 像银行的排队取号机,用双向链表实现,公平地管理等待的线程。

  3. tryAcquire/tryRelease(需开发者自定义)

    • 像银行的业务规则(比如VIP优先),由具体的锁(如ReentrantLock)自己实现。


⚙️ AQS 的工作原理(以ReentrantLock为例)

1. 加锁(lock())
final void lock() {
    if (compareAndSetState(0, 1)) { // 尝试抢锁(CAS修改state)
        setExclusiveOwnerThread(Thread.currentThread()); // 抢成功,记录持有者
    } else {
        acquire(1); // 抢失败,进入队列排队
    }
}
  • 步骤

    1. 线程A尝试用CAS把 state 从 0 改成 1(模拟抢锁)。

    2. 如果成功,线程A获得锁。

    3. 如果失败,线程A进入队列等待,并可能被挂起(park())。

2. 解锁(unlock())
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;
}
  • 步骤

    1. 线程A释放锁,state 变回 0

    2. AQS唤醒队列中的下一个线程(线程B)。


 现实场景类比

  • 场景:奶茶店点单

    • state:当前可用的点单柜台数(比如1个)。

    • 队列:顾客(线程)排队,先到先得。

    • tryAcquire:规则是“每人限购1杯”(state从1变0)。

    • tryRelease:顾客拿到奶茶后,柜台空闲(state从0变1),叫下一个顾客。


 AQS 的经典实现

  1. ReentrantLock

    • 通过 state 记录重入次数,支持公平/非公平锁。

  2. CountDownLatch

    • state 初始值为N,每调用 countDown() 减1,减到0时唤醒所有等待线程。

  3. Semaphore

    • state 表示剩余许可证数量,获取/释放许可证时修改 state


 为什么AQS重要?

  1. 解耦

    • AQS处理排队和唤醒的复杂逻辑,开发者只需实现 tryAcquire 等少量方法。

  2. 高性能

    • 通过CAS和自旋减少线程挂起,提升并发效率。

  3. 统一标准

    • JDK中的锁、同步器基于AQS,保证行为一致。


 

  • AQS = 线程同步的通用模板,核心是 state + 队列 + CAS。

  • 开发者只需关注:如何获取/释放资源(实现 tryAcquire/tryRelease)。

  • 像银行排队:资源(state)不够时线程排队,资源释放时按规则唤醒。

理解AQS后,再学 ReentrantLockSemaphore 等会发现它们只是AQS的“皮肤”!

6、什么是Lock接口?

Lock 是 Java 提供的手动锁接口,用于替代传统的 synchronized,提供更灵活的加锁、解锁控制。

核心作用
  • 手动加锁 & 解锁(不像 synchronized 自动释放)。

  • 支持更高级功能:可中断锁、超时锁、公平锁等。

  • 基于 AQS(AbstractQueuedSynchronizer) 实现(如 ReentrantLock)。


为什么需要 Lock?

synchronized 的局限性:

  1. 无法手动控制锁的获取和释放(必须等代码块执行完)。

  2. 不可中断(线程一直阻塞,无法被 interrupt() 打断)。

  3. 不支持超时(不能设置“等锁最多等5秒”)。

  4. 非公平锁(不保证先等待的线程先拿到锁)。

Lock 接口解决了这些问题!


Lock 接口的核心方法

方法 作用
lock() 加锁(如果锁被占用,线程会阻塞)
unlock() 释放锁(必须手动调用,否则死锁!)
tryLock() 尝试加锁(非阻塞,成功返回 true,失败 false
tryLock(timeout) 超时尝试加锁(等一段时间,超时放弃)
lockInterruptibly() 可中断加锁(等待锁时,线程可被 interrupt() 打断)

代码示例

1. 基本用法(必须手动解锁!)
Lock lock = new ReentrantLock();

lock.lock();  // 加锁
try {
    // 临界区代码(线程安全)
} finally {
    lock.unlock();  // 必须放在 finally,防止异常导致锁未释放!
}
2. 超时锁(避免死等)
if (lock.tryLock(3, TimeUnit.SECONDS)) {  // 最多等3秒
    try {
        // 拿到锁,执行任务
    } finally {
        lock.unlock();
    }
} else {
    System.out.println("等了3秒还没锁,干别的去!");
}
3. 可中断锁(可被其他线程打断)
try {
    lock.lockInterruptibly();  // 可被 interrupt() 中断
    // 拿到锁后的操作
} catch (InterruptedException e) {
    System.out.println("被中断了,不等了!");
} finally {
    if (lock.isHeldByCurrentThread()) {  // 检查是否持有锁
        lock.unlock();
    }
}

Lock vs synchronized

特性 Lock synchronized
锁的获取 手动 lock() / unlock() 自动(代码块结束释放)
可中断 ✅(lockInterruptibly()
超时尝试 ✅(tryLock(timeout)
公平锁 ✅(new ReentrantLock(true) ❌(非公平)
性能 高竞争时更优 低竞争时更优
代码复杂度 需要手动管理锁 简单,但功能有限

注意事项

  1. 必须手动 unlock(),否则会导致死锁(建议用 try-finally 确保释放)。

  2. 不要嵌套使用,比如:

    lock.lock();
    lock.lock();  // 同一个线程重复加锁(可重入锁可以,但容易忘记解锁次数)

  3. 公平锁可能降低吞吐量(维护队列需要额外开销)。


使用场景

  • 需要精细控制锁(如超时、可中断)→ Lock

  • 简单同步 → synchronized(代码更简洁)

  • 高并发竞争 → Lock(性能更好)


总结

  • Lock 是 synchronized 的增强版,提供更灵活的锁控制。

  • 核心实现类ReentrantLock(可重入锁)、ReentrantReadWriteLock(读写锁)。

  • 一定要记得 unlock() 否则锁泄漏,系统卡死。

一句话

Lock = 手动挡汽车(精准控制),synchronized = 自动挡汽车(简单但功能有限)。

7、什么是同步器/计数器? 

1. CountDownLatch(倒计时门闩)

作用

  • 一个或多个线程等待,直到其他线程完成一组操作后再继续执行。

  • 类似火箭发射倒计时:所有准备工作(线程)完成后,主线程才能点火。

核心方法

方法 说明
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("所有检查完成,火箭发射!");

适用场景

  • 主线程等待多个子线程初始化完成。

  • 并行任务完成后汇总结果。


2. CyclicBarrier(循环栅栏)

作用

  • 一组线程互相等待,直到所有线程都到达某个屏障点(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();
}

适用场景

  • 多阶段任务(如游戏关卡、并行计算)。

  • 需要线程间协同工作的场景。


3. Semaphore(信号量)

作用

  • 控制同时访问某个资源的线程数量(限流)。

  • 类似停车场空位:只有空闲车位时,新车才能进入。

核心方法

方法 说明
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 限制并发访问数 限流、资源池管理

如何选择?

  1. 需要“主线程等子线程” → CountDownLatch

    • 比如:启动服务前,等待所有组件初始化完成。

  2. 需要“所有线程到齐后一起执行” → CyclicBarrier

    • 比如:并行计算,多个线程分阶段协作。

  3. 需要“限制同时运行的线程数” → Semaphore

    • 比如:数据库连接池、接口限流。


总结

  • CountDownLatch“人等事”(主线程等子线程)。

  • CyclicBarrier“事等人”(所有线程互相等)。

  • Semaphore“资源限流”(控制并发访问数)。

它们都是基于 AQS(AbstractQueuedSynchronizer) 实现的,理解AQS后,这些工具的原理就一目了然了!

8、什么是CAS?

CAS 是什么?

CAS(Compare And Swap,比较并交换) 是计算机的一种无锁并发操作,用来保证多线程修改变量时的安全性。
它的核心思想是:

“我认为现在的值是 A,如果是的话,我就把它改成 B;如果不是 A,说明被别人改过了,那我就不改了。”

现实比喻

想象你在玩一个多人抢红包游戏:

  1. 你看到红包里还剩 10元(这就是你读取的旧值 A=10)。

  2. 你想抢 5元,于是系统检查红包是否还是 10元

    • 如果是 → 成功扣掉 5元,红包变成 5元(新值 B=5)。

    • 如果不是 → 说明别人已经抢过,你得重新尝试

这就是 CAS 的过程!


⚙️ 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需要重试

CAS 的优点

  1. 无锁(Lock-Free)

    • 不需要 synchronized,减少线程阻塞,性能更高。

  2. 轻量级

    • 比锁更高效,适合简单的原子操作(如计数器)。


⚠️ CAS 的缺点

  1. ABA 问题

    • 如果值从 A → B → A,CAS 会误以为没变(可以用 AtomicStampedReference 解决)。

  2. 自旋开销

    • 如果竞争激烈,线程会一直重试(消耗 CPU)。

  3. 只能保证一个变量的原子性

    • 多个变量要用锁或其他方式。


实际应用场景

  1. AtomicIntegerAtomicLong

    • 高并发计数器(如网站访问量统计)。

  2. 乐观锁(如数据库版本号控制)

    • 更新前检查版本号是否变化。

  3. 无锁数据结构(如 ConcurrentHashMap

    • 用 CAS 实现线程安全的插入、删除。


CAS vs 锁(synchronized)

CAS 锁(synchronized)
实现方式 无锁(CPU 指令) 阻塞(JVM 管程)
性能 高(无上下文切换) 低(竞争激烈时退化)
适用场景 简单原子操作 复杂同步逻辑
ABA 问题 存在 不存在

一句话总结

CAS = 乐观无锁并发控制,像抢红包一样“先看再改”,失败就重试,适合高并发简单操作!

9、http进化史

HTTP 进化史(1.0 → 2.0 → 3.0)


HTTP 1.0(1996年)

特点

  • 短连接:每次请求完就断开TCP连接(像打电话说完就挂)。

  • 无状态:服务器不记得你是谁,每次请求都要带完整信息(如Cookie)。

  • 简单但慢:下载网页要反复建立连接,效率低。

问题
❌ 打开一个网页(含图片/CSS/JS)要建立多次TCP连接,浪费资源。


HTTP 1.1(1999年)

改进

  • 长连接:一个TCP连接可以发多个请求(像电话不挂,连续聊天)。

  • 管道化(Pipelining):允许连续发多个请求(但响应必须按顺序返回,容易堵车)。

  • 缓存优化:支持 Cache-Control 等头部,减少重复下载。

遗留问题
❌ 队头阻塞(Head-of-Line Blocking)—— 如果第一个请求卡住,后面的全得等。
❌ 头部冗余(每次请求都带相同的Cookie/User-Agent,浪费流量)。


HTTP/2(2015年)

核心改进

  1. 二进制分帧

    • 数据拆成小块(帧),乱序发送,接收方组装(像乐高积木拼图)。

  2. 多路复用(Multiplexing)

    • 一个TCP连接上并行传输多个请求/响应(彻底解决队头阻塞)。

  3. 头部压缩(HPACK)

    • 用字典压缩重复的头部(比如 User-Agent 只传一次)。

  4. 服务器推送(Server Push)

    • 服务器可以主动推送资源(如提前把CSS推给浏览器)。

优点
网页加载速度显著提升(尤其多资源页面)。

遗留问题
❌ 仍基于TCP,TCP的丢包重传会拖累所有流(底层协议限制)。


HTTP/3(2022年正式标准化)

革命性变化

  1. 改用QUIC协议(基于UDP,非TCP):

    • 解决TCP队头阻塞问题(即使丢包,其他流不受影响)。

  2. 0-RTT快速连接

    • 首次连接后,下次访问无需握手(类似“记住密码”)。

  3. 更好的移动网络支持

    • 网络切换(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)”。

目的始终是:更快、更稳、更省流量! 

你可能感兴趣的:(java,开发语言)