侧边栏壁纸
博主头像
5faith分享栈

憧憬未来

  • 累计撰写 9 篇文章
  • 累计创建 13 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

【面试题】Java部分[2025/1/13 ~ 2025/1/19]

faith5
2025-01-21 / 0 评论 / 0 点赞 / 6 阅读 / 0 字

7. Java 中的 final 关键字是否能保证变量的可见性?

可见性定义: 一般而言指的是一个线程修改了共享变量,另一个线程可以立马得知更改,得到最新修改后的值。
final 并不能保证这种情况的发生,volatile 才可以。

final的可见性指的是:

  • final 修饰的字段在构造方法初始化完成,并且期间没有把 this 传递出去,那么当构造器执行完毕之后,其他线程就能看见 final 字段的值。
  • 如果不用 final 修饰的话,那么有可能在构造函数里面对字段的写操作被排序到外部,这样别的线程就拿不到写操作之后的值。

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

Java 内存模型(Java Memory Model, JMM) 是 Java 虚拟机 (JVM) 定义的一种规范,用于描述多线程程序中变量(包括实例字段、静态字段和数组元素)如何在内存中存储和传递的规则。规范了线程何时会从主内存中读取数据、何时会把数据写回主内存。

目的:

  1. 可见性:确保一个线程对变量的修改,能及时被其他线程看到。关键字 volatile 就是用来保证可见性的,它强制线程每次读写时都直接从主内存中获取最新值。
  2. 有序性:指线程执行操作的顺序。JMM 允许某些指令重排序以提高性能,但会保证线程内的操作顺序不会被破坏,并通过 happens-before 关系保证跨线程的有序性。
  3. 原子性:是指操作不可分割,线程不会在执行过程中被中断。例如,synchronized 关键字能确保方法或代码块的原子性。

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

CAS 是一种硬件级别的原子操作,它比较内存中的某个值是否为预期值,如果是,则更新为新值,否则不做修改。

  1. 工作原理: 期望值, 内存地址, 新值
    • 比较(Compare):CAS 会检查内存中的某个值是否与预期值相等。
    • 交换(Swap):如果相等,则将内存中的值更新为新值。
    • 失败重试:如果不相等,说明有其他线程已经修改了该值,CAS 操作失败,一般会利用重试,直到成功。
  2. 优点:
    • 无锁并发:CAS 操作不使用锁,因此不会导致线程阻塞,提高了系统的并发性和性能。
    • 原子性:CAS 操作是原子的,保证了线程安全。
  3. 缺点:
    • ABA 问题:CAS 操作中,如果一个变量值从 A 变成 B,又变回 A,CAS 无法检测到这种变化,可能导致错误。
    • 单变量限制:CAS 操作仅适用于单个变量的更新,不适用于涉及多个变量的复杂操作。
    • 自旋开销:CAS 操作通常通过自旋实现,可能导致 CPU 资源浪费,尤其在高并发情况下。

10. 什么是 Java 中的 ABA 问题?

ABA 问题是指在多线程环境下,某个变量的值在一段时间内经历了从 A 到 B 再到 A 的变化,这种变化可能被线程误认为值没有变化,从而导致错误的判断和操作。ABA 问题常发生在使用 CAS(Compare-And-Swap) 操作的无锁并发编程中。

解决方法:

  • AtomicStampedReference: 增加int的版本号
  • AtomicMarkableReference: 增加boolean的版本号

11. 为什么 Java 中的 ThreadLocal 对 key 的引用为弱引用?

使用弱引用作为 ThreadLocal 的键可以防止内存泄漏。

  • 若 ThreadLocal 实例被不再需要的线程持有为强引用,那么当该线程结束时,相关的 ThreadLocal 实例及其对应的数据可能无法被回收,导致内存持续占用。
  • 若弱引用允许垃圾回收器在内存不足时回收对象。这样,当没有其他强引用指向某个 ThreadLocal 实例时,它可以被及时回收,避免长时间占用内存。

12. 你了解 Java 线程池的原理吗?

线程池是一种池化技术,用于预先创建并管理一组线程,避免频繁创建和销毁线程的开销,提高性能和响应速度。

  1. ThreadPoolExecutor关键参数
  • 核心线程数coreSize
  • 最大线程数maximumPollSize
  • 空闲存活时间keepAliveTime
  • 时间单位TimeUnit
  • 工作队列workQueue
    • SynchronousQueue:不存储任务,直接将任务提交给线程。
    • LinkedBlockingQueue:链表结构的阻塞队列,大小无限。
    • ArrayBlockingQueue:数组结构的有界阻塞队列。
    • PriorityBlockingQueue:带优先级的无界阻塞队列。
  • 线程工厂ThreadFactory
  • 拒绝策略RejectFactoryHandler
    • AbortPolicy,当任务队列满且没有线程空闲,此时添加任务会直接抛出 RejectedExecutionException 错误,这也是默认的拒绝策略。适用于必须通知调用者任务未能被执行的场景。
    • CallerRunsPolicy,当任务队列满且没有线程空闲,此时添加任务由即调用者线程执行。适用于希望通过减缓任务提交速度来稳定系统的场景。
    • DiscardOldestPolicy,当任务队列满且没有线程空闲,会删除最早的任务,然后重新提交当前任务。适用于希望丢弃最旧的任务以保证新的重要任务能够被处理的场景。
    • DiscardPolicy,直接丢弃当前提交的任务,不会执行任何操作,也不会抛出异常。适用于对部分任务丢弃没有影响的场景,或系统负载较高时不需要处理所有任务。
  1. 工作原理
    • 默认情况下线程不会预创建,任务提交之后才会创建核心线程(不过设置 prestartAllCoreThreads 可以预创建核心线程)。
    • 当核心线程满了之后不会新建线程,而是把任务堆积到工作队列中。
    • 如果工作队列放不下了,然后才会新增线程,直至达到最大线程数。
    • 如果工作队列满了,然后也已经达到最大线程数了,这时候来任务会执行拒绝策略
    • 当线程空闲时间超过空闲存活时间,并且线程线程数是大于核心线程数的则会销毁线程,直到线程数等于核心线程数(设置 allowCoreThreadTimeOut 为 true 可以回收核心线程,默认为 false)。

13. 你使用过哪些 Java 并发工具类?

  1. ConcurrentHashMap
    • 是一个线程安全且高效的哈希表,支持并发访问。
    • 多个线程可以同时进行读写操作,而不会导致线程安全问题。
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
Integer value = map.get("key1");
map.computeIfAbsent("key2", k -> 2);
  1. AtomicInteger
    • 提供一种线程安全的方式对 int 类型进行原子操作,如增减、比较。
    • 适用于需要频繁对数值进行无锁操作的场景。
AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.incrementAndGet(); // 递增
atomicInt.decrementAndGet(); // 递减
atomicInt.compareAndSet(1, 2); // 比较并设置
  1. BlockingQueue
    • 提供一种线程安全的方式对 int 类型进行原子操作,如增减、比较。
    • 适用于需要频繁对数值进行无锁操作的场景。
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
Runnable producer = () -> {
    try {
        queue.put("item"); // 放入元素
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};
Runnable consumer = () -> {
    try {
        String item = queue.take(); // 取出元素
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};
new Thread(producer).start();
new Thread(consumer).start();

  1. CompletableFuture
    • 一个线程可以异步执行任务,并在任务完成后通知其他线程。
    • 适用于需要异步处理任务并等待结果的场景,可以组合多个异步任务并处理它们的最终结果。
// 创建一个 CompletableFuture 任务
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            // 模拟异步任务
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Hello, CompletableFuture!";
        });

        // 主线程可以继续执行其他任务
        System.out.println("主线程继续执行...");

        // 等待异步任务完成并获取结果
        String result = future.get(); // 阻塞等待任务完成
        System.out.println("异步任务结果: " + result);
  1. Semaphore
    • 控制访问资源的线程数,可以用来实现限流或访问控制。
    • 在资源有限的情况下,控制同时访问的线程数量。
Semaphore semaphore = new Semaphore(3);
try {
    semaphore.acquire(); // 获取许可
    // 执行任务
} finally {
    semaphore.release(); // 释放许可
}
  1. CyclicBarrier
    • 让一组线程到达一个共同的同步点,然后一起继续执行。常用于分阶段任务执行。
    • 适用于需要所有线程在某个点都完成后再继续的场景。
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有线程都到达了屏障点");
});
Runnable task = () -> {
    try {
        // 执行任务
        barrier.await(); // 等待其他线程
    } catch (Exception e) {
        e.printStackTrace();
    }
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();

  1. 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("所有任务都完成了");

14. Synchronized 和 ReentrantLock 有什么区别?

  • Synchronized 是 Java 内置的关键字,实现基本的同步机制,非公平,不可中断,不支持多条件,不支持超时。是可重入锁, 不需要手动加锁解锁
  • ReentrantLock 是 JUC 类库提供的,由 JDK 1.5 引入,支持公平锁,可中断,支持多条件判断,并且支持设置超时时间,可以避免死锁,比较灵活。同样是可重入锁, 需要手动加锁解锁

15. 单例模式有哪几种实现?如何保证线程安全?

  1. 饿汉式:实例在类加载时就创建,线程安全,但如果实例初始化较重或没有被使用会浪费资源。
public class Singleton {
    private static final Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

  1. 懒汉式:实例在首次访问时创建,节约资源,但需要确保线程安全。
public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

  1. 双重检查锁定:在懒汉式的基础上优化,直接加锁效率太低,双重检查锁只在第一次检查实例为空时加锁,提高性能。
public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

  1. 静态内部类:利用类加载机制实现懒加载和线程安全,推荐使用。
public class StaticSingleton {
    private StaticSingleton() {}

    private static class Inner {
        private static final StaticSingleton INSTANCE = new StaticSingleton();
    }

    public static StaticSingleton getInstance() {
        return Inner.INSTANCE;
    }
}

  1. 枚举单例(Java 特有):通过枚举实现单例,简单且防止反射和序列化攻击。
public enum EnumSingleton {
    INSTANCE;

    public void test() {
        // 一些业务逻辑方法
    }
}

//使用
EnumSingleton singleton = EnumSingleton.INSTANCE;
singleton.test();

16. Java 的 synchronized 是怎么实现的?

synchronized 实现原理依赖于 JVM 的 Monitor(监视器锁) 和 对象头(Object Header)。

  1. 当 synchronized 修饰在方法或代码块上时,会对特定的对象或类加锁,从而确保同一时刻只有一个线程能执行加锁的代码块。

    • synchronized 修饰方法:当 synchronized 修饰方法时,JVM 会在方法的字节码中添加一个 ACC_SYNCHRONIZED 标志。每次线程调用该方法时,JVM 会检查该标志。
      • 如果方法被标记为 synchronized,线程必须先获取该方法所属对象的监视器锁(monitor)。
      • 如果是实例方法,锁对象是 this;如果是静态方法,锁对象是类的 Class 对象。
      • 获取锁后,线程可以执行方法体,方法执行完成后释放锁。
    • synchronized 修饰代码块:当 synchronized 修饰代码块时,JVM 在字节码层面插入 monitorenter 和 monitorexit 指令:。
      • monitorenter:在进入代码块时,线程尝试获取指定对象的监视器锁。
        • 如果锁未被占用,线程获取锁并继续执行。
        • 如果锁已被其他线程占用,当前线程进入阻塞状态,等待锁释放。
      • monitorexit:在退出代码块时,线程释放监视器锁。
      • 如果代码块中发生异常,JVM 会确保在异常传播到外部之前,monitorexit 指令被正确执行,从而释放锁。
  2. 锁升级总结:

    • 偏向锁:当一个线程第一次获取锁时,JVM 会将该线程标记为“偏向”状态,后续若该线程再获取该锁,几乎没有开销。
    • 轻量级锁:当另一个线程尝试获取已经被偏向的锁时,锁会升级为轻量级锁,使用 CAS 操作来减少锁竞争的开销。
    • 重量级锁:当 CAS 失败无法获取锁,锁会升级为重量级锁,线程会被挂起,直到锁被释放。
  3. 锁消除和锁粗化

    • 锁消除:JVM 会通过逃逸分析判断对象是否只在当前线程使用,如果是,那么会消除不必要的加锁操作。
    • 锁粗化:当多个锁操作频繁出现时,JVM 会将这些锁操作合并,减少锁获取和释放的开销。

17. 如何优化 Java 中的锁的使用?

  1. 减小锁的粒度(使用的时间):
    • 尽量缩小加锁的范围,减少锁的持有时间。即在必要的最小代码块内使用锁,避免对整个方法或过多代码块加锁。
    • 使用更细粒度的锁,比如将一个大对象锁拆分为多个小对象锁,以提高并行度(参考 HashTable 和ConcurrentHashMap 的区别)。
    • 对于读多写少的场景,可以使用读写锁(ReentrantReadWriteLock)代替独占锁。
  2. 减少锁的使用:
    • 通过无锁编程CAS(Compare-And-Swap)操作和原子类(如 AtomicInteger、AtomicReference)来避免使用锁,从而减少锁带来的性能损耗。
    • 通过减少共享资源的使用,避免线程对同一个资源的竞争。例如,使用局部变量线程本地变量(ThreadLocal)来减少多个线程对同一资源的访问。

18. Java 中 volatile 关键字的作用是什么?

volatile 它的主要作用是保证变量的可见性和禁止指令重排优化。

  1. 可见性(Visibility):
    volatile 关键字确保变量的可见性。当一个线程修改了 volatile 变量的值,新值会立即被刷新到主内存中,其他线程在读取该变量时可以立即获得最新的值。这样可以避免线程间由于缓存一致性问题导致的“看见”旧值的现象。
  2. 禁止指令重排序(Ordering):
    volatile 还通过内存屏障来禁止特定情况下的指令重排序,从而保证程序的执行顺序符合预期。对 volatile 变量的写操作会在其前面插入一个 StoreStore 屏障,而对 volatile 变量的读操作则会在其后插入一个 LoadLoad 屏障。这确保了在多线程环境下,某些代码块执行顺序的可预测性。

19. Java线程的生命周期在 Java 中是如何定义的?

  1. Java线程的生命周期由其状态决定,这些状态定义了线程在其生命周期中的不同阶段。线程的状态主要由 java.lang.Thread.State 枚举定义,包括以下几种:

    • NEW(新建状态)
      当线程被创建但尚未启动时,线程处于 NEW 状态。例如,通过 new Thread() 创建线程后,线程处于此状态。
    • RUNNABLE(可运行状态)
      当线程调用 start() 方法后,线程进入 RUNNABLE 状态。在这个状态下,线程可能正在运行,也可能正在等待 CPU 资源。
    • BLOCKED(阻塞状态)
      当线程试图获取一个被其他线程持有的锁时,线程进入 BLOCKED 状态。例如,在同步代码块中等待锁释放。
    • WAITING(等待状态)
      当线程调用 Object.wait()、Thread.join() 或 LockSupport.park() 等方法时,线程进入 WAITING 状态。这些方法需要被其他线程唤醒。
    • TIMED_WAITING(计时等待状态)
      当线程调用 Object.wait(long)、Thread.sleep(long) 或 LockSupport.parkNanos() 等方法时,线程进入 TIMED_WAITING 状态。这种状态的线程会在指定的时间后自动唤醒。
    • TERMINATED(终止状态)
      当线程的执行完成或因为异常退出时,线程进入 TERMINATED 状态。一旦线程进入此状态,它将无法再被启动。
  2. 线程状态之间的转换关系如下:

    • NEW -> RUNNABLE:调用 start() 方法。
    • RUNNABLE -> BLOCKED:试图获取锁。
    • RUNNABLE -> WAITING:调用 wait() 或 join()。
    • RUNNABLE -> TIMED_WAITING:调用 sleep() 或 wait(long)。
    • RUNNABLE -> TERMINATED:执行完成或异常退出。
    • BLOCKED -> RUNNABLE:获取锁。
    • WAITING -> RUNNABLE:被唤醒。
    • TIMED_WAITING -> RUNNABLE:时间到期或被唤醒。

    ==注意事项==
    线程一旦进入 TERMINATED 状态,就无法再被启动,否则会抛出 IllegalThreadStateException。

0

评论区