并发安全

juc包下你常用的类?(xxxx)

线程池相关:

ThreadPoolExecutor:最核心的线程池类,用于创建和管理线程池。通过它可以灵活地配置线程池的参数,如核心线程数、最大线程数、任务队列等,以满足不同的并发处理需求。
Executors:线程池工厂类,提供了一系列静态方法来创建不同类型的线程池,如newFixedThreadPool(创建固定线程数的线程池)、newCachedThreadPool(创建可缓存线程池)、newSingleThreadExecutor(创建单线程线程池)等,方便开发者快速创建线程池。
并发集合类:

ConcurrentHashMap:线程安全的哈希映射表,用于在多线程环境下高效地存储和访问键值对。它采用了分段锁等技术,允许多个线程同时访问不同的段,提高了并发性能,在高并发场景下比传统的Hashtable性能更好。
CopyOnWriteArrayList:线程安全的列表,在对列表进行修改操作时,会创建一个新的底层数组,将修改操作应用到新数组上,而读操作仍然可以在旧数组上进行,从而实现了读写分离,提高了并发读的性能,适用于读多写少的场景。
同步工具类:

CountDownLatch:允许一个或多个线程等待其他一组线程完成操作后再继续执行。它通过一个计数器来实现,计数器初始化为线程的数量,每个线程完成任务后调用countDown方法将计数器减一,当计数器为零时,等待的线程可以继续执行。常用于多个线程完成各自任务后,再进行汇总或下一步操作的场景。
CyclicBarrier:让一组线程互相等待,直到所有线程都到达某个屏障点后,再一起继续执行。与CountDownLatch不同的是,CyclicBarrier可以重复使用,当所有线程都通过屏障后,计数器会重置,可以再次用于下一轮的等待。适用于多个线程需要协同工作,在某个阶段完成后再一起进入下一个阶段的场景。
Semaphore:信号量,用于控制同时访问某个资源的线程数量。它维护了一个许可计数器,线程在访问资源前需要获取许可,如果有可用许可,则获取成功并将许可计数器减一,否则线程需要等待,直到有其他线程释放许可。常用于控制对有限资源的访问,如数据库连接池、线程池中的线程数量等。
原子类:

AtomicInteger:原子整数类,提供了对整数类型的原子操作,如自增、自减、比较并交换等。通过硬件级别的原子指令来保证操作的原子性和线程安全性,避免了使用锁带来的性能开销,在多线程环境下对整数进行计数、状态标记等操作非常方便。
AtomicReference:原子引用类,用于对对象引用进行原子操作。可以保证在多线程环境下,对对象的更新操作是原子性的,即要么全部成功,要么全部失败,不会出现数据不一致的情况。常用于实现无锁数据结构或需要对对象进行原子更新的场景。

怎么保证多线程安全?(1、线程之间进行通信,2、用并发集合)

Java中有哪些常用的锁,在什么场景下使用?(Java中常用锁包括synchronized内置锁(简单同步)、ReentrantLock(精细控制)、读写锁(读多写少)、悲观锁(高并发写入)、乐观锁(低冲突无锁)、自旋锁CAS(Compare-And-Swap)(短竞争自旋))

锁类型

实现方式/类

机制特点

适用场景

内置锁 (synchronized)

synchronized 关键字

自动获取/释放锁;支持偏向锁(单线程无竞争)、轻量级锁(低竞争)、重量级锁(高竞争)。

简单的线程同步场景,如单方法/代码块互斥访问。

ReentrantLock

java.util.concurrent.locks.ReentrantLock

显式锁,支持公平锁(按请求顺序分配)、非公平锁(竞争优先)、可中断锁、超时锁。

需要精细控制锁获取的场景(如避免死锁、公平性要求、带超时的锁请求)。

读写锁 (ReadWriteLock)

ReentrantReadWriteLock

读锁共享(允许多线程读)、写锁独占(仅单线程写),提升读多写少的并发性能。

读操作远多于写操作的场景(如缓存、资源池)。

悲观锁

synchronizedReentrantLock

访问资源前直接加锁,假设数据会被其他线程修改。

高并发写入场景,或需要强一致性的共享资源访问。

乐观锁

版本号/时间戳(如Atomic类、CAS操作)

不加锁,更新时检查数据是否被修改(如compareAndSet)。

低冲突场景(如计数器、状态标记),避免锁开销。

自旋锁

基于CAS(Compare-And-Swap)实现(如AtomicBoolean

线程循环检查锁状态(自旋),而非阻塞等待,减少线程切换开销。

锁竞争时间极短的场景(如轻量级任务队列),避免线程阻塞的开销。

synchronized和reentrantlock(底层实现主要依赖于 AbstractQueuedSynchronizer(AQS))及其应用场景?( synchronized是Java内置的自动锁,适合简单同步场景;ReentrantLock提供可中断、超时、公平锁及多条件变量等高级功能,适用于复杂并发控制和性能优化需求)

特性

synchronized

ReentrantLock

锁类型

隐式锁(自动管理)

显式锁(需手动lock()unlock()

锁释放

自动释放(代码块/方法结束)

需在finally块中手动释放

可中断性

❌ 不支持

✅ 支持(lockInterruptibly()

超时机制

❌ 不支持

✅ 支持(tryLock(10, TimeUnit.SECONDS)

公平锁

❌ 仅非公平锁

✅ 支持(构造函数传true

条件变量

单一条件(wait()/notify()

多条件(Condition对象,如notEmpty/notFull

性能优化

JVM自动优化锁升级

非公平锁减少上下文切换,高竞争下性能更优

代码复杂度

简单(无需手动管理)

复杂(需处理锁释放和异常)

synchronized锁静态方法和普通方法区别?(synchronized普通方法锁定当前实例(this),不同实例的同步方法互不影响;静态方法锁定类的Class对象,所有实例共享同一把锁,全局互斥)

怎么理解可重入锁?(可重入锁允许同一线程多次获取同一把锁,eg:ReentrantLock通过计数器机制跟踪锁的持有次数,避免自我死锁,确保同步代码的递归或嵌套调用安全)

synchronized 支持重入吗?如何实现的?(synchronized 支持可重入性,通过 线程持有标记(eg:如果锁状态是0,代表该锁没有被占用) 和锁计数器 跟踪重入次数)

synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。

synchronized底层是利用计算机系统mutex Lock实现的。每一个可重入锁都会关联一个线程ID和一个锁状态status。

  • 当一个线程请求方法时,会去检查锁状态。
    如果锁状态是0,代表该锁没有被占用,使用CAS操作获取锁,将线程ID替换成自己的线程ID。
    如果锁状态不是0,代表有线程在访问该方法。此时,如果线程ID是自己的线程ID,如果是可重入锁,会将status自增1,然后获取到该锁,进而执行相应的方法;如果是非重入锁,就会进入阻塞队列等待。
  • 在释放锁时
    如果是可重入锁的,每一次退出方法,就会将status减1,直至status的值为0,最后释放该锁。
    如果非可重入锁的,线程退出方法,直接就会释放该锁。

syncronized锁升级的过程讲一下(无锁→偏向锁(线程ID匹配)→轻量级锁(CAS自旋,进行while循环操作,这是非常消耗CPU的)→重量级锁(后续线程阻塞,节约CPU资源))(整个过程由JVM自动触发)

JAVA面试题---并发安全(Java并发编程)_线程池

简要总结

  1. 偏向锁检查:线程A抢锁时,JVM先判断是否为偏向锁状态:
  • 若对象头(Mark Word)中的线程ID与线程A匹配,直接执行代码
  • 若线程ID不匹配,触发自旋尝试获取锁
  1. 锁升级
  • 自旋成功:更新Mark Word为线程A的ID,保持偏向锁;
  • 自旋失败:撤销偏向锁,膨胀为轻量级锁(CAS自旋竞争)。
  1. 轻量级→重量级
  • 若轻量级锁自旋失败(高竞争),升级为重量级锁,后续线程进入阻塞队列,由操作系统调度,避免CPU空转。

JVM对Synchornized的优化?( 1、锁膨胀(动态升级锁粒度)、2、锁消除(去除无竞争场景的同步)、3、锁粗化(合并相邻锁操作)和4、自适应自旋锁(智能控制自旋次数))(在保障线程安全的前提下,最大限度减少用户态/内核态切换及同步开销,显著提升并发性能)

synchronized 核心优化方案主要包含以下 4 个:

  • 锁膨胀:synchronized 从无锁升级到偏向锁,再到轻量级锁,最后到重量级锁的过程,它叫做锁膨胀也叫做锁升级。JDK 1.6 之前,synchronized 是重量级锁,也就是说 synchronized 在释放和获取锁时都会从用户态转换成内核态,而转换的效率是比较低的。但有了锁膨胀机制之后,synchronized 的状态就多了无锁、偏向锁以及轻量级锁了,这时候在进行并发操作时,大部分的场景都不需要用户态到内核态的转换了,这样就大幅的提升了 synchronized 的性能。
  • 锁消除:指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。
  • 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
  • 自适应自旋锁:指通过自身循环,尝试获取锁的一种方式,优点在于它避免一些线程的挂起和恢复操作,因为挂起线程和恢复线程都需要从用户态转入内核态,这个过程是比较慢的,所以通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。

CAS 和 AQS 有什么关系?(CAS 是原子操作的基础能力,AQS 是基于 CAS 和队列机制的高层同步框架)(CAS 提供无锁化的状态更新能力,而 AQS 利用 CAS 实现状态管理,并结合队列机制处理线程竞争,两者共同支撑了 Java 高效、灵活的并发控制模型。)

  • CAS 和 AQS 两者的区别:
  • CAS 是一种乐观锁机制,它包含三个操作数:内存位置(V)、预期值(A)和新值(B)。CAS 操作的逻辑是,如果内存位置 V 的值等于预期值 A,则将其更新为新值 B,否则不做任何操作。整个过程是原子性的,通常由硬件指令支持,如在现代处理器上,cmpxchg 指令可以实现 CAS 操作。
  • AQS 是一个用于构建锁和同步器的框架,许多同步器如 ReentrantLock、Semaphore、CountDownLatch 等都是基于 AQS 构建的。AQS 使用一个 volatile 的整数变量 state 来表示同步状态,通过内置的 FIFO 队列来管理等待线程。它提供了一些基本的操作,如 acquire(获取资源)和 release(释放资源),这些操作会修改 state 的值,并根据 state 的值来判断线程是否可以获取或释放资源。AQS 的 acquire 操作通常会先尝试获取资源,如果失败,线程将被添加到等待队列中,并阻塞等待。release 操作会释放资源,并唤醒等待队列中的线程。
  • CAS 和 AQS 两者的联系:
  • CAS 为 AQS 提供原子操作支持:AQS 内部使用 CAS 操作来更新 state 变量,以实现线程安全的状态修改。在 acquire 操作中,当线程尝试获取资源时,会使用 CAS 操作尝试将 state 从一个值更新为另一个值,如果更新失败,说明资源已被占用,线程会进入等待队列。在 release 操作中,当线程释放资源时,也会使用 CAS 操作将 state 恢复到相应的值,以保证状态更新的原子性。

如何用 AQS 实现一个可重入的公平锁?(通过继承 AQS 重写 tryAcquire/tryRelease,在 tryAcquire 中利用 hasQueuedPredecessors 实现公平性,并通过 state 变量跟踪锁重入次数,最终封装为可重入的公平锁)

AQS 实现一个可重入的公平锁的详细步骤:

  • 继承 AbstractQueuedSynchronizer:创建一个内部类继承自 AbstractQueuedSynchronizer,重写 tryAcquire、tryRelease、isHeldExclusively 等方法,这些方法将用于实现锁的获取、释放和判断锁是否被当前线程持有。
  • 实现可重入逻辑:在 tryAcquire 方法中,检查当前线程是否已经持有锁,如果是,则增加锁的持有次数(通过 state 变量);如果不是,尝试使用 CAS操作来获取锁。
  • 实现公平性:在 tryAcquire 方法中,按照队列顺序来获取锁,即先检查等待队列中是否有线程在等待,如果有,当前线程必须进入队列等待,而不是直接竞争锁。
  • 创建锁的外部类:创建一个外部类,内部持有 AbstractQueuedSynchronizer 的子类对象,并提供 lock 和 unlock 方法,这些方法将调用 AbstractQueuedSynchronizer 子类中的方法。
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

public class FairReentrantLock {

    private static class Sync extends AbstractQueuedSynchronizer {

        // 判断锁是否被当前线程持有
        protected boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        // 尝试获取锁
        protected boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // 公平性检查:检查队列中是否有前驱节点,如果有,则当前线程不能获取锁
                if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            } else if (current == getExclusiveOwnerThread()) {
                // 可重入逻辑:如果是当前线程持有锁,则增加持有次数
                int nextc = c + acquires;
                if (nextc < 0) {
                    throw new Error("Maximum lock count exceeded");
                }
                setState(nextc);
                return true;
            }
            return false;
        }

        // 尝试释放锁
        protected boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread()!= getExclusiveOwnerThread()) {
                throw new IllegalMonitorStateException();
            }
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

        // 提供一个条件变量,用于实现更复杂的同步需求,这里只是简单实现
        ConditionObject newCondition() {
            return new ConditionObject();
        }
    }

    private final Sync sync = new Sync();

    // 加锁方法
    public void lock() {
        sync.acquire(1);
    }

    // 解锁方法
    public void unlock() {
        sync.release(1);
    }

    // 判断当前线程是否持有锁
    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    // 提供一个条件变量,用于实现更复杂的同步需求,这里只是简单实现
    public Condition newCondition() {
        return sync.newCondition();
    }
}

Threadlocal作用,原理,具体里面存的key value是啥,会有什么问题,如何解决?(ThreadLocal 通过每个线程独有的 ThreadLocalMap(以弱引用 ThreadLocal 为键存储强引用值)实现线程隔离,但需及时调用 remove() 清理因键弱引用被回收导致的残留值,避免内存泄漏)

作用

  1. 线程隔离:每个线程持有变量的独立副本,避免多线程竞争,确保线程安全。
  2. 减少同步开销:无锁化操作,提升并发性能。
  3. 跨方法共享数据:在同一线程的不同方法间隐式传递数据(如用户会话信息)。

原理

  • 存储结构:每个线程(Thread类)内部维护一个 ThreadLocalMap,其 Entry 数组以 ThreadLocal 实例为键,存储线程专属的值。
  • 键值对结构
  • KeyThreadLocal 对象(弱引用)。
  • Value:对应线程的变量值(强引用)。
  • 核心操作
  • set(T value):将当前线程的 ThreadLocal 关联值存入 ThreadLocalMap
  • get():从当前线程的 ThreadLocalMap 中根据键(ThreadLocal 对象)获取值。
  • remove():清除当前线程中该 ThreadLocal 的键值对。

内存结构示例

// 线程类内部结构
class Thread {
    ThreadLocal.ThreadLocalMap threadLocals; // 存储线程的ThreadLocal变量
}

// ThreadLocalMap结构
class ThreadLocalMap {
    Entry[] table; // Entry数组
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value; // 实际存储的值(强引用)
    }
}

潜在问题

  1. 内存泄漏
  • 原因Entry 的键是弱引用的 ThreadLocal,若 ThreadLocal 被回收,键变为 null,但值仍是强引用。若线程长期存活(如线程池),这些无主值会累积导致内存泄漏。
  • 示例
ThreadLocal<byte[]> local = new ThreadLocal<>();
local.set(new byte[1024 * 1024]); // 占用1MB内存
local = null; // ThreadLocal对象被回收,但Entry的值仍存在

应用场景

  1. 用户会话管理
public class UserContext {
    private static final ThreadLocal<User> holder = new ThreadLocal<>();
    public static void set(User user) { holder.set(user); }
    public static User get() { return holder.get(); }
    public static void clear() { holder.remove(); }
}
// 在拦截器中设置和清除
public void handleRequest() {
    try {
        UserContext.set(currentUser);
        processRequest();
    } finally {
        UserContext.clear();
    }
}
  1. 数据库连接管理
public class ConnectionManager {
    private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
    public static Connection getConnection() {
        Connection conn = connectionHolder.get();
        if (conn == null) {
            conn = createConnection();
            connectionHolder.set(conn);
        }
        return conn;
    }
}

Java中想实现一个乐观锁,都有哪些方式?(Java 中实现乐观锁可通过 CAS 原子操作(如 Atomic 类)(乐观锁的基础)或 数据版本控制(版本号/时间戳)实现,前者依赖硬件指令保证原子性,后者通过比对数据版本来避免并发冲突)

CAS 有什么缺点?(1、自旋时间长导致 CPU 开销高,2、 仅支持单变量原子性(多个可以通过AtomicReference来处理或者使用锁synchronized实现))

voliatle关键字有什么作用?(1、通过 内存屏障 确保变量的 可见性(直接读写主存),2、 禁止指令重排序,避免多线程环境下的数据不一致)

volatite作用有 2 个:

1、保证变量对所有线程的可见性。当一个变量被声明为volatile时,它会保证对这个变量的写操作会立即刷新到主存中,而对这个变量的读操作会直接从主存中读取,从而确保了多线程环境下对该变量访问的可见性。这意味着一个线程修改了volatile变量的值,其他线程能够立刻看到这个修改,不会受到各自线程工作内存的影响。

2、禁止指令重排序优化。volatile关键字在Java中主要通过内存屏障来禁止特定类型的指令重排序。

  • 1)写-写(Write-Write)屏障:在对volatile变量执行写操作之前,会插入一个写屏障。这确保了在该变量写操作之前的所有普通写操作都已完成,防止了这些写操作被移到volatile写操作之后。
  • 2)读-写(Read-Write)屏障:在对volatile变量执行读操作之后,会插入一个读屏障。它确保了对volatile变量的读操作之后的所有普通读操作都不会被提前到volatile读之前执行,保证了读取到的数据是最新的。
  • 3)写-读(Write-Read)屏障:这是最重要的一个屏障,它发生在volatile写之后和volatile读之前。这个屏障确保了volatile写操作之前的所有内存操作(包括写操作)都不会被重排序到volatile读之后,同时也确保了volatile读操作之后的所有内存操作(包括读操作)都不会被重排序到volatile写之前。

指令重排序的原理是什么?(指令重排序的原理是处理器和编译器在 不改变单线程执行结果不破坏数据依赖关系的前提下优化指令顺序,以提高性能,但可能导致多线程并发问题)

JAVA面试题---并发安全(Java并发编程)_线程池_02

volatile可以保证线程安全吗?(volatile 仅保证 可见性 和 有序性,但无法保证操作的 原子性,因此 不能完全保证线程安全(如 i++ 等复合操作仍需同步机制))

什么是公平锁(eg:ReentrantLock)和非公平锁(eg:Synchronized)?(公平锁严格按申请顺序分配锁,保证公平但吞吐量低;非公平锁允许插队抢锁,吞吐量高但可能引发线程饥饿问题)

什么情况会产生死锁问题?如何解决?(死锁需同时满足1、互斥,2、持有等待,3、不可剥夺,4、环路等待四个条件,通过资源有序分配法统一线程获取资源的顺序可破坏环路等待,从而避免死锁)

死锁只有同时满足以下四个条件才会发生:

1、互斥条件:互斥条件是指多个线程不能同时使用同一个资源。
2、持有并等待条件:持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。
3、不可剥夺条件:不可剥夺条件是指,当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
4、环路等待条件:环路等待条件指的是,在死锁发生的时候,两个线程获取资源的顺序构成了环形链。
例如,线程 A 持有资源 R1 并试图获取资源 R2,而线程 B 持有资源 R2 并试图获取资源 R1,此时两个线程相互等待对方释放资源,从而导致死锁。

避免死锁问题就只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件。

JAVA面试题---并发安全(Java并发编程)_公平锁_03