1.juc包下你常用的类?

“在日常开发中,我常用的 JUC 类主要分为 4 大类1. 线程池相关 (Thread Pool) —— 最常用

  • ThreadPoolExecutor:这是最核心的类,用于自定义创建线程池,我们在生产环境一般都用它来控制线程数量,避免资源耗尽。
  • Executors:这是个工厂类,虽然方便(比如 newFixedThreadPool),但在大厂规范里通常禁止直接使用,因为它容易导致 OOM,所以我们主要用它来辅助理解,实际都手写 ThreadPoolExecutor^3daa81

2. 并发集合类 (Collections)

  • ConcurrentHashMap:这是高并发下替代 HashMap 的神器,通过分段锁(JDK 7)或 CAS + synchronized(JDK 8)保证了线程安全且高性能。
  • CopyOnWriteArrayList:适合读多写少的场景(比如白名单缓存),写的时候复制新数组,读的时候不加锁。

3. 并发工具类 (Tools)

  • CountDownLatch:用于‘倒计时’。比如主线程等待 5 个子线程都执行完,才汇总结果。
// 1. 创建一个计数器,初始值为 5
CountDownLatch latch = new CountDownLatch(5);
 
for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + " 交卷了!");
        // 2. 每一个线程做完事,就让计数器减 1
        latch.countDown(); 
    }).start();
}
 
// 3. 主线程在这死等,直到计数器变成 0 才会继续往下走
latch.await(); 
 
System.out.println("所有人都交卷了,老师锁门回家!");
  • CyclicBarrier:用于‘循环栅栏’。比如让一组线程互相等待,都到了集结点再一起并发执行。
  • Semaphore:用于‘信号量’。主要用来限流,比如只允许 10 个线程同时访问数据库。

4. 原子类 (Atomic)

  • AtomicInteger / AtomicLong:基于 CAS(Compare-And-Swap,比较并交换) 原理实现的无锁原子更新,常用于做计数器(比 synchronized 性能高)。”

2.怎么保证多线程安全?

策略分类核心手段典型代表适用场景
互斥同步 (锁)阻塞其他线程synchronized, ReentrantLock复杂的业务逻辑,临界区代码较长
非阻塞同步CAS 自旋AtomicInteger, LongAdder简单的数值计算,计数器
数据隔离线程独享ThreadLocal数据库连接,Session 管理
不可变只读String, final 变量配置信息,常量
可见性保障内存屏障volatile状态标志位,单例模式

BlockingQueue

  • 线程 A(生产者)把数据丢进队列。

  • 线程 B(消费者)从队列另一头取数据。

  • 交换意义:这实现了数据的所有权转移。数据从 A 的私有空间,通过队列,正式移交给了 B。

3.Java中有哪些常用的锁,在什么场景下使用?

1. 内置锁 synchronized (最基础)

  • 特点: Java 语言层面的关键字,使用最简单。它会自动加锁和释放锁。

  • 升级机制: JVM 对它做了大量优化,支持偏向锁 轻量级锁 重量级锁的升级过程,性能已经很不错了。

  • 场景: 绝大多数需要同步的常规业务场景,代码简洁,不易出错。

2. 显式锁 ReentrantLock (最灵活)

  • 特点: JUC 包下的 API 锁。需要手动 lock()finallyunlock()

  • 优势: 比 synchronized 强大,支持公平锁(先来后到)、可中断(等不到锁就算了)、超时获取等高级功能。

  • 场景: 需要精细控制锁的逻辑,或者需要实现公平锁的场景。

3. 读写锁 ReadWriteLock / ReentrantReadWriteLock

  • 特点: 把锁分成了‘读锁’和‘写锁’。

    • 读锁是共享的(大家都能读)。

    • 写锁是独占的(我写的时候,你们不能读也不能写)。

  • 场景: 典型的读多写少场景,比如缓存系统、配置中心,能极大提升并发性能。

4. 乐观锁 (CAS 机制)

  • 特点: 其实不是一种‘锁’,而是一种无锁算法。典型的实现是 AtomicInteger 等原子类。

  • 原理: 修改时不加锁,而是判断一下‘原来的值有没有被别人改过’,没改过就更新,改过就重试。

  • 场景: 并发竞争不激烈的场景(比如简单的计数器)。如果竞争太激烈,会导致 CPU 空转(自旋)严重。”

4.怎么在实践中用锁的?

锁类型核心实践规范 (Best Practice)后果 (如果不遵守)
synchronized缩小锁范围 (只锁核心代码)范围太大导致性能急剧下降 (串行化)
ReentrantLock必须在 finally 中 unlock业务抛异常会导致死锁 (锁永远不释放)
ReadWriteLock区分读写操作用错锁会导致性能没有提升

5.Java 并发工具你知道哪些?

“在 java.util.concurrent 包下,我最常用的并发工具主要有三个:CountDownLatchCyclicBarrierSemaphore

1. CountDownLatch (倒计时门闩)

  • 作用: 它允许一个或多个线程等待其他线程完成操作。

  • 原理: 初始化一个计数器(比如 5),每次一个任务完成就调用 countDown() 减 1。主线程调用 await() 阻塞等待,直到计数器归零,门闩打开,主线程继续执行。

  • 场景: 比如系统启动时,主线程要等待数据库连接、缓存加载、配置读取这 5 个子任务都初始化完毕,才能正式对外提供服务。

2. CyclicBarrier (循环栅栏)

  • 作用: 让一组线程互相等待,直到所有人都到达某个屏障点(Barrier),再一起继续执行。

  • 与 CountDownLatch 的区别: CountDownLatch 是一次性的(减法),而 CyclicBarrier 是可循环利用的(加法),且它是多线程互相等待,而不是主线程等子线程。

  • 场景: 比如多线程计算,把一个大表格拆成 4 份给 4 个线程算,大家都算完了,在栅栏处汇合,再汇总结果。或者像游戏加载,10 个玩家都加载到 100% 了,才一起开始游戏。

3. Semaphore (信号量)

  • 作用: 用于控制同时访问特定资源的线程数量(限流)。

  • 原理: 比如有 3 个许可证,来了 10 个线程。前 3 个拿走许可证(acquire)执行,剩下的 7 个排队。等有人归还(release)了,后面的人才能进。

  • 场景: 流量控制。比如数据库连接池只有 50 个连接,为了不把数据库打挂,我们可以用 Semaphore 限制同时只有 50 个线程能访问 DB。”

  • Callable (任务制造者)

    • 它是一个函数式接口,类似于 Runnable

    • 核心方法是 call()

    • 特点: 有返回值(泛型 V),并且可以抛出异常。

  • Future (结果领取凭证)

    • 它是用来接收 Callable 任务执行结果的‘票据’。

    • 当我们把 Callable 扔给线程池执行时,线程池会立刻返回一个 Future 对象给我们。

    • 核心功能: 我们可以拿着这个 Future 去检查任务是不是做完了(isDone),或者阻塞等待获取结果(get),甚至取消任务(cancel)。

6.CountDownLatch 是做什么的讲一讲?

CountDownLatch(倒计时门闩)是 JUC 包下的一个同步工具类。 它的核心作用是:允许一个或多个线程等待其他线程完成操作

我们可以把它理解为一个**‘倒计时器’**。

1. 核心工作原理

  • 初始化时,我们给它设置一个计数器(比如 5)。

  • 子线程执行完任务后,调用 countDown() 方法,计数器减 1。

  • 主线程调用 await() 方法,此时主线程会被阻塞(卡在这里不动)。

  • 当计数器减到 0 时,门闩打开,阻塞的主线程被唤醒,继续执行后续代码。

2. 典型应用场景 它最经典的使用场景是 ‘一等多’(One waits for Many):

  • 系统初始化: 比如电商系统启动时,主线程需要等待 数据库连接池Redis 缓存MQ 消费者 这 3 个模块都初始化完毕,才能对外提供服务。

    • 初始化 new CountDownLatch(3)

    • 3 个模块启动完分别 countDown()

    • 主线程 await() 等它们都好了再放行。

  • 多线程数据汇总: 比如做一个 Excel 报表,需要查询 5 个不同的数据库表。我们可以开 5 个线程并行去查,主线程等它们都查完了,再聚合数据。

3. 与 CyclicBarrier 的区别 (加分项)

  • CountDownLatch一次性的,计数器减到 0 就废了,不能复用。

  • CyclicBarrier可循环的,而且侧重于‘多线程互相等待’。

7.synchronized和reentrantlock及其应用场景?

特性synchronizedReentrantLock(只能用在代码块上)
层级JVM 关键字JDK API 类
释放锁自动释放必须手动 unlock (在 finally 中)
公平性非公平默认非公平,可支持公平
中断响应不可中断可中断
条件队列单个 (wait/notify)多个 (Condition)
适用性简单场景,常规业务复杂并发控制,需要高级功能

8.除了用synchronized,还有什么方法可以实现线程同步??

“除了 JVM 层面的 synchronized 关键字外,JDK 的 JUC 包还提供了非常丰富的同步机制,主要有以下 3 类替代方案:

1. 使用显式锁 Lock 接口 (最直接的替代) 最典型的实现类是 ReentrantLock

  • 原理: 它是基于 AQS 实现的。

  • 优势: 相比 synchronized,它功能更强大。

    • 支持公平锁(先来后到)。

    • 支持非阻塞获取锁tryLock,拿不到就走,不傻等)。

    • 支持中断等待(等得不耐烦了可以取消)。

  • 场景: 需要灵活控制锁的获取和释放,或者需要公平锁时。

2. 使用原子类 Atomic (高性能无锁方案) 比如 AtomicIntegerAtomicReference

  • 原理: 它们不加锁,而是利用底层的 CAS (Compare-And-Swap) 机制来保证线程安全。

  • 优势: 性能比加锁高,没有上下文切换的开销。

  • 场景: 适用于简单的计数器状态标记等更新操作。

3. 使用并发工具类 (协作同步) 如果不是为了互斥,而是为了线程间的协作,可以使用:

  • ReadWriteLock (读写锁): 适合读多写少的场景,读读不互斥,提升并发性能。

  • Semaphore (信号量): 控制同时访问资源的线程数量(限流)。

  • CountDownLatch / CyclicBarrier 用于线程间的等待和协调。”

9.synchronized锁静态方法和普通方法区别?

“它们的区别主要在于**加锁的对象(锁的范围)**不同:

1. 锁的对象不同

  • 普通同步方法 (Instance Method): 锁的是当前实例对象 (this)

    • 也就是说,锁是属于具体的某一个对象的。
  • 静态同步方法 (Static Method): 锁的是当前类的 Class 对象 (ClassName.class)

    • 因为静态方法属于类,不属于对象,所以它锁住的是整个类(相当于全局锁)。

2. 作用范围与并发度不同

  • 普通方法: 如果创建了两个不同的对象 obj1obj2。线程 A 调用 obj1 的同步方法,线程 B 调用 obj2 的同步方法,它们互不干扰(因为锁的是不同的房子)。

  • 静态方法: 无论创建了多少个对象,静态方法的锁只有一把(Class 对象只有一份)。只要有一个线程正在执行静态同步方法,所有对象的其他线程想执行静态同步方法,都必须排队。

3. 是否互斥 (关键考点)

  • 它们之间不互斥!

    • 如果线程 A 访问 静态同步方法(拿的是 Class 锁)。

    • 此时线程 B 访问同一个类的 普通同步方法(拿的是对象锁)。

    • 结果: 它们可以同时运行,不会阻塞。因为它们抢的不是同一把锁。”

  • 普通方法 = 锁自家大门(不同实例之间不影响)。

  • 静态方法 = 锁小区大门(所有人进出都得排队)。

  • 两者混用 = 你锁你的小区门,我锁我的自家门,互不冲突。

10.怎么理解可重入锁?

1. 什么是可重入锁?(核心定义) 可重入锁,也叫递归锁。 它的含义是:当一个线程已经持有了某个锁,再次请求这个锁时,无需排队,可以直接进入。

简单来说就是:‘自己家的大门,自己手里有钥匙,进了一道门之后,屋里还有一道门,依然可以用这把钥匙直接打开,不需要去重新领钥匙’。

2. 为什么需要可重入性?(解决什么问题) 主要是为了避免死锁。 如果锁不可重入,当一个线程在执行 methodA(拿了锁)时,内部调用了 methodB(也需要这把锁)。如果不可重入,线程就会在门口自己等自己释放锁,结果永远等不到,导致自己把自己锁死

3. Java 中的代表

  • synchronized:天生就是可重入的(隐式实现)。

  • ReentrantLock:从名字就能看出是可重入的(显式实现)。

4. 底层实现原理 (ReentrantLock 视角) 它的底层通过 AQS 的 state 变量和一个 exclusiveOwnerThread 变量来实现:

  • 线程来抢锁时,判断 exclusiveOwnerThread 是不是自己?

  • 是自己: 直接允许进入,并且把 state 加 1(表示重入次数)。

  • 不是自己: 乖乖去排队。

  • 释放锁时: state 减 1,直到减为 0 时,才真正释放锁。”

11.synchronized 支持重入吗?如何实现的?

1. 结论 支持。 synchronized 是可重入锁。如果不可重入,子类调用父类的同步方法、或者在一个同步方法里调用另一个同步方法时,就会产生死锁,这显然是不合理的。

2. 实现原理 synchronized 的重入机制是基于 JVM 底层的 ObjectMonitor (监视器锁) 来实现的。 Monitor 对象内部维护了两个核心变量:

  • _owner:记录当前持有锁的线程。

  • _count(或 _recursions):记录锁的重入次数(计数器)。

特性synchronizedReentrantLock
实现层级JVM 层 (ObjectMonitor)JDK 层 (AQS)
计数器位置C++ 对象 (_count)Java 变量 (state)
释放方式编译器自动插入指令必须手动代码释放
逻辑本质Owner 匹配 + 计数器Thread 匹配 + 计数器

12.*syncronized锁升级的过程讲一下

“在 JDK 1.6 之前,synchronized 是重量级锁,性能较差。但在 1.6 之后,JVM 引入了锁升级机制,锁的状态会随着竞争情况逐渐升级,主要分为 4 个阶段

1. 无锁状态 (No Lock)

  • 对象刚被创建出来,没有任何线程来抢,Header 里的 Mark Word 是干净的。

2. 偏向锁 (Biased Lock) —— “一人独占”

  • 场景: 只有一个线程(线程 A)在访问同步块,完全没有竞争

  • 过程: JVM 会把线程 A 的 ID 记录在对象的 Mark Word 中。

  • 效果: 以后线程 A 再来,只要检查一下 ID 是自己,就直接进入,完全没有加锁解锁的开销(连 CAS 都不用做),性能极高。

3. 轻量级锁 (Lightweight Lock) —— “交替使用”

  • 场景: 线程 A 还没执行完,线程 B 来了。也就是说,有竞争,但竞争不激烈(锁持有时间短,线程交替执行)。

  • 过程: 偏向锁失效。JVM 会在当前线程的栈帧中创建一个 Lock Record(锁记录),然后尝试用 CAS 操作把对象的 Mark Word 替换为指向 Lock Record 的指针。

  • 效果: 谁 CAS 成功谁就拿到锁。没抢到的线程不会立刻阻塞睡觉,而是会自旋(Spin,空循环)几次,看看能不能马上等到锁释放(自旋锁)。

4. 重量级锁 (Heavyweight Lock) —— “激烈竞争”

  • 场景: 竞争很激烈。或者线程 B 自旋了很多次还是没抢到锁。

  • 过程: 锁膨胀为重量级锁。底层的 Mark Word 指向 ObjectMonitor

  • 效果: 没抢到锁的线程会被挂起(阻塞),进入操作系统内核态等待,消耗较大的上下文切换资源。”

13.JVM对Synchornized的优化?

优化手段核心逻辑目的
锁升级偏向 轻量 重量匹配不同的竞争程度
锁消除逃逸分析,没别人看得到去掉多余的锁
锁粗化循环/连续加锁减少加锁频率
自适应自旋参考历史成功率避免 CPU 盲目空转

14.介绍一下AQS

AQS(抽象队列同步器)是 Java JUC 包下的一个抽象类,它是构建锁(如 ReentrantLock)和同步器(如 SemaphoreCountDownLatch)的基础框架。 它的核心思想是:如果请求的资源空闲,就将当前线程设置为工作线程;如果资源被占用,就将当前线程加入到一个队列中排队等待。

设计模式:模板方法模式 AQS 使用了模板方法模式

  • AQS 爸爸把排队、阻塞、唤醒这些脏活累活都干了(定义在顶层逻辑里)。

  • 具体的同步器(如 ReentrantLock)只需要实现 tryAcquire(尝试获取资源)和 tryRelease(尝试释放资源)这两个简单的钩子方法,告诉 AQS ‘什么样的状态算获取成功’ 即可。”

15.CAS 和 AQS 有什么关系?

  • AQS (AbstractQueuedSynchronizer) 就像是 银行的排队叫号系统(框架),它规定了谁先来谁后到、谁该排队。

  • CAS (Compare-And-Swap) 就像是 取号机的出票动作(底层原子操作)。

    • 不管有多少人挤在取号机前,CAS 保证了**同一张号码(state)**只能被一个人拿走,不可能两个人拿到同一个号。

AQS 的底层实现强依赖于 CAS。 AQS 是一个同步框架,而 CAS 是这个框架中用于保证并发安全修改数据的核心工具。

16.如何用 AQS 实现一个可重入的公平锁?

“要实现这样一个锁,核心是自定义一个内部类去继承 AbstractQueuedSynchronizer,并重写它的 tryAcquiretryRelease 方法。具体逻辑如下:

特性实现逻辑关键 API
公平性抢锁前先看有没有人排队!hasQueuedPredecessors()
可重入锁被占时,检查是否是自己current == getExclusiveOwnerThread()
原子性修改状态必须用 CAScompareAndSetState(0, 1)

1. 定义状态 (State)

  • 使用 AQS 的 state 变量。

  • state = 0:表示锁空闲。

  • state > 0:表示锁被占用,数值表示重入的次数

2. 实现获取锁 (tryAcquire) —— 核心难点 这是抢锁的逻辑,必须同时满足公平性可重入性

  • 第一步:获取当前 state

  • 第二步:如果 state == 0 (锁是空的):

    • 关键点 (体现公平性): 必须先判断 ‘队列里有没有人在排队’ (hasQueuedPredecessors())。

    • 只有 队列为空(没人排队)或者 我是队头 时,才允许尝试 CASstate 从 0 改为 1。

    • CAS 成功后,标记当前线程为 ExclusiveOwnerThread (持有锁的线程)。

  • 第三步:如果 state > 0 (锁被占了):

    • 关键点 (体现可重入性): 判断 ‘持有锁的线程是不是我自己’ (current == getExclusiveOwnerThread())。

    • 如果是自己,就将 state 加 1 (state + acquires),表示重入成功。

    • 如果不是自己,返回 false (抢锁失败,AQS 会把你扔进队列排队)。

3. 实现释放锁 (tryRelease)

  • 获取 state 并减去释放量 (state - releases)。

  • 如果减完后 state == 0,说明锁完全释放了,清空持有线程 (setExclusiveOwnerThread(null))。

  • 更新 state 的值。”

16Threadlocal作用,原理,具体里面存的key value是啥,会有什么问题,如何解决?

6.了解的哈希冲突解决方法有哪些? 作用 原理 (Map) 内存泄漏 (核心坑) 解决方案

ThreadLocal 的核心作用是实现线程间的数据隔离。 它能让每个线程都拥有自己独立的变量副本,互不干扰。这就好像每个线程都有自己的‘私人保险箱’,不需要加锁也能保证安全。

  • 典型场景: 存储数据库连接、Session 信息、或者 Spring 事务管理中的 Connection。
维度说明
存储位置数据存在 Thread (线程) 里的 ThreadLocalMap
KeyThreadLocal 对象 (弱引用)
Value目标数据 (强引用)
致命问题内存泄漏 (Key没了,Value还在)
解决方案用完必须 remove()
// 定义
static ThreadLocal<User> userContext = new ThreadLocal<>();
 
try {
    // 1. 存数据
    userContext.set(new User("小林"));
    // 2. 业务逻辑...
} finally {
    // 3. 【必须】清理数据,防止内存泄漏
    userContext.remove();
}

17.悲观锁和乐观锁的区别?

特性悲观锁 (Pessimistic)乐观锁 (Optimistic)
心态总觉得有人抢觉得没人抢
动作先加锁,后操作先操作,更新时检查
线程状态阻塞 (Block)自旋 (Spin) / 重试
实现代表synchronized, ReentrantLockAtomic 类, 版本号机制
缺点性能低,上下文切换开销大高并发写时 CPU 飙高 (自旋)
最佳场景写多读少 (高冲突)读多写少 (低冲突)

18.Java中想实现一个乐观锁,都有哪些方式?

方式核心机制底层依赖典型应用
CAS 算法比较并交换 + 自旋CPU 硬件指令 (cmpxchg)AtomicInteger, ReentrantLock (内部状态)
版本号机制Compare Version数据库 UPDATE 条件库存扣减、抢票系统
整数换成了时间戳。但时间戳可能存在精度问题(比如两台机器时间不同步),所以推荐优先用版本号。”

19.CAS 有什么问题,Java是怎么解决的?

问题核心描述Java 解决方案
ABA 问题狸猫换太子 (ABA)副作用:栈顶元素变化AtomicStampedReference (加版本号)
CPU 开销大很多线程一直自旋LongAdder (分段锁思想) 或 加锁
单变量局限只能改一个值AtomicReference (封装成对象)

20.voliatle关键字有什么作用?

volatile 是 Java 虚拟机提供的轻量级同步机制,它主要有 2 个 核心作用:

1. 保证变量的可见性 (Visibility)

  • 原理: 在 JMM(Java 内存模型)中,线程操作变量通常是先改自己工作内存里的副本,不一定立刻写回主内存。

  • volatile 的作用: 一旦一个变量被 volatile 修饰,只要某个线程修改了它,JMM 会强制:

    1. 立刻把新值刷新回主内存

    2. 强制让其他线程工作内存中的缓存失效,下次读取时必须去主内存拿最新值。

  • 效果: 保证了‘一个线程改了,其他线程立刻就能看见’。

2. 禁止指令重排序 (Ordering)

  • 原理: 编译器和处理器为了优化性能,可能会调整代码的执行顺序(指令重排)。

  • volatile 的作用: 它通过插入 内存屏障 (Memory Barrier) 来禁止特定类型的重排序。

  • 经典场景: 单例模式的 DCL(双重检查锁)

    • instance = new Singleton() 分为三步:1.分配内存 2.初始化 3.引用指向地址。

    • 如果不加 volatile,可能会重排成 132。导致另一个线程拿到一个还没初始化完的半成品对象(不为 null 但没数据),从而报错。

3. 【重要】它不保证原子性 (Atomicity) 这是面试官最爱挖的坑。

  • volatile 只能保证读/写的那一刻是原子的,但像 i++ 这种‘读-改-写’复合操作,volatile 救不了

  • 例子: 10 个线程同时对 volatile int i 进行 i++,结果往往小于预期。解决原子性问题必须用 synchronizedLockAtomic 类。”

21.指令重排序的原理是什么?

1. 什么是指令重排序? 指令重排序是指编译器或处理器为了优化程序性能,在不改变单线程执行结果的前提下,改变代码原有的执行顺序。 简单来说,就是:你写的代码顺序和计算机真正执行的顺序可能是不一样的。

2. 为什么要重排序?(原理/好处) 核心目的是 提高运行效率

  • CPU 层面: 现代 CPU 都有流水线(Pipeline)技术。如果严格按顺序执行,前一条指令(比如读内存)卡住了,后面的指令哪怕和它没关系,也得干等。为了不让 CPU 闲着,处理器会把后面不依赖前者的指令提到前面先做(指令级并行)。

  • 编译器层面: 编译器在生成字节码或机器码时,也会调整顺序,以便更好地利用寄存器或减少指令数。

3. 重排序的分类 主要分为三种:

  • 编译器优化重排序: 编译器在编译阶段搞的。

  • 指令级并行重排序: 处理器在执行阶段搞的。

  • 内存系统重排序: 由于 CPU 缓存和读写缓冲区的存在,导致数据写入主内存的顺序看起来像是乱序的。

4. 带来的问题(核心考点)单线程环境下,重排序是安全的(As-if-serial 语义保证了结果不变)。 但在多线程环境下,重排序就是灾难。

  • 例子: 线程 A 先 init = true,再 context = load()(被重排了)。线程 B 看到 init 为 true 了,冲进去拿 context,结果发现是空的,直接报错。

5. 如何禁止重排序? Java 提供了 volatile 关键字。

  • 它的底层原理是插入了 内存屏障 (Memory Barrier)

  • 屏障就像一堵墙,告诉编译器和 CPU:‘这行代码上面的指令不准跑到下面去,下面的也不准跑到上面来’,从而保证了有序性。”

22.volatile可以保证线程安全吗?

举个经典例子:i++ 假设有一个 volatile int count = 0;。 当执行 count++ 时,在底层其实分为三步:

  1. 读: 从主内存读取 count (0)。

  2. 改: 在 CPU 寄存器里加 1 (变成 1)。

  3. 写: 把 1 写回主内存。

并发场景下:

  • 线程 A 读取了 0,还没来得及加。

  • 线程 B 同时也读取了 0(因为 A 还没写回,所以 B 读到的也是 0)。

  • 线程 A 加完写回 1。 (两者都是在CPU寄存器中完成,而不用读主内存)

  • 线程 B 加完也写回 1。

  • 结果: 两个线程各加了一次,理论上应该是 2,实际上主内存里是 1。数据丢失了。

是的,必须要在 B 还没有把数据“装进口袋”(寄存器)之前,A 的写回才对 B 有效。 如果 B 已经开始算了,那就太晚了。 volatile 的作用范围:仅限于“读”的那一刻。它不负责监视后续的计算过程。

23.volatile和synchronized比较?

维度volatilesynchronized
修饰目标变量方法、代码块
原子性❌ 不保证保证
可见性✅ 保证✅ 保证
有序性✅ 保证 (禁止重排)✅ 保证
是否阻塞非阻塞阻塞
开销很小较大 (有上下文切换)

24.什么是公平锁和非公平锁?

特性公平锁非公平锁
抢锁策略严格排队 (FIFO)尝试插队,失败再排队
吞吐量 (减少了唤醒开销)
饥饿问题 (可能饿死老线程)
默认情况ReentrantLock 默认 / synchronized

25.非公平锁吞吐量为什么比公平锁大?

总结 非公平锁相当于在‘唤醒排队者’的漫长过程中,插进去了一个动作极快的线程。 这样在相同的时间内,非公平锁多处理了一个任务,所以吞吐量更大,但代价是队列里的线程可能会饥饿。”

26.Synchronized是公平锁吗?

27.ReentrantLock是怎么实现公平锁的?

ReentrantLock 实现公平锁的核心,在于它在尝试获取锁(tryAcquire)时,增加了一个排队检查的逻辑。

核心判断逻辑 在公平锁的 tryAcquire 方法中,即使发现当前锁的状态是空闲的(state == 0),它也不会直接去抢锁(CAS)。 它会多做一个判断:调用 hasQueuedPredecessors() 方法。

  • 含义: 这个方法就是问 AQS 队列:‘我看锁是空的,但我前面还有人在排队吗?’

  • 结果:

    • 如果有人在排队:那我不能插队,我也乖乖去队尾排队。

    • 如果没人排队(或者我是队头):那我才可以尝试用 CAS 去抢锁。

28.什么情况会产生死锁问题?如何解决?

必要条件描述破解之道
互斥资源独占改用无锁/读写锁 (难完全避免)
请求与保持占着茅坑不拉屎一次性申请所有资源
不剥夺没人能抢我的锁使用 tryLock 主动释放
循环等待A等B,B等A按固定顺序加锁 (最推荐)