NAA's Blog

Back

Java并发#

image.png

线程基础知识#

1. 什么是 Java 中的线程同步?#

回答重点#

怎么保证线程安全?

**是什么:**是一种机制,一种在多线程环境下,确保同一时刻,只有一个线程可以执行共享资源的临界区代码

为什么:因为多个线程同时对共享资源(如变量、对象或文件)进行访问或写入的操作,可能会导致数据不一致、静态条件、死锁等问题的产生。

怎么做:通过加锁的方式,来实现临界区的代码在同一时刻只能被一个线程执行。

怎么加锁呢?

  1. synchronized关键字,通过在方法或者代码块上添加synchronized关键字,实现同步机制

  2. 通过ReenTrantLock类的 lock() 和 unlock() 方法实现

  3. 通过原子类实现,Java中 的 Atomic 类提供了一系列的原子类,例如:AtomicInteger、AtomicLong、AtomicRefrence等,通过这些原子类,可以实现在不使用java代码加锁的形式实现同步机制,原理是这些原子类通过硬件级别的原子操作来实现操作的原子性。

扩展知识#

共享资源:** **在多线程或者分布式系统中,多个线程、进程、组件都可以执行访问或者写入的操作的资源,常见的共享资源是数据或者对象,比如:实例变量、静态变量、集合等

静态条件: 当多个线程竞争同一个资源的时候,线程的输出或者行为,取决于线程被CPU调度的时机,而不是程序本身的逻辑

临界区: 是一段代码,这段代码里写的是,在多线程环境下,对共享资源进行访问或写入的代码逻辑

Java 中常见的同步方式#

https://www\.yuque\.com/kusanagi\-mdqmk/kb/a6a655112e42f32fc335066b2df45513\#EJWuq

synchronized#

Java 提供的加锁关键字,用于在方法或代码块上加锁,以确保同一时刻只有一个线程能够执行被同步的方法或代码块。

synchronized实现同步机制:

  1. synchronized加在 实例方法 上,锁住的对象是调用方法的实例对象

  2. synchronized加在 静态方法 上,锁住的是类对象

  3. synchronized加在 代码块 上,可以指定一个特定的对象作为锁,此方法更加灵活

**线程通信:**在 synchronized 可以使用 wait()、notify() 和 notifyAll() 实现条件等待通知。

  • wait():当前线程进入等待状态,直到被其他线程唤醒。必须在同步块或同步方法中调用。

  • notify():唤醒一个等待的线程。如果有多个线程在等待,同一时刻只能唤醒一个。

  • notifyAll():唤醒所有等待的线程。

例如在 synchronized 块或方法中,可以使用 wait() 方法使线程等待某个条件满足,可使用 notify() 或 notifyAll() 方法唤醒等待的线程。

ReentrantLock#

是 JUC (java.util.concurrent)提供的可重入锁,相比 synchronized 它更加灵活

ReentrantLock 使用 Condition 对象来提供了更灵活的等待/通知机制。每个 ReentrantLock 可以创建一个或多个 Condition 对象,通过 newCondition() 方法创建。

  • await():使当前线程等待,直到收到信号或被中断。

  • signal():唤醒一个等待线程。

  • signalAll():唤醒所有等待线程。

相比于 synchronized,ReentrantLock 还提供了可中断、可超时、公平锁、多条件变量等特性。

2. 线程和进程?线程状态?#

线程、进程 和 协程#

image.png

image.png

进程是资源分配的最小单位,线程是CPU调度和运行的最小单位

  • 线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程不拥有系统资源,只有运行必须的一些数据结构。

  • 它与父进程的其它线程共享该进程所拥有的全部资源。线程可以创建和撤消线程,从而实现程序的并发执行。 一般,线程具有就绪阻塞运行三种基本状态。

  • 在多CPU的系统里,不同线程可以同时在不同的CPU上运行甚至当它们属于同一个进程时也是如此。大多数支持多处理器的操作系统都提供编程接口来让进程可以控制自己的线程与各处理器之间的关联度(affinity)。

  • 线程也称作轻量级进程。就像进程一样,线程在程序中是独立的、并发的执行路径,每个线程有它自己的堆栈、自己的程序计数器和自己的局部变量。但是,与分隔的进程相比,进程中的线程之间的隔离程度要小。它们共享内存、文件句柄和其它每个进程应有的状态。

  • 单核CPU在某一时刻其实只能运行一个线程,并不能真正实现并行。但操作系统会通过时间片轮转的调度方式,在多个线程之间频繁切换,让用户感觉好像多个任务“同时”进行(其实是轮流分时执行)。

线程、进程、协程有什么区别?

  • 进程是操作系统中进行资源分配的最小单位它拥有自己的独立内存空间和系统资源。每个进程都有独立的堆和栈,不与其他进程共享进程间通信需要通过特定的机制,如管道、消息队列、信号量等。由于进程拥有独立的内存空间,因此其稳定性和安全性相对较高,但同时上下文切换的开销也较大,因为需要保存和恢复整个进程的状态。

  • 线程CPU调度和运行的最小单位,线程共享进程的内存空间,包括堆和全局变量。线程之间通信更加高效,因为它们可以直接读写共享内存。线程的上下文切换开销较小,因为只需要保存和恢复线程的上下文,而不是整个进程的状态。然而,由于多个线程共享内存空间,因此存在数据竞争和线程安全的问题,需要通过同步和互斥机制来解决。

  • 协程是一种用户态的轻量级线程,其调度完全由用户程序控制,而不需要内核的参与。协程拥有自己的寄存器上下文和栈,但与其他协程共享堆内存。协程的切换开销非常小,因为只需要保存和恢复协程的上下文,而无需进行内核级的上下文切换。这使得协程在处理大量并发任务时具有非常高的效率。然而,协程需要程序员显式地进行调度和管理,相对于线程和进程来说,其编程模型更为复杂。 Java 在 jdk 19 引入了虚拟线程,算是 java 中的协程。

  • 什么是协程?Java 支持协程吗?

⭐线程状态#

在 Java 中,线程的生命周期可以细化为以下几个状态:

  • New(初始状态):线程对象创建后,但未调用 start() 方法。

  • Runnable(可运行状态):调用 start() 方法后,线程进入就绪状态,等待 CPU 调度。

  • Blocked(阻塞状态):线程试图获取一个对象锁而被阻塞,或者等待I/O被阻塞。

  • Waiting(等待状态):调用wait(),线程进入等待状态,需要被显式唤醒才能继续执行。

  • Timed Waiting(含等待时间的等待状态):调用sleep(),线程进入等待状态,但指定了等待时间,超时后会被唤醒。

  • Terminated(终止状态):线程执行完成或因异常退出。

image.png

3. 线程创建的方式有哪些?#

image.png

image.png

image.png

image.png

image.png

使用 Runnable 和 Callable 有什么区别?#

image.png

线程的 run() 方法 和 start() 方法有啥区别?#

run(): 调用 run() 方法就像调用普通方法一样,会在当前线程执行任务,不具备多线程效果**。**可以被调用多次

start():只有调用 start() 方法,才会真正创建新线程,然后自动调用该线程的 run() 方法,**实现多线程并发。**只能调用一次

4. 如何让线程/任务按照顺序执行?#

如何在 Java 中控制多个线程的执行顺序?

join,线程通信(wait+notify),ReentrantLock+condition,CountDownLatch,线程池放一个线程。

image.png

Synchronize + wait/notify#

join#

image.png

ReentrantLock#

notify() 和 notifyAll() 有什么区别?#

image.png

5. ⭐sleep() 和 wait() 有啥区别?#

wait和sleep都是放弃cpu使用权,进入等待状态。

sleep是Thread的静态方法,wait是Object的成员方法。

sleep通过 Thread.sleep()调用,不会释放锁,

wait则是需要首先获得锁lock,synchronize(lock),然后调用lock.wait()来进行等待,调用wait()就会释放该锁 lock。

image.png

6. 如何停止一个正在运行的线程?#

image.png

Volatile + 标记

这里要注意配合 volatile 关键字

image.png

使用thread.stop()方法(不推荐,方法已作废)

使用 thread.interrupt()

注意如果打断阻塞(sleep, wait, join)中的线程,线程会抛出 InterruptedException 异常

使用了interrupt()方法后,线程的thread.isInterrupted()会返回 true(默认是false)

image.png


线程安全#

7. ⭐说说 synchronized 锁升级?#

首先synchronized根据不同的使用场景,可能会使用偏向锁、轻量级锁、重量级锁。synchronize使用一个object作为锁,每个对象都有一个对象头markword,里面就指向了是无锁还是偏向锁、轻量级锁还是重量级锁

  1. 没有锁竞争,就一个线程获取锁的时候,是偏向锁,这时会在对象头markword里记录当前线程的id,该线程再次获取该锁,因为记录了id,就可以实现锁重入。

  2. **在偏向锁持有期间,另一个线程请求同一把锁,JVM 会撤销偏向锁,并升级为轻量级锁。**锁对象markword记录锁记录的地址,这时线程采用 CAS 自旋获取锁,避免用户态内核态切换导致比较大的消耗。如果CAS成功,那么这个线程就获得了锁;如果CAS失败,那么就会进入重量级锁阶段。

  3. 当轻量级锁阶段的CAS操作失败时,就会升级为重量级锁,底层用一个monitor(owner,waitset,entrylist)。重量级锁意味着线程会进入阻塞状态,等待锁的释放,会导致上下文切换,性能下降。此时没有竞争到锁的线程会被阻塞,插到一个entrylist里,需要等待锁释放来唤醒。线程释放重量级锁时,JVM 会唤醒所有阻塞的线程,允许它们再次尝试获取锁

(什么是自旋锁?)

自旋锁是在用户态层面实现的,是通过多次自旋去充实竞争锁,避免了用户态到内核态的切换的性能损耗。

image.png

image.png

上述说的 monitor 是synchronized 的默认实现方式,基于操作系统的互斥量(mutex)实现。每次加锁和解锁都需要进行内核态和用户态之间的转换,导致性能较低。适用于锁竞争激烈的情况

synchronized关键字根据不同的使用场景,可能会使用偏向锁、轻量级锁、重量级锁

Monitor(重量级锁)#

image.png

notify() 操作可以让 WaitSet 里面的线程转到 EntryList 中。 也就是说notify() 只是将等待队列中的一个线程从 WAITING 状态转为 BLOCKED 状态。建议在 synchronized 块的末尾调用 notify()

那么 lock 对象 是怎么和 monitor 关联上的呢?我们得看看lock对象在 JVM 中的内存结构

image.png

如果 lock 对象关联上了 monitor,那么lock对象头里面的 MarkWord 就会有一个指向 monitor 的指针,从而关联上 monitor。(重量级锁)

在Java SE 1.6版本之后,synchronized关键字根据不同的使用场景,可能会使用 偏向锁、轻量级锁或重量级锁

轻量级锁#

image.png

image.png

轻量级锁的加锁和解锁:(视频参考:https://www\.bilibili\.com/video/BV1yT411H7YK?t=503\.8\&p=95)

image.png

image.png

偏向锁(锁重入不用重新 CAS)#

image.png

image.png

总结,锁升级?#

synchronized关键字的锁状态(偏向锁、轻量级锁、重量级锁)是由 JVM 根据线程竞争情况动态调整的,这一过程称为锁升级​(或锁膨胀)。

image.png

8. 说说 JMM(Java 内存模型)?#

什么是 Java 内存模型(JMM)?

首先脑子浮现这个图

image.png

在多核CPU架构中,每个CPU都有自己的寄存器和多级缓存。

此时获取数据就有两种方式,第一种方式是直接去操作主内存,但是效率比较低,因为主存和CPU的速度差很多倍。第二种方式就是从缓存中读取,此时性能就非常高,所以CPU肯定是使用第二种方式读取。

这时候就有问题了,假如有一个共享变量,此时共享变量放在主内存里面的,比如有多线程同时去操作它,而且这两个线程分别在不同的CPU上运行,那么更新缓存的话也都是往自己这一块CPU上的缓存更新(其实CPU和缓存都是焊在一块的),所以这两个线程对共享遍历变量的操作是不可见的,所以就会线程安全问题。

而JMM的出现就是为了解决不同的CPU(线程)之间操作共享变量不可见的问题

JMM是啥?

JMM就是Java Memory Mode也就是Java内存模型。JMM是一种规范,并不是具体实现(注意)。JMM定义了JVM虚拟机和计算机内存(RAM)的工作方式。把内存分成主内存以及各线程的工作内存,主内存就是计算机内存(RAM),工作内存就是CPU缓存规定线程操作一个变量的时候不能直接在主内存里面去改,因为直接在主内存里面去改性能比较低,所以操作共享变量的时候首先得把共享变量复制一份放到工作内存里面,操作之后还要保证必须把这个共享变量刷到主内存里面让其他的CPU线程能够看到共享变量的修改。JMM是解决了可见性、有序性 和 原子性。

JMM解决了什么问题?

第一个:可见性。更改共享变量其他的线程也要看见,使用的就是volatile

第二个:有序性。保证指令不被重排,volatile

第三个:原子性。可以使用synchronied解决

happens-before规则?

这个就是规定了对共享变量的写操作对其他线程的读操作可见,以及执行顺序的保障。

如果A线程在B线程执行之前执行,那就是A happens before B,这个时候就需要保证可见性,也就是B能读取到A最新修改的值。然后保证了顺序性,不管编译器怎么样去优化,不管处理器怎么样去重排序,都不能影响A先执行然后再执行B这个原则。

注意JMM是一种规范,而happens before是规则。讲清除一点就是,JMM是一个规范,而happens-before是遵循这个规范之后表现出来的规则。

9. CAS 你知道吗?#

什么是 Java 的 CAS(Compare-And-Swap)操作?

CAS 能够保证原子性,且是在无锁状态保证原子性

假如有线程A和线程B,对一个变量 a = 1并发修改,线程A要进行 + 1,线程B要进行 - 1,我们要保证有序性,那最终结果应该还为1,就可以使用CAS。那线程A和线程B的工作内存都读取到主内存中 a = 1到各自的工作内存,各自执行操作,A线程工作内存中a变为2,B线程变为0,那这时 A线程工作内存的a刷到内存了,内存里的a变为了2,那次是线程B进行CAS操作的时候,就比较了原来内存里的a应该为1,但此时是2,那么就需要重新自旋,拉取新的a的值,a=2,再进行 - 1操作,此时就保证了有序性(可见性、原子性)。

包括我们synchronize里的轻量级锁,也是CAS操作,比较锁对象头里字段,如果是无锁,就可以进行加锁。如果指针是指向自己当前这个线程,如果是,就可以进行重入锁,或者进行解锁。如果是指向其他线程,那么就需要自旋等待。

优点:

  • 无锁并发:CAS 操作不使用锁,因此不会导致线程阻塞,提高了系统的并发性和性能。

  • 原子性:CAS 操作是原子的,保证了线程安全

缺点:

  • ABA 问题:CAS 操作中,如果一个变量值从 A 变成 B,又变回 A,CAS 无法检测到这种变化,可能导致错误。

  • 自旋开销:CAS 操作通常通过自旋实现,可能导致 CPU 资源浪费,尤其在高并发情况下。

  • 单变量限制:CAS 操作仅适用于单个变量的更新,不适用于涉及多个变量的复杂操作。

image.png

image.png

image.png

CAS 底层实现#

image.png

ABA 问题#

ABA 问题是指当变量值从 A 变为 B 再变回 A 时,CAS 操作无法检测到这种变化。解决 ABA 问题的一种常见方法是引入版本号或时间戳,每次更新变量时同时更新版本号,从而检测到变量的变化。

用版本号或者时间戳解决。做任何一个值的修改,都加一个版本号,在进行CAS操作的时候,除了比较内存中的实际值与期望值外,还比较版本号。版本号相同就修改,否则失败重试。

java里面的AtomicStampedReference,就使用版本号解决了ABA问题,里面是维护了一个int类型的stamp。

java底层CAS的实现使用的是Unsafe类。X86架构上CAS是使用的cmpxchg和lock指令实现的。AtomicInteger底层依赖于CAS。

CAS总线风暴:

lock指令会将缓存通过总线刷到主存,同时通过总线通知其它的cpu核更新自己的缓存,如果cas操作频繁,那么总线上的通信流量就会过大,从而导致总线风暴。

乐观锁和悲观锁#

悲观锁,**先拿锁后干活。**乐观锁,先干活后检验检验不对,那就自旋重干

image.png

数据库中哪里用到了悲观锁和乐观锁?#

悲观锁通常依赖于数据库自身的锁机制(行锁、表锁),主要由InnoDB存储引擎支持。

常见使用场景:

  • 事务中需要强一致性的数据读写

  • 银行转账、库存扣减等场景

典型用法:

  • SELECT ... FOR UPDATE:当前读,对读取的行加排他锁(写锁),其他事务不能修改、不能加FOR UPDATE锁,直到当前事务提交或回滚。

示例:

START TRANSACTION;
SELECT stock FROM product WHERE id = 1 FOR UPDATE;
UPDATE product SET stock = stock - 1 WHERE id = 1;
COMMIT;
plaintext
  • 上述查询会锁住id=1的那一行(记录锁,可看小林coding),其他事务不能再加FOR UPDATE锁,防止并发扣减库存导致超卖。

**乐观锁 **需应用层实现,MySQL本身没有内置的乐观锁机制。常用方法:在表中增加version字段或last_update_time字段

为啥要乐观锁:

  • 性能要求高,对一致性要求不是特别严格

  • 例如:订单状态修改、用户资料更新等

乐观锁怎么做:CAS操作

  1. 首先读取数据及其版本号

  2. 更新时带上版本号,只在版本号未变时更新成功,否则失败

示例:

-- 假设表结构:-- id | stock | version
-- 1. 查询当前库存和版本号
SELECT stock, version FROM product WHERE id = 1;

-- 2. 更新时,带上版本号UPDATE product
SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = 5;
-- 如果受影响行数为1,说明更新成功;为0,则说明有并发,需重试
plaintext

总结对比#

10. 说说 volatile 关键字的理解?#

volatile可以保证线程安全吗?

volatile关键字可以保证可见性,但不能保证原子性,因此不能完全保证线程安全。volatile关键字用于修饰变量,当一个线程修改了volatile修饰的变量的值,其他线程能够立即看到最新的值,从而避免了线程之间的数据不一致。

但是,volatile并不能解决多线程并发下的复合操作问题,比如** i++ 这种操作不是原子操作**,如果**多个线程同时对i进行自增操作,**volatile不能保证线程安全。对于复合操作,**需要使用 synchronized ****、**Lock 或 CAS 操作来保证原子性和线程安全

  • Synchronized:Synchronized是一种排他性的同步机制,保证了多个线程访问共享资源时的互斥性,即同一时刻只允许一个线程访问共享资源。通过对代码块或方法添加 Synchronized 关键字来实现同步。

  • Volatile:Volatile是一种轻量级的同步机制,用来保证变量的可见性和禁止指令重排序。当一个变量被声明为 Volatile 时,线程在读取该变量时会直接从内存中读取,而不会使用缓存,同时对该变量的写操作会立即刷回主内存,而不是缓存在本地内存中

image.png

image.png

① 保证线程间的可见性#

image.png

image.png

② 禁止指令重排序#

image.png

image.png

11. 什么是 AQS?#

简单来说** AQS** 就是多线程中的队列同步器,它是****JUC包中的多个组件的底层实现。起到了一个抽象、封装的作用,将一些排队、入队、加锁、中断等方法提供出来,便于其他相关 JUC 锁的使用,具体加锁、入队的实现都需要实现类自己控制。

常见的实现类比如 ReentrantLock、CountDownlatch、Semaphore都是用到了AQS。

AQS 常见的实现类有 ReentrantLock、CountDownLatch、Semaphore 等等。

对于锁机制:

AQS提供了两种锁的机制,分别是排他锁共享锁独占锁,多个线程中只能有一个线程去获得该锁的资源。比如lock中的ReentrantLock,他的一个实现就是用到了中AQS的一个独占锁的功能。 **共享锁,**就是允许多个线程同时获得锁的资源。比如CountDownlatch以及Semaphore都用到了AQS中的共享锁的功能。

对于state:

它主要通过维护一个volatile 修饰的共享状态(state),来管理线程对共享资源的访问,例如互斥锁state为0表示当前没有任何线程竞争到锁资源。而大于等于1表示已经有线程正在持有锁资源。一个线程来获得锁资源,会检查state是否为0,如果是0表示无锁,会通过CAS尝试把state变为1(如果不是0说明有人占了,看看是不是自己,如果是就重入),AQS采用了CAS机制去保证state复制变量更新的一个原子性注意这里,没获得锁的线程不是自旋,而是进行阻塞,加入到一个双向链表的结构中**。当获得锁资源的线程在释放锁之后会从这样一个双向链表的头部去唤醒下一个等待的线程,再去竞争锁。**

状态可以表示多种含义,例如在 ReentrantLock 中,状态表示锁的重入次数;在 Semaphore 中,状态表示可用的许可数

对于等待队列:

AQS 维护了一个** FIFO 的等待队列**,用于管理等待获取同步状态的线程。每个节点(Node)代表一个等待的线程,节点之间通过 next 和 prev 指针链接。当一个线程获取同步状态失败时,它会被添加到等待队列中,并自旋等待或被阻塞,直到前面的线程释放同步状态。 持有锁的线程释放锁时,会将state修改为0,并对阻塞队列的第一个线程进行唤醒,被唤醒的线程就可以进行锁的竞争了。

对于公平锁和非公平锁:

最后,AQS可以实现公平锁和非公平锁公平锁就是来了个线程,需要排到阻塞队列里面,排队等待锁。非公平锁就是该线程不管阻塞队列,直接去尝试更改state去竞争锁。

image.png

image.png

AQS 基本工作机制#

image.png

image.png

AQS 是公平锁吗?#

在 AQS 的不同实现类中,公平锁和非公平锁都能实现

image.png

12. 说说 ReentrantLock 的实现原理?#

ReentrantLock是 JUC 包下的一个基于 CAS + AQS**** 实现的一种 可重入 互斥锁

类似于synchronized,是一个互斥锁,可以保证线程安全。相对于synchronize,ReentrantLock具有以下的特点:

  1. 可中断

  2. 可设置获取锁的超时时间tryLock

  3. 可设置为公平锁

  4. 支持设置多个条件变量(不同线程可以等待在不同的条件变量上,然后唤醒特定条件变量上的线程)

image.png

实现原理#

image.png

ReentrantLock的多条件变量有什么用?使用场景呢?#

作用:

  • 精准控制线程等待/唤醒:每个条件变量都可以维护一组等待线程await() 让线程在对应的条件下等待,signal()signalAll() 唤醒特定条件下的线程。

  • 比 synchronized/notify/notifyAll 更灵活:传统的 wait/notify 只能唤醒全部等待的线程,无法区分不同等待条件的线程。而多条件变量能细分等待队列,实现更细粒度的线程协作

比如有一个容量固定的队列,有一些生产者进程和消费者进程,我们可以创建两个条件变量notFull、notEmtpy,队列满的时候,生产者会等待在notFull条件变量上,等待消费者线程进行唤醒;同样地,队列空的时候,消费者要消费则会等待在notEmpty条件变量上,等待生产者线程进行唤醒。这样就避免了一下唤醒所有的线程。

13. Synchronized 和 Lock 有啥区别?#

语法)synchronize是 java 提供的同步关键字,而 Lock 是 JUC 包里提供的一个接口,其中有很多实现类,例如ReentrantLock这样的可重入锁的实现类。 (锁的粒度)synchronize可以加在方法或者代码块上控制锁的粒度,而 lock 则更加灵活,可以通过lock.lock()和lock.unlock()方法来控制锁的粒度,可以自主决定什么时候加锁和释放锁,synchronize锁的释放是被动的,代码块执行结束或出现异常才会释放锁。 (底层实现)synchronize底层有一个锁升级的过程,ReentrantLock则是用了AQS + CAS操作来上锁的 (lock的特性)lock还可以实现可超时、可打断、公平锁、多条件变量等功能。 (性能)性能差别不大,synchronize引入了偏向锁、轻量级锁、重量级锁 和 锁升级的机制来优化。lock 则是通过CAS自旋锁 + AQS来优化。

Synchronized 修饰静态方法:锁的是这个类的 Class 对象。也就是说,无论创建了多少个该类的实例,所有的实例共享同一个锁,因为这个锁属于类本身而不是某个对象实例。

Synchronized 修饰实例方法:锁的是当前实例(调用该方法的对象),也就是这个对象的内在锁。这也就是说每个对象实例都有自己独立的锁。

image.png

wait/notify 和 await/signal 是怎么使用的?#

  • **wait/notify:**属于 Object 类的方法,必须在 synchronized 同步块或同步方法中使用。锁机制是基于对象的内置监视器锁(monitor),每个锁对象只有一个等待队列,功能相对简单,只支持基本的等待和唤醒。notifyAll() 会唤醒所有在该锁下等待的线程。lock.wait(), lock.notify()

  • await/signal:属于 Condition 接口的方法,必须在 ReentrantLocklock / unlock 之间使用。是基于显式锁(ReentrantLock),需要手动创建 Condition 对象。一个 Lock 可以创建多个 Condition,支持多个等待队列,可以精确控制唤醒特定条件下等待的线程,且支持超时等待不响应中断的等待等高级功能 condition.await(), condition.signal()

代码参考#

14. 死锁产生的条件是什么?#

image.png

死锁产生的 4 个必要条件:

互斥,不可剥夺,请求和保持,循环等待

⭐怎么预防死锁?#

从四个条件上来说明,

  • 破坏互斥?一般来说互斥条件是无法破坏的。

  • 破坏不可剥夺?可以,当有线程持有了一些资源,并请求新的资源没有得到满足时,那么就把已经保持的资源全部释放,待以后需要使用的时候再重新申请。(会有效率损耗)

  • 破坏请求和保持?线程必须一次性的请求所有需要使用的全部资源,可能造成资源利用率低。

  • 破坏循环等待,可以将每个资源编号,必须按照编号顺序来请求资源。

在数据库层面上呢?

  1. 设置事务等待锁的超时时间,在innodb中,设置参数 innodb_lock_wait_timeout 来设置超时时间

  2. 开启主动死锁检测,检测到死锁后,主动回滚其中一个事务,将 innodb_deadlock_detect 设置为 on。

⭐怎么进行死锁诊断?(怎么看程序里有没有死锁)#

首先使用jps查看是否有线程发生了死锁,定位到对应的threadid,然后用jstack -l threadid,来查看具体是哪里的代码发生了死锁

image.png

image.png

15. 聊一下 ConcurrentHashMap#

Java 中 ConcurrentHashMap 1.7 和 1.8 之间有哪些区别?

在jdk8及之后的版本,ConcurrentHashmap是由数组+链表+红黑树构成哈希表,并采用** **CAS + synchronized 来实现更细粒度的并发控制,

对于写put操作,会对key进行hash取值来知道落到哈希表哪个桶,如果这个桶为空,那么会通过CAS存入新的节点。 如果不为空,会对头结点进行加synchronize锁,然后equals判断是否为同一元素,如果是同一个元素,就会替换掉value值,不过不是同一元素,则直接存入。

然后对于读操作,这个过程几乎是无锁的,因此在性能方面会比HashTable高

image.png

image.png

  • 只有“空桶”时才用CAS(Compare And Swap)尝试无锁插入。

  • 如果不是空桶(即已经有链表/红黑树/已有元素),直接用synchronized加锁,不会用CAS。

  • 如果不同的key哈希到不同的桶,操作互不影响,可以高并发执行。只有当多个线程****操作同一个桶时,才会有锁竞争

16. 什么是线程安全?怎么保证线程安全?#

什么是?

线程安全是指多个线程访问某一共享资源时,能够保证一致性和正确性,即无论线程如何交替执行,程序都能够产生预期的结果,且不会出现数据竞争或内存冲突。在 Java 中,线程安全的实现通常依赖于同步机制和线程隔离。

  • 同步锁:通过 synchronized 关键字或 ReentrantLock 实现对共享资源的同步控制。

  • 原子操作类:Java 提供的 AtomicIntegerAtomicReference 等类确保多线程环境下的原子性操作。

  • 线程安全容器:如 ConcurrentHashMapCopyOnWriteArrayList 等,避免手动加锁。

  • ThreadLocal:类似于局部变量,属于线程本地资源,通过线程隔离保证了线程安全。

  • 局部变量:线程内独立的局部变量天然是线程安全的,因为每个线程都有自己的栈空间(线程隔离)。

怎么保证?

  1. 原子性。一个操作是不可分割的,要么全部完成,要么全部不做,中间不能被其他线程干扰。 使用 synchronized、lock、CAS 解决。

  2. 可见性。指一个线程对共享变量的修改,能及时被其他线程看到。 使用 volatile、synchronized、lock 解决。

  3. 有序性。指程序执行顺序按照代码的先后顺序进行。在并发环境下,编译器、CPU可能会对指令进行重排序优化,如果缺乏同步措施,实际执行顺序可能与代码书写顺序不一致。 使用 volatile、synchronized、lock 解决。

可以看到,锁(synchronized 和 lock)可以保证并发安全的三大特性。synchronizedReentrantLock 等互斥锁,都有这个效果。

  • 用锁包住的代码块,保证了锁内操作的有序性,不会被重排序到锁外,也不会把锁外操作重排序到锁内。

  • 不仅保证有序性,还保证了可见性和原子性。

线程池#

17. 线程池的执行原理?7大核心参数?#

核心线程数,总线程数,备用线程存活时间,时间单位,任务阻塞队列(Array还是Linked),线程工厂,拒绝策略

https://www\.yuque\.com/kusanagi\-mdqmk/kb/a6a655112e42f32fc335066b2df45513\#RRI1p

线程池是一种是一个线程复用技术,用于预先创建并管理一组线程,避免频繁创建和销毁线程的开销可以提高线程的利用率

线程池的核心参数#

在JDK5版本中提供了代表线程池的接口ExecutorService,而这个接口下有一个实现类叫ThreadPoolExecutor类,使用ThreadPoolExecutor类就可以用来创建线程池对象。

image.png

image.png

线程池执行流程?#

流程:来了一个任务,那么首先用核心线程来处理。如果核心线程满了,那么把任务加到阻塞队列里。如果阻塞队列满了,还有备用线程(临时线程),那么就调用备用线程来处理新任务(如果备用线程都没了,那就直接走拒绝策略)。

新来的任务处理完了之后,如果阻塞队列里还有未完成任务,那么核心线程和备用线程都可以去阻塞队列里去处理阻塞中的任务。

线程池中线程抛异常了会怎样?#

Java 线程池内部任务出异常后,如何知道是哪个线程出了异常?

1.自定义ThreadFactory,给每个线程都搞一个UncaughtExceptionHandler来处理异常

2.使用Future,使用submit方法提交,get方法的时候try-catch捕获异常

3.run方法内部,try-catch捕获异常

execute和submit的区别:

1.execute无返回值,submit会返回一个future对象;

2.异常处理区别: execute:如果任务在执行过程中抛出了未捕获的异常,那么这个异常会被包装成RuntimeException,并在线程终止时由Thread对象的getUncaughtExceptionHandler方法处理; submit:异常会被捕获并存储在Future对象中,可以通过Future.get()方法重新抛出。

3.execute只能提交Runnable任务;submit可以以提交Runnable任务 或者 Callable任务

线程池中的线程在执行任务时,如果抛出异常会怎么样?

execute提交的任务:如果没有自己处理这个异常,底层会remove掉这个抛异常的线程,然后addWorker一个新的线程。

submit提交的任务:异常会被捕获并且放在Future对象中,底层不会remove掉这个抛异常的线程,也不会addWorker一个新的线程。

任务的拒绝策略#

  1. 直接抛异常。AbortPolicy,直接抛出 RejectedExecutionException,这是默认的拒绝策略。适用于必须通知调用者任务未能被执行的场景

  2. 直接丢弃该任务。DiscardPolicy,也不抛异常。适用于对部分任务丢弃没有影响的场景,或系统负载高时不需要处理所有任务

  3. 丢弃任务队列最前的任务。DiscardOldestPolicy,删除最早的任务,然后重新提交当前任务。适用于希望丢弃最旧的任务以保证新的重要任务能够被处理的场景。

  4. 由调用者所在线程来执行任务。CallerRunsPolicy,由调用者线程执行。适用于希望通过减缓任务提交速度来稳定系统的场景。

image.png

使用线程池执行任务时,线程池执行的任务可以有两种,一种是实现****Runnable任务;一种是实现 ****Callable任务ThreadPoolExecutor线程池执行任务相关方法:

image.png

示例:

18. 说说线程池里的阻塞队列有哪些?#

阻塞队列主要用来阻塞队列的插入和获取操作,当队列满了的时候插入操作会被阻塞,直到队列有空位。当队列为空的时候获取操作会被阻塞,直到队列有值。

常用在实现生产者和消费者场景,在笔试题中比较常见。

常见的阻塞队列包括:

  1. ArrayBlockingQueue:一个有界队列,底层基于数组实现。需要在初始化时指定队列的大小,队列满时,生产者会被阻塞,队列空时,消费者会被阻塞。

  2. LinkedBlockingQueue:基于链表的阻塞队列,允许可选的界限(有界或无界)。无界模式下可以不断添加元素,直到耗尽系统资源。有界模式则类似于 ArrayBlockingQueue,但吞吐量通常较高。

  3. PriorityBlockingQueue:一个无界的优先级队列,元素按照自然顺序或者指定的比较器顺序进行排序。与其他阻塞队列不同的是,PriorityBlockingQueue 不保证元素的 FIFO 顺序。

  4. DelayQueue:一个无界队列,队列中的元素必须实现 Delayed 接口,只有当元素的延迟时间到期时,才能被取出。常用于延迟任务调度。**是一个优先级队列,任务根据它们的延迟时间被组织起来,延迟时间最短的任务排在队列的前面。这种数据结构允许任务按照预定的时间顺序被取出和执行,**线程池会不断检查队首元素的延迟时间,未到时间就阻塞等待。用于****定时、延迟任务

  5. SynchronousQueue: 没有容量,不能存储任何元素。它的主要作用是在线程之间传递数据,即一个线程放入数据,另一个线程取出数据。 有新任务就交给空闲线程执行,没空闲线程就尝试新建线程,否则拒绝。

image.png

  • ArrayBlockingQueue:一把锁,生产 消费必须串行。性能略低。

  • LinkedBlockingQueue:两把锁,put和take各自有锁,生产消费可以并发进行。put和take互不影响,提高了并发性能。

如果用无界的 LinkedBlockingQueue,线程池只会创建核心线程数的线程不会创建救急线程。这可能导致任务堆积甚至 OOM(OutOfMemoryError),要慎用无界队列!

19. 如何确定线程池的核心线程数?#

image.png

CPU 密集型任务#

CPU 密集型任务,就好比单纯的数学计算任务,它不会涉及 I/O 操作,也就是说它可以充分利用 CPU 资源(如果涉及 I/O,在进行 I/O 的时候 CPU 是空闲的),不会因为 I/O 操作被阻塞,因此不需要很多线程,线程多了上下文开销反而会变多。

根据经验法则,CPU 密集型任务线程数 = CPU 核心数 + 1

I/O 密集型任务#

I/O 密集型任务,有很多 I/O 操作,例如文件的读取、数据库的读取等等,任务在读取这些数据的时候,是无法利用 CPU 的,对应的线程会被阻塞等待 I/O 读取完成,因此如果任务比较多,就需要有更多的线程来执行任务,来提高等待 I/O 时候的 CPU 利用率。

根据经验法则,I/O 密集型任务线程数 = CPU 核心数 * 2 或更多一些。

(这句话一定要和面试官说)以上公式仅是一个纯理论值,仅供参考!

在生产上,需要考虑机器的硬件配置,设置预期的 CPU 利用率、CPU负载等因素,再通过实际的测试不断调整得到合理的线程池配置参数。

20. Executors 线程池种类有哪些?#

**4种。**使用 java.util.concurrent.Executors 类来创建。

  1. 固定线程数的线程池(无临时线程)。

  2. 单线程的线程池。

  3. 可缓存线程池。

  4. 延迟或周期执行的线程池。

image.png

image.png

image.png

image.png

内部是使用了一个优先级队列,越先执行的任务优先级越高。

21. 为什么不推荐 Executors 创建线程池?#

image.png

22. ⭐谈谈对 ThreadLocal 的理解?#

image.png

image.png

  1. 介绍一下 ThreadLocal

ThreadLocal就是让每个线程都可以存一份自己的数据副本,因此不需要考虑线程安全问题,在我做过的项目中一般用来存储用户信息,或者每个线程存放自己的数据库连接。因为后台中每一个请求都是一个线程,我们在后续需要这个数据的时候就可以很方便的获取这个信息。

而在每条线程Thread内部有各自的 ThreadLocalMap,内部是一个Entry数组,key值为ThreadLocal对象,value为变量副本。开发的时候一般把threadLocal声明为一个static静态变量,那么这个时候threadLocal对象就是所有线程共享的,调用 set 和 get 方法的时候,就是以当前的 threadLocal 对象为key,去ThreadLocalMap里去取value。由于每条线程Thread内部有各自的 ThreadLocalMap,故不用担心线程安全问题。

ThreadLocal的内存泄漏问题?

线程内的ThreadLocalMap里面维护了一个Entry数组key是弱引用的ThreadLocal,value是数据。如果一个ThreadLocal对象没有强引用来指向它,那么堆内存不足的时候会被GC掉这些弱引用的Key,那么就会出现key为null但是value不为null的情况,导致GC无法回收造成内存的泄漏,这个value就是泄漏的对象。

为什么 key 要设置为弱引用?

使用弱引用作为 ThreadLocal 的键可以防止内存泄漏。若 ThreadLocal 实例被不再需要的线程持有为强引用,那么当该线程结束时,相关的 ThreadLocal 实例及其对应的数据可能无法被回收,导致内存持续占用。

而弱引用允许垃圾回收器在内存不足时回收对象。这样,当没有其他强引用指向某个 ThreadLocal 实例时,它可以被及时回收,避免长时间占用内存。此时 key就变为null了,下次再调用get、set、remove的时候会自动清理key为null的Entry。

为什么value不设置为弱引用?value值后续可能会用到,如果设置为弱引用,可能会被GC错误回收

但是ThreadLocal在执行get、set、remove的时候会自动清理key为null的Entry,但是更好的做法还是在使用完ThreadLocal之后调用ThreadLocal中的remove()方法清空该Entry解决该问题

ThreadLocal原理:

ThreadLocalMap里面有个Entry数组,Entry的key是ThreadLocal对象,key是通过弱引用指向ThreadLocal对象的

,value是设置的值。

get方法:

get方法会先获取当前线程,然后得到当前线程的ThreadLocalMap,然后将当前的ThreadLocal对象作为key

从map里面拿到Entry,如果找到的话,直接返回这个entry中的value;

没有找到的话,就会调用初始化方法,返回初始值。

getEntry方法: key是传进来的ThreadLocal对象

通过key计算一个hash值,然后和hashmap一样,模上数组的长度-1,得到一个下标,如果这个下标对应的entry的key正好是

要找的ThreadLocal对象的话,就表示命中了直接返回;

没有命中的话,就从当前下标位置,顺序的遍历entry数组去找,找到就返回,找不到就返回null,也就是所谓开放式寻址法。

set方法:

根据key算一个hash值,模上数组的长度-1,得到一个下标,

然后从这个下标对应的entry元素开始,顺序遍历整个entry数组,检查这个下标是不是已经有元素了,如果有的话,对比一下

是不是要寻找的entry,如果是的话,直接更新entry对应的value;如果不是的话,就继续往下找,

如果找到一个null值了,说明entry还不存在,就创建一个新的entry。

image.png

image.png

  • 加锁 lock/同步块synchronized:适合多线程间共享变量,保证同一数据的安全,但有性能损耗。

  • ThreadLocal:每个线程独立变量,适合“每线程一份状态”场景,无锁高效

ThreadLocal 类基本使用#

  • threadLocal.set(value) 设置值

  • threadLocal.get() 获取值

  • threadLocal.remove() 清除值

在实际应用中,一个线程往往需要传递多个上下文信息,比如用户ID、请求ID、数据库事务ID等。

ThreadLocal 只能存“一个东西”,那我们就让这个“东西”变成一个可以容纳很多其他东西的容器,比如一个 Map 或者一个自定义的上下文对象(POJO)。

各个线程会各自操作 ThreadLocal 提供的变量副本,互不干扰,解决了多线程之间的共享数据竞争问题。

ThreadLocal 实现原理#

image.png

image.png

image.png

ThreadLocal 内存泄漏#

ThreadLocalMap 中的 key 是弱引用,值为强引用;

Key 会被 GC 释放内存,关联的 value 内存并不会释放。

建议主动 remove 释放 key,value

image.png

23. 线程池 shutdown 与 shutdownNow 的区别是什么?#

shutdown()shutdownNow() 都用于关闭线程池,但工作方式有所不同:

1)shutdown()

启动线程池的平滑关闭。它不再接受新的任务,但会继续执行已经提交的任务(包括在队列中的任务)

线程池会进入 SHUTDOWN 状态,所有已执行和正在执行的任务都会继续完成,只有所有任务完成后,线程池才会完全终止。

2)shutdownNow()

启动线程池的强制关闭。它会**尝试停止所有正在执行的任务,并返回等待执行的任务列表。它会尽力中断正在执行的任务,但不能保证所有任务都能被立即停止。**线程池进入 STOP 状态,除了尝试中断正在执行的任务外,还会清空任务队列,返回未执行的任务列表。

返回的任务队列列表,可以记录在日志或者提交给另一个线程池

shutdownNow会通过thread.interrupt()方法来尝试中断线程,如果被中断的线程没有对中断进行处理的话(如没有检测interrupted状态),就无法中断。


shutdown和shutdownNow多次调用不会有额外效果

使用场景:

  • shutdown():适用于程序需要平滑停止线程池的场景,如应用程序正常退出时。保证所有已提交的任务都能执行完毕,避免任务丢失。

  • shutdownNow():适用于紧急情况或需要立即停止线程池的场景,如程序异常退出时。快速清理资源,但可能导致部分任务未完成。

因为线程是否能够响应中断请求完全依赖于该线程内部是如何处理 Thread.interrupted() 方法的。如果一个任务没有检查中断标志或者在长时间的阻塞操作(例如 I/O 操作)中忽略了中断信号,那么即使调用了 shutdownNow(), 这个任务也可能不会被终止。

24. 说说CountDownLatch 和 CyclicBarrier 的区别?#

CountDownLatch是一个线程等其他多个线程完成任务,分->总关系,例如其他多个线程查询不同分片数据,主线程进行数据聚合

CyclicBarrier是多个线程同步,一组线程互相等待到某个状态,然后这组线程再同时执行,比如多个线程同时载入缓存,等都载入完成在之后,再多个线程同时进行数据运算。

image.png

image.png

25. 线程安全的集合有哪些?#

image.png

26. 你用过哪些并发安全类?#

比如:ConcurrentHashMap、AtomicInteger、Semaphore、CyclicBarrier、CountDownLatch、BlockingQueue 等等。

这个问题只要把你知道的一些并发类名字说出来就行了,然后等面试官选择其中一个去询问即可(一般需要结合简历中项目的业务场景,所以需要根据自己的业务提前准备)。

  1. 我使用了 CompletebleFuture,在OJ项目里用它异步去执行判题操作。

什么是 Java 的 CompletableFuture?

// 执行判题服务
Long questionSubmitId = questionSubmit.getId();
CompletableFuture.*runAsync*(() -> judgeService.doJudge(questionSubmitId));
return questionSubmitId;
plaintext
  1. 我使用了 ConcurrentHashMap 作为本地缓存Cache,用来存放热点视频信息,key 为 videoId,value 为对应的 VideoInfo,保存了视频的各种信息。在每次查询的时候,先判断该videoId是否在Cache里,在就直接返回视频信息;否则判断该 videoId 是否在当前的热门视频列表中(怎么判断见下),如果在就查数据库并将该视频信息加入到 Cache。

    热门视频列表每小时更新一次,通过一个定时任务(@EnableScheduling),来排序24小时内视频的播放量,作为score存到redis的zset,这个zset的前100名就是当前的热点视频id,该定时任务也会去检查Cache里的videoId是否是热点id,如果不是则remove。

  1. 我使用了 AtomicLong原子类来统计每天网站的访问量,每次调用login或autoLogin的时候就会去增加计数,每天0点清零。

  2. 我使用了CountDownLatch。在我的 VideoCommentController 的 loadComment 方法中,需要加载评论列表,同时还需要查询当前登录用户对当前视频下评论的点赞/点踩状态。这两个操作可以并行执行,执行完了就latch.countDown(),主线程等待latch计数器变0,再汇总成VO,以缩短总响应时间。

  3. 我使用了synchronize,up主在对评论进行置顶的时候,需要首先将 (1.已置顶的评论取消置顶,2.再将选中评论置顶),需要保证这个过程的原子性,不能出现两个评论同时置顶,所以在这段逻辑上加了synchronize。

    synchronized (videoId.intern()) {
        // 1. 先把该视频下已有的置顶评论取消置顶
        VideoComment updateComment = new VideoComment();
        updateComment.setTopType(CommentTopTypeEnum.NO_TOP.getType());
        VideoCommentQuery videoCommentQuery = new VideoCommentQuery();
        videoCommentQuery.setVideoId(videoId);
        videoCommentQuery.setTopType(CommentTopTypeEnum.TOP.getType());
        videoCommentMapper.updateByParam(updateComment, videoCommentQuery);
    
        // 2. 置顶本评论 (保留您原有的调用方式)
        videoCommentMapper.updateCountInfo(commentId, "top_type", 1, null);
    }
    plaintext

扩展知识#

1. ConcurrentHashMap#

  • 作用: 是一个线程安全且高效的哈希表,支持并发访问。

  • 用法: 多个线程可以同时进行读写操作,而不会导致线程安全问题。

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
Integer value = map.get("key1");
map.computeIfAbsent("key2", k -> 2);
plaintext

2. AtomicInteger#

  • 作用: 提供一种线程安全的方式对 int 类型进行原子操作,如增减、比较。

  • 用法: 适用于需要频繁对数值进行无锁操作的场景。

AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.incrementAndGet(); *// 递增*
atomicInt.decrementAndGet(); *// 递减*
atomicInt.compareAndSet(1, 2); *// 比较并设置*
plaintext

3. Semaphore#

定义几个信号灯,acquire到信号灯的线程才能运行,acquire到一次信号灯,信号灯数量减1,信号灯数量到0之后,后面的线程就acquire不到了。

适用场景:限流。作用:控制同时运行的线程的数量

支持公平与非公平锁,通过第二个参数fair设置。公平锁一个线程来了会先去队列里面看看有没有别的线程在,线程按请求许可的顺序获取许可。非公平,顺序不保证。

  • 作用: 控制访问资源的线程数,可以用来实现限流或访问控制。

  • 用法: 在资源有限的情况下,控制同时访问的线程数量。

Semaphore semaphore = new Semaphore(3);
try {
    semaphore.acquire(); *// 获取许可*
    *// 执行任务。。。*
} finally {
    semaphore.release(); *// 释放许可*
}
plaintext

4. CyclicBarrier#

  • 作用: 让一组线程到达一个共同的同步点,然后一起继续执行。常用于分阶段任务执行。

  • 用法: 适用于需要所有线程在某个点都完成后再继续的场景。

image.png

5. CountDownLatch#

  • 作用: 一个线程(或多个)等待其他线程完成操作。

  • 用法: 适用于主线程需要等待多个子线程完成任务的场景。

CountDownLatch latch = new CountDownLatch(3);
Runnable task = () -> {
    try {
        *// 执行任务*
    } finally {
        latch.countDown(); *// 任务完成,计数器减一*
    }
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
latch.await(); *// 等待所有任务完成*
System.out.println("所有任务都完成了");
plaintext

6. BlockingQueue#

  • 作用: 是一个线程安全的队列,支持阻塞操作,适用于生产者-消费者模式。

  • 用法: 生产者线程将元素放入队列,消费者线程从队列中取元素,队列为空时消费者线程阻塞。

关联题目#

什么是 Java 的 Semaphore?

什么是 Java 的 CyclicBarrier?

什么是 Java 的 CountDownLatch?

什么是 Java 的 StampedLock?

什么是 Java 的 CompletableFuture?

Java 线程安全的集合有哪些?

Java并发知识
https://natsumeaiovo.github.io/blog/260725-java%E5%B9%B6%E5%8F%91/java%E5%B9%B6%E5%8F%91
Author NAA
Published at July 25, 2026
Comment seems to stuck. Try to refresh?✨